@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
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { copyFile, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
DIAGRAM_PLACEHOLDER,
|
|
7
|
+
d2CodeBlockRegex,
|
|
8
|
+
diagramImageBlockRegex,
|
|
9
|
+
ensureTmpDir,
|
|
10
|
+
} from "../../utils/d2-utils.mjs";
|
|
11
|
+
import { DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR } from "../../utils/constants/index.mjs";
|
|
12
|
+
import { getContentHash, getFileName } from "../../utils/utils.mjs";
|
|
13
|
+
import { getExtnameFromContentType } from "../../utils/file-utils.mjs";
|
|
14
|
+
import { debug } from "../../utils/debug.mjs";
|
|
15
|
+
import { compressImage } from "../../utils/image-compress.mjs";
|
|
16
|
+
|
|
17
|
+
const SIZE_THRESHOLD = 1 * 1024 * 1024; // 1MB
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate hash for an image file
|
|
21
|
+
* For files < 1MB: use file content
|
|
22
|
+
* For files >= 1MB: use path + size + mtime to avoid memory issues
|
|
23
|
+
* @param {string} absolutePath - The absolute path to the image file
|
|
24
|
+
* @returns {Promise<string>} - The hash of the file
|
|
25
|
+
*/
|
|
26
|
+
async function calculateImageHash(absolutePath) {
|
|
27
|
+
const stats = await stat(absolutePath);
|
|
28
|
+
|
|
29
|
+
if (stats.size < SIZE_THRESHOLD) {
|
|
30
|
+
// Small file: use full content
|
|
31
|
+
const content = await readFile(absolutePath);
|
|
32
|
+
return createHash("sha256").update(content).digest("hex");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Large file: use path + size + mtime
|
|
36
|
+
const hashInput = `${absolutePath}:${stats.size}:${stats.mtimeMs}`;
|
|
37
|
+
return createHash("sha256").update(hashInput).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Replace D2 code blocks with generated image in document content
|
|
42
|
+
* This mimics the @image insertion pattern
|
|
43
|
+
* Saves images to assets/diagram (relative to docsDir) and replaces DIAGRAM_PLACEHOLDER with image reference
|
|
44
|
+
*
|
|
45
|
+
* File naming: {documentFileNameWithoutExt}-{index:02d}.{ext} (e.g., "guides-getting-started-01.jpg")
|
|
46
|
+
* - Uses getFileName() to get the actual document filename, ensuring exact match
|
|
47
|
+
* - Example: document "guides-getting-started.md" → diagram "guides-getting-started-01.jpg"
|
|
48
|
+
* - Uses 2-digit zero-padded sequential numbering (01, 02, 03...) for easy sorting
|
|
49
|
+
* - Same document + same position = same filename (overwrites on update)
|
|
50
|
+
* - File name matches document filename (without extension) for easy tracking and identification
|
|
51
|
+
*
|
|
52
|
+
* Note: Images are saved immediately during replacement to ensure they exist before document save.
|
|
53
|
+
* This is necessary because the image path is embedded in the document content.
|
|
54
|
+
*/
|
|
55
|
+
export default async function replaceD2WithImage({
|
|
56
|
+
imageResult,
|
|
57
|
+
images,
|
|
58
|
+
content,
|
|
59
|
+
documentContent,
|
|
60
|
+
diagramType,
|
|
61
|
+
aspectRatio,
|
|
62
|
+
diagramIndex,
|
|
63
|
+
originalContent,
|
|
64
|
+
feedback,
|
|
65
|
+
path: docPath,
|
|
66
|
+
docsDir,
|
|
67
|
+
locale: inputLocale,
|
|
68
|
+
}) {
|
|
69
|
+
// Extract locale from imageResult if not provided directly
|
|
70
|
+
// imageResult contains all input parameters when include_input_in_output: true
|
|
71
|
+
const locale = inputLocale || imageResult?.locale || "en";
|
|
72
|
+
|
|
73
|
+
// Extract path and docsDir from imageResult if not provided directly
|
|
74
|
+
const finalDocPath = docPath || imageResult?.path;
|
|
75
|
+
const finalDocsDir = docsDir || imageResult?.docsDir;
|
|
76
|
+
|
|
77
|
+
// Determine which content to use for finding diagrams and final replacement
|
|
78
|
+
// Priority:
|
|
79
|
+
// 1. documentContent (may contain DIAGRAM_PLACEHOLDER from replaceD2WithPlaceholder)
|
|
80
|
+
// 2. originalContent (for finding existing diagrams when updating)
|
|
81
|
+
// 3. content (fallback)
|
|
82
|
+
const contentForFindingDiagrams = originalContent || documentContent || content || "";
|
|
83
|
+
// For final content, prefer documentContent first (may have placeholder), then originalContent
|
|
84
|
+
let finalContent = documentContent || originalContent || content || "";
|
|
85
|
+
|
|
86
|
+
// Extract diagram index from feedback if not explicitly provided
|
|
87
|
+
let targetDiagramIndex = diagramIndex;
|
|
88
|
+
if (targetDiagramIndex === undefined && feedback) {
|
|
89
|
+
const extractedIndex = extractDiagramIndexFromFeedback(feedback);
|
|
90
|
+
if (extractedIndex !== null) {
|
|
91
|
+
targetDiagramIndex = extractedIndex;
|
|
92
|
+
debug(`Extracted diagram index ${targetDiagramIndex} from feedback: "${feedback}"`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Extract image from the image generation result
|
|
97
|
+
// In team agent, image agent output is merged into the context
|
|
98
|
+
// So we need to check both imageResult and direct images field
|
|
99
|
+
let image = null;
|
|
100
|
+
|
|
101
|
+
// First check if images array is directly available (from image agent output)
|
|
102
|
+
// Image agent outputs: { images: [{ filename, mimeType, type: "local", path }], ... }
|
|
103
|
+
if (images && Array.isArray(images) && images.length > 0) {
|
|
104
|
+
image = images[0];
|
|
105
|
+
}
|
|
106
|
+
// Then check imageResult (might be the whole output object)
|
|
107
|
+
else if (imageResult) {
|
|
108
|
+
// Check for images array (from image generation agents)
|
|
109
|
+
if (imageResult.images && Array.isArray(imageResult.images) && imageResult.images.length > 0) {
|
|
110
|
+
image = imageResult.images[0];
|
|
111
|
+
}
|
|
112
|
+
// Fallback to old format
|
|
113
|
+
else if (imageResult.imageUrl || imageResult.image || imageResult.url || imageResult.path) {
|
|
114
|
+
image = {
|
|
115
|
+
path: imageResult.imageUrl || imageResult.image || imageResult.url || imageResult.path,
|
|
116
|
+
filename: path.basename(
|
|
117
|
+
imageResult.imageUrl || imageResult.image || imageResult.url || imageResult.path,
|
|
118
|
+
),
|
|
119
|
+
mimeType: imageResult.mimeType || "image/jpeg",
|
|
120
|
+
type: "local",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Check nested output
|
|
124
|
+
else if (imageResult.output) {
|
|
125
|
+
if (
|
|
126
|
+
imageResult.output.images &&
|
|
127
|
+
Array.isArray(imageResult.output.images) &&
|
|
128
|
+
imageResult.output.images.length > 0
|
|
129
|
+
) {
|
|
130
|
+
image = imageResult.output.images[0];
|
|
131
|
+
} else if (
|
|
132
|
+
imageResult.output.imageUrl ||
|
|
133
|
+
imageResult.output.image ||
|
|
134
|
+
imageResult.output.url
|
|
135
|
+
) {
|
|
136
|
+
image = {
|
|
137
|
+
path: imageResult.output.imageUrl || imageResult.output.image || imageResult.output.url,
|
|
138
|
+
filename: path.basename(
|
|
139
|
+
imageResult.output.imageUrl || imageResult.output.image || imageResult.output.url,
|
|
140
|
+
),
|
|
141
|
+
mimeType: imageResult.output.mimeType || "image/jpeg",
|
|
142
|
+
type: "local",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!image || !image.path || image.type !== "local") {
|
|
149
|
+
// Debug: log what we received to help diagnose the issue
|
|
150
|
+
debug("⚠️ No valid image found in replace-d2-with-image.mjs");
|
|
151
|
+
debug(
|
|
152
|
+
" - images:",
|
|
153
|
+
images ? `${Array.isArray(images) ? images.length : "not array"} items` : "undefined",
|
|
154
|
+
);
|
|
155
|
+
debug(" - imageResult:", imageResult ? Object.keys(imageResult).join(", ") : "undefined");
|
|
156
|
+
debug(
|
|
157
|
+
" - documentContent contains DIAGRAM_PLACEHOLDER:",
|
|
158
|
+
finalContent.includes("DIAGRAM_PLACEHOLDER"),
|
|
159
|
+
);
|
|
160
|
+
// If no image, return content as-is (keep D2 code blocks or placeholder)
|
|
161
|
+
return { content: finalContent };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Determine asset directory: assets/diagram (relative to docsDir, not in .tmp)
|
|
165
|
+
// If docsDir is provided, use it; otherwise fallback to .tmp/assets/diagram for backward compatibility
|
|
166
|
+
let assetDir;
|
|
167
|
+
let relativePathPrefix;
|
|
168
|
+
|
|
169
|
+
if (finalDocsDir) {
|
|
170
|
+
// New approach: save to assets/diagram relative to docsDir (can be committed to git)
|
|
171
|
+
assetDir = path.join(process.cwd(), finalDocsDir, "assets", "diagram");
|
|
172
|
+
relativePathPrefix = "assets/diagram";
|
|
173
|
+
} else {
|
|
174
|
+
// Fallback: use .tmp/assets/diagram for backward compatibility
|
|
175
|
+
await ensureTmpDir();
|
|
176
|
+
assetDir = path.join(process.cwd(), DOC_SMITH_DIR, TMP_DIR, TMP_ASSETS_DIR, "diagram");
|
|
177
|
+
relativePathPrefix = path.posix.join("..", TMP_DIR, TMP_ASSETS_DIR, "diagram");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
await fs.ensureDir(assetDir);
|
|
181
|
+
|
|
182
|
+
// Get file extension from source path
|
|
183
|
+
let ext = path.extname(image.path);
|
|
184
|
+
|
|
185
|
+
// If no extension found, try to determine from mimeType
|
|
186
|
+
if (!ext && image.mimeType) {
|
|
187
|
+
const extFromMime = getExtnameFromContentType(image.mimeType);
|
|
188
|
+
if (extFromMime) {
|
|
189
|
+
ext = `.${extFromMime}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Ensure we have a file extension
|
|
194
|
+
if (!ext) {
|
|
195
|
+
console.warn(
|
|
196
|
+
`Could not determine file extension for diagram image from ${image.path} - using .jpg as fallback`,
|
|
197
|
+
);
|
|
198
|
+
ext = ".jpg";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find all diagram locations to determine the target index
|
|
202
|
+
const diagramLocations = findAllDiagramLocations(contentForFindingDiagrams);
|
|
203
|
+
let targetIndex = targetDiagramIndex !== undefined ? targetDiagramIndex : 0;
|
|
204
|
+
|
|
205
|
+
if (targetIndex < 0 || targetIndex >= diagramLocations.length) {
|
|
206
|
+
// If index is out of range, use the next available index (for new diagrams)
|
|
207
|
+
targetIndex = diagramLocations.length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Generate filename based on document name and diagram index
|
|
211
|
+
// Format: {flatDocumentName}-{index:02d}.{ext} (e.g., "guides-getting-started-01.jpg")
|
|
212
|
+
// This ensures:
|
|
213
|
+
// - Same document + same position = same filename (overwrite on update)
|
|
214
|
+
// - Different documents = different filenames
|
|
215
|
+
// - Different positions in same document = different filenames
|
|
216
|
+
// - File name matches the actual document filename (without extension) for easy tracking
|
|
217
|
+
// - Sequential numbering (01, 02, 03...) for easy sorting and identification
|
|
218
|
+
let fileName;
|
|
219
|
+
|
|
220
|
+
if (finalDocPath) {
|
|
221
|
+
// Use getFileName() to get the actual document filename, then remove extension
|
|
222
|
+
// This ensures the diagram filename exactly matches the document filename format
|
|
223
|
+
// Example: docPath "guides/getting-started" + locale "en" -> "guides-getting-started.md"
|
|
224
|
+
// Remove .md extension -> "guides-getting-started"
|
|
225
|
+
const documentFileName = getFileName(finalDocPath, locale);
|
|
226
|
+
const documentNameWithoutExt = documentFileName.replace(/\.(md|markdown)$/i, "");
|
|
227
|
+
|
|
228
|
+
// Format: {documentNameWithoutExt}-{index:02d}.{ext}
|
|
229
|
+
// Example: guides-getting-started-01.jpg, guides-getting-started-02.jpg
|
|
230
|
+
// Using 2-digit zero-padded index for better sorting and readability
|
|
231
|
+
const indexStr = String(targetIndex + 1).padStart(2, "0"); // Convert 0-based to 1-based, pad to 2 digits
|
|
232
|
+
fileName = `${documentNameWithoutExt}-${indexStr}${ext}`;
|
|
233
|
+
} else {
|
|
234
|
+
// Fallback: use hash-based naming if path is not provided
|
|
235
|
+
try {
|
|
236
|
+
const imageHash = await calculateImageHash(image.path);
|
|
237
|
+
fileName = `${imageHash}${ext}`;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
debug(`Failed to calculate image hash, using path hash: ${error.message}`);
|
|
240
|
+
const hash = getContentHash(image.path);
|
|
241
|
+
fileName = `diagram-${hash}${ext}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const destPath = path.join(assetDir, fileName);
|
|
246
|
+
|
|
247
|
+
// Copy image from temp directory to assets directory
|
|
248
|
+
try {
|
|
249
|
+
// Check if source file exists
|
|
250
|
+
if (!(await fs.pathExists(image.path))) {
|
|
251
|
+
console.error(`Source image file does not exist: ${image.path}`);
|
|
252
|
+
return { content: finalContent };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Always overwrite existing file (since filename is based on document + position)
|
|
256
|
+
// This ensures updates replace old images
|
|
257
|
+
if (await fs.pathExists(destPath)) {
|
|
258
|
+
debug(`Overwriting existing diagram image: ${destPath}`);
|
|
259
|
+
}
|
|
260
|
+
// Compress the image directly to destination path
|
|
261
|
+
try {
|
|
262
|
+
debug(`Compressing image directly to destination: ${image.path} -> ${destPath}`);
|
|
263
|
+
const compressedPath = await compressImage(image.path, {
|
|
264
|
+
quality: 85,
|
|
265
|
+
outputPath: destPath,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// If compression failed, fallback to copying original file
|
|
269
|
+
if (compressedPath === image.path) {
|
|
270
|
+
debug(`Compression failed, copying original file: ${image.path}`);
|
|
271
|
+
await copyFile(image.path, destPath);
|
|
272
|
+
}
|
|
273
|
+
debug(`✅ Diagram image saved to: ${destPath}`);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
debug(`Image compression failed, copying original: ${error.message}`);
|
|
276
|
+
// Fallback to copying original file if compression fails
|
|
277
|
+
await copyFile(image.path, destPath);
|
|
278
|
+
debug(`✅ Diagram image saved to: ${destPath}`);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error(
|
|
282
|
+
`Failed to copy diagram image from ${image.path} to ${destPath}: ${error.message}`,
|
|
283
|
+
);
|
|
284
|
+
debug(` Source exists: ${await fs.pathExists(image.path)}`);
|
|
285
|
+
debug(` Dest dir exists: ${await fs.pathExists(assetDir)}`);
|
|
286
|
+
// If copy fails, return content as-is (keep D2 code blocks or placeholder)
|
|
287
|
+
return { content: finalContent };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Generate alt text from document content
|
|
291
|
+
const altText = extractAltText(documentContent);
|
|
292
|
+
|
|
293
|
+
// Create relative path from markdown file to assets directory
|
|
294
|
+
// Documents are saved in docsDir root (flattened paths), images are in docsDir/assets/diagram/
|
|
295
|
+
// So relative path is always: assets/diagram/filename.jpg (same directory level)
|
|
296
|
+
let relativePath;
|
|
297
|
+
if (finalDocsDir && finalDocPath) {
|
|
298
|
+
// All documents are in docsDir root (paths are flattened), assets are in docsDir/assets/diagram/
|
|
299
|
+
// So relative path is simply: assets/diagram/filename.jpg
|
|
300
|
+
relativePath = path.posix.join("assets", "diagram", fileName);
|
|
301
|
+
} else {
|
|
302
|
+
// Fallback: use the relativePathPrefix determined earlier
|
|
303
|
+
relativePath = path.posix.join(relativePathPrefix, fileName);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Create markdown image reference with markers for easy replacement
|
|
307
|
+
// Format: <!-- DIAGRAM_IMAGE_START:type:aspectRatio --><!-- DIAGRAM_IMAGE_END -->
|
|
308
|
+
const diagramTypeTag = diagramType || "unknown";
|
|
309
|
+
const aspectRatioTag = aspectRatio || "unknown";
|
|
310
|
+
const imageMarkdown = `<!-- DIAGRAM_IMAGE_START:${diagramTypeTag}:${aspectRatioTag} -->\n\n<!-- DIAGRAM_IMAGE_END -->`;
|
|
311
|
+
|
|
312
|
+
// Note: diagramLocations was already found above for filename generation, reuse it
|
|
313
|
+
|
|
314
|
+
// Debug: log found locations
|
|
315
|
+
if (diagramLocations.length > 0) {
|
|
316
|
+
debug(
|
|
317
|
+
`Found ${diagramLocations.length} diagram location(s):`,
|
|
318
|
+
diagramLocations.map((loc) => `${loc.type} at ${loc.start}-${loc.end}`).join(", "),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Determine which diagram to replace
|
|
323
|
+
// Note: targetIndex was already calculated above for filename generation, reuse it
|
|
324
|
+
|
|
325
|
+
if (targetIndex < 0 || targetIndex >= diagramLocations.length) {
|
|
326
|
+
// If index is out of range, default to first available or insert new
|
|
327
|
+
targetIndex = diagramLocations.length > 0 ? 0 : -1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Replace DIAGRAM_PLACEHOLDER first (highest priority)
|
|
331
|
+
// Check both finalContent and documentContent for placeholder
|
|
332
|
+
const hasPlaceholder =
|
|
333
|
+
finalContent.includes(DIAGRAM_PLACEHOLDER) || documentContent?.includes(DIAGRAM_PLACEHOLDER);
|
|
334
|
+
|
|
335
|
+
if (hasPlaceholder) {
|
|
336
|
+
debug("Replacing DIAGRAM_PLACEHOLDER");
|
|
337
|
+
// Use documentContent if it has placeholder, otherwise use finalContent
|
|
338
|
+
const contentWithPlaceholder = documentContent?.includes(DIAGRAM_PLACEHOLDER)
|
|
339
|
+
? documentContent
|
|
340
|
+
: finalContent;
|
|
341
|
+
finalContent = contentWithPlaceholder.replace(DIAGRAM_PLACEHOLDER, imageMarkdown);
|
|
342
|
+
} else if (diagramLocations.length > 0 && targetIndex >= 0) {
|
|
343
|
+
// Replace the diagram at the specified index
|
|
344
|
+
// Use originalContent if available (for accurate position), otherwise use finalContent
|
|
345
|
+
const contentToReplace = originalContent || finalContent;
|
|
346
|
+
const targetLocation = diagramLocations[targetIndex];
|
|
347
|
+
if (targetLocation) {
|
|
348
|
+
debug(
|
|
349
|
+
`Replacing diagram at index ${targetIndex} (type: ${targetLocation.type}, position: ${targetLocation.start}-${targetLocation.end})`,
|
|
350
|
+
);
|
|
351
|
+
const beforeReplace = contentToReplace.slice(0, targetLocation.start);
|
|
352
|
+
const afterReplace = contentToReplace.slice(targetLocation.end);
|
|
353
|
+
finalContent = beforeReplace + imageMarkdown + afterReplace;
|
|
354
|
+
} else {
|
|
355
|
+
debug(`⚠️ Target location at index ${targetIndex} not found`);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
// No diagrams found and no placeholder
|
|
359
|
+
// This can happen when:
|
|
360
|
+
// 1. User requests to update a diagram but no diagrams exist in the document
|
|
361
|
+
// 2. New document generation without any diagram markers
|
|
362
|
+
// In this case, append the diagram to the end of the document
|
|
363
|
+
debug("⚠️ No diagram location found to replace. Appending diagram to end of document.");
|
|
364
|
+
debug(` - Content length: ${finalContent.length}`);
|
|
365
|
+
debug(` - Contains DIAGRAM_PLACEHOLDER: ${finalContent.includes(DIAGRAM_PLACEHOLDER)}`);
|
|
366
|
+
debug(` - Contains DIAGRAM_IMAGE_START: ${finalContent.includes("DIAGRAM_IMAGE_START")}`);
|
|
367
|
+
debug(` - Contains \`\`\`d2: ${finalContent.includes("```d2")}`);
|
|
368
|
+
debug(` - Contains \`\`\`mermaid: ${finalContent.includes("```mermaid")}`);
|
|
369
|
+
|
|
370
|
+
// Append diagram to the end of the document with proper spacing
|
|
371
|
+
const trimmedContent = finalContent.trimEnd();
|
|
372
|
+
const separator = trimmedContent && !trimmedContent.endsWith("\n") ? "\n\n" : "\n";
|
|
373
|
+
finalContent = trimmedContent + separator + imageMarkdown;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Sync diagram images to translation files
|
|
377
|
+
// Only sync if we actually replaced/added a diagram (not just returned original content)
|
|
378
|
+
if (finalContent !== (originalContent || documentContent || content || "")) {
|
|
379
|
+
try {
|
|
380
|
+
const { syncDiagramToTranslations } = await import(
|
|
381
|
+
"../../utils/sync-diagram-to-translations.mjs"
|
|
382
|
+
);
|
|
383
|
+
const syncResult = await syncDiagramToTranslations(
|
|
384
|
+
finalContent,
|
|
385
|
+
finalDocPath,
|
|
386
|
+
finalDocsDir,
|
|
387
|
+
locale,
|
|
388
|
+
"update", // Operation type: update - skip if main has 0 diagrams
|
|
389
|
+
);
|
|
390
|
+
if (syncResult.updated > 0) {
|
|
391
|
+
debug(
|
|
392
|
+
`✅ Synced diagram images to ${syncResult.updated} translation file(s)${syncResult.errors.length > 0 ? ` (${syncResult.errors.length} error(s))` : ""}`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
// Don't fail the whole operation if sync fails
|
|
397
|
+
debug(`⚠️ Failed to sync diagram to translations: ${error.message}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return { content: finalContent };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Find all diagram locations in content
|
|
406
|
+
* Returns array of { type, start, end } for each diagram found
|
|
407
|
+
* Types: 'placeholder', 'image', 'd2', 'mermaid'
|
|
408
|
+
*/
|
|
409
|
+
function findAllDiagramLocations(content) {
|
|
410
|
+
const locations = [];
|
|
411
|
+
|
|
412
|
+
// 1. Find DIAGRAM_PLACEHOLDER
|
|
413
|
+
let placeholderIndex = content.indexOf(DIAGRAM_PLACEHOLDER);
|
|
414
|
+
while (placeholderIndex !== -1) {
|
|
415
|
+
locations.push({
|
|
416
|
+
type: "placeholder",
|
|
417
|
+
start: placeholderIndex,
|
|
418
|
+
end: placeholderIndex + DIAGRAM_PLACEHOLDER.length,
|
|
419
|
+
});
|
|
420
|
+
placeholderIndex = content.indexOf(DIAGRAM_PLACEHOLDER, placeholderIndex + 1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 2. Find DIAGRAM_IMAGE_START markers
|
|
424
|
+
// Format: <!-- DIAGRAM_IMAGE_START:type:aspectRatio -->...<!-- DIAGRAM_IMAGE_END -->
|
|
425
|
+
// Note: aspectRatio can contain colon (e.g., "16:9"), so we need to match until -->
|
|
426
|
+
const imageMatches = Array.from(content.matchAll(diagramImageBlockRegex));
|
|
427
|
+
for (const match of imageMatches) {
|
|
428
|
+
locations.push({
|
|
429
|
+
type: "image",
|
|
430
|
+
start: match.index,
|
|
431
|
+
end: match.index + match[0].length,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 3. Find D2 code blocks
|
|
436
|
+
// Note: .* matches title or other text after ```d2 (e.g., ```d2 Vault 驗證流程)
|
|
437
|
+
const d2Matches = Array.from(content.matchAll(d2CodeBlockRegex));
|
|
438
|
+
for (const match of d2Matches) {
|
|
439
|
+
locations.push({
|
|
440
|
+
type: "d2",
|
|
441
|
+
start: match.index,
|
|
442
|
+
end: match.index + match[0].length,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 4. Find Mermaid code blocks
|
|
447
|
+
// Note: .* matches title or other text after ```mermaid (e.g., ```mermaid Flow Chart)
|
|
448
|
+
const mermaidCodeBlockRegex = /```mermaid.*\n([\s\S]*?)```/g;
|
|
449
|
+
const mermaidMatches = Array.from(content.matchAll(mermaidCodeBlockRegex));
|
|
450
|
+
for (const match of mermaidMatches) {
|
|
451
|
+
locations.push({
|
|
452
|
+
type: "mermaid",
|
|
453
|
+
start: match.index,
|
|
454
|
+
end: match.index + match[0].length,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Sort by position in document (top to bottom)
|
|
459
|
+
locations.sort((a, b) => a.start - b.start);
|
|
460
|
+
|
|
461
|
+
return locations;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Extract diagram index from feedback
|
|
466
|
+
* Returns 0-based index, or null if not specified
|
|
467
|
+
* Examples: "first diagram" -> 0, "second diagram" -> 1, "第2张图" -> 1
|
|
468
|
+
*/
|
|
469
|
+
export function extractDiagramIndexFromFeedback(feedback) {
|
|
470
|
+
if (!feedback) return null;
|
|
471
|
+
|
|
472
|
+
const feedbackLower = feedback.toLowerCase();
|
|
473
|
+
|
|
474
|
+
// Check Chinese patterns first (more specific)
|
|
475
|
+
// Examples: "第一张图", "第二张图", "第2张图"
|
|
476
|
+
const chinesePattern = /第([一二三四五六七八九十]|\d+)[张个]图/i;
|
|
477
|
+
const chineseMatch = feedbackLower.match(chinesePattern);
|
|
478
|
+
if (chineseMatch?.[1]) {
|
|
479
|
+
const chineseNumbers = {
|
|
480
|
+
一: 1,
|
|
481
|
+
二: 2,
|
|
482
|
+
三: 3,
|
|
483
|
+
四: 4,
|
|
484
|
+
五: 5,
|
|
485
|
+
六: 6,
|
|
486
|
+
七: 7,
|
|
487
|
+
八: 8,
|
|
488
|
+
九: 9,
|
|
489
|
+
十: 10,
|
|
490
|
+
};
|
|
491
|
+
const numStr = chineseMatch[1];
|
|
492
|
+
const num = chineseNumbers[numStr] || parseInt(numStr, 10);
|
|
493
|
+
return num > 0 ? num - 1 : 0; // Convert to 0-based index
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Check number patterns (diagram #2, image 3, etc.)
|
|
497
|
+
const numberPattern = /(?:diagram|image|picture|chart|graph)\s*#?(\d+)/i;
|
|
498
|
+
const numberMatch = feedbackLower.match(numberPattern);
|
|
499
|
+
if (numberMatch?.[1]) {
|
|
500
|
+
const num = parseInt(numberMatch[1], 10);
|
|
501
|
+
return num > 0 ? num - 1 : 0; // Convert to 0-based index
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Check ordinal patterns (first, second, third, etc.)
|
|
505
|
+
const ordinalMap = {
|
|
506
|
+
first: 0,
|
|
507
|
+
"1st": 0,
|
|
508
|
+
1: 0,
|
|
509
|
+
second: 1,
|
|
510
|
+
"2nd": 1,
|
|
511
|
+
2: 1,
|
|
512
|
+
third: 2,
|
|
513
|
+
"3rd": 2,
|
|
514
|
+
3: 2,
|
|
515
|
+
fourth: 3,
|
|
516
|
+
"4th": 3,
|
|
517
|
+
4: 3,
|
|
518
|
+
fifth: 4,
|
|
519
|
+
"5th": 4,
|
|
520
|
+
5: 4,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
for (const [ordinal, index] of Object.entries(ordinalMap)) {
|
|
524
|
+
const ordinalPattern = new RegExp(
|
|
525
|
+
`(?:${ordinal})\\s+(?:diagram|image|picture|chart|graph)`,
|
|
526
|
+
"i",
|
|
527
|
+
);
|
|
528
|
+
if (ordinalPattern.test(feedbackLower)) {
|
|
529
|
+
return index;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Extract alt text from document content
|
|
538
|
+
*/
|
|
539
|
+
function extractAltText(documentContent) {
|
|
540
|
+
if (!documentContent) return "Diagram";
|
|
541
|
+
|
|
542
|
+
const lines = documentContent.split("\n").filter((line) => line.trim());
|
|
543
|
+
if (lines.length > 0) {
|
|
544
|
+
let altText = lines[0].trim();
|
|
545
|
+
// Remove markdown headers
|
|
546
|
+
altText = altText.replace(/^#+\s*/, "");
|
|
547
|
+
if (altText.length > 100) {
|
|
548
|
+
altText = `${altText.substring(0, 97)}...`;
|
|
549
|
+
}
|
|
550
|
+
return altText || "Diagram";
|
|
551
|
+
}
|
|
552
|
+
return "Diagram";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
replaceD2WithImage.input_schema = {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
images: {
|
|
559
|
+
type: "array",
|
|
560
|
+
description: "Images array from image generation agent",
|
|
561
|
+
},
|
|
562
|
+
imageResult: {
|
|
563
|
+
type: "object",
|
|
564
|
+
description: "The result from image generation agent (fallback)",
|
|
565
|
+
},
|
|
566
|
+
content: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "The document content (may contain DIAGRAM_PLACEHOLDER)",
|
|
569
|
+
},
|
|
570
|
+
documentContent: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Original document content containing DIAGRAM_PLACEHOLDER",
|
|
573
|
+
},
|
|
574
|
+
diagramType: {
|
|
575
|
+
type: "string",
|
|
576
|
+
description: "The diagram type (for marking the image)",
|
|
577
|
+
enum: ["architecture", "flowchart", "guide", "intro", "sequence", "network"],
|
|
578
|
+
},
|
|
579
|
+
aspectRatio: {
|
|
580
|
+
type: "string",
|
|
581
|
+
description: "The aspect ratio of the diagram (for marking the image)",
|
|
582
|
+
enum: ["1:1", "5:4", "4:3", "3:2", "16:9", "21:9"],
|
|
583
|
+
},
|
|
584
|
+
diagramIndex: {
|
|
585
|
+
type: "number",
|
|
586
|
+
description:
|
|
587
|
+
"Index of the diagram to replace (0-based). If not provided, will try to extract from feedback (e.g., 'first diagram' -> 0, 'second diagram' -> 1), otherwise defaults to 0.",
|
|
588
|
+
},
|
|
589
|
+
originalContent: {
|
|
590
|
+
type: "string",
|
|
591
|
+
description:
|
|
592
|
+
"Original document content before any modifications. Used to find existing diagrams when updating.",
|
|
593
|
+
},
|
|
594
|
+
path: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description:
|
|
597
|
+
"Document path (e.g., 'guides/getting-started.md') used for generating image filename",
|
|
598
|
+
},
|
|
599
|
+
docsDir: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "Documentation directory where assets will be saved (relative to project root)",
|
|
602
|
+
},
|
|
603
|
+
locale: {
|
|
604
|
+
type: "string",
|
|
605
|
+
description: "Main language locale (e.g., 'en', 'zh') for syncing to translations",
|
|
606
|
+
default: "en",
|
|
607
|
+
},
|
|
608
|
+
feedback: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "User feedback (for extracting diagram index)",
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
required: ["documentContent"],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
replaceD2WithImage.output_schema = {
|
|
617
|
+
type: "object",
|
|
618
|
+
properties: {
|
|
619
|
+
content: {
|
|
620
|
+
type: "string",
|
|
621
|
+
description: "Document content with D2 code blocks replaced by image",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
required: ["content"],
|
|
625
|
+
};
|
|
@@ -63,7 +63,7 @@ export default async function userReviewDocumentStructure({ documentStructure, .
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Get the refineDocumentStructure agent
|
|
66
|
-
const refineAgent = options.context.agents["
|
|
66
|
+
const refineAgent = options.context.agents["generateStructureExp"];
|
|
67
67
|
if (!refineAgent) {
|
|
68
68
|
console.log(
|
|
69
69
|
"Unable to process your feedback - the documentation structure update feature is unavailable.",
|
|
@@ -81,15 +81,16 @@ export default async function userReviewDocumentStructure({ documentStructure, .
|
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
83
|
// Call refineDocumentStructure agent with feedback
|
|
84
|
-
const { message } = await options.context.invoke(refineAgent, {
|
|
84
|
+
const { message, ...result } = await options.context.invoke(refineAgent, {
|
|
85
85
|
...rest,
|
|
86
|
-
dataSourceChunk: rest.dataSources[0].dataSourceChunk,
|
|
87
|
-
|
|
88
|
-
documentStructure: currentStructure,
|
|
89
|
-
userPreferences,
|
|
86
|
+
// dataSourceChunk: rest.dataSources[0].dataSourceChunk,
|
|
87
|
+
userFeedback: feedback.trim(),
|
|
88
|
+
// documentStructure: currentStructure,
|
|
89
|
+
// userPreferences,
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
currentStructure = options.context.userContext.currentStructure;
|
|
92
|
+
// currentStructure = options.context.userContext.currentStructure;
|
|
93
|
+
currentStructure = result.documentStructure;
|
|
93
94
|
|
|
94
95
|
if (rest.isChat && equal(currentStructure, documentStructure)) {
|
|
95
96
|
throw new Error(
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
pathToFlatName,
|
|
4
|
-
readFileContent,
|
|
5
|
-
} from "../../../utils/docs-finder-utils.mjs";
|
|
6
|
-
import { userContextAt } from "../../../utils/utils.mjs";
|
|
1
|
+
import { readFileContent } from "../../../utils/docs-finder-utils.mjs";
|
|
2
|
+
import { getFileName, userContextAt } from "../../../utils/utils.mjs";
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Initialize currentContents in userContext for document update
|
|
10
6
|
* Reads document content from file system and sets it in userContext
|
|
7
|
+
* Uses getFileName utility to generate filename consistently
|
|
11
8
|
*/
|
|
12
9
|
export default async function initCurrentContent(input, options) {
|
|
13
10
|
const { path, docsDir, locale = "en" } = input;
|
|
@@ -16,9 +13,8 @@ export default async function initCurrentContent(input, options) {
|
|
|
16
13
|
return {};
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
// Generate filename
|
|
20
|
-
const
|
|
21
|
-
const fileName = generateFileName(flatName, locale);
|
|
16
|
+
// Generate filename using unified utility function
|
|
17
|
+
const fileName = getFileName(path, locale);
|
|
22
18
|
|
|
23
19
|
// Read document content
|
|
24
20
|
const content = docsDir ? await readFileContent(docsDir, fileName) : null;
|