@aigne/doc-smith 0.9.7 → 0.9.8-alpha.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 +20 -0
- package/agents/create/analyze-diagram-type-llm.yaml +160 -0
- package/agents/create/analyze-diagram-type.mjs +297 -0
- package/agents/create/check-need-generate-structure.mjs +1 -34
- package/agents/create/generate-diagram-image.yaml +60 -0
- package/agents/create/index.yaml +9 -5
- package/agents/create/replace-d2-with-image.mjs +625 -0
- package/agents/create/user-review-document-structure.mjs +8 -7
- package/agents/create/utils/init-current-content.mjs +5 -9
- package/agents/evaluate/document.yaml +6 -0
- package/agents/evaluate/index.yaml +1 -0
- package/agents/init/index.mjs +36 -388
- package/agents/localize/index.yaml +4 -4
- package/agents/media/batch-generate-media-description.yaml +2 -0
- package/agents/media/generate-media-description.yaml +3 -0
- package/agents/media/load-media-description.mjs +44 -15
- package/agents/publish/index.yaml +1 -0
- package/agents/publish/publish-docs.mjs +1 -4
- package/agents/update/check-diagram-flag.mjs +116 -0
- package/agents/update/check-document.mjs +0 -1
- package/agents/update/check-generate-diagram.mjs +48 -30
- package/agents/update/check-sync-image-flag.mjs +55 -0
- package/agents/update/check-update-is-single.mjs +11 -0
- package/agents/update/generate-diagram.yaml +43 -9
- package/agents/update/generate-document.yaml +9 -0
- package/agents/update/handle-document-update.yaml +10 -8
- package/agents/update/index.yaml +25 -7
- package/agents/update/sync-images-and-exit.mjs +148 -0
- package/agents/update/update-single/update-single-document-detail.mjs +131 -17
- package/agents/utils/analyze-feedback-intent.mjs +136 -0
- package/agents/utils/choose-docs.mjs +185 -40
- package/agents/utils/generate-document-or-skip.mjs +41 -0
- package/agents/utils/handle-diagram-operations.mjs +263 -0
- package/agents/utils/load-all-document-content.mjs +30 -0
- package/agents/utils/load-sources.mjs +2 -2
- package/agents/utils/post-generate.mjs +14 -3
- package/agents/utils/read-current-document-content.mjs +46 -0
- package/agents/utils/save-doc-translation.mjs +34 -0
- package/agents/utils/save-doc.mjs +42 -0
- package/agents/utils/save-sidebar.mjs +19 -6
- package/agents/utils/skip-if-content-exists.mjs +27 -0
- package/aigne.yaml +15 -3
- package/assets/report-template/report.html +17 -17
- package/docs-mcp/read-doc-content.mjs +30 -1
- package/package.json +8 -7
- package/prompts/detail/diagram/generate-image-system.md +135 -0
- package/prompts/detail/diagram/generate-image-user.md +32 -0
- package/prompts/detail/generate/user-prompt.md +27 -13
- package/prompts/evaluate/document.md +23 -10
- package/prompts/media/media-description/system-prompt.md +10 -2
- package/prompts/media/media-description/user-prompt.md +9 -0
- package/utils/check-document-has-diagram.mjs +95 -0
- package/utils/constants/index.mjs +46 -0
- package/utils/d2-utils.mjs +119 -178
- package/utils/delete-diagram-images.mjs +99 -0
- package/utils/docs-finder-utils.mjs +133 -25
- package/utils/image-compress.mjs +75 -0
- package/utils/kroki-utils.mjs +2 -3
- package/utils/load-config.mjs +29 -0
- package/utils/sync-diagram-to-translations.mjs +262 -0
- package/utils/utils.mjs +24 -0
- package/agents/create/check-diagram.mjs +0 -40
- package/agents/create/draw-diagram.yaml +0 -27
- package/agents/create/merge-diagram.yaml +0 -39
- package/agents/create/wrap-diagram-code.mjs +0 -35
package/utils/d2-utils.mjs
CHANGED
|
@@ -1,189 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { D2 } from "@terrastruct/d2";
|
|
4
3
|
import fs from "fs-extra";
|
|
5
|
-
import { glob } from "glob";
|
|
6
|
-
import pMap from "p-map";
|
|
7
4
|
|
|
8
|
-
import {
|
|
9
|
-
D2_CONCURRENCY,
|
|
10
|
-
D2_CONFIG,
|
|
11
|
-
DOC_SMITH_DIR,
|
|
12
|
-
FILE_CONCURRENCY,
|
|
13
|
-
TMP_ASSETS_DIR,
|
|
14
|
-
TMP_DIR,
|
|
15
|
-
} from "./constants/index.mjs";
|
|
16
|
-
import { debug } from "./debug.mjs";
|
|
17
|
-
import { iconMap } from "./icon-map.mjs";
|
|
18
|
-
import { getContentHash } from "./utils.mjs";
|
|
5
|
+
import { DOC_SMITH_DIR, TMP_DIR } from "./constants/index.mjs";
|
|
19
6
|
|
|
20
|
-
|
|
7
|
+
// Note: .* matches title or other text after ```d2 (e.g., ```d2 Vault 驗證流程)
|
|
8
|
+
// Export regex for reuse across the codebase to avoid duplication
|
|
9
|
+
export const d2CodeBlockRegex = /```d2.*\n([\s\S]*?)```/g;
|
|
21
10
|
|
|
22
11
|
export const DIAGRAM_PLACEHOLDER = "DIAGRAM_PLACEHOLDER";
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const escapedUrls = iconUrlList.map((url) => url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
28
|
-
const regexPattern = escapedUrls.join("|");
|
|
29
|
-
const regex = new RegExp(regexPattern, "g");
|
|
13
|
+
// Diagram image regex patterns for reuse across the codebase
|
|
14
|
+
// Pattern 1: Match only the start marker (for checking existence)
|
|
15
|
+
export const diagramImageStartRegex = /<!--\s*DIAGRAM_IMAGE_START:[^>]+-->/g;
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const { diagram, renderOptions, graph } = await d2.compile(contentWithBase64Img);
|
|
17
|
+
// Pattern 2: Match full diagram image block without capturing image path (for finding/replacing)
|
|
18
|
+
export const diagramImageBlockRegex =
|
|
19
|
+
/<!--\s*DIAGRAM_IMAGE_START:[^>]+-->\s*[\s\S]*?<!--\s*DIAGRAM_IMAGE_END\s*-->/g;
|
|
36
20
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
graph.root.attributes.shape.value !== "sequence_diagram"
|
|
41
|
-
) {
|
|
42
|
-
// Save the first-level container.
|
|
43
|
-
const firstLevelContainer = new Set();
|
|
44
|
-
diagram.shapes.forEach((x) => {
|
|
45
|
-
const idList = x.id.split(".");
|
|
46
|
-
if (idList.length > 1) {
|
|
47
|
-
const targetShape = diagram.shapes.find((x) => x.id === idList[0]);
|
|
48
|
-
if (targetShape && !["c4-person", "cylinder", "queue"].includes(targetShape.type)) {
|
|
49
|
-
firstLevelContainer.add(targetShape);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
firstLevelContainer.forEach((shape) => {
|
|
54
|
-
if (!shape.strokeDash) {
|
|
55
|
-
// Note: The data structure here is different from the d2 source code.
|
|
56
|
-
shape.strokeDash = 3;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const svg = await d2.render(diagram, renderOptions);
|
|
62
|
-
|
|
63
|
-
return svg;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
if (strict) throw err;
|
|
66
|
-
|
|
67
|
-
console.error("Failed to generate D2 diagram. Content:", content, "Error:", err);
|
|
68
|
-
return null;
|
|
69
|
-
} finally {
|
|
70
|
-
d2.worker.terminate();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function saveAssets({ markdown, docsDir }) {
|
|
75
|
-
if (!markdown) {
|
|
76
|
-
return markdown;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const { replaced } = await runIterator({
|
|
80
|
-
input: markdown,
|
|
81
|
-
regexp: codeBlockRegex,
|
|
82
|
-
replace: true,
|
|
83
|
-
fn: async ([_match, _code]) => {
|
|
84
|
-
const assetDir = path.join(docsDir, "../", TMP_ASSETS_DIR, "d2");
|
|
85
|
-
await fs.ensureDir(assetDir);
|
|
86
|
-
const d2Content = [D2_CONFIG, _code].join("\n");
|
|
87
|
-
const fileName = `${getContentHash(d2Content)}.svg`;
|
|
88
|
-
const svgPath = path.join(assetDir, fileName);
|
|
89
|
-
|
|
90
|
-
if (await fs.pathExists(svgPath)) {
|
|
91
|
-
debug("Asset cache found, skipping generation", svgPath);
|
|
92
|
-
} else {
|
|
93
|
-
try {
|
|
94
|
-
debug("Generating d2 diagram", svgPath);
|
|
95
|
-
if (debug.enabled) {
|
|
96
|
-
const d2FileName = `${getContentHash(d2Content)}.d2`;
|
|
97
|
-
const d2Path = path.join(assetDir, d2FileName);
|
|
98
|
-
await fs.writeFile(d2Path, d2Content, { encoding: "utf8" });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const svg = await getChart({ content: d2Content });
|
|
102
|
-
if (svg) {
|
|
103
|
-
await fs.writeFile(svgPath, svg, { encoding: "utf8" });
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
debug("Failed to generate D2 diagram. Content:", d2Content, "Error:", error);
|
|
107
|
-
return _code;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return `})`;
|
|
111
|
-
},
|
|
112
|
-
options: { concurrency: D2_CONCURRENCY },
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return replaced;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export async function beforePublishHook({ docsDir }) {
|
|
119
|
-
// Process each markdown file to save d2 svg assets.
|
|
120
|
-
const mdFilePaths = await glob("**/*.md", { cwd: docsDir });
|
|
121
|
-
await pMap(
|
|
122
|
-
mdFilePaths,
|
|
123
|
-
async (filePath) => {
|
|
124
|
-
let finalContent = await fs.readFile(path.join(docsDir, filePath), { encoding: "utf8" });
|
|
125
|
-
finalContent = await saveAssets({ markdown: finalContent, docsDir });
|
|
126
|
-
|
|
127
|
-
await fs.writeFile(path.join(docsDir, filePath), finalContent, { encoding: "utf8" });
|
|
128
|
-
},
|
|
129
|
-
{ concurrency: FILE_CONCURRENCY },
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function runIterator({ input, regexp, fn = () => {}, options, replace = false }) {
|
|
134
|
-
if (!input) return input;
|
|
135
|
-
const matches = [...input.matchAll(regexp)];
|
|
136
|
-
const results = [];
|
|
137
|
-
await pMap(
|
|
138
|
-
matches,
|
|
139
|
-
async (...args) => {
|
|
140
|
-
const resultItem = await fn(...args);
|
|
141
|
-
results.push(resultItem);
|
|
142
|
-
},
|
|
143
|
-
options,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
let replaced = input;
|
|
147
|
-
if (replace) {
|
|
148
|
-
let index = 0;
|
|
149
|
-
replaced = replaced.replace(regexp, () => {
|
|
150
|
-
return results[index++];
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
results,
|
|
156
|
-
replaced,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function checkContent({ content: _content }) {
|
|
161
|
-
const matches = Array.from(_content.matchAll(codeBlockRegex));
|
|
162
|
-
let content = _content;
|
|
163
|
-
if (matches.length > 0) {
|
|
164
|
-
content = matches[0][1];
|
|
165
|
-
}
|
|
166
|
-
await ensureTmpDir();
|
|
167
|
-
const assetDir = path.join(DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "d2");
|
|
168
|
-
await fs.ensureDir(assetDir);
|
|
169
|
-
const d2Content = [D2_CONFIG, content].join("\n");
|
|
170
|
-
const fileName = `${getContentHash(d2Content)}.svg`;
|
|
171
|
-
const svgPath = path.join(assetDir, fileName);
|
|
172
|
-
|
|
173
|
-
if (debug.enabled) {
|
|
174
|
-
const d2FileName = `${getContentHash(d2Content)}.d2`;
|
|
175
|
-
const d2Path = path.join(assetDir, d2FileName);
|
|
176
|
-
await fs.writeFile(d2Path, d2Content, { encoding: "utf8" });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (await fs.pathExists(svgPath)) {
|
|
180
|
-
debug("Asset cache found, skipping generation", svgPath);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const svg = await getChart({ content: d2Content, strict: true });
|
|
185
|
-
await fs.writeFile(svgPath, svg, { encoding: "utf8" });
|
|
186
|
-
}
|
|
21
|
+
// Pattern 3: Match full diagram image block with image path capture (for extracting paths)
|
|
22
|
+
export const diagramImageWithPathRegex =
|
|
23
|
+
/<!--\s*DIAGRAM_IMAGE_START:[^>]+-->\s*!\[[^\]]*\]\(([^)]+)\)\s*<!--\s*DIAGRAM_IMAGE_END\s*-->/g;
|
|
187
24
|
|
|
188
25
|
export async function ensureTmpDir() {
|
|
189
26
|
const tmpDir = path.join(DOC_SMITH_DIR, TMP_DIR);
|
|
@@ -198,7 +35,7 @@ export function isValidCode(lang) {
|
|
|
198
35
|
}
|
|
199
36
|
|
|
200
37
|
export function wrapCode({ content }) {
|
|
201
|
-
const matches = Array.from(content.matchAll(
|
|
38
|
+
const matches = Array.from(content.matchAll(d2CodeBlockRegex));
|
|
202
39
|
if (matches.length > 0) {
|
|
203
40
|
return content;
|
|
204
41
|
}
|
|
@@ -212,7 +49,7 @@ export function wrapCode({ content }) {
|
|
|
212
49
|
* @returns {Array} - [contentWithPlaceholder, originalCodeBlock]
|
|
213
50
|
*/
|
|
214
51
|
export function replaceD2WithPlaceholder({ content }) {
|
|
215
|
-
const [firstMatch] = Array.from(content.matchAll(
|
|
52
|
+
const [firstMatch] = Array.from(content.matchAll(d2CodeBlockRegex));
|
|
216
53
|
if (firstMatch) {
|
|
217
54
|
const matchContent = firstMatch[0];
|
|
218
55
|
const cleanContent = content.replace(matchContent, DIAGRAM_PLACEHOLDER);
|
|
@@ -255,3 +92,107 @@ export function replacePlaceholderWithD2({ content, diagramSourceCode }) {
|
|
|
255
92
|
|
|
256
93
|
return content.replace(DIAGRAM_PLACEHOLDER, replacement);
|
|
257
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Replace all diagrams (D2 code blocks and generated images) with DIAGRAM_PLACEHOLDER
|
|
98
|
+
* Used for deletion operations to normalize all diagram types to a single placeholder
|
|
99
|
+
* @param {string} content - Document content containing diagrams
|
|
100
|
+
* @param {number} [diagramIndex] - Optional index of diagram to replace (0-based). If not provided, replaces all diagrams.
|
|
101
|
+
* @returns {string} - Content with diagrams replaced by DIAGRAM_PLACEHOLDER
|
|
102
|
+
*/
|
|
103
|
+
export function replaceDiagramsWithPlaceholder({ content, diagramIndex }) {
|
|
104
|
+
if (!content) {
|
|
105
|
+
return content;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use exported regex to find all diagram locations
|
|
109
|
+
// We'll use a similar approach to findAllDiagramLocations
|
|
110
|
+
const mermaidCodeBlockRegex = /```mermaid.*\n([\s\S]*?)```/g;
|
|
111
|
+
|
|
112
|
+
// Find all diagram locations
|
|
113
|
+
const locations = [];
|
|
114
|
+
|
|
115
|
+
// 1. Find DIAGRAM_PLACEHOLDER (already a placeholder, keep as is)
|
|
116
|
+
let placeholderIndex = content.indexOf(DIAGRAM_PLACEHOLDER);
|
|
117
|
+
while (placeholderIndex !== -1) {
|
|
118
|
+
locations.push({
|
|
119
|
+
type: "placeholder",
|
|
120
|
+
start: placeholderIndex,
|
|
121
|
+
end: placeholderIndex + DIAGRAM_PLACEHOLDER.length,
|
|
122
|
+
});
|
|
123
|
+
placeholderIndex = content.indexOf(DIAGRAM_PLACEHOLDER, placeholderIndex + 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Find DIAGRAM_IMAGE_START markers (generated images)
|
|
127
|
+
const imageMatches = Array.from(content.matchAll(diagramImageBlockRegex));
|
|
128
|
+
for (const match of imageMatches) {
|
|
129
|
+
locations.push({
|
|
130
|
+
type: "image",
|
|
131
|
+
start: match.index,
|
|
132
|
+
end: match.index + match[0].length,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Find D2 code blocks
|
|
137
|
+
const d2Matches = Array.from(content.matchAll(d2CodeBlockRegex));
|
|
138
|
+
for (const match of d2Matches) {
|
|
139
|
+
locations.push({
|
|
140
|
+
type: "d2",
|
|
141
|
+
start: match.index,
|
|
142
|
+
end: match.index + match[0].length,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 4. Find Mermaid code blocks
|
|
147
|
+
const mermaidMatches = Array.from(content.matchAll(mermaidCodeBlockRegex));
|
|
148
|
+
for (const match of mermaidMatches) {
|
|
149
|
+
locations.push({
|
|
150
|
+
type: "mermaid",
|
|
151
|
+
start: match.index,
|
|
152
|
+
end: match.index + match[0].length,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sort by position (top to bottom)
|
|
157
|
+
locations.sort((a, b) => a.start - b.start);
|
|
158
|
+
|
|
159
|
+
if (locations.length === 0) {
|
|
160
|
+
return content;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// If diagramIndex is provided, only replace that specific diagram
|
|
164
|
+
if (diagramIndex !== undefined && diagramIndex >= 0 && diagramIndex < locations.length) {
|
|
165
|
+
const targetLocation = locations[diagramIndex];
|
|
166
|
+
const before = content.substring(0, targetLocation.start);
|
|
167
|
+
const after = content.substring(targetLocation.end);
|
|
168
|
+
// Add newlines if needed
|
|
169
|
+
let replacement = DIAGRAM_PLACEHOLDER;
|
|
170
|
+
if (before && !before.endsWith("\n")) {
|
|
171
|
+
replacement = `\n${replacement}`;
|
|
172
|
+
}
|
|
173
|
+
if (after && !after.startsWith("\n")) {
|
|
174
|
+
replacement = `${replacement}\n`;
|
|
175
|
+
}
|
|
176
|
+
return before + replacement + after;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Replace all diagrams with placeholder (for deletion)
|
|
180
|
+
// Process from end to start to preserve indices
|
|
181
|
+
let result = content;
|
|
182
|
+
for (let i = locations.length - 1; i >= 0; i--) {
|
|
183
|
+
const location = locations[i];
|
|
184
|
+
const before = result.substring(0, location.start);
|
|
185
|
+
const after = result.substring(location.end);
|
|
186
|
+
// Add newlines if needed
|
|
187
|
+
let replacement = DIAGRAM_PLACEHOLDER;
|
|
188
|
+
if (before && !before.endsWith("\n")) {
|
|
189
|
+
replacement = `\n${replacement}`;
|
|
190
|
+
}
|
|
191
|
+
if (after && !after.startsWith("\n")) {
|
|
192
|
+
replacement = `${replacement}\n`;
|
|
193
|
+
}
|
|
194
|
+
result = before + replacement + after;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { unlink } from "node:fs/promises";
|
|
2
|
+
import { join, dirname, normalize } from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { debug } from "./debug.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract image file paths from markdown content
|
|
8
|
+
* Finds all diagram image references and extracts their file paths
|
|
9
|
+
* @param {string} content - Markdown content
|
|
10
|
+
* @param {string} path - Document path (e.g., "guides/getting-started.md")
|
|
11
|
+
* @param {string} docsDir - Documentation directory
|
|
12
|
+
* @returns {Promise<Array<string>>} Array of absolute paths to image files
|
|
13
|
+
*/
|
|
14
|
+
export async function extractDiagramImagePaths(content, path, docsDir) {
|
|
15
|
+
if (!content || !path || !docsDir) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const imagePaths = [];
|
|
20
|
+
|
|
21
|
+
// Pattern to match: <!-- DIAGRAM_IMAGE_START:... --><!-- DIAGRAM_IMAGE_END -->
|
|
22
|
+
const { diagramImageWithPathRegex } = await import("./d2-utils.mjs");
|
|
23
|
+
const matches = Array.from(content.matchAll(diagramImageWithPathRegex));
|
|
24
|
+
|
|
25
|
+
for (const match of matches) {
|
|
26
|
+
const imagePath = match[1];
|
|
27
|
+
|
|
28
|
+
// Resolve absolute path
|
|
29
|
+
// If imagePath is relative, resolve from document location
|
|
30
|
+
// If imagePath is absolute or starts with http, skip
|
|
31
|
+
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
32
|
+
continue; // Skip remote URLs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Calculate relative path from document to image
|
|
36
|
+
const docDir = dirname(path);
|
|
37
|
+
const imageRelativePath = imagePath.startsWith("../")
|
|
38
|
+
? imagePath
|
|
39
|
+
: join(docDir, imagePath).replace(/\\/g, "/");
|
|
40
|
+
|
|
41
|
+
// Resolve absolute path
|
|
42
|
+
const absolutePath = join(process.cwd(), docsDir, imageRelativePath);
|
|
43
|
+
|
|
44
|
+
// Normalize path (remove .. and .)
|
|
45
|
+
const normalizedPath = normalize(absolutePath);
|
|
46
|
+
|
|
47
|
+
if (await fs.pathExists(normalizedPath)) {
|
|
48
|
+
imagePaths.push(normalizedPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return imagePaths;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Delete diagram image files associated with a document
|
|
57
|
+
* @param {string} content - Markdown content (before deletion)
|
|
58
|
+
* @param {string} path - Document path
|
|
59
|
+
* @param {string} docsDir - Documentation directory
|
|
60
|
+
* @returns {Promise<{deleted: number, failed: number}>}
|
|
61
|
+
*/
|
|
62
|
+
export async function deleteDiagramImages(content, path, docsDir) {
|
|
63
|
+
if (!content || !path || !docsDir) {
|
|
64
|
+
return { deleted: 0, failed: 0 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const imagePaths = await extractDiagramImagePaths(content, path, docsDir);
|
|
69
|
+
|
|
70
|
+
if (imagePaths.length === 0) {
|
|
71
|
+
return { deleted: 0, failed: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let deleted = 0;
|
|
75
|
+
let failed = 0;
|
|
76
|
+
|
|
77
|
+
for (const imagePath of imagePaths) {
|
|
78
|
+
try {
|
|
79
|
+
await unlink(imagePath);
|
|
80
|
+
debug(`Deleted diagram image: ${imagePath}`);
|
|
81
|
+
deleted++;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code !== "ENOENT") {
|
|
84
|
+
// File not found is ok, other errors should be logged
|
|
85
|
+
console.warn(`Failed to delete diagram image ${imagePath}: ${error.message}`);
|
|
86
|
+
failed++;
|
|
87
|
+
} else {
|
|
88
|
+
// File already doesn't exist, count as deleted
|
|
89
|
+
deleted++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { deleted, failed };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn(`Error deleting diagram images: ${error.message}`);
|
|
97
|
+
return { deleted: 0, failed: 0 };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -2,6 +2,7 @@ import { access, readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import pLimit from "p-limit";
|
|
5
|
+
import yaml from "yaml";
|
|
5
6
|
import { pathExists } from "./file-utils.mjs";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -107,10 +108,43 @@ export async function findItemByPath(documentStructure, docPath, boardId, docsDi
|
|
|
107
108
|
* @param {string} fileName - File name to read
|
|
108
109
|
* @returns {Promise<string|null>} File content or null if failed
|
|
109
110
|
*/
|
|
111
|
+
/**
|
|
112
|
+
* Remove base64 encoded images from markdown content
|
|
113
|
+
* This prevents large binary data from being included in document content
|
|
114
|
+
* Base64 images are completely removed (not replaced with placeholders) because:
|
|
115
|
+
* 1. They significantly increase token usage without providing useful information to LLM
|
|
116
|
+
* 2. Normal image references (file paths) are preserved and should be used instead
|
|
117
|
+
* 3. Base64 images are typically temporary or erroneous entries
|
|
118
|
+
* @param {string} content - Markdown content that may contain base64 images
|
|
119
|
+
* @returns {string} - Content with base64 images completely removed
|
|
120
|
+
*/
|
|
121
|
+
function removeBase64Images(content) {
|
|
122
|
+
if (!content || typeof content !== "string") {
|
|
123
|
+
return content;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Match markdown image syntax with data URLs: 
|
|
127
|
+
// This regex matches:
|
|
128
|
+
// - 
|
|
129
|
+
// - 
|
|
130
|
+
// - [](link)
|
|
131
|
+
const base64ImageRegex = /!\[([^\]]*)\]\(data:image\/[^)]+\)/g;
|
|
132
|
+
|
|
133
|
+
// Completely remove base64 images (including the entire markdown image syntax)
|
|
134
|
+
// This maximizes token reduction while preserving normal image references
|
|
135
|
+
const cleanedContent = content.replace(base64ImageRegex, "");
|
|
136
|
+
|
|
137
|
+
return cleanedContent;
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
export async function readFileContent(docsDir, fileName) {
|
|
111
141
|
try {
|
|
112
142
|
const filePath = join(docsDir, fileName);
|
|
113
|
-
|
|
143
|
+
const content = await readFile(filePath, "utf-8");
|
|
144
|
+
|
|
145
|
+
// Remove base64 encoded images to reduce token usage
|
|
146
|
+
// Base64 image data is not useful for LLM processing and significantly increases token count
|
|
147
|
+
return removeBase64Images(content);
|
|
114
148
|
} catch (readError) {
|
|
115
149
|
console.warn(`⚠️ Could not read content from ${fileName}:`, readError.message);
|
|
116
150
|
return null;
|
|
@@ -146,7 +180,8 @@ export async function getMainLanguageFiles(docsDir, locale, documentStructure =
|
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
// If main language is English, return files without language suffix
|
|
149
|
-
|
|
183
|
+
// FIXME: 临时修改为 zh,后续需要优化
|
|
184
|
+
if (locale === "zh") {
|
|
150
185
|
// Return files that don't have language suffixes (e.g., overview.md, not overview.zh.md)
|
|
151
186
|
return !file.match(/\.\w+(-\w+)?\.md$/);
|
|
152
187
|
} else {
|
|
@@ -274,8 +309,58 @@ export function addFeedbackToItems(items, feedback) {
|
|
|
274
309
|
}
|
|
275
310
|
|
|
276
311
|
/**
|
|
277
|
-
*
|
|
278
|
-
* @param {
|
|
312
|
+
* Convert YAML document structure to flat array format
|
|
313
|
+
* @param {Object} yamlData - Parsed YAML data with documents array
|
|
314
|
+
* @returns {Array} Flat array of document structure items
|
|
315
|
+
*/
|
|
316
|
+
function convertYamlToStructure(yamlData) {
|
|
317
|
+
const result = [];
|
|
318
|
+
|
|
319
|
+
function flattenDocuments(documents, parentId = null) {
|
|
320
|
+
if (!Array.isArray(documents)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const doc of documents) {
|
|
325
|
+
if (!doc.path || !doc.title) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Create structure item
|
|
330
|
+
const item = {
|
|
331
|
+
title: doc.title,
|
|
332
|
+
description: doc.description || "",
|
|
333
|
+
path: doc.path,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
if (parentId) {
|
|
337
|
+
item.parentId = parentId;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (doc.icon) {
|
|
341
|
+
item.icon = doc.icon;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (doc.sourcePaths) {
|
|
345
|
+
item.sourcePaths = doc.sourcePaths;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
result.push(item);
|
|
349
|
+
|
|
350
|
+
// Recursively process children
|
|
351
|
+
if (doc.children && Array.isArray(doc.children)) {
|
|
352
|
+
flattenDocuments(doc.children, doc.path);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
flattenDocuments(yamlData.documents);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Load document execution structure from structure-plan.json or document_structure.yaml
|
|
363
|
+
* @param {string} outputDir - Output directory containing structure files
|
|
279
364
|
* @returns {Promise<Array|null>} Document execution structure array or null if not found/failed
|
|
280
365
|
*/
|
|
281
366
|
export async function loadDocumentStructure(outputDir) {
|
|
@@ -283,41 +368,64 @@ export async function loadDocumentStructure(outputDir) {
|
|
|
283
368
|
return null;
|
|
284
369
|
}
|
|
285
370
|
|
|
371
|
+
// Try loading structure-plan.json first
|
|
286
372
|
try {
|
|
287
373
|
const structurePlanPath = join(outputDir, "structure-plan.json");
|
|
288
374
|
const structureExists = await pathExists(structurePlanPath);
|
|
289
375
|
|
|
290
|
-
if (
|
|
291
|
-
|
|
376
|
+
if (structureExists) {
|
|
377
|
+
const structureContent = await readFile(structurePlanPath, "utf8");
|
|
378
|
+
if (structureContent?.trim()) {
|
|
379
|
+
try {
|
|
380
|
+
// Validate that the content looks like JSON before parsing
|
|
381
|
+
const trimmedContent = structureContent.trim();
|
|
382
|
+
if (trimmedContent.startsWith("[") || trimmedContent.startsWith("{")) {
|
|
383
|
+
const parsed = JSON.parse(structureContent);
|
|
384
|
+
// Return array if it's an array, otherwise return null
|
|
385
|
+
if (Array.isArray(parsed)) {
|
|
386
|
+
return parsed;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
console.warn("structure-plan.json contains non-JSON content, skipping parse");
|
|
390
|
+
}
|
|
391
|
+
} catch (parseError) {
|
|
392
|
+
console.error(`Failed to parse structure-plan.json: ${parseError.message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
292
395
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
|
|
396
|
+
} catch (readError) {
|
|
397
|
+
// Only warn if it's not a "file not found" error
|
|
398
|
+
if (readError.code !== "ENOENT") {
|
|
399
|
+
console.warn(`Error reading structure-plan.json: ${readError.message}`);
|
|
297
400
|
}
|
|
401
|
+
}
|
|
298
402
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
403
|
+
// Try loading document_structure.yaml as fallback
|
|
404
|
+
try {
|
|
405
|
+
const yamlPath = join(outputDir, "document_structure.yaml");
|
|
406
|
+
const yamlExists = await pathExists(yamlPath);
|
|
407
|
+
|
|
408
|
+
if (yamlExists) {
|
|
409
|
+
const yamlContent = await readFile(yamlPath, "utf8");
|
|
410
|
+
if (yamlContent?.trim()) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = yaml.parse(yamlContent);
|
|
413
|
+
if (parsed && parsed.documents) {
|
|
414
|
+
return convertYamlToStructure(parsed);
|
|
415
|
+
}
|
|
416
|
+
} catch (parseError) {
|
|
417
|
+
console.error(`Failed to parse document_structure.yaml: ${parseError.message}`);
|
|
418
|
+
}
|
|
305
419
|
}
|
|
306
|
-
|
|
307
|
-
const parsed = JSON.parse(structureContent);
|
|
308
|
-
// Return array if it's an array, otherwise return null
|
|
309
|
-
return Array.isArray(parsed) ? parsed : null;
|
|
310
|
-
} catch (parseError) {
|
|
311
|
-
console.error(`Failed to parse structure-plan.json: ${parseError.message}`);
|
|
312
|
-
return null;
|
|
313
420
|
}
|
|
314
421
|
} catch (readError) {
|
|
315
422
|
// Only warn if it's not a "file not found" error
|
|
316
423
|
if (readError.code !== "ENOENT") {
|
|
317
|
-
console.warn(`Error reading
|
|
424
|
+
console.warn(`Error reading document_structure.yaml: ${readError.message}`);
|
|
318
425
|
}
|
|
319
|
-
return null;
|
|
320
426
|
}
|
|
427
|
+
|
|
428
|
+
return null;
|
|
321
429
|
}
|
|
322
430
|
|
|
323
431
|
/**
|