style: set eol to lf, code formatting (#7923)
* chore(gitattributes): set eol to lf * chore: git renormalize * style: reformatting * chore: keep eslint prettier plugin consistent on eol
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
2
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
@@ -73,4 +73,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
2
.github/ISSUE_TEMPLATE/3_others.yml
vendored
@@ -73,4 +73,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other information that could help us better understand your question, including screenshots or relevant links
|
||||
description: Any other information that could help us better understand your question, including screenshots or relevant links
|
||||
|
||||
90
.github/issue-checker.yml
vendored
90
.github/issue-checker.yml
vendored
@@ -9,115 +9,115 @@ labels:
|
||||
# skips and removes
|
||||
- name: skip all
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
|
||||
regexes: '[Ss]kip (?:[Aa]ll |)[Ll]abels?'
|
||||
- name: remove all
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
|
||||
regexes: '[Rr]emove (?:[Aa]ll |)[Ll]abels?'
|
||||
|
||||
- name: skip kind/bug
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
|
||||
- name: remove kind/bug
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)'
|
||||
|
||||
- name: skip kind/enhancement
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
|
||||
- name: remove kind/enhancement
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)'
|
||||
|
||||
- name: skip kind/question
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
|
||||
- name: remove kind/question
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)'
|
||||
|
||||
- name: skip area/Connectivity
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
|
||||
- name: remove area/Connectivity
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)'
|
||||
|
||||
- name: skip area/UI/UX
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
|
||||
- name: remove area/UI/UX
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)'
|
||||
|
||||
- name: skip kind/documentation
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
|
||||
- name: remove kind/documentation
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)'
|
||||
|
||||
- name: skip client:linux
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
|
||||
- name: remove client:linux
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)'
|
||||
|
||||
- name: skip client:mac
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
|
||||
- name: remove client:mac
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)'
|
||||
|
||||
- name: skip client:win
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
|
||||
- name: remove client:win
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)'
|
||||
|
||||
- name: skip sig/Assistant
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
|
||||
- name: remove sig/Assistant
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)'
|
||||
|
||||
- name: skip sig/Data
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
|
||||
- name: remove sig/Data
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)'
|
||||
|
||||
- name: skip sig/MCP
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
|
||||
- name: remove sig/MCP
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)'
|
||||
|
||||
- name: skip sig/RAG
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
|
||||
- name: remove sig/RAG
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)'
|
||||
|
||||
- name: skip lgtm
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
|
||||
- name: remove lgtm
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)'
|
||||
|
||||
- name: skip License
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)"
|
||||
regexes: '[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)'
|
||||
- name: remove License
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)"
|
||||
regexes: '[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)'
|
||||
|
||||
# `Dev Team`
|
||||
- name: Dev Team
|
||||
@@ -129,7 +129,7 @@ labels:
|
||||
# Area labels
|
||||
- name: area/Connectivity
|
||||
content: area/Connectivity
|
||||
regexes: "代理|[Pp]roxy"
|
||||
regexes: '代理|[Pp]roxy'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/Connectivity
|
||||
@@ -139,7 +139,7 @@ labels:
|
||||
|
||||
- name: area/UI/UX
|
||||
content: area/UI/UX
|
||||
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]"
|
||||
regexes: '界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/UI/UX
|
||||
@@ -150,7 +150,7 @@ labels:
|
||||
# Kind labels
|
||||
- name: kind/documentation
|
||||
content: kind/documentation
|
||||
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme"
|
||||
regexes: '文档|教程|[Dd]oc(s|umentation)|[Rr]eadme'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip kind/documentation
|
||||
@@ -161,7 +161,7 @@ labels:
|
||||
# Client labels
|
||||
- name: client:linux
|
||||
content: client:linux
|
||||
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)"
|
||||
regexes: '(?:[Ll]inux|[Uu]buntu|[Dd]ebian)'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:linux
|
||||
@@ -171,7 +171,7 @@ labels:
|
||||
|
||||
- name: client:mac
|
||||
content: client:mac
|
||||
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
|
||||
regexes: '(?:[Mm]ac|[Mm]acOS|[Oo]SX)'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:mac
|
||||
@@ -181,7 +181,7 @@ labels:
|
||||
|
||||
- name: client:win
|
||||
content: client:win
|
||||
regexes: "(?:[Ww]in|[Ww]indows)"
|
||||
regexes: '(?:[Ww]in|[Ww]indows)'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:win
|
||||
@@ -192,7 +192,7 @@ labels:
|
||||
# SIG labels
|
||||
- name: sig/Assistant
|
||||
content: sig/Assistant
|
||||
regexes: "快捷助手|[Aa]ssistant"
|
||||
regexes: '快捷助手|[Aa]ssistant'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Assistant
|
||||
@@ -202,7 +202,7 @@ labels:
|
||||
|
||||
- name: sig/Data
|
||||
content: sig/Data
|
||||
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
|
||||
regexes: '[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Data
|
||||
@@ -212,7 +212,7 @@ labels:
|
||||
|
||||
- name: sig/MCP
|
||||
content: sig/MCP
|
||||
regexes: "[Mm][Cc][Pp]"
|
||||
regexes: '[Mm][Cc][Pp]'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/MCP
|
||||
@@ -222,7 +222,7 @@ labels:
|
||||
|
||||
- name: sig/RAG
|
||||
content: sig/RAG
|
||||
regexes: "知识库|[Rr][Aa][Gg]"
|
||||
regexes: '知识库|[Rr][Aa][Gg]'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/RAG
|
||||
@@ -233,7 +233,7 @@ labels:
|
||||
# Other labels
|
||||
- name: lgtm
|
||||
content: lgtm
|
||||
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
|
||||
regexes: '(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip lgtm
|
||||
@@ -243,7 +243,7 @@ labels:
|
||||
|
||||
- name: License
|
||||
content: License
|
||||
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
|
||||
regexes: '(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)'
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip License
|
||||
|
||||
6
.github/workflows/issue-checker.yml
vendored
6
.github/workflows/issue-checker.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Issue Checker"
|
||||
name: 'Issue Checker'
|
||||
|
||||
on:
|
||||
issues:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: MaaAssistantArknights/issue-checker@v1.14
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
configuration-path: .github/issue-checker.yml
|
||||
not-before: 2022-08-05T00:00:00Z
|
||||
include-title: 1
|
||||
include-title: 1
|
||||
|
||||
20
.github/workflows/issue-management.yml
vendored
20
.github/workflows/issue-management.yml
vendored
@@ -1,8 +1,8 @@
|
||||
name: "Stale Issue Management"
|
||||
name: 'Stale Issue Management'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
@@ -24,18 +24,18 @@ jobs:
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-info"
|
||||
only-labels: 'needs-more-info'
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: "inactive"
|
||||
close-issue-label: "closed:no-response"
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: 'inactive'
|
||||
close-issue-label: 'closed:no-response'
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
It will be closed now due to lack of additional information.
|
||||
|
||||
|
||||
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
|
||||
operations-per-run: 50
|
||||
exempt-issue-labels: "pending, Dev Team"
|
||||
exempt-issue-labels: 'pending, Dev Team'
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
||||
@@ -45,11 +45,11 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: ${{ env.daysBeforeClose }}
|
||||
stale-issue-label: "inactive"
|
||||
stale-issue-label: 'inactive'
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
exempt-issue-labels: "pending, Dev Team, kind/enhancement"
|
||||
exempt-issue-labels: 'pending, Dev Team, kind/enhancement'
|
||||
days-before-pr-stale: -1 # Completely disable stalling for PRs
|
||||
days-before-pr-close: -1 # Completely disable closing for PRs
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -117,4 +117,4 @@ jobs:
|
||||
makeLatest: false
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/*.blockmap'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
12788
.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch
vendored
12788
.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch
vendored
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ export default defineConfig([
|
||||
'simple-import-sort/exports': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'@eslint-react/no-prop-types': 'error',
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }]
|
||||
'prettier/prettier': ['error']
|
||||
}
|
||||
},
|
||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ThemeMode } from '@types'
|
||||
import { BrowserWindow, nativeTheme } from 'electron'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
class ThemeService {
|
||||
private theme: ThemeMode = ThemeMode.system
|
||||
constructor() {
|
||||
this.theme = configManager.getTheme()
|
||||
|
||||
if (this.theme === ThemeMode.dark || this.theme === ThemeMode.light || this.theme === ThemeMode.system) {
|
||||
nativeTheme.themeSource = this.theme
|
||||
} else {
|
||||
// 兼容旧版本
|
||||
configManager.setTheme(ThemeMode.system)
|
||||
nativeTheme.themeSource = ThemeMode.system
|
||||
}
|
||||
nativeTheme.on('updated', this.themeUpdatadHandler.bind(this))
|
||||
}
|
||||
|
||||
themeUpdatadHandler() {
|
||||
BrowserWindow.getAllWindows().forEach((win) => {
|
||||
if (win && !win.isDestroyed() && win.setTitleBarOverlay) {
|
||||
try {
|
||||
win.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
} catch (error) {
|
||||
// don't throw error if setTitleBarOverlay failed
|
||||
// Because it may be called with some windows have some title bar
|
||||
}
|
||||
}
|
||||
win.webContents.send(IpcChannel.ThemeUpdated, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
|
||||
})
|
||||
}
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
if (theme === this.theme) {
|
||||
return
|
||||
}
|
||||
|
||||
this.theme = theme
|
||||
nativeTheme.themeSource = theme
|
||||
configManager.setTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
export const themeService = new ThemeService()
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ThemeMode } from '@types'
|
||||
import { BrowserWindow, nativeTheme } from 'electron'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
import { configManager } from './ConfigManager'
|
||||
|
||||
class ThemeService {
|
||||
private theme: ThemeMode = ThemeMode.system
|
||||
constructor() {
|
||||
this.theme = configManager.getTheme()
|
||||
|
||||
if (this.theme === ThemeMode.dark || this.theme === ThemeMode.light || this.theme === ThemeMode.system) {
|
||||
nativeTheme.themeSource = this.theme
|
||||
} else {
|
||||
// 兼容旧版本
|
||||
configManager.setTheme(ThemeMode.system)
|
||||
nativeTheme.themeSource = ThemeMode.system
|
||||
}
|
||||
nativeTheme.on('updated', this.themeUpdatadHandler.bind(this))
|
||||
}
|
||||
|
||||
themeUpdatadHandler() {
|
||||
BrowserWindow.getAllWindows().forEach((win) => {
|
||||
if (win && !win.isDestroyed() && win.setTitleBarOverlay) {
|
||||
try {
|
||||
win.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
} catch (error) {
|
||||
// don't throw error if setTitleBarOverlay failed
|
||||
// Because it may be called with some windows have some title bar
|
||||
}
|
||||
}
|
||||
win.webContents.send(IpcChannel.ThemeUpdated, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
|
||||
})
|
||||
}
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
if (theme === this.theme) {
|
||||
return
|
||||
}
|
||||
|
||||
this.theme = theme
|
||||
nativeTheme.themeSource = theme
|
||||
configManager.setTheme(theme)
|
||||
}
|
||||
}
|
||||
|
||||
export const themeService = new ThemeService()
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
import { configManager } from '../services/ConfigManager'
|
||||
|
||||
export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
|
||||
if (reset) {
|
||||
wins.forEach((win) => {
|
||||
win.webContents.setZoomFactor(1)
|
||||
})
|
||||
configManager.setZoomFactor(1)
|
||||
return
|
||||
}
|
||||
|
||||
if (delta === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentZoom = configManager.getZoomFactor()
|
||||
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||
if (newZoom >= 0.5 && newZoom <= 2.0) {
|
||||
wins.forEach((win) => {
|
||||
win.webContents.setZoomFactor(newZoom)
|
||||
})
|
||||
configManager.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
import { configManager } from '../services/ConfigManager'
|
||||
|
||||
export function handleZoomFactor(wins: BrowserWindow[], delta: number, reset: boolean = false) {
|
||||
if (reset) {
|
||||
wins.forEach((win) => {
|
||||
win.webContents.setZoomFactor(1)
|
||||
})
|
||||
configManager.setZoomFactor(1)
|
||||
return
|
||||
}
|
||||
|
||||
if (delta === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentZoom = configManager.getZoomFactor()
|
||||
const newZoom = Number((currentZoom + delta).toFixed(1))
|
||||
if (newZoom >= 0.5 && newZoom <= 2.0) {
|
||||
wins.forEach((win) => {
|
||||
win.webContents.setZoomFactor(newZoom)
|
||||
})
|
||||
configManager.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' 'unsafe-inline' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' 'unsafe-inline' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#spinner {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#spinner img {
|
||||
width: 100px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
#spinner img {
|
||||
width: 100px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script>
|
||||
console.time('init')
|
||||
</script>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/entryPoint.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script>
|
||||
console.time('init')
|
||||
</script>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/entryPoint.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/mini/entryPoint.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/mini/entryPoint.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio Selection Assistant</title>
|
||||
</head>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/selection/action/entryPoint.tsx"></script>
|
||||
<style>
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,46 +1,43 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio Selection Toolbar</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio Selection Toolbar</title>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/selection/toolbar/entryPoint.tsx"></script>
|
||||
<style>
|
||||
html {
|
||||
margin: 0 !important;
|
||||
background-color: transparent !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
</head>
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/windows/selection/toolbar/entryPoint.tsx"></script>
|
||||
<style>
|
||||
html {
|
||||
margin: 0 !important;
|
||||
background-color: transparent !important;
|
||||
background-image: none !important;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: max-content !important;
|
||||
height: fit-content !important;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
#root {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: max-content !important;
|
||||
height: fit-content !important;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@font-face {
|
||||
font-family: 'Twemoji Country Flags';
|
||||
unicode-range:
|
||||
U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
|
||||
/*https://github.com/beyondkmp/country-flag-emoji-polyfill/blob/master/font/TwemojiCountryFlags.woff2 */
|
||||
src: url('TwemojiCountryFlags.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 国旗字体样式类 */
|
||||
.country-flag-font {
|
||||
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Twemoji Country Flags';
|
||||
unicode-range:
|
||||
U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F;
|
||||
/*https://github.com/beyondkmp/country-flag-emoji-polyfill/blob/master/font/TwemojiCountryFlags.woff2 */
|
||||
src: url('TwemojiCountryFlags.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 国旗字体样式类 */
|
||||
.country-flag-font {
|
||||
font-family: 'Twemoji Country Flags', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
:root {
|
||||
--font-family:
|
||||
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
|
||||
--font-family-serif:
|
||||
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
// Windows系统专用字体配置
|
||||
body[os='windows'] {
|
||||
--font-family:
|
||||
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
|
||||
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
:root {
|
||||
--font-family:
|
||||
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
|
||||
--font-family-serif:
|
||||
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
// Windows系统专用字体配置
|
||||
body[os='windows'] {
|
||||
--font-family:
|
||||
'Twemoji Country Flags', Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen,
|
||||
Cantarell, 'Open Sans', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,178 +1,178 @@
|
||||
import type { MCPServer } from '@renderer/types'
|
||||
import i18next from 'i18next'
|
||||
|
||||
// Token storage constants and utilities
|
||||
const TOKEN_STORAGE_KEY = 'tokenLanyunToken'
|
||||
export const TOKENLANYUN_HOST = 'https://mcp.lanyun.net'
|
||||
export const LANYUN_MCP_HOST = TOKENLANYUN_HOST + '/mcp/manager/selectListByApiKey'
|
||||
export const LANYUN_KEY_HOST = TOKENLANYUN_HOST + '/#/manage/apiKey'
|
||||
|
||||
export const saveTokenLanYunToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
export const getTokenLanYunToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const clearTokenLanYunToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const hasTokenLanYunToken = (): boolean => {
|
||||
return !!getTokenLanYunToken()
|
||||
}
|
||||
|
||||
interface TokenLanYunServer {
|
||||
id: string
|
||||
/**
|
||||
* locales 字段用于存储多语言信息。
|
||||
* 其中 key(lang)为语言代码(如 'zh', 'en'),
|
||||
* value 为该语言下的 name 和 description。
|
||||
* 例如:
|
||||
* {
|
||||
* "zh": { name: "文档处理工具", description: "..." },
|
||||
* "en": { name: "Document Processor", description: "..." }
|
||||
* }
|
||||
*/
|
||||
locales?: {
|
||||
[lang: string]: {
|
||||
description?: string
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
chineseName?: string
|
||||
description?: string
|
||||
operationalUrls?: { url: string }[]
|
||||
tags?: string[]
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
interface TokenLanYunSyncResult {
|
||||
success: boolean
|
||||
message: string
|
||||
addedServers: MCPServer[]
|
||||
errorDetails?: string
|
||||
}
|
||||
|
||||
// Function to fetch and process TokenLanYun servers
|
||||
export const syncTokenLanYunServers = async (
|
||||
token: string,
|
||||
existingServers: MCPServer[]
|
||||
): Promise<TokenLanYunSyncResult> => {
|
||||
const t = i18next.t
|
||||
|
||||
try {
|
||||
const response = await fetch(LANYUN_MCP_HOST, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
// Handle authentication errors
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearTokenLanYunToken()
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Handle server errors
|
||||
if (response.status === 500 || !response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
|
||||
// Process successful response
|
||||
const data = await response.json()
|
||||
if (data.code === 401) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
if (data.code === 500) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
|
||||
const servers: TokenLanYunServer[] = data.data || []
|
||||
|
||||
if (servers.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Transform Token servers to MCP servers format
|
||||
const addedServers: MCPServer[] = []
|
||||
console.log('TokenLanYun servers:', servers)
|
||||
for (const server of servers) {
|
||||
try {
|
||||
if (!server.operationalUrls?.[0]?.url) continue
|
||||
|
||||
// If any existing server id contains '@lanyun', clear them before adding new ones
|
||||
// if (existingServers.some((s) => s.id.startsWith('@lanyun'))) {
|
||||
// for (let i = existingServers.length - 1; i >= 0; i--) {
|
||||
// if (existingServers[i].id.startsWith('@lanyun')) {
|
||||
// existingServers.splice(i, 1)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Skip if server already exists after clearing
|
||||
if (existingServers.some((s) => s.id === `@lanyun/${server.id}`)) continue
|
||||
|
||||
const mcpServer: MCPServer = {
|
||||
id: `@lanyun/${server.id}`,
|
||||
name:
|
||||
server.chineseName || server.locales?.zh?.name || server.locales?.en?.name || `LanYun Server ${server.id}`,
|
||||
description: server.description || '',
|
||||
type: 'sse',
|
||||
baseUrl: server.operationalUrls[0].url,
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
isActive: true,
|
||||
provider: '蓝耘科技',
|
||||
providerUrl: server.operationalUrls[0].url,
|
||||
logoUrl: server.logoUrl || '',
|
||||
tags: server.tags ?? (server.chineseName ? [server.chineseName] : [])
|
||||
}
|
||||
|
||||
addedServers.push(mcpServer)
|
||||
} catch (err) {
|
||||
console.error('Error processing LanYun server:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.success', { count: addedServers.length }),
|
||||
addedServers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('TokenLanyun sync error:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
import type { MCPServer } from '@renderer/types'
|
||||
import i18next from 'i18next'
|
||||
|
||||
// Token storage constants and utilities
|
||||
const TOKEN_STORAGE_KEY = 'tokenLanyunToken'
|
||||
export const TOKENLANYUN_HOST = 'https://mcp.lanyun.net'
|
||||
export const LANYUN_MCP_HOST = TOKENLANYUN_HOST + '/mcp/manager/selectListByApiKey'
|
||||
export const LANYUN_KEY_HOST = TOKENLANYUN_HOST + '/#/manage/apiKey'
|
||||
|
||||
export const saveTokenLanYunToken = (token: string): void => {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
export const getTokenLanYunToken = (): string | null => {
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const clearTokenLanYunToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export const hasTokenLanYunToken = (): boolean => {
|
||||
return !!getTokenLanYunToken()
|
||||
}
|
||||
|
||||
interface TokenLanYunServer {
|
||||
id: string
|
||||
/**
|
||||
* locales 字段用于存储多语言信息。
|
||||
* 其中 key(lang)为语言代码(如 'zh', 'en'),
|
||||
* value 为该语言下的 name 和 description。
|
||||
* 例如:
|
||||
* {
|
||||
* "zh": { name: "文档处理工具", description: "..." },
|
||||
* "en": { name: "Document Processor", description: "..." }
|
||||
* }
|
||||
*/
|
||||
locales?: {
|
||||
[lang: string]: {
|
||||
description?: string
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
chineseName?: string
|
||||
description?: string
|
||||
operationalUrls?: { url: string }[]
|
||||
tags?: string[]
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
interface TokenLanYunSyncResult {
|
||||
success: boolean
|
||||
message: string
|
||||
addedServers: MCPServer[]
|
||||
errorDetails?: string
|
||||
}
|
||||
|
||||
// Function to fetch and process TokenLanYun servers
|
||||
export const syncTokenLanYunServers = async (
|
||||
token: string,
|
||||
existingServers: MCPServer[]
|
||||
): Promise<TokenLanYunSyncResult> => {
|
||||
const t = i18next.t
|
||||
|
||||
try {
|
||||
const response = await fetch(LANYUN_MCP_HOST, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
// Handle authentication errors
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
clearTokenLanYunToken()
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Handle server errors
|
||||
if (response.status === 500 || !response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
|
||||
// Process successful response
|
||||
const data = await response.json()
|
||||
if (data.code === 401) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
if (data.code === 500) {
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: `Status: ${response.status}`
|
||||
}
|
||||
}
|
||||
|
||||
const servers: TokenLanYunServer[] = data.data || []
|
||||
|
||||
if (servers.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
|
||||
addedServers: []
|
||||
}
|
||||
}
|
||||
|
||||
// Transform Token servers to MCP servers format
|
||||
const addedServers: MCPServer[] = []
|
||||
console.log('TokenLanYun servers:', servers)
|
||||
for (const server of servers) {
|
||||
try {
|
||||
if (!server.operationalUrls?.[0]?.url) continue
|
||||
|
||||
// If any existing server id contains '@lanyun', clear them before adding new ones
|
||||
// if (existingServers.some((s) => s.id.startsWith('@lanyun'))) {
|
||||
// for (let i = existingServers.length - 1; i >= 0; i--) {
|
||||
// if (existingServers[i].id.startsWith('@lanyun')) {
|
||||
// existingServers.splice(i, 1)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Skip if server already exists after clearing
|
||||
if (existingServers.some((s) => s.id === `@lanyun/${server.id}`)) continue
|
||||
|
||||
const mcpServer: MCPServer = {
|
||||
id: `@lanyun/${server.id}`,
|
||||
name:
|
||||
server.chineseName || server.locales?.zh?.name || server.locales?.en?.name || `LanYun Server ${server.id}`,
|
||||
description: server.description || '',
|
||||
type: 'sse',
|
||||
baseUrl: server.operationalUrls[0].url,
|
||||
command: '',
|
||||
args: [],
|
||||
env: {},
|
||||
isActive: true,
|
||||
provider: '蓝耘科技',
|
||||
providerUrl: server.operationalUrls[0].url,
|
||||
logoUrl: server.logoUrl || '',
|
||||
tags: server.tags ?? (server.chineseName ? [server.chineseName] : [])
|
||||
}
|
||||
|
||||
addedServers.push(mcpServer)
|
||||
} catch (err) {
|
||||
console.error('Error processing LanYun server:', err)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('settings.mcp.sync.success', { count: addedServers.length }),
|
||||
addedServers
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('TokenLanyun sync error:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: t('settings.mcp.sync.error'),
|
||||
addedServers: [],
|
||||
errorDetails: String(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,216 +1,216 @@
|
||||
import Logger from '@renderer/config/logger'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { getFileExtension } from '@renderer/utils'
|
||||
|
||||
// Track last focused component
|
||||
type ComponentType = 'inputbar' | 'messageEditor' | null
|
||||
let lastFocusedComponent: ComponentType = 'inputbar' // Default to inputbar
|
||||
|
||||
// 处理函数类型
|
||||
type PasteHandler = (event: ClipboardEvent) => Promise<boolean>
|
||||
|
||||
// 处理函数存储
|
||||
const handlers: {
|
||||
inputbar?: PasteHandler
|
||||
messageEditor?: PasteHandler
|
||||
} = {}
|
||||
|
||||
// 初始化标志
|
||||
let isInitialized = false
|
||||
|
||||
/**
|
||||
* 处理粘贴事件的通用服务
|
||||
* 处理各种粘贴场景,包括文本和文件
|
||||
*/
|
||||
export const handlePaste = async (
|
||||
event: ClipboardEvent,
|
||||
isVisionModel: boolean,
|
||||
isGenerateImageModel: boolean,
|
||||
supportExts: string[],
|
||||
setFiles: (updater: (prevFiles: FileMetadata[]) => FileMetadata[]) => void,
|
||||
setText?: (text: string) => void,
|
||||
pasteLongTextAsFile?: boolean,
|
||||
pasteLongTextThreshold?: number,
|
||||
text?: string,
|
||||
resizeTextArea?: () => void,
|
||||
t?: (key: string) => string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
// 优先处理文本粘贴
|
||||
const clipboardText = event.clipboardData?.getData('text')
|
||||
if (clipboardText) {
|
||||
// 1. 文本粘贴
|
||||
if (pasteLongTextAsFile && pasteLongTextThreshold && clipboardText.length > pasteLongTextThreshold) {
|
||||
// 长文本直接转文件,阻止默认粘贴
|
||||
event.preventDefault()
|
||||
|
||||
const tempFilePath = await window.api.file.createTempFile('pasted_text.txt')
|
||||
await window.api.file.write(tempFilePath, clipboardText)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
if (setText && text) setText(text) // 保持输入框内容不变
|
||||
if (resizeTextArea) setTimeout(() => resizeTextArea(), 50)
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 短文本走默认粘贴行为,直接返回
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 文件/图片粘贴(仅在无文本时处理)
|
||||
if (event.clipboardData?.files && event.clipboardData.files.length > 0) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
for (const file of event.clipboardData.files) {
|
||||
// 使用新的API获取文件路径
|
||||
const filePath = window.api.file.getPathForFile(file)
|
||||
|
||||
// 如果没有路径,可能是剪贴板中的图像数据
|
||||
if (!filePath) {
|
||||
// 图像生成也支持图像编辑
|
||||
if (file.type.startsWith('image/') && (isVisionModel || isGenerateImageModel)) {
|
||||
const tempFilePath = await window.api.file.createTempFile(file.name)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
await window.api.file.write(tempFilePath, uint8Array)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if (t) {
|
||||
window.message.info({
|
||||
key: 'file_not_supported',
|
||||
content: t('chat.input.file_not_supported')
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 有路径的情况
|
||||
if (supportExts.includes(getFileExtension(filePath))) {
|
||||
const selectedFile = await window.api.file.get(filePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
} else {
|
||||
if (t) {
|
||||
window.message.info({
|
||||
key: 'file_not_supported',
|
||||
content: t('chat.input.file_not_supported')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[PasteService] onPaste:', error)
|
||||
if (t) {
|
||||
window.message.error(t('chat.input.file_error'))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 其他情况默认粘贴
|
||||
return false
|
||||
} catch (error) {
|
||||
Logger.error('[PasteService] handlePaste error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后聚焦的组件
|
||||
*/
|
||||
export const setLastFocusedComponent = (component: ComponentType) => {
|
||||
lastFocusedComponent = component
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后聚焦的组件
|
||||
*/
|
||||
export const getLastFocusedComponent = (): ComponentType => {
|
||||
return lastFocusedComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局粘贴事件监听
|
||||
* 应用启动时只调用一次
|
||||
*/
|
||||
export const init = () => {
|
||||
if (isInitialized) return
|
||||
|
||||
// 添加全局粘贴事件监听
|
||||
document.addEventListener('paste', async (event) => {
|
||||
await handleGlobalPaste(event)
|
||||
})
|
||||
|
||||
isInitialized = true
|
||||
Logger.info('[PasteService] Global paste handler initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件的粘贴处理函数
|
||||
*/
|
||||
export const registerHandler = (component: ComponentType, handler: PasteHandler) => {
|
||||
if (!component) return
|
||||
|
||||
// Only log and update if the handler actually changes
|
||||
if (!handlers[component] || handlers[component] !== handler) {
|
||||
handlers[component] = handler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除组件的粘贴处理函数
|
||||
*/
|
||||
export const unregisterHandler = (component: ComponentType) => {
|
||||
if (!component || !handlers[component]) return
|
||||
|
||||
delete handlers[component]
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局粘贴处理函数,根据最后聚焦的组件路由粘贴事件
|
||||
*/
|
||||
const handleGlobalPaste = async (event: ClipboardEvent): Promise<boolean> => {
|
||||
// 如果当前有活动元素且是输入区域,不执行全局处理
|
||||
const activeElement = document.activeElement
|
||||
if (
|
||||
activeElement &&
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.getAttribute('contenteditable') === 'true')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据最后聚焦的组件调用相应处理程序
|
||||
if (lastFocusedComponent && handlers[lastFocusedComponent]) {
|
||||
const handler = handlers[lastFocusedComponent]
|
||||
if (handler) {
|
||||
return await handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的处理程序,默认使用inputbar处理
|
||||
if (handlers.inputbar) {
|
||||
const handler = handlers.inputbar
|
||||
if (handler) {
|
||||
return await handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default {
|
||||
handlePaste,
|
||||
setLastFocusedComponent,
|
||||
getLastFocusedComponent,
|
||||
init,
|
||||
registerHandler,
|
||||
unregisterHandler
|
||||
}
|
||||
import Logger from '@renderer/config/logger'
|
||||
import { FileMetadata } from '@renderer/types'
|
||||
import { getFileExtension } from '@renderer/utils'
|
||||
|
||||
// Track last focused component
|
||||
type ComponentType = 'inputbar' | 'messageEditor' | null
|
||||
let lastFocusedComponent: ComponentType = 'inputbar' // Default to inputbar
|
||||
|
||||
// 处理函数类型
|
||||
type PasteHandler = (event: ClipboardEvent) => Promise<boolean>
|
||||
|
||||
// 处理函数存储
|
||||
const handlers: {
|
||||
inputbar?: PasteHandler
|
||||
messageEditor?: PasteHandler
|
||||
} = {}
|
||||
|
||||
// 初始化标志
|
||||
let isInitialized = false
|
||||
|
||||
/**
|
||||
* 处理粘贴事件的通用服务
|
||||
* 处理各种粘贴场景,包括文本和文件
|
||||
*/
|
||||
export const handlePaste = async (
|
||||
event: ClipboardEvent,
|
||||
isVisionModel: boolean,
|
||||
isGenerateImageModel: boolean,
|
||||
supportExts: string[],
|
||||
setFiles: (updater: (prevFiles: FileMetadata[]) => FileMetadata[]) => void,
|
||||
setText?: (text: string) => void,
|
||||
pasteLongTextAsFile?: boolean,
|
||||
pasteLongTextThreshold?: number,
|
||||
text?: string,
|
||||
resizeTextArea?: () => void,
|
||||
t?: (key: string) => string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
// 优先处理文本粘贴
|
||||
const clipboardText = event.clipboardData?.getData('text')
|
||||
if (clipboardText) {
|
||||
// 1. 文本粘贴
|
||||
if (pasteLongTextAsFile && pasteLongTextThreshold && clipboardText.length > pasteLongTextThreshold) {
|
||||
// 长文本直接转文件,阻止默认粘贴
|
||||
event.preventDefault()
|
||||
|
||||
const tempFilePath = await window.api.file.createTempFile('pasted_text.txt')
|
||||
await window.api.file.write(tempFilePath, clipboardText)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
if (setText && text) setText(text) // 保持输入框内容不变
|
||||
if (resizeTextArea) setTimeout(() => resizeTextArea(), 50)
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 短文本走默认粘贴行为,直接返回
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. 文件/图片粘贴(仅在无文本时处理)
|
||||
if (event.clipboardData?.files && event.clipboardData.files.length > 0) {
|
||||
event.preventDefault()
|
||||
try {
|
||||
for (const file of event.clipboardData.files) {
|
||||
// 使用新的API获取文件路径
|
||||
const filePath = window.api.file.getPathForFile(file)
|
||||
|
||||
// 如果没有路径,可能是剪贴板中的图像数据
|
||||
if (!filePath) {
|
||||
// 图像生成也支持图像编辑
|
||||
if (file.type.startsWith('image/') && (isVisionModel || isGenerateImageModel)) {
|
||||
const tempFilePath = await window.api.file.createTempFile(file.name)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
await window.api.file.write(tempFilePath, uint8Array)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if (t) {
|
||||
window.message.info({
|
||||
key: 'file_not_supported',
|
||||
content: t('chat.input.file_not_supported')
|
||||
})
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 有路径的情况
|
||||
if (supportExts.includes(getFileExtension(filePath))) {
|
||||
const selectedFile = await window.api.file.get(filePath)
|
||||
if (selectedFile) {
|
||||
setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
} else {
|
||||
if (t) {
|
||||
window.message.info({
|
||||
key: 'file_not_supported',
|
||||
content: t('chat.input.file_not_supported')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[PasteService] onPaste:', error)
|
||||
if (t) {
|
||||
window.message.error(t('chat.input.file_error'))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
// 其他情况默认粘贴
|
||||
return false
|
||||
} catch (error) {
|
||||
Logger.error('[PasteService] handlePaste error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最后聚焦的组件
|
||||
*/
|
||||
export const setLastFocusedComponent = (component: ComponentType) => {
|
||||
lastFocusedComponent = component
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后聚焦的组件
|
||||
*/
|
||||
export const getLastFocusedComponent = (): ComponentType => {
|
||||
return lastFocusedComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局粘贴事件监听
|
||||
* 应用启动时只调用一次
|
||||
*/
|
||||
export const init = () => {
|
||||
if (isInitialized) return
|
||||
|
||||
// 添加全局粘贴事件监听
|
||||
document.addEventListener('paste', async (event) => {
|
||||
await handleGlobalPaste(event)
|
||||
})
|
||||
|
||||
isInitialized = true
|
||||
Logger.info('[PasteService] Global paste handler initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件的粘贴处理函数
|
||||
*/
|
||||
export const registerHandler = (component: ComponentType, handler: PasteHandler) => {
|
||||
if (!component) return
|
||||
|
||||
// Only log and update if the handler actually changes
|
||||
if (!handlers[component] || handlers[component] !== handler) {
|
||||
handlers[component] = handler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除组件的粘贴处理函数
|
||||
*/
|
||||
export const unregisterHandler = (component: ComponentType) => {
|
||||
if (!component || !handlers[component]) return
|
||||
|
||||
delete handlers[component]
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局粘贴处理函数,根据最后聚焦的组件路由粘贴事件
|
||||
*/
|
||||
const handleGlobalPaste = async (event: ClipboardEvent): Promise<boolean> => {
|
||||
// 如果当前有活动元素且是输入区域,不执行全局处理
|
||||
const activeElement = document.activeElement
|
||||
if (
|
||||
activeElement &&
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.getAttribute('contenteditable') === 'true')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据最后聚焦的组件调用相应处理程序
|
||||
if (lastFocusedComponent && handlers[lastFocusedComponent]) {
|
||||
const handler = handlers[lastFocusedComponent]
|
||||
if (handler) {
|
||||
return await handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的处理程序,默认使用inputbar处理
|
||||
if (handlers.inputbar) {
|
||||
const handler = handlers.inputbar
|
||||
if (handler) {
|
||||
return await handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default {
|
||||
handlePaste,
|
||||
setLastFocusedComponent,
|
||||
getLastFocusedComponent,
|
||||
init,
|
||||
registerHandler,
|
||||
unregisterHandler
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user