@aigne/doc-smith 0.8.15-beta → 0.8.15-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/agents/generate/draw-diagram.yaml +4 -0
- package/agents/update/check-document.mjs +15 -0
- package/agents/update/generate-document.yaml +4 -0
- package/agents/utils/choose-docs.mjs +6 -1
- package/agents/utils/load-sources.mjs +80 -22
- package/agents/utils/transform-detail-datasources.mjs +24 -2
- package/package.json +1 -1
- package/prompts/detail/d2-diagram/system-prompt.md +33 -7
- package/prompts/detail/d2-diagram/user-prompt.md +11 -1
- package/prompts/detail/generate/user-prompt.md +37 -0
- package/prompts/structure/generate/user-prompt.md +34 -0
- package/utils/file-utils.mjs +83 -7
- package/utils/openapi/index.mjs +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [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
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* locale not applied when generating D2 diagrams ([#215](https://github.com/AIGNE-io/aigne-doc-smith/issues/215)) ([7e6f42f](https://github.com/AIGNE-io/aigne-doc-smith/commit/7e6f42faf15a115e53c57dbc2d5a19a82cf44295))
|
|
9
|
+
|
|
10
|
+
## [0.8.15-beta.1](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.15-beta...v0.8.15-beta.1) (2025-10-23)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* support openapi datasource, generate better api docs ([#212](https://github.com/AIGNE-io/aigne-doc-smith/issues/212)) ([6a683f1](https://github.com/AIGNE-io/aigne-doc-smith/commit/6a683f117aa94f265383be6e1b3b957803004032))
|
|
16
|
+
|
|
3
17
|
## [0.8.15-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.14...v0.8.15-beta) (2025-10-21)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -11,6 +11,10 @@ input_schema:
|
|
|
11
11
|
documentContent:
|
|
12
12
|
type: string
|
|
13
13
|
description: The **raw text content** of the current document. (**Note:** This is the original document and **does not include** any diagram source code.)
|
|
14
|
+
locale:
|
|
15
|
+
type: string
|
|
16
|
+
description: Language for diagram labels and text
|
|
17
|
+
default: en
|
|
14
18
|
required:
|
|
15
19
|
- documentContent
|
|
16
20
|
output_schema:
|
|
@@ -2,6 +2,7 @@ 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
|
+
|
|
5
6
|
import checkDetailResult from "../utils/check-detail-result.mjs";
|
|
6
7
|
|
|
7
8
|
// Get current script directory
|
|
@@ -102,6 +103,19 @@ export default async function checkDocument(
|
|
|
102
103
|
options.context.agents["saveSingleDoc"],
|
|
103
104
|
],
|
|
104
105
|
});
|
|
106
|
+
let openAPISpec = null;
|
|
107
|
+
|
|
108
|
+
if (options.context?.userContext?.openAPISpec?.sourceId) {
|
|
109
|
+
const matchingDocument = originalDocumentStructure.find((item) => {
|
|
110
|
+
if (item.path === path) {
|
|
111
|
+
return item.sourceIds.find((x) => x === options.context.userContext.openAPISpec.sourceId);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
});
|
|
115
|
+
if (matchingDocument) {
|
|
116
|
+
openAPISpec = options.context.userContext.openAPISpec;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
105
119
|
|
|
106
120
|
const result = await options.context.invoke(teamAgent, {
|
|
107
121
|
...rest,
|
|
@@ -112,6 +126,7 @@ export default async function checkDocument(
|
|
|
112
126
|
originalDocumentStructure,
|
|
113
127
|
documentStructure,
|
|
114
128
|
detailFeedback: contentValidationFailed ? validationResult.detailFeedback : "",
|
|
129
|
+
openAPISpec,
|
|
115
130
|
});
|
|
116
131
|
|
|
117
132
|
return {
|
|
@@ -76,6 +76,10 @@ skills:
|
|
|
76
76
|
documentContent:
|
|
77
77
|
type: string
|
|
78
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
|
|
79
83
|
required:
|
|
80
84
|
- documentContent
|
|
81
85
|
output_schema:
|
|
@@ -50,7 +50,12 @@ export default async function chooseDocs(
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
// Use title if available, otherwise fall back to filename
|
|
53
|
-
|
|
53
|
+
let displayName = docItem?.title;
|
|
54
|
+
if (displayName) {
|
|
55
|
+
displayName = `${displayName} (${file})`;
|
|
56
|
+
} else {
|
|
57
|
+
displayName = file;
|
|
58
|
+
}
|
|
54
59
|
|
|
55
60
|
return {
|
|
56
61
|
name: displayName,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import imageSize from "image-size";
|
|
4
5
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
loadFilesFromPaths,
|
|
8
9
|
readFileContents,
|
|
9
10
|
getMimeType,
|
|
11
|
+
checkIsRemoteFile,
|
|
10
12
|
} from "../../utils/file-utils.mjs";
|
|
11
13
|
import {
|
|
12
14
|
getCurrentGitHead,
|
|
@@ -18,28 +20,63 @@ import {
|
|
|
18
20
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
19
21
|
DEFAULT_INCLUDE_PATTERNS,
|
|
20
22
|
} from "../../utils/constants/index.mjs";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
import { isOpenAPISpecFile } from "../../utils/openapi/index.mjs";
|
|
24
|
+
|
|
25
|
+
export default async function loadSources(
|
|
26
|
+
{
|
|
27
|
+
sources = [],
|
|
28
|
+
sourcesPath = [],
|
|
29
|
+
includePatterns,
|
|
30
|
+
excludePatterns,
|
|
31
|
+
outputDir,
|
|
32
|
+
docsDir,
|
|
33
|
+
"doc-path": docPath,
|
|
34
|
+
boardId,
|
|
35
|
+
useDefaultPatterns = true,
|
|
36
|
+
lastGitHead,
|
|
37
|
+
reset = false,
|
|
38
|
+
media,
|
|
39
|
+
} = {},
|
|
40
|
+
options,
|
|
41
|
+
) {
|
|
36
42
|
let files = Array.isArray(sources) ? [...sources] : [];
|
|
37
43
|
const { minImageWidth } = media || { minImageWidth: 800 };
|
|
38
44
|
|
|
39
45
|
if (sourcesPath) {
|
|
40
|
-
const
|
|
46
|
+
const sourcesPathList = Array.isArray(sourcesPath) ? sourcesPath : [sourcesPath];
|
|
47
|
+
const pickSourcesPath = [];
|
|
48
|
+
const omitSourcesPath = [];
|
|
49
|
+
sourcesPathList.forEach((x) => {
|
|
50
|
+
if (typeof x !== "string" || !x) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (x.startsWith("!")) {
|
|
54
|
+
omitSourcesPath.push(x.substring(1));
|
|
55
|
+
} else {
|
|
56
|
+
pickSourcesPath.push(x);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const customExcludePatterns = omitSourcesPath
|
|
61
|
+
.map((x) => {
|
|
62
|
+
try {
|
|
63
|
+
const stats = statSync(x);
|
|
64
|
+
if (stats.isFile()) {
|
|
65
|
+
return x;
|
|
66
|
+
}
|
|
67
|
+
if (stats.isDirectory()) {
|
|
68
|
+
return `${x}/**`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.warn(`Failed to stat path ${x}: ${error.message}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
const allFiles = await loadFilesFromPaths(pickSourcesPath, {
|
|
41
78
|
includePatterns,
|
|
42
|
-
excludePatterns,
|
|
79
|
+
excludePatterns: [...new Set([...(excludePatterns || []), ...customExcludePatterns])],
|
|
43
80
|
useDefaultPatterns,
|
|
44
81
|
defaultIncludePatterns: DEFAULT_INCLUDE_PATTERNS,
|
|
45
82
|
defaultExcludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
@@ -50,9 +87,6 @@ export default async function loadSources({
|
|
|
50
87
|
|
|
51
88
|
files = [...new Set(files)];
|
|
52
89
|
|
|
53
|
-
// all files path
|
|
54
|
-
const allFilesPaths = files.map((file) => `- ${toRelativePath(file)}`).join("\n");
|
|
55
|
-
|
|
56
90
|
// Define media file extensions
|
|
57
91
|
const mediaExtensions = [
|
|
58
92
|
".jpg",
|
|
@@ -110,7 +144,7 @@ export default async function loadSources({
|
|
|
110
144
|
files.map(async (file) => {
|
|
111
145
|
const ext = path.extname(file).toLowerCase();
|
|
112
146
|
|
|
113
|
-
if (mediaExtensions.includes(ext)) {
|
|
147
|
+
if (mediaExtensions.includes(ext) && !checkIsRemoteFile(file)) {
|
|
114
148
|
// This is a media file
|
|
115
149
|
const relativePath = path.relative(docsDir, file);
|
|
116
150
|
const fileName = path.basename(file);
|
|
@@ -161,7 +195,7 @@ export default async function loadSources({
|
|
|
161
195
|
}
|
|
162
196
|
|
|
163
197
|
// Read all source files using the utility function
|
|
164
|
-
|
|
198
|
+
let sourceFiles = await readFileContents(sourceFilesPaths, process.cwd());
|
|
165
199
|
|
|
166
200
|
// Count tokens and lines using utility function
|
|
167
201
|
const { totalTokens, totalLines } = calculateFileStats(sourceFiles);
|
|
@@ -169,8 +203,32 @@ export default async function loadSources({
|
|
|
169
203
|
// check if totalTokens is too large
|
|
170
204
|
const isLargeContext = totalTokens > INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD;
|
|
171
205
|
|
|
206
|
+
// filter OpenAPI doc should after check isLargeContext
|
|
207
|
+
sourceFiles = sourceFiles.filter((file) => {
|
|
208
|
+
if (options?.context?.userContext.openAPISpec) return true;
|
|
209
|
+
|
|
210
|
+
const isOpenAPI = isOpenAPISpecFile(file.content);
|
|
211
|
+
if (isOpenAPI && options?.context?.userContext) {
|
|
212
|
+
options.context.userContext.openAPISpec = file;
|
|
213
|
+
}
|
|
214
|
+
return !isOpenAPI;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const httpFileList = [];
|
|
218
|
+
|
|
219
|
+
sourceFiles.forEach((file) => {
|
|
220
|
+
if (checkIsRemoteFile(file.sourceId)) {
|
|
221
|
+
httpFileList.push(file);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (options?.context?.userContext) {
|
|
225
|
+
options.context.userContext.httpFileList = httpFileList;
|
|
226
|
+
}
|
|
227
|
+
|
|
172
228
|
// Build allSources string using utility function
|
|
173
229
|
const allSources = buildSourcesContent(sourceFiles, isLargeContext);
|
|
230
|
+
// all files path
|
|
231
|
+
const allFilesPaths = sourceFiles.map((x) => `- ${toRelativePath(x.sourceId)}`).join("\n");
|
|
174
232
|
|
|
175
233
|
// Get the last documentation structure
|
|
176
234
|
let originalDocumentStructure;
|
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { normalizePath, toRelativePath } from "../../utils/utils.mjs";
|
|
3
|
+
import { checkIsRemoteFile } from "../../utils/file-utils.mjs";
|
|
3
4
|
|
|
4
|
-
export default function transformDetailDatasources({ sourceIds }) {
|
|
5
|
+
export default function transformDetailDatasources({ sourceIds }, options = {}) {
|
|
5
6
|
// Read file content for each sourceId, ignoring failures
|
|
7
|
+
let openAPISpec;
|
|
8
|
+
const httpFileList = options?.context?.userContext?.httpFileList || [];
|
|
6
9
|
const contents = (sourceIds || [])
|
|
10
|
+
.filter((id) => {
|
|
11
|
+
const openApiSourceId = options?.context?.userContext?.openAPISpec?.sourceId;
|
|
12
|
+
if (openApiSourceId !== undefined && openApiSourceId === id) {
|
|
13
|
+
openAPISpec = options.context.userContext.openAPISpec;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
})
|
|
7
18
|
.map((id) => {
|
|
8
19
|
try {
|
|
20
|
+
if (checkIsRemoteFile(id)) {
|
|
21
|
+
const findFile = httpFileList.find((f) => f.sourceId === id);
|
|
22
|
+
if (findFile) {
|
|
23
|
+
return `// sourceId: ${id}\n${findFile.content}\n`;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
const normalizedId = normalizePath(id);
|
|
10
29
|
const content = fs.readFileSync(normalizedId, "utf8");
|
|
11
30
|
const relativeId = toRelativePath(id);
|
|
@@ -17,7 +36,10 @@ export default function transformDetailDatasources({ sourceIds }) {
|
|
|
17
36
|
})
|
|
18
37
|
.filter(Boolean);
|
|
19
38
|
|
|
20
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
detailDataSources: contents.join(""),
|
|
41
|
+
openAPISpec,
|
|
42
|
+
};
|
|
21
43
|
}
|
|
22
44
|
|
|
23
45
|
transformDetailDatasources.task_render_mode = "hide";
|
package/package.json
CHANGED
|
@@ -913,6 +913,28 @@ Ensure that the shape names used in connections are accurate and match the actua
|
|
|
913
913
|
- **Good Practice:**
|
|
914
914
|
```d2
|
|
915
915
|
shape: sequence_diagram
|
|
916
|
+
User: {
|
|
917
|
+
shape: c4-person
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
App: {
|
|
921
|
+
label: "Your Application"
|
|
922
|
+
shape: rectangle
|
|
923
|
+
|
|
924
|
+
ResumeSubscription: {
|
|
925
|
+
label: "ResumeSubscription Component"
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
Payment-API: {
|
|
930
|
+
label: "Payment Backend API"
|
|
931
|
+
shape: rectangle
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
DID-Wallet: {
|
|
935
|
+
label: "DID Wallet"
|
|
936
|
+
icon: "https://www.arcblock.io/image-bin/uploads/37198ddc4a0b9e91e5c1c821ab895a34.svg"
|
|
937
|
+
}
|
|
916
938
|
|
|
917
939
|
User -> App.ResumeSubscription: "1. Triggers resume action"
|
|
918
940
|
|
|
@@ -922,12 +944,16 @@ Ensure that the shape names used in connections are accurate and match the actua
|
|
|
922
944
|
App.ResumeSubscription.t1 -> User: "4. Display confirmation dialog"
|
|
923
945
|
User -> App.ResumeSubscription.t1: "5. Clicks 'Confirm'"
|
|
924
946
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
947
|
+
"If Re-Staking is Required": {
|
|
948
|
+
App.ResumeSubscription.t1 -> DID-Wallet: "6a. Open 're-stake' session"
|
|
949
|
+
User -> DID-Wallet: "7a. Approve in wallet"
|
|
950
|
+
DID-Wallet -> App.ResumeSubscription.t1: "8a. Send success callback"
|
|
951
|
+
}
|
|
928
952
|
|
|
929
|
-
|
|
930
|
-
|
|
953
|
+
"If No Staking is Required": {
|
|
954
|
+
App.ResumeSubscription.t1 -> Payment-API: "6b. Call recover endpoint\n(PUT /recover)"
|
|
955
|
+
Payment-API -> App.ResumeSubscription.t1: "7b. Return success"
|
|
956
|
+
}
|
|
931
957
|
|
|
932
958
|
App.ResumeSubscription.t1 -> Payment-API: "9. Fetch updated subscription details"
|
|
933
959
|
Payment-API -> App.ResumeSubscription.t1: "10. Return latest subscription"
|
|
@@ -1086,12 +1112,12 @@ Ensure that the shape names used in connections are accurate and match the actua
|
|
|
1086
1112
|
Blocklet-Service -> Application.Auth-Middleware: "4. Return permissions"
|
|
1087
1113
|
Application.Auth-Middleware -> Application.Auth-Middleware: "5. Evaluate all rules"
|
|
1088
1114
|
|
|
1089
|
-
"If Authorized" {
|
|
1115
|
+
"If Authorized": {
|
|
1090
1116
|
Application.Auth-Middleware -> Application.Protected-Route: "6a. next()"
|
|
1091
1117
|
Application.Protected-Route -> Client: "7a. 200 OK Response"
|
|
1092
1118
|
}
|
|
1093
1119
|
|
|
1094
|
-
"If Forbidden" {
|
|
1120
|
+
"If Forbidden": {
|
|
1095
1121
|
Application.Auth-Middleware -> Client: "6b. 403 Forbidden Response"
|
|
1096
1122
|
}
|
|
1097
1123
|
```
|
|
@@ -2,6 +2,16 @@ Follow the given rules and ISTJ style from your system instructions.
|
|
|
2
2
|
|
|
3
3
|
Generate a d2 diagram that represents the following document content:
|
|
4
4
|
|
|
5
|
+
<user_locale>
|
|
6
|
+
{{ locale }}
|
|
7
|
+
</user_locale>
|
|
8
|
+
|
|
9
|
+
<user_rules>
|
|
10
|
+
|
|
11
|
+
- Output only the diagram labels and text in the {{ locale }} language — keep all variable names, component names, and syntax unchanged.
|
|
12
|
+
|
|
13
|
+
</user_rules>
|
|
14
|
+
|
|
5
15
|
<document_content>
|
|
6
16
|
{{documentContent}}
|
|
7
|
-
</document_content>
|
|
17
|
+
</document_content>
|
|
@@ -27,6 +27,43 @@
|
|
|
27
27
|
|
|
28
28
|
</datasources>
|
|
29
29
|
|
|
30
|
+
{% if openAPISpec %}
|
|
31
|
+
<openapi>
|
|
32
|
+
|
|
33
|
+
**Goal:** Use the provided OpenAPI (Swagger) specification, align it with the current page objective, and leverage it to refine this document.
|
|
34
|
+
|
|
35
|
+
**OpenAPI File Content:**
|
|
36
|
+
<openapi_doc>
|
|
37
|
+
|
|
38
|
+
{{ openAPISpec }}
|
|
39
|
+
|
|
40
|
+
</openapi_doc>
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### **Documentation Requirements and Constraints**
|
|
45
|
+
|
|
46
|
+
1. **Extract the core content:**
|
|
47
|
+
* Organize the document by functional modules.
|
|
48
|
+
* For each path item, include the following elements:
|
|
49
|
+
* HTTP method and path.
|
|
50
|
+
* Concise summary.
|
|
51
|
+
* Detailed description.
|
|
52
|
+
* Request parameters: name, location (`in`), type, required flag, description.
|
|
53
|
+
* Request body: describe its structure when present.
|
|
54
|
+
* Responses: at least the key status codes (e.g., 200, 201, 400, 500) and their schemas.
|
|
55
|
+
|
|
56
|
+
2. **Mandatory API description constraints (deduplication rule):**
|
|
57
|
+
* **Ensure that throughout the document (including preface, overview, etc.), any introduction to the project APIs appears only within this OpenAPI-generated "API reference" section.**
|
|
58
|
+
* **Never** repeat or expand the interface list elsewhere in the document (for example, "Quick Start" or "Architecture Overview" sections).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
**Expected output format:** A concise, clear, and easy-to-scan Markdown document.
|
|
63
|
+
|
|
64
|
+
</openapi>
|
|
65
|
+
{% endif %}
|
|
66
|
+
|
|
30
67
|
|
|
31
68
|
{% include "./detail-example.md" %}
|
|
32
69
|
|
|
@@ -12,6 +12,40 @@
|
|
|
12
12
|
{{ datasources }}
|
|
13
13
|
</datasources>
|
|
14
14
|
|
|
15
|
+
{% if userContext.openAPISpec %}
|
|
16
|
+
<openapi>
|
|
17
|
+
|
|
18
|
+
**Goal:** Use the provided OpenAPI (Swagger) specification to design how the OpenAPI content and the overall document should be structured together.
|
|
19
|
+
|
|
20
|
+
**OpenAPI File Content:**
|
|
21
|
+
<openapi_doc>
|
|
22
|
+
|
|
23
|
+
{{ userContext.openAPISpec }}
|
|
24
|
+
|
|
25
|
+
</openapi_doc>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### **Documentation Requirements and Constraints**
|
|
30
|
+
|
|
31
|
+
1. **Section structure and titles:**
|
|
32
|
+
* Create a dedicated top-level section for the OpenAPI content.
|
|
33
|
+
* The section title must be professional and user friendly; **never** include terms such as OpenAPI, Swagger, or file formats. Recommended titles include **"API Interface Reference"** or **"Interface Reference"**.
|
|
34
|
+
|
|
35
|
+
2. **Content hierarchy and presentation:**
|
|
36
|
+
* **Ideal state (single-level page):** Prefer to present all API endpoints within **one Markdown file (one page)**.
|
|
37
|
+
* **Split criteria (two-level pages):** Only when the number of endpoints is too large for a single file should you split by OpenAPI tags or logical modules, creating individual Markdown files per module.
|
|
38
|
+
* **Forced file hierarchy constraint:** Whether using one or two levels, the generated API reference files (Markdown) may contain **no more than two levels.**
|
|
39
|
+
* **Example (two-level structure):** `/api-reference.md` (index) -> `/api/user.md`, `/api/order.md` (module pages)
|
|
40
|
+
* **Disallow any third level or deeper structure:** for example, `/api/v1/user/get.md`.
|
|
41
|
+
|
|
42
|
+
3. **Mandatory API description constraints (deduplication rule):**
|
|
43
|
+
* **Ensure that for the entire document (including preface, overview, etc.), any introduction to the project APIs appears only within this OpenAPI-generated "API reference" section.**
|
|
44
|
+
* **Never** repeat or extend the API list elsewhere in the document (for example, "Quick Start" or "Architecture Overview" sections).
|
|
45
|
+
|
|
46
|
+
</openapi>
|
|
47
|
+
{% endif %}
|
|
48
|
+
|
|
15
49
|
|
|
16
50
|
{% if originalDocumentStructure %}
|
|
17
51
|
<last_document_structure>
|
package/utils/file-utils.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { isBinaryFile } from "isbinaryfile";
|
|
|
8
8
|
import { encode } from "gpt-tokenizer";
|
|
9
9
|
import { fileTypeFromBuffer } from "file-type";
|
|
10
10
|
import { gunzipSync } from "node:zlib";
|
|
11
|
+
|
|
12
|
+
import { debug } from "./debug.mjs";
|
|
11
13
|
import { isGlobPattern } from "./utils.mjs";
|
|
12
14
|
import { INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD } from "./constants/index.mjs";
|
|
13
15
|
import { uploadFiles } from "./upload-files.mjs";
|
|
@@ -284,6 +286,11 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
|
284
286
|
continue;
|
|
285
287
|
}
|
|
286
288
|
|
|
289
|
+
if (checkIsRemoteFile(dir)) {
|
|
290
|
+
allFiles.push(dir);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
287
294
|
// First try to access as a file or directory
|
|
288
295
|
const stats = await stat(dir);
|
|
289
296
|
|
|
@@ -313,7 +320,13 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
|
313
320
|
: [];
|
|
314
321
|
|
|
315
322
|
finalIncludePatterns = [...defaultIncludePatterns, ...userInclude];
|
|
316
|
-
finalExcludePatterns = [
|
|
323
|
+
finalExcludePatterns = [
|
|
324
|
+
...defaultExcludePatterns,
|
|
325
|
+
...userExclude.map((x) => {
|
|
326
|
+
const prefix = `${dir}/`;
|
|
327
|
+
return x.startsWith(prefix) ? x.slice(prefix.length) : x;
|
|
328
|
+
}),
|
|
329
|
+
];
|
|
317
330
|
} else {
|
|
318
331
|
// Use only user patterns
|
|
319
332
|
if (includePatterns) {
|
|
@@ -374,6 +387,10 @@ export async function loadFilesFromPaths(sourcesPath, options = {}) {
|
|
|
374
387
|
* @returns {Promise<boolean>} True if file appears to be a text file
|
|
375
388
|
*/
|
|
376
389
|
async function isTextFile(filePath) {
|
|
390
|
+
if (checkIsRemoteFile(filePath)) {
|
|
391
|
+
return checkIsHttpTextFile(filePath);
|
|
392
|
+
}
|
|
393
|
+
|
|
377
394
|
try {
|
|
378
395
|
const isBinary = await isBinaryFile(filePath);
|
|
379
396
|
return !isBinary;
|
|
@@ -383,6 +400,53 @@ async function isTextFile(filePath) {
|
|
|
383
400
|
}
|
|
384
401
|
}
|
|
385
402
|
|
|
403
|
+
export function checkIsRemoteFile(filepath) {
|
|
404
|
+
if (filepath.startsWith("http://") || filepath.startsWith("https://")) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function checkIsHttpTextFile(fileUrl) {
|
|
411
|
+
try {
|
|
412
|
+
const res = await fetch(fileUrl, {
|
|
413
|
+
method: "HEAD",
|
|
414
|
+
});
|
|
415
|
+
const contentType = res.headers.get("content-type") || "";
|
|
416
|
+
const textMimeTypes = [
|
|
417
|
+
"application/json",
|
|
418
|
+
"application/ld+json",
|
|
419
|
+
"application/graphql+json",
|
|
420
|
+
"application/xml",
|
|
421
|
+
"application/xhtml+xml",
|
|
422
|
+
"application/javascript",
|
|
423
|
+
"application/ecmascript",
|
|
424
|
+
"application/x-www-form-urlencoded",
|
|
425
|
+
"application/rss+xml",
|
|
426
|
+
"application/atom+xml",
|
|
427
|
+
];
|
|
428
|
+
if (contentType.startsWith("text/") || textMimeTypes.includes(contentType)) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
debug(`Failed to check HTTP file content type: ${fileUrl} - ${error.message}`);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function getHttpFileContent(file) {
|
|
439
|
+
if (!file) return null;
|
|
440
|
+
try {
|
|
441
|
+
const res = await fetch(file);
|
|
442
|
+
const text = await res.text();
|
|
443
|
+
return text;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
debug(`Failed to fetch HTTP file content: ${file} - ${error.message}`);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
386
450
|
/**
|
|
387
451
|
* Read and parse file contents from an array of file paths
|
|
388
452
|
* @param {string[]} files - Array of file paths to read
|
|
@@ -405,12 +469,24 @@ export async function readFileContents(files, baseDir = process.cwd(), options =
|
|
|
405
469
|
}
|
|
406
470
|
|
|
407
471
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
472
|
+
if (checkIsRemoteFile(file)) {
|
|
473
|
+
const content = await getHttpFileContent(file);
|
|
474
|
+
if (content) {
|
|
475
|
+
return {
|
|
476
|
+
sourceId: file,
|
|
477
|
+
content,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return null;
|
|
482
|
+
} else {
|
|
483
|
+
const content = await readFile(file, "utf8");
|
|
484
|
+
const relativePath = path.relative(baseDir, file);
|
|
485
|
+
return {
|
|
486
|
+
sourceId: relativePath,
|
|
487
|
+
content,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
414
490
|
} catch (error) {
|
|
415
491
|
// If reading as text fails (e.g., binary file), skip it
|
|
416
492
|
console.warn(`Failed to read file as text: ${file} - ${error.message}`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parse } from "yaml";
|
|
2
|
+
|
|
3
|
+
export function isOpenAPISpecFile(content) {
|
|
4
|
+
const trimmedContent = content.trim();
|
|
5
|
+
try {
|
|
6
|
+
const parsed = parse(trimmedContent, {
|
|
7
|
+
logLevel: "silent",
|
|
8
|
+
});
|
|
9
|
+
if (parsed.openapi || parsed.swagger) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
//
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(trimmedContent);
|
|
17
|
+
if (parsed.openapi || parsed.swagger) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
//
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|