@aigne/doc-smith 0.8.15-beta.2 ā 0.8.15-beta.4
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 +19 -0
- package/agents/generate/index.yaml +2 -1
- package/agents/init/check.mjs +3 -1
- package/agents/init/index.mjs +40 -6
- package/agents/publish/publish-docs.mjs +3 -8
- package/agents/translate/index.yaml +0 -3
- package/agents/translate/translate-multilingual.yaml +1 -1
- package/agents/update/batch-update-document.yaml +1 -1
- package/agents/update/check-document.mjs +35 -13
- package/agents/update/generate-diagram.yaml +30 -0
- package/agents/update/generate-document.yaml +1 -30
- package/agents/update/handle-document-update.yaml +1 -0
- package/agents/update/save-and-translate-document.mjs +10 -50
- package/agents/update/update-document-detail.yaml +1 -0
- package/agents/update/update-single-document.yaml +1 -0
- package/agents/utils/load-sources.mjs +6 -6
- package/agents/utils/{save-docs.mjs ā post-generate.mjs} +2 -51
- package/agents/utils/save-doc-translation.mjs +27 -0
- package/agents/utils/save-doc.mjs +55 -0
- package/agents/utils/save-sidebar.mjs +59 -0
- package/agents/utils/transform-detail-datasources.mjs +4 -4
- package/aigne.yaml +6 -3
- package/package.json +1 -1
- package/prompts/detail/generate/user-prompt.md +1 -0
- package/utils/file-utils.mjs +19 -12
- package/utils/load-config.mjs +1 -1
- package/utils/utils.mjs +67 -65
- package/agents/utils/save-single-doc.mjs +0 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.15-beta.4](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.3...v0.8.15-beta.4) (2025-10-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* fix update translate not working ([#221](https://github.com/AIGNE-io/aigne-doc-smith/issues/221)) ([95bc49e](https://github.com/AIGNE-io/aigne-doc-smith/commit/95bc49ec8b1e18fe20dd1360d9707afdd6629bad))
|
|
9
|
+
|
|
10
|
+
## [0.8.15-beta.3](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.2...v0.8.15-beta.3) (2025-10-28)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* glossary support load from remote-url ([#220](https://github.com/AIGNE-io/aigne-doc-smith/issues/220)) ([5d52746](https://github.com/AIGNE-io/aigne-doc-smith/commit/5d527469819a4dc08f17338b1d1fb2cd7ccf07c0))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* polish generate/update/translate save doc logic ([#218](https://github.com/AIGNE-io/aigne-doc-smith/issues/218)) ([dada1a4](https://github.com/AIGNE-io/aigne-doc-smith/commit/dada1a422d589512f2a0f6e5c69df4845eca44ae))
|
|
21
|
+
|
|
3
22
|
## [0.8.15-beta.2](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta.1...v0.8.15-beta.2) (2025-10-24)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -23,6 +23,7 @@ skills:
|
|
|
23
23
|
savePath:
|
|
24
24
|
$get: outputDir
|
|
25
25
|
fileName: structure-plan.json
|
|
26
|
+
- ../utils/save-sidebar.mjs
|
|
26
27
|
- type: transform
|
|
27
28
|
name: transformData
|
|
28
29
|
task_render_mode: hide
|
|
@@ -47,7 +48,7 @@ skills:
|
|
|
47
48
|
- url: ../utils/check-feedback-refiner.mjs
|
|
48
49
|
default_input:
|
|
49
50
|
stage: document_structure
|
|
50
|
-
- ../utils/
|
|
51
|
+
- ../utils/post-generate.mjs
|
|
51
52
|
|
|
52
53
|
input_schema:
|
|
53
54
|
type: object
|
package/agents/init/check.mjs
CHANGED
package/agents/init/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isGlobPattern,
|
|
21
21
|
validatePath,
|
|
22
22
|
} from "../../utils/utils.mjs";
|
|
23
|
+
import { isRemoteFile } from "../../utils/file-utils.mjs";
|
|
23
24
|
|
|
24
25
|
const _PRESS_ENTER_TO_FINISH = "Press Enter to finish";
|
|
25
26
|
|
|
@@ -246,17 +247,23 @@ export default async function init(
|
|
|
246
247
|
// 8. Content sources
|
|
247
248
|
console.log("\nš [8/9]: Content Sources");
|
|
248
249
|
console.log(
|
|
249
|
-
"Please specify the folders and files we should analyze to generate your documentation
|
|
250
|
+
"Please specify the folders and files we should analyze to generate your documentation.",
|
|
250
251
|
);
|
|
251
252
|
console.log(
|
|
252
|
-
|
|
253
|
+
` 1. You can use local file paths like ${chalk.green("./src")}, ${chalk.green("./docs")}, ${chalk.green("./README.md")} (prefix with '!' to ignore a file or folder like ${chalk.green("!./src/private")}).`,
|
|
254
|
+
);
|
|
255
|
+
console.log(
|
|
256
|
+
` 2. You can also use glob patterns like ${chalk.green("src/**/*.js")} or ${chalk.green("docs/**/*.md")} for more specific file matching. (prefix with '!' to ignore a file or folder like ${chalk.green("!private/**/*.js")}).`,
|
|
257
|
+
);
|
|
258
|
+
console.log(
|
|
259
|
+
` 3. You can also use remote url like ${chalk.green("https://example.com/openapi.yaml")}.`,
|
|
253
260
|
);
|
|
254
261
|
console.log("š” If you leave this empty, we will scan the entire directory.");
|
|
255
262
|
|
|
256
263
|
const sourcePaths = [];
|
|
257
264
|
while (true) {
|
|
258
265
|
const selectedPath = await options.prompts.search({
|
|
259
|
-
message: "Please enter a file or folder path, or a glob pattern:",
|
|
266
|
+
message: "Please enter a file or folder path, or a glob pattern or remote url:",
|
|
260
267
|
source: async (input) => {
|
|
261
268
|
if (!input || input.trim() === "") {
|
|
262
269
|
return [
|
|
@@ -268,13 +275,23 @@ export default async function init(
|
|
|
268
275
|
];
|
|
269
276
|
}
|
|
270
277
|
|
|
278
|
+
let isIgnore = false;
|
|
271
279
|
const searchTerm = input.trim();
|
|
280
|
+
let cleanSearchTerm = searchTerm;
|
|
281
|
+
if (cleanSearchTerm.startsWith("!")) {
|
|
282
|
+
isIgnore = true;
|
|
283
|
+
cleanSearchTerm = searchTerm.slice(1);
|
|
284
|
+
}
|
|
272
285
|
|
|
273
286
|
// Search for matching files and folders in current directory
|
|
274
|
-
const availablePaths = getAvailablePaths(
|
|
287
|
+
const availablePaths = getAvailablePaths(cleanSearchTerm);
|
|
275
288
|
|
|
276
289
|
// Also add option to use as glob pattern
|
|
277
|
-
const options = [...availablePaths]
|
|
290
|
+
const options = [...availablePaths].map((x) => ({
|
|
291
|
+
...x,
|
|
292
|
+
name: isIgnore ? `!${x.name}` : x.name,
|
|
293
|
+
value: isIgnore ? `!${x.value}` : x.value,
|
|
294
|
+
}));
|
|
278
295
|
|
|
279
296
|
// Check if input looks like a glob pattern
|
|
280
297
|
const isGlobPatternResult = isGlobPattern(searchTerm);
|
|
@@ -287,6 +304,14 @@ export default async function init(
|
|
|
287
304
|
});
|
|
288
305
|
}
|
|
289
306
|
|
|
307
|
+
if (!isIgnore && isRemoteFile(searchTerm)) {
|
|
308
|
+
options.push({
|
|
309
|
+
name: searchTerm,
|
|
310
|
+
value: searchTerm,
|
|
311
|
+
description: "Use this remote url for content source.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
290
315
|
return options;
|
|
291
316
|
},
|
|
292
317
|
});
|
|
@@ -301,6 +326,14 @@ export default async function init(
|
|
|
301
326
|
// Check if it's a glob pattern
|
|
302
327
|
const isGlobPatternResult = isGlobPattern(trimmedPath);
|
|
303
328
|
|
|
329
|
+
if (isRemoteFile(trimmedPath)) {
|
|
330
|
+
// For remote urls, just add them without validation
|
|
331
|
+
if (sourcePaths.includes(trimmedPath)) {
|
|
332
|
+
console.log(`ā ļø URL already exists: ${trimmedPath}`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
sourcePaths.push(trimmedPath);
|
|
336
|
+
}
|
|
304
337
|
if (isGlobPatternResult) {
|
|
305
338
|
// For glob patterns, just add them without validation
|
|
306
339
|
if (sourcePaths.includes(trimmedPath)) {
|
|
@@ -309,8 +342,9 @@ export default async function init(
|
|
|
309
342
|
}
|
|
310
343
|
sourcePaths.push(trimmedPath);
|
|
311
344
|
} else {
|
|
345
|
+
const cleanTrimmedPath = trimmedPath.startsWith("!") ? trimmedPath.slice(1) : trimmedPath;
|
|
312
346
|
// Use validatePath to check if path is valid for regular paths
|
|
313
|
-
const validation = validatePath(
|
|
347
|
+
const validation = validatePath(cleanTrimmedPath);
|
|
314
348
|
|
|
315
349
|
if (!validation.isValid) {
|
|
316
350
|
console.log(`ā ļø ${validation.error}`);
|
|
@@ -14,14 +14,9 @@ import {
|
|
|
14
14
|
} from "../../utils/constants/index.mjs";
|
|
15
15
|
import { beforePublishHook, ensureTmpDir } from "../../utils/d2-utils.mjs";
|
|
16
16
|
import { deploy } from "../../utils/deploy.mjs";
|
|
17
|
-
import {
|
|
18
|
-
getGithubRepoUrl,
|
|
19
|
-
isHttp,
|
|
20
|
-
loadConfigFromFile,
|
|
21
|
-
saveValueToConfig,
|
|
22
|
-
} from "../../utils/utils.mjs";
|
|
17
|
+
import { getGithubRepoUrl, loadConfigFromFile, saveValueToConfig } from "../../utils/utils.mjs";
|
|
23
18
|
import updateBranding from "../utils/update-branding.mjs";
|
|
24
|
-
import { downloadAndUploadImage } from "../../utils/file-utils.mjs";
|
|
19
|
+
import { isRemoteFile, downloadAndUploadImage } from "../../utils/file-utils.mjs";
|
|
25
20
|
|
|
26
21
|
const BASE_URL = process.env.DOC_SMITH_BASE_URL || CLOUD_SERVICE_URL_PROD;
|
|
27
22
|
|
|
@@ -205,7 +200,7 @@ export default async function publishDocs(
|
|
|
205
200
|
let finalPath = null;
|
|
206
201
|
|
|
207
202
|
// Handle project logo download if it's a URL
|
|
208
|
-
if (projectInfo.icon &&
|
|
203
|
+
if (projectInfo.icon && isRemoteFile(projectInfo.icon)) {
|
|
209
204
|
const { url: uploadedImageUrl, downloadFinalPath } = await downloadAndUploadImage(
|
|
210
205
|
projectInfo.icon,
|
|
211
206
|
docsDir,
|
|
@@ -34,9 +34,6 @@ skills:
|
|
|
34
34
|
skills:
|
|
35
35
|
- ../translate/translate-multilingual.yaml
|
|
36
36
|
- url: ./record-translation-history.mjs
|
|
37
|
-
- url: ../utils/save-single-doc.mjs
|
|
38
|
-
default_input:
|
|
39
|
-
isTranslate: true
|
|
40
37
|
iterate_on: selectedDocs
|
|
41
38
|
concurrency: 10
|
|
42
39
|
- url: ../utils/check-feedback-refiner.mjs
|
|
@@ -22,6 +22,7 @@ skills:
|
|
|
22
22
|
max_iterations: 5
|
|
23
23
|
return_last_on_max_iterations: true
|
|
24
24
|
task_title: Translate '{{ title }}' to '{{ language }}'
|
|
25
|
+
- ../utils/save-doc-translation.mjs
|
|
25
26
|
input_schema:
|
|
26
27
|
type: object
|
|
27
28
|
properties:
|
|
@@ -48,4 +49,3 @@ output_schema:
|
|
|
48
49
|
type: string
|
|
49
50
|
iterate_on: translates
|
|
50
51
|
concurrency: 3
|
|
51
|
-
|
|
@@ -2,7 +2,10 @@ import { access, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { TeamAgent } from "@aigne/core";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import pMap from "p-map";
|
|
5
7
|
|
|
8
|
+
import { getFileName } from "../../utils/utils.mjs";
|
|
6
9
|
import checkDetailResult from "../utils/check-detail-result.mjs";
|
|
7
10
|
|
|
8
11
|
// Get current script directory
|
|
@@ -18,13 +21,13 @@ export default async function checkDocument(
|
|
|
18
21
|
modifiedFiles,
|
|
19
22
|
forceRegenerate,
|
|
20
23
|
locale,
|
|
24
|
+
translates,
|
|
21
25
|
...rest
|
|
22
26
|
},
|
|
23
27
|
options,
|
|
24
28
|
) {
|
|
25
29
|
// Check if the detail file already exists
|
|
26
|
-
const
|
|
27
|
-
const fileFullName = locale === "en" ? `${flatName}.md` : `${flatName}.${locale}.md`;
|
|
30
|
+
const fileFullName = getFileName(path, locale);
|
|
28
31
|
const filePath = join(docsDir, fileFullName);
|
|
29
32
|
let detailGenerated = true;
|
|
30
33
|
let fileContent = null;
|
|
@@ -84,24 +87,42 @@ export default async function checkDocument(
|
|
|
84
87
|
contentValidationFailed = true;
|
|
85
88
|
}
|
|
86
89
|
}
|
|
90
|
+
const languages = translates.map((x) => x.language);
|
|
91
|
+
const lackLanguages = new Set(languages);
|
|
92
|
+
const skills = [];
|
|
87
93
|
|
|
88
94
|
// If file exists, sourceIds haven't changed, source files haven't changed, and content validation passes, no need to regenerate
|
|
89
95
|
if (detailGenerated && !sourceIdsChanged && !contentValidationFailed && !forceRegenerate) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
await pMap(
|
|
97
|
+
languages,
|
|
98
|
+
async (x) => {
|
|
99
|
+
const languageFileName = getFileName(path, x);
|
|
100
|
+
const languageFilePath = join(docsDir, languageFileName);
|
|
101
|
+
if (await fs.exists(languageFilePath)) {
|
|
102
|
+
lackLanguages.delete(x);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{ concurrency: 10 },
|
|
106
|
+
);
|
|
107
|
+
if (lackLanguages.size === 0) {
|
|
108
|
+
return {
|
|
109
|
+
path,
|
|
110
|
+
docsDir,
|
|
111
|
+
...rest,
|
|
112
|
+
detailGenerated: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// translations during generation don't need feedback, content is satisfactory
|
|
116
|
+
rest.content = fileContent;
|
|
117
|
+
} else {
|
|
118
|
+
skills.push(options.context.agents["handleDocumentUpdate"]);
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
skills.push(options.context.agents["translateMultilingual"]);
|
|
122
|
+
|
|
98
123
|
const teamAgent = TeamAgent.from({
|
|
99
124
|
name: "generateDocument",
|
|
100
|
-
skills
|
|
101
|
-
options.context.agents["handleDocumentUpdate"],
|
|
102
|
-
options.context.agents["translateMultilingual"],
|
|
103
|
-
options.context.agents["saveSingleDoc"],
|
|
104
|
-
],
|
|
125
|
+
skills,
|
|
105
126
|
});
|
|
106
127
|
let openAPISpec = null;
|
|
107
128
|
|
|
@@ -119,6 +140,7 @@ export default async function checkDocument(
|
|
|
119
140
|
|
|
120
141
|
const result = await options.context.invoke(teamAgent, {
|
|
121
142
|
...rest,
|
|
143
|
+
translates: translates.filter((x) => lackLanguages.has(x.language)),
|
|
122
144
|
locale,
|
|
123
145
|
docsDir,
|
|
124
146
|
path,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type: team
|
|
2
|
+
task_render_mode: collapse
|
|
3
|
+
name: generateDiagram
|
|
4
|
+
skills:
|
|
5
|
+
- ../generate/draw-diagram.yaml
|
|
6
|
+
- ../generate/wrap-diagram-code.mjs
|
|
7
|
+
reflection:
|
|
8
|
+
reviewer: ../generate/check-diagram.mjs
|
|
9
|
+
is_approved: isValid
|
|
10
|
+
max_iterations: 5
|
|
11
|
+
return_last_on_max_iterations: false
|
|
12
|
+
custom_error_message: "MUST NOT generate any diagram: validation failed after max iterations."
|
|
13
|
+
input_schema:
|
|
14
|
+
type: object
|
|
15
|
+
properties:
|
|
16
|
+
documentContent:
|
|
17
|
+
type: string
|
|
18
|
+
description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
|
|
19
|
+
locale:
|
|
20
|
+
type: string
|
|
21
|
+
description: Language for diagram labels and text
|
|
22
|
+
default: en
|
|
23
|
+
required:
|
|
24
|
+
- documentContent
|
|
25
|
+
output_schema:
|
|
26
|
+
type: object
|
|
27
|
+
properties:
|
|
28
|
+
diagramSourceCode:
|
|
29
|
+
type: string
|
|
30
|
+
description: The **diagram source code** generated from the input text.
|
|
@@ -58,33 +58,4 @@ afs:
|
|
|
58
58
|
should search and read as needed while generating document content
|
|
59
59
|
keep_text_in_tool_uses: false
|
|
60
60
|
skills:
|
|
61
|
-
-
|
|
62
|
-
task_render_mode: collapse
|
|
63
|
-
name: generateDiagram
|
|
64
|
-
skills:
|
|
65
|
-
- ../generate/draw-diagram.yaml
|
|
66
|
-
- ../generate/wrap-diagram-code.mjs
|
|
67
|
-
reflection:
|
|
68
|
-
reviewer: ../generate/check-diagram.mjs
|
|
69
|
-
is_approved: isValid
|
|
70
|
-
max_iterations: 5
|
|
71
|
-
return_last_on_max_iterations: false
|
|
72
|
-
custom_error_message: "MUST NOT generate any diagram: validation failed after max iterations."
|
|
73
|
-
input_schema:
|
|
74
|
-
type: object
|
|
75
|
-
properties:
|
|
76
|
-
documentContent:
|
|
77
|
-
type: string
|
|
78
|
-
description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
|
|
79
|
-
locale:
|
|
80
|
-
type: string
|
|
81
|
-
description: Language for diagram labels and text
|
|
82
|
-
default: en
|
|
83
|
-
required:
|
|
84
|
-
- documentContent
|
|
85
|
-
output_schema:
|
|
86
|
-
type: object
|
|
87
|
-
properties:
|
|
88
|
-
diagramSourceCode:
|
|
89
|
-
type: string
|
|
90
|
-
description: The **diagram source code** generated from the input text.
|
|
61
|
+
- ./generate-diagram.yaml
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import pMap from "p-map";
|
|
2
2
|
|
|
3
3
|
export default async function saveAndTranslateDocument(input, options) {
|
|
4
4
|
const { selectedDocs, docsDir, translateLanguages, locale } = input;
|
|
@@ -7,21 +7,6 @@ export default async function saveAndTranslateDocument(input, options) {
|
|
|
7
7
|
return {};
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// Saves a document with optional translation data
|
|
11
|
-
const saveDocument = async (doc, translates = null, isTranslate = false) => {
|
|
12
|
-
const saveAgent = options.context.agents["saveSingleDoc"];
|
|
13
|
-
|
|
14
|
-
return await options.context.invoke(saveAgent, {
|
|
15
|
-
path: doc.path,
|
|
16
|
-
content: doc.content,
|
|
17
|
-
docsDir: docsDir,
|
|
18
|
-
locale: locale,
|
|
19
|
-
translates: translates || doc.translates,
|
|
20
|
-
labels: doc.labels,
|
|
21
|
-
isTranslate: isTranslate,
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
10
|
// Only prompt user if translation is actually needed
|
|
26
11
|
let shouldTranslate = false;
|
|
27
12
|
if (
|
|
@@ -46,28 +31,6 @@ export default async function saveAndTranslateDocument(input, options) {
|
|
|
46
31
|
|
|
47
32
|
// Save documents in batches
|
|
48
33
|
const batchSize = 3;
|
|
49
|
-
for (let i = 0; i < selectedDocs.length; i += batchSize) {
|
|
50
|
-
const batch = selectedDocs.slice(i, i + batchSize);
|
|
51
|
-
|
|
52
|
-
const savePromises = batch.map(async (doc) => {
|
|
53
|
-
try {
|
|
54
|
-
await saveDocument(doc);
|
|
55
|
-
|
|
56
|
-
// Record history for each document if feedback is provided
|
|
57
|
-
if (doc.feedback?.trim()) {
|
|
58
|
-
recordUpdate({
|
|
59
|
-
operation: "document_update",
|
|
60
|
-
feedback: doc.feedback.trim(),
|
|
61
|
-
documentPath: doc.path,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error(`ā Failed to save document ${doc.path}:`, error.message);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await Promise.all(savePromises);
|
|
70
|
-
}
|
|
71
34
|
|
|
72
35
|
// Return results if user chose to skip translation
|
|
73
36
|
if (!shouldTranslate) {
|
|
@@ -77,30 +40,27 @@ export default async function saveAndTranslateDocument(input, options) {
|
|
|
77
40
|
// Translate documents in batches
|
|
78
41
|
const translateAgent = options.context.agents["translateMultilingual"];
|
|
79
42
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const translatePromises = batch.map(async (doc) => {
|
|
43
|
+
await pMap(
|
|
44
|
+
selectedDocs,
|
|
45
|
+
async (doc) => {
|
|
84
46
|
try {
|
|
85
47
|
// Clear feedback to ensure translation is not affected by update feedback
|
|
86
48
|
doc.feedback = "";
|
|
87
49
|
|
|
88
|
-
|
|
50
|
+
await options.context.invoke(translateAgent, {
|
|
89
51
|
...input, // context is required
|
|
90
52
|
content: doc.content,
|
|
91
53
|
translates: doc.translates,
|
|
92
54
|
title: doc.title,
|
|
55
|
+
path: doc.path,
|
|
56
|
+
docsDir,
|
|
93
57
|
});
|
|
94
|
-
|
|
95
|
-
// Save the translated content
|
|
96
|
-
await saveDocument(doc, result.translates, true);
|
|
97
58
|
} catch (error) {
|
|
98
59
|
console.error(`ā Failed to translate document ${doc.path}:`, error.message);
|
|
99
60
|
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
61
|
+
},
|
|
62
|
+
{ concurrency: batchSize },
|
|
63
|
+
);
|
|
104
64
|
|
|
105
65
|
return {};
|
|
106
66
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
loadFilesFromPaths,
|
|
9
9
|
readFileContents,
|
|
10
10
|
getMimeType,
|
|
11
|
-
|
|
11
|
+
isRemoteFile,
|
|
12
12
|
} from "../../utils/file-utils.mjs";
|
|
13
13
|
import {
|
|
14
14
|
getCurrentGitHead,
|
|
@@ -144,7 +144,7 @@ export default async function loadSources(
|
|
|
144
144
|
files.map(async (file) => {
|
|
145
145
|
const ext = path.extname(file).toLowerCase();
|
|
146
146
|
|
|
147
|
-
if (mediaExtensions.includes(ext) && !
|
|
147
|
+
if (mediaExtensions.includes(ext) && !isRemoteFile(file)) {
|
|
148
148
|
// This is a media file
|
|
149
149
|
const relativePath = path.relative(docsDir, file);
|
|
150
150
|
const fileName = path.basename(file);
|
|
@@ -214,15 +214,15 @@ export default async function loadSources(
|
|
|
214
214
|
return !isOpenAPI;
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
const
|
|
217
|
+
const remoteFileList = [];
|
|
218
218
|
|
|
219
219
|
sourceFiles.forEach((file) => {
|
|
220
|
-
if (
|
|
221
|
-
|
|
220
|
+
if (isRemoteFile(file.sourceId)) {
|
|
221
|
+
remoteFileList.push(file);
|
|
222
222
|
}
|
|
223
223
|
});
|
|
224
224
|
if (options?.context?.userContext) {
|
|
225
|
-
options.context.userContext.
|
|
225
|
+
options.context.userContext.remoteFileList = remoteFileList;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// Build allSources string using utility function
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, unlink
|
|
1
|
+
import { readdir, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
|
|
4
4
|
import { getCurrentGitHead, saveGitHeadToConfig } from "../../utils/utils.mjs";
|
|
@@ -10,7 +10,7 @@ import { getCurrentGitHead, saveGitHeadToConfig } from "../../utils/utils.mjs";
|
|
|
10
10
|
* @param {Array<string>} [params.translateLanguages] - Translation languages
|
|
11
11
|
* @returns {Promise<Array<{ path: string, success: boolean, error?: string }>>}
|
|
12
12
|
*/
|
|
13
|
-
export default async function
|
|
13
|
+
export default async function postGenerate({
|
|
14
14
|
documentExecutionStructure: documentStructure,
|
|
15
15
|
docsDir,
|
|
16
16
|
translateLanguages = [],
|
|
@@ -26,15 +26,6 @@ export default async function saveDocs({
|
|
|
26
26
|
console.warn("Failed to save git HEAD:", err.message);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Generate _sidebar.md
|
|
30
|
-
try {
|
|
31
|
-
const sidebar = generateSidebar(documentStructure);
|
|
32
|
-
const sidebarPath = join(docsDir, "_sidebar.md");
|
|
33
|
-
await writeFile(sidebarPath, sidebar, "utf8");
|
|
34
|
-
} catch (err) {
|
|
35
|
-
console.error("Failed to save _sidebar.md:", err.message);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
// Clean up invalid .md files that are no longer in the documentation structure
|
|
39
30
|
try {
|
|
40
31
|
await cleanupInvalidFiles(documentStructure, docsDir, translateLanguages, locale);
|
|
@@ -140,43 +131,3 @@ async function cleanupInvalidFiles(documentStructure, docsDir, translateLanguage
|
|
|
140
131
|
}
|
|
141
132
|
|
|
142
133
|
// Generate sidebar content, support nested structure, and the order is consistent with documentStructure
|
|
143
|
-
function generateSidebar(documentStructure) {
|
|
144
|
-
// Build tree structure
|
|
145
|
-
const root = {};
|
|
146
|
-
for (const { path, title, parentId } of documentStructure) {
|
|
147
|
-
const relPath = path.replace(/^\//, "");
|
|
148
|
-
const segments = relPath.split("/");
|
|
149
|
-
let node = root;
|
|
150
|
-
for (let i = 0; i < segments.length; i++) {
|
|
151
|
-
const seg = segments[i];
|
|
152
|
-
if (!node[seg])
|
|
153
|
-
node[seg] = {
|
|
154
|
-
__children: {},
|
|
155
|
-
__title: null,
|
|
156
|
-
__fullPath: segments.slice(0, i + 1).join("/"),
|
|
157
|
-
__parentId: parentId,
|
|
158
|
-
};
|
|
159
|
-
if (i === segments.length - 1) node[seg].__title = title;
|
|
160
|
-
node = node[seg].__children;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// Recursively generate sidebar text, the link path is the flattened file name
|
|
164
|
-
function walk(node, parentSegments = [], indent = "") {
|
|
165
|
-
let out = "";
|
|
166
|
-
for (const key of Object.keys(node)) {
|
|
167
|
-
const item = node[key];
|
|
168
|
-
const fullSegments = [...parentSegments, key];
|
|
169
|
-
const flatFile = `${fullSegments.join("-")}.md`;
|
|
170
|
-
if (item.__title) {
|
|
171
|
-
const realIndent = item.__parentId === null ? "" : indent;
|
|
172
|
-
out += `${realIndent}* [${item.__title}](/${flatFile})\n`;
|
|
173
|
-
}
|
|
174
|
-
const children = item.__children;
|
|
175
|
-
if (Object.keys(children).length > 0) {
|
|
176
|
-
out += walk(children, fullSegments, `${indent} `);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
return walk(root).replace(/\n+$/, "");
|
|
182
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { saveDocTranslation as _saveDocTranslation } from "../../utils/utils.mjs";
|
|
2
|
+
|
|
3
|
+
export default async function saveDocTranslation({
|
|
4
|
+
path,
|
|
5
|
+
docsDir,
|
|
6
|
+
translation,
|
|
7
|
+
language,
|
|
8
|
+
labels,
|
|
9
|
+
isShowMessage = false,
|
|
10
|
+
}) {
|
|
11
|
+
await _saveDocTranslation({
|
|
12
|
+
path,
|
|
13
|
+
docsDir,
|
|
14
|
+
language,
|
|
15
|
+
translation,
|
|
16
|
+
labels,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (isShowMessage) {
|
|
20
|
+
const message = `ā
Translation completed successfully.`;
|
|
21
|
+
return { message };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
saveDocTranslation.task_render_mode = "hide";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { recordUpdate } from "../../utils/history-utils.mjs";
|
|
2
|
+
import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
|
|
3
|
+
import { saveDoc as _saveDoc } from "../../utils/utils.mjs";
|
|
4
|
+
|
|
5
|
+
export default async function saveDoc({
|
|
6
|
+
path,
|
|
7
|
+
content,
|
|
8
|
+
docsDir,
|
|
9
|
+
labels,
|
|
10
|
+
locale,
|
|
11
|
+
feedback,
|
|
12
|
+
isShowMessage = false,
|
|
13
|
+
...rest
|
|
14
|
+
}) {
|
|
15
|
+
await _saveDoc({
|
|
16
|
+
path,
|
|
17
|
+
content,
|
|
18
|
+
docsDir,
|
|
19
|
+
labels,
|
|
20
|
+
locale,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (feedback?.trim()) {
|
|
24
|
+
recordUpdate({
|
|
25
|
+
operation: "document_update",
|
|
26
|
+
feedback: feedback.trim(),
|
|
27
|
+
documentPath: path,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isShowMessage) {
|
|
32
|
+
// Shutdown mermaid worker pool to ensure clean exit
|
|
33
|
+
try {
|
|
34
|
+
await shutdownMermaidWorkerPool();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn("Failed to shutdown mermaid worker pool:", error.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const message = `ā
Document updated successfully.`;
|
|
40
|
+
return { message };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
path,
|
|
45
|
+
content,
|
|
46
|
+
docsDir,
|
|
47
|
+
labels,
|
|
48
|
+
locale,
|
|
49
|
+
feedback,
|
|
50
|
+
isShowMessage,
|
|
51
|
+
...rest,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
saveDoc.task_render_mode = "hide";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
|
|
4
|
+
export default async function saveSidebar({ documentStructure, docsDir }) {
|
|
5
|
+
// Generate _sidebar.md
|
|
6
|
+
try {
|
|
7
|
+
const sidebar = generateSidebar(documentStructure);
|
|
8
|
+
const sidebarPath = join(docsDir, "_sidebar.md");
|
|
9
|
+
|
|
10
|
+
await fs.ensureDir(docsDir);
|
|
11
|
+
await fs.writeFile(sidebarPath, sidebar, "utf8");
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error("Failed to save _sidebar.md:", err.message);
|
|
14
|
+
}
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Recursively generate sidebar text, the link path is the flattened file name
|
|
19
|
+
function walk(node, parentSegments = [], indent = "") {
|
|
20
|
+
let out = "";
|
|
21
|
+
for (const key of Object.keys(node)) {
|
|
22
|
+
const item = node[key];
|
|
23
|
+
const fullSegments = [...parentSegments, key];
|
|
24
|
+
const flatFile = `${fullSegments.join("-")}.md`;
|
|
25
|
+
if (item.__title) {
|
|
26
|
+
const realIndent = item.__parentId === null ? "" : indent;
|
|
27
|
+
out += `${realIndent}* [${item.__title}](/${flatFile})\n`;
|
|
28
|
+
}
|
|
29
|
+
const children = item.__children;
|
|
30
|
+
if (Object.keys(children).length > 0) {
|
|
31
|
+
out += walk(children, fullSegments, `${indent} `);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateSidebar(documentStructure) {
|
|
38
|
+
// Build tree structure
|
|
39
|
+
const root = {};
|
|
40
|
+
for (const { path, title, parentId } of documentStructure) {
|
|
41
|
+
const relPath = path.replace(/^\//, "");
|
|
42
|
+
const segments = relPath.split("/");
|
|
43
|
+
let node = root;
|
|
44
|
+
for (let i = 0; i < segments.length; i++) {
|
|
45
|
+
const seg = segments[i];
|
|
46
|
+
if (!node[seg])
|
|
47
|
+
node[seg] = {
|
|
48
|
+
__children: {},
|
|
49
|
+
__title: null,
|
|
50
|
+
__fullPath: segments.slice(0, i + 1).join("/"),
|
|
51
|
+
__parentId: parentId,
|
|
52
|
+
};
|
|
53
|
+
if (i === segments.length - 1) node[seg].__title = title;
|
|
54
|
+
node = node[seg].__children;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return walk(root).replace(/\n+$/, "");
|
|
59
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { isRemoteFile } from "../../utils/file-utils.mjs";
|
|
2
3
|
import { normalizePath, toRelativePath } from "../../utils/utils.mjs";
|
|
3
|
-
import { checkIsRemoteFile } from "../../utils/file-utils.mjs";
|
|
4
4
|
|
|
5
5
|
export default function transformDetailDatasources({ sourceIds }, options = {}) {
|
|
6
6
|
// Read file content for each sourceId, ignoring failures
|
|
7
7
|
let openAPISpec;
|
|
8
|
-
const
|
|
8
|
+
const remoteFileList = options?.context?.userContext?.remoteFileList || [];
|
|
9
9
|
const contents = (sourceIds || [])
|
|
10
10
|
.filter((id) => {
|
|
11
11
|
const openApiSourceId = options?.context?.userContext?.openAPISpec?.sourceId;
|
|
@@ -17,8 +17,8 @@ export default function transformDetailDatasources({ sourceIds }, options = {})
|
|
|
17
17
|
})
|
|
18
18
|
.map((id) => {
|
|
19
19
|
try {
|
|
20
|
-
if (
|
|
21
|
-
const findFile =
|
|
20
|
+
if (isRemoteFile(id)) {
|
|
21
|
+
const findFile = remoteFileList.find((f) => f.sourceId === id);
|
|
22
22
|
if (findFile) {
|
|
23
23
|
return `// sourceId: ${id}\n${findFile.content}\n`;
|
|
24
24
|
}
|
package/aigne.yaml
CHANGED
|
@@ -18,7 +18,7 @@ agents:
|
|
|
18
18
|
- ./agents/generate/check-document-structure.yaml
|
|
19
19
|
- ./agents/generate/user-review-document-structure.mjs
|
|
20
20
|
- ./agents/generate/index.yaml
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
# Documentation Structure Tools
|
|
23
23
|
- ./agents/generate/document-structure-tools/add-document.mjs
|
|
24
24
|
- ./agents/generate/document-structure-tools/delete-document.mjs
|
|
@@ -47,6 +47,7 @@ agents:
|
|
|
47
47
|
|
|
48
48
|
# Publishing
|
|
49
49
|
- ./agents/publish/publish-docs.mjs
|
|
50
|
+
- ./agents/publish/translate-meta.mjs
|
|
50
51
|
- ./agents/publish/index.yaml
|
|
51
52
|
|
|
52
53
|
# Media
|
|
@@ -65,9 +66,11 @@ agents:
|
|
|
65
66
|
|
|
66
67
|
# Utilities
|
|
67
68
|
- ./agents/utils/load-sources.mjs
|
|
68
|
-
- ./agents/utils/
|
|
69
|
+
- ./agents/utils/post-generate.mjs
|
|
70
|
+
- ./agents/utils/save-sidebar.mjs
|
|
69
71
|
- ./agents/utils/transform-detail-datasources.mjs
|
|
70
|
-
- ./agents/utils/save-
|
|
72
|
+
- ./agents/utils/save-doc.mjs
|
|
73
|
+
- ./agents/utils/save-doc-translation.mjs
|
|
71
74
|
- ./agents/utils/save-output.mjs
|
|
72
75
|
- ./agents/utils/format-document-structure.mjs
|
|
73
76
|
- ./agents/utils/find-item-by-path.mjs
|
package/package.json
CHANGED
|
@@ -97,6 +97,7 @@ Generate detailed and well-structured document for the current {{nodeName}} base
|
|
|
97
97
|
YOU SHOULD:
|
|
98
98
|
- Use AFS tools `afs_list` `afs_search` or `afs_read` to gather relevant and accurate information to enhance the content.
|
|
99
99
|
- Follow rules in `<diagram_generation_guide>`: use `generateDiagram` tool to create and embed a diagram when appropriate, following the diagram generation guidelines.
|
|
100
|
+
- If the `generateDiagram` tool is not called, do not attempt to add any diagrams.
|
|
100
101
|
|
|
101
102
|
<steps>
|
|
102
103
|
1. Analyze the provided document structure and user requirements to plan the content.
|
package/utils/file-utils.mjs
CHANGED
|
@@ -286,7 +286,7 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
if (
|
|
289
|
+
if (isRemoteFile(dir)) {
|
|
290
290
|
allFiles.push(dir);
|
|
291
291
|
continue;
|
|
292
292
|
}
|
|
@@ -387,8 +387,8 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
|
387
387
|
* @returns {Promise<boolean>} True if file appears to be a text file
|
|
388
388
|
*/
|
|
389
389
|
async function isTextFile(filePath) {
|
|
390
|
-
if (
|
|
391
|
-
return
|
|
390
|
+
if (isRemoteFile(filePath)) {
|
|
391
|
+
return isRemoteTextFile(filePath);
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
try {
|
|
@@ -400,14 +400,21 @@ async function isTextFile(filePath) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Check if a string is an HTTP/HTTPS URL
|
|
405
|
+
* @param {string} fileUrl - The string to check
|
|
406
|
+
* @returns {boolean} - True if the string starts with http:// or https://
|
|
407
|
+
*/
|
|
408
|
+
export function isRemoteFile(fileUrl) {
|
|
409
|
+
if (typeof fileUrl !== "string") return false;
|
|
410
|
+
|
|
411
|
+
if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) {
|
|
405
412
|
return true;
|
|
406
413
|
}
|
|
407
414
|
return false;
|
|
408
415
|
}
|
|
409
416
|
|
|
410
|
-
export async function
|
|
417
|
+
export async function isRemoteTextFile(fileUrl) {
|
|
411
418
|
try {
|
|
412
419
|
const res = await fetch(fileUrl, {
|
|
413
420
|
method: "HEAD",
|
|
@@ -435,14 +442,14 @@ export async function checkIsHttpTextFile(fileUrl) {
|
|
|
435
442
|
}
|
|
436
443
|
}
|
|
437
444
|
|
|
438
|
-
export async function
|
|
439
|
-
if (!
|
|
445
|
+
export async function getRemoteFileContent(fileUrl) {
|
|
446
|
+
if (!fileUrl) return null;
|
|
440
447
|
try {
|
|
441
|
-
const res = await fetch(
|
|
448
|
+
const res = await fetch(fileUrl);
|
|
442
449
|
const text = await res.text();
|
|
443
450
|
return text;
|
|
444
451
|
} catch (error) {
|
|
445
|
-
debug(`Failed to fetch HTTP file content: ${
|
|
452
|
+
debug(`Failed to fetch HTTP file content: ${fileUrl} - ${error.message}`);
|
|
446
453
|
return null;
|
|
447
454
|
}
|
|
448
455
|
}
|
|
@@ -469,8 +476,8 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
|
|
|
469
476
|
}
|
|
470
477
|
|
|
471
478
|
try {
|
|
472
|
-
if (
|
|
473
|
-
const content = await
|
|
479
|
+
if (isRemoteFile(file)) {
|
|
480
|
+
const content = await getRemoteFileContent(file);
|
|
474
481
|
if (content) {
|
|
475
482
|
return {
|
|
476
483
|
sourceId: file,
|
package/utils/load-config.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export default async function loadConfig({ config, appUrl }) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Parse new configuration fields and convert keys to actual content
|
|
31
|
-
const processedConfig = processConfigFields(parsedConfig);
|
|
31
|
+
const processedConfig = await processConfigFields(parsedConfig);
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
lastGitHead: parsedConfig.lastGitHead || "",
|
package/utils/utils.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SUPPORTED_LANGUAGES,
|
|
19
19
|
TARGET_AUDIENCES,
|
|
20
20
|
} from "./constants/index.mjs";
|
|
21
|
+
import { isRemoteFile, getRemoteFileContent } from "./file-utils.mjs";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Normalize path to absolute path for consistent comparison
|
|
@@ -47,16 +48,6 @@ export function isGlobPattern(pattern) {
|
|
|
47
48
|
return /[*?[\]]|(\*\*)/.test(pattern);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
* Check if a string is an HTTP/HTTPS URL
|
|
52
|
-
* @param {string} url - The string to check
|
|
53
|
-
* @returns {boolean} - True if the string starts with http:// or https://
|
|
54
|
-
*/
|
|
55
|
-
export function isHttp(url) {
|
|
56
|
-
if (typeof url !== "string") return false;
|
|
57
|
-
return url.startsWith("http://") || url.startsWith("https://");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
export function processContent({ content }) {
|
|
61
52
|
// Match markdown regular links [text](link), exclude images 
|
|
62
53
|
return content.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, (match, text, link) => {
|
|
@@ -81,77 +72,72 @@ export function processContent({ content }) {
|
|
|
81
72
|
});
|
|
82
73
|
}
|
|
83
74
|
|
|
75
|
+
// Helper function to generate filename based on language
|
|
76
|
+
export function getFileName(docPath, language) {
|
|
77
|
+
// Flatten path: remove leading /, replace all / with -
|
|
78
|
+
const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
|
|
79
|
+
const isEnglish = language === "en";
|
|
80
|
+
return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
|
|
81
|
+
}
|
|
82
|
+
|
|
84
83
|
/**
|
|
85
|
-
* Save a single document
|
|
84
|
+
* Save a single document to files
|
|
86
85
|
* @param {Object} params
|
|
87
86
|
* @param {string} params.path - Relative path (without extension)
|
|
88
87
|
* @param {string} params.content - Main document content
|
|
89
88
|
* @param {string} params.docsDir - Root directory
|
|
90
89
|
* @param {string} params.locale - Main content language (e.g., 'en', 'zh', 'fr')
|
|
91
|
-
* @param {Array<{language: string, translation: string}>} [params.translates] - Translation content
|
|
92
90
|
* @param {Array<string>} [params.labels] - Document labels for front matter
|
|
93
|
-
* @returns {Promise<
|
|
91
|
+
* @returns {Promise<{ path: string, success: boolean, error?: string }>}
|
|
94
92
|
*/
|
|
95
|
-
export async function
|
|
96
|
-
path: docPath,
|
|
97
|
-
content,
|
|
98
|
-
docsDir,
|
|
99
|
-
locale,
|
|
100
|
-
translates = [],
|
|
101
|
-
labels,
|
|
102
|
-
isTranslate = false,
|
|
103
|
-
}) {
|
|
104
|
-
const results = [];
|
|
93
|
+
export async function saveDoc({ path: docPath, content, docsDir, locale, labels }) {
|
|
105
94
|
try {
|
|
106
|
-
// Flatten path: remove leading /, replace all / with -
|
|
107
|
-
const flatName = docPath.replace(/^\//, "").replace(/\//g, "-");
|
|
108
95
|
await fs.mkdir(docsDir, { recursive: true });
|
|
96
|
+
const mainFileName = getFileName(docPath, locale);
|
|
97
|
+
const mainFilePath = path.join(docsDir, mainFileName);
|
|
109
98
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
const isEnglish = language === "en";
|
|
113
|
-
return isEnglish ? `${flatName}.md` : `${flatName}.${language}.md`;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Save main content with appropriate filename based on locale (skip if isTranslate is true)
|
|
117
|
-
if (!isTranslate) {
|
|
118
|
-
const mainFileName = getFileName(locale);
|
|
119
|
-
const mainFilePath = path.join(docsDir, mainFileName);
|
|
120
|
-
|
|
121
|
-
// Add labels front matter if labels are provided
|
|
122
|
-
let finalContent = processContent({ content });
|
|
99
|
+
// Add labels front matter if labels are provided
|
|
100
|
+
let finalContent = processContent({ content });
|
|
123
101
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
await fs.writeFile(mainFilePath, finalContent, "utf8");
|
|
130
|
-
results.push({ path: mainFilePath, success: true });
|
|
102
|
+
if (labels && labels.length > 0) {
|
|
103
|
+
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
104
|
+
finalContent = frontMatter + finalContent;
|
|
131
105
|
}
|
|
132
106
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
107
|
+
await fs.writeFile(mainFilePath, finalContent, "utf8");
|
|
108
|
+
return { path: mainFilePath, success: true };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { path: docPath, success: false, error: err.message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
137
113
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
114
|
+
export async function saveDocTranslation({
|
|
115
|
+
path: docPath,
|
|
116
|
+
docsDir,
|
|
117
|
+
translation,
|
|
118
|
+
language,
|
|
119
|
+
labels,
|
|
120
|
+
}) {
|
|
121
|
+
try {
|
|
122
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
123
|
+
const translateFileName = getFileName(docPath, language);
|
|
124
|
+
const translatePath = path.join(docsDir, translateFileName);
|
|
142
125
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
// Add labels front matter to translation content if labels are provided
|
|
127
|
+
let finalTranslationContent = processContent({
|
|
128
|
+
content: translation,
|
|
129
|
+
});
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
if (labels && labels.length > 0) {
|
|
132
|
+
const frontMatter = `---\nlabels: ${JSON.stringify(labels)}\n---\n\n`;
|
|
133
|
+
finalTranslationContent = frontMatter + finalTranslationContent;
|
|
150
134
|
}
|
|
135
|
+
|
|
136
|
+
await fs.writeFile(translatePath, finalTranslationContent, "utf8");
|
|
137
|
+
return { path: translatePath, success: true };
|
|
151
138
|
} catch (err) {
|
|
152
|
-
|
|
139
|
+
return { path: docPath, success: false, error: err.message };
|
|
153
140
|
}
|
|
154
|
-
return results;
|
|
155
141
|
}
|
|
156
142
|
|
|
157
143
|
/**
|
|
@@ -963,7 +949,7 @@ export function processTargetAudience(targetAudienceTypes, existingTargetAudienc
|
|
|
963
949
|
* @param {Object} config - Parsed configuration
|
|
964
950
|
* @returns {Object} Processed configuration with content fields
|
|
965
951
|
*/
|
|
966
|
-
export function processConfigFields(config) {
|
|
952
|
+
export async function processConfigFields(config) {
|
|
967
953
|
const processed = {};
|
|
968
954
|
const allRulesContent = [];
|
|
969
955
|
|
|
@@ -995,7 +981,15 @@ export function processConfigFields(config) {
|
|
|
995
981
|
if (typeof config.rules === "string") {
|
|
996
982
|
const existingRules = config.rules.trim();
|
|
997
983
|
if (existingRules) {
|
|
998
|
-
|
|
984
|
+
// load rules from remote url
|
|
985
|
+
if (isRemoteFile(existingRules)) {
|
|
986
|
+
const remoteFileContent = await getRemoteFileContent(existingRules);
|
|
987
|
+
if (remoteFileContent) {
|
|
988
|
+
allRulesContent.push(remoteFileContent);
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
allRulesContent.push(existingRules);
|
|
992
|
+
}
|
|
999
993
|
}
|
|
1000
994
|
} else if (Array.isArray(config.rules)) {
|
|
1001
995
|
// Handle array of rules - join them with newlines
|
|
@@ -1054,6 +1048,12 @@ export function processConfigFields(config) {
|
|
|
1054
1048
|
}
|
|
1055
1049
|
}
|
|
1056
1050
|
|
|
1051
|
+
if (config.glossary) {
|
|
1052
|
+
if (isRemoteFile(config.glossary)) {
|
|
1053
|
+
processed.glossary = await getRemoteFileContent(config.glossary);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
1057
|
// Detect and handle conflicts in user selections
|
|
1058
1058
|
const conflicts = detectResolvableConflicts(config);
|
|
1059
1059
|
if (conflicts.length > 0) {
|
|
@@ -1097,8 +1097,10 @@ export function processConfigFields(config) {
|
|
|
1097
1097
|
* @returns {Promise<any>} - The processed configuration with file content loaded in place of references.
|
|
1098
1098
|
*/
|
|
1099
1099
|
export async function resolveFileReferences(obj, basePath = process.cwd()) {
|
|
1100
|
-
if (typeof obj === "string"
|
|
1101
|
-
|
|
1100
|
+
if (typeof obj === "string") {
|
|
1101
|
+
if (obj.startsWith("@")) {
|
|
1102
|
+
return await loadFileContent(obj.slice(1), basePath);
|
|
1103
|
+
}
|
|
1102
1104
|
}
|
|
1103
1105
|
|
|
1104
1106
|
if (Array.isArray(obj)) {
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { shutdownMermaidWorkerPool } from "../../utils/mermaid-worker-pool.mjs";
|
|
2
|
-
import { saveDocWithTranslations } from "../../utils/utils.mjs";
|
|
3
|
-
|
|
4
|
-
export default async function saveSingleDoc({
|
|
5
|
-
path,
|
|
6
|
-
content,
|
|
7
|
-
docsDir,
|
|
8
|
-
translates,
|
|
9
|
-
labels,
|
|
10
|
-
locale,
|
|
11
|
-
isTranslate = false,
|
|
12
|
-
isShowMessage = false,
|
|
13
|
-
}) {
|
|
14
|
-
const _results = await saveDocWithTranslations({
|
|
15
|
-
path,
|
|
16
|
-
content,
|
|
17
|
-
docsDir,
|
|
18
|
-
translates,
|
|
19
|
-
labels,
|
|
20
|
-
locale,
|
|
21
|
-
isTranslate,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
if (isShowMessage) {
|
|
25
|
-
// Shutdown mermaid worker pool to ensure clean exit
|
|
26
|
-
try {
|
|
27
|
-
await shutdownMermaidWorkerPool();
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.warn("Failed to shutdown mermaid worker pool:", error.message);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const message = isTranslate
|
|
33
|
-
? `ā
Translation completed successfully`
|
|
34
|
-
: `ā
Document updated successfully`;
|
|
35
|
-
return { message };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
saveSingleDoc.task_render_mode = "hide";
|