feat(OCR): add MacSysOcrProvider for macOS OCR functionality

- Introduced MacSysOcrProvider to handle OCR tasks using the mac-system-ocr library.
- Enhanced the OcrProviderFactory to support the new macOS OCR provider.
- Updated file handling to include base64 file processing.
- Added a new SVG icon for the macOS OCR provider.
- Updated the state management to include the new system OCR provider option.
- Improved user interface to conditionally display the system OCR provider based on the operating system.
This commit is contained in:
suyao
2025-03-25 17:24:35 +08:00
parent adb34464c9
commit 25bdb61c56
15 changed files with 324 additions and 18 deletions
+2
View File
@@ -50,6 +50,7 @@
"prepare": "husky"
},
"dependencies": {
"@cherrystudio/mac-system-ocr": "^0.1.0",
"@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.21.0",
@@ -82,6 +83,7 @@
"markdown-it": "^14.1.0",
"officeparser": "^4.1.1",
"pdf-parse": "^1.1.1",
"pdf-to-img": "^4.4.0",
"proxy-agent": "^6.5.0",
"tar": "^7.4.3",
"undici": "^7.4.0",
+1
View File
@@ -181,6 +181,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('file:write', fileManager.writeFile)
ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:base64File', fileManager.base64File)
ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile)
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
+91
View File
@@ -0,0 +1,91 @@
import MacOCR, { OCRResult } from '@cherrystudio/mac-system-ocr'
import { FileSource, isLocalFile, LocalFileSource, OcrProvider } from '@types'
import Logger from 'electron-log'
import * as fs from 'fs'
import PQueue from 'p-queue'
import * as path from 'path'
import BaseOcrProvider from './BaseOcrProvider'
export default class MacSysOcrProvider extends BaseOcrProvider {
constructor(provider: OcrProvider) {
super(provider)
}
private async processPage(
pageBuffer: Buffer,
pageNum: number,
totalPages: number,
sourceId: string,
writeStream: fs.WriteStream
): Promise<void> {
try {
const text: OCRResult = await MacOCR.recognizeFromBuffer(pageBuffer)
writeStream.write(text.text + '\n')
await this.sendOcrProgress(sourceId, (pageNum / totalPages) * 100)
} catch (error) {
Logger.error(`[OCR] Error processing page ${pageNum}:`, error)
throw error
}
}
public async parseFile(sourceId: string, file: FileSource): Promise<{ processedFile: LocalFileSource }> {
Logger.info(`[OCR] Starting OCR process for file: ${file.name}`)
if (isLocalFile(file)) {
if (file.ext === '.pdf') {
try {
const { pdf } = await import('pdf-to-img')
const readStream = fs.createReadStream(file.path)
const results = await pdf(readStream)
const pages = results.length
Logger.info('[OCR] PDF successfully converted to image')
const baseDir = path.dirname(file.path)
const baseName = path.basename(file.path, path.extname(file.path))
const txtFileName = `${baseName}.txt`
const txtFilePath = path.join(baseDir, txtFileName)
// Create a write stream
const writeStream = fs.createWriteStream(txtFilePath)
const queue = new PQueue({ concurrency: 5 })
const tasks: Promise<void>[] = []
for (let i = 0; i < pages; i++) {
const pageNum = i + 1
const task = queue.add(async () => {
const pageBuffer = await results.getPage(pageNum)
await this.processPage(pageBuffer, pageNum, pages, sourceId, writeStream)
})
tasks.push(task)
}
await Promise.all(tasks)
await new Promise<void>((resolve, reject) => {
writeStream.end(() => {
Logger.info('[OCR] OCR process completed successfully')
resolve()
})
writeStream.on('error', reject)
})
return {
processedFile: {
...file,
name: txtFileName,
path: txtFilePath,
ext: '.txt',
size: fs.statSync(txtFilePath).size
}
}
} catch (error) {
Logger.error('[OCR] Error during OCR process:', error)
throw error
}
}
return { processedFile: file }
}
throw new Error('Only local files are supported')
}
}
+1 -1
View File
@@ -189,7 +189,7 @@ export default class MistralOcrProvider extends BaseOcrProvider {
return {
id: conversionId,
name: mdFileName,
name: file.name.replace(/\.[^/.]+$/, '.md'),
origin_name: mdFileName,
path: mdFilePath,
created_at: new Date().toISOString(),
+10 -5
View File
@@ -3,14 +3,19 @@ import { OcrProvider } from '@types'
import BaseOcrProvider from './BaseOcrProvider'
import DefaultOcrProvider from './DefaultOcrProvider'
import Doc2xOcrProvider from './Doc2xOcrProvider'
import MacSysOcrProvider from './MacSysOcrProvider'
import MistralOcrProvider from './MistralOcrProvider'
export default class OcrProviderFactory {
static create(provider: OcrProvider): BaseOcrProvider {
if (provider.id === 'doc2x') {
return new Doc2xOcrProvider(provider)
} else if (provider.id === 'mistral') {
return new MistralOcrProvider(provider)
switch (provider.id) {
case 'doc2x':
return new Doc2xOcrProvider(provider)
case 'mistral':
return new MistralOcrProvider(provider)
case 'system':
return new MacSysOcrProvider(provider)
default:
return new DefaultOcrProvider(provider)
}
return new DefaultOcrProvider(provider)
}
}
+2 -2
View File
@@ -159,6 +159,7 @@ class KnowledgeService {
): LoaderTask {
const { base, item, forceReload } = options
const file = item.content as FileType
file.source = 'local'
const loaderTask: LoaderTask = {
loaderTasks: [
@@ -169,12 +170,11 @@ class KnowledgeService {
let fileToProcess: FileType = file
if (base.preprocessing && base.ocrProvider && file.ext.toLowerCase() === '.pdf') {
try {
file.source = 'local'
const ocrProvider = new OcrProvider(base.ocrProvider)
Logger.info(`Starting OCR processing for file: ${file.path}`)
const { processedFile } = await ocrProvider.parseFile(item.id, file)
Logger.info(`OCR processing completed: ${processedFile.path}`)
fileToProcess = processedFile
Logger.info(`OCR processing completed: ${fileToProcess.path}`)
} catch (err) {
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="macOS" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#ffffff"/><path d="M282 170v-4c-52 0-5 34 0 4zm24-18c7-21 43-23 47 3h-10c-3-15-28-16-28 11 0 15 23 24 28 6h10c-6 33-59 21-47-20zm-146-16h10v9c5-12 27-13 31 1 7-15 35-14 35 7v37h-11v-34c0-15-22-15-22 1v33h-11v-35c-2.447-9.36-14.915-11.23-20-3l-2 5v33h-10zm23 259c-47 0-76-33-76-86s29-85 76-85 77 33 77 85-29 86-77 86zm88-205c-29 7-33-30-3-31l14-1v-4c1-12-19-13-22-2h-10a14 14 0 012-7c8-14 41-14 41 8v37h-10v-9a18 18 0 01-12 9zm68 205c-36-2-61-19-63-49h24c23 72 146-5 25-30-19-4-33-13-39-24-38-74 109-96 113-20h-23c-7-49-98-22-65 12 14 14 43 13 64 22 50 23 26 91-36 89zM183 245c-32 0-52 25-52 64s20 64 52 64 53-24 53-64-20-64-53-64z"/></svg>

After

Width:  |  Height:  |  Size: 896 B

+3
View File
@@ -1,4 +1,5 @@
import Doc2xLogo from '@renderer/assets/images/ocr/doc2x.svg'
import MacOSLogo from '@renderer/assets/images/providers/macos.svg'
import MistralLogo from '@renderer/assets/images/providers/mistral.png'
export function getOcrProviderLogo(providerId: string) {
@@ -7,6 +8,8 @@ export function getOcrProviderLogo(providerId: string) {
return Doc2xLogo
case 'mistral':
return MistralLogo
case 'system':
return MacOSLogo
default:
return undefined
}
+3 -2
View File
@@ -19,8 +19,9 @@ const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
const [loading, setLoading] = useState(false)
const fetchFiles = useCallback(async () => {
const { files } = await window.api.fileService.list(provider.type, provider.apiKey)
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
const reponses = await window.api.fileService.list(provider.type, provider.apiKey)
const files = reponses.files.map((file) => file.originalFile as FileMetadataResponse)
setFiles(files)
}, [provider])
const columns: ColumnsType<FileMetadataResponse> = [
@@ -1,3 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultOcrProvider, useOcrProviders } from '@renderer/hooks/useOcr'
import { OcrProvider } from '@renderer/types'
@@ -37,7 +38,11 @@ const KnowledgeSettings: FC = () => {
style={{ width: '200px' }}
onChange={(value: string) => updateSelectedOcrProvider(value)}
placeholder={t('settings.tool.ocr.provider_placeholder')}
options={ocrProviders.map((p) => ({ value: p.id, label: p.name }))}
options={ocrProviders.map((p) => ({
value: p.id,
label: p.name,
disabled: !isMac && p.id === 'system' // 在非 Mac 系统下禁用 system 选项
}))}
/>
</div>
</SettingRow>
+2 -1
View File
@@ -88,7 +88,7 @@ export default class GeminiProvider extends BaseProvider {
const { data, mimeType } = await fileToBase64(file.path)
return {
inlineData: {
data,
data: data.toString('base64'),
mimeType
}
} as InlineDataPart
@@ -150,6 +150,7 @@ export default class GeminiProvider extends BaseProvider {
}
for (const file of message.files || []) {
console.log('file', file)
if (file.type === FileTypes.IMAGE) {
const base64Data = await window.api.file.base64Image(file.id + file.ext)
parts.push({
+6
View File
@@ -818,6 +818,12 @@ const migrateConfig = {
}
]
}
if (!state.ocr.providers.find((provider) => provider.id === 'system')) {
state.ocr.providers.push({
id: 'system',
name: 'System(Mac Only)'
})
}
return state
}
}
+4
View File
@@ -20,6 +20,10 @@ const initialState: OcrState = {
model: 'mistral-ocr-latest',
apiKey: '',
apiHost: 'https://api.mistral.ai'
},
{
id: 'system',
name: 'System(Mac Only)'
}
],
defaultProvider: ''
+2 -2
View File
@@ -1,7 +1,7 @@
export const fileToBase64 = async (filePath: string) => {
const result = await window.api.file.base64Image(filePath)
const result = await window.api.file.base64File(filePath)
return {
data: result.base64,
data: result.data,
mimeType: result.mime
}
}
+184 -4
View File
@@ -451,6 +451,17 @@ __metadata:
languageName: unknown
linkType: soft
"@cherrystudio/mac-system-ocr@npm:^0.1.0":
version: 0.1.0
resolution: "@cherrystudio/mac-system-ocr@npm:0.1.0"
dependencies:
bindings: "npm:^1.5.0"
node-api-headers: "npm:^1.0.1"
node-gyp: "npm:latest"
checksum: 10c0/a8257e4c6015f93c630091b44643e3c89e1bd6c2ece836a2ecfa517938c60932f0b8040d126b458dd9e5708194ce9c39c9b288ea5d81d55a111a7f11c1c21c7e
languageName: node
linkType: hard
"@develar/schema-utils@npm:~2.6.5":
version: 2.6.5
resolution: "@develar/schema-utils@npm:2.6.5"
@@ -2389,6 +2400,25 @@ __metadata:
languageName: node
linkType: hard
"@mapbox/node-pre-gyp@npm:^1.0.0":
version: 1.0.11
resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
dependencies:
detect-libc: "npm:^2.0.0"
https-proxy-agent: "npm:^5.0.0"
make-dir: "npm:^3.1.0"
node-fetch: "npm:^2.6.7"
nopt: "npm:^5.0.0"
npmlog: "npm:^5.0.1"
rimraf: "npm:^3.0.2"
semver: "npm:^7.3.5"
tar: "npm:^6.1.11"
bin:
node-pre-gyp: bin/node-pre-gyp
checksum: 10c0/2b24b93c31beca1c91336fa3b3769fda98e202fb7f9771f0f4062588d36dcc30fcf8118c36aa747fa7f7610d8cf601872bdaaf62ce7822bb08b545d1bbe086cc
languageName: node
linkType: hard
"@mistralai/mistralai@npm:^1.5.2":
version: 1.5.2
resolution: "@mistralai/mistralai@npm:1.5.2"
@@ -3816,6 +3846,7 @@ __metadata:
"@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3"
"@anthropic-ai/sdk": "npm:^0.38.0"
"@cherrystudio/mac-system-ocr": "npm:^0.1.0"
"@electron-toolkit/eslint-config-prettier": "npm:^3.0.0"
"@electron-toolkit/eslint-config-ts": "npm:^3.0.0"
"@electron-toolkit/preload": "npm:^3.0.0"
@@ -3902,6 +3933,7 @@ __metadata:
openai: "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch"
p-queue: "npm:^8.1.0"
pdf-parse: "npm:^1.1.1"
pdf-to-img: "npm:^4.4.0"
prettier: "npm:^3.5.3"
proxy-agent: "npm:^6.5.0"
react: "npm:^18.2.0"
@@ -4283,6 +4315,16 @@ __metadata:
languageName: node
linkType: hard
"are-we-there-yet@npm:^2.0.0":
version: 2.0.0
resolution: "are-we-there-yet@npm:2.0.0"
dependencies:
delegates: "npm:^1.0.0"
readable-stream: "npm:^3.6.0"
checksum: 10c0/375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c
languageName: node
linkType: hard
"are-we-there-yet@npm:^3.0.0":
version: 3.0.1
resolution: "are-we-there-yet@npm:3.0.1"
@@ -4948,6 +4990,29 @@ __metadata:
languageName: node
linkType: hard
"canvas@npm:3.1.0":
version: 3.1.0
resolution: "canvas@npm:3.1.0"
dependencies:
node-addon-api: "npm:^7.0.0"
node-gyp: "npm:latest"
prebuild-install: "npm:^7.1.1"
checksum: 10c0/28da5184c1d7e97049ba6a24f10690b9ed4b303bbd25517d95c892fa3a6331417791657a3a7467068e40af0dda2dcc9120d062f7426a3d796131e69a30e3cbf1
languageName: node
linkType: hard
"canvas@npm:^2.11.2":
version: 2.11.2
resolution: "canvas@npm:2.11.2"
dependencies:
"@mapbox/node-pre-gyp": "npm:^1.0.0"
nan: "npm:^2.17.0"
node-gyp: "npm:latest"
simple-get: "npm:^3.0.3"
checksum: 10c0/943368798ad1b66b18633aa34b6181e1038dac5433fc9727cd07be35f0a633f572b60d9edb95f5ff90b6a9128e86d5312035f91a2934101c73185b15d906230a
languageName: node
linkType: hard
"caseless@npm:~0.12.0":
version: 0.12.0
resolution: "caseless@npm:0.12.0"
@@ -5226,7 +5291,7 @@ __metadata:
languageName: node
linkType: hard
"color-support@npm:^1.1.3":
"color-support@npm:^1.1.2, color-support@npm:^1.1.3":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
bin:
@@ -5710,6 +5775,15 @@ __metadata:
languageName: node
linkType: hard
"decompress-response@npm:^4.2.0":
version: 4.2.1
resolution: "decompress-response@npm:4.2.1"
dependencies:
mimic-response: "npm:^2.0.0"
checksum: 10c0/5e4821be332e80e3639acee2441c41d245fc07ac3ee85a6f28893c10c079d66d9bf09e8d84bffeae5656a4625e09e9b93fb4a5705adbe6b07202eea64fae1c8d
languageName: node
linkType: hard
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@@ -7764,6 +7838,23 @@ __metadata:
languageName: node
linkType: hard
"gauge@npm:^3.0.0":
version: 3.0.2
resolution: "gauge@npm:3.0.2"
dependencies:
aproba: "npm:^1.0.3 || ^2.0.0"
color-support: "npm:^1.1.2"
console-control-strings: "npm:^1.0.0"
has-unicode: "npm:^2.0.1"
object-assign: "npm:^4.1.1"
signal-exit: "npm:^3.0.0"
string-width: "npm:^4.2.3"
strip-ansi: "npm:^6.0.1"
wide-align: "npm:^1.1.2"
checksum: 10c0/75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913
languageName: node
linkType: hard
"gauge@npm:^4.0.3":
version: 4.0.4
resolution: "gauge@npm:4.0.4"
@@ -9924,6 +10015,15 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
dependencies:
semver: "npm:^6.0.0"
checksum: 10c0/56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa
languageName: node
linkType: hard
"make-fetch-happen@npm:^14.0.3":
version: 14.0.3
resolution: "make-fetch-happen@npm:14.0.3"
@@ -11024,6 +11124,13 @@ __metadata:
languageName: node
linkType: hard
"mimic-response@npm:^2.0.0":
version: 2.1.0
resolution: "mimic-response@npm:2.1.0"
checksum: 10c0/717475c840f20deca87a16cb2f7561f9115f5de225ea2377739e09890c81aec72f43c81fd4984650c4044e66be5a846fa7a517ac7908f01009e1e624e19864d5
languageName: node
linkType: hard
"mimic-response@npm:^3.1.0":
version: 3.1.0
resolution: "mimic-response@npm:3.1.0"
@@ -11305,6 +11412,15 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:^2.17.0":
version: 2.22.2
resolution: "nan@npm:2.22.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/971f963b8120631880fa47a389c71b00cadc1c1b00ef8f147782a3f4387d4fc8195d0695911272d57438c11562fb27b24c4ae5f8c05d5e4eeb4478ba51bb73c5
languageName: node
linkType: hard
"nan@npm:~2.10.0":
version: 2.10.0
resolution: "nan@npm:2.10.0"
@@ -11414,6 +11530,13 @@ __metadata:
languageName: node
linkType: hard
"node-api-headers@npm:^1.0.1":
version: 1.5.0
resolution: "node-api-headers@npm:1.5.0"
checksum: 10c0/e8dfe99e8e3ca92cd5d37989413dfc96551e8f7883110b948917dad07e554cfbf1119130e96d0167f5cb5a05f651a4ac735402a305ff25d9ace422a2e429ae3b
languageName: node
linkType: hard
"node-domexception@npm:1.0.0, node-domexception@npm:^1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
@@ -11628,6 +11751,18 @@ __metadata:
languageName: node
linkType: hard
"npmlog@npm:^5.0.1":
version: 5.0.1
resolution: "npmlog@npm:5.0.1"
dependencies:
are-we-there-yet: "npm:^2.0.0"
console-control-strings: "npm:^1.1.0"
gauge: "npm:^3.0.0"
set-blocking: "npm:^2.0.0"
checksum: 10c0/489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa
languageName: node
linkType: hard
"npmlog@npm:^6.0.0":
version: 6.0.2
resolution: "npmlog@npm:6.0.2"
@@ -11661,7 +11796,7 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0":
"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
@@ -12297,6 +12432,13 @@ __metadata:
languageName: node
linkType: hard
"path2d@npm:^0.2.0":
version: 0.2.2
resolution: "path2d@npm:0.2.2"
checksum: 10c0/1bb76c7f275d07f1bc7ca12171d828e91bf8a12596f0765a52e9d4d47fe1a428455dc1dd4c9002924a9bc554f6ac25e09a6c22eaecf32e5e33fba2985b5168f8
languageName: node
linkType: hard
"pdf-parse@npm:1.1.1, pdf-parse@npm:^1.1.1":
version: 1.1.1
resolution: "pdf-parse@npm:1.1.1"
@@ -12317,6 +12459,33 @@ __metadata:
languageName: node
linkType: hard
"pdf-to-img@npm:^4.4.0":
version: 4.4.0
resolution: "pdf-to-img@npm:4.4.0"
dependencies:
canvas: "npm:3.1.0"
pdfjs-dist: "npm:4.2.67"
bin:
pdf2img: bin/cli.mjs
checksum: 10c0/1b6eb36042d3b54f067241a74dc80d27e976685a15e730736f61bdbbc8b76bdd20e58976e87f1b0b9d9cfe534179ebe748b0eb3ec71d0807b83384447f861ee2
languageName: node
linkType: hard
"pdfjs-dist@npm:4.2.67":
version: 4.2.67
resolution: "pdfjs-dist@npm:4.2.67"
dependencies:
canvas: "npm:^2.11.2"
path2d: "npm:^0.2.0"
dependenciesMeta:
canvas:
optional: true
path2d:
optional: true
checksum: 10c0/1d6d427a2253b2c15cbb168d7f95fc26428134ff61113359653c36f92475a4abd8552913b1492489933304ccc4285328b5cbcff36825ae533b8cd4c279881348
languageName: node
linkType: hard
"peberminta@npm:^0.9.0":
version: 0.9.0
resolution: "peberminta@npm:0.9.0"
@@ -14278,7 +14447,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^6.2.0, semver@npm:^6.3.1":
"semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.1":
version: 6.3.1
resolution: "semver@npm:6.3.1"
bin:
@@ -14466,6 +14635,17 @@ __metadata:
languageName: node
linkType: hard
"simple-get@npm:^3.0.3":
version: 3.1.1
resolution: "simple-get@npm:3.1.1"
dependencies:
decompress-response: "npm:^4.2.0"
once: "npm:^1.3.1"
simple-concat: "npm:^1.0.0"
checksum: 10c0/438c78844ea1b1e7268d13ee0b3a39c7d644183367aec916aed3b676b45d3037a61d9f975c200a49b42eb851f29f03745118af1e13c01e60a7b4044f2fd60be7
languageName: node
linkType: hard
"simple-get@npm:^4.0.0":
version: 4.0.1
resolution: "simple-get@npm:4.0.1"
@@ -16124,7 +16304,7 @@ __metadata:
languageName: node
linkType: hard
"wide-align@npm:^1.1.0, wide-align@npm:^1.1.5":
"wide-align@npm:^1.1.0, wide-align@npm:^1.1.2, wide-align@npm:^1.1.5":
version: 1.1.5
resolution: "wide-align@npm:1.1.5"
dependencies: