@aigne/doc-smith 0.8.13-beta → 0.8.14-beta
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
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.14-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.13...v0.8.14-beta) (2025-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **file-utils:** add comprehensive file type detection and image upload functionality ([#200](https://github.com/AIGNE-io/aigne-doc-smith/issues/200)) ([ab7aae3](https://github.com/AIGNE-io/aigne-doc-smith/commit/ab7aae33ed9b5209e824ba91fbf9c94d37187d83))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* use `output` field present tool result to enhance LLM understanding ([#202](https://github.com/AIGNE-io/aigne-doc-smith/issues/202)) ([511178d](https://github.com/AIGNE-io/aigne-doc-smith/commit/511178de7a88fdecf2136f90035d87957e23b23f))
|
|
14
|
+
|
|
15
|
+
## [0.8.13](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.13-beta...v0.8.13) (2025-10-16)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Miscellaneous Chores
|
|
19
|
+
|
|
20
|
+
* release 0.8.13 ([496e30f](https://github.com/AIGNE-io/aigne-doc-smith/commit/496e30f60d7e78602ffdc0fc070bf09ce4415fde))
|
|
21
|
+
|
|
3
22
|
## [0.8.13-beta](https://github.com/AIGNE-io/aigne-doc-smith/compare/v0.8.12...v0.8.13-beta) (2025-10-16)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { checkContent } from "../../utils/d2-utils.mjs";
|
|
2
2
|
|
|
3
|
-
export default async function checkD2DiagramIsValid({
|
|
3
|
+
export default async function checkD2DiagramIsValid({ output }) {
|
|
4
4
|
try {
|
|
5
|
-
await checkContent({ content:
|
|
5
|
+
await checkContent({ content: output });
|
|
6
6
|
return {
|
|
7
7
|
isValid: true,
|
|
8
8
|
};
|
|
@@ -17,12 +17,12 @@ export default async function checkD2DiagramIsValid({ diagramSourceCode }) {
|
|
|
17
17
|
checkD2DiagramIsValid.input_schema = {
|
|
18
18
|
type: "object",
|
|
19
19
|
properties: {
|
|
20
|
-
|
|
20
|
+
output: {
|
|
21
21
|
type: "string",
|
|
22
22
|
description: "Source code of d2 diagram",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
|
-
required: ["
|
|
25
|
+
required: ["output"],
|
|
26
26
|
};
|
|
27
27
|
checkD2DiagramIsValid.output_schema = {
|
|
28
28
|
type: "object",
|
|
@@ -4,11 +4,11 @@ export default async function wrapDiagramCode({ diagramSourceCode }) {
|
|
|
4
4
|
try {
|
|
5
5
|
const result = await wrapCode({ content: diagramSourceCode });
|
|
6
6
|
return {
|
|
7
|
-
|
|
7
|
+
output: result,
|
|
8
8
|
};
|
|
9
9
|
} catch {
|
|
10
10
|
return {
|
|
11
|
-
diagramSourceCode,
|
|
11
|
+
output: diagramSourceCode,
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -26,10 +26,10 @@ wrapDiagramCode.input_schema = {
|
|
|
26
26
|
wrapDiagramCode.output_schema = {
|
|
27
27
|
type: "object",
|
|
28
28
|
properties: {
|
|
29
|
-
|
|
29
|
+
output: {
|
|
30
30
|
type: "string",
|
|
31
31
|
description: "Source code of d2 diagram",
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
-
required: ["
|
|
34
|
+
required: ["output"],
|
|
35
35
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { basename,
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
2
|
import { publishDocs as publishDocsFn } from "@aigne/publish-docs";
|
|
3
3
|
import { BrokerClient } from "@blocklet/payment-broker-client/node";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import fs from "fs-extra";
|
|
6
|
-
import slugify from "slugify";
|
|
7
6
|
|
|
8
7
|
import { getAccessToken, getOfficialAccessToken } from "../../utils/auth-utils.mjs";
|
|
9
8
|
import {
|
|
@@ -15,8 +14,6 @@ import {
|
|
|
15
14
|
} from "../../utils/constants/index.mjs";
|
|
16
15
|
import { beforePublishHook, ensureTmpDir } from "../../utils/d2-utils.mjs";
|
|
17
16
|
import { deploy } from "../../utils/deploy.mjs";
|
|
18
|
-
import { getExtnameFromContentType } from "../../utils/file-utils.mjs";
|
|
19
|
-
import { uploadFiles } from "../../utils/upload-files.mjs";
|
|
20
17
|
import {
|
|
21
18
|
getGithubRepoUrl,
|
|
22
19
|
isHttp,
|
|
@@ -24,6 +21,7 @@ import {
|
|
|
24
21
|
saveValueToConfig,
|
|
25
22
|
} from "../../utils/utils.mjs";
|
|
26
23
|
import updateBranding from "../utils/update-branding.mjs";
|
|
24
|
+
import { downloadAndUploadImage } from "../../utils/file-utils.mjs";
|
|
27
25
|
|
|
28
26
|
const BASE_URL = process.env.DOC_SMITH_BASE_URL || CLOUD_SERVICE_URL_PROD;
|
|
29
27
|
|
|
@@ -175,46 +173,22 @@ export default async function publishDocs(
|
|
|
175
173
|
description: projectDesc || config?.projectDesc || "",
|
|
176
174
|
icon: projectLogo || config?.projectLogo || "",
|
|
177
175
|
};
|
|
176
|
+
let finalPath = null;
|
|
178
177
|
|
|
179
178
|
// Handle project logo download if it's a URL
|
|
180
179
|
if (projectInfo.icon && isHttp(projectInfo.icon)) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
190
|
-
|
|
191
|
-
if (response.ok) {
|
|
192
|
-
if (!ext) {
|
|
193
|
-
const contentType = response.headers.get("content-type");
|
|
194
|
-
ext = getExtnameFromContentType(contentType);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const finalPath = ext ? `${tempFilePath}.${ext}` : tempFilePath;
|
|
198
|
-
fs.writeFileSync(finalPath, buffer);
|
|
199
|
-
|
|
200
|
-
const filePath = join(process.cwd(), finalPath);
|
|
201
|
-
const { results: uploadResults } = await uploadFiles({
|
|
202
|
-
appUrl,
|
|
203
|
-
filePaths: [filePath],
|
|
204
|
-
accessToken,
|
|
205
|
-
concurrency: 1,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// FIXME: 暂时保持使用绝对路径,需要修改为相对路径 @pengfei
|
|
209
|
-
projectInfo.icon = uploadResults?.[0]?.url || projectInfo.icon;
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.warn(`Failed to download project logo from ${projectInfo.icon}: ${error.message}`);
|
|
213
|
-
}
|
|
180
|
+
const { url: uploadedImageUrl, downloadFinalPath } = await downloadAndUploadImage(
|
|
181
|
+
projectInfo.icon,
|
|
182
|
+
docsDir,
|
|
183
|
+
appUrl,
|
|
184
|
+
accessToken,
|
|
185
|
+
);
|
|
186
|
+
projectInfo.icon = uploadedImageUrl;
|
|
187
|
+
finalPath = downloadFinalPath;
|
|
214
188
|
}
|
|
215
189
|
|
|
216
190
|
if (shouldWithBranding) {
|
|
217
|
-
updateBranding({ appUrl, projectInfo, accessToken });
|
|
191
|
+
updateBranding({ appUrl, projectInfo, accessToken, finalPath });
|
|
218
192
|
}
|
|
219
193
|
|
|
220
194
|
// Construct boardMeta object
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
2
|
import chalk from "chalk";
|
|
4
3
|
import { joinURL } from "ufo";
|
|
5
4
|
|
|
@@ -8,12 +7,11 @@ import {
|
|
|
8
7
|
CLOUD_SERVICE_URL_PROD,
|
|
9
8
|
CLOUD_SERVICE_URL_STAGING,
|
|
10
9
|
DISCUSS_KIT_DID,
|
|
11
|
-
DOC_SMITH_DIR,
|
|
12
10
|
} from "../../utils/constants/index.mjs";
|
|
13
11
|
import { requestWithAuthToken } from "../../utils/request.mjs";
|
|
14
12
|
import { uploadFiles } from "../../utils/upload-files.mjs";
|
|
15
13
|
|
|
16
|
-
export default async function updateBranding({ appUrl, projectInfo, accessToken }) {
|
|
14
|
+
export default async function updateBranding({ appUrl, projectInfo, accessToken, finalPath }) {
|
|
17
15
|
try {
|
|
18
16
|
const origin = new URL(appUrl).origin;
|
|
19
17
|
if ([CLOUD_SERVICE_URL_PROD, CLOUD_SERVICE_URL_STAGING].includes(origin)) {
|
|
@@ -43,15 +41,19 @@ export default async function updateBranding({ appUrl, projectInfo, accessToken
|
|
|
43
41
|
);
|
|
44
42
|
|
|
45
43
|
if (res.success) {
|
|
44
|
+
if (!finalPath) {
|
|
45
|
+
console.warn("\n🔄 Skipped updating branding for missing logo file\n");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
46
49
|
try {
|
|
47
|
-
const
|
|
48
|
-
const projectLogoStat = await stat(projectLogoPath);
|
|
50
|
+
const projectLogoStat = await stat(finalPath);
|
|
49
51
|
|
|
50
52
|
if (projectLogoStat.isFile()) {
|
|
51
53
|
// Upload to blocklet logo endpoint
|
|
52
54
|
await uploadFiles({
|
|
53
55
|
appUrl: origin,
|
|
54
|
-
filePaths: [
|
|
56
|
+
filePaths: [finalPath],
|
|
55
57
|
accessToken,
|
|
56
58
|
concurrency: 1,
|
|
57
59
|
endpoint: `${origin}/.well-known/service/blocklet/logo/upload/square/${componentInfo.did}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/doc-smith",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.14-beta",
|
|
4
4
|
"description": "AI-driven documentation generation tool built on the AIGNE Framework",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"debug": "^4.4.1",
|
|
39
39
|
"diff": "^8.0.2",
|
|
40
40
|
"dompurify": "^3.2.6",
|
|
41
|
+
"file-type": "^21.0.0",
|
|
41
42
|
"fs-extra": "^11.3.1",
|
|
42
43
|
"glob": "^11.0.3",
|
|
43
44
|
"gpt-tokenizer": "^3.2.0",
|
|
@@ -54,7 +55,6 @@
|
|
|
54
55
|
"remark-gfm": "^4.0.1",
|
|
55
56
|
"remark-lint": "^10.0.1",
|
|
56
57
|
"remark-parse": "^11.0.0",
|
|
57
|
-
"slugify": "^1.6.6",
|
|
58
58
|
"terminal-link": "^4.0.0",
|
|
59
59
|
"ufo": "^1.6.1",
|
|
60
60
|
"unified": "^11.0.5",
|
package/utils/file-utils.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
+
import path, { join } from "node:path";
|
|
4
5
|
import { glob } from "glob";
|
|
6
|
+
import fs from "fs-extra";
|
|
5
7
|
import { isBinaryFile } from "isbinaryfile";
|
|
6
8
|
import { encode } from "gpt-tokenizer";
|
|
9
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
10
|
+
import { gunzipSync } from "node:zlib";
|
|
7
11
|
import { isGlobPattern } from "./utils.mjs";
|
|
8
12
|
import { INTELLIGENT_SUGGESTION_TOKEN_THRESHOLD } from "./constants/index.mjs";
|
|
13
|
+
import { uploadFiles } from "./upload-files.mjs";
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Check if a directory is inside a git repository using git command
|
|
@@ -644,3 +649,182 @@ export function getMediaDescriptionCachePath() {
|
|
|
644
649
|
const cwd = process.cwd();
|
|
645
650
|
return path.join(cwd, ".aigne", "doc-smith", "media-description.yaml");
|
|
646
651
|
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Detect file type from buffer with comprehensive fallback strategy
|
|
655
|
+
* @param {Buffer} buffer - File buffer
|
|
656
|
+
* @param {string} contentType - HTTP Content-Type header
|
|
657
|
+
* @param {string} url - Original URL (for fallback)
|
|
658
|
+
* @returns {Promise<{ext: string, mime: string}>} File extension and MIME type
|
|
659
|
+
*/
|
|
660
|
+
export async function detectFileType(buffer, contentType, url = "") {
|
|
661
|
+
// 1. Try file-type for binary images (PNG, JPG, WebP, GIF, etc.)
|
|
662
|
+
try {
|
|
663
|
+
const fileType = await fileTypeFromBuffer(buffer);
|
|
664
|
+
if (fileType) {
|
|
665
|
+
return {
|
|
666
|
+
ext: fileType.ext,
|
|
667
|
+
mime: fileType.mime,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.debug("file-type detection failed:", error.message);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 2. Check for SVG/SVGZ
|
|
675
|
+
const svgResult = await detectSvgType(buffer, contentType);
|
|
676
|
+
if (svgResult) {
|
|
677
|
+
return svgResult;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 3. Fallback to Content-Type
|
|
681
|
+
if (contentType) {
|
|
682
|
+
const ext = getExtnameFromContentType(contentType);
|
|
683
|
+
if (ext) {
|
|
684
|
+
return {
|
|
685
|
+
ext,
|
|
686
|
+
mime: contentType.split(";")[0].trim(),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// 4. Fallback to URL extension
|
|
692
|
+
if (url) {
|
|
693
|
+
const urlExt = path.extname(url).toLowerCase();
|
|
694
|
+
if (urlExt) {
|
|
695
|
+
return {
|
|
696
|
+
ext: urlExt.slice(1), // Remove leading dot
|
|
697
|
+
mime: getMimeType(url),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 5. Default fallback
|
|
703
|
+
return {
|
|
704
|
+
ext: "bin",
|
|
705
|
+
mime: "application/octet-stream",
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Detect SVG/SVGZ file type
|
|
711
|
+
* @param {Buffer} buffer - File buffer
|
|
712
|
+
* @param {string} contentType - HTTP Content-Type header
|
|
713
|
+
* @returns {Promise<{ext: string, mime: string} | null>} SVG info or null
|
|
714
|
+
*/
|
|
715
|
+
async function detectSvgType(buffer, contentType) {
|
|
716
|
+
// Check Content-Type first
|
|
717
|
+
if (contentType?.includes("image/svg+xml")) {
|
|
718
|
+
return {
|
|
719
|
+
ext: "svg",
|
|
720
|
+
mime: "image/svg+xml",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
let text = "";
|
|
726
|
+
|
|
727
|
+
// Check if it's gzipped (SVGZ)
|
|
728
|
+
if (buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
|
729
|
+
try {
|
|
730
|
+
const decompressed = gunzipSync(buffer);
|
|
731
|
+
text = decompressed.toString("utf8");
|
|
732
|
+
if (isSvgContent(text)) {
|
|
733
|
+
return {
|
|
734
|
+
ext: "svgz",
|
|
735
|
+
mime: "image/svg+xml",
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
// Not gzipped, continue with regular text detection
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Try as regular text
|
|
744
|
+
if (!text) {
|
|
745
|
+
text = buffer.toString("utf8");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (isSvgContent(text)) {
|
|
749
|
+
return {
|
|
750
|
+
ext: "svg",
|
|
751
|
+
mime: "image/svg+xml",
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.debug("SVG detection failed:", error.message);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Check if text content is SVG
|
|
763
|
+
* @param {string} text - Text content
|
|
764
|
+
* @returns {boolean} True if SVG
|
|
765
|
+
*/
|
|
766
|
+
function isSvgContent(text) {
|
|
767
|
+
if (!text || typeof text !== "string") return false;
|
|
768
|
+
|
|
769
|
+
// Remove BOM and trim
|
|
770
|
+
const cleanText = text.replace(/^\uFEFF/, "").trim();
|
|
771
|
+
|
|
772
|
+
// Check for SVG root element
|
|
773
|
+
return /^<\?xml\s+[^>]*>\s*<svg/i.test(cleanText) || /^<svg/i.test(cleanText);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Download and upload a remote image URL
|
|
778
|
+
* @param {string} imageUrl - The remote image URL
|
|
779
|
+
* @param {string} docsDir - Directory to save temporary files
|
|
780
|
+
* @param {string} appUrl - Application URL for upload
|
|
781
|
+
* @param {string} accessToken - Access token for upload
|
|
782
|
+
* @returns {Promise<{url: string, downloadFinalPath: string | null}>} The uploaded image URL and final path if failed
|
|
783
|
+
*/
|
|
784
|
+
export async function downloadAndUploadImage(imageUrl, docsDir, appUrl, accessToken) {
|
|
785
|
+
let downloadFinalPath = null;
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
// 1. Download with timeout control
|
|
789
|
+
const controller = new AbortController();
|
|
790
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
|
|
791
|
+
|
|
792
|
+
const response = await fetch(imageUrl, {
|
|
793
|
+
signal: controller.signal,
|
|
794
|
+
});
|
|
795
|
+
clearTimeout(timeoutId);
|
|
796
|
+
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// 2. Convert to Buffer for file type detection
|
|
802
|
+
const blob = await response.blob();
|
|
803
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
804
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
805
|
+
|
|
806
|
+
// 3. Detect file type with comprehensive fallback
|
|
807
|
+
const contentType = response.headers.get("content-type");
|
|
808
|
+
const { ext } = await detectFileType(buffer, contentType, imageUrl);
|
|
809
|
+
|
|
810
|
+
// 4. Generate random filename and save file
|
|
811
|
+
const randomId = randomBytes(16).toString("hex");
|
|
812
|
+
const tempFilePath = join(docsDir, `temp-logo-${randomId}`);
|
|
813
|
+
downloadFinalPath = ext ? `${tempFilePath}.${ext}` : tempFilePath;
|
|
814
|
+
fs.writeFileSync(downloadFinalPath, buffer);
|
|
815
|
+
|
|
816
|
+
// 5. Upload and get URL
|
|
817
|
+
const { results: uploadResults } = await uploadFiles({
|
|
818
|
+
appUrl,
|
|
819
|
+
filePaths: [downloadFinalPath],
|
|
820
|
+
accessToken,
|
|
821
|
+
concurrency: 1,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// 6. Return uploaded URL
|
|
825
|
+
return { url: uploadResults?.[0]?.url || imageUrl, downloadFinalPath };
|
|
826
|
+
} catch (error) {
|
|
827
|
+
console.warn(`Failed to download and upload image from ${imageUrl}: ${error.message}`);
|
|
828
|
+
return { url: imageUrl, downloadFinalPath: null };
|
|
829
|
+
}
|
|
830
|
+
}
|