yuque-cookie-plugin 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/.env.example +5 -0
- package/README.md +253 -0
- package/bin/yuque-local.mjs +3 -0
- package/dist/auth.js +439 -0
- package/dist/cli.js +692 -0
- package/dist/client-cookie.js +544 -0
- package/dist/download-utils.js +579 -0
- package/dist/downloader.js +388 -0
- package/dist/editor-bridge.js +240 -0
- package/dist/fs-utils.js +9 -0
- package/dist/lake-diff.js +113 -0
- package/dist/lake-heading-numbering.js +29 -0
- package/dist/lake-insert.js +34 -0
- package/dist/lake-markdown.js +54 -0
- package/dist/lake-parser.js +79 -0
- package/dist/lake-transform.js +12 -0
- package/dist/reports.js +10 -0
- package/dist/serve-book.js +134 -0
- package/dist/types.js +1 -0
- package/docs/docker-quickstart.md +96 -0
- package/docs/native-lake-capability.md +101 -0
- package/docs/npm-release.md +91 -0
- package/docs/real-acceptance.md +146 -0
- package/docs/usage-zh.md +580 -0
- package/package.json +71 -0
- package/skills/yuque-cookie-plugin/SKILL.md +58 -0
- package/skills/yuque-cookie-plugin/commands.yaml +61 -0
- package/skills/yuque-cookie-plugin/reference/agent-spec.md +51 -0
- package/vendor/lake-editor/CodeMirror.js +1 -0
- package/vendor/lake-editor/antd.4.24.13.css +26886 -0
- package/vendor/lake-editor/doc.css +23603 -0
- package/vendor/lake-editor/doc.umd.js +2 -0
- package/vendor/lake-editor/katex.js +18829 -0
- package/vendor/lake-editor/lake-editor-icon.js +1 -0
- package/vendor/lake-editor/react-dom.production.min.js +267 -0
- package/vendor/lake-editor/react.production.min.js +31 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { parseLake } from "./lake-parser.js";
|
|
2
|
+
export function diffLakeSnapshots(before, after) {
|
|
3
|
+
const beforeEdit = before.edit || {};
|
|
4
|
+
const afterEdit = after.edit || {};
|
|
5
|
+
const beforeLake = beforeEdit.body_asl || before.lake?.sourcecode || '';
|
|
6
|
+
const afterLake = afterEdit.body_asl || after.lake?.sourcecode || '';
|
|
7
|
+
const beforeHeadings = extractLakeHeadings(beforeLake);
|
|
8
|
+
const afterHeadings = extractLakeHeadings(afterLake);
|
|
9
|
+
const beforeParsed = parseLake(beforeLake);
|
|
10
|
+
const afterParsed = parseLake(afterLake);
|
|
11
|
+
return {
|
|
12
|
+
before: {
|
|
13
|
+
doc: before.doc,
|
|
14
|
+
heading_count: beforeHeadings.length
|
|
15
|
+
},
|
|
16
|
+
after: {
|
|
17
|
+
doc: after.doc,
|
|
18
|
+
heading_count: afterHeadings.length
|
|
19
|
+
},
|
|
20
|
+
headings: diffHeadings(beforeHeadings, afterHeadings),
|
|
21
|
+
stats: {
|
|
22
|
+
body_asl_length_before: beforeLake.length,
|
|
23
|
+
body_asl_length_after: afterLake.length,
|
|
24
|
+
body_asl_changed: beforeLake !== afterLake
|
|
25
|
+
},
|
|
26
|
+
structure: {
|
|
27
|
+
before: summarizeStructure(beforeParsed),
|
|
28
|
+
after: summarizeStructure(afterParsed),
|
|
29
|
+
changed: summarizeStructure(beforeParsed).join('|') !== summarizeStructure(afterParsed).join('|')
|
|
30
|
+
},
|
|
31
|
+
cards: diffNamedCounts(countByName(beforeParsed.cards), countByName(afterParsed.cards)),
|
|
32
|
+
lists: {
|
|
33
|
+
before: beforeParsed.lists.map((list) => ({ ordered: list.ordered, item_count: list.item_count })),
|
|
34
|
+
after: afterParsed.lists.map((list) => ({ ordered: list.ordered, item_count: list.item_count }))
|
|
35
|
+
},
|
|
36
|
+
tables: {
|
|
37
|
+
before: beforeParsed.tables.map((table) => ({ row_count: table.row_count })),
|
|
38
|
+
after: afterParsed.tables.map((table) => ({ row_count: table.row_count }))
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function extractLakeHeadings(lake) {
|
|
43
|
+
const result = [];
|
|
44
|
+
const headingRegex = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/g;
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = headingRegex.exec(lake))) {
|
|
47
|
+
const block = match[0];
|
|
48
|
+
result.push({
|
|
49
|
+
type: 'heading',
|
|
50
|
+
level: Number(match[1]),
|
|
51
|
+
attrs: {},
|
|
52
|
+
text: decodeEntities(block.replace(/<[^>]+>/g, '')),
|
|
53
|
+
block
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function diffHeadings(before, after) {
|
|
59
|
+
const count = Math.max(before.length, after.length);
|
|
60
|
+
const changes = [];
|
|
61
|
+
for (let i = 0; i < count; i += 1) {
|
|
62
|
+
const b = before[i];
|
|
63
|
+
const a = after[i];
|
|
64
|
+
if (!b || !a || b.level !== a.level || b.text !== a.text || b.block !== a.block) {
|
|
65
|
+
changes.push({
|
|
66
|
+
index: i,
|
|
67
|
+
before: b && summarizeHeading(b),
|
|
68
|
+
after: a && summarizeHeading(a)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return changes;
|
|
73
|
+
}
|
|
74
|
+
function summarizeHeading(heading) {
|
|
75
|
+
return {
|
|
76
|
+
level: heading.level,
|
|
77
|
+
text: heading.text,
|
|
78
|
+
attrs: heading.attrs,
|
|
79
|
+
block_preview: heading.block.slice(0, 500)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function decodeEntities(value) {
|
|
83
|
+
return value
|
|
84
|
+
.replace(/"/g, '"')
|
|
85
|
+
.replace(/&/g, '&')
|
|
86
|
+
.replace(/</g, '<')
|
|
87
|
+
.replace(/>/g, '>')
|
|
88
|
+
.replace(/'/g, "'");
|
|
89
|
+
}
|
|
90
|
+
function summarizeStructure(parsed) {
|
|
91
|
+
return [
|
|
92
|
+
`headings:${parsed.headings.length}`,
|
|
93
|
+
`cards:${parsed.cards.length}`,
|
|
94
|
+
`lists:${parsed.lists.length}`,
|
|
95
|
+
`tables:${parsed.tables.length}`,
|
|
96
|
+
`paragraphs:${parsed.stats.paragraph_count}`,
|
|
97
|
+
`length:${parsed.stats.length}`
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
function countByName(items) {
|
|
101
|
+
const counts = {};
|
|
102
|
+
for (const item of items)
|
|
103
|
+
counts[item.name] = (counts[item.name] || 0) + 1;
|
|
104
|
+
return counts;
|
|
105
|
+
}
|
|
106
|
+
function diffNamedCounts(before, after) {
|
|
107
|
+
const names = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
108
|
+
return [...names].sort().map((name) => ({
|
|
109
|
+
name,
|
|
110
|
+
before: before[name] || 0,
|
|
111
|
+
after: after[name] || 0
|
|
112
|
+
})).filter((item) => item.before !== item.after);
|
|
113
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { parse } from 'node-html-parser';
|
|
2
|
+
export function enableLakeHeadingNumbering(lake) {
|
|
3
|
+
if (!lake.trim())
|
|
4
|
+
return lake;
|
|
5
|
+
const root = parse(lake, {
|
|
6
|
+
lowerCaseTagName: true,
|
|
7
|
+
comment: false,
|
|
8
|
+
blockTextElements: {
|
|
9
|
+
script: true,
|
|
10
|
+
noscript: true,
|
|
11
|
+
style: true,
|
|
12
|
+
pre: true
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
for (const heading of root.querySelectorAll('h1,h2,h3,h4,h5,h6')) {
|
|
16
|
+
heading.setAttribute('data-lake-index-type', '2');
|
|
17
|
+
stripTextualHeadingNumber(heading);
|
|
18
|
+
}
|
|
19
|
+
return root.toString();
|
|
20
|
+
}
|
|
21
|
+
function stripTextualHeadingNumber(heading) {
|
|
22
|
+
const firstSpan = heading.querySelector('span');
|
|
23
|
+
if (!firstSpan)
|
|
24
|
+
return;
|
|
25
|
+
const current = firstSpan.text;
|
|
26
|
+
const next = current.replace(/^\s*\d+(?:\.\d+)*\.?\s+/, '');
|
|
27
|
+
if (next !== current)
|
|
28
|
+
firstSpan.set_content(next);
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function extractFirstImageParagraph(lake) {
|
|
2
|
+
const paragraphMatch = /<p\b[^>]*>\s*<card\b[^>]*\bname="image"[^>]*><\/card>\s*<\/p>/i.exec(lake);
|
|
3
|
+
if (paragraphMatch?.[0])
|
|
4
|
+
return paragraphMatch[0];
|
|
5
|
+
const cardMatch = /<card\b[^>]*\bname="image"[^>]*><\/card>/i.exec(lake);
|
|
6
|
+
if (cardMatch?.[0])
|
|
7
|
+
return `<p>${cardMatch[0]}</p>`;
|
|
8
|
+
throw new Error('Serialized Lake did not contain an image card.');
|
|
9
|
+
}
|
|
10
|
+
export function insertLakeBlock(lake, block, afterText) {
|
|
11
|
+
if (!lake.trim())
|
|
12
|
+
throw new Error('Current Lake document is empty.');
|
|
13
|
+
if (!block.trim())
|
|
14
|
+
throw new Error('Inserted Lake block is empty.');
|
|
15
|
+
if (afterText?.trim()) {
|
|
16
|
+
const inserted = insertAfterTextBlock(lake, block, afterText.trim());
|
|
17
|
+
if (inserted)
|
|
18
|
+
return inserted;
|
|
19
|
+
throw new Error(`Could not find a Lake block containing --after-text: ${afterText}`);
|
|
20
|
+
}
|
|
21
|
+
return `${lake}${block}`;
|
|
22
|
+
}
|
|
23
|
+
function insertAfterTextBlock(lake, block, text) {
|
|
24
|
+
const escapedText = escapeRegExp(text);
|
|
25
|
+
const blockPattern = new RegExp(`<(p|h[1-6]|blockquote|li)\\b[^>]*>[\\s\\S]*?${escapedText}[\\s\\S]*?<\\/\\1>`);
|
|
26
|
+
const match = blockPattern.exec(lake);
|
|
27
|
+
if (!match?.[0] || match.index === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
const insertAt = match.index + match[0].length;
|
|
30
|
+
return `${lake.slice(0, insertAt)}${block}${lake.slice(insertAt)}`;
|
|
31
|
+
}
|
|
32
|
+
function escapeRegExp(value) {
|
|
33
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseLake, stripTags } from "./lake-parser.js";
|
|
2
|
+
export function snapshotToMarkdown(snapshot) {
|
|
3
|
+
const lake = snapshot.edit?.body_asl || snapshot.lake?.sourcecode || snapshot.edit?.body_draft_asl || '';
|
|
4
|
+
return lakeToMarkdown(lake, snapshot);
|
|
5
|
+
}
|
|
6
|
+
export function lakeToMarkdown(lake = '', snapshot) {
|
|
7
|
+
const parsed = parseLake(lake);
|
|
8
|
+
const title = snapshot?.doc?.title || 'Yuque Document';
|
|
9
|
+
const lines = [`# ${title}`, ''];
|
|
10
|
+
lines.push(`> Source: ${snapshot?.url || 'snapshot'}`);
|
|
11
|
+
lines.push(`> Headings: ${parsed.headings.length}; Cards: ${parsed.cards.length}; Lists: ${parsed.lists.length}; Tables: ${parsed.tables.length}`);
|
|
12
|
+
lines.push('');
|
|
13
|
+
const body = lake
|
|
14
|
+
.replace(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1>/g, (_match, level, content) => `${'#'.repeat(Number(level))} ${stripTags(content)}\n`)
|
|
15
|
+
.replace(/<p\b[^>]*>([\s\S]*?)<\/p>/g, (_match, content) => `${inlineToMarkdown(content)}\n`)
|
|
16
|
+
.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/g, (_match, content) => `> ${inlineToMarkdown(content)}\n`)
|
|
17
|
+
.replace(/<card\b([^>]*)>(?:<\/card>)?/g, (block) => cardToMarkdown(block))
|
|
18
|
+
.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/g, (_match, content) => `- ${inlineToMarkdown(content)}\n`)
|
|
19
|
+
.replace(/<[^>]+>/g, '')
|
|
20
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
21
|
+
.trim();
|
|
22
|
+
if (body)
|
|
23
|
+
lines.push(body, '');
|
|
24
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`;
|
|
25
|
+
}
|
|
26
|
+
function inlineToMarkdown(value = '') {
|
|
27
|
+
return stripTags(value
|
|
28
|
+
.replace(/<strong\b[^>]*>([\s\S]*?)<\/strong>/g, '**$1**')
|
|
29
|
+
.replace(/<em\b[^>]*>([\s\S]*?)<\/em>/g, '*$1*')
|
|
30
|
+
.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/g, '`$1`')
|
|
31
|
+
.replace(/<a\b[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g, '[$2]($1)'));
|
|
32
|
+
}
|
|
33
|
+
function cardToMarkdown(block) {
|
|
34
|
+
const name = (block.match(/\bname="([^"]+)"/) || [])[1] || 'unknown';
|
|
35
|
+
const value = (block.match(/\bvalue="([^"]+)"/) || [])[1] || '';
|
|
36
|
+
const data = decodeCardValue(value);
|
|
37
|
+
if (name === 'codeblock')
|
|
38
|
+
return `\n\`\`\`${data.mode || 'plain'}\n${data.code || ''}\n\`\`\`\n`;
|
|
39
|
+
if (name === 'image')
|
|
40
|
+
return `\n\n`;
|
|
41
|
+
if (name === 'hr')
|
|
42
|
+
return '\n---\n';
|
|
43
|
+
return `\n[Unsupported Yuque card: ${name}]\n`;
|
|
44
|
+
}
|
|
45
|
+
function decodeCardValue(value) {
|
|
46
|
+
if (!value.startsWith('data:'))
|
|
47
|
+
return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(decodeURIComponent(value.slice(5)));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { parse } from 'node-html-parser';
|
|
2
|
+
export function parseLake(lake = '') {
|
|
3
|
+
const root = parse(lake, {
|
|
4
|
+
lowerCaseTagName: true,
|
|
5
|
+
comment: false,
|
|
6
|
+
blockTextElements: {
|
|
7
|
+
script: true,
|
|
8
|
+
noscript: true,
|
|
9
|
+
style: true,
|
|
10
|
+
pre: true
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const headings = root.querySelectorAll('h1,h2,h3,h4,h5,h6').map((node) => ({
|
|
14
|
+
type: 'heading',
|
|
15
|
+
level: Number(node.tagName.slice(1)),
|
|
16
|
+
attrs: attrsFromNode(node),
|
|
17
|
+
text: decodeEntities(node.text.trim().replace(/\s+/g, ' ')),
|
|
18
|
+
block: node.toString()
|
|
19
|
+
}));
|
|
20
|
+
const cards = root.querySelectorAll('card').map((node) => {
|
|
21
|
+
const attrs = attrsFromNode(node);
|
|
22
|
+
return {
|
|
23
|
+
type: 'card',
|
|
24
|
+
attrs,
|
|
25
|
+
name: attrs.name || '',
|
|
26
|
+
block: node.toString()
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const lists = root.querySelectorAll('ul,ol').map((node) => ({
|
|
30
|
+
type: 'list',
|
|
31
|
+
ordered: node.tagName === 'ol',
|
|
32
|
+
attrs: attrsFromNode(node),
|
|
33
|
+
item_count: node.querySelectorAll('li').length,
|
|
34
|
+
block: node.toString()
|
|
35
|
+
}));
|
|
36
|
+
const tables = root.querySelectorAll('table').map((node) => ({
|
|
37
|
+
type: 'table',
|
|
38
|
+
attrs: attrsFromNode(node),
|
|
39
|
+
row_count: node.querySelectorAll('tr').length,
|
|
40
|
+
block: node.toString()
|
|
41
|
+
}));
|
|
42
|
+
return {
|
|
43
|
+
headings,
|
|
44
|
+
cards,
|
|
45
|
+
lists,
|
|
46
|
+
tables,
|
|
47
|
+
stats: {
|
|
48
|
+
length: lake.length,
|
|
49
|
+
paragraph_count: root.querySelectorAll('p').length
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function parseAttrs(value = '') {
|
|
54
|
+
const attrs = {};
|
|
55
|
+
const attrRegex = /([:\w-]+)(?:="([^"]*)")?/g;
|
|
56
|
+
let match;
|
|
57
|
+
while ((match = attrRegex.exec(value))) {
|
|
58
|
+
attrs[match[1]] = decodeEntities(match[2] || '');
|
|
59
|
+
}
|
|
60
|
+
return attrs;
|
|
61
|
+
}
|
|
62
|
+
export function stripTags(value = '') {
|
|
63
|
+
return decodeEntities(value.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim());
|
|
64
|
+
}
|
|
65
|
+
function attrsFromNode(node) {
|
|
66
|
+
const attrs = {};
|
|
67
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
68
|
+
attrs[key] = decodeEntities(String(value));
|
|
69
|
+
}
|
|
70
|
+
return attrs;
|
|
71
|
+
}
|
|
72
|
+
function decodeEntities(value) {
|
|
73
|
+
return value
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
.replace(/&/g, '&')
|
|
76
|
+
.replace(/</g, '<')
|
|
77
|
+
.replace(/>/g, '>')
|
|
78
|
+
.replace(/'/g, "'");
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Focused Lake transforms live here. Keep these small and tested against
|
|
2
|
+
// snapshot before/after pairs captured from the Yuque web editor.
|
|
3
|
+
export function removeTextualHeadingNumbers(lake) {
|
|
4
|
+
return lake.replace(/<h([1-6])\b[^>]*>[\s\S]*?<\/h\1>/g, (block) => {
|
|
5
|
+
return block.replace(/(<span\b[^>]*>)([\s\S]*?)(<\/span>)/, (_match, open, text, close) => {
|
|
6
|
+
return `${open}${stripNumber(text)}${close}`;
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function stripNumber(text) {
|
|
11
|
+
return text.replace(/^\s*\d+(?:\.\d+)*[..、]?\s+/, '');
|
|
12
|
+
}
|
package/dist/reports.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { timestampForFile } from "./fs-utils.js";
|
|
4
|
+
export async function writeReport(prefix, data) {
|
|
5
|
+
const dir = path.resolve('reports');
|
|
6
|
+
await mkdir(dir, { recursive: true });
|
|
7
|
+
const file = path.join(dir, `${prefix}-${timestampForFile()}.json`);
|
|
8
|
+
await writeFile(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
9
|
+
return file;
|
|
10
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { access, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function serveBook(root, options = {}) {
|
|
4
|
+
const rootPath = path.resolve(root);
|
|
5
|
+
await access(rootPath);
|
|
6
|
+
const vitepressPath = path.join(rootPath, '.vitepress');
|
|
7
|
+
let generated = false;
|
|
8
|
+
if (options.force) {
|
|
9
|
+
await createVitePressConfigForTest(rootPath);
|
|
10
|
+
generated = true;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
try {
|
|
14
|
+
await access(vitepressPath);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
await createVitePressConfigForTest(rootPath);
|
|
18
|
+
generated = true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const configPath = path.join(vitepressPath, 'config.mjs');
|
|
22
|
+
if (options.configOnly) {
|
|
23
|
+
return {
|
|
24
|
+
root: rootPath,
|
|
25
|
+
config: configPath,
|
|
26
|
+
generated
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const createVitePressServer = options.createServer || await loadVitePressCreateServer();
|
|
30
|
+
const server = await createVitePressServer(rootPath, {
|
|
31
|
+
host: options.host || 'localhost',
|
|
32
|
+
port: Number(options.port || 5173)
|
|
33
|
+
});
|
|
34
|
+
await server.listen();
|
|
35
|
+
server.printUrls();
|
|
36
|
+
return server;
|
|
37
|
+
}
|
|
38
|
+
async function loadVitePressCreateServer() {
|
|
39
|
+
try {
|
|
40
|
+
const mod = await import('vitepress');
|
|
41
|
+
return mod.createServer;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`VitePress is required for "serve-book" without --config-only. Install it in your current project or use "serve-book <book-path> --config-only". Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function createVitePressConfigForTest(root) {
|
|
48
|
+
const vitepressPath = path.join(root, '.vitepress');
|
|
49
|
+
await mkdir(vitepressPath, { recursive: true });
|
|
50
|
+
const progress = await readProgress(root);
|
|
51
|
+
const sidebar = await createSidebar(root, root, progress);
|
|
52
|
+
const config = `export default {
|
|
53
|
+
title: ${JSON.stringify(path.basename(root))},
|
|
54
|
+
themeConfig: {
|
|
55
|
+
search: { provider: 'local' },
|
|
56
|
+
sidebar: ${JSON.stringify(sidebar, null, 2)}
|
|
57
|
+
},
|
|
58
|
+
markdown: {
|
|
59
|
+
html: true,
|
|
60
|
+
breaks: true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
await writeFile(path.join(vitepressPath, 'config.mjs'), config);
|
|
65
|
+
}
|
|
66
|
+
async function createSidebar(root, current, progress) {
|
|
67
|
+
const ignore = new Set(['.vitepress', 'img', 'attachments', 'progress.json']);
|
|
68
|
+
const entries = sortEntriesByProgress(root, (await readdir(current)).filter((entry) => !ignore.has(entry)), current, progress);
|
|
69
|
+
const items = [];
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const full = path.join(current, entry);
|
|
72
|
+
const info = await stat(full);
|
|
73
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
74
|
+
if (info.isDirectory()) {
|
|
75
|
+
const children = await createSidebar(root, full, progress);
|
|
76
|
+
if (!children.length)
|
|
77
|
+
continue;
|
|
78
|
+
const item = {
|
|
79
|
+
text: entry,
|
|
80
|
+
collapsed: true,
|
|
81
|
+
items: children
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
await access(path.join(full, 'index.md'));
|
|
85
|
+
item.link = `/${rel}/`;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// no index
|
|
89
|
+
}
|
|
90
|
+
items.push(item);
|
|
91
|
+
}
|
|
92
|
+
else if (/\.md$/i.test(entry)) {
|
|
93
|
+
items.push({
|
|
94
|
+
text: entry.replace(/\.md$/i, ''),
|
|
95
|
+
link: `/${rel.replace(/\.md$/i, '')}`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return items;
|
|
100
|
+
}
|
|
101
|
+
function sortEntriesByProgress(root, entries, current, progress) {
|
|
102
|
+
if (!progress.length)
|
|
103
|
+
return entries.sort();
|
|
104
|
+
const order = new Map();
|
|
105
|
+
progress.forEach((item, index) => {
|
|
106
|
+
const normalized = item.path.split(path.sep).join('/');
|
|
107
|
+
order.set(normalized, index);
|
|
108
|
+
const firstPart = normalized.split('/')[0];
|
|
109
|
+
if (firstPart && !order.has(firstPart))
|
|
110
|
+
order.set(firstPart, index);
|
|
111
|
+
const dirname = path.dirname(normalized);
|
|
112
|
+
if (dirname && dirname !== '.' && !order.has(dirname))
|
|
113
|
+
order.set(dirname, index);
|
|
114
|
+
});
|
|
115
|
+
const currentRel = path.relative(root, current).split(path.sep).join('/');
|
|
116
|
+
return [...entries].sort((a, b) => {
|
|
117
|
+
const aKey = currentRel ? `${currentRel}/${a}` : a;
|
|
118
|
+
const bKey = currentRel ? `${currentRel}/${b}` : b;
|
|
119
|
+
const aOrder = order.get(aKey.replace(/\.md$/i, '')) ?? order.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
|
120
|
+
const bOrder = order.get(bKey.replace(/\.md$/i, '')) ?? order.get(bKey) ?? Number.MAX_SAFE_INTEGER;
|
|
121
|
+
if (aOrder !== bOrder)
|
|
122
|
+
return aOrder - bOrder;
|
|
123
|
+
return a.localeCompare(b, 'zh-CN');
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function readProgress(root) {
|
|
127
|
+
try {
|
|
128
|
+
const data = await readFile(path.join(root, 'progress.json'), 'utf8');
|
|
129
|
+
return JSON.parse(data);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Docker 隔离试用
|
|
2
|
+
|
|
3
|
+
本文档用于在不影响宿主机 Node/npm/Python 环境的情况下,使用 Docker 试用 `yuque-cookie-plugin`。
|
|
4
|
+
|
|
5
|
+
## 一行启动并安装
|
|
6
|
+
|
|
7
|
+
复制下面命令时保持为完整一行,不要在 `npm install -g` 后换行:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
docker run --rm -it -p 34567:34567 node:22-bookworm bash -lc 'python3 --version && git --version && npm install -g yuque-cookie-plugin && yuque-local --help && bash'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
进入容器后运行登录:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yuque-local auth login --manual
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
如果你想使用网页填写模式,也可以运行:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yuque-local auth login --port 34567
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
然后在宿主机浏览器打开:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
http://127.0.0.1:34567/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 备用方式:进入容器后逐条执行
|
|
32
|
+
|
|
33
|
+
如果终端复制长命令时容易自动换行,推荐先进入容器:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
docker run --rm -it -p 34567:34567 node:22-bookworm bash
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
然后在容器里逐条执行:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 --version
|
|
43
|
+
git --version
|
|
44
|
+
npm install -g yuque-cookie-plugin
|
|
45
|
+
yuque-local --help
|
|
46
|
+
yuque-local auth login --manual
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
网页填写模式可以改用:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
yuque-local auth login --port 34567
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
宿主机浏览器同样打开:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
http://127.0.0.1:34567/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 常见错误
|
|
62
|
+
|
|
63
|
+
错误示例:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install -g
|
|
67
|
+
yuque-cookie-plugin
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
这会导致 npm 没收到安装地址,然后报错:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Could not read package.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
另一个错误示例:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bash -lc 'python3 --version && git --version
|
|
80
|
+
&& npm install -g yuque-cookie-plugin'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
这会导致第二行以 `&&` 开头,bash 报:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
syntax error near unexpected token `&&'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
结论:要么使用完整单行命令,要么进入容器后逐条执行。
|
|
90
|
+
|
|
91
|
+
## 说明
|
|
92
|
+
|
|
93
|
+
- `node:22-bookworm` 镜像内已经包含 Node、npm、Python 3 和 git。
|
|
94
|
+
- 登录凭据会保存到容器内的 `~/.config/yuque-cookie-plugin/config.json`。
|
|
95
|
+
- 使用 `--rm` 启动的容器退出后会删除,登录凭据也会丢失。
|
|
96
|
+
- 如果需要保留配置,可以后续增加 Docker volume 映射。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# 语雀原生 Lake 写入能力矩阵
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
|
|
5
|
+
本项目允许 AI 输入 Markdown、HTML 或本地文件,但写入语雀后的最终形态应尽可能是语雀编辑器原生 Lake 结构,而不是把 Markdown 文本硬塞进文档。
|
|
6
|
+
|
|
7
|
+
核心判断标准:
|
|
8
|
+
|
|
9
|
+
- 最终文档 `format` 应为 `lake`。
|
|
10
|
+
- `body_asl` / `body_draft_asl` 应包含语雀 Lake 节点、`data-lake-id`、卡片元数据等结构。
|
|
11
|
+
- 对复杂结构必须先通过真实语雀文档 snapshot before/after 验证,再固化自动化逻辑。
|
|
12
|
+
- 不使用纯文本伪装编辑器功能。例如标题编号不能写成标题文本里的 `1. 标题`。
|
|
13
|
+
|
|
14
|
+
## 当前已真实验证
|
|
15
|
+
|
|
16
|
+
| 能力 | 当前状态 | 原生证据 | 说明 |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| 文档创建 | 已验证 | `create-doc` 真实创建并加入 TOC | 不传 `--slug` 时由语雀自动生成文档 slug |
|
|
19
|
+
| 最终 Lake 化 | 已验证 | 创建后读取 `body_asl` 并通过 `/api/docs/:id/content` 写回 `format: lake` | `create-doc` 默认执行,除非显式传 `--no-native-lake` |
|
|
20
|
+
| 标题 | 已验证 | `<h1 data-lake-id="..." id="...">` | 由语雀 Markdown 导入生成,再 Lake 写回 |
|
|
21
|
+
| 段落 | 已验证 | `<p data-lake-id="..." id="...">` | 普通正文是 Lake 原生段落 |
|
|
22
|
+
| 行内代码 | 已验证 | `<code data-lake-id="...">` | 快照中已出现 |
|
|
23
|
+
| 无序列表 | 已验证 | `<ul list="..."><li data-lake-id="...">` | 语雀会生成 list id 和连续 start |
|
|
24
|
+
| 有序列表 | 已验证 | `<ol list="...">`、`start="2"` | 真实快照显示语雀会拆成连续 Lake 列表节点 |
|
|
25
|
+
| 嵌套列表 | 已验证 | `data-lake-indent="1"` | 真实快照显示二级列表使用缩进属性 |
|
|
26
|
+
| 引用块 | 已验证 | `<blockquote data-lake-id="...">` | 由 Markdown `>` 导入生成 |
|
|
27
|
+
| 分隔线 | 已验证 | `<card type="block" name="hr" value="data:...">` | 语雀使用 hr card |
|
|
28
|
+
| 加粗/斜体/删除线 | 已验证 | `<strong>`、`<em>`、`<del>` | 行内结构保留为 Lake 原生节点 |
|
|
29
|
+
| 普通链接 | 已验证 | `<a href="..." target="_blank" data-lake-id="...">` | 普通外链可原生写入 |
|
|
30
|
+
| 表格 | 已验证 | `<table class="lake-table">`、`<colgroup>`、`td/p/span` | 当前真实快照已经是 Lake 表格 |
|
|
31
|
+
| 代码块 | 已验证 | `<card type="inline" name="codeblock" value="data:...">` | 语雀使用 codeblock card,而不是普通 `<pre>` |
|
|
32
|
+
| 标题章节编号 | 已验证 | 标题节点 `data-lake-index-type="2"` | 这是编辑器原生编号;渲染层的 `lake-read-ignore` 编号 span 不应写入正文 |
|
|
33
|
+
|
|
34
|
+
## 当前待真实验证
|
|
35
|
+
|
|
36
|
+
| 能力 | 当前判断 | 下一步验证方式 |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| 下划线 | 未单独验收 | 创建含 HTML `<u>` 的样例并 snapshot |
|
|
39
|
+
| bookmark 链接卡片 | 未验收 | 验证普通链接何时升级为 bookmark card |
|
|
40
|
+
| 图片 | 已验证 | `POST /api/upload/attach` + `<card type="inline" name="image" value="data:...">` | 已真实上传本地 PNG,写入 Lake,下载回本地资源无 warning |
|
|
41
|
+
| 附件/PDF | 已验证 | `POST /api/upload/attach` + `<card type="inline" name="file" value="data:...">` | file card 必须包含 `src` 和 `name`;`src` 使用 `/office/...pdf?from=...` 预览 URL,否则阅读页点击会变成 `about:blank` |
|
|
42
|
+
| 音视频 | 下载侧已有部分兼容;写入侧未支持 | 需要真实文档 before/after,确认 card name/value 结构 |
|
|
43
|
+
| 思维导图 | 未支持写入 | 需要手动创建一篇含思维导图的文档,snapshot 分析 card/resource 结构 |
|
|
44
|
+
| 画板 | 未支持写入 | 需要手动创建一篇含画板的文档,snapshot 分析 board/card/resource 结构 |
|
|
45
|
+
| 语雀表格/数据表等复杂卡片 | 未支持写入 | 先只保留、解析、diff,不主动生成 |
|
|
46
|
+
|
|
47
|
+
## 当前写入策略
|
|
48
|
+
|
|
49
|
+
`create-doc` 的默认策略是:
|
|
50
|
+
|
|
51
|
+
1. 使用 Web Session 创建 Markdown 文档,让语雀服务器先做一次 Markdown 到编辑器内容的转换。
|
|
52
|
+
2. 立即 snapshot 新文档,读取语雀生成的 `body_asl`。
|
|
53
|
+
3. 必要时只做经过真实验证的 Lake 变换,例如标题编号 `data-lake-index-type="2"`。
|
|
54
|
+
4. 通过安全 Lake 写入链路写回,生成备份和报告。
|
|
55
|
+
|
|
56
|
+
这样做的原因是:语雀自己的 Markdown 导入器比手写 Lake 更接近编辑器原生结构,尤其是表格、代码块、列表等结构。
|
|
57
|
+
|
|
58
|
+
## 禁止策略
|
|
59
|
+
|
|
60
|
+
- 禁止直接把章节编号写进标题文本。
|
|
61
|
+
- 禁止在未做 snapshot diff 前手写复杂 card。
|
|
62
|
+
- 禁止用 Markdown 覆盖已有 Lake 文档来做批量排版。
|
|
63
|
+
- 禁止静默丢弃图片、附件、思维导图、画板等资源。
|
|
64
|
+
|
|
65
|
+
## 图片和附件探索路线
|
|
66
|
+
|
|
67
|
+
图片、附件要达到原生写入,需要补齐上传链路:
|
|
68
|
+
|
|
69
|
+
1. 用真实测试文档手动插入一张图片和一个附件。
|
|
70
|
+
2. 分别 snapshot before/after,记录 `body_asl` card 结构。
|
|
71
|
+
3. 探索 Web Session 接口,重点参考已调研到的 `/api/upload/attach`。
|
|
72
|
+
4. 实现并真实验证 `upload-attach <doc-url> --file <file>`,只上传并输出返回 JSON,不先写入正文。
|
|
73
|
+
5. `insert-image` 已支持 dry-run 和真实写入。`insert-attachment` 已支持 PDF 附件真实写入,并保留 `downloadUrl/download_url` 供本地下载器使用。
|
|
74
|
+
6. 单篇真实文档 apply 后人工检查。
|
|
75
|
+
|
|
76
|
+
## 思维导图和画板探索路线
|
|
77
|
+
|
|
78
|
+
思维导图、画板属于复杂资源,不应直接猜结构。
|
|
79
|
+
|
|
80
|
+
1. 用户或 AI 在测试知识库中创建包含思维导图/画板的真实文档。
|
|
81
|
+
2. 使用 `snapshot` 保存原始 Lake。
|
|
82
|
+
3. 使用 `diff-lake` 和人工阅读 `body_asl` 分析 card name、value、resource id。
|
|
83
|
+
4. 先实现“识别和保留”,再考虑“创建和更新”。
|
|
84
|
+
5. 如果资源本体不在 `body_asl` 中,需要继续探索资源接口,不能只写 card 壳。
|
|
85
|
+
|
|
86
|
+
## 真实验收记录
|
|
87
|
+
|
|
88
|
+
- `reports/real-ai-use-numbered/snapshot-default-native-lake.json`
|
|
89
|
+
- 验证标题、段落、列表、表格、代码块为 Lake 原生结构。
|
|
90
|
+
- `reports/real-ai-use-numbered/snapshot-lake-native-heading-number.json`
|
|
91
|
+
- 验证标题编号原生写法为 `data-lake-index-type="2"`。
|
|
92
|
+
- `reports/real-ai-use-native-rich/snapshot-native-rich.json`
|
|
93
|
+
- 验证有序列表、嵌套列表、引用、分隔线、加粗、斜体、删除线、普通链接均为 Lake 原生结构。
|
|
94
|
+
- `reports/real-ai-use-native-image/snapshot-after-image.json`
|
|
95
|
+
- 验证本地 PNG 可通过 `/api/upload/attach` 上传,并以原生 image card 写入 Lake。
|
|
96
|
+
- `reports/real-ai-use-native-attachment/snapshot-after-a99-src-attachment.json`
|
|
97
|
+
- 验证本地 PDF 可通过 `/api/upload/attach` 上传,并以 file card 写入 Lake。
|
|
98
|
+
- 关键结论:语雀阅读页 file card 渲染读取 `src + name`,不是 `url/previewUrl`。`src` 必须是 `https://www.yuque.com/office/<filekey>?from=<doc-url>` 形式,才能打开语雀内置 PDF 预览。
|
|
99
|
+
- `downloadUrl/download_url` 仍保留为附件下载地址,供 `download-doc` 资源落盘使用。
|
|
100
|
+
|
|
101
|
+
后续每补齐一种复杂内容,都必须在本文件追加真实验收记录。
|