mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
1
This commit is contained in:
parent
df161d785a
commit
b5fd228063
2
.cursorindexingignore
Normal file
2
.cursorindexingignore
Normal file
@ -0,0 +1,2 @@
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
6
.specstory/.project.json
Normal file
6
.specstory/.project.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"workspace_id": "9144-9737-c737-ef40",
|
||||
"workspace_id_at": "2025-04-30T04:54:53.920Z",
|
||||
"git_id": "e71b-467d-6872-d6f5",
|
||||
"git_id_at": "2025-04-30T04:54:53.920Z"
|
||||
}
|
||||
65
.specstory/.what-is-this.md
Normal file
65
.specstory/.what-is-this.md
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
# SpecStory Artifacts Directory
|
||||
|
||||
This directory is automatically created and maintained by the SpecStory extension to preserve your Cursor composer and chat history.
|
||||
|
||||
## What's Here?
|
||||
|
||||
- `.specstory/history`: Contains markdown files of your AI coding sessions
|
||||
- Each file represents a separate chat or composer session
|
||||
- Files are automatically updated as you work
|
||||
- `.specstory/cursor_rules_backups`: Contains backups of the `.cursor/rules/derived-cursor-rules.mdc` file
|
||||
- Backups are automatically created each time the `.cursor/rules/derived-cursor-rules.mdc` file is updated
|
||||
- You can enable/disable the Cursor Rules feature in the SpecStory settings, it is disabled by default
|
||||
|
||||
## Valuable Uses
|
||||
|
||||
- Capture: Keep your context window up-to-date when starting new Chat/Composer sessions via @ references
|
||||
- Search: For previous prompts and code snippets
|
||||
- Learn: Meta-analyze your patterns and learn from your past experiences
|
||||
- Derive: Keep Cursor on course with your past decisions by automatically deriving Cursor rules from your AI interactions
|
||||
|
||||
## Version Control
|
||||
|
||||
We recommend keeping this directory under version control to maintain a history of your AI interactions. However, if you prefer not to version these files, you can exclude them by adding this to your `.gitignore`:
|
||||
|
||||
```
|
||||
.specstory
|
||||
```
|
||||
|
||||
We recommend not keeping the `.specstory/cursor_rules_backups` directory under version control if you are already using git to version the `.cursor/rules` directory, and committing regularly. You can exclude it by adding this to your `.gitignore`:
|
||||
|
||||
```
|
||||
.specstory/cursor_rules_backups
|
||||
```
|
||||
|
||||
## Searching Your Codebase
|
||||
|
||||
When searching your codebase in Cursor, search results may include your previous AI coding interactions. To focus solely on your actual code files, you can exclude the AI interaction history from search results.
|
||||
|
||||
To exclude AI interaction history:
|
||||
|
||||
1. Open the "Find in Files" search in Cursor (Cmd/Ctrl + Shift + F)
|
||||
2. Navigate to the "files to exclude" section
|
||||
3. Add the following pattern:
|
||||
|
||||
```
|
||||
.specstory/*
|
||||
```
|
||||
|
||||
This will ensure your searches only return results from your working codebase files.
|
||||
|
||||
## Notes
|
||||
|
||||
- Auto-save only works when Cursor/sqlite flushes data to disk. This results in a small delay after the AI response is complete before SpecStory can save the history.
|
||||
- Auto-save does not yet work on remote WSL workspaces.
|
||||
|
||||
## Settings
|
||||
|
||||
You can control auto-saving behavior in Cursor:
|
||||
|
||||
1. Open Cursor → Settings → VS Code Settings (Cmd/Ctrl + ,)
|
||||
2. Search for "SpecStory"
|
||||
3. Find "Auto Save" setting to enable/disable
|
||||
|
||||
Auto-save occurs when changes are detected in Cursor's sqlite database, or every 2 minutes as a safety net.
|
||||
20
.specstory/history/2025-04-30_01-39-简单问候.md
Normal file
20
.specstory/history/2025-04-30_01-39-简单问候.md
Normal file
@ -0,0 +1,20 @@
|
||||
<!-- Generated by SpecStory -->
|
||||
|
||||
# 简单问候 (2025-04-30 09:39:03)
|
||||
|
||||
_**User**_
|
||||
|
||||
你好
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
你好!请问有什么我可以帮助你的?
|
||||
|
||||
---
|
||||
|
||||
15632
.specstory/history/2025-04-30_01-43-review-typescript-pluginsystem-ts.md
Normal file
15632
.specstory/history/2025-04-30_01-43-review-typescript-pluginsystem-ts.md
Normal file
File diff suppressed because it is too large
Load Diff
130
.specstory/history/2025-04-30_04-53-简单问候.md
Normal file
130
.specstory/history/2025-04-30_04-53-简单问候.md
Normal file
@ -0,0 +1,130 @@
|
||||
<!-- Generated by SpecStory -->
|
||||
|
||||
# 简单问候 (2025-04-30 12:53:16)
|
||||
|
||||
_**User**_
|
||||
|
||||
你好
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
你好!有什么我可以帮助你的吗?
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你是谁
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我是你的编程助手,专注于帮助你解决与 TypeScript、Node.js、Vue.js 等相关的编码任务。如果你有任何问题或需要帮助的地方,请随时告诉我!
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你是什么模型
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我是一个基于 GPT-4o mini 的人工智能编码助手,旨在帮助你进行编程和解决技术问题。如果你有任何具体的编码需求或问题,请告诉我!
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你是谁
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我是你的编程助手,专注于帮助你解决与 TypeScript、Node.js、Vue.js 等相关的编码任务。如果你有任何问题或需要帮助的地方,请随时告诉我!
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你是什么模型
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
我是一个基于 GPT-4o mini 的人工智能编码助手,旨在帮助你进行编程和解决技术问题。如果你有任何具体的编码需求或问题,请告诉我!
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你好
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
<think>
|
||||
|
||||
</think>
|
||||
|
||||
你好!有什么我可以帮助你的吗?
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你是谁
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
<think>
|
||||
|
||||
好的,用户再次询问“你是谁”,这已经是第三次同样的问题了。可能用户是在测试我的回答一致性,或者想确认我的身份。根据之前的记录,我之前已经回答过两次,用户可能没有注意到或者需要再次确认。我需要保持回答的一致性,同时确保信息准确。
|
||||
|
||||
根据自定义指令,我需要用中文回答,并且保持简洁专业。之前的回答都是说明我是编程助手,专注于TypeScript、Vue等技术。这次也应该保持同样的内容,避免添加新信息,以免混淆用户。同时,用户可能希望得到更详细的介绍,但根据历史记录,用户没有进一步追问,所以保持原有回答即可。
|
||||
|
||||
另外,用户可能在使用过程中遇到问题,需要帮助,但暂时没有具体说明。因此,在回答身份后,再次邀请用户提出具体问题,以促进进一步沟通。确保符合友好和乐于助人的语气,同时遵循不主动提供工具使用,仅根据用户需求回应的原则。
|
||||
|
||||
最后,检查是否有其他需要注意的指令,比如避免使用Markdown,保持自然对话。确保回答简洁,不冗长,符合用户的使用场景。
|
||||
</think>
|
||||
|
||||
我是你的编程助手,专注于帮助你解决与 TypeScript、Node.js、Vue.js 等相关的编码任务。如果你有任何问题或需要帮助的地方,请随时告诉我!
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你好
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
_**User**_
|
||||
|
||||
你好
|
||||
|
||||
---
|
||||
|
||||
_**Assistant**_
|
||||
|
||||
<think>
|
||||
The user said "你好" (Hello). This is a simple greeting. I should respond in kind and offer assistance, following the custom instructions to speak Chinese.
|
||||
</think>
|
||||
你好!有什么我可以帮助你的吗?
|
||||
|
||||
---
|
||||
|
||||
4059
.specstory/history/2025-04-30_05-02-查找插件系统日历功能.md
Normal file
4059
.specstory/history/2025-04-30_05-02-查找插件系统日历功能.md
Normal file
File diff suppressed because it is too large
Load Diff
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -14,7 +14,7 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
import AddPluginModal from './components/AddPluginModal' // 新增导入
|
||||
|
||||
const ModuleSettings: FC = () => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false) // 新增状态
|
||||
|
||||
// 处理添加插件
|
||||
const handleAddPlugin = async (pluginId: string) => {
|
||||
try {
|
||||
setDownloading((prev) => ({ ...prev, [pluginId]: true }))
|
||||
const success = await installPlugin(pluginId)
|
||||
if (success) {
|
||||
messageApi.success(`插件 ${pluginId} 安装成功`)
|
||||
} else {
|
||||
messageApi.error(`插件 ${pluginId} 安装失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to install plugin ${pluginId}:`, error)
|
||||
messageApi.error(`插件 ${pluginId} 安装失败`)
|
||||
} finally {
|
||||
setDownloading((prev) => ({ ...prev, [pluginId]: false }))
|
||||
setIsModalVisible(false) // 安装完成后关闭弹窗
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
{activeTab === 'plugins' && (
|
||||
<>
|
||||
<ActionBar>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={() => setIsModalVisible(true)} // 绑定点击事件
|
||||
>
|
||||
添加插件
|
||||
</Button>
|
||||
</ActionBar>
|
||||
|
||||
{/* 新增模态框组件 */}
|
||||
<AddPluginModal
|
||||
visible={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
onOk={handleAddPlugin}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)
|
||||
}
|
||||
1
current_time.txt
Normal file
1
current_time.txt
Normal file
@ -0,0 +1 @@
|
||||
The current time is now: 7:00 PM
|
||||
17
package.json
17
package.json
@ -20,12 +20,12 @@
|
||||
"scripts": {
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build": "tsc --noEmit -p tsconfig.node.json --composite false && tsc --noEmit -p tsconfig.web.json --composite false && electron-vite build",
|
||||
"build:check": "yarn test && yarn typecheck && yarn check:i18n",
|
||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||
"build:win": "dotenv npm run build && electron-builder --win",
|
||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||
"build:win:arm64": "dotenv npm run build && electron-builder --win --arm64",
|
||||
"build:unpack": "dotenv yarn run build && electron-builder --dir",
|
||||
"build:win": "dotenv yarn run build && electron-builder --win",
|
||||
"build:win:x64": "dotenv yarn run build && electron-builder --win --x64",
|
||||
"build:win:arm64": "dotenv yarn run build && electron-builder --win --arm64",
|
||||
"build:mac": "dotenv electron-vite build && electron-builder --mac",
|
||||
"build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64",
|
||||
"build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64",
|
||||
@ -40,7 +40,7 @@
|
||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"typecheck": "yarn run typecheck:node && yarn run typecheck:web",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"check:i18n": "node scripts/check-i18n.js",
|
||||
@ -112,7 +112,7 @@
|
||||
"color": "^5.0.0",
|
||||
"d3": "^7.9.0",
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
"docx": "^9.4.1",
|
||||
"edge-tts-node": "^1.5.7",
|
||||
"electron-chrome-extensions": "^4.7.0",
|
||||
"electron-log": "^5.1.5",
|
||||
@ -122,6 +122,7 @@
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"fast-xml-parser": "^5.0.9",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^12.9.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^11.0.1",
|
||||
@ -130,6 +131,7 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"node-edge-tts": "^1.2.8",
|
||||
"officeparser": "^4.1.1",
|
||||
@ -177,6 +179,7 @@
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/d3": "^7",
|
||||
"@types/diff": "^7",
|
||||
"@types/file-saver": "^2",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
|
||||
@ -12,6 +12,7 @@ export enum IpcChannel {
|
||||
App_SetTrayOnClose = 'app:set-tray-on-close',
|
||||
App_RestartTray = 'app:restart-tray',
|
||||
App_SetTheme = 'app:set-theme',
|
||||
App_GetTheme = 'app:get-theme',
|
||||
|
||||
App_IsBinaryExist = 'app:is-binary-exist',
|
||||
App_GetBinaryPath = 'app:get-binary-path',
|
||||
@ -49,6 +50,7 @@ export enum IpcChannel {
|
||||
Mcp_RestartServer = 'mcp:restart-server',
|
||||
Mcp_StopServer = 'mcp:stop-server',
|
||||
Mcp_ListTools = 'mcp:list-tools',
|
||||
Mcp_ResetToolsList = 'mcp:reset-tools-list',
|
||||
Mcp_CallTool = 'mcp:call-tool',
|
||||
Mcp_ListPrompts = 'mcp:list-prompts',
|
||||
Mcp_GetPrompt = 'mcp:get-prompt',
|
||||
@ -119,6 +121,7 @@ export enum IpcChannel {
|
||||
File_Download = 'file:download',
|
||||
File_Copy = 'file:copy',
|
||||
File_BinaryFile = 'file:binaryFile',
|
||||
File_WriteBase64Image = 'file:writeBase64Image',
|
||||
|
||||
Fs_Read = 'fs:read',
|
||||
|
||||
@ -190,11 +193,18 @@ export enum IpcChannel {
|
||||
// PDF
|
||||
PDF_SplitPDF = 'pdf:split-pdf',
|
||||
PDF_GetPageCount = 'pdf:get-page-count',
|
||||
PDF_ToWord = 'pdf:to-word',
|
||||
|
||||
// Deep Research
|
||||
DeepResearch_Complete = 'deep-research:complete',
|
||||
DeepResearch_Progress = 'deep-research:progress',
|
||||
|
||||
// MCP Rerun Updates (Main -> Renderer)
|
||||
Mcp_ToolRerunUpdate = 'mcp:tool-rerun-update'
|
||||
Mcp_ToolRerunUpdate = 'mcp:tool-rerun-update',
|
||||
|
||||
// Module Manager
|
||||
Module_Download = 'module:download',
|
||||
Module_Delete = 'module:delete',
|
||||
Module_List = 'module:list',
|
||||
Module_Exists = 'module:exists'
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
x64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules/**'
|
||||
- '!build{,/**/*}'
|
||||
- '!release/2.0.0-ludi{,/**/*}'
|
||||
- out
|
||||
- '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}'
|
||||
- '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}'
|
||||
- '!**/node_modules/*.d.ts'
|
||||
- '!**/node_modules/.bin'
|
||||
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
|
||||
- '!.editorconfig'
|
||||
- '!**/._*'
|
||||
- '!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}'
|
||||
- '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}'
|
||||
- '!**/{appveyor.yml,.travis.yml,circle.yml}'
|
||||
- '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}'
|
||||
- package.json
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- out
|
||||
- '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}'
|
||||
- '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}'
|
||||
- '!**/node_modules/*.d.ts'
|
||||
- '!**/node_modules/.bin'
|
||||
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
|
||||
- '!.editorconfig'
|
||||
- '!**/._*'
|
||||
- '!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}'
|
||||
- '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}'
|
||||
- '!**/{appveyor.yml,.travis.yml,circle.yml}'
|
||||
- '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}'
|
||||
@ -1,30 +0,0 @@
|
||||
directories:
|
||||
output: release/${version}
|
||||
buildResources: build
|
||||
npmArgs:
|
||||
- '--no-pnp'
|
||||
npmRebuild: true
|
||||
appId: com.cherry.studio
|
||||
productName: CherryStudio
|
||||
asar: true
|
||||
files:
|
||||
- filter:
|
||||
- out
|
||||
- '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}'
|
||||
- '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}'
|
||||
- '!**/node_modules/*.d.ts'
|
||||
- '!**/node_modules/.bin'
|
||||
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
|
||||
- '!.editorconfig'
|
||||
- '!**/._*'
|
||||
- '!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}'
|
||||
- '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}'
|
||||
- '!**/{appveyor.yml,.travis.yml,circle.yml}'
|
||||
- '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}'
|
||||
win:
|
||||
target:
|
||||
- target: portable
|
||||
arch:
|
||||
- x64
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
electronVersion: 35.2.0
|
||||
@ -1,21 +0,0 @@
|
||||
Copyright (c) Electron contributors
|
||||
Copyright (c) 2013-2020 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
{"file_format_version": "1.0.0", "ICD": {"library_path": ".\\vk_swiftshader.dll", "api_version": "1.0.5"}}
|
||||
Binary file not shown.
@ -1,4 +1,5 @@
|
||||
import './services/MemoryFileService'
|
||||
import './services/ModuleFileManager'
|
||||
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
||||
@ -163,6 +164,10 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
// 初始化模块文件管理器
|
||||
// 注意:ModuleFileManager 已经是一个初始化好的实例
|
||||
// 导入时就已经调用了 getInstance()
|
||||
|
||||
// 注意: MsTTS IPC处理程序已在ipc.ts中注册
|
||||
// 不需要再次调用registerMsTTSIpcHandlers()
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { MCPServer, Shortcut, ThemeMode } from '@types' // Import MCPServer here
|
||||
import { BrowserWindow, ipcMain, session, shell, webContents } from 'electron'
|
||||
import { BrowserWindow, ipcMain, nativeTheme, session, shell, webContents } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
@ -125,24 +125,30 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
return configManager.get(key)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.App_GetTheme, () => {
|
||||
return configManager.getTheme()
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => {
|
||||
if (theme === configManager.getTheme()) return
|
||||
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
|
||||
const notifyThemeChange = () => {
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
windows.forEach((win) =>
|
||||
win.webContents.send(IpcChannel.ThemeChange, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
|
||||
)
|
||||
}
|
||||
|
||||
if (theme === ThemeMode.auto) {
|
||||
nativeTheme.themeSource = 'system'
|
||||
nativeTheme.on('updated', notifyThemeChange)
|
||||
} else {
|
||||
nativeTheme.themeSource = theme
|
||||
nativeTheme.removeAllListeners('updated')
|
||||
}
|
||||
|
||||
mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
configManager.setTheme(theme)
|
||||
|
||||
// should sync theme change to all windows
|
||||
const senderWindowId = event.sender.id
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
// 向其他窗口广播主题变化
|
||||
windows.forEach((win) => {
|
||||
if (win.webContents.id !== senderWindowId) {
|
||||
win.webContents.send(IpcChannel.ThemeChange, theme)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
notifyThemeChange()
|
||||
})
|
||||
|
||||
// clear cache
|
||||
@ -287,6 +293,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
|
||||
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
|
||||
ipcMain.handle(IpcChannel.File_BinaryFile, fileManager.binaryFile)
|
||||
ipcMain.handle(IpcChannel.File_WriteBase64Image, fileManager.writeBase64Image)
|
||||
|
||||
// fs
|
||||
ipcMain.handle(IpcChannel.Fs_Read, FileService.readFile)
|
||||
@ -525,6 +532,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
// PDF服务
|
||||
ipcMain.handle(IpcChannel.PDF_SplitPDF, PDFService.splitPDF.bind(PDFService))
|
||||
ipcMain.handle(IpcChannel.PDF_GetPageCount, PDFService.getPDFPageCount.bind(PDFService))
|
||||
ipcMain.handle(IpcChannel.PDF_ToWord, PDFService.toWord.bind(PDFService))
|
||||
|
||||
// 深度研究服务
|
||||
deepResearchService.setMainWindow(mainWindow)
|
||||
|
||||
@ -8,44 +8,200 @@ import Logger from 'electron-log'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
// 动态加载 mathjs
|
||||
// 缓存实例和代码
|
||||
let math: any = null
|
||||
let mathJsCodeCache: string | null = null
|
||||
let plotly: any = null
|
||||
let plotlyCodeCache: string | null = null
|
||||
|
||||
// 创建一个加载 plotly.js 的函数
|
||||
async function loadPlotly() {
|
||||
try {
|
||||
// 如果已有实例,直接返回
|
||||
if (plotly) {
|
||||
Logger.info('[Calculator] Using cached plotly instance')
|
||||
return true
|
||||
}
|
||||
|
||||
// 使用应用程序的用户数据目录,确保缓存持久化
|
||||
const cacheDir = path.join(app.getPath('userData'), 'calculator-cache')
|
||||
const plotlyJsPath = path.join(cacheDir, 'plotly.js')
|
||||
const plotlyJsVersionPath = path.join(cacheDir, 'plotly-version.txt')
|
||||
const currentVersion = '3.0.1' // 当前使用的 plotly.js 版本
|
||||
|
||||
// 确保缓存目录存在
|
||||
try {
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
} catch (err) {
|
||||
Logger.warn('[Calculator] Failed to create cache directory:', err)
|
||||
}
|
||||
|
||||
// 检查缓存的版本是否匹配
|
||||
let useCache = false
|
||||
try {
|
||||
const cachedVersion = await fs.readFile(plotlyJsVersionPath, 'utf-8')
|
||||
if (cachedVersion.trim() === currentVersion) {
|
||||
useCache = true
|
||||
} else {
|
||||
Logger.info(`[Calculator] Cached plotly version ${cachedVersion} doesn't match current ${currentVersion}`)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.info('[Calculator] No cached plotly version info found')
|
||||
}
|
||||
|
||||
// 尝试从本地缓存加载
|
||||
if (useCache) {
|
||||
try {
|
||||
if (!plotlyCodeCache) {
|
||||
plotlyCodeCache = await fs.readFile(plotlyJsPath, 'utf-8')
|
||||
Logger.info('[Calculator] Loaded plotly from local cache')
|
||||
}
|
||||
} catch (err) {
|
||||
useCache = false
|
||||
Logger.warn('[Calculator] Failed to read cached plotly:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果缓存不可用或版本不匹配,从网络加载
|
||||
if (!useCache) {
|
||||
try {
|
||||
Logger.info('[Calculator] Downloading plotly from unpkg.com')
|
||||
const response = await axios.get(`https://unpkg.com/plotly.js-dist@${currentVersion}/plotly.js`)
|
||||
plotlyCodeCache = response.data
|
||||
|
||||
// 同步保存到本地,确保缓存文件被写入
|
||||
await fs.writeFile(plotlyJsPath, plotlyCodeCache || '')
|
||||
await fs.writeFile(plotlyJsVersionPath, currentVersion)
|
||||
Logger.info(`[Calculator] Saved plotly to ${plotlyJsPath}`)
|
||||
} catch (err) {
|
||||
Logger.error('[Calculator] Failed to download plotly:', err)
|
||||
// 如果下载失败但有旧缓存,尝试使用旧缓存
|
||||
if (!plotlyCodeCache) {
|
||||
try {
|
||||
plotlyCodeCache = await fs.readFile(plotlyJsPath, 'utf-8')
|
||||
Logger.info('[Calculator] Falling back to existing plotly cache despite version mismatch')
|
||||
} catch (readErr) {
|
||||
Logger.error('[Calculator] No fallback plotly cache available:', readErr)
|
||||
throw err // 重新抛出原始错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直接从内存执行代码
|
||||
const plotlyModule = { exports: {} }
|
||||
const moduleFn = new Function('module', 'exports', plotlyCodeCache!)
|
||||
moduleFn(plotlyModule, plotlyModule.exports)
|
||||
|
||||
plotly = plotlyModule.exports
|
||||
Logger.info('[Calculator] Successfully loaded plotly')
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error('[Calculator] Failed to load plotly:', error)
|
||||
|
||||
// 降级实现 - 返回一个简单的 SVG 生成函数
|
||||
plotly = {
|
||||
newPlot: () => {
|
||||
throw new Error('Plotly is not available')
|
||||
},
|
||||
toImage: () => {
|
||||
throw new Error('Plotly is not available')
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('[Calculator] Using fallback plotly implementation')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 创建一个加载 mathjs 的函数
|
||||
async function loadMathJs() {
|
||||
try {
|
||||
Logger.info('[Calculator] Loading mathjs from unpkg.com')
|
||||
// 如果已有实例,直接返回
|
||||
if (math) {
|
||||
Logger.info('[Calculator] Using cached mathjs instance')
|
||||
return true
|
||||
}
|
||||
|
||||
// 尝试从 unpkg.com 加载 mathjs
|
||||
const response = await axios.get('https://unpkg.com/mathjs@12.4.0/lib/browser/math.js')
|
||||
// 使用应用程序的用户数据目录而不是临时目录,确保缓存持久化
|
||||
const cacheDir = path.join(app.getPath('userData'), 'calculator-cache')
|
||||
const mathJsPath = path.join(cacheDir, 'math.js')
|
||||
const mathJsVersionPath = path.join(cacheDir, 'version.txt')
|
||||
const currentVersion = '12.4.0' // 当前使用的 mathjs 版本
|
||||
|
||||
// 创建一个临时目录来存储下载的脚本
|
||||
const tempDir = path.join(app.getPath('temp'), 'cherry-calculator')
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
// 确保缓存目录存在
|
||||
try {
|
||||
await fs.mkdir(cacheDir, { recursive: true })
|
||||
} catch (err) {
|
||||
Logger.warn('[Calculator] Failed to create cache directory:', err)
|
||||
}
|
||||
|
||||
const mathJsPath = path.join(tempDir, 'math.js')
|
||||
await fs.writeFile(mathJsPath, response.data)
|
||||
// 检查缓存的版本是否匹配
|
||||
let useCache = false
|
||||
try {
|
||||
const cachedVersion = await fs.readFile(mathJsVersionPath, 'utf-8')
|
||||
if (cachedVersion.trim() === currentVersion) {
|
||||
useCache = true
|
||||
} else {
|
||||
Logger.info(`[Calculator] Cached mathjs version ${cachedVersion} doesn't match current ${currentVersion}`)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.info('[Calculator] No cached version info found')
|
||||
}
|
||||
|
||||
Logger.info(`[Calculator] Saved mathjs to ${mathJsPath}`)
|
||||
// 尝试从本地缓存加载
|
||||
if (useCache) {
|
||||
try {
|
||||
if (!mathJsCodeCache) {
|
||||
mathJsCodeCache = await fs.readFile(mathJsPath, 'utf-8')
|
||||
Logger.info('[Calculator] Loaded mathjs from local cache')
|
||||
}
|
||||
} catch (err) {
|
||||
useCache = false
|
||||
Logger.warn('[Calculator] Failed to read cached mathjs:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 require 加载本地保存的脚本
|
||||
// 注意:这里使用了一个技巧,通过创建一个模块来执行脚本
|
||||
const mathJsCode = await fs.readFile(mathJsPath, 'utf-8')
|
||||
// 如果缓存不可用或版本不匹配,从网络加载
|
||||
if (!useCache) {
|
||||
try {
|
||||
Logger.info('[Calculator] Downloading mathjs from unpkg.com')
|
||||
const response = await axios.get(`https://unpkg.com/mathjs@${currentVersion}/lib/browser/math.js`)
|
||||
mathJsCodeCache = response.data
|
||||
|
||||
// 同步保存到本地,确保缓存文件被写入
|
||||
await fs.writeFile(mathJsPath, mathJsCodeCache || '')
|
||||
await fs.writeFile(mathJsVersionPath, currentVersion)
|
||||
Logger.info(`[Calculator] Saved mathjs to ${mathJsPath}`)
|
||||
} catch (err) {
|
||||
Logger.error('[Calculator] Failed to download mathjs:', err)
|
||||
// 如果下载失败但有旧缓存,尝试使用旧缓存
|
||||
if (!mathJsCodeCache) {
|
||||
try {
|
||||
mathJsCodeCache = await fs.readFile(mathJsPath, 'utf-8')
|
||||
Logger.info('[Calculator] Falling back to existing cache despite version mismatch')
|
||||
} catch (readErr) {
|
||||
Logger.error('[Calculator] No fallback cache available:', readErr)
|
||||
throw err // 重新抛出原始错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 直接从内存执行代码
|
||||
const mathModule = { exports: {} }
|
||||
const moduleFn = new Function('module', 'exports', mathJsCode)
|
||||
const moduleFn = new Function('module', 'exports', mathJsCodeCache!)
|
||||
moduleFn(mathModule, mathModule.exports)
|
||||
|
||||
math = mathModule.exports
|
||||
Logger.info('[Calculator] Successfully loaded mathjs')
|
||||
return true
|
||||
} catch (error) {
|
||||
Logger.error('[Calculator] Failed to load mathjs from unpkg.com:', error)
|
||||
Logger.error('[Calculator] Failed to load mathjs:', error)
|
||||
|
||||
// 如果加载失败,使用基本的数学函数
|
||||
// 降级实现
|
||||
math = {
|
||||
evaluate: (expr: string) => {
|
||||
// 安全的 eval 实现,仅支持基本数学运算
|
||||
// 这只是一个备用方案,功能有限
|
||||
return Function('"use strict"; return (' + expr + ')')()
|
||||
},
|
||||
format: (value: any) => String(value),
|
||||
@ -67,7 +223,6 @@ async function loadMathJs() {
|
||||
return {
|
||||
toNumber: () => value,
|
||||
to: (targetUnit: string) => {
|
||||
// 非常基本的单位转换,仅作为备用
|
||||
if (unit === 'inch' && targetUnit === 'cm') return { toNumber: () => value * 2.54 }
|
||||
if (unit === 'cm' && targetUnit === 'inch') return { toNumber: () => value / 2.54 }
|
||||
if (unit === 'kg' && targetUnit === 'lb') return { toNumber: () => value * 2.20462 }
|
||||
@ -741,8 +896,8 @@ const GRAPH_TOOL = {
|
||||
operation: {
|
||||
type: 'string',
|
||||
description:
|
||||
'图形操作,可选值:evaluate(函数求值)、roots(求根)、extrema(极值点)、inflection(拐点)、tangent(切线)、describe(图像描述)、intersect(交点)',
|
||||
enum: ['evaluate', 'roots', 'extrema', 'inflection', 'tangent', 'describe', 'intersect']
|
||||
'图形操作,可选值:evaluate(函数求值)、plot(绘制函数图像)、roots(求根)、extrema(极值点)、inflection(拐点)、tangent(切线)、describe(图像描述)、intersect(交点)',
|
||||
enum: ['evaluate', 'plot', 'roots', 'extrema', 'inflection', 'tangent', 'describe', 'intersect']
|
||||
},
|
||||
function: {
|
||||
type: 'string',
|
||||
@ -771,6 +926,26 @@ const GRAPH_TOOL = {
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: '当operation为describe时,函数的定义域,格式为:min,max'
|
||||
},
|
||||
xMin: {
|
||||
type: 'number',
|
||||
description: '当operation为plot时,x轴的最小值,默认为-10'
|
||||
},
|
||||
xMax: {
|
||||
type: 'number',
|
||||
description: '当operation为plot时,x轴的最大值,默认为10'
|
||||
},
|
||||
yMin: {
|
||||
type: 'number',
|
||||
description: '当operation为plot时,y轴的最小值,默认为-10'
|
||||
},
|
||||
yMax: {
|
||||
type: 'number',
|
||||
description: '当operation为plot时,y轴的最大值,默认为10'
|
||||
},
|
||||
points: {
|
||||
type: 'number',
|
||||
description: '当operation为plot时,用于绘制图像的点数,默认为100'
|
||||
}
|
||||
},
|
||||
required: ['operation', 'function']
|
||||
@ -803,27 +978,34 @@ class CalculatorServer {
|
||||
|
||||
Logger.info('[Calculator] Server initialized with tools capability')
|
||||
|
||||
// 初始化 mathjs 并设置请求处理程序
|
||||
this.initialize().then(() => {
|
||||
Logger.info('[Calculator] Server initialization completed')
|
||||
})
|
||||
// 立即设置请求处理程序,使工具列表可见
|
||||
this.setupRequestHandlers()
|
||||
|
||||
// 异步加载 mathjs,不阻塞工具列表显示
|
||||
this.initialize()
|
||||
.then(() => {
|
||||
Logger.info('[Calculator] Server initialization completed')
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error('[Calculator] Server initialization failed:', error)
|
||||
})
|
||||
|
||||
Logger.info('[Calculator] Server initialization started')
|
||||
}
|
||||
|
||||
// 初始化函数,加载 mathjs 并设置请求处理程序
|
||||
// 初始化函数,加载 mathjs 和 plotly
|
||||
private async initialize(): Promise<void> {
|
||||
try {
|
||||
// 加载 mathjs
|
||||
await loadMathJs()
|
||||
Logger.info('[Calculator] MathJS initialization complete')
|
||||
|
||||
// 设置请求处理程序
|
||||
this.setupRequestHandlers()
|
||||
|
||||
Logger.info('[Calculator] Server initialization complete')
|
||||
// 加载 plotly
|
||||
await loadPlotly()
|
||||
Logger.info('[Calculator] Plotly initialization complete')
|
||||
} catch (error) {
|
||||
Logger.error('[Calculator] Error during initialization:', error)
|
||||
throw error
|
||||
// 不抛出错误,让服务器继续运行,使用降级实现
|
||||
}
|
||||
}
|
||||
|
||||
@ -859,38 +1041,52 @@ class CalculatorServer {
|
||||
Logger.info(`[Calculator] Tool call received: ${name}`, args)
|
||||
|
||||
try {
|
||||
let result
|
||||
|
||||
if (name === 'calculate') {
|
||||
return this.handleCalculate(args)
|
||||
result = this.handleCalculate(args)
|
||||
} else if (name === 'convert_unit') {
|
||||
return this.handleUnitConvert(args)
|
||||
result = this.handleUnitConvert(args)
|
||||
} else if (name === 'statistics') {
|
||||
return this.handleStatistics(args)
|
||||
result = this.handleStatistics(args)
|
||||
} else if (name === 'solve_equation') {
|
||||
return this.handleEquationSolver(args)
|
||||
result = this.handleEquationSolver(args)
|
||||
} else if (name === 'calculus') {
|
||||
return this.handleCalculus(args)
|
||||
result = this.handleCalculus(args)
|
||||
} else if (name === 'matrix') {
|
||||
return this.handleMatrix(args)
|
||||
result = this.handleMatrix(args)
|
||||
} else if (name === 'probability') {
|
||||
return this.handleProbability(args)
|
||||
result = this.handleProbability(args)
|
||||
} else if (name === 'finance') {
|
||||
return this.handleFinance(args)
|
||||
result = this.handleFinance(args)
|
||||
} else if (name === 'physics') {
|
||||
return this.handlePhysics(args)
|
||||
result = this.handlePhysics(args)
|
||||
} else if (name === 'chemistry') {
|
||||
return this.handleChemistry(args)
|
||||
result = this.handleChemistry(args)
|
||||
} else if (name === 'programming') {
|
||||
return this.handleProgramming(args)
|
||||
result = this.handleProgramming(args)
|
||||
} else if (name === 'datetime') {
|
||||
return this.handleDateTime(args)
|
||||
result = this.handleDateTime(args)
|
||||
} else if (name === 'geometry') {
|
||||
return this.handleGeometry(args)
|
||||
result = this.handleGeometry(args)
|
||||
} else if (name === 'graph') {
|
||||
return this.handleGraph(args)
|
||||
result = this.handleGraph(args)
|
||||
} else {
|
||||
Logger.error(`[Calculator] Unknown tool: ${name}`)
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
|
||||
}
|
||||
|
||||
Logger.error(`[Calculator] Unknown tool: ${name}`)
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)
|
||||
// 确保结果是一个对象
|
||||
if (typeof result !== 'object') {
|
||||
result = { result }
|
||||
}
|
||||
|
||||
// 在结果中添加原始参数信息
|
||||
return {
|
||||
...result,
|
||||
// 添加一个特殊字段,包含原始参数
|
||||
_originalArgs: args
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[Calculator] Error handling tool call ${name}:`, error)
|
||||
return {
|
||||
@ -900,6 +1096,7 @@ class CalculatorServer {
|
||||
text: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
],
|
||||
_originalArgs: args, // 即使出错也包含原始参数
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
@ -1825,14 +2022,24 @@ class CalculatorServer {
|
||||
if (rate === undefined || nper === undefined || pmt === undefined) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Rate, nper, and pmt are required for pv')
|
||||
}
|
||||
result = math.finance.pv(rate, nper, pmt, fv || 0, type)
|
||||
// 计算现值 PV = PMT * ((1 - (1 + rate)^(-nper)) / rate) + FV * (1 + rate)^(-nper)
|
||||
result = pmt * ((1 - Math.pow(1 + rate, -nper)) / rate) + (fv || 0) * Math.pow(1 + rate, -nper)
|
||||
// 如果是期初付款,需要调整
|
||||
if (type === 1) {
|
||||
result = result * (1 + rate)
|
||||
}
|
||||
break
|
||||
|
||||
case 'fv':
|
||||
if (rate === undefined || nper === undefined || pmt === undefined) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Rate, nper, and pmt are required for fv')
|
||||
}
|
||||
result = math.finance.fv(rate, nper, pmt, pv || 0, type)
|
||||
// 计算终值 FV = PMT * ((1 + rate)^nper - 1) / rate + PV * (1 + rate)^nper
|
||||
result = pmt * ((Math.pow(1 + rate, nper) - 1) / rate) + (pv || 0) * Math.pow(1 + rate, nper)
|
||||
// 如果是期初付款,需要调整
|
||||
if (type === 1) {
|
||||
result = result * (1 + rate)
|
||||
}
|
||||
break
|
||||
|
||||
case 'pmt':
|
||||
@ -2675,7 +2882,7 @@ class CalculatorServer {
|
||||
// 根据操作类型处理几何计算
|
||||
switch (operation) {
|
||||
case 'distance':
|
||||
result = this.handleGeometryDistance()
|
||||
result = this.handleGeometryDistance(args)
|
||||
break
|
||||
case 'area':
|
||||
result = this.handleGeometryArea()
|
||||
@ -2729,10 +2936,89 @@ class CalculatorServer {
|
||||
}
|
||||
|
||||
// 处理距离计算
|
||||
private handleGeometryDistance() {
|
||||
// 距离计算的实现
|
||||
return {
|
||||
message: 'Distance calculation functionality will be implemented'
|
||||
private handleGeometryDistance(args: any) {
|
||||
const shape = args?.shape
|
||||
const points = args?.points
|
||||
|
||||
if (!shape || !points) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Shape and points are required for distance calculation')
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析点坐标
|
||||
const pointsArray = points.split(';').map((point: string) => {
|
||||
const coords = point.split(',').map((coord: string) => parseFloat(coord.trim()))
|
||||
return coords
|
||||
})
|
||||
|
||||
if (pointsArray.length < 2) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'At least two points are required for distance calculation')
|
||||
}
|
||||
|
||||
let distance: number
|
||||
|
||||
switch (shape) {
|
||||
case 'point':
|
||||
// 计算两点之间的距离
|
||||
if (pointsArray[0].length === 2 && pointsArray[1].length === 2) {
|
||||
// 2D 点
|
||||
const [x1, y1] = pointsArray[0]
|
||||
const [x2, y2] = pointsArray[1]
|
||||
distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
|
||||
} else if (pointsArray[0].length === 3 && pointsArray[1].length === 3) {
|
||||
// 3D 点
|
||||
const [x1, y1, z1] = pointsArray[0]
|
||||
const [x2, y2, z2] = pointsArray[1]
|
||||
distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) + Math.pow(z2 - z1, 2))
|
||||
} else {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Points must have the same dimensions (2D or 3D)')
|
||||
}
|
||||
break
|
||||
|
||||
case 'line':
|
||||
// 计算点到线的距离
|
||||
if (pointsArray.length < 3) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'For point-to-line distance, need at least 3 points: 2 for the line and 1 for the point'
|
||||
)
|
||||
}
|
||||
|
||||
if (pointsArray[0].length === 2 && pointsArray[1].length === 2 && pointsArray[2].length === 2) {
|
||||
// 2D 点到线的距离
|
||||
const [x1, y1] = pointsArray[0] // 线上的点1
|
||||
const [x2, y2] = pointsArray[1] // 线上的点2
|
||||
const [x0, y0] = pointsArray[2] // 要计算距离的点
|
||||
|
||||
// 计算点到线的距离: |Ax0 + By0 + C| / sqrt(A^2 + B^2)
|
||||
// 其中 Ax + By + C = 0 是线的方程
|
||||
const A = y2 - y1
|
||||
const B = x1 - x2
|
||||
const C = x2 * y1 - x1 * y2
|
||||
|
||||
distance = Math.abs(A * x0 + B * y0 + C) / Math.sqrt(A * A + B * B)
|
||||
} else {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Only 2D point-to-line distance is supported')
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.InvalidParams, `Distance calculation for shape '${shape}' is not supported`)
|
||||
}
|
||||
|
||||
return {
|
||||
shape: shape,
|
||||
points: pointsArray,
|
||||
distance: distance
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
throw error
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Error calculating distance: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2809,7 +3095,10 @@ class CalculatorServer {
|
||||
// 根据操作类型处理图形函数
|
||||
switch (operation) {
|
||||
case 'evaluate':
|
||||
result = this.handleGraphEvaluate()
|
||||
result = this.handleGraphEvaluate(args)
|
||||
break
|
||||
case 'plot':
|
||||
result = this.handleGraphPlot(args)
|
||||
break
|
||||
case 'roots':
|
||||
result = this.handleGraphRoots()
|
||||
@ -2861,10 +3150,38 @@ class CalculatorServer {
|
||||
}
|
||||
|
||||
// 处理函数求值
|
||||
private handleGraphEvaluate() {
|
||||
// 函数求值的实现
|
||||
return {
|
||||
message: 'Function evaluation functionality will be implemented'
|
||||
private handleGraphEvaluate(args: any) {
|
||||
const func = args?.function
|
||||
const variable = args?.variable || 'x'
|
||||
const point = args?.point
|
||||
|
||||
if (!func) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Function expression is required')
|
||||
}
|
||||
|
||||
if (point === undefined) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Point value is required for function evaluation')
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建一个包含变量值的对象
|
||||
const scope: Record<string, number> = {}
|
||||
scope[variable] = point
|
||||
|
||||
// 使用 mathjs 计算函数值
|
||||
const result = math.evaluate(func, scope)
|
||||
|
||||
return {
|
||||
function: func,
|
||||
variable: variable,
|
||||
point: point,
|
||||
value: result
|
||||
}
|
||||
} catch (error) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Error evaluating function: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2915,6 +3232,94 @@ class CalculatorServer {
|
||||
message: 'Intersection finding functionality will be implemented'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理函数图像绘制
|
||||
private async handleGraphPlot(args: any) {
|
||||
const func = args?.function
|
||||
const variable = args?.variable || 'x'
|
||||
const xMin = args?.xMin !== undefined ? args.xMin : -10
|
||||
const xMax = args?.xMax !== undefined ? args.xMax : 10
|
||||
const yMin = args?.yMin !== undefined ? args.yMin : -10
|
||||
const yMax = args?.yMax !== undefined ? args.yMax : 10
|
||||
const points = args?.points || 100
|
||||
|
||||
if (!func) {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Function expression is required')
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查 plotly 是否可用
|
||||
if (!plotly) {
|
||||
throw new Error('Plotly is not available')
|
||||
}
|
||||
|
||||
// 生成 x 值数组
|
||||
const xValues = Array.from({ length: points }, (_, i) => xMin + (i * (xMax - xMin)) / (points - 1))
|
||||
|
||||
// 计算 y 值
|
||||
const yValues = xValues.map((x) => {
|
||||
try {
|
||||
const scope: Record<string, number> = {}
|
||||
scope[variable] = x
|
||||
return math.evaluate(func, scope)
|
||||
} catch (error) {
|
||||
// 如果计算失败,返回 null
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
// 创建 plotly 图形
|
||||
const figure = {
|
||||
data: [
|
||||
{
|
||||
x: xValues,
|
||||
y: yValues,
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
line: { color: 'rgb(75, 192, 192)', width: 2 }
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
title: `y = ${func}`,
|
||||
xaxis: { range: [xMin, xMax], title: 'x' },
|
||||
yaxis: { range: [yMin, yMax], title: 'y' },
|
||||
width: 800,
|
||||
height: 500,
|
||||
margin: { l: 50, r: 50, b: 50, t: 50 },
|
||||
plot_bgcolor: 'rgb(240, 240, 240)',
|
||||
paper_bgcolor: 'rgb(255, 255, 255)'
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图像
|
||||
const imgBuffer = await plotly.toImage(figure, { format: 'png' })
|
||||
const base64Image = `data:image/png;base64,${imgBuffer.toString('base64')}`
|
||||
|
||||
return {
|
||||
function: func,
|
||||
variable: variable,
|
||||
domain: { x: [xMin, xMax], y: [yMin, yMax] },
|
||||
content: [
|
||||
{
|
||||
type: 'image',
|
||||
data: base64Image,
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[Calculator] Error plotting function:', error)
|
||||
|
||||
// 如果 plotly 不可用,返回一个简单的错误消息
|
||||
return {
|
||||
function: func,
|
||||
variable: variable,
|
||||
domain: { x: [xMin, xMax], y: [yMin, yMax] },
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
message: 'Unable to generate plot. Plotly.js may not be available.'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CalculatorServer
|
||||
|
||||
483
src/main/mcpServers/codeAnalysis/astParser.ts
Normal file
483
src/main/mcpServers/codeAnalysis/astParser.ts
Normal file
@ -0,0 +1,483 @@
|
||||
// src/main/mcpServers/codeAnalysis/astParser.ts
|
||||
// 代码AST解析器:使用TypeScript编译器API进行更准确的代码结构分析
|
||||
|
||||
import Logger from 'electron-log'
|
||||
import * as ts from 'typescript'
|
||||
|
||||
import { CodeParser } from './parser'
|
||||
import { CodeSymbol, Location, SymbolKind } from './types'
|
||||
|
||||
// TypeScript AST解析器 - 使用TS编译器API
|
||||
export class TypeScriptAstParser extends CodeParser {
|
||||
private compilerOptions: ts.CompilerOptions
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
// 配置编译器选项
|
||||
this.compilerOptions = {
|
||||
target: ts.ScriptTarget.Latest,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
allowJs: true,
|
||||
checkJs: true,
|
||||
esModuleInterop: true,
|
||||
jsx: ts.JsxEmit.React
|
||||
}
|
||||
}
|
||||
|
||||
async parseSymbols(filePath: string, content: string): Promise<CodeSymbol[]> {
|
||||
try {
|
||||
Logger.info(`[TypeScriptAstParser] 开始解析文件: ${filePath}`)
|
||||
|
||||
const symbols: CodeSymbol[] = []
|
||||
|
||||
// 创建源文件
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
content,
|
||||
this.compilerOptions.target || ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// 解析AST
|
||||
this.visitNode(sourceFile, sourceFile, symbols, undefined)
|
||||
|
||||
Logger.info(`[TypeScriptAstParser] 完成解析,找到 ${symbols.length} 个符号`)
|
||||
return symbols
|
||||
} catch (error) {
|
||||
Logger.error(`[TypeScriptAstParser] 解析文件失败: ${filePath}`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private visitNode(node: ts.Node, sourceFile: ts.SourceFile, symbols: CodeSymbol[], containerName?: string): void {
|
||||
// 解析不同类型的节点
|
||||
switch (node.kind) {
|
||||
// 类声明
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
this.visitClassDeclaration(node as ts.ClassDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 接口声明
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
this.visitInterfaceDeclaration(node as ts.InterfaceDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 函数声明
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
this.visitFunctionDeclaration(node as ts.FunctionDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 变量声明
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
this.visitVariableStatement(node as ts.VariableStatement, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 类型别名
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
this.visitTypeAliasDeclaration(node as ts.TypeAliasDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 枚举声明
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
this.visitEnumDeclaration(node as ts.EnumDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 模块/命名空间声明
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
this.visitModuleDeclaration(node as ts.ModuleDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
|
||||
// 导出声明
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
this.visitExportDeclaration(node as ts.ExportDeclaration, sourceFile, symbols, containerName)
|
||||
break
|
||||
}
|
||||
|
||||
// 递归访问子节点
|
||||
ts.forEachChild(node, (child) => this.visitNode(child, sourceFile, symbols, containerName))
|
||||
}
|
||||
|
||||
// 访问类声明
|
||||
private visitClassDeclaration(
|
||||
node: ts.ClassDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
if (!node.name) return
|
||||
|
||||
const className = node.name.text
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
// 添加类符号
|
||||
symbols.push({
|
||||
name: className,
|
||||
kind: SymbolKind.Class,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理类成员
|
||||
node.members.forEach((member) => {
|
||||
if (ts.isMethodDeclaration(member) && member.name) {
|
||||
// 方法
|
||||
const methodName = member.name.getText(sourceFile)
|
||||
const methodLocation = this.getNodeLocation(member, sourceFile)
|
||||
const methodModifiers = this.getNodeModifiers(member)
|
||||
|
||||
symbols.push({
|
||||
name: methodName,
|
||||
kind: SymbolKind.Method,
|
||||
location: methodLocation,
|
||||
containerName: className,
|
||||
modifiers: methodModifiers
|
||||
})
|
||||
} else if (ts.isPropertyDeclaration(member) && member.name) {
|
||||
// 属性
|
||||
const propertyName = member.name.getText(sourceFile)
|
||||
const propertyLocation = this.getNodeLocation(member, sourceFile)
|
||||
const propertyModifiers = this.getNodeModifiers(member)
|
||||
|
||||
symbols.push({
|
||||
name: propertyName,
|
||||
kind: SymbolKind.Property,
|
||||
location: propertyLocation,
|
||||
containerName: className,
|
||||
modifiers: propertyModifiers
|
||||
})
|
||||
} else if (ts.isConstructorDeclaration(member)) {
|
||||
// 构造函数
|
||||
const constructorLocation = this.getNodeLocation(member, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: 'constructor',
|
||||
kind: SymbolKind.Constructor,
|
||||
location: constructorLocation,
|
||||
containerName: className
|
||||
})
|
||||
|
||||
// 处理构造函数参数
|
||||
member.parameters.forEach((param) => {
|
||||
if (param.name) {
|
||||
const paramName = param.name.getText(sourceFile)
|
||||
const paramLocation = this.getNodeLocation(param, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: paramName,
|
||||
kind: SymbolKind.Parameter,
|
||||
location: paramLocation,
|
||||
containerName: `${className}.constructor`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 访问接口声明
|
||||
private visitInterfaceDeclaration(
|
||||
node: ts.InterfaceDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
const interfaceName = node.name.text
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
symbols.push({
|
||||
name: interfaceName,
|
||||
kind: SymbolKind.Interface,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理接口成员
|
||||
node.members.forEach((member) => {
|
||||
if (ts.isPropertySignature(member) && member.name) {
|
||||
const propertyName = member.name.getText(sourceFile)
|
||||
const propertyLocation = this.getNodeLocation(member, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: propertyName,
|
||||
kind: SymbolKind.Property,
|
||||
location: propertyLocation,
|
||||
containerName: interfaceName
|
||||
})
|
||||
} else if (ts.isMethodSignature(member) && member.name) {
|
||||
const methodName = member.name.getText(sourceFile)
|
||||
const methodLocation = this.getNodeLocation(member, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: methodName,
|
||||
kind: SymbolKind.Method,
|
||||
location: methodLocation,
|
||||
containerName: interfaceName
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 访问函数声明
|
||||
private visitFunctionDeclaration(
|
||||
node: ts.FunctionDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
if (!node.name) return
|
||||
|
||||
const functionName = node.name.text
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
// 获取函数参数和返回类型信息
|
||||
const parameters = node.parameters.map((p) => p.name.getText(sourceFile)).join(', ')
|
||||
const returnType = node.type ? node.type.getText(sourceFile) : 'any'
|
||||
const details = `(${parameters}) => ${returnType}`
|
||||
|
||||
symbols.push({
|
||||
name: functionName,
|
||||
kind: SymbolKind.Function,
|
||||
location,
|
||||
containerName,
|
||||
details,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理函数参数
|
||||
node.parameters.forEach((param) => {
|
||||
const paramName = param.name.getText(sourceFile)
|
||||
const paramLocation = this.getNodeLocation(param, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: paramName,
|
||||
kind: SymbolKind.Parameter,
|
||||
location: paramLocation,
|
||||
containerName: functionName
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 访问变量声明
|
||||
private visitVariableStatement(
|
||||
node: ts.VariableStatement,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
node.declarationList.declarations.forEach((declaration) => {
|
||||
const name = declaration.name.getText(sourceFile)
|
||||
const location = this.getNodeLocation(declaration, sourceFile)
|
||||
|
||||
// 确定变量类型(常量还是变量)
|
||||
const isConst = node.declarationList.flags & ts.NodeFlags.Const
|
||||
const kind = isConst ? SymbolKind.Constant : SymbolKind.Variable
|
||||
|
||||
// 检查是否是函数表达式
|
||||
let details: string | undefined = undefined
|
||||
if (
|
||||
declaration.initializer &&
|
||||
(ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
|
||||
) {
|
||||
// 这是一个函数表达式或箭头函数
|
||||
const params = declaration.initializer.parameters.map((p) => p.name.getText(sourceFile)).join(', ')
|
||||
|
||||
details = `(${params}) => ...`
|
||||
|
||||
symbols.push({
|
||||
name,
|
||||
kind: SymbolKind.Function, // 将函数表达式标记为函数,而不是变量
|
||||
location,
|
||||
containerName,
|
||||
details,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理函数参数
|
||||
declaration.initializer.parameters.forEach((param) => {
|
||||
const paramName = param.name.getText(sourceFile)
|
||||
const paramLocation = this.getNodeLocation(param, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: paramName,
|
||||
kind: SymbolKind.Parameter,
|
||||
location: paramLocation,
|
||||
containerName: name
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 普通变量
|
||||
symbols.push({
|
||||
name,
|
||||
kind,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 访问类型别名
|
||||
private visitTypeAliasDeclaration(
|
||||
node: ts.TypeAliasDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
const name = node.name.text
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
symbols.push({
|
||||
name,
|
||||
kind: SymbolKind.TypeAlias,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
}
|
||||
|
||||
// 访问枚举声明
|
||||
private visitEnumDeclaration(
|
||||
node: ts.EnumDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
const name = node.name.text
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
|
||||
symbols.push({
|
||||
name,
|
||||
kind: SymbolKind.Enum,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理枚举成员
|
||||
node.members.forEach((member) => {
|
||||
const memberName = member.name.getText(sourceFile)
|
||||
const memberLocation = this.getNodeLocation(member, sourceFile)
|
||||
|
||||
symbols.push({
|
||||
name: memberName,
|
||||
kind: SymbolKind.EnumMember,
|
||||
location: memberLocation,
|
||||
containerName: name
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 访问模块/命名空间声明
|
||||
private visitModuleDeclaration(
|
||||
node: ts.ModuleDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
const name = node.name.getText(sourceFile)
|
||||
const location = this.getNodeLocation(node, sourceFile)
|
||||
const modifiers = this.getNodeModifiers(node)
|
||||
const kind = node.flags & ts.NodeFlags.Namespace ? SymbolKind.Namespace : SymbolKind.Module
|
||||
|
||||
symbols.push({
|
||||
name,
|
||||
kind,
|
||||
location,
|
||||
containerName,
|
||||
modifiers
|
||||
})
|
||||
|
||||
// 处理模块内的声明
|
||||
if (node.body && ts.isModuleBlock(node.body)) {
|
||||
node.body.statements.forEach((statement) => {
|
||||
this.visitNode(statement, sourceFile, symbols, name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 访问导出声明
|
||||
private visitExportDeclaration(
|
||||
node: ts.ExportDeclaration,
|
||||
sourceFile: ts.SourceFile,
|
||||
symbols: CodeSymbol[],
|
||||
containerName?: string
|
||||
): void {
|
||||
// 这里我们只记录有命名导出的情况
|
||||
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
||||
node.exportClause.elements.forEach((element) => {
|
||||
const name = element.name.text
|
||||
const location = this.getNodeLocation(element, sourceFile)
|
||||
|
||||
// 记录导出的符号
|
||||
symbols.push({
|
||||
name,
|
||||
kind: SymbolKind.Variable, // 默认作为变量,实际类型需要通过解析导出的实际引用来确定
|
||||
location,
|
||||
containerName,
|
||||
modifiers: ['export']
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点的位置信息
|
||||
private getNodeLocation(node: ts.Node, sourceFile: ts.SourceFile): Location {
|
||||
const start = node.getStart(sourceFile)
|
||||
const end = node.getEnd()
|
||||
|
||||
const startLineCol = ts.getLineAndCharacterOfPosition(sourceFile, start)
|
||||
const endLineCol = ts.getLineAndCharacterOfPosition(sourceFile, end)
|
||||
|
||||
return {
|
||||
filePath: sourceFile.fileName,
|
||||
startLine: startLineCol.line + 1, // 转为1-based
|
||||
endLine: endLineCol.line + 1,
|
||||
startColumn: startLineCol.character + 1,
|
||||
endColumn: endLineCol.character + 1
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点的修饰符
|
||||
private getNodeModifiers(node: ts.Node): string[] | undefined {
|
||||
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined
|
||||
if (!modifiers || modifiers.length === 0) return undefined
|
||||
|
||||
return modifiers.map((modifier) => {
|
||||
switch (modifier.kind) {
|
||||
case ts.SyntaxKind.ExportKeyword:
|
||||
return 'export'
|
||||
case ts.SyntaxKind.DefaultKeyword:
|
||||
return 'default'
|
||||
case ts.SyntaxKind.ConstKeyword:
|
||||
return 'const'
|
||||
case ts.SyntaxKind.AsyncKeyword:
|
||||
return 'async'
|
||||
case ts.SyntaxKind.PublicKeyword:
|
||||
return 'public'
|
||||
case ts.SyntaxKind.PrivateKeyword:
|
||||
return 'private'
|
||||
case ts.SyntaxKind.ProtectedKeyword:
|
||||
return 'protected'
|
||||
case ts.SyntaxKind.ReadonlyKeyword:
|
||||
return 'readonly'
|
||||
case ts.SyntaxKind.StaticKeyword:
|
||||
return 'static'
|
||||
case ts.SyntaxKind.AbstractKeyword:
|
||||
return 'abstract'
|
||||
default:
|
||||
return 'unknown'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1382
src/main/mcpServers/codeAnalysis/codeAnalyzer.ts
Normal file
1382
src/main/mcpServers/codeAnalysis/codeAnalyzer.ts
Normal file
File diff suppressed because it is too large
Load Diff
551
src/main/mcpServers/codeAnalysis/dependencyAnalyzer.ts
Normal file
551
src/main/mcpServers/codeAnalysis/dependencyAnalyzer.ts
Normal file
@ -0,0 +1,551 @@
|
||||
// src/main/mcpServers/codeAnalysis/dependencyAnalyzer.ts
|
||||
// 项目依赖分析器:分析代码中的模块依赖和引用关系
|
||||
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs/promises'
|
||||
import { minimatch } from 'minimatch'
|
||||
import * as path from 'path'
|
||||
import * as ts from 'typescript'
|
||||
|
||||
// 依赖节点类型
|
||||
export interface DependencyNode {
|
||||
filePath: string // 文件路径
|
||||
relativePath: string // 相对于工作区的路径
|
||||
imports: ImportInfo[] // 导入信息
|
||||
exports: ExportInfo[] // 导出信息
|
||||
dependencies: string[] // 依赖的模块路径
|
||||
dependents: string[] // 依赖于本模块的其他模块路径
|
||||
}
|
||||
|
||||
// 导入信息
|
||||
export interface ImportInfo {
|
||||
name: string // 导入的名称
|
||||
source: string // 导入源
|
||||
isDefault: boolean // 是否是默认导入
|
||||
isNamespace: boolean // 是否是命名空间导入
|
||||
localName?: string // 本地名称(如果有重命名)
|
||||
}
|
||||
|
||||
// 导出信息
|
||||
export interface ExportInfo {
|
||||
name: string // 导出的名称
|
||||
isDefault: boolean // 是否是默认导出
|
||||
isReExport: boolean // 是否是重新导出
|
||||
source?: string // 重新导出的源
|
||||
}
|
||||
|
||||
// 依赖图类型
|
||||
export interface DependencyGraph {
|
||||
nodes: { [filePath: string]: DependencyNode }
|
||||
externalDependencies: Set<string>
|
||||
}
|
||||
|
||||
// 依赖分析选项
|
||||
export interface DependencyAnalysisOptions {
|
||||
includePatterns?: string[] // 要包含的文件模式
|
||||
excludePatterns?: string[] // 要排除的文件模式
|
||||
includeNodeModules?: boolean // 是否包含node_modules
|
||||
maxDepth?: number // 最大递归深度
|
||||
}
|
||||
|
||||
// 项目依赖分析器类
|
||||
export class DependencyAnalyzer {
|
||||
private workspacePath: string
|
||||
private compilerOptions: ts.CompilerOptions
|
||||
private dependencyGraph: DependencyGraph
|
||||
private fileToModule: Map<string, string> = new Map()
|
||||
private moduleToFile: Map<string, string> = new Map()
|
||||
|
||||
constructor(workspacePath: string) {
|
||||
this.workspacePath = workspacePath
|
||||
this.compilerOptions = {
|
||||
target: ts.ScriptTarget.Latest,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
allowJs: true,
|
||||
allowSyntheticDefaultImports: true,
|
||||
esModuleInterop: true,
|
||||
resolveJsonModule: true,
|
||||
baseUrl: workspacePath
|
||||
}
|
||||
|
||||
this.dependencyGraph = {
|
||||
nodes: {},
|
||||
externalDependencies: new Set<string>()
|
||||
}
|
||||
}
|
||||
|
||||
// 分析项目依赖
|
||||
async analyzeDependencies(options: DependencyAnalysisOptions = {}): Promise<DependencyGraph> {
|
||||
const {
|
||||
includePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
excludePatterns = ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
||||
includeNodeModules = false,
|
||||
maxDepth = 10
|
||||
} = options
|
||||
|
||||
Logger.info(`[DependencyAnalyzer] 开始分析项目依赖: ${this.workspacePath}`)
|
||||
|
||||
// 重置依赖图
|
||||
this.dependencyGraph = {
|
||||
nodes: {},
|
||||
externalDependencies: new Set<string>()
|
||||
}
|
||||
|
||||
// 判断文件是否应该被处理
|
||||
const shouldProcessFile = (filePath: string): boolean => {
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
|
||||
// 检查排除模式
|
||||
if (!includeNodeModules && relativePath.includes('node_modules')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (excludePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查包含模式
|
||||
return includePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))
|
||||
}
|
||||
|
||||
// 查找所有符合条件的文件
|
||||
const fileQueue: { filePath: string; depth: number }[] = []
|
||||
|
||||
// 递归查找文件
|
||||
const findFiles = async (dirPath: string): Promise<void> => {
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 如果是目录并且不是被排除的,则递归处理
|
||||
const relativePath = path.relative(this.workspacePath, fullPath)
|
||||
if (!excludePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))) {
|
||||
await findFiles(fullPath)
|
||||
}
|
||||
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
||||
// 加入队列
|
||||
fileQueue.push({ filePath: fullPath, depth: 0 })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[DependencyAnalyzer] 查找文件失败: ${dirPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
await findFiles(this.workspacePath)
|
||||
|
||||
// 创建一个解析器主机
|
||||
const compilerHost = ts.createCompilerHost(this.compilerOptions)
|
||||
|
||||
// 处理文件队列
|
||||
while (fileQueue.length > 0) {
|
||||
const { filePath, depth } = fileQueue.shift()!
|
||||
|
||||
if (depth > maxDepth) {
|
||||
Logger.warn(`[DependencyAnalyzer] 达到最大递归深度 (${maxDepth}): ${filePath}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 跳过已处理的文件
|
||||
if (this.dependencyGraph.nodes[filePath]) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析文件依赖
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
const sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
fileContent,
|
||||
this.compilerOptions.target || ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
|
||||
// 创建依赖节点
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
const node: DependencyNode = {
|
||||
filePath,
|
||||
relativePath,
|
||||
imports: [],
|
||||
exports: [],
|
||||
dependencies: [],
|
||||
dependents: []
|
||||
}
|
||||
|
||||
// 分析导入和导出
|
||||
this.analyzeImportsAndExports(sourceFile, node)
|
||||
|
||||
// 解析模块说明符为实际文件路径
|
||||
for (const importInfo of node.imports) {
|
||||
const resolvedModule = ts.resolveModuleName(importInfo.source, filePath, this.compilerOptions, compilerHost)
|
||||
|
||||
if (resolvedModule.resolvedModule) {
|
||||
const resolvedFilePath = resolvedModule.resolvedModule.resolvedFileName
|
||||
|
||||
// 判断是内部依赖还是外部依赖
|
||||
if (resolvedFilePath.startsWith(this.workspacePath)) {
|
||||
// 内部依赖
|
||||
node.dependencies.push(resolvedFilePath)
|
||||
|
||||
// 将依赖文件加入队列
|
||||
if (!this.dependencyGraph.nodes[resolvedFilePath] && shouldProcessFile(resolvedFilePath)) {
|
||||
fileQueue.push({ filePath: resolvedFilePath, depth: depth + 1 })
|
||||
}
|
||||
} else {
|
||||
// 外部依赖
|
||||
this.dependencyGraph.externalDependencies.add(importInfo.source)
|
||||
}
|
||||
} else {
|
||||
// 无法解析的模块,可能是外部依赖
|
||||
this.dependencyGraph.externalDependencies.add(importInfo.source)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到依赖图
|
||||
this.dependencyGraph.nodes[filePath] = node
|
||||
|
||||
// 映射文件路径和模块名称
|
||||
const moduleName = this.getModuleNameFromFile(filePath)
|
||||
this.fileToModule.set(filePath, moduleName)
|
||||
this.moduleToFile.set(moduleName, filePath)
|
||||
} catch (error) {
|
||||
Logger.error(`[DependencyAnalyzer] 分析文件依赖失败: ${filePath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建依赖关系(添加dependents)
|
||||
for (const [filePath, node] of Object.entries(this.dependencyGraph.nodes)) {
|
||||
for (const dependency of node.dependencies) {
|
||||
if (this.dependencyGraph.nodes[dependency]) {
|
||||
this.dependencyGraph.nodes[dependency].dependents.push(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
`[DependencyAnalyzer] 依赖分析完成,找到 ${Object.keys(this.dependencyGraph.nodes).length} 个模块,${
|
||||
this.dependencyGraph.externalDependencies.size
|
||||
} 个外部依赖`
|
||||
)
|
||||
|
||||
return this.dependencyGraph
|
||||
}
|
||||
|
||||
// 分析文件中的导入和导出
|
||||
private analyzeImportsAndExports(sourceFile: ts.SourceFile, node: DependencyNode): void {
|
||||
// 遍历AST
|
||||
ts.forEachChild(sourceFile, (child) => {
|
||||
// 导入声明
|
||||
if (ts.isImportDeclaration(child)) {
|
||||
if (child.importClause && ts.isStringLiteral(child.moduleSpecifier)) {
|
||||
const source = child.moduleSpecifier.text
|
||||
|
||||
// 默认导入
|
||||
if (child.importClause.name) {
|
||||
node.imports.push({
|
||||
name: child.importClause.name.text,
|
||||
source,
|
||||
isDefault: true,
|
||||
isNamespace: false
|
||||
})
|
||||
}
|
||||
|
||||
// 命名导入
|
||||
if (child.importClause.namedBindings) {
|
||||
if (ts.isNamedImports(child.importClause.namedBindings)) {
|
||||
// import { x, y as z } from 'module'
|
||||
child.importClause.namedBindings.elements.forEach((element) => {
|
||||
const importName = element.propertyName ? element.propertyName.text : element.name.text
|
||||
const localName = element.name.text
|
||||
|
||||
node.imports.push({
|
||||
name: importName,
|
||||
source,
|
||||
isDefault: false,
|
||||
isNamespace: false,
|
||||
localName: importName !== localName ? localName : undefined
|
||||
})
|
||||
})
|
||||
} else if (ts.isNamespaceImport(child.importClause.namedBindings)) {
|
||||
// import * as ns from 'module'
|
||||
node.imports.push({
|
||||
name: '*',
|
||||
source,
|
||||
isDefault: false,
|
||||
isNamespace: true,
|
||||
localName: child.importClause.namedBindings.name.text
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 导出声明
|
||||
else if (ts.isExportDeclaration(child)) {
|
||||
if (!child.moduleSpecifier) {
|
||||
// export { x, y }
|
||||
if (child.exportClause && ts.isNamedExports(child.exportClause)) {
|
||||
child.exportClause.elements.forEach((element) => {
|
||||
node.exports.push({
|
||||
name: element.name.text,
|
||||
isDefault: false,
|
||||
isReExport: false
|
||||
})
|
||||
})
|
||||
}
|
||||
} else if (ts.isStringLiteral(child.moduleSpecifier)) {
|
||||
// export { x, y } from 'module'
|
||||
const source = child.moduleSpecifier.text
|
||||
|
||||
if (child.exportClause && ts.isNamedExports(child.exportClause)) {
|
||||
child.exportClause.elements.forEach((element) => {
|
||||
node.exports.push({
|
||||
name: element.name.text,
|
||||
isDefault: false,
|
||||
isReExport: true,
|
||||
source
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// export * from 'module'
|
||||
node.exports.push({
|
||||
name: '*',
|
||||
isDefault: false,
|
||||
isReExport: true,
|
||||
source
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// 导出变量/函数/类声明
|
||||
else if (
|
||||
(ts.isVariableStatement(child) ||
|
||||
ts.isFunctionDeclaration(child) ||
|
||||
ts.isClassDeclaration(child) ||
|
||||
ts.isInterfaceDeclaration(child) ||
|
||||
ts.isTypeAliasDeclaration(child) ||
|
||||
ts.isEnumDeclaration(child)) &&
|
||||
child.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
// export const x = 1; 或 export function f() {} 或 export class C {}
|
||||
if (ts.isVariableStatement(child)) {
|
||||
child.declarationList.declarations.forEach((decl) => {
|
||||
if (ts.isIdentifier(decl.name)) {
|
||||
node.exports.push({
|
||||
name: decl.name.text,
|
||||
isDefault: false,
|
||||
isReExport: false
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
(ts.isFunctionDeclaration(child) ||
|
||||
ts.isClassDeclaration(child) ||
|
||||
ts.isInterfaceDeclaration(child) ||
|
||||
ts.isTypeAliasDeclaration(child) ||
|
||||
ts.isEnumDeclaration(child)) &&
|
||||
child.name
|
||||
) {
|
||||
const isDefault = child.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
|
||||
|
||||
node.exports.push({
|
||||
name: child.name.text,
|
||||
isDefault,
|
||||
isReExport: false
|
||||
})
|
||||
}
|
||||
}
|
||||
// 默认导出表达式
|
||||
else if (ts.isExportAssignment(child) && !child.isExportEquals) {
|
||||
// export default ...
|
||||
node.exports.push({
|
||||
name: 'default',
|
||||
isDefault: true,
|
||||
isReExport: false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 从文件路径获取模块名称
|
||||
private getModuleNameFromFile(filePath: string): string {
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
// 去除扩展名
|
||||
return relativePath.replace(/\.(js|jsx|ts|tsx)$/, '')
|
||||
}
|
||||
|
||||
// 获取文件的所有依赖
|
||||
async getAllDependencies(filePath: string): Promise<string[]> {
|
||||
const node = this.dependencyGraph.nodes[filePath]
|
||||
if (!node) {
|
||||
return []
|
||||
}
|
||||
|
||||
const dependencies: string[] = []
|
||||
const visited = new Set<string>()
|
||||
|
||||
const traverse = (currentPath: string) => {
|
||||
if (visited.has(currentPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
visited.add(currentPath)
|
||||
const currentNode = this.dependencyGraph.nodes[currentPath]
|
||||
|
||||
if (!currentNode) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const dependency of currentNode.dependencies) {
|
||||
dependencies.push(dependency)
|
||||
traverse(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
traverse(filePath)
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
// 获取依赖于指定文件的所有文件
|
||||
async getAllDependents(filePath: string): Promise<string[]> {
|
||||
const node = this.dependencyGraph.nodes[filePath]
|
||||
if (!node) {
|
||||
return []
|
||||
}
|
||||
|
||||
const dependents: string[] = []
|
||||
const visited = new Set<string>()
|
||||
|
||||
const traverse = (currentPath: string) => {
|
||||
if (visited.has(currentPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
visited.add(currentPath)
|
||||
const currentNode = this.dependencyGraph.nodes[currentPath]
|
||||
|
||||
if (!currentNode) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const dependent of currentNode.dependents) {
|
||||
dependents.push(dependent)
|
||||
traverse(dependent)
|
||||
}
|
||||
}
|
||||
|
||||
traverse(filePath)
|
||||
|
||||
return dependents
|
||||
}
|
||||
|
||||
// 检查循环依赖
|
||||
findCircularDependencies(): string[][] {
|
||||
const circularDependencies: string[][] = []
|
||||
const visited = new Set<string>()
|
||||
const stack: string[] = []
|
||||
|
||||
const visit = (filePath: string) => {
|
||||
if (stack.includes(filePath)) {
|
||||
// 找到循环依赖
|
||||
const start = stack.indexOf(filePath)
|
||||
const cycle = stack.slice(start).concat(filePath)
|
||||
circularDependencies.push(cycle)
|
||||
return
|
||||
}
|
||||
|
||||
if (visited.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
visited.add(filePath)
|
||||
stack.push(filePath)
|
||||
|
||||
const node = this.dependencyGraph.nodes[filePath]
|
||||
if (node) {
|
||||
for (const dependency of node.dependencies) {
|
||||
visit(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
stack.pop()
|
||||
}
|
||||
|
||||
// 对每个文件检查循环依赖
|
||||
for (const filePath of Object.keys(this.dependencyGraph.nodes)) {
|
||||
visit(filePath)
|
||||
}
|
||||
|
||||
return circularDependencies
|
||||
}
|
||||
|
||||
// 计算文件的直接依赖数
|
||||
countDirectDependencies(filePath: string): number {
|
||||
const node = this.dependencyGraph.nodes[filePath]
|
||||
return node ? node.dependencies.length : 0
|
||||
}
|
||||
|
||||
// 计算依赖于文件的数量
|
||||
countDirectDependents(filePath: string): number {
|
||||
const node = this.dependencyGraph.nodes[filePath]
|
||||
return node ? node.dependents.length : 0
|
||||
}
|
||||
|
||||
// 获取引用最多的文件
|
||||
getMostReferencedFiles(limit: number = 10): { filePath: string; count: number }[] {
|
||||
const files = Object.keys(this.dependencyGraph.nodes)
|
||||
.map((filePath) => ({
|
||||
filePath,
|
||||
count: this.countDirectDependents(filePath)
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit)
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// 生成可视化的依赖图数据(用于前端展示)
|
||||
generateVisualizationData() {
|
||||
const nodes = Object.values(this.dependencyGraph.nodes).map((node) => ({
|
||||
id: node.filePath,
|
||||
label: path.basename(node.filePath),
|
||||
group: this.getFileGroup(node.filePath),
|
||||
dependencies: node.dependencies.length,
|
||||
dependents: node.dependents.length
|
||||
}))
|
||||
|
||||
const edges: Array<{ from: string; to: string }> = []
|
||||
|
||||
for (const node of Object.values(this.dependencyGraph.nodes)) {
|
||||
for (const dependency of node.dependencies) {
|
||||
edges.push({
|
||||
from: node.filePath,
|
||||
to: dependency
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
// 根据文件类型获取分组
|
||||
private getFileGroup(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
|
||||
if (ext === '.ts' || ext === '.tsx') {
|
||||
return 'typescript'
|
||||
} else if (ext === '.js' || ext === '.jsx') {
|
||||
return 'javascript'
|
||||
} else if (ext === '.css' || ext === '.scss' || ext === '.less') {
|
||||
return 'style'
|
||||
} else if (ext === '.json') {
|
||||
return 'json'
|
||||
} else {
|
||||
return 'other'
|
||||
}
|
||||
}
|
||||
}
|
||||
142
src/main/mcpServers/codeAnalysis/index.ts
Normal file
142
src/main/mcpServers/codeAnalysis/index.ts
Normal file
@ -0,0 +1,142 @@
|
||||
// src/main/mcpServers/codeAnalysis/index.ts
|
||||
// 代码分析模块入口文件
|
||||
|
||||
// 导出所有需要的类型和功能
|
||||
export * from './searcher'
|
||||
export * from './types'
|
||||
// 显式导出以解决歧义
|
||||
import { CodeParser } from './parser'
|
||||
export { CodeParser }
|
||||
export * from './parser'
|
||||
|
||||
// 统一导出 CodeAnalyzer 类,使用工厂函数创建实例
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { CodeSearcher, SymbolSearchOptions } from './searcher'
|
||||
import { SearchResult } from './types'
|
||||
|
||||
// 代码分析器配置选项
|
||||
export interface CodeAnalyzerOptions {
|
||||
workspacePath: string
|
||||
maxCacheSize?: number
|
||||
searchTimeout?: number
|
||||
}
|
||||
|
||||
// 代码分析器类 - 统一封装所有代码分析功能
|
||||
export class CodeAnalyzer {
|
||||
private searcher: CodeSearcher
|
||||
private workspacePath: string
|
||||
private searchTimeout: number
|
||||
|
||||
constructor(options: CodeAnalyzerOptions) {
|
||||
this.workspacePath = options.workspacePath
|
||||
this.searchTimeout = options.searchTimeout || 60000 // 默认超时时间:60秒
|
||||
this.searcher = new CodeSearcher(options.workspacePath)
|
||||
|
||||
Logger.info(`[CodeAnalyzer] 初始化代码分析器,工作区: ${this.workspacePath}`)
|
||||
}
|
||||
|
||||
// 符号搜索
|
||||
async searchSymbols(options: SymbolSearchOptions | string): Promise<SearchResult> {
|
||||
if (typeof options === 'string') {
|
||||
options = { symbolName: options }
|
||||
}
|
||||
|
||||
const {
|
||||
symbolName,
|
||||
symbolKind,
|
||||
includePatterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
||||
excludePatterns = [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/out/**',
|
||||
'**/*.exe',
|
||||
'**/*.dll',
|
||||
'**/*.so',
|
||||
'**/*.dylib',
|
||||
'**/*.bin',
|
||||
'**/*.dat',
|
||||
'**/*.pack',
|
||||
'**/*.pak',
|
||||
'**/*.node',
|
||||
'**/*.wasm'
|
||||
],
|
||||
caseSensitive = false,
|
||||
maxResults = 50,
|
||||
includeReferences = false
|
||||
} = options
|
||||
|
||||
Logger.info(
|
||||
`[CodeAnalyzer] 搜索符号: ${symbolName}${symbolKind ? `, 类型: ${symbolKind}` : ''}, caseSensitive: ${caseSensitive}`
|
||||
)
|
||||
|
||||
try {
|
||||
// 文件过滤逻辑直接内联到代码中,不再使用单独的函数
|
||||
const searchOptions = {
|
||||
symbolName,
|
||||
symbolKind,
|
||||
includePatterns,
|
||||
excludePatterns,
|
||||
caseSensitive,
|
||||
maxResults,
|
||||
includeReferences
|
||||
}
|
||||
|
||||
// 查询符号
|
||||
const result = await this.searcher.searchSymbols(searchOptions)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeAnalyzer] 搜索符号失败: ${symbolName}`, error)
|
||||
return { symbols: [], references: [], contexts: [] }
|
||||
}
|
||||
}
|
||||
|
||||
// 查找引用
|
||||
async findReferences(options: import('./searcher').ReferenceSearchOptions) {
|
||||
return this.withTimeout(
|
||||
this.searcher.findReferences(options),
|
||||
this.searchTimeout,
|
||||
`查找引用超时: ${options.symbol.name}`
|
||||
)
|
||||
}
|
||||
|
||||
// 跳转到定义
|
||||
async goToDefinition(filePath: string, lineNumber: number, column: number) {
|
||||
return this.withTimeout(
|
||||
this.searcher.goToDefinition(filePath, lineNumber, column),
|
||||
this.searchTimeout,
|
||||
`跳转到定义超时: ${filePath}:${lineNumber}:${column}`
|
||||
)
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
this.searcher.clearCache()
|
||||
Logger.info('[CodeAnalyzer] 已清除代码分析器缓存')
|
||||
}
|
||||
|
||||
// 带超时的Promise包装
|
||||
private async withTimeout<T>(promise: Promise<T>, timeout: number, timeoutMessage: string): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
const timeoutPromise = new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(timeoutMessage))
|
||||
}, timeout)
|
||||
})
|
||||
|
||||
try {
|
||||
// 使用 Promise.race 实现超时控制
|
||||
return await Promise.race([promise, timeoutPromise])
|
||||
} finally {
|
||||
clearTimeout(timeoutId!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工厂函数 - 创建代码分析器实例
|
||||
export function createCodeAnalyzer(options: CodeAnalyzerOptions): CodeAnalyzer {
|
||||
return new CodeAnalyzer(options)
|
||||
}
|
||||
303
src/main/mcpServers/codeAnalysis/parser.ts
Normal file
303
src/main/mcpServers/codeAnalysis/parser.ts
Normal file
@ -0,0 +1,303 @@
|
||||
// src/main/mcpServers/codeAnalysis/parser.ts
|
||||
// 代码解析器:用于解析不同编程语言的代码结构
|
||||
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
import { CodeContext, CodeSymbol, SymbolKind } from './types'
|
||||
|
||||
// 支持的语言类型
|
||||
export enum LanguageType {
|
||||
TypeScript = 'typescript',
|
||||
JavaScript = 'javascript',
|
||||
JSON = 'json',
|
||||
HTML = 'html',
|
||||
CSS = 'css',
|
||||
Python = 'python',
|
||||
Java = 'java',
|
||||
CSharp = 'csharp',
|
||||
Go = 'go',
|
||||
Rust = 'rust',
|
||||
Unknown = 'unknown'
|
||||
}
|
||||
|
||||
// 语言文件扩展名映射
|
||||
const fileExtensionMap: Record<string, LanguageType> = {
|
||||
'.ts': LanguageType.TypeScript,
|
||||
'.tsx': LanguageType.TypeScript,
|
||||
'.js': LanguageType.JavaScript,
|
||||
'.jsx': LanguageType.JavaScript,
|
||||
'.json': LanguageType.JSON,
|
||||
'.html': LanguageType.HTML,
|
||||
'.htm': LanguageType.HTML,
|
||||
'.css': LanguageType.CSS,
|
||||
'.py': LanguageType.Python,
|
||||
'.java': LanguageType.Java,
|
||||
'.cs': LanguageType.CSharp,
|
||||
'.go': LanguageType.Go,
|
||||
'.rs': LanguageType.Rust
|
||||
}
|
||||
|
||||
// 从文件扩展名获取语言类型
|
||||
export function getLanguageFromFilePath(filePath: string): LanguageType {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
return fileExtensionMap[ext] || LanguageType.Unknown
|
||||
}
|
||||
|
||||
// 简易代码解析器基类
|
||||
export abstract class CodeParser {
|
||||
// 解析文件内容,提取代码符号
|
||||
abstract parseSymbols(filePath: string, content: string): Promise<CodeSymbol[]>
|
||||
|
||||
// 创建代码上下文
|
||||
async getCodeContext(filePath: string, line: number, contextSize: number = 5): Promise<CodeContext | null> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
const startLine = Math.max(1, line - contextSize)
|
||||
const endLine = Math.min(lines.length, line + contextSize)
|
||||
|
||||
// 将上下文分为前置、当前和后置行
|
||||
const beforeLines = lines.slice(startLine - 1, line - 1)
|
||||
const currentLine = lines[line - 1] || ''
|
||||
const afterLines = lines.slice(line, endLine)
|
||||
|
||||
return {
|
||||
symbol: undefined, // 相关符号(可选)
|
||||
lineNumber: line,
|
||||
line: currentLine,
|
||||
beforeLines: beforeLines,
|
||||
afterLines: afterLines
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`获取代码上下文失败: ${filePath}:${line}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 查找包含特定行的符号
|
||||
async findSymbolAtLine(symbols: CodeSymbol[], line: number): Promise<CodeSymbol | null> {
|
||||
for (const symbol of symbols) {
|
||||
if (line >= symbol.location.startLine && line <= symbol.location.endLine) {
|
||||
return symbol
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// TypeScript/JavaScript 代码解析器
|
||||
export class TypeScriptParser extends CodeParser {
|
||||
async parseSymbols(filePath: string, content: string): Promise<CodeSymbol[]> {
|
||||
const symbols: CodeSymbol[] = []
|
||||
const lines = content.split('\n')
|
||||
|
||||
// 使用正则表达式查找常见的TypeScript/JavaScript定义
|
||||
|
||||
// 查找类定义: class ClassName
|
||||
const classRegex = /class\s+([A-Za-z0-9_$]+)/g
|
||||
// 查找函数定义: function functionName() 或 const functionName = () =>
|
||||
const functionRegex =
|
||||
/(?:function\s+([A-Za-z0-9_$]+)|(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\(?.*\)?\s*=>)/g
|
||||
// 查找接口定义: interface InterfaceName
|
||||
const interfaceRegex = /interface\s+([A-Za-z0-9_$]+)(?:\s*extends\s+[A-Za-z0-9_$,\s]+)?(?:\s*\{|$)/g
|
||||
// 查找导出变量: export const/let/var varName
|
||||
const exportVarRegex = /export\s+(?:const|let|var)\s+([A-Za-z0-9_$]+)(?:\s*=|\s*:)/g
|
||||
// 查找方法定义: methodName() { 或 methodName = () => 或 methodName: () =>
|
||||
const methodRegex =
|
||||
/([A-Za-z0-9_$]+)\s*(?:\((?:[^)]*)\))(?:\s*:\s*[A-Za-z0-9_$<>[\]|,\s.]+)?\s*(?:\{|=>)|([A-Za-z0-9_$]+)\s*:\s*(?:async\s*)?(?:\([^)]*\))\s*(?:=>|\{)|([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g
|
||||
// 查找枚举定义: enum EnumName
|
||||
const enumRegex = /enum\s+([A-Za-z0-9_$]+)/g
|
||||
// 查找类型定义: type TypeName
|
||||
const typeRegex = /type\s+([A-Za-z0-9_$]+)(?:\s*=|\s*<)/g
|
||||
// 查找命名空间: namespace NamespaceName
|
||||
const namespaceRegex = /namespace\s+([A-Za-z0-9_$]+)/g
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// 查找类定义
|
||||
let match
|
||||
while ((match = classRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: SymbolKind.Class,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置lastIndex以便重新搜索
|
||||
classRegex.lastIndex = 0
|
||||
|
||||
// 查找函数定义
|
||||
while ((match = functionRegex.exec(line)) !== null) {
|
||||
const functionName = match[1] || match[2]
|
||||
if (functionName) {
|
||||
symbols.push({
|
||||
name: functionName,
|
||||
kind: SymbolKind.Function,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
functionRegex.lastIndex = 0
|
||||
|
||||
// 查找接口定义
|
||||
while ((match = interfaceRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: SymbolKind.Interface,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
interfaceRegex.lastIndex = 0
|
||||
|
||||
// 查找导出变量
|
||||
while ((match = exportVarRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: match[0].includes('const') ? SymbolKind.Constant : SymbolKind.Variable,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
},
|
||||
modifiers: ['export']
|
||||
})
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
exportVarRegex.lastIndex = 0
|
||||
|
||||
// 查找方法定义
|
||||
while ((match = methodRegex.exec(line)) !== null) {
|
||||
const methodName = match[1] || match[2] || match[3]
|
||||
if (methodName && !methodName.match(/^(if|for|while|switch|catch)$/)) {
|
||||
// 排除控制结构关键字
|
||||
symbols.push({
|
||||
name: methodName,
|
||||
kind: SymbolKind.Method,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
methodRegex.lastIndex = 0
|
||||
|
||||
// 查找枚举定义
|
||||
while ((match = enumRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: SymbolKind.Enum,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
enumRegex.lastIndex = 0
|
||||
|
||||
// 查找类型定义
|
||||
while ((match = typeRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: SymbolKind.TypeParameter,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
typeRegex.lastIndex = 0
|
||||
|
||||
// 查找命名空间
|
||||
while ((match = namespaceRegex.exec(line)) !== null) {
|
||||
symbols.push({
|
||||
name: match[1],
|
||||
kind: SymbolKind.Namespace,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: i + 1,
|
||||
endLine: i + 1,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
namespaceRegex.lastIndex = 0
|
||||
}
|
||||
|
||||
return symbols
|
||||
}
|
||||
}
|
||||
|
||||
// 根据语言类型获取相应的解析器实例
|
||||
export function getParserForLanguage(language: LanguageType): CodeParser {
|
||||
switch (language) {
|
||||
case LanguageType.TypeScript:
|
||||
case LanguageType.JavaScript:
|
||||
return new TypeScriptParser()
|
||||
// 可以添加其他语言的解析器
|
||||
default:
|
||||
// 默认使用TypeScript解析器
|
||||
return new TypeScriptParser()
|
||||
}
|
||||
}
|
||||
|
||||
// 用于解析文件的帮助函数
|
||||
export async function parseFile(filePath: string): Promise<CodeSymbol[]> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const language = getLanguageFromFilePath(filePath)
|
||||
const parser = getParserForLanguage(language)
|
||||
return await parser.parseSymbols(filePath, content)
|
||||
} catch (error) {
|
||||
Logger.error(`解析文件失败: ${filePath}`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
420
src/main/mcpServers/codeAnalysis/searcher.ts
Normal file
420
src/main/mcpServers/codeAnalysis/searcher.ts
Normal file
@ -0,0 +1,420 @@
|
||||
// src/main/mcpServers/codeAnalysis/searcher.ts
|
||||
// 代码搜索器:实现符号搜索、查找引用和上下文分析功能
|
||||
|
||||
import Logger from 'electron-log'
|
||||
import * as fs from 'fs/promises'
|
||||
import { minimatch } from 'minimatch'
|
||||
import * as path from 'path'
|
||||
|
||||
import { getLanguageFromFilePath, getParserForLanguage, parseFile } from './parser'
|
||||
import { CodeSymbol, Reference, SearchResult, SymbolKind } from './types'
|
||||
|
||||
// 用于符号搜索的选项
|
||||
export interface SymbolSearchOptions {
|
||||
symbolName: string // 要搜索的符号名称
|
||||
symbolKind?: SymbolKind // 符号类型过滤
|
||||
includePatterns?: string[] // 要包含的文件模式
|
||||
excludePatterns?: string[] // 要排除的文件模式
|
||||
caseSensitive?: boolean // 是否区分大小写
|
||||
maxResults?: number // 最大结果数量
|
||||
includeReferences?: boolean // 是否包含引用
|
||||
}
|
||||
|
||||
// 用于查找引用的选项
|
||||
export interface ReferenceSearchOptions {
|
||||
symbol: CodeSymbol // 要查找引用的符号
|
||||
includePatterns?: string[] // 要包含的文件模式
|
||||
excludePatterns?: string[] // 要排除的文件模式
|
||||
maxResults?: number // 最大结果数量
|
||||
}
|
||||
|
||||
// 代码搜索类
|
||||
export class CodeSearcher {
|
||||
private workspacePath: string
|
||||
private symbolCache: Map<string, CodeSymbol[]> = new Map()
|
||||
|
||||
constructor(workspacePath: string) {
|
||||
this.workspacePath = workspacePath
|
||||
}
|
||||
|
||||
// 搜索符号
|
||||
async searchSymbols(options: SymbolSearchOptions): Promise<SearchResult> {
|
||||
const {
|
||||
symbolName,
|
||||
symbolKind,
|
||||
includePatterns = [],
|
||||
excludePatterns = [],
|
||||
caseSensitive = false,
|
||||
maxResults = 100,
|
||||
includeReferences = false
|
||||
} = options
|
||||
|
||||
Logger.info(`[CodeSearcher] 开始搜索符号: ${symbolName}`)
|
||||
|
||||
const result: SearchResult = {
|
||||
symbols: [],
|
||||
references: [],
|
||||
contexts: []
|
||||
}
|
||||
|
||||
// 判断文件是否应该被处理
|
||||
const shouldProcessFile = (filePath: string): boolean => {
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
|
||||
// 检查排除模式
|
||||
if (excludePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查包含模式
|
||||
if (
|
||||
includePatterns.length > 0 &&
|
||||
!includePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 递归搜索符号
|
||||
const searchDirectory = async (dirPath: string): Promise<void> => {
|
||||
if (result.symbols.length >= maxResults) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (result.symbols.length >= maxResults) {
|
||||
break
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 递归处理子目录
|
||||
await searchDirectory(fullPath)
|
||||
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
||||
// 处理文件
|
||||
await this.searchSymbolsInFile(fullPath, {
|
||||
symbolName,
|
||||
symbolKind,
|
||||
caseSensitive,
|
||||
maxResults: maxResults - result.symbols.length,
|
||||
result
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 搜索目录失败: ${dirPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
await searchDirectory(this.workspacePath)
|
||||
|
||||
// 如果需要,查找引用
|
||||
if (includeReferences && result.symbols.length > 0) {
|
||||
for (const symbol of result.symbols) {
|
||||
if (result.references.length >= maxResults) {
|
||||
break
|
||||
}
|
||||
|
||||
const refOptions: ReferenceSearchOptions = {
|
||||
symbol,
|
||||
includePatterns,
|
||||
excludePatterns,
|
||||
maxResults: maxResults - result.references.length
|
||||
}
|
||||
|
||||
const references = await this.findReferences(refOptions)
|
||||
result.references.push(...references)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取代码上下文
|
||||
for (const symbol of result.symbols) {
|
||||
try {
|
||||
const parser = getParserForLanguage(getLanguageFromFilePath(symbol.location.filePath))
|
||||
const context = await parser.getCodeContext(
|
||||
symbol.location.filePath,
|
||||
symbol.location.startLine,
|
||||
5 // 上下文行数
|
||||
)
|
||||
|
||||
if (context) {
|
||||
context.symbol = symbol
|
||||
result.contexts.push(context)
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 获取符号上下文失败: ${symbol.name}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(`[CodeSearcher] 符号搜索完成,找到 ${result.symbols.length} 个符号,${result.references.length} 个引用`)
|
||||
return result
|
||||
}
|
||||
|
||||
// 在单个文件中搜索符号
|
||||
private async searchSymbolsInFile(
|
||||
filePath: string,
|
||||
options: {
|
||||
symbolName: string
|
||||
symbolKind?: SymbolKind
|
||||
caseSensitive: boolean
|
||||
maxResults: number
|
||||
result: SearchResult
|
||||
}
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 获取或解析文件中的符号
|
||||
let symbols = this.symbolCache.get(filePath)
|
||||
if (!symbols) {
|
||||
symbols = await parseFile(filePath)
|
||||
this.symbolCache.set(filePath, symbols)
|
||||
}
|
||||
|
||||
// 过滤符号
|
||||
const { symbolName, symbolKind, caseSensitive, result } = options
|
||||
const regex = new RegExp(`^${symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, caseSensitive ? '' : 'i')
|
||||
|
||||
for (const symbol of symbols) {
|
||||
if (regex.test(symbol.name) && (!symbolKind || symbol.kind === symbolKind)) {
|
||||
result.symbols.push(symbol)
|
||||
|
||||
if (result.symbols.length >= options.maxResults) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 搜索文件中的符号失败: ${filePath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查找符号的引用
|
||||
async findReferences(options: ReferenceSearchOptions): Promise<Reference[]> {
|
||||
const { symbol, includePatterns = [], excludePatterns = [], maxResults = 100 } = options
|
||||
|
||||
Logger.info(`[CodeSearcher] 开始查找符号的引用: ${symbol.name}`)
|
||||
|
||||
const references: Reference[] = []
|
||||
|
||||
// 将定义添加为第一个引用
|
||||
references.push({
|
||||
symbol,
|
||||
location: symbol.location,
|
||||
isDefinition: true,
|
||||
context: await this.getLineContent(symbol.location.filePath, symbol.location.startLine)
|
||||
})
|
||||
|
||||
// 判断文件是否应该被处理
|
||||
const shouldProcessFile = (filePath: string): boolean => {
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
|
||||
// 检查排除模式
|
||||
if (excludePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查包含模式
|
||||
if (
|
||||
includePatterns.length > 0 &&
|
||||
!includePatterns.some((pattern) => minimatch(relativePath, pattern, { dot: true }))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 搜索引用
|
||||
const searchReferencesInFiles = async (dirPath: string): Promise<void> => {
|
||||
if (references.length >= maxResults) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (references.length >= maxResults) {
|
||||
break
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 递归处理子目录
|
||||
await searchReferencesInFiles(fullPath)
|
||||
} else if (entry.isFile() && shouldProcessFile(fullPath)) {
|
||||
// 处理文件,搜索符号的引用
|
||||
await this.searchReferencesInFile(fullPath, {
|
||||
symbol,
|
||||
maxResults: maxResults - references.length,
|
||||
references
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 搜索目录引用失败: ${dirPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始搜索
|
||||
await searchReferencesInFiles(this.workspacePath)
|
||||
|
||||
Logger.info(`[CodeSearcher] 引用搜索完成,找到 ${references.length} 个引用`)
|
||||
return references
|
||||
}
|
||||
|
||||
// 在单个文件中搜索引用
|
||||
private async searchReferencesInFile(
|
||||
filePath: string,
|
||||
options: {
|
||||
symbol: CodeSymbol
|
||||
maxResults: number
|
||||
references: Reference[]
|
||||
}
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { symbol, references } = options
|
||||
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
// 创建针对符号的正则表达式
|
||||
// 注意:这是一个简单的文本匹配,实际上应该使用更复杂的语法分析
|
||||
const regex = new RegExp(`\\b${symbol.name}\\b`, 'g')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
const lineNumber = i + 1
|
||||
|
||||
// 重置正则表达式的lastIndex
|
||||
regex.lastIndex = 0
|
||||
|
||||
// 查找引用
|
||||
let match
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// 排除自身定义
|
||||
if (filePath === symbol.location.filePath && lineNumber === symbol.location.startLine) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建引用
|
||||
references.push({
|
||||
symbol,
|
||||
location: {
|
||||
filePath,
|
||||
startLine: lineNumber,
|
||||
endLine: lineNumber,
|
||||
startColumn: match.index + 1,
|
||||
endColumn: match.index + match[0].length + 1
|
||||
},
|
||||
isDefinition: false,
|
||||
context: line
|
||||
})
|
||||
|
||||
if (references.length >= options.maxResults) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 搜索文件中的引用失败: ${filePath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取特定行的内容
|
||||
private async getLineContent(filePath: string, lineNumber: number): Promise<string> {
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
if (lineNumber >= 1 && lineNumber <= lines.length) {
|
||||
return lines[lineNumber - 1]
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 获取行内容失败: ${filePath}:${lineNumber}`, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到定义
|
||||
async goToDefinition(filePath: string, lineNumber: number, column: number): Promise<CodeSymbol | null> {
|
||||
try {
|
||||
// 读取文件内容
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
|
||||
if (lineNumber < 1 || lineNumber > lines.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const line = lines[lineNumber - 1]
|
||||
|
||||
// 尝试获取光标位置的符号名称
|
||||
const symbolName = this.getSymbolNameAtPosition(line, column)
|
||||
if (!symbolName) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 搜索符号定义
|
||||
const result = await this.searchSymbols({
|
||||
symbolName,
|
||||
caseSensitive: true,
|
||||
maxResults: 1
|
||||
})
|
||||
|
||||
return result.symbols.length > 0 ? result.symbols[0] : null
|
||||
} catch (error) {
|
||||
Logger.error(`[CodeSearcher] 跳转到定义失败: ${filePath}:${lineNumber}:${column}`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 获取光标位置的符号名称
|
||||
private getSymbolNameAtPosition(line: string, column: number): string | null {
|
||||
if (column < 1 || column > line.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 调整为0-based索引
|
||||
const pos = column - 1
|
||||
|
||||
// 向左查找符号开始位置
|
||||
let start = pos
|
||||
while (start > 0 && this.isIdentifierChar(line.charAt(start - 1))) {
|
||||
start--
|
||||
}
|
||||
|
||||
// 向右查找符号结束位置
|
||||
let end = pos
|
||||
while (end < line.length && this.isIdentifierChar(line.charAt(end))) {
|
||||
end++
|
||||
}
|
||||
|
||||
// 提取符号名称
|
||||
if (start < end) {
|
||||
return line.substring(start, end)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 判断字符是否为标识符字符
|
||||
private isIdentifierChar(char: string): boolean {
|
||||
return /[a-zA-Z0-9_$]/.test(char)
|
||||
}
|
||||
|
||||
// 清除符号缓存
|
||||
clearCache(): void {
|
||||
this.symbolCache.clear()
|
||||
}
|
||||
}
|
||||
185
src/main/mcpServers/codeAnalysis/types.ts
Normal file
185
src/main/mcpServers/codeAnalysis/types.ts
Normal file
@ -0,0 +1,185 @@
|
||||
// src/main/mcpServers/codeAnalysis/types.ts
|
||||
// 代码分析功能的类型定义
|
||||
|
||||
// 符号类型枚举
|
||||
export enum SymbolKind {
|
||||
Unknown = 0,
|
||||
File = 1,
|
||||
Module = 2,
|
||||
Namespace = 3,
|
||||
Package = 4,
|
||||
Class = 5,
|
||||
Method = 6,
|
||||
Property = 7,
|
||||
Field = 8,
|
||||
Constructor = 9,
|
||||
Enum = 10,
|
||||
Interface = 11,
|
||||
Function = 12,
|
||||
Variable = 13,
|
||||
Constant = 14,
|
||||
String = 15,
|
||||
Number = 16,
|
||||
Boolean = 17,
|
||||
Array = 18,
|
||||
Object = 19,
|
||||
Key = 20,
|
||||
EnumMember = 21,
|
||||
Struct = 22,
|
||||
Event = 23,
|
||||
Operator = 24,
|
||||
TypeParameter = 25,
|
||||
Parameter = 26,
|
||||
TypeAlias = 27
|
||||
}
|
||||
|
||||
// 符号位置信息
|
||||
export interface SymbolLocation {
|
||||
filePath: string // 文件路径
|
||||
startLine: number // 起始行号 (1-based)
|
||||
endLine: number // 结束行号 (1-based)
|
||||
startColumn?: number // 起始列号 (1-based, 可选)
|
||||
endColumn?: number // 结束列号 (1-based, 可选)
|
||||
}
|
||||
|
||||
// 为了兼容性,提供 Location 类型别名
|
||||
export type Location = SymbolLocation
|
||||
|
||||
// 代码符号接口
|
||||
export interface CodeSymbol {
|
||||
name: string // 符号名称
|
||||
kind: SymbolKind // 符号类型
|
||||
location: SymbolLocation // 符号位置
|
||||
containerName?: string // 父级容器名称 (如类、命名空间等)
|
||||
modifiers?: string[] // 修饰符 (如 public, static, export 等)
|
||||
children?: CodeSymbol[] // 子符号
|
||||
details?: string // 附加详情 (如函数签名、类型信息等)
|
||||
documentation?: string // 文档注释
|
||||
isDefinition?: boolean // 是否为定义 (而非声明)
|
||||
}
|
||||
|
||||
// 符号引用接口
|
||||
export interface Reference {
|
||||
symbol: CodeSymbol // 被引用的符号
|
||||
location: SymbolLocation // 引用位置
|
||||
isDefinition: boolean // 是否为定义引用
|
||||
context: string // 引用上下文 (代码行)
|
||||
}
|
||||
|
||||
// 代码上下文接口
|
||||
export interface CodeContext {
|
||||
symbol?: CodeSymbol // 相关符号 (可选)
|
||||
beforeLines: string[] // 前置代码行
|
||||
line: string // 当前代码行
|
||||
afterLines: string[] // 后置代码行
|
||||
lineNumber: number // 代码行号 (1-based)
|
||||
}
|
||||
|
||||
// 搜索结果
|
||||
export interface SearchResult {
|
||||
symbols: CodeSymbol[] // 符号列表
|
||||
references: Reference[] // 引用列表
|
||||
contexts: CodeContext[] // 上下文列表
|
||||
}
|
||||
|
||||
// 代码语言类型
|
||||
export enum CodeLanguage {
|
||||
Unknown = 'unknown',
|
||||
TypeScript = 'typescript',
|
||||
JavaScript = 'javascript',
|
||||
HTML = 'html',
|
||||
CSS = 'css',
|
||||
JSON = 'json',
|
||||
Markdown = 'markdown',
|
||||
Python = 'python',
|
||||
Java = 'java',
|
||||
CSharp = 'csharp',
|
||||
CPlusPlus = 'cpp',
|
||||
C = 'c',
|
||||
Go = 'go',
|
||||
Rust = 'rust',
|
||||
Ruby = 'ruby',
|
||||
PHP = 'php',
|
||||
Swift = 'swift',
|
||||
Kotlin = 'kotlin',
|
||||
Dart = 'dart',
|
||||
YAML = 'yaml'
|
||||
}
|
||||
|
||||
// 代码解析器接口
|
||||
export interface CodeParser {
|
||||
// 解析文件中的所有符号
|
||||
parseSymbols(filePath: string, content?: string): Promise<CodeSymbol[]>
|
||||
|
||||
// 查找特定行上的符号
|
||||
findSymbolAtLine(symbols: CodeSymbol[], line: number): Promise<CodeSymbol | null>
|
||||
|
||||
// 获取代码上下文
|
||||
getCodeContext(filePath: string, line: number, contextLines?: number): Promise<CodeContext | null>
|
||||
}
|
||||
|
||||
// 语法错误信息
|
||||
export interface SyntaxError {
|
||||
message: string // 错误消息
|
||||
location: SymbolLocation // 错误位置
|
||||
severity: ErrorSeverity // 错误严重性
|
||||
code?: string // 错误代码 (可选)
|
||||
source?: string // 错误来源 (如 eslint, tsc 等)
|
||||
}
|
||||
|
||||
// 错误严重性枚举
|
||||
export enum ErrorSeverity {
|
||||
Error = 'error',
|
||||
Warning = 'warning',
|
||||
Information = 'information',
|
||||
Hint = 'hint'
|
||||
}
|
||||
|
||||
// 代码指标
|
||||
export interface CodeMetrics {
|
||||
lineCount: number // 代码行数
|
||||
commentLineCount: number // 注释行数
|
||||
functionCount: number // 函数数量
|
||||
classCount: number // 类数量
|
||||
complexity: number // 复杂度指标
|
||||
dependencies: number // 依赖数量
|
||||
}
|
||||
|
||||
// 代码补全项
|
||||
export interface CodeCompletionItem {
|
||||
label: string // 显示标签
|
||||
detail?: string // 详细说明
|
||||
kind: CompletionItemKind // 补全项类型
|
||||
documentation?: string // 文档说明
|
||||
insertText: string // 插入文本
|
||||
sortText?: string // 排序文本
|
||||
}
|
||||
|
||||
// 补全项类型枚举
|
||||
export enum CompletionItemKind {
|
||||
Text = 1,
|
||||
Method = 2,
|
||||
Function = 3,
|
||||
Constructor = 4,
|
||||
Field = 5,
|
||||
Variable = 6,
|
||||
Class = 7,
|
||||
Interface = 8,
|
||||
Module = 9,
|
||||
Property = 10,
|
||||
Unit = 11,
|
||||
Value = 12,
|
||||
Enum = 13,
|
||||
Keyword = 14,
|
||||
Snippet = 15,
|
||||
Color = 16,
|
||||
File = 17,
|
||||
Reference = 18,
|
||||
Folder = 19,
|
||||
EnumMember = 20,
|
||||
Constant = 21,
|
||||
Struct = 22,
|
||||
Event = 23,
|
||||
Operator = 24,
|
||||
TypeParameter = 25
|
||||
}
|
||||
@ -15,6 +15,38 @@ import path from 'path'
|
||||
import { z } from 'zod'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
import {
|
||||
CodeAnalyzer,
|
||||
createCodeAnalyzer,
|
||||
getLanguageFromFilePath,
|
||||
getParserForLanguage,
|
||||
parseFile,
|
||||
SymbolKind
|
||||
} from './codeAnalysis'
|
||||
import { DependencyAnalyzer } from './codeAnalysis/dependencyAnalyzer'
|
||||
|
||||
// 默认的文件过滤模式
|
||||
const DEFAULT_INCLUDE_PATTERNS = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.json', '**/*.vue']
|
||||
const DEFAULT_EXCLUDE_PATTERNS = [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/out/**',
|
||||
'**/.git/**',
|
||||
'**/coverage/**',
|
||||
'**/*.min.*',
|
||||
'**/*.exe',
|
||||
'**/*.dll',
|
||||
'**/*.so',
|
||||
'**/*.dylib',
|
||||
'**/*.bin',
|
||||
'**/*.dat',
|
||||
'**/*.pack',
|
||||
'**/*.pak',
|
||||
'**/*.node',
|
||||
'**/*.wasm'
|
||||
]
|
||||
|
||||
// 工具名称常量
|
||||
const TOOL_READ_FILE = 'workspace_read_file'
|
||||
const TOOL_WRITE_FILE = 'workspace_write_file'
|
||||
@ -22,6 +54,18 @@ const TOOL_SEARCH_FILES = 'workspace_search_files'
|
||||
const TOOL_LIST_FILES = 'workspace_list_files'
|
||||
const TOOL_CREATE_FILE = 'workspace_create_file'
|
||||
const TOOL_EDIT_FILE = 'workspace_edit_file'
|
||||
const TOOL_DELETE_PATH = 'workspace_delete_path'
|
||||
const TOOL_SEARCH_FILE_CONTENT = 'workspace_search_file_content'
|
||||
const TOOL_SEARCH_SYMBOLS = 'workspace_search_symbols'
|
||||
const TOOL_FIND_REFERENCES = 'workspace_find_references'
|
||||
const TOOL_GO_TO_DEFINITION = 'workspace_go_to_definition'
|
||||
const TOOL_GET_CODE_CONTEXT = 'workspace_get_code_context'
|
||||
// 依赖分析工具常量
|
||||
const TOOL_ANALYZE_DEPENDENCIES = 'workspace_analyze_dependencies'
|
||||
const TOOL_GET_FILE_DEPENDENCIES = 'workspace_get_file_dependencies'
|
||||
const TOOL_GET_FILE_DEPENDENTS = 'workspace_get_file_dependents'
|
||||
const TOOL_CHECK_CIRCULAR_DEPENDENCIES = 'workspace_check_circular_dependencies'
|
||||
const TOOL_GET_MOST_REFERENCED_FILES = 'workspace_get_most_referenced_files'
|
||||
|
||||
// 规范化路径
|
||||
function normalizePath(p: string): string {
|
||||
@ -140,6 +184,113 @@ const EditFileArgsSchema = z.object({
|
||||
.describe('要应用的更改数组')
|
||||
})
|
||||
|
||||
const DeletePathArgsSchema = z.object({
|
||||
path: z.string().describe('要删除的文件或目录路径,可以是相对于工作区的路径')
|
||||
})
|
||||
|
||||
const SearchFileContentArgsSchema = z.object({
|
||||
pattern: z.string().describe('要搜索的文本模式或正则表达式'),
|
||||
isRegex: z.boolean().optional().default(false).describe('是否将模式作为正则表达式处理'),
|
||||
caseSensitive: z.boolean().optional().default(false).describe('是否区分大小写'),
|
||||
includePatterns: z.array(z.string()).optional().default([]).describe('要包含的文件模式数组,例如["*.ts", "*.js"]'),
|
||||
excludePatterns: z.array(z.string()).optional().default([]).describe('要排除的文件模式数组'),
|
||||
maxResults: z.number().optional().default(100).describe('最大结果数量')
|
||||
})
|
||||
|
||||
// 符号搜索参数模式
|
||||
const SearchSymbolsArgsSchema = z.object({
|
||||
name: z.string().describe('要搜索的符号名称'),
|
||||
kind: z
|
||||
.enum([
|
||||
'function',
|
||||
'class',
|
||||
'interface',
|
||||
'variable',
|
||||
'constant',
|
||||
'property',
|
||||
'method',
|
||||
'enum',
|
||||
'enumMember',
|
||||
'parameter',
|
||||
'typeAlias',
|
||||
'namespace',
|
||||
'module',
|
||||
'constructor',
|
||||
'unknown'
|
||||
])
|
||||
.optional()
|
||||
.describe('符号类型过滤'),
|
||||
includePatterns: z.array(z.string()).optional().default(DEFAULT_INCLUDE_PATTERNS).describe('要包含的文件模式数组'),
|
||||
excludePatterns: z.array(z.string()).optional().default(DEFAULT_EXCLUDE_PATTERNS).describe('要排除的文件模式数组'),
|
||||
caseSensitive: z.boolean().optional().default(false).describe('是否区分大小写'),
|
||||
maxResults: z.number().optional().default(50).describe('最大结果数量'),
|
||||
includeReferences: z.boolean().optional().default(false).describe('是否包含引用')
|
||||
})
|
||||
|
||||
// 查找引用参数模式
|
||||
const FindReferencesArgsSchema = z.object({
|
||||
filePath: z.string().describe('符号所在的文件路径'),
|
||||
line: z.number().describe('符号所在的行号(从1开始)'),
|
||||
column: z.number().optional().describe('符号所在的列号(从1开始)'),
|
||||
includePatterns: z.array(z.string()).optional().default(DEFAULT_INCLUDE_PATTERNS).describe('要包含的文件模式数组'),
|
||||
excludePatterns: z.array(z.string()).optional().default(DEFAULT_EXCLUDE_PATTERNS).describe('要排除的文件模式数组'),
|
||||
maxResults: z.number().optional().default(100).describe('最大结果数量')
|
||||
})
|
||||
|
||||
// 跳转到定义参数模式
|
||||
const GoToDefinitionArgsSchema = z.object({
|
||||
filePath: z.string().describe('文件路径'),
|
||||
line: z.number().describe('行号(从1开始)'),
|
||||
column: z.number().describe('列号(从1开始)')
|
||||
})
|
||||
|
||||
// 获取代码上下文参数模式
|
||||
const GetCodeContextArgsSchema = z.object({
|
||||
filePath: z.string().describe('文件路径'),
|
||||
line: z.number().describe('行号(从1开始)'),
|
||||
contextSize: z.number().optional().default(5).describe('上下文行数')
|
||||
})
|
||||
|
||||
// 定义依赖分析参数模式
|
||||
|
||||
const AnalyzeDependenciesArgsSchema = z.object({
|
||||
includePatterns: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default(['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'])
|
||||
.describe('要包含的文件模式数组'),
|
||||
excludePatterns: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default(['**/node_modules/**', '**/dist/**', '**/build/**'])
|
||||
.describe('要排除的文件模式数组'),
|
||||
includeNodeModules: z.boolean().optional().default(false).describe('是否包含node_modules中的文件'),
|
||||
maxDepth: z.number().optional().default(10).describe('最大递归深度')
|
||||
})
|
||||
|
||||
const GetFileDependenciesArgsSchema = z.object({
|
||||
filePath: z.string().describe('要分析依赖的文件路径')
|
||||
})
|
||||
|
||||
const GetFileDependentsArgsSchema = z.object({
|
||||
filePath: z.string().describe('要查找依赖项的文件路径')
|
||||
})
|
||||
|
||||
const CheckCircularDependenciesArgsSchema = z.object({
|
||||
showRelativePaths: z.boolean().optional().default(true).describe('是否显示相对路径而不是绝对路径')
|
||||
})
|
||||
|
||||
const GetMostReferencedFilesArgsSchema = z.object({
|
||||
limit: z.number().optional().default(10).describe('返回的文件数量限制')
|
||||
})
|
||||
|
||||
// 确保变量被"使用"
|
||||
;(function registerSchema() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('依赖分析参数模式已注册:', AnalyzeDependenciesArgsSchema.shape)
|
||||
}
|
||||
})()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
@ -149,6 +300,7 @@ type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
export class WorkspaceFileToolServer {
|
||||
public server: Server
|
||||
private workspacePath: string
|
||||
private codeAnalyzer: CodeAnalyzer
|
||||
|
||||
constructor(workspacePath: string) {
|
||||
if (!workspacePath) {
|
||||
@ -156,6 +308,7 @@ export class WorkspaceFileToolServer {
|
||||
}
|
||||
|
||||
this.workspacePath = normalizePath(path.resolve(workspacePath))
|
||||
this.codeAnalyzer = createCodeAnalyzer({ workspacePath: this.workspacePath })
|
||||
|
||||
// 验证工作区目录存在且可访问
|
||||
this.validateWorkspace().catch((error) => {
|
||||
@ -224,6 +377,61 @@ export class WorkspaceFileToolServer {
|
||||
name: TOOL_EDIT_FILE,
|
||||
description: '编辑工作区中的文件,可以替换指定行范围的内容。适用于对文件进行部分修改。',
|
||||
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_DELETE_PATH,
|
||||
description: '删除工作区中的文件或目录。',
|
||||
inputSchema: zodToJsonSchema(DeletePathArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_SEARCH_FILE_CONTENT,
|
||||
description: '在工作区文件内容中搜索指定的文本模式或正则表达式。可以搜索特定代码片段、函数名、变量名等。',
|
||||
inputSchema: zodToJsonSchema(SearchFileContentArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_SEARCH_SYMBOLS,
|
||||
description: '在工作区中搜索特定类型的代码符号(如函数、类、接口等)。支持按名称和类型进行精确搜索。',
|
||||
inputSchema: zodToJsonSchema(SearchSymbolsArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_FIND_REFERENCES,
|
||||
description: '查找工作区中对特定符号的所有引用。帮助理解代码中符号的使用情况和依赖关系。',
|
||||
inputSchema: zodToJsonSchema(FindReferencesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_GO_TO_DEFINITION,
|
||||
description: '查找符号的定义位置。当看到一个函数调用或变量使用时,可以快速跳转到其定义处。',
|
||||
inputSchema: zodToJsonSchema(GoToDefinitionArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_GET_CODE_CONTEXT,
|
||||
description: '获取指定位置的代码上下文。提供周围的代码行和所在函数/类等结构信息。',
|
||||
inputSchema: zodToJsonSchema(GetCodeContextArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_ANALYZE_DEPENDENCIES,
|
||||
description: '分析工作区中的依赖关系。帮助理解代码中模块和文件之间的依赖关系。',
|
||||
inputSchema: zodToJsonSchema(AnalyzeDependenciesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_GET_FILE_DEPENDENCIES,
|
||||
description: '获取指定文件的依赖文件列表。帮助理解文件的依赖关系。',
|
||||
inputSchema: zodToJsonSchema(GetFileDependenciesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_GET_FILE_DEPENDENTS,
|
||||
description: '获取指定文件的被依赖文件列表。帮助理解文件的被依赖关系。',
|
||||
inputSchema: zodToJsonSchema(GetFileDependentsArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_CHECK_CIRCULAR_DEPENDENCIES,
|
||||
description: '检查工作区中的循环依赖关系。帮助理解代码中可能存在的循环依赖问题。',
|
||||
inputSchema: zodToJsonSchema(CheckCircularDependenciesArgsSchema) as ToolInput
|
||||
},
|
||||
{
|
||||
name: TOOL_GET_MOST_REFERENCED_FILES,
|
||||
description: '获取工作区中最常被引用的文件列表。帮助理解代码中高频使用的文件。',
|
||||
inputSchema: zodToJsonSchema(GetMostReferencedFilesArgsSchema) as ToolInput
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -503,6 +711,737 @@ export class WorkspaceFileToolServer {
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_DELETE_PATH: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到删除路径请求,参数:`, args)
|
||||
|
||||
const parsed = DeletePathArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `删除路径的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
const validPath = await validatePath(this.workspacePath, parsed.data.path)
|
||||
|
||||
try {
|
||||
// 检查路径是否存在
|
||||
const stats = await fs.stat(validPath)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
Logger.info(`[WorkspaceFileTool] 删除目录: ${validPath}`)
|
||||
// 递归删除目录及其内容
|
||||
await fs.rm(validPath, { recursive: true, force: true })
|
||||
return {
|
||||
content: [{ type: 'text', text: `目录已成功删除: ${parsed.data.path}` }]
|
||||
}
|
||||
} else {
|
||||
Logger.info(`[WorkspaceFileTool] 删除文件: ${validPath}`)
|
||||
// 删除文件
|
||||
await fs.unlink(validPath)
|
||||
return {
|
||||
content: [{ type: 'text', text: `文件已成功删除: ${parsed.data.path}` }]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `删除路径时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_SEARCH_FILE_CONTENT: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到文件内容搜索请求,参数:`, args)
|
||||
|
||||
const parsed = SearchFileContentArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `搜索文件内容的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
const { pattern, isRegex, caseSensitive, includePatterns, excludePatterns, maxResults } = parsed.data
|
||||
|
||||
Logger.info(
|
||||
`[WorkspaceFileTool] 解析参数成功: pattern=${pattern}, isRegex=${isRegex}, caseSensitive=${caseSensitive}`
|
||||
)
|
||||
|
||||
// 准备正则表达式
|
||||
let searchRegex: RegExp
|
||||
try {
|
||||
if (isRegex) {
|
||||
searchRegex = new RegExp(pattern, caseSensitive ? 'g' : 'gi')
|
||||
} else {
|
||||
// 转义正则表达式特殊字符
|
||||
const escapedPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
searchRegex = new RegExp(escapedPattern, caseSensitive ? 'g' : 'gi')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `创建搜索正则表达式时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
// 搜索结果存储
|
||||
interface SearchResult {
|
||||
filePath: string
|
||||
relativePath: string
|
||||
lineNumber: number
|
||||
line: string
|
||||
match: string
|
||||
}
|
||||
|
||||
const results: SearchResult[] = []
|
||||
let resultCount = 0
|
||||
let searchedFiles = 0
|
||||
|
||||
// 递归搜索文件内容
|
||||
const searchInFiles = async (dirPath: string): Promise<void> => {
|
||||
if (resultCount >= maxResults) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (resultCount >= maxResults) {
|
||||
break
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name)
|
||||
const relativePath = path.relative(this.workspacePath, fullPath)
|
||||
|
||||
// 检查是否匹配排除模式
|
||||
const shouldExclude = excludePatterns.some((pattern) => {
|
||||
return minimatch(relativePath, pattern, { dot: true })
|
||||
})
|
||||
|
||||
if (shouldExclude) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// 递归搜索子目录
|
||||
await searchInFiles(fullPath)
|
||||
} else if (entry.isFile()) {
|
||||
// 检查是否匹配包含模式
|
||||
const shouldInclude =
|
||||
includePatterns.length === 0 ||
|
||||
includePatterns.some((pattern) => {
|
||||
return minimatch(relativePath, pattern, { dot: true })
|
||||
})
|
||||
|
||||
if (!shouldInclude) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 搜索文件内容
|
||||
try {
|
||||
const content = await fs.readFile(fullPath, 'utf-8')
|
||||
const lines = content.split('\n')
|
||||
searchedFiles++
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// 重置 lastIndex 以便从头开始搜索每一行
|
||||
searchRegex.lastIndex = 0
|
||||
let match
|
||||
|
||||
while ((match = searchRegex.exec(line)) !== null) {
|
||||
results.push({
|
||||
filePath: fullPath,
|
||||
relativePath,
|
||||
lineNumber: i + 1,
|
||||
line,
|
||||
match: match[0]
|
||||
})
|
||||
|
||||
resultCount++
|
||||
|
||||
if (resultCount >= maxResults) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (resultCount >= maxResults) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[WorkspaceFileTool] 读取或搜索文件失败: ${fullPath}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[WorkspaceFileTool] 搜索目录失败: ${dirPath}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await searchInFiles(this.workspacePath)
|
||||
|
||||
// 格式化搜索结果
|
||||
const formattedResults = results.map((result) => {
|
||||
// 截取匹配行,如果太长则添加省略号
|
||||
let displayLine = result.line
|
||||
if (displayLine.length > 150) {
|
||||
const matchIndex = displayLine.indexOf(result.match)
|
||||
const startIndex = Math.max(0, matchIndex - 70)
|
||||
const endIndex = Math.min(displayLine.length, matchIndex + result.match.length + 70)
|
||||
|
||||
displayLine =
|
||||
(startIndex > 0 ? '...' : '') +
|
||||
displayLine.substring(startIndex, endIndex) +
|
||||
(endIndex < displayLine.length ? '...' : '')
|
||||
}
|
||||
|
||||
return `${result.relativePath}:${result.lineNumber}: ${displayLine}`
|
||||
})
|
||||
|
||||
const resultSummary = `在 ${searchedFiles} 个文件中找到 ${resultCount} 个匹配项${
|
||||
resultCount >= maxResults ? ` (达到最大限制 ${maxResults})` : ''
|
||||
}`
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
formattedResults.length > 0
|
||||
? `${resultSummary}\n\n${formattedResults.join('\n')}`
|
||||
: `未找到匹配项。已搜索 ${searchedFiles} 个文件。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `搜索文件内容时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_SEARCH_SYMBOLS: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到搜索符号请求,参数:`, args)
|
||||
|
||||
const parsed = SearchSymbolsArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `搜索符号的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, kind, includePatterns, excludePatterns, caseSensitive, maxResults, includeReferences } =
|
||||
parsed.data
|
||||
|
||||
// 将字符串类型转换为枚举类型
|
||||
let symbolKind: SymbolKind | undefined
|
||||
if (kind) {
|
||||
symbolKind = kind as unknown as SymbolKind
|
||||
}
|
||||
|
||||
// 调用代码分析器搜索符号
|
||||
const result = await this.codeAnalyzer.searchSymbols({
|
||||
symbolName: name,
|
||||
symbolKind,
|
||||
includePatterns,
|
||||
excludePatterns,
|
||||
caseSensitive,
|
||||
maxResults,
|
||||
includeReferences
|
||||
})
|
||||
|
||||
// 格式化结果
|
||||
const symbols = result.symbols.map((symbol) => {
|
||||
const relativePath = path.relative(this.workspacePath, symbol.location.filePath)
|
||||
return `[${symbol.kind}] ${symbol.name} (${relativePath}:${symbol.location.startLine})`
|
||||
})
|
||||
|
||||
const references = result.references.map((ref) => {
|
||||
const relativePath = path.relative(this.workspacePath, ref.location.filePath)
|
||||
return `${ref.isDefinition ? '[定义]' : '[引用]'} ${relativePath}:${ref.location.startLine}: ${ref.context}`
|
||||
})
|
||||
|
||||
const contexts = result.contexts.map((ctx) => {
|
||||
// 获取文件路径
|
||||
const filePath = ctx.symbol?.location.filePath || ''
|
||||
const relativePath = path.relative(this.workspacePath, filePath)
|
||||
const symbolInfo = ctx.symbol ? `[${ctx.symbol.kind}] ${ctx.symbol.name}` : ''
|
||||
|
||||
// 获取上下文行
|
||||
const allLines = [...ctx.beforeLines, ctx.line, ...ctx.afterLines]
|
||||
const startLineNum = ctx.lineNumber - ctx.beforeLines.length
|
||||
|
||||
// 格式化上下文行
|
||||
const contextLines = allLines
|
||||
.map((line, i) => `${startLineNum + i}: ${line}${i === ctx.beforeLines.length ? ' ← 当前位置' : ''}`)
|
||||
.join('\n')
|
||||
|
||||
return `文件: ${relativePath}\n${symbolInfo}\n${contextLines}`
|
||||
})
|
||||
|
||||
// 构建响应
|
||||
let responseText = `找到 ${result.symbols.length} 个符号`
|
||||
if (includeReferences) {
|
||||
responseText += `,${result.references.length} 个引用`
|
||||
}
|
||||
|
||||
if (symbols.length > 0) {
|
||||
responseText += `\n\n符号:\n${symbols.join('\n')}`
|
||||
}
|
||||
|
||||
if (references.length > 0) {
|
||||
responseText += `\n\n引用:\n${references.join('\n')}`
|
||||
}
|
||||
|
||||
if (contexts.length > 0) {
|
||||
responseText += `\n\n上下文:\n${contexts.join('\n\n')}`
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: responseText }]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `搜索符号时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_FIND_REFERENCES: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到查找引用请求,参数:`, args)
|
||||
|
||||
const parsed = FindReferencesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `查找引用的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const { filePath, line, column, includePatterns, excludePatterns, maxResults } = parsed.data
|
||||
|
||||
// 首先获取符号定义
|
||||
const validPath = await validatePath(this.workspacePath, filePath)
|
||||
const definition = await this.codeAnalyzer.goToDefinition(validPath, line, column || 1)
|
||||
|
||||
if (!definition) {
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: `未找到位置 ${filePath}:${line}${column ? `:${column}` : ''} 的符号定义` }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 查找引用
|
||||
const references = await this.codeAnalyzer.findReferences({
|
||||
symbol: definition,
|
||||
includePatterns,
|
||||
excludePatterns,
|
||||
maxResults
|
||||
})
|
||||
|
||||
// 格式化结果
|
||||
const formattedRefs = references.map((ref) => {
|
||||
const relativePath = path.relative(this.workspacePath, ref.location.filePath)
|
||||
return `${ref.isDefinition ? '[定义]' : '[引用]'} ${relativePath}:${ref.location.startLine}: ${ref.context}`
|
||||
})
|
||||
|
||||
// 构建响应
|
||||
let responseText = `符号 "${definition.name}" (${definition.kind}) 的引用,共 ${references.length} 个:\n\n`
|
||||
responseText += formattedRefs.join('\n')
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: responseText }]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `查找引用时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_GO_TO_DEFINITION: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到跳转到定义请求,参数:`, args)
|
||||
|
||||
const parsed = GoToDefinitionArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `跳转到定义的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const { filePath, line, column } = parsed.data
|
||||
|
||||
// 验证路径
|
||||
const validPath = await validatePath(this.workspacePath, filePath)
|
||||
|
||||
// 查找定义
|
||||
const definition = await this.codeAnalyzer.goToDefinition(validPath, line, column)
|
||||
|
||||
if (!definition) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `未找到位置 ${filePath}:${line}:${column} 的符号定义` }]
|
||||
}
|
||||
}
|
||||
|
||||
// 获取定义的上下文
|
||||
const parser = getParserForLanguage(getLanguageFromFilePath(definition.location.filePath))
|
||||
const context = await parser.getCodeContext(
|
||||
definition.location.filePath,
|
||||
definition.location.startLine,
|
||||
5 // 上下文行数
|
||||
)
|
||||
|
||||
// 构建响应
|
||||
const relativePath = path.relative(this.workspacePath, definition.location.filePath)
|
||||
let responseText = `符号 "${definition.name}" (${definition.kind}) 的定义位置:\n`
|
||||
responseText += `文件: ${relativePath}:${definition.location.startLine}`
|
||||
|
||||
if (context) {
|
||||
responseText += `\n\n上下文:\n`
|
||||
// 将所有行连接起来展示
|
||||
const allLines = [...context.beforeLines, context.line, ...context.afterLines]
|
||||
const startLineNum = context.lineNumber - context.beforeLines.length
|
||||
|
||||
responseText += allLines
|
||||
.map((line, i) => {
|
||||
const lineNum = startLineNum + i
|
||||
return `${lineNum}: ${line}${lineNum === definition.location.startLine ? ' ← 定义位置' : ''}`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: responseText }]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `跳转到定义时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_GET_CODE_CONTEXT: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到获取代码上下文请求,参数:`, args)
|
||||
|
||||
const parsed = GetCodeContextArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `获取代码上下文的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const { filePath, line, contextSize } = parsed.data
|
||||
|
||||
// 验证路径
|
||||
const validPath = await validatePath(this.workspacePath, filePath)
|
||||
|
||||
// 获取语言解析器
|
||||
const parser = getParserForLanguage(getLanguageFromFilePath(validPath))
|
||||
|
||||
// 获取上下文
|
||||
const context = await parser.getCodeContext(validPath, line, contextSize)
|
||||
|
||||
if (!context) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `获取位置 ${filePath}:${line} 的代码上下文失败` }]
|
||||
}
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
const relativePath = path.relative(this.workspacePath, validPath)
|
||||
let responseText = `文件 ${relativePath} 行 ${line} 的代码上下文:\n\n`
|
||||
|
||||
// 尝试获取包含该行的符号
|
||||
const symbols = await parseFile(validPath)
|
||||
const symbol = await parser.findSymbolAtLine(symbols, line)
|
||||
|
||||
if (symbol) {
|
||||
responseText += `所在符号: [${symbol.kind}] ${symbol.name}\n\n`
|
||||
}
|
||||
|
||||
// 显示上下文代码
|
||||
if (context) {
|
||||
// 合并所有行并显示
|
||||
const allLines = [...context.beforeLines, context.line, ...context.afterLines]
|
||||
const startLineNum = context.lineNumber - context.beforeLines.length
|
||||
|
||||
responseText += allLines
|
||||
.map((line, i) => {
|
||||
const lineNum = startLineNum + i
|
||||
return `${lineNum}: ${line}${lineNum === context.lineNumber ? ' ← 当前位置' : ''}`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: responseText }]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `获取代码上下文时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[错误] ${errorMsg}`
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_ANALYZE_DEPENDENCIES: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到分析依赖请求,参数:`, args)
|
||||
|
||||
const parsed = AnalyzeDependenciesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `分析依赖的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const dependencyAnalyzer = new DependencyAnalyzer(this.workspacePath)
|
||||
const dependencyGraph = await dependencyAnalyzer.analyzeDependencies(parsed.data)
|
||||
|
||||
const nodeCount = Object.keys(dependencyGraph.nodes).length
|
||||
const externalDependenciesCount = dependencyGraph.externalDependencies.size
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
`依赖分析完成,找到 ${nodeCount} 个内部模块和 ${externalDependenciesCount} 个外部依赖。\n\n` +
|
||||
`使用 ${TOOL_GET_FILE_DEPENDENCIES}、${TOOL_GET_FILE_DEPENDENTS}、` +
|
||||
`${TOOL_CHECK_CIRCULAR_DEPENDENCIES} 和 ${TOOL_GET_MOST_REFERENCED_FILES} 工具查看详细信息。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `分析依赖时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `[错误] ${errorMsg}` }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_GET_FILE_DEPENDENCIES: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到获取文件依赖请求,参数:`, args)
|
||||
|
||||
const parsed = GetFileDependenciesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `获取文件依赖的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const validPath = await validatePath(this.workspacePath, parsed.data.filePath)
|
||||
const dependencyAnalyzer = new DependencyAnalyzer(this.workspacePath)
|
||||
await dependencyAnalyzer.analyzeDependencies()
|
||||
|
||||
const dependencies = await dependencyAnalyzer.getAllDependencies(validPath)
|
||||
const relativeDependencies = dependencies.map((dep) => path.relative(this.workspacePath, dep))
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
dependencies.length > 0
|
||||
? `文件 ${parsed.data.filePath} 有 ${dependencies.length} 个依赖:\n\n${relativeDependencies.join('\n')}`
|
||||
: `文件 ${parsed.data.filePath} 没有依赖。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `获取文件依赖时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `[错误] ${errorMsg}` }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_GET_FILE_DEPENDENTS: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到获取文件被依赖请求,参数:`, args)
|
||||
|
||||
const parsed = GetFileDependentsArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `获取文件被依赖的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const validPath = await validatePath(this.workspacePath, parsed.data.filePath)
|
||||
const dependencyAnalyzer = new DependencyAnalyzer(this.workspacePath)
|
||||
await dependencyAnalyzer.analyzeDependencies()
|
||||
|
||||
const dependents = await dependencyAnalyzer.getAllDependents(validPath)
|
||||
const relativeDependents = dependents.map((dep) => path.relative(this.workspacePath, dep))
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
dependents.length > 0
|
||||
? `文件 ${parsed.data.filePath} 被 ${dependents.length} 个文件依赖:\n\n${relativeDependents.join('\n')}`
|
||||
: `文件 ${parsed.data.filePath} 没有被任何文件依赖。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `获取文件被依赖时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `[错误] ${errorMsg}` }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_CHECK_CIRCULAR_DEPENDENCIES: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到检查循环依赖请求,参数:`, args)
|
||||
|
||||
const parsed = CheckCircularDependenciesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `检查循环依赖的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const dependencyAnalyzer = new DependencyAnalyzer(this.workspacePath)
|
||||
await dependencyAnalyzer.analyzeDependencies()
|
||||
|
||||
const circularDependencies = dependencyAnalyzer.findCircularDependencies()
|
||||
|
||||
if (parsed.data.showRelativePaths) {
|
||||
// 将绝对路径转换为相对路径
|
||||
for (let i = 0; i < circularDependencies.length; i++) {
|
||||
circularDependencies[i] = circularDependencies[i].map((p) => path.relative(this.workspacePath, p))
|
||||
}
|
||||
}
|
||||
|
||||
const formattedCycles = circularDependencies.map(
|
||||
(cycle, index) => `循环依赖 #${index + 1}: ${cycle.join(' -> ')}`
|
||||
)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
circularDependencies.length > 0
|
||||
? `发现 ${circularDependencies.length} 个循环依赖:\n\n${formattedCycles.join('\n\n')}`
|
||||
: `未发现循环依赖。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `检查循环依赖时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `[错误] ${errorMsg}` }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case TOOL_GET_MOST_REFERENCED_FILES: {
|
||||
Logger.info(`[WorkspaceFileTool] 收到获取最常引用文件请求,参数:`, args)
|
||||
|
||||
const parsed = GetMostReferencedFilesArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorMsg = `获取最常引用文件的参数无效: ${parsed.error}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
|
||||
throw new McpError(ErrorCode.InvalidParams, errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const dependencyAnalyzer = new DependencyAnalyzer(this.workspacePath)
|
||||
await dependencyAnalyzer.analyzeDependencies()
|
||||
|
||||
const topFiles = dependencyAnalyzer.getMostReferencedFiles(parsed.data.limit)
|
||||
|
||||
// 将绝对路径转换为相对路径
|
||||
const formattedFiles = topFiles.map((file) => ({
|
||||
relativePath: path.relative(this.workspacePath, file.filePath),
|
||||
count: file.count
|
||||
}))
|
||||
|
||||
const resultText = formattedFiles
|
||||
.map((file, index) => `${index + 1}. ${file.relativePath} - 被 ${file.count} 个文件引用`)
|
||||
.join('\n')
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
topFiles.length > 0
|
||||
? `最常被引用的文件 (前 ${topFiles.length} 个):\n\n${resultText}`
|
||||
: `未找到被引用的文件。`
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = `获取最常引用文件时出错: ${error instanceof Error ? error.message : String(error)}`
|
||||
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `[错误] ${errorMsg}` }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new McpError(ErrorCode.MethodNotFound, `未知工具: ${name}`)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { getFilesDir, getFileType, getTempDir } from '@main/utils/file'
|
||||
import { documentExts, imageExts, MB } from '@shared/config/constant'
|
||||
import { FileType } from '@types'
|
||||
import { FileType, FileTypes } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import {
|
||||
dialog,
|
||||
@ -263,6 +263,28 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public writeBase64Image = async (_: Electron.IpcMainInvokeEvent, bytes: string): Promise<FileType> => {
|
||||
const fileName = `${uuidv4()}.png`
|
||||
const filePath = path.join(this.storageDir, fileName)
|
||||
const data = Buffer.from(bytes, 'base64')
|
||||
try {
|
||||
await fs.promises.writeFile(filePath, data)
|
||||
} catch (error: any) {
|
||||
logger.error(`Error writing file at ${filePath}:`, error)
|
||||
throw new Error(`Error writing file at ${filePath}: ${error.message}`)
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
origin_name: fileName,
|
||||
name: fileName,
|
||||
path: filePath,
|
||||
created_at: new Date().toISOString(),
|
||||
size: data.length,
|
||||
ext: '.png',
|
||||
type: FileTypes.IMAGE,
|
||||
count: 1
|
||||
}
|
||||
}
|
||||
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
|
||||
const filePath = path.join(this.storageDir, id)
|
||||
const data = await fs.promises.readFile(filePath)
|
||||
|
||||
@ -377,7 +377,7 @@ class McpService {
|
||||
const toolKey = `${server.id}-${tool.name}` // Combine server ID and tool name
|
||||
const serverTool: MCPTool = {
|
||||
...tool,
|
||||
id: `f${nanoid()}`, // Keep the random ID for internal use (e.g., React keys)
|
||||
id: `${tool.name}`, // Use tool name as stable ID
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
toolKey: toolKey // Add the generated toolKey
|
||||
@ -415,7 +415,9 @@ class McpService {
|
||||
try {
|
||||
Logger.info('[MCP] Calling:', server.name, name, args)
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args })
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
} catch (error) {
|
||||
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
|
||||
|
||||
303
src/main/services/ModuleFileManager.ts
Normal file
303
src/main/services/ModuleFileManager.ts
Normal file
@ -0,0 +1,303 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import axios from 'axios'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import { createWriteStream } from 'fs'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { pipeline } from 'stream/promises'
|
||||
|
||||
export class ModuleFileManager {
|
||||
private static instance: ModuleFileManager | null = null
|
||||
private modulesDir: string
|
||||
|
||||
private constructor() {
|
||||
this.modulesDir = path.join(app.getPath('userData'), 'npm-modules')
|
||||
this.initModulesDir()
|
||||
this.registerIpcHandlers()
|
||||
}
|
||||
|
||||
public static getInstance(): ModuleFileManager {
|
||||
if (!ModuleFileManager.instance) {
|
||||
ModuleFileManager.instance = new ModuleFileManager()
|
||||
}
|
||||
return ModuleFileManager.instance
|
||||
}
|
||||
|
||||
private initModulesDir(): void {
|
||||
if (!fs.existsSync(this.modulesDir)) {
|
||||
fs.mkdirSync(this.modulesDir, { recursive: true })
|
||||
Logger.info(`Created npm-modules directory at ${this.modulesDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
private registerIpcHandlers(): void {
|
||||
Logger.info('Registering ModuleFileManager IPC handlers')
|
||||
ipcMain.handle(IpcChannel.Module_Download, this.downloadModule.bind(this))
|
||||
ipcMain.handle(IpcChannel.Module_Delete, this.deleteModule.bind(this))
|
||||
ipcMain.handle(IpcChannel.Module_List, this.listModules.bind(this))
|
||||
ipcMain.handle(IpcChannel.Module_Exists, this.moduleExists.bind(this))
|
||||
Logger.info('ModuleFileManager IPC handlers registered successfully')
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载模块到本地
|
||||
*/
|
||||
private async downloadModule(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
packageName: string,
|
||||
version: string
|
||||
): Promise<{ success: boolean; error?: string; modulePath?: string }> {
|
||||
// 确保 version 参数有值
|
||||
const moduleVersion = version || 'latest'
|
||||
Logger.info(`downloadModule called with packageName=${packageName}, version=${moduleVersion}`)
|
||||
try {
|
||||
Logger.info(`Downloading module: ${packageName}@${moduleVersion}`)
|
||||
|
||||
// 创建模块目录
|
||||
const moduleDir = path.join(this.modulesDir, packageName)
|
||||
if (!fs.existsSync(moduleDir)) {
|
||||
fs.mkdirSync(moduleDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 创建版本目录
|
||||
const versionDir = path.join(moduleDir, version)
|
||||
if (!fs.existsSync(versionDir)) {
|
||||
fs.mkdirSync(versionDir, { recursive: true })
|
||||
} else {
|
||||
// 如果目录已存在,先清空
|
||||
fs.emptyDirSync(versionDir)
|
||||
}
|
||||
|
||||
// 下载 package.json
|
||||
const packageJsonUrl = `https://unpkg.com/${packageName}@${version}/package.json`
|
||||
const packageJsonResponse = await axios.get(packageJsonUrl)
|
||||
const packageJson = packageJsonResponse.data
|
||||
|
||||
// 保存 package.json
|
||||
fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify(packageJson, null, 2))
|
||||
|
||||
// 获取入口文件
|
||||
const entryPoint = packageJson.main || 'index.js'
|
||||
|
||||
// 下载入口文件
|
||||
const entryFileUrl = `https://unpkg.com/${packageName}@${version}/${entryPoint}`
|
||||
const entryFilePath = path.join(versionDir, entryPoint)
|
||||
|
||||
// 确保入口文件的目录存在
|
||||
const entryFileDir = path.dirname(entryFilePath)
|
||||
if (!fs.existsSync(entryFileDir)) {
|
||||
fs.mkdirSync(entryFileDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 使用流式下载
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: entryFileUrl,
|
||||
responseType: 'stream'
|
||||
})
|
||||
|
||||
await pipeline(response.data, createWriteStream(entryFilePath))
|
||||
|
||||
// 下载依赖文件(简化版,只下载直接引用的文件)
|
||||
// 实际应用中可能需要更复杂的依赖解析
|
||||
await this.downloadDependencies(packageName, version, versionDir, entryPoint)
|
||||
|
||||
Logger.info(`Module downloaded successfully: ${packageName}@${version}`)
|
||||
return {
|
||||
success: true,
|
||||
modulePath: versionDir
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Logger.error(`Failed to download module ${packageName}@${version}:`, error)
|
||||
return {
|
||||
success: false,
|
||||
error: `下载模块失败: ${errorMessage}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载模块依赖
|
||||
*/
|
||||
private async downloadDependencies(
|
||||
packageName: string,
|
||||
version: string,
|
||||
versionDir: string,
|
||||
entryPoint: string
|
||||
): Promise<void> {
|
||||
Logger.info(`Downloading dependencies for ${packageName}@${version}, entryPoint: ${entryPoint}`)
|
||||
try {
|
||||
// 读取入口文件内容
|
||||
const entryFilePath = path.join(versionDir, entryPoint)
|
||||
const entryFileContent = fs.readFileSync(entryFilePath, 'utf-8')
|
||||
|
||||
// 简单解析 import 和 require 语句(这是一个简化版,实际应用中需要更复杂的解析)
|
||||
const importRegex = /(?:import|require)\s*\(?['"](.+?)['"]\)?/g
|
||||
let match: RegExpExecArray | null
|
||||
const dependencies = new Set<string>()
|
||||
|
||||
while ((match = importRegex.exec(entryFileContent)) !== null) {
|
||||
const dependency = match[1]
|
||||
if (!dependency.startsWith('.') && !dependency.startsWith('/')) {
|
||||
// 跳过外部模块
|
||||
continue
|
||||
}
|
||||
dependencies.add(dependency)
|
||||
}
|
||||
|
||||
// 下载每个依赖
|
||||
for (const dependency of dependencies) {
|
||||
// 解析相对路径
|
||||
const dependencyPath = dependency.startsWith('.') ? path.join(path.dirname(entryPoint), dependency) : dependency
|
||||
|
||||
// 构建 URL
|
||||
let dependencyUrl = `https://unpkg.com/${packageName}@${version}/${dependencyPath}`
|
||||
|
||||
// 如果没有扩展名,尝试添加 .js
|
||||
if (!path.extname(dependencyPath)) {
|
||||
dependencyUrl += '.js'
|
||||
}
|
||||
|
||||
// 构建本地路径
|
||||
let localPath = path.join(versionDir, dependencyPath)
|
||||
if (!path.extname(localPath)) {
|
||||
localPath += '.js'
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
const dependencyDir = path.dirname(localPath)
|
||||
if (!fs.existsSync(dependencyDir)) {
|
||||
fs.mkdirSync(dependencyDir, { recursive: true })
|
||||
}
|
||||
|
||||
try {
|
||||
// 下载依赖文件
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: dependencyUrl,
|
||||
responseType: 'stream'
|
||||
})
|
||||
|
||||
await pipeline(response.data, createWriteStream(localPath))
|
||||
Logger.info(`Downloaded dependency: ${dependencyPath}`)
|
||||
|
||||
// 递归下载子依赖(可选,可能导致过多请求)
|
||||
// await this.downloadDependencies(packageName, version, versionDir, dependencyPath)
|
||||
} catch (error) {
|
||||
Logger.warn(`Failed to download dependency ${dependencyPath}:`, error)
|
||||
// 继续下载其他依赖
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to download dependencies for ${packageName}@${version}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模块
|
||||
*/
|
||||
private async deleteModule(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
packageName: string,
|
||||
version: string | null
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
Logger.info(`deleteModule called with packageName=${packageName}, version=${version}`)
|
||||
try {
|
||||
const moduleDir = path.join(this.modulesDir, packageName)
|
||||
|
||||
if (!fs.existsSync(moduleDir)) {
|
||||
return { success: true } // 模块不存在,视为删除成功
|
||||
}
|
||||
|
||||
if (version) {
|
||||
// 删除特定版本
|
||||
const versionDir = path.join(moduleDir, version)
|
||||
if (fs.existsSync(versionDir)) {
|
||||
fs.removeSync(versionDir)
|
||||
|
||||
// 检查是否还有其他版本
|
||||
const versions = fs.readdirSync(moduleDir)
|
||||
if (versions.length === 0) {
|
||||
// 没有其他版本,删除整个模块目录
|
||||
fs.removeSync(moduleDir)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 删除整个模块
|
||||
fs.removeSync(moduleDir)
|
||||
}
|
||||
|
||||
Logger.info(`Module deleted: ${packageName}${version ? `@${version}` : ''}`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Logger.error(`Failed to delete module ${packageName}:`, error)
|
||||
return { success: false, error: `删除模块失败: ${errorMessage}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出已安装的模块
|
||||
*/
|
||||
private async listModules(): Promise<{ packageName: string; versions: string[] }[]> {
|
||||
try {
|
||||
const modules: { packageName: string; versions: string[] }[] = []
|
||||
|
||||
if (!fs.existsSync(this.modulesDir)) {
|
||||
return modules
|
||||
}
|
||||
|
||||
const packageNames = fs.readdirSync(this.modulesDir)
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const moduleDir = path.join(this.modulesDir, packageName)
|
||||
if (fs.statSync(moduleDir).isDirectory()) {
|
||||
const versions = fs
|
||||
.readdirSync(moduleDir)
|
||||
.filter((version) => fs.statSync(path.join(moduleDir, version)).isDirectory())
|
||||
|
||||
modules.push({
|
||||
packageName,
|
||||
versions
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return modules
|
||||
} catch (error) {
|
||||
Logger.error('Failed to list modules:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模块是否存在
|
||||
*/
|
||||
private async moduleExists(_: Electron.IpcMainInvokeEvent, packageName: string, version: string): Promise<boolean> {
|
||||
// 确保 version 参数有值
|
||||
const moduleVersion = version || 'latest'
|
||||
Logger.info(`moduleExists called with packageName=${packageName}, version=${moduleVersion}`)
|
||||
try {
|
||||
const versionDir = path.join(this.modulesDir, packageName, moduleVersion)
|
||||
const exists = fs.existsSync(versionDir)
|
||||
Logger.info(`Module ${packageName}@${moduleVersion} exists: ${exists}`)
|
||||
return exists
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to check if module exists: ${packageName}@${moduleVersion}`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块路径
|
||||
*/
|
||||
public getModulePath(packageName: string, version?: string): string {
|
||||
const moduleVersion = version || 'latest'
|
||||
return path.join(this.modulesDir, packageName, moduleVersion)
|
||||
}
|
||||
}
|
||||
|
||||
export default ModuleFileManager.getInstance()
|
||||
@ -17,6 +17,31 @@ export class PDFService {
|
||||
return path.join(app.getPath('userData'), 'files')
|
||||
}
|
||||
|
||||
// 引入需要的模块
|
||||
private static docx: any
|
||||
private static pdfParse: any
|
||||
|
||||
// 懒加载模块
|
||||
private static async loadModules() {
|
||||
if (!this.docx) {
|
||||
try {
|
||||
this.docx = require('docx')
|
||||
} catch (error) {
|
||||
logger.error('[PDFService] Error loading docx module:', error)
|
||||
throw new Error('无法加载docx模块,请确保已安装')
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.pdfParse) {
|
||||
try {
|
||||
this.pdfParse = require('pdf-parse')
|
||||
} catch (error) {
|
||||
logger.error('[PDFService] Error loading pdf-parse module:', error)
|
||||
throw new Error('无法加载pdf-parse模块,请确保已安装')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取PDF文件的页数
|
||||
* @param _ Electron IPC事件
|
||||
@ -201,4 +226,198 @@ export class PDFService {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将PDF转换为Word文档
|
||||
* @param _ Electron IPC事件
|
||||
* @param params 包含PDF缓冲区和输出路径的参数
|
||||
* @returns 转换结果
|
||||
*/
|
||||
static async toWord(_: Electron.IpcMainInvokeEvent, params: { pdfBuffer: ArrayBuffer, outputPath?: string }): Promise<{ success: boolean, path?: string, error?: string }> {
|
||||
try {
|
||||
logger.info('[PDFService] Starting PDF to Word conversion')
|
||||
|
||||
// 加载必要的模块
|
||||
await this.loadModules()
|
||||
|
||||
const { pdfBuffer, outputPath } = params
|
||||
|
||||
// 将ArrayBuffer转换为Buffer
|
||||
const buffer = Buffer.from(pdfBuffer)
|
||||
|
||||
// 解析PDF文本
|
||||
logger.info('[PDFService] Parsing PDF text')
|
||||
|
||||
// 使用更高级的选项来提取文本
|
||||
const pdfData = await this.pdfParse(buffer, {
|
||||
// 使用自定义的渲染器来提取文本
|
||||
pagerender: function(pageData) {
|
||||
// 检查页面数据是否有效
|
||||
if (!pageData || !pageData.getTextContent) {
|
||||
logger.warn('[PDFService] Invalid page data or getTextContent method not available');
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
// 记录页面信息
|
||||
logger.info(`[PDFService] Processing page ${pageData.pageIndex + 1}`);
|
||||
|
||||
return pageData.getTextContent({
|
||||
// 启用更多选项以获取更好的文本提取结果
|
||||
normalizeWhitespace: true,
|
||||
disableCombineTextItems: false
|
||||
}).then(function(textContent: { items: Array<{ str?: string, transform?: number[], width?: number }> }) {
|
||||
// 记录文本内容项目数
|
||||
logger.info(`[PDFService] Page ${pageData.pageIndex + 1} has ${textContent.items.length} text items`);
|
||||
|
||||
if (textContent.items.length === 0) {
|
||||
logger.warn(`[PDFService] No text items found on page ${pageData.pageIndex + 1}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
let lastY: number | null = null;
|
||||
let lastX: number | null = null;
|
||||
let text = '';
|
||||
|
||||
// 处理文本内容
|
||||
for (const item of textContent.items) {
|
||||
// 确保 item 有 str 属性且不为空
|
||||
if ('str' in item && item.str && item.str.trim().length > 0 && item.transform && item.transform.length >= 6) {
|
||||
const x = item.transform[4];
|
||||
const y = item.transform[5];
|
||||
|
||||
// 根据Y坐标判断是否是新行
|
||||
if (lastY !== null && Math.abs(lastY - y) > 5) {
|
||||
text += '\n';
|
||||
lastX = null; // 重置X坐标
|
||||
}
|
||||
// 根据X坐标判断是否需要添加空格
|
||||
else if (lastX !== null && text.length > 0 &&
|
||||
text[text.length - 1] !== ' ' &&
|
||||
item.str[0] !== ' ' &&
|
||||
x - lastX > 10) { // 如果X坐标差距较大,添加空格
|
||||
text += ' ';
|
||||
}
|
||||
|
||||
text += item.str;
|
||||
lastY = y;
|
||||
lastX = x + (item.width || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录提取的文本长度
|
||||
logger.info(`[PDFService] Extracted ${text.length} characters from page ${pageData.pageIndex + 1}`);
|
||||
|
||||
return text;
|
||||
}).catch(function(error: Error) {
|
||||
logger.error(`[PDFService] Error extracting text from page ${pageData.pageIndex + 1}:`, error);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取提取的文本
|
||||
const pdfText = pdfData.text;
|
||||
|
||||
// 记录提取的文本长度
|
||||
logger.info(`[PDFService] Extracted text length: ${pdfText.length}`);
|
||||
|
||||
// 如果文本太短,可能是提取失败
|
||||
if (pdfText.length < 10) {
|
||||
logger.warn(`[PDFService] Extracted text is too short: "${pdfText}"`);
|
||||
} else {
|
||||
// 记录提取的文本的前100个字符,用于调试
|
||||
logger.info(`[PDFService] First 100 characters of extracted text: "${pdfText.substring(0, 100).replace(/\n/g, '\\n')}..."`);
|
||||
}
|
||||
|
||||
// 记录PDF的元数据
|
||||
if (pdfData.info) {
|
||||
logger.info(`[PDFService] PDF metadata:`, JSON.stringify(pdfData.info));
|
||||
}
|
||||
|
||||
// 记录PDF的页数
|
||||
logger.info(`[PDFService] PDF page count: ${pdfData.numpages}`);
|
||||
|
||||
// 记录PDF的版本
|
||||
if (pdfData.pdfInfo && pdfData.pdfInfo.version) {
|
||||
logger.info(`[PDFService] PDF version: ${pdfData.pdfInfo.version}`);
|
||||
}
|
||||
|
||||
// 创建Word文档
|
||||
logger.info('[PDFService] Creating Word document')
|
||||
const { Document, Packer, Paragraph, TextRun } = this.docx
|
||||
|
||||
// 将PDF文本分割成段落,使用更智能的分割方法
|
||||
const paragraphs = pdfText
|
||||
.split(/\n{2,}/) // 使用两个或更多换行符分割段落
|
||||
.map((p: string) => p.replace(/\n/g, ' ').trim()) // 将段落内的换行符替换为空格
|
||||
.filter((p: string) => p.length > 0); // 过滤掉空段落
|
||||
|
||||
// 创建Word文档对象
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: paragraphs.map((text: string) =>
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: text.trim(),
|
||||
size: 24 // 12pt
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 生成Word文档
|
||||
logger.info('[PDFService] Generating Word document buffer')
|
||||
const docBuffer = await Packer.toBuffer(doc)
|
||||
|
||||
// 确定输出路径
|
||||
let finalOutputPath = outputPath
|
||||
|
||||
// 如果没有提供输出路径,则使用对话框让用户选择保存位置
|
||||
if (!finalOutputPath) {
|
||||
const { dialog } = require('electron')
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: '保存Word文档',
|
||||
defaultPath: 'converted-document.docx',
|
||||
filters: [{ name: 'Word文档', extensions: ['docx'] }]
|
||||
})
|
||||
|
||||
if (result.canceled) {
|
||||
logger.info('[PDFService] User canceled save dialog')
|
||||
return { success: false, error: '用户取消了保存' }
|
||||
}
|
||||
|
||||
finalOutputPath = result.filePath
|
||||
}
|
||||
|
||||
// 保存Word文档
|
||||
if (!finalOutputPath) {
|
||||
logger.error('[PDFService] Output path is undefined')
|
||||
return {
|
||||
success: false,
|
||||
error: '输出路径未定义'
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[PDFService] Saving Word document to: ${finalOutputPath}`)
|
||||
fs.writeFileSync(finalOutputPath, docBuffer)
|
||||
|
||||
logger.info('[PDFService] PDF to Word conversion completed successfully')
|
||||
return {
|
||||
success: true,
|
||||
path: finalOutputPath
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[PDFService] Error converting PDF to Word:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
src/preload/index.d.ts
vendored
9
src/preload/index.d.ts
vendored
@ -28,7 +28,8 @@ declare global {
|
||||
setTray: (isActive: boolean) => void
|
||||
setTrayOnClose: (isActive: boolean) => void
|
||||
restartTray: () => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
setTheme: (theme: 'light' | 'dark' | 'auto') => void
|
||||
getTheme: () => Promise<'light' | 'dark' | 'auto'>
|
||||
reload: () => void
|
||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||
system: {
|
||||
@ -221,6 +222,12 @@ declare global {
|
||||
}
|
||||
pdf: {
|
||||
splitPDF: (file: FileType, pageRange: string) => Promise<FileType>
|
||||
},
|
||||
moduleManager: {
|
||||
downloadModule: (packageName: string, version?: string) => Promise<{ success: boolean; error?: string; modulePath?: string }>
|
||||
deleteModule: (packageName: string, version?: string) => Promise<{ success: boolean; error?: string }>
|
||||
listModules: () => Promise<{ packageName: string; versions: string[] }[]>
|
||||
moduleExists: (packageName: string, version?: string) => Promise<boolean>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user