dsh-knowledge-library 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +221 -2
- package/cordis.patch.yml +3 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +721 -0
- package/dist/core/extract.d.ts +9 -0
- package/dist/core/extract.js +105 -0
- package/dist/core/fetch.d.ts +10 -0
- package/dist/core/fetch.js +105 -0
- package/dist/core/jobs.d.ts +30 -0
- package/dist/core/jobs.js +96 -0
- package/dist/core/library.d.ts +61 -0
- package/dist/core/library.js +386 -0
- package/dist/http.d.ts +6 -0
- package/dist/http.js +157 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +35 -0
- package/dist/public/app.css +182 -0
- package/dist/public/app.js +56 -0
- package/dist/public/index.html +1 -0
- package/dist/standalone.d.ts +1 -0
- package/dist/standalone.js +38 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +62 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.js +222 -0
- package/dist/ui/icons.d.ts +8 -0
- package/dist/ui/icons.js +25 -0
- package/dist/ui/main.d.ts +1 -0
- package/dist/ui/main.js +4 -0
- package/package.json +94 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Segment } from '../types.js';
|
|
2
|
+
export declare function extractArticle(html: string, url: string): {
|
|
3
|
+
title: string;
|
|
4
|
+
author: string | undefined;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseSubtitles(input: string): Segment[];
|
|
8
|
+
export declare function formatTime(value: number): string;
|
|
9
|
+
export declare function splitSegments(content: string): Segment[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseHTML } from 'linkedom';
|
|
2
|
+
import { Readability } from '@mozilla/readability';
|
|
3
|
+
import TurndownService from 'turndown';
|
|
4
|
+
const markdown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
5
|
+
markdown.remove(['script', 'style', 'iframe', 'form', 'input', 'button']);
|
|
6
|
+
function parseDocument(html, url) {
|
|
7
|
+
let { document } = parseHTML(html);
|
|
8
|
+
if (document.documentElement?.localName !== 'html') {
|
|
9
|
+
({ document } = parseHTML(`<!doctype html><html><head></head><body>${html}</body></html>`));
|
|
10
|
+
}
|
|
11
|
+
let baseURI = url;
|
|
12
|
+
try {
|
|
13
|
+
baseURI = new URL(document.querySelector('base[href]')?.getAttribute('href') || url, url).href;
|
|
14
|
+
}
|
|
15
|
+
catch { /* Keep the source URL for invalid base tags. */ }
|
|
16
|
+
Object.defineProperties(document, { documentURI: { value: url }, baseURI: { value: baseURI } });
|
|
17
|
+
return document;
|
|
18
|
+
}
|
|
19
|
+
export function extractArticle(html, url) {
|
|
20
|
+
// Linkedom parses inert DOM only: no script execution, windows, or resource loading.
|
|
21
|
+
// It also avoids the punycode/ dependency that DSH 0.2.1-rc.2 misroutes in packed profiles.
|
|
22
|
+
const document = parseDocument(html, url);
|
|
23
|
+
for (const node of Array.from(document.querySelectorAll('script,style,iframe,form')))
|
|
24
|
+
node.remove();
|
|
25
|
+
const article = new Readability(document).parse();
|
|
26
|
+
if (!article?.content || (article.textContent?.trim().length ?? 0) < 40) {
|
|
27
|
+
throw new Error('未提取到有效正文。该网页可能需要登录或动态渲染,请复制正文或导入保存的 HTML。');
|
|
28
|
+
}
|
|
29
|
+
const clean = parseDocument(`<!doctype html><html><head></head><body>${article.content}</body></html>`, url);
|
|
30
|
+
for (const anchor of Array.from(clean.querySelectorAll('a'))) {
|
|
31
|
+
try {
|
|
32
|
+
const href = new URL(anchor.getAttribute('href') ?? '', url);
|
|
33
|
+
if (['http:', 'https:'].includes(href.protocol))
|
|
34
|
+
anchor.setAttribute('href', href.href);
|
|
35
|
+
else
|
|
36
|
+
anchor.removeAttribute('href');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
anchor.removeAttribute('href');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Preserve image references; do not silently fetch third-party resources.
|
|
43
|
+
for (const image of Array.from(clean.querySelectorAll('img'))) {
|
|
44
|
+
try {
|
|
45
|
+
const src = new URL(image.getAttribute('src') ?? '', url);
|
|
46
|
+
if (['http:', 'https:'].includes(src.protocol))
|
|
47
|
+
image.setAttribute('src', src.href);
|
|
48
|
+
else
|
|
49
|
+
image.remove();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
image.remove();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
title: article.title?.trim() || new URL(url).hostname,
|
|
57
|
+
author: article.byline || undefined,
|
|
58
|
+
content: markdown.turndown(clean.body.innerHTML).trim(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function seconds(value) {
|
|
62
|
+
const parts = value.replace(',', '.').split(':').map(Number);
|
|
63
|
+
if (parts.some(n => !Number.isFinite(n) || n < 0))
|
|
64
|
+
throw new Error('字幕时间戳无效');
|
|
65
|
+
return parts.reduce((sum, part) => sum * 60 + part, 0);
|
|
66
|
+
}
|
|
67
|
+
export function parseSubtitles(input) {
|
|
68
|
+
const blocks = input.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split(/\n\s*\n/);
|
|
69
|
+
const segments = [];
|
|
70
|
+
for (const block of blocks) {
|
|
71
|
+
if (/^(NOTE|STYLE|REGION)(?:\s|$)/.test(block.trim()))
|
|
72
|
+
continue;
|
|
73
|
+
const lines = block.split('\n');
|
|
74
|
+
const index = lines.findIndex(line => /-->/.test(line));
|
|
75
|
+
if (index < 0)
|
|
76
|
+
continue;
|
|
77
|
+
const match = lines[index].match(/((?:\d{1,3}:)?\d{2}:\d{2}[.,]\d{3})\s*-->\s*((?:\d{1,3}:)?\d{2}:\d{2}[.,]\d{3})/);
|
|
78
|
+
if (!match)
|
|
79
|
+
throw new Error('字幕时间戳格式不正确,请使用 SRT 或 WebVTT');
|
|
80
|
+
const text = lines.slice(index + 1).join('\n').replace(/<[^>]+>/g, '').trim();
|
|
81
|
+
if (!text)
|
|
82
|
+
continue;
|
|
83
|
+
const startSeconds = seconds(match[1]);
|
|
84
|
+
const endSeconds = seconds(match[2]);
|
|
85
|
+
if (endSeconds < startSeconds)
|
|
86
|
+
throw new Error('字幕结束时间早于开始时间');
|
|
87
|
+
segments.push({ id: `p${String(segments.length + 1).padStart(4, '0')}`, text, startSeconds, endSeconds });
|
|
88
|
+
}
|
|
89
|
+
if (!segments.length)
|
|
90
|
+
throw new Error('没有找到可读取的字幕,请检查文件是否为 SRT 或 WebVTT');
|
|
91
|
+
return segments;
|
|
92
|
+
}
|
|
93
|
+
export function formatTime(value) {
|
|
94
|
+
const s = Math.floor(value);
|
|
95
|
+
return `${String(Math.floor(s / 3600)).padStart(2, '0')}:${String(Math.floor(s / 60) % 60).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
|
96
|
+
}
|
|
97
|
+
export function splitSegments(content) {
|
|
98
|
+
const chunks = [];
|
|
99
|
+
for (const paragraph of content.split(/\n\s*\n/).map(p => p.trim()).filter(Boolean)) {
|
|
100
|
+
// Bound each readable segment without assuming spaces exist in Chinese.
|
|
101
|
+
for (let offset = 0; offset < paragraph.length; offset += 1600)
|
|
102
|
+
chunks.push(paragraph.slice(offset, offset + 1600));
|
|
103
|
+
}
|
|
104
|
+
return chunks.map((text, index) => ({ id: `p${String(index + 1).padStart(4, '0')}`, text }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function isPublicAddress(address: string): boolean;
|
|
2
|
+
export declare function validateUrl(input: string): URL;
|
|
3
|
+
export interface FetchPolicy {
|
|
4
|
+
allowProxyFakeIp?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function permittedAddress(hostname: string, address: string, policy: FetchPolicy): boolean;
|
|
7
|
+
export declare function fetchPublicPage(input: string, signal?: AbortSignal, policy?: FetchPolicy): Promise<{
|
|
8
|
+
html: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { request as httpRequest } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
import { lookup } from 'node:dns/promises';
|
|
4
|
+
import { isIP } from 'node:net';
|
|
5
|
+
const MAX_BYTES = 6 * 1024 * 1024;
|
|
6
|
+
export function isPublicAddress(address) {
|
|
7
|
+
if (isIP(address) === 4) {
|
|
8
|
+
const [a, b, c] = address.split('.').map(Number);
|
|
9
|
+
return !(a === 0 || a === 10 || a === 127 || a >= 224
|
|
10
|
+
|| (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31)
|
|
11
|
+
|| (a === 192 && (b === 168 || b === 0 || (b === 88 && c === 99)))
|
|
12
|
+
|| (a === 100 && b >= 64 && b <= 127) || (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
|
|
13
|
+
|| (a === 203 && b === 0 && c === 113));
|
|
14
|
+
}
|
|
15
|
+
if (isIP(address) === 6) {
|
|
16
|
+
const lower = address.toLowerCase();
|
|
17
|
+
return /^[23][0-9a-f]{3}:/.test(lower) && !lower.startsWith('2001:db8:')
|
|
18
|
+
&& !lower.startsWith('2001:0:') && !lower.startsWith('2002:');
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
export function validateUrl(input) {
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(input);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error('请输入完整的 http 或 https 网页地址');
|
|
29
|
+
}
|
|
30
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
31
|
+
throw new Error('仅支持不含账号密码的 http/https 地址');
|
|
32
|
+
}
|
|
33
|
+
if (url.port && !['80', '443'].includes(url.port))
|
|
34
|
+
throw new Error('网页采集仅支持标准 HTTP/HTTPS 端口');
|
|
35
|
+
url.hash = '';
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
export function permittedAddress(hostname, address, policy) {
|
|
39
|
+
if (isPublicAddress(address))
|
|
40
|
+
return true;
|
|
41
|
+
// An explicit deployment opt-in for Clash/Mihomo TUN fake DNS, not a general private-network exemption.
|
|
42
|
+
return policy.allowProxyFakeIp === true && isIP(hostname) === 0
|
|
43
|
+
&& hostname.includes('.') && !/\.(localhost|local|internal|lan)$/i.test(hostname)
|
|
44
|
+
&& /^198\.(18|19)\.\d{1,3}\.\d{1,3}$/.test(address);
|
|
45
|
+
}
|
|
46
|
+
export async function fetchPublicPage(input, signal, policy = {}) {
|
|
47
|
+
let url = validateUrl(input);
|
|
48
|
+
const timeout = AbortSignal.timeout(25_000);
|
|
49
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
50
|
+
for (let redirect = 0; redirect <= 4; redirect++) {
|
|
51
|
+
combined.throwIfAborted();
|
|
52
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '');
|
|
53
|
+
const addresses = await lookup(hostname, { all: true });
|
|
54
|
+
if (!addresses.length || addresses.some(item => !permittedAddress(hostname, item.address, policy))) {
|
|
55
|
+
if (addresses.some(item => /^198\.(18|19)\./.test(item.address)))
|
|
56
|
+
throw new Error('检测到代理 fake-IP DNS。若本机使用 Clash/Mihomo TUN,请启用插件的 allowProxyFakeIp;否则请直接导入文本。');
|
|
57
|
+
throw new Error('网页采集不访问本机、内网或保留地址,请直接导入文件或文本');
|
|
58
|
+
}
|
|
59
|
+
const address = addresses[0];
|
|
60
|
+
const response = await new Promise((resolve, reject) => {
|
|
61
|
+
const req = (url.protocol === 'https:' ? httpsRequest : httpRequest)(url, {
|
|
62
|
+
method: 'GET', signal: combined, family: address.family,
|
|
63
|
+
headers: { 'User-Agent': 'DSH-Knowledge-Library/0.1', Accept: 'text/html,text/plain;q=0.8', 'Accept-Encoding': 'identity' },
|
|
64
|
+
// Pin the validated DNS result so a second resolution cannot reach an internal host.
|
|
65
|
+
lookup: (_host, _options, callback) => callback(null, address.address, address.family),
|
|
66
|
+
}, res => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
res.on('data', (chunk) => {
|
|
70
|
+
size += chunk.length;
|
|
71
|
+
if (size > MAX_BYTES) {
|
|
72
|
+
req.destroy(new Error('网页超过 6 MB,请下载后手动导入'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
chunks.push(chunk);
|
|
76
|
+
});
|
|
77
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 0, location: res.headers.location, contentType: String(res.headers['content-type'] ?? ''), bytes: Buffer.concat(chunks) }));
|
|
78
|
+
res.on('error', reject);
|
|
79
|
+
});
|
|
80
|
+
req.on('error', reject);
|
|
81
|
+
req.end();
|
|
82
|
+
});
|
|
83
|
+
if ([301, 302, 303, 307, 308].includes(response.status) && response.location) {
|
|
84
|
+
url = validateUrl(new URL(response.location, url).href);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (response.status < 200 || response.status >= 300)
|
|
88
|
+
throw new Error(`网页返回 HTTP ${response.status},请检查链接或改为手动导入`);
|
|
89
|
+
if (!/text\/(html|plain)|application\/xhtml\+xml/i.test(response.contentType))
|
|
90
|
+
throw new Error('该地址不是可提取的网页,请下载文件后导入');
|
|
91
|
+
const encoding = /charset=["']?([\w-]+)/i.exec(response.contentType)?.[1] || 'utf-8';
|
|
92
|
+
let html;
|
|
93
|
+
try {
|
|
94
|
+
html = new TextDecoder(encoding).decode(response.bytes);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error(`暂不支持网页编码 ${encoding}`);
|
|
98
|
+
}
|
|
99
|
+
if (response.contentType.includes('text/plain')) {
|
|
100
|
+
html = `<html><body><article><p>${html.replaceAll('&', '&').replaceAll('<', '<').replaceAll('\n', '</p><p>')}</p></article></body></html>`;
|
|
101
|
+
}
|
|
102
|
+
return { html, url: url.href };
|
|
103
|
+
}
|
|
104
|
+
throw new Error('网页重定向次数过多');
|
|
105
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CaptureFields, ImportJob } from '../types.js';
|
|
2
|
+
import { Library } from './library.js';
|
|
3
|
+
import { fetchPublicPage } from './fetch.js';
|
|
4
|
+
export declare class ImportQueue {
|
|
5
|
+
private library;
|
|
6
|
+
private fetchPage;
|
|
7
|
+
private jobs;
|
|
8
|
+
private file;
|
|
9
|
+
private running;
|
|
10
|
+
private stopped;
|
|
11
|
+
private abort;
|
|
12
|
+
private active?;
|
|
13
|
+
constructor(library: Library, fetchPage?: typeof fetchPublicPage);
|
|
14
|
+
list(): {
|
|
15
|
+
id: string;
|
|
16
|
+
url: string;
|
|
17
|
+
status: "queued" | "running" | "completed" | "failed";
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
attempts: number;
|
|
21
|
+
materialId?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}[];
|
|
24
|
+
add(url: string, fields?: CaptureFields): ImportJob;
|
|
25
|
+
retry(id: string): ImportJob;
|
|
26
|
+
private persist;
|
|
27
|
+
private kick;
|
|
28
|
+
idle(): Promise<void>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { atomicJson } from './library.js';
|
|
5
|
+
import { fetchPublicPage, validateUrl } from './fetch.js';
|
|
6
|
+
export class ImportQueue {
|
|
7
|
+
library;
|
|
8
|
+
fetchPage;
|
|
9
|
+
jobs;
|
|
10
|
+
file;
|
|
11
|
+
running = false;
|
|
12
|
+
stopped = false;
|
|
13
|
+
abort = new AbortController();
|
|
14
|
+
active;
|
|
15
|
+
constructor(library, fetchPage = fetchPublicPage) {
|
|
16
|
+
this.library = library;
|
|
17
|
+
this.fetchPage = fetchPage;
|
|
18
|
+
this.file = join(library.root, '.system', 'jobs.json');
|
|
19
|
+
this.jobs = existsSync(this.file) ? JSON.parse(readFileSync(this.file, 'utf8')) : [];
|
|
20
|
+
if (!Array.isArray(this.jobs))
|
|
21
|
+
throw new Error('任务记录损坏,请备份并检查 .system/jobs.json');
|
|
22
|
+
for (const job of this.jobs)
|
|
23
|
+
if (job.status === 'running') {
|
|
24
|
+
job.status = job.attempts >= 3 ? 'failed' : 'queued';
|
|
25
|
+
job.error = '上次处理被中断,等待恢复';
|
|
26
|
+
}
|
|
27
|
+
this.persist();
|
|
28
|
+
this.kick();
|
|
29
|
+
}
|
|
30
|
+
list() { return this.jobs.slice().reverse().slice(0, 100).map(({ fields: _fields, ...job }) => job); }
|
|
31
|
+
add(url, fields = {}) {
|
|
32
|
+
const normalized = validateUrl(url).href;
|
|
33
|
+
const existing = this.jobs.find(j => j.url === normalized && ['queued', 'running'].includes(j.status));
|
|
34
|
+
if (existing)
|
|
35
|
+
return structuredClone(existing);
|
|
36
|
+
if (this.jobs.filter(j => ['queued', 'running'].includes(j.status)).length >= 50)
|
|
37
|
+
throw new Error('最多同时排队 50 个网页');
|
|
38
|
+
const timestamp = new Date().toISOString();
|
|
39
|
+
const job = { id: `job_${randomUUID()}`, url: normalized, fields, status: 'queued', createdAt: timestamp, updatedAt: timestamp, attempts: 0 };
|
|
40
|
+
this.jobs.push(job);
|
|
41
|
+
this.persist();
|
|
42
|
+
this.kick();
|
|
43
|
+
return structuredClone(job);
|
|
44
|
+
}
|
|
45
|
+
retry(id) {
|
|
46
|
+
const job = this.jobs.find(j => j.id === id);
|
|
47
|
+
if (!job || job.status !== 'failed')
|
|
48
|
+
throw new Error('只能重试失败的任务');
|
|
49
|
+
job.status = 'queued';
|
|
50
|
+
job.error = undefined;
|
|
51
|
+
job.attempts = 0;
|
|
52
|
+
this.persist();
|
|
53
|
+
this.kick();
|
|
54
|
+
return structuredClone(job);
|
|
55
|
+
}
|
|
56
|
+
persist() { atomicJson(this.file, this.jobs); }
|
|
57
|
+
kick() {
|
|
58
|
+
if (this.running || this.stopped)
|
|
59
|
+
return;
|
|
60
|
+
this.running = true;
|
|
61
|
+
this.active = new Promise(resolve => setImmediate(resolve)).then(async () => {
|
|
62
|
+
while (!this.stopped) {
|
|
63
|
+
const job = this.jobs.find(j => j.status === 'queued');
|
|
64
|
+
if (!job)
|
|
65
|
+
break;
|
|
66
|
+
job.status = 'running';
|
|
67
|
+
job.attempts++;
|
|
68
|
+
job.updatedAt = new Date().toISOString();
|
|
69
|
+
this.persist();
|
|
70
|
+
try {
|
|
71
|
+
const page = await this.fetchPage(job.url, this.abort.signal);
|
|
72
|
+
if (this.stopped)
|
|
73
|
+
break;
|
|
74
|
+
const result = this.library.captureHtml(page.html, page.url, job.fields);
|
|
75
|
+
job.materialId = result.material.id;
|
|
76
|
+
job.status = 'completed';
|
|
77
|
+
job.error = undefined;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
job.status = this.stopped ? 'queued' : 'failed';
|
|
81
|
+
job.error = error.message;
|
|
82
|
+
}
|
|
83
|
+
job.updatedAt = new Date().toISOString();
|
|
84
|
+
this.persist();
|
|
85
|
+
}
|
|
86
|
+
}).finally(() => {
|
|
87
|
+
for (const job of this.jobs)
|
|
88
|
+
if (job.status === 'running')
|
|
89
|
+
job.status = 'queued';
|
|
90
|
+
this.persist();
|
|
91
|
+
this.running = false;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async idle() { await this.active; }
|
|
95
|
+
async close() { this.stopped = true; this.abort.abort(); await this.active; }
|
|
96
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { CaptureFields, CaptureResult, Citation, MaterialDetail, SearchOptions, SearchResult } from '../types.js';
|
|
3
|
+
export declare const MAX_FILE_BYTES: number;
|
|
4
|
+
export declare const MAX_TEXT_CHARS = 2000000;
|
|
5
|
+
export declare function categoryName(value: unknown): string;
|
|
6
|
+
export declare function atomicJson(path: string, value: unknown): void;
|
|
7
|
+
export declare class Library {
|
|
8
|
+
readonly root: string;
|
|
9
|
+
readonly db: DatabaseSync;
|
|
10
|
+
readonly warnings: string[];
|
|
11
|
+
private lockPath;
|
|
12
|
+
private closed;
|
|
13
|
+
constructor(root: string);
|
|
14
|
+
close(): void;
|
|
15
|
+
private assertContained;
|
|
16
|
+
private index;
|
|
17
|
+
rebuild(): {
|
|
18
|
+
indexed: number;
|
|
19
|
+
warnings: string[];
|
|
20
|
+
};
|
|
21
|
+
private row;
|
|
22
|
+
get(id: string): MaterialDetail;
|
|
23
|
+
original(id: string): {
|
|
24
|
+
bytes: Buffer;
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
private save;
|
|
28
|
+
private tags;
|
|
29
|
+
captureText(text: string, fields?: CaptureFields): CaptureResult;
|
|
30
|
+
captureHtml(html: string, url: string, fields?: CaptureFields): CaptureResult;
|
|
31
|
+
importFile(path: string, fields?: CaptureFields): CaptureResult;
|
|
32
|
+
importBytes(name: string, bytes: Buffer, fields?: CaptureFields): CaptureResult;
|
|
33
|
+
private decode;
|
|
34
|
+
update(id: string, fields: CaptureFields): MaterialDetail;
|
|
35
|
+
saveNote(content: string, citations: Citation[], fields: CaptureFields): CaptureResult;
|
|
36
|
+
search(options?: SearchOptions): {
|
|
37
|
+
items: SearchResult[];
|
|
38
|
+
total: number;
|
|
39
|
+
};
|
|
40
|
+
stats(): {
|
|
41
|
+
total: number;
|
|
42
|
+
ready: number;
|
|
43
|
+
notes: number;
|
|
44
|
+
inbox: number;
|
|
45
|
+
categories: {
|
|
46
|
+
name: string;
|
|
47
|
+
count: number;
|
|
48
|
+
}[];
|
|
49
|
+
tags: {
|
|
50
|
+
name: string;
|
|
51
|
+
count: number;
|
|
52
|
+
}[];
|
|
53
|
+
collections: {
|
|
54
|
+
name: string;
|
|
55
|
+
count: number;
|
|
56
|
+
}[];
|
|
57
|
+
root: string;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
};
|
|
60
|
+
exportMarkdown(id: string): string;
|
|
61
|
+
}
|