Compare commits
2 Commits
ci/gd-pr-t
...
feat/image
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ee8a68ce | ||
|
|
d521a88d30 |
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -1,5 +1,4 @@
|
||||
/src/renderer/src/store/ @0xfullex
|
||||
/src/renderer/src/databases/ @0xfullex
|
||||
/src/main/services/ConfigManager.ts @0xfullex
|
||||
/packages/shared/IpcChannel.ts @0xfullex
|
||||
/src/main/ipc.ts @0xfullex
|
||||
/src/main/ipc.ts @0xfullex
|
||||
160
.github/pr-modules.yml
vendored
160
.github/pr-modules.yml
vendored
@@ -1,160 +0,0 @@
|
||||
# 模块 → 路径匹配(globs)与 GitHub 审核人列表
|
||||
# 多模块命中时取优先级最高的为主类,其余在卡片中显示“涉及模块”
|
||||
|
||||
categories:
|
||||
ai_core:
|
||||
name: "AI Core"
|
||||
globs:
|
||||
- "packages/aiCore/**"
|
||||
- "src/renderer/src/aiCore/**"
|
||||
github_reviewers: ["DeJeune", "MyPrototypeWhat", "Vaayne"]
|
||||
|
||||
agent:
|
||||
name: "Agent"
|
||||
globs:
|
||||
- "packages/shared/agents/**"
|
||||
- "resources/data/agents-*.json"
|
||||
- "src/renderer/src/api/agent.ts"
|
||||
- "src/renderer/src/types/agent.ts"
|
||||
- "src/renderer/src/utils/agentSession.ts"
|
||||
- "src/renderer/src/services/db/AgentMessageDataSource.ts"
|
||||
- "src/renderer/src/hooks/agents/**"
|
||||
- "src/renderer/src/components/Popups/agent/**"
|
||||
- "src/renderer/src/pages/home/**/Agent*.tsx"
|
||||
- "src/renderer/src/pages/settings/AgentSettings/**"
|
||||
- "src/main/services/agents/**"
|
||||
- "src/main/apiServer/routes/agents/**"
|
||||
github_reviewers: ["EurFelux", "Vaayne", "DeJeune"]
|
||||
|
||||
provider:
|
||||
name: "Provider"
|
||||
globs:
|
||||
- "src/renderer/src/config/providers.ts"
|
||||
- "src/renderer/src/config/preprocessProviders.ts"
|
||||
- "src/renderer/src/config/webSearchProviders.ts"
|
||||
- "src/renderer/src/hooks/useWebSearchProviders.ts"
|
||||
- "src/renderer/src/providers/**"
|
||||
- "src/renderer/src/pages/settings/ProviderSettings/**"
|
||||
- "src/renderer/src/pages/settings/WebSearchSettings/**"
|
||||
- "src/renderer/src/pages/settings/DocProcessSettings/PreprocessProviderSettings.tsx"
|
||||
- "src/renderer/src/pages/settings/MCPSettings/providers/**"
|
||||
- "src/renderer/src/assets/images/providers/**"
|
||||
github_reviewers: ["YinsenHo", "kangfenmao", "alephpiece"]
|
||||
|
||||
backend:
|
||||
name: "后端/平台"
|
||||
globs:
|
||||
- "src/main/apiServer/**"
|
||||
- "src/main/services/**"
|
||||
- "src/main/*.ts"
|
||||
- "src/preload/**"
|
||||
- "src/main/mcpServers/**"
|
||||
github_reviewers: ["beyondkmp", "Vaayne", "kangfenmao"]
|
||||
|
||||
knowledge:
|
||||
name: "知识库"
|
||||
globs:
|
||||
- "src/main/knowledge/**"
|
||||
- "src/renderer/src/pages/knowledge/**"
|
||||
- "src/renderer/src/store/knowledge.ts"
|
||||
- "src/renderer/src/queue/KnowledgeQueue.ts"
|
||||
github_reviewers: ["eeee0717", "alephpiece", "GeorgeDong32"]
|
||||
|
||||
data_storage:
|
||||
name: "数据与存储"
|
||||
globs:
|
||||
- "src/renderer/src/databases/**"
|
||||
- "src/renderer/src/services/db/**"
|
||||
- "src/main/services/agents/database/**"
|
||||
- "resources/database/drizzle/**"
|
||||
- "src/renderer/src/store/migrate.ts"
|
||||
- "src/renderer/src/databases/upgrades.ts"
|
||||
github_reviewers: ["0xfullex", "kangfenmao", "Vaayne", "DeJeune"]
|
||||
|
||||
backup_export:
|
||||
name: "备份/导出"
|
||||
globs:
|
||||
- "src/renderer/src/components/*Backup*"
|
||||
- "src/renderer/src/components/Webdav*"
|
||||
- "src/renderer/src/components/ObsidianExportDialog.tsx"
|
||||
- "src/renderer/src/components/S3*"
|
||||
- "src/renderer/src/store/backup.ts"
|
||||
- "src/renderer/src/store/nutstore.ts"
|
||||
- "src/renderer/src/pages/settings/DataSettings/**"
|
||||
github_reviewers: ["beyondkmp", "GeorgeDong32"]
|
||||
|
||||
minapps:
|
||||
name: "小程序"
|
||||
globs:
|
||||
- "src/renderer/src/pages/minapps/**"
|
||||
- "src/renderer/src/store/minapps.ts"
|
||||
- "src/renderer/src/config/minapps.ts"
|
||||
github_reviewers: ["GeorgeDong32", "beyondkmp"]
|
||||
|
||||
chat:
|
||||
name: "对话"
|
||||
globs:
|
||||
- "src/renderer/src/pages/home/**"
|
||||
- "src/renderer/src/store/newMessage.ts"
|
||||
- "src/renderer/src/store/messageBlock.ts"
|
||||
- "src/renderer/src/store/memory.ts"
|
||||
- "src/renderer/src/store/llm.ts"
|
||||
github_reviewers: ["kangfenmao", "alephpiece", "EurFelux"]
|
||||
|
||||
draw:
|
||||
name: "绘图"
|
||||
globs:
|
||||
- "src/renderer/src/pages/paintings/**"
|
||||
- "src/renderer/src/store/paintings.ts"
|
||||
github_reviewers: ["EurFelux", "DeJeune"]
|
||||
|
||||
uiux:
|
||||
name: "UI/UX"
|
||||
globs:
|
||||
- "src/renderer/src/components/**"
|
||||
- "src/renderer/src/ui/**"
|
||||
- "src/renderer/src/assets/styles/**"
|
||||
- "src/renderer/src/windows/**"
|
||||
github_reviewers: ["kangfenmao", "MyPrototypeWhat", "alephpiece"]
|
||||
|
||||
build-config:
|
||||
name: "构建/配置"
|
||||
globs:
|
||||
- "package.json"
|
||||
- "tsconfig*.json"
|
||||
- "electron-builder.yml"
|
||||
- "electron.vite.config.ts"
|
||||
- "vitest.config.ts"
|
||||
- "playwright.config.ts"
|
||||
- ".github/workflows/**"
|
||||
- "scripts/**"
|
||||
github_reviewers: ["kangfenmao", "beyondkmp", "alephpiece"]
|
||||
|
||||
test:
|
||||
name: "测试"
|
||||
globs:
|
||||
- "tests/**"
|
||||
- "src/**/__tests__/**"
|
||||
- "scripts/__tests__/**"
|
||||
github_reviewers: ["alephpiece", "DeJeune", "EurFelux"]
|
||||
|
||||
docs:
|
||||
name: "文档"
|
||||
globs:
|
||||
- "docs/**"
|
||||
- "README*.md"
|
||||
- "SECURITY.md"
|
||||
- "CODE_OF_CONDUCT.md"
|
||||
- "AGENTS.md"
|
||||
github_reviewers: ["kangfenmao", "0xfullex", "EurFelux"]
|
||||
|
||||
rules:
|
||||
vendor_added:
|
||||
# 新增供应商时的强制审核人
|
||||
github_reviewers: ["YinsenHo"]
|
||||
large_change:
|
||||
# 重大变更阈值(改动文件数 > changed_files_gt 触发)
|
||||
changed_files_gt: 30
|
||||
github_reviewers: ["kangfenmao"]
|
||||
|
||||
|
||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -3,18 +3,6 @@
|
||||
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
<!--
|
||||
|
||||
⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On Hold ⚠️
|
||||
|
||||
Please note: For our current development cycle, we are not accepting feature Pull Requests that introduce changes to Redux data models or IndexedDB schemas.
|
||||
|
||||
While we value your contributions, PRs of this nature will be blocked without merge. We welcome all other contributions (bug fixes, perf enhancements, docs, etc.). Thank you!
|
||||
|
||||
Once version 2.0.0 is released, we will resume reviewing feature PRs.
|
||||
|
||||
-->
|
||||
|
||||
### What this PR does
|
||||
|
||||
Before this PR:
|
||||
|
||||
455
.github/reviewer-suggestions.json
vendored
455
.github/reviewer-suggestions.json
vendored
@@ -1,455 +0,0 @@
|
||||
{
|
||||
"generatedAt": "2025-10-29T06:19:19.098Z",
|
||||
"suggestions": {
|
||||
"ai_core": [
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 33
|
||||
},
|
||||
{
|
||||
"github": "MyPrototypeWhat",
|
||||
"name": "MyPrototypeWhat",
|
||||
"email": "daoquqiexing@gmail.com",
|
||||
"commits": 12
|
||||
},
|
||||
{
|
||||
"github": "Vaayne",
|
||||
"name": "Vaayne",
|
||||
"email": "liu.vaayne@gmail.com",
|
||||
"commits": 10
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 9
|
||||
},
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 6
|
||||
}
|
||||
],
|
||||
"agent": [
|
||||
{
|
||||
"github": "icarus",
|
||||
"name": "icarus",
|
||||
"email": "eurfelux@gmail.com",
|
||||
"commits": 152
|
||||
},
|
||||
{
|
||||
"github": "Vaayne",
|
||||
"name": "Vaayne",
|
||||
"email": "liu.vaayne@gmail.com",
|
||||
"commits": 80
|
||||
},
|
||||
{
|
||||
"github": "suyao",
|
||||
"name": "suyao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 29
|
||||
},
|
||||
{
|
||||
"github": "defi-failure",
|
||||
"name": "defi-failure",
|
||||
"email": "159208748+defi-failure@users.noreply.github.com",
|
||||
"commits": 8
|
||||
},
|
||||
{
|
||||
"github": "Phantom",
|
||||
"name": "Phantom",
|
||||
"email": "eurfelux@gmail.com",
|
||||
"commits": 5
|
||||
}
|
||||
],
|
||||
"provider": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 53
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 32
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 30
|
||||
},
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 14
|
||||
},
|
||||
{
|
||||
"github": "eeee0717",
|
||||
"name": "Chen Tao",
|
||||
"email": "70054568+eeee0717@users.noreply.github.com",
|
||||
"commits": 10
|
||||
}
|
||||
],
|
||||
"backend": [
|
||||
{
|
||||
"github": "beyondkmp",
|
||||
"name": "beyondkmp",
|
||||
"email": "beyondkmp@gmail.com",
|
||||
"commits": 99
|
||||
},
|
||||
{
|
||||
"github": "Vaayne",
|
||||
"name": "Vaayne",
|
||||
"email": "liu.vaayne@gmail.com",
|
||||
"commits": 96
|
||||
},
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 84
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 49
|
||||
},
|
||||
{
|
||||
"github": "vaayne",
|
||||
"name": "LiuVaayne",
|
||||
"email": "10231735+vaayne@users.noreply.github.com",
|
||||
"commits": 33
|
||||
}
|
||||
],
|
||||
"knowledge": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 20
|
||||
},
|
||||
{
|
||||
"github": "eeee0717",
|
||||
"name": "Chen Tao",
|
||||
"email": "70054568+eeee0717@users.noreply.github.com",
|
||||
"commits": 13
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 8
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 6
|
||||
},
|
||||
{
|
||||
"github": "beyondkmp",
|
||||
"name": "beyondkmp",
|
||||
"email": "beyondkmp@gmail.com",
|
||||
"commits": 5
|
||||
}
|
||||
],
|
||||
"data_storage": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 63
|
||||
},
|
||||
{
|
||||
"github": "Vaayne",
|
||||
"name": "Vaayne",
|
||||
"email": "liu.vaayne@gmail.com",
|
||||
"commits": 21
|
||||
},
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 20
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 17
|
||||
},
|
||||
{
|
||||
"github": "suyao",
|
||||
"name": "suyao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 13
|
||||
}
|
||||
],
|
||||
"backup_export": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 23
|
||||
},
|
||||
{
|
||||
"github": "beyondkmp",
|
||||
"name": "beyondkmp",
|
||||
"email": "beyondkmp@gmail.com",
|
||||
"commits": 12
|
||||
},
|
||||
{
|
||||
"github": "GeorgeDong32",
|
||||
"name": "George·Dong",
|
||||
"email": "98630204+GeorgeDong32@users.noreply.github.com",
|
||||
"commits": 9
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 5
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 5
|
||||
}
|
||||
],
|
||||
"minapps": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 12
|
||||
},
|
||||
{
|
||||
"github": "beyondkmp",
|
||||
"name": "beyondkmp",
|
||||
"email": "beyondkmp@gmail.com",
|
||||
"commits": 5
|
||||
},
|
||||
{
|
||||
"github": "GeorgeDong32",
|
||||
"name": "George·Dong",
|
||||
"email": "98630204+GeorgeDong32@users.noreply.github.com",
|
||||
"commits": 4
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 4
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 3
|
||||
}
|
||||
],
|
||||
"chat": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 189
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 86
|
||||
},
|
||||
{
|
||||
"github": "icarus",
|
||||
"name": "icarus",
|
||||
"email": "eurfelux@gmail.com",
|
||||
"commits": 85
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 52
|
||||
},
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 48
|
||||
}
|
||||
],
|
||||
"draw": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 8
|
||||
},
|
||||
{
|
||||
"github": "jin-wang-c",
|
||||
"name": "Caelan",
|
||||
"email": "79105826+jin-wang-c@users.noreply.github.com",
|
||||
"commits": 7
|
||||
},
|
||||
{
|
||||
"github": "DDU1222",
|
||||
"name": "chenxue",
|
||||
"email": "DDU1222@users.noreply.github.com",
|
||||
"commits": 6
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 5
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 4
|
||||
}
|
||||
],
|
||||
"uiux": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 109
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 89
|
||||
},
|
||||
{
|
||||
"github": "icarus",
|
||||
"name": "icarus",
|
||||
"email": "eurfelux@gmail.com",
|
||||
"commits": 35
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 32
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 24
|
||||
}
|
||||
],
|
||||
"build-config": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 170
|
||||
},
|
||||
{
|
||||
"github": "beyondkmp",
|
||||
"name": "beyondkmp",
|
||||
"email": "beyondkmp@gmail.com",
|
||||
"commits": 65
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 40
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 34
|
||||
},
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 32
|
||||
}
|
||||
],
|
||||
"test": [
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 45
|
||||
},
|
||||
{
|
||||
"github": "SuYao",
|
||||
"name": "SuYao",
|
||||
"email": "sy20010504@gmail.com",
|
||||
"commits": 27
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 20
|
||||
},
|
||||
{
|
||||
"github": "Vaayne",
|
||||
"name": "Vaayne",
|
||||
"email": "liu.vaayne@gmail.com",
|
||||
"commits": 19
|
||||
},
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 18
|
||||
}
|
||||
],
|
||||
"docs": [
|
||||
{
|
||||
"github": "kangfenmao",
|
||||
"name": "kangfenmao",
|
||||
"email": "kangfenmao@qq.com",
|
||||
"commits": 18
|
||||
},
|
||||
{
|
||||
"github": "0xfullex",
|
||||
"name": "fullex",
|
||||
"email": "106392080+0xfullex@users.noreply.github.com",
|
||||
"commits": 7
|
||||
},
|
||||
{
|
||||
"github": "EurFelux",
|
||||
"name": "Phantom",
|
||||
"email": "59059173+EurFelux@users.noreply.github.com",
|
||||
"commits": 6
|
||||
},
|
||||
{
|
||||
"github": "one",
|
||||
"name": "one",
|
||||
"email": "wangan.cs@gmail.com",
|
||||
"commits": 6
|
||||
},
|
||||
{
|
||||
"github": "sunrise0o0",
|
||||
"name": "牡丹凤凰",
|
||||
"email": "87239270+sunrise0o0@users.noreply.github.com",
|
||||
"commits": 4
|
||||
}
|
||||
],
|
||||
"vendor_added": [],
|
||||
"large_change": []
|
||||
}
|
||||
}
|
||||
2
.github/workflows/auto-i18n.yml
vendored
2
.github/workflows/auto-i18n.yml
vendored
@@ -4,7 +4,6 @@ env:
|
||||
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
TRANSLATION_MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}}
|
||||
TRANSLATION_BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}}
|
||||
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -30,7 +29,6 @@ jobs:
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
run: |
|
||||
|
||||
285
.github/workflows/github-pr-tracker.yml
vendored
285
.github/workflows/github-pr-tracker.yml
vendored
@@ -1,285 +0,0 @@
|
||||
name: GitHub PR Tracker with Feishu Notification
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review, review_requested, reopened]
|
||||
schedule:
|
||||
# Run every day at 8:30 Beijing Time (00:30 UTC)
|
||||
- cron: '30 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
process-new-pr:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check PR conditions
|
||||
id: check_pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Check if PR is draft
|
||||
if (pr.draft) {
|
||||
console.log('⏭️ PR is in draft state, skipping notification');
|
||||
core.setOutput('should_notify', 'false');
|
||||
core.setOutput('skip_reason', 'draft');
|
||||
return;
|
||||
}
|
||||
|
||||
// We will notify regardless of whether reviewers/assignees are set
|
||||
console.log('✅ PR meets notification criteria');
|
||||
core.setOutput('should_notify', 'true');
|
||||
|
||||
// Prepare reviewer and assignee lists
|
||||
const reviewers = (pr.requested_reviewers || []).map(r => r.login).join(',');
|
||||
const assignees = (pr.assignees || []).map(a => a.login).join(',');
|
||||
|
||||
core.setOutput('reviewers', reviewers);
|
||||
core.setOutput('assignees', assignees);
|
||||
|
||||
- name: Check Beijing Time
|
||||
if: steps.check_pr.outputs.should_notify == 'true'
|
||||
id: check_time
|
||||
run: |
|
||||
# Get current time in Beijing timezone (UTC+8)
|
||||
BEIJING_HOUR=$(TZ='Asia/Shanghai' date +%H)
|
||||
BEIJING_MINUTE=$(TZ='Asia/Shanghai' date +%M)
|
||||
|
||||
echo "Beijing Time: ${BEIJING_HOUR}:${BEIJING_MINUTE}"
|
||||
|
||||
# Check if time is between 00:00 and 08:30
|
||||
if [ $BEIJING_HOUR -lt 8 ] || ([ $BEIJING_HOUR -eq 8 ] && [ $BEIJING_MINUTE -le 30 ]); then
|
||||
echo "should_delay=true" >> $GITHUB_OUTPUT
|
||||
echo "⏰ PR created during quiet hours (00:00-08:30 Beijing Time)"
|
||||
echo "Will schedule notification for 08:30"
|
||||
else
|
||||
echo "should_delay=false" >> $GITHUB_OUTPUT
|
||||
echo "✅ PR created during active hours, will notify immediately"
|
||||
fi
|
||||
|
||||
- name: Add pending label if in quiet hours
|
||||
if: steps.check_pr.outputs.should_notify == 'true' && steps.check_time.outputs.should_delay == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: ['pending-feishu-pr-notification']
|
||||
});
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_pr.outputs.should_notify == 'true' && steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Process PR with Claude
|
||||
if: steps.check_pr.outputs.should_notify == 'true' && steps.check_time.outputs.should_delay == 'false'
|
||||
uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
allowed_non_write_users: "*"
|
||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
||||
claude_args: "--allowed-tools Bash(gh pr:*),Bash(node scripts/feishu-pr-notify.js)"
|
||||
prompt: |
|
||||
你是一个GitHub Pull Request自动化处理助手。请完成以下任务:
|
||||
|
||||
## 当前PR信息
|
||||
- PR编号:#${{ github.event.pull_request.number }}
|
||||
- 标题:${{ github.event.pull_request.title }}
|
||||
- 作者:${{ github.event.pull_request.user.login }}
|
||||
- URL:${{ github.event.pull_request.html_url }}
|
||||
- 内容:${{ github.event.pull_request.body }}
|
||||
- 标签:${{ join(github.event.pull_request.labels.*.name, ', ') }}
|
||||
- 改动文件数:${{ github.event.pull_request.changed_files }}
|
||||
- 增加行数:${{ github.event.pull_request.additions }}
|
||||
- 删除行数:${{ github.event.pull_request.deletions }}
|
||||
- Reviewers:${{ steps.check_pr.outputs.reviewers }}
|
||||
- Assignees:${{ steps.check_pr.outputs.assignees }}
|
||||
|
||||
## 任务步骤
|
||||
|
||||
1. **分析PR改动内容**
|
||||
首先使用以下命令获取PR的文件变更列表:
|
||||
```bash
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path'
|
||||
```
|
||||
|
||||
2. **分类PR内容**
|
||||
根据改动的文件路径和PR标题、描述,判断PR的类型(若命中多个模块则输出 `multiple`,若均未命中则 `other`):
|
||||
- chat(对话): src/renderer/src/pages/home/**, src/renderer/src/store/(newMessage|messageBlock|memory).ts
|
||||
- draw(绘图): src/renderer/src/pages/paintings/**, src/renderer/src/store/paintings.ts
|
||||
- uiux(UI/UX): src/renderer/src/components/**, src/renderer/src/ui/**, src/renderer/src/assets/styles/**, src/renderer/src/windows/**
|
||||
- knowledge(知识库): src/main/knowledge/**, src/renderer/src/pages/knowledge/**, src/renderer/src/store/knowledge.ts, src/renderer/src/queue/KnowledgeQueue.ts
|
||||
- minapps(小程序): src/renderer/src/pages/minapps/**, src/renderer/src/store/minapps.ts, src/renderer/src/config/minapps.ts
|
||||
- backup_export(备份/导出): src/renderer/src/components/*Backup*, src/renderer/src/components/Webdav*, src/renderer/src/components/ObsidianExportDialog.tsx, src/renderer/src/components/S3*, src/renderer/src/store/(backup|nutstore).ts
|
||||
- data_storage(数据与存储): src/renderer/src/databases/**, src/renderer/src/services/db/**, src/main/services/agents/database/**, resources/database/drizzle/**, src/renderer/src/store/migrate.ts, src/renderer/src/databases/upgrades.ts
|
||||
- ai_core(AI基础设施): packages/aiCore/**, src/renderer/src/aiCore/**
|
||||
- backend(后端/平台): src/main/apiServer/**, src/main/services/**, src/main/*.ts, src/preload/**, src/main/mcpServers/**
|
||||
- agent(Agent): packages/shared/agents/**, resources/data/agents-*.json, src/renderer/src/api/agent.ts, src/renderer/src/types/agent.ts, src/renderer/src/utils/agentSession.ts, src/renderer/src/services/db/AgentMessageDataSource.ts, src/renderer/src/hooks/agents/**, src/renderer/src/components/Popups/agent/**, src/renderer/src/pages/home/**/Agent*.tsx, src/renderer/src/pages/settings/AgentSettings/**, src/main/services/agents/**, src/main/apiServer/routes/agents/**
|
||||
- provider(Provider): src/renderer/src/config/(providers|preprocessProviders|webSearchProviders).ts, src/renderer/src/hooks/useWebSearchProviders.ts, src/renderer/src/providers/**, src/renderer/src/pages/settings/ProviderSettings/**, src/renderer/src/pages/settings/WebSearchSettings/**, src/renderer/src/pages/settings/DocProcessSettings/(OcrProviderSettings|PreprocessProviderSettings).tsx, src/renderer/src/pages/settings/MCPSettings/providers/**, src/renderer/src/assets/images/providers/**, src/main/services/urlschema/handle-providers.ts
|
||||
- build-config(构建/配置): package.json, tsconfig*.json, electron-builder.yml, electron.vite.config.ts, vitest.config.ts, playwright.config.ts, .github/workflows/**, scripts/**
|
||||
- test(测试): tests/**, src/**/__tests__/**, scripts/__tests__/**
|
||||
- docs(文档): docs/**, README*.md, SECURITY.md, CODE_OF_CONDUCT.md, AGENTS.md
|
||||
|
||||
2.1 **识别是否“新增供应商”**
|
||||
满足以下任一条件则视为“新增供应商”并设置变量 PR_VENDOR_ADDED=true,否则为 false:
|
||||
- 改动文件包含:
|
||||
- src/renderer/src/config/providers.ts
|
||||
- src/renderer/src/providers/**
|
||||
- packages/aiCore/**/provider/** 或 packages/aiCore/src/provider/**
|
||||
- resources/data/agents-*.json
|
||||
- 或 PR 标题/描述包含关键词:"供应商"、"厂商"、"provider"、"新增"、"集成"
|
||||
|
||||
3. **总结PR**
|
||||
用中文(简体)提供简洁的总结(2-3句话),包括:
|
||||
- PR的主要改动内容
|
||||
- 核心功能或修复
|
||||
- 重要的技术细节或影响范围
|
||||
|
||||
4. **发送飞书通知**
|
||||
使用以下命令发送飞书通知:
|
||||
```bash
|
||||
PR_URL="${{ github.event.pull_request.html_url }}" \
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}" \
|
||||
PR_TITLE="${{ github.event.pull_request.title }}" \
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}" \
|
||||
PR_LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}" \
|
||||
PR_SUMMARY="<你生成的中文总结>" \
|
||||
PR_REVIEWERS="${{ steps.check_pr.outputs.reviewers }}" \
|
||||
PR_ASSIGNEES="${{ steps.check_pr.outputs.assignees }}" \
|
||||
PR_CATEGORY="<你判断的PR类型>" \
|
||||
PR_VENDOR_ADDED="<true 或 false>" \
|
||||
PR_CHANGED_FILES="${{ github.event.pull_request.changed_files }}" \
|
||||
PR_ADDITIONS="${{ github.event.pull_request.additions }}" \
|
||||
PR_DELETIONS="${{ github.event.pull_request.deletions }}" \
|
||||
node scripts/feishu-pr-notify.js
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
- 总结必须使用简体中文
|
||||
- PR_SUMMARY 和其他参数在传递时需要正确转义特殊字符
|
||||
- PR_CATEGORY 必须是上述定义的类型之一
|
||||
- 如果PR内容为空,也要提供一个简短的说明
|
||||
|
||||
请开始执行任务!
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }}
|
||||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||||
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
|
||||
FEISHU_USER_MAPPING: ${{ secrets.FEISHU_USER_MAPPING }}
|
||||
|
||||
process-pending-prs:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Process pending PRs with Claude
|
||||
uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
||||
allowed_non_write_users: "*"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
claude_args: "--allowed-tools Bash(gh pr:*),Bash(gh api:*),Bash(node scripts/feishu-pr-notify.js)"
|
||||
prompt: |
|
||||
你是一个GitHub Pull Request自动化处理助手。请完成以下任务:
|
||||
|
||||
## 任务说明
|
||||
处理所有待发送飞书通知的GitHub PRs(标记为 `pending-feishu-pr-notification` 的PRs)
|
||||
|
||||
## 步骤
|
||||
|
||||
1. **获取待处理的PRs**
|
||||
使用以下命令获取所有带 `pending-feishu-pr-notification` 标签的PRs:
|
||||
```bash
|
||||
gh api repos/${{ github.repository }}/pulls?state=open | jq '.[] | select(.labels[]?.name == "pending-feishu-pr-notification")'
|
||||
```
|
||||
|
||||
2. **验证PR条件**
|
||||
对于每个PR,检查:
|
||||
- 是否仍然不是draft状态
|
||||
- 是否有reviewers或assignees
|
||||
- 如果不满足条件,移除标签并跳过
|
||||
|
||||
3. **分析和分类PR**
|
||||
获取PR的文件变更:
|
||||
```bash
|
||||
gh pr view <PR编号> --json files --jq '.files[].path'
|
||||
```
|
||||
|
||||
根据文件路径判断PR类型(chat/draw/uiux/knowledge/minapps/backup_export/data_storage/ai_core/backend/agent/provider/docs/build-config/test/multiple/other)
|
||||
|
||||
4. **总结每个PR**
|
||||
用中文提供简洁的总结(2-3句话),包括:
|
||||
- PR的主要改动内容
|
||||
- 核心功能或修复
|
||||
- 重要的技术细节
|
||||
|
||||
5. **发送飞书通知**
|
||||
使用以下命令发送通知:
|
||||
```bash
|
||||
PR_URL="<PR的html_url>" \
|
||||
PR_NUMBER="<PR编号>" \
|
||||
PR_TITLE="<PR标题>" \
|
||||
PR_AUTHOR="<PR作者>" \
|
||||
PR_LABELS="<逗号分隔的标签列表,排除pending-feishu-pr-notification>" \
|
||||
PR_SUMMARY="<你生成的中文总结>" \
|
||||
PR_REVIEWERS="<reviewers列表,逗号分隔>" \
|
||||
PR_ASSIGNEES="<assignees列表,逗号分隔>" \
|
||||
PR_CATEGORY="<PR类型>" \
|
||||
PR_VENDOR_ADDED="<true 或 false>" \
|
||||
PR_CHANGED_FILES="<改动文件数>" \
|
||||
PR_ADDITIONS="<增加行数>" \
|
||||
PR_DELETIONS="<删除行数>" \
|
||||
node scripts/feishu-pr-notify.js
|
||||
```
|
||||
|
||||
6. **移除标签**
|
||||
成功发送后,移除标签:
|
||||
```bash
|
||||
gh api -X DELETE repos/${{ github.repository }}/issues/<PR编号>/labels/pending-feishu-pr-notification
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
- Repository: ${{ github.repository }}
|
||||
- Feishu webhook URL和密钥已配置
|
||||
|
||||
## 注意事项
|
||||
- 如果没有待处理的PRs,输出提示后结束
|
||||
- 处理多个PRs时,每个之间等待2-3秒
|
||||
- 某个PR失败不中断整个流程
|
||||
- 所有总结使用简体中文
|
||||
|
||||
请开始执行任务!
|
||||
env:
|
||||
ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }}
|
||||
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
|
||||
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
|
||||
FEISHU_USER_MAPPING: ${{ secrets.FEISHU_USER_MAPPING }}
|
||||
|
||||
|
||||
6
.github/workflows/issue-management.yml
vendored
6
.github/workflows/issue-management.yml
vendored
@@ -29,10 +29,8 @@ jobs:
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: 'inactive'
|
||||
close-issue-label: 'closed:no-response'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
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 }} 天没有任何活动,将立即关闭。
|
||||
@@ -48,8 +46,6 @@ jobs:
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: ${{ env.daysBeforeClose }}
|
||||
stale-issue-label: 'inactive'
|
||||
exempt-all-milestones: true
|
||||
exempt-all-assignees: true
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
|
||||
13
.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch
vendored
Normal file
13
.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 69ab1599c76801dc1167551b6fa283dded123466..f0af43bba7ad1196fe05338817e65b4ebda40955 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId?.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
@@ -1,26 +0,0 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index 4cc66d83af1cef39f6447dc62e680251e05ddf9f..eb9819cb674c1808845ceb29936196c4bb355172 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -471,7 +471,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index a032505ec54e132dc386dde001dc51f710f84c83..5efada51b9a8b56e3f01b35e734908ebe3c37043 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -477,7 +477,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||
|
||||
// src/get-model-path.ts
|
||||
function getModelPath(modelId) {
|
||||
- return modelId.includes("/") ? modelId : `models/${modelId}`;
|
||||
+ return modelId.includes("models/") ? modelId : `models/${modelId}`;
|
||||
}
|
||||
|
||||
// src/google-generative-ai-options.ts
|
||||
@@ -1,131 +0,0 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index b3f018730a93639aad7c203f15fb1aeb766c73f4..ade2a43d66e9184799d072153df61ef7be4ea110 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -296,7 +296,14 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
metadata: huggingfaceOptions == null ? void 0 : huggingfaceOptions.metadata,
|
||||
instructions: huggingfaceOptions == null ? void 0 : huggingfaceOptions.instructions,
|
||||
...preparedTools && { tools: preparedTools },
|
||||
- ...preparedToolChoice && { tool_choice: preparedToolChoice }
|
||||
+ ...preparedToolChoice && { tool_choice: preparedToolChoice },
|
||||
+ ...(huggingfaceOptions?.reasoningEffort != null && {
|
||||
+ reasoning: {
|
||||
+ ...(huggingfaceOptions?.reasoningEffort != null && {
|
||||
+ effort: huggingfaceOptions.reasoningEffort,
|
||||
+ }),
|
||||
+ },
|
||||
+ }),
|
||||
};
|
||||
return { args: baseArgs, warnings };
|
||||
}
|
||||
@@ -365,6 +372,20 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
}
|
||||
break;
|
||||
}
|
||||
+ case 'reasoning': {
|
||||
+ for (const contentPart of part.content) {
|
||||
+ content.push({
|
||||
+ type: 'reasoning',
|
||||
+ text: contentPart.text,
|
||||
+ providerMetadata: {
|
||||
+ huggingface: {
|
||||
+ itemId: part.id,
|
||||
+ },
|
||||
+ },
|
||||
+ });
|
||||
+ }
|
||||
+ break;
|
||||
+ }
|
||||
case "mcp_call": {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
@@ -519,6 +540,11 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
id: value.item.call_id,
|
||||
toolName: value.item.name
|
||||
});
|
||||
+ } else if (value.item.type === 'reasoning') {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-start',
|
||||
+ id: value.item.id,
|
||||
+ });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -570,6 +596,22 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
});
|
||||
return;
|
||||
}
|
||||
+ if (isReasoningDeltaChunk(value)) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-delta',
|
||||
+ id: value.item_id,
|
||||
+ delta: value.delta,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (isReasoningEndChunk(value)) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-end',
|
||||
+ id: value.item_id,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
},
|
||||
flush(controller) {
|
||||
controller.enqueue({
|
||||
@@ -593,7 +635,8 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
var huggingfaceResponsesProviderOptionsSchema = z2.object({
|
||||
metadata: z2.record(z2.string(), z2.string()).optional(),
|
||||
instructions: z2.string().optional(),
|
||||
- strictJsonSchema: z2.boolean().optional()
|
||||
+ strictJsonSchema: z2.boolean().optional(),
|
||||
+ reasoningEffort: z2.string().optional(),
|
||||
});
|
||||
var huggingfaceResponsesResponseSchema = z2.object({
|
||||
id: z2.string(),
|
||||
@@ -727,12 +770,31 @@ var responseCreatedChunkSchema = z2.object({
|
||||
model: z2.string()
|
||||
})
|
||||
});
|
||||
+var reasoningTextDeltaChunkSchema = z2.object({
|
||||
+ type: z2.literal('response.reasoning_text.delta'),
|
||||
+ item_id: z2.string(),
|
||||
+ output_index: z2.number(),
|
||||
+ content_index: z2.number(),
|
||||
+ delta: z2.string(),
|
||||
+ sequence_number: z2.number(),
|
||||
+});
|
||||
+
|
||||
+var reasoningTextEndChunkSchema = z2.object({
|
||||
+ type: z2.literal('response.reasoning_text.done'),
|
||||
+ item_id: z2.string(),
|
||||
+ output_index: z2.number(),
|
||||
+ content_index: z2.number(),
|
||||
+ text: z2.string(),
|
||||
+ sequence_number: z2.number(),
|
||||
+});
|
||||
var huggingfaceResponsesChunkSchema = z2.union([
|
||||
responseOutputItemAddedSchema,
|
||||
responseOutputItemDoneSchema,
|
||||
textDeltaChunkSchema,
|
||||
responseCompletedChunkSchema,
|
||||
responseCreatedChunkSchema,
|
||||
+ reasoningTextDeltaChunkSchema,
|
||||
+ reasoningTextEndChunkSchema,
|
||||
z2.object({ type: z2.string() }).loose()
|
||||
// fallback for unknown chunks
|
||||
]);
|
||||
@@ -751,6 +813,12 @@ function isResponseCompletedChunk(chunk) {
|
||||
function isResponseCreatedChunk(chunk) {
|
||||
return chunk.type === "response.created";
|
||||
}
|
||||
+function isReasoningDeltaChunk(chunk) {
|
||||
+ return chunk.type === 'response.reasoning_text.delta';
|
||||
+}
|
||||
+function isReasoningEndChunk(chunk) {
|
||||
+ return chunk.type === 'response.reasoning_text.done';
|
||||
+}
|
||||
|
||||
// src/huggingface-provider.ts
|
||||
function createHuggingFace(options = {}) {
|
||||
@@ -1,76 +0,0 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index cc6652c4e7f32878a64a2614115bf7eeb3b7c890..76e989017549c89b45d633525efb1f318026d9b2 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
message: import_v42.z.object({
|
||||
role: import_v42.z.literal("assistant").nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
id: import_v42.z.string().nullish(),
|
||||
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
|
||||
delta: import_v42.z.object({
|
||||
role: import_v42.z.enum(["assistant"]).nullish(),
|
||||
content: import_v42.z.string().nullish(),
|
||||
+ reasoning_content: import_v42.z.string().nullish(),
|
||||
tool_calls: import_v42.z.array(
|
||||
import_v42.z.object({
|
||||
index: import_v42.z.number(),
|
||||
@@ -785,6 +787,14 @@ var OpenAIChatLanguageModel = class {
|
||||
if (text != null && text.length > 0) {
|
||||
content.push({ type: "text", text });
|
||||
}
|
||||
+ const reasoning =
|
||||
+ choice.message.reasoning_content;
|
||||
+ if (reasoning != null && reasoning.length > 0) {
|
||||
+ content.push({
|
||||
+ type: 'reasoning',
|
||||
+ text: reasoning,
|
||||
+ });
|
||||
+ }
|
||||
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
@@ -866,6 +876,7 @@ var OpenAIChatLanguageModel = class {
|
||||
};
|
||||
let isFirstChunk = true;
|
||||
let isActiveText = false;
|
||||
+ let isActiveReasoning = false;
|
||||
const providerMetadata = { openai: {} };
|
||||
return {
|
||||
stream: response.pipeThrough(
|
||||
@@ -920,6 +931,22 @@ var OpenAIChatLanguageModel = class {
|
||||
return;
|
||||
}
|
||||
const delta = choice.delta;
|
||||
+ const reasoningContent = delta.reasoning_content;
|
||||
+ if (reasoningContent) {
|
||||
+ if (!isActiveReasoning) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-start',
|
||||
+ id: 'reasoning-0',
|
||||
+ });
|
||||
+ isActiveReasoning = true;
|
||||
+ }
|
||||
+
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-delta',
|
||||
+ id: 'reasoning-0',
|
||||
+ delta: reasoningContent,
|
||||
+ });
|
||||
+ }
|
||||
if (delta.content != null) {
|
||||
if (!isActiveText) {
|
||||
controller.enqueue({ type: "text-start", id: "0" });
|
||||
@@ -1032,6 +1059,9 @@ var OpenAIChatLanguageModel = class {
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
+ if (isActiveReasoning) {
|
||||
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
|
||||
+ }
|
||||
if (isActiveText) {
|
||||
controller.enqueue({ type: "text-end", id: "0" });
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
diff --git a/sdk.mjs b/sdk.mjs
|
||||
index 10162e5b1624f8ce667768943347a6e41089ad2f..32568ae08946590e382270c88d85fba81187568e 100755
|
||||
index 461e9a2ba246778261108a682762ffcf26f7224e..44bd667d9f591969d36a105ba5eb8b478c738dd8 100644
|
||||
--- a/sdk.mjs
|
||||
+++ b/sdk.mjs
|
||||
@@ -6213,7 +6213,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
@@ -6215,7 +6215,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
|
||||
}
|
||||
|
||||
|
||||
// ../src/transport/ProcessTransport.ts
|
||||
-import { spawn } from "child_process";
|
||||
+import { fork } from "child_process";
|
||||
import { createInterface } from "readline";
|
||||
|
||||
|
||||
// ../src/utils/fsOperations.ts
|
||||
@@ -6487,14 +6487,11 @@ class ProcessTransport {
|
||||
@@ -6473,14 +6473,11 @@ class ProcessTransport {
|
||||
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
||||
throw new ReferenceError(errorMessage);
|
||||
}
|
||||
- const isNative = isNativeBinary(pathToClaudeCodeExecutable);
|
||||
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
||||
- const spawnArgs = isNative ? [...executableArgs, ...args] : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${spawnCommand} ${spawnArgs.join(" ")}` : `Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")}`);
|
||||
- const spawnArgs = isNative ? args : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
||||
- this.logForDebugging(isNative ? `Spawning Claude Code native binary: ${pathToClaudeCodeExecutable} ${args.join(" ")}` : `Spawning Claude Code process: ${executable} ${[...executableArgs, pathToClaudeCodeExecutable, ...args].join(" ")}`);
|
||||
+ this.logForDebugging(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
||||
const stderrMode = env.DEBUG || stderr ? "pipe" : "ignore";
|
||||
- this.child = spawn(spawnCommand, spawnArgs, {
|
||||
@@ -10,9 +10,8 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, `
|
||||
📝 docs:`).
|
||||
- **Seek review**: Ask a human developer to review substantial changes before merging.
|
||||
- **Commit in rhythm**: Keep commits small, conventional, and emoji-tagged.
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
@@ -65,28 +65,7 @@ The Test Plan aims to provide users with a more stable application experience an
|
||||
### Other Suggestions
|
||||
|
||||
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
|
||||
|
||||
## Important Contribution Guidelines & Focus Areas
|
||||
|
||||
Please review the following critical information before submitting your Pull Request:
|
||||
|
||||
### Temporary Restriction on Data-Changing Feature PRs 🚫
|
||||
|
||||
**Currently, we are NOT accepting feature Pull Requests that introduce changes to our Redux data models or IndexedDB schemas.**
|
||||
|
||||
Our core team is currently focused on significant architectural updates that involve these data structures. To ensure stability and focus during this period, contributions of this nature will be temporarily managed internally.
|
||||
|
||||
* **PRs that require changes to Redux state shape or IndexedDB schemas will be closed.**
|
||||
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162).
|
||||
|
||||
We highly encourage contributions for:
|
||||
* Bug fixes 🐞
|
||||
* Performance improvements 🚀
|
||||
* Documentation updates 📚
|
||||
* Features that **do not** alter Redux data models or IndexedDB schemas (e.g., UI enhancements, new components, minor refactors). ✨
|
||||
|
||||
We appreciate your understanding and continued support during this important development phase. Thank you!
|
||||
|
||||
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
|
||||
|
||||
## Contact Us
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][twitter-shield]][twitter-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
</div>
|
||||
<div align="center">
|
||||
|
||||
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-nightly-shield]][github-nightly-link]
|
||||
[![][github-contributors-shield]][github-contributors-link]
|
||||
@@ -248,10 +248,10 @@ The Enterprise Edition addresses core challenges in team collaboration by centra
|
||||
|
||||
| Feature | Community Edition | Enterprise Edition |
|
||||
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
||||
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
||||
| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee |
|
||||
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
|
||||
| **Server** | — | ✅ Dedicated Private Deployment |
|
||||
| **Server** | — | ✅ Dedicated Private Deployment |
|
||||
|
||||
## Get the Enterprise Edition
|
||||
|
||||
|
||||
@@ -21,11 +21,7 @@
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"files": { "ignoreUnknown": false },
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
|
||||
@@ -69,28 +69,7 @@ git commit --signoff -m "Your commit message"
|
||||
### 其他建议
|
||||
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
|
||||
## 重要贡献指南与关注点
|
||||
|
||||
在提交 Pull Request 之前,请务必阅读以下关键信息:
|
||||
|
||||
### 🚫 暂时限制涉及数据更改的功能性 PR
|
||||
|
||||
**目前,我们不接受涉及 Redux 数据模型或 IndexedDB schema 变更的功能性 Pull Request。**
|
||||
|
||||
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
|
||||
|
||||
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/CherryHQ/cherry-studio/pull/10162) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
|
||||
我们非常鼓励以下类型的贡献:
|
||||
* 错误修复 🐞
|
||||
* 性能改进 🚀
|
||||
* 文档更新 📚
|
||||
* 不改变 Redux 数据模型或 IndexedDB schema 的功能(例如,UI 增强、新组件、小型重构)。✨
|
||||
|
||||
感谢您在此重要开发阶段的理解与持续支持。谢谢!
|
||||
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
|
||||
|
||||
## 联系我们
|
||||
|
||||
|
||||
305
docs/EXPORT_IMAGES_PLAN.md
Normal file
305
docs/EXPORT_IMAGES_PLAN.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 对话图片导出功能设计方案
|
||||
|
||||
## 一、需求背景
|
||||
|
||||
随着多模态AI模型的普及,用户在对话中使用图片的频率增加。当前的导出功能只处理文本内容,图片被完全忽略,需要增加图片导出能力。
|
||||
|
||||
## 二、现状分析
|
||||
|
||||
### 2.1 图片存储机制
|
||||
|
||||
当前系统中图片有两种存储方式:
|
||||
|
||||
1. **用户上传的图片**
|
||||
- 存储位置:本地文件系统
|
||||
- 访问方式:通过 `FileMetadata.path` 字段,使用 `file://` 协议
|
||||
- 数据结构:`ImageMessageBlock.file`
|
||||
|
||||
2. **AI生成的图片**
|
||||
- 存储位置:内存中的Base64字符串
|
||||
- 访问方式:`ImageMessageBlock.metadata.generateImageResponse.images` 数组
|
||||
- 数据格式:Base64编码的图片数据
|
||||
|
||||
### 2.2 现有导出功能
|
||||
|
||||
当前支持的导出格式:
|
||||
- Markdown(本地文件/指定路径)
|
||||
- Word文档(.docx)
|
||||
- Notion(需要API配置)
|
||||
- 语雀(需要API配置)
|
||||
- Obsidian(带弹窗配置)
|
||||
- Joplin(需要API配置)
|
||||
- 思源笔记(需要API配置)
|
||||
- 笔记工作区
|
||||
- 纯文本
|
||||
- 图片截图
|
||||
|
||||
### 2.3 导出菜单问题
|
||||
|
||||
1. **设置分散**:导出相关设置分布在多个地方
|
||||
2. **每次导出可能需要不同配置**:如是否包含推理内容、是否包含引用等
|
||||
3. **缺乏统一的导出界面**:除Obsidian外,其他格式直接执行导出
|
||||
|
||||
## 三、解决方案
|
||||
|
||||
### 3.1 第一阶段:图片导出功能实现
|
||||
|
||||
#### 3.1.1 导出模式设计
|
||||
|
||||
提供两种图片导出模式供用户选择:
|
||||
|
||||
**模式1:Base64嵌入模式**
|
||||
```markdown
|
||||

|
||||
```
|
||||
- 优点:单文件、便于分享、保证完整性
|
||||
- 缺点:文件体积大、部分编辑器不支持、性能较差
|
||||
|
||||
**模式2:文件夹模式**
|
||||
```
|
||||
导出结构:
|
||||
conversation_2024-01-21/
|
||||
├── conversation.md
|
||||
└── images/
|
||||
├── user_upload_1.png
|
||||
├── ai_generated_1.png
|
||||
└── ...
|
||||
```
|
||||
Markdown中使用相对路径:
|
||||
```markdown
|
||||

|
||||
```
|
||||
- 优点:文件体积小、兼容性好、性能优秀
|
||||
- 缺点:需要管理多个文件、分享需打包
|
||||
|
||||
#### 3.1.2 核心功能实现
|
||||
|
||||
1. **新增图片处理工具函数** (`utils/export.ts`)
|
||||
```typescript
|
||||
// 处理消息中的所有图片块
|
||||
export async function processImageBlocks(
|
||||
message: Message,
|
||||
mode: 'base64' | 'folder',
|
||||
outputDir?: string
|
||||
): Promise<ImageExportResult[]>
|
||||
|
||||
// 将file://协议的图片转换为Base64
|
||||
export async function convertFileToBase64(filePath: string): Promise<string>
|
||||
|
||||
// 保存图片到指定文件夹
|
||||
export async function saveImageToFolder(
|
||||
image: string | Buffer,
|
||||
outputDir: string,
|
||||
fileName: string
|
||||
): Promise<string>
|
||||
|
||||
// 在Markdown中插入图片引用
|
||||
export function insertImageIntoMarkdown(
|
||||
markdown: string,
|
||||
images: ImageExportResult[]
|
||||
): string
|
||||
```
|
||||
|
||||
2. **更新现有导出函数**
|
||||
- `messageToMarkdown()`: 增加图片处理参数
|
||||
- `topicToMarkdown()`: 批量处理话题中的图片
|
||||
- `exportTopicAsMarkdown()`: 支持图片导出选项
|
||||
|
||||
3. **图片元数据保留**
|
||||
- AI生成图片:保存prompt信息
|
||||
- 用户上传图片:保留原始文件名
|
||||
- 添加图片索引和时间戳
|
||||
|
||||
### 3.2 第二阶段:统一导出弹窗(后续实施)
|
||||
|
||||
#### 3.2.1 弹窗设计
|
||||
|
||||
创建统一的导出配置弹窗 `UnifiedExportDialog`:
|
||||
|
||||
```typescript
|
||||
interface ExportDialogProps {
|
||||
// 导出内容
|
||||
content: {
|
||||
message?: Message
|
||||
messages?: Message[]
|
||||
topic?: Topic
|
||||
rawContent?: string
|
||||
}
|
||||
|
||||
// 导出格式
|
||||
format: ExportFormat
|
||||
|
||||
// 通用配置
|
||||
options: {
|
||||
includeReasoning?: boolean // 包含推理内容
|
||||
excludeCitations?: boolean // 排除引用
|
||||
imageExportMode?: 'base64' | 'folder' | 'none' // 图片导出模式
|
||||
imageQuality?: number // 图片质量(0-100)
|
||||
maxImageSize?: number // 最大图片尺寸
|
||||
}
|
||||
|
||||
// 格式特定配置
|
||||
formatOptions?: {
|
||||
// Markdown特定
|
||||
markdownPath?: string
|
||||
|
||||
// Notion特定
|
||||
notionDatabase?: string
|
||||
notionPageName?: string
|
||||
|
||||
// Obsidian特定
|
||||
obsidianVault?: string
|
||||
obsidianFolder?: string
|
||||
processingMethod?: string
|
||||
|
||||
// 其他格式配置...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 交互流程
|
||||
|
||||
1. 用户点击导出按钮
|
||||
2. 弹出统一导出弹窗
|
||||
3. 用户选择导出格式
|
||||
4. 根据格式显示相应配置选项
|
||||
5. 用户调整配置
|
||||
6. 点击确认执行导出
|
||||
|
||||
#### 3.2.3 优势
|
||||
|
||||
1. **配置集中管理**:所有导出配置在一个界面完成
|
||||
2. **动态配置**:每次导出可以调整不同设置
|
||||
3. **用户体验统一**:所有格式使用相同的交互模式
|
||||
4. **易于扩展**:新增导出格式只需添加配置项
|
||||
|
||||
## 四、实施计划
|
||||
|
||||
### Phase 1: 基础图片导出(本次实施)
|
||||
- [x] 创建设计文档
|
||||
- [ ] 实现图片处理工具函数
|
||||
- [ ] 更新Markdown导出支持图片
|
||||
- [ ] 添加图片导出模式设置
|
||||
- [ ] 测试不同场景
|
||||
|
||||
### Phase 2: 扩展格式支持
|
||||
- [ ] Word文档图片嵌入
|
||||
- [ ] Obsidian图片处理
|
||||
- [ ] Joplin图片上传
|
||||
- [ ] 思源笔记图片支持
|
||||
|
||||
### Phase 3: 统一导出弹窗
|
||||
- [ ] 设计弹窗UI组件
|
||||
- [ ] 实现配置管理逻辑
|
||||
- [ ] 迁移现有导出功能
|
||||
- [ ] 添加配置持久化
|
||||
|
||||
### Phase 4: 高级功能
|
||||
- [ ] 图片压缩优化
|
||||
- [ ] 批量导出进度显示
|
||||
- [ ] 导出历史记录
|
||||
- [ ] 导出模板系统
|
||||
|
||||
## 五、技术细节
|
||||
|
||||
### 5.1 图片格式转换
|
||||
|
||||
```typescript
|
||||
// Base64转换示例
|
||||
async function imageToBase64(imagePath: string): Promise<string> {
|
||||
if (imagePath.startsWith('file://')) {
|
||||
const actualPath = imagePath.slice(7)
|
||||
const buffer = await fs.readFile(actualPath)
|
||||
const mimeType = getMimeType(actualPath)
|
||||
return `data:${mimeType};base64,${buffer.toString('base64')}`
|
||||
}
|
||||
return imagePath // 已经是Base64
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 文件夹结构生成
|
||||
|
||||
```typescript
|
||||
async function createExportFolder(topicName: string): Promise<string> {
|
||||
const timestamp = dayjs().format('YYYY-MM-DD-HH-mm-ss')
|
||||
const folderName = `${sanitizeFileName(topicName)}_${timestamp}`
|
||||
const exportPath = path.join(getExportDir(), folderName)
|
||||
|
||||
await fs.mkdir(path.join(exportPath, 'images'), { recursive: true })
|
||||
return exportPath
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Markdown图片引用更新
|
||||
|
||||
```typescript
|
||||
function updateMarkdownImages(
|
||||
markdown: string,
|
||||
imageMap: Map<string, string>
|
||||
): string {
|
||||
let updatedMarkdown = markdown
|
||||
|
||||
for (const [originalPath, newPath] of imageMap) {
|
||||
// 替换图片引用
|
||||
const regex = new RegExp(`!\\[([^\\]]*)\\]\\(${escapeRegex(originalPath)}\\)`, 'g')
|
||||
updatedMarkdown = updatedMarkdown.replace(
|
||||
regex,
|
||||
``
|
||||
)
|
||||
}
|
||||
|
||||
return updatedMarkdown
|
||||
}
|
||||
```
|
||||
|
||||
## 六、注意事项
|
||||
|
||||
1. **性能考虑**
|
||||
- 大量图片时使用异步处理
|
||||
- 提供进度反馈
|
||||
- 实现取消操作
|
||||
|
||||
2. **兼容性**
|
||||
- 检测目标应用对图片格式的支持
|
||||
- 提供降级方案
|
||||
|
||||
3. **安全性**
|
||||
- 验证文件路径合法性
|
||||
- 限制图片大小
|
||||
- 清理临时文件
|
||||
|
||||
4. **用户体验**
|
||||
- 清晰的配置说明
|
||||
- 合理的默认值
|
||||
- 错误提示友好
|
||||
|
||||
## 七、后续优化
|
||||
|
||||
1. **Notion图片支持**(需要调研)
|
||||
- 研究Notion API的图片上传能力
|
||||
- 评估 `@notionhq/client` 库的图片处理功能
|
||||
- 可能需要先上传到图床再引用
|
||||
|
||||
2. **智能压缩**
|
||||
- 根据图片内容自动选择压缩算法
|
||||
- 保持图片质量的同时减小体积
|
||||
|
||||
3. **批量导出**
|
||||
- 支持多个话题同时导出
|
||||
- 生成导出报告
|
||||
|
||||
4. **云存储集成**
|
||||
- 支持直接上传到云存储
|
||||
- 生成分享链接
|
||||
|
||||
## 八、参考资料
|
||||
|
||||
- [Notion API Documentation](https://developers.notion.com/)
|
||||
- [Obsidian URI Protocol](https://help.obsidian.md/Extending+Obsidian/Obsidian+URI)
|
||||
- [Joplin Web Clipper API](https://joplinapp.org/api/references/rest_api/)
|
||||
- [思源笔记 API](https://github.com/siyuan-note/siyuan/blob/master/API.md)
|
||||
|
||||
---
|
||||
|
||||
*文档创建日期:2025-01-21*
|
||||
*最后更新:2025-01-21*
|
||||
@@ -64,12 +64,6 @@ asarUnpack:
|
||||
- resources/**
|
||||
- "**/*.{metal,exp,lib}"
|
||||
- "node_modules/@img/sharp-libvips-*/**"
|
||||
|
||||
# copy from node_modules/claude-code-plugins/plugins to resources/data/claude-code-pluginso
|
||||
extraResources:
|
||||
- from: "./node_modules/claude-code-plugins/plugins/"
|
||||
to: "claude-code-plugins"
|
||||
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
|
||||
21
package.json
21
package.json
@@ -78,7 +78,7 @@
|
||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.25#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.25-08bbabb5d3.patch",
|
||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.1#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.1-d937b73fed.patch",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
@@ -86,8 +86,6 @@
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"gray-matter": "^4.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "26.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.2.0",
|
||||
@@ -103,9 +101,8 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.42",
|
||||
"@ai-sdk/google-vertex": "^3.0.48",
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||
"@ai-sdk/google-vertex": "^3.0.40",
|
||||
"@ai-sdk/mistral": "^2.0.19",
|
||||
"@ai-sdk/perplexity": "^2.0.13",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
@@ -151,7 +148,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^1.1.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
@@ -197,7 +194,6 @@
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/he": "^1",
|
||||
"@types/html-to-text": "^9",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
@@ -227,7 +223,7 @@
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"ai": "^5.0.76",
|
||||
"ai": "^5.0.68",
|
||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
@@ -237,7 +233,6 @@
|
||||
"check-disk-space": "3.4.0",
|
||||
"cheerio": "^1.1.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"claude-code-plugins": "1.0.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"code-inspector-plugin": "^0.20.14",
|
||||
@@ -390,15 +385,13 @@
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.23": "patch:@ai-sdk/google@npm%3A2.0.23#~/.yarn/patches/@ai-sdk-google-npm-2.0.23-81682e07b0.patch",
|
||||
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-linux-arm": "0.34.3",
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3",
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
|
||||
"@img/sharp-win32-x64": "0.34.3"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
"ai": "^5.0.26"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.32",
|
||||
"@ai-sdk/azure": "^2.0.53",
|
||||
"@ai-sdk/anthropic": "^2.0.27",
|
||||
"@ai-sdk/azure": "^2.0.49",
|
||||
"@ai-sdk/deepseek": "^1.0.23",
|
||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
||||
"@ai-sdk/openai": "^2.0.48",
|
||||
"@ai-sdk/openai-compatible": "^1.0.22",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.12",
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createAzure } from '@ai-sdk/azure'
|
||||
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||
import { createHuggingFace } from '@ai-sdk/huggingface'
|
||||
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||
import { LanguageModelV2 } from '@ai-sdk/provider'
|
||||
@@ -29,8 +28,7 @@ export const baseProviderIds = [
|
||||
'azure',
|
||||
'azure-responses',
|
||||
'deepseek',
|
||||
'openrouter',
|
||||
'huggingface'
|
||||
'openrouter'
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -134,12 +132,6 @@ export const baseProviders = [
|
||||
name: 'OpenRouter',
|
||||
creator: createOpenRouter,
|
||||
supportsImageGeneration: true
|
||||
},
|
||||
{
|
||||
id: 'huggingface',
|
||||
name: 'HuggingFace',
|
||||
creator: createHuggingFace,
|
||||
supportsImageGeneration: true
|
||||
}
|
||||
] as const satisfies BaseProvider[]
|
||||
|
||||
|
||||
@@ -96,10 +96,6 @@ export enum IpcChannel {
|
||||
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
||||
AgentMessage_GetHistory = 'agent-message:get-history',
|
||||
|
||||
AgentToolPermission_Request = 'agent-tool-permission:request',
|
||||
AgentToolPermission_Response = 'agent-tool-permission:response',
|
||||
AgentToolPermission_Result = 'agent-tool-permission:result',
|
||||
|
||||
//copilot
|
||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||
@@ -195,6 +191,11 @@ export enum IpcChannel {
|
||||
File_StartWatcher = 'file:startWatcher',
|
||||
File_StopWatcher = 'file:stopWatcher',
|
||||
File_ShowInFolder = 'file:showInFolder',
|
||||
// Image export specific channels
|
||||
File_ReadBinary = 'file:readBinary',
|
||||
File_WriteBinary = 'file:writeBinary',
|
||||
File_CopyFile = 'file:copyFile',
|
||||
File_CreateDirectory = 'file:createDirectory',
|
||||
|
||||
// file service
|
||||
FileService_Upload = 'file-service:upload',
|
||||
@@ -354,14 +355,5 @@ export enum IpcChannel {
|
||||
Ovms_StopOVMS = 'ovms:stop-ovms',
|
||||
|
||||
// CherryAI
|
||||
Cherryai_GetSignature = 'cherryai:get-signature',
|
||||
|
||||
// Claude Code Plugins
|
||||
ClaudeCodePlugin_ListAvailable = 'claudeCodePlugin:list-available',
|
||||
ClaudeCodePlugin_Install = 'claudeCodePlugin:install',
|
||||
ClaudeCodePlugin_Uninstall = 'claudeCodePlugin:uninstall',
|
||||
ClaudeCodePlugin_ListInstalled = 'claudeCodePlugin:list-installed',
|
||||
ClaudeCodePlugin_InvalidateCache = 'claudeCodePlugin:invalidate-cache',
|
||||
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content'
|
||||
Cherryai_GetSignature = 'cherryai:get-signature'
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ Performance Optimization Recommendations:
|
||||
- For unstable services: MAX_CONCURRENT_TRANSLATIONS=2, TRANSLATION_DELAY_MS=500
|
||||
|
||||
Environment Variables:
|
||||
- TRANSLATION_BASE_LOCALE: Base locale for translation (default: 'en-us')
|
||||
- BASE_LOCALE: Base locale for translation (default: 'en-us')
|
||||
- TRANSLATION_BASE_URL: Custom API endpoint URL
|
||||
- TRANSLATION_MODEL: Custom translation model name
|
||||
*/
|
||||
@@ -258,7 +258,7 @@ const main = async () => {
|
||||
|
||||
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
|
||||
const baseLocale = process.env.TRANSLATION_BASE_LOCALE ?? 'en-us'
|
||||
const baseLocale = process.env.BASE_LOCALE ?? 'en-us'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseLocalePath = path.join(__dirname, '../src/renderer/src/i18n/locales', baseFileName)
|
||||
if (!fs.existsSync(baseLocalePath)) {
|
||||
@@ -284,7 +284,6 @@ const main = async () => {
|
||||
const translateFiles = getFiles(translateDir)
|
||||
const files = [...localeFiles, ...translateFiles]
|
||||
|
||||
console.info(`📂 Base Locale: ${baseLocale}`)
|
||||
console.info('📂 Files to translate:')
|
||||
files.forEach((filePath) => {
|
||||
const filename = path.basename(filePath, '.json')
|
||||
|
||||
@@ -1,635 +0,0 @@
|
||||
/**
|
||||
* Feishu (Lark) Webhook Notification Script for Pull Requests
|
||||
* Sends GitHub PR summaries to Feishu with @ mentions for reviewers and assignees
|
||||
*/
|
||||
|
||||
const crypto = require('crypto')
|
||||
const https = require('https')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* Generate Feishu webhook signature
|
||||
* @param {string} secret - Feishu webhook secret
|
||||
* @param {number} timestamp - Unix timestamp in seconds
|
||||
* @returns {string} Base64 encoded signature
|
||||
*/
|
||||
function generateSignature(secret, timestamp) {
|
||||
const stringToSign = `${timestamp}\n${secret}`
|
||||
const hmac = crypto.createHmac('sha256', stringToSign)
|
||||
return hmac.digest('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Feishu webhook
|
||||
* @param {string} webhookUrl - Feishu webhook URL
|
||||
* @param {string} secret - Feishu webhook secret
|
||||
* @param {object} content - Message content
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function sendToFeishu(webhookUrl, secret, content) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timestamp = Math.floor(Date.now() / 1000)
|
||||
const sign = generateSignature(secret, timestamp)
|
||||
|
||||
const payload = JSON.stringify({
|
||||
timestamp: timestamp.toString(),
|
||||
sign: sign,
|
||||
msg_type: 'interactive',
|
||||
card: content
|
||||
})
|
||||
|
||||
const url = new URL(webhookUrl)
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload)
|
||||
}
|
||||
}
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = ''
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk
|
||||
})
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
console.log('✅ Successfully sent to Feishu:', data)
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error(`Feishu API error: ${res.statusCode} - ${data}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
req.write(payload)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse user mapping from environment variable
|
||||
* Expected format: "github_user1:feishu_id1,github_user2:feishu_id2"
|
||||
* @param {string} mappingStr - User mapping string
|
||||
* @returns {Map<string, string>} Map of GitHub username to Feishu user ID
|
||||
*/
|
||||
function parseUserMapping(mappingStr) {
|
||||
const mapping = new Map()
|
||||
if (!mappingStr) {
|
||||
return mapping
|
||||
}
|
||||
|
||||
const pairs = mappingStr.split(',')
|
||||
for (const pair of pairs) {
|
||||
const [github, feishu] = pair.split(':').map((s) => s.trim())
|
||||
if (github && feishu) {
|
||||
mapping.set(github, feishu)
|
||||
}
|
||||
}
|
||||
return mapping
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PR category display info
|
||||
* @param {string} category - PR category
|
||||
* @returns {object} Category display info
|
||||
*/
|
||||
function getCategoryInfo(category) {
|
||||
const categoryMap = {
|
||||
chat: { emoji: '💬', name: '对话', color: 'blue' },
|
||||
draw: { emoji: '🖼️', name: '绘图', color: 'blue' },
|
||||
uiux: { emoji: '🎨', name: 'UI/UX', color: 'blue' },
|
||||
knowledge: { emoji: '🧠', name: '知识库', color: 'green' },
|
||||
agent: { emoji: '🕹️', name: 'Agent', color: 'turquoise' },
|
||||
provider: { emoji: '🔌', name: 'Provider', color: 'turquoise' },
|
||||
minapps: { emoji: '🧩', name: '小程序', color: 'turquoise' },
|
||||
backup_export: { emoji: '💾', name: '备份/导出', color: 'purple' },
|
||||
data_storage: { emoji: '🗄️', name: '数据与存储', color: 'purple' },
|
||||
ai_core: { emoji: '🤖', name: 'AI基础设施', color: 'purple' },
|
||||
backend: { emoji: '⚙️', name: '后端/平台', color: 'green' },
|
||||
docs: { emoji: '📚', name: '文档', color: 'grey' },
|
||||
'build-config': { emoji: '🔧', name: '构建/配置', color: 'orange' },
|
||||
test: { emoji: '🧪', name: '测试', color: 'yellow' },
|
||||
multiple: { emoji: '🔀', name: '多模块', color: 'red' },
|
||||
other: { emoji: '📝', name: '其他', color: 'blue' }
|
||||
}
|
||||
|
||||
return categoryMap[category] || categoryMap.other
|
||||
}
|
||||
|
||||
/**
|
||||
* Load GitHub reviewers per category from .github/pr-modules.yml (optional)
|
||||
* Supports inline array style: github_reviewers: ["user1","user2"] or []
|
||||
* @returns {Map<string, string[]>}
|
||||
*/
|
||||
function loadConfigGithubReviewersByCategory() {
|
||||
const result = new Map()
|
||||
result.__rules = { vendor_added: [], large_change: { changed_files_gt: 30, reviewers: [] } }
|
||||
try {
|
||||
const candidates = [
|
||||
path.join(process.cwd(), '.github', 'pr-modules.yml'),
|
||||
path.join(process.cwd(), '.github', 'pr-modules.yaml')
|
||||
]
|
||||
let filePath = null
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) {
|
||||
filePath = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!filePath) return result
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf8')
|
||||
const lines = content.split(/\r?\n/)
|
||||
let inCategories = false
|
||||
let inRules = false
|
||||
let currentCategory = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (!inCategories && !inRules) {
|
||||
if (/^categories:\s*$/.test(line)) {
|
||||
inCategories = true
|
||||
continue
|
||||
}
|
||||
if (/^rules:\s*$/.test(line)) {
|
||||
inRules = true
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (inCategories) {
|
||||
const catMatch = /^\s{2}([a-zA-Z0-9_-]+):\s*$/.exec(line)
|
||||
if (catMatch) {
|
||||
currentCategory = catMatch[1]
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentCategory) {
|
||||
const reviewersMatch = /^\s{4}github_reviewers:\s*(.*)$/.exec(line)
|
||||
if (reviewersMatch) {
|
||||
let value = (reviewersMatch[1] || '').trim()
|
||||
let users = []
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
const inner = value.slice(1, -1).trim()
|
||||
if (inner.length > 0) {
|
||||
users = inner
|
||||
.split(',')
|
||||
.map((s) => s.trim().replace(/^"|"$/g, '').replace(/^'|'$/g, ''))
|
||||
.filter(Boolean)
|
||||
}
|
||||
} else if (value === '' || value === '[]') {
|
||||
// try to parse dash list style
|
||||
const collected = []
|
||||
let j = i + 1
|
||||
while (j < lines.length) {
|
||||
const l = lines[j]
|
||||
const dash = /^\s{6}-\s*(["']?)([^"']*)\1\s*$/.exec(l)
|
||||
if (dash) {
|
||||
const user = dash[2].trim()
|
||||
if (user) collected.push(user)
|
||||
j++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
users = collected
|
||||
}
|
||||
result.set(currentCategory, Array.from(new Set(users)))
|
||||
}
|
||||
}
|
||||
} else if (inRules) {
|
||||
// vendor_added block
|
||||
if (/^\s{2}vendor_added:\s*$/.test(line)) {
|
||||
// parse github_reviewers under vendor_added
|
||||
let j = i + 1
|
||||
const reviewers = []
|
||||
while (j < lines.length) {
|
||||
const l = lines[j]
|
||||
const reviewersLine = /^\s{4}github_reviewers:\s*(.*)$/.exec(l)
|
||||
if (reviewersLine) {
|
||||
let value = (reviewersLine[1] || '').trim()
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
const inner = value.slice(1, -1).trim()
|
||||
if (inner.length > 0) {
|
||||
inner.split(',').forEach((s) => {
|
||||
const u = s.trim().replace(/^"|"$/g, '').replace(/^'|'$/g, '')
|
||||
if (u) reviewers.push(u)
|
||||
})
|
||||
}
|
||||
}
|
||||
j++
|
||||
continue
|
||||
}
|
||||
const dash = /^\s{6}-\s*(["']?)([^"']*)\1\s*$/.exec(l)
|
||||
if (dash) {
|
||||
const u = dash[2].trim()
|
||||
if (u) reviewers.push(u)
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if (/^\s{2}[a-zA-Z0-9_-]+:\s*$/.test(l)) break
|
||||
j++
|
||||
}
|
||||
result.__rules.vendor_added = Array.from(new Set(reviewers))
|
||||
}
|
||||
|
||||
// large_change block
|
||||
if (/^\s{2}large_change:\s*$/.test(line)) {
|
||||
let j = i + 1
|
||||
const rule = { changed_files_gt: 30, reviewers: [] }
|
||||
while (j < lines.length) {
|
||||
const l = lines[j]
|
||||
const threshold = /^\s{4}changed_files_gt:\s*(\d+)\s*$/.exec(l)
|
||||
if (threshold) {
|
||||
rule.changed_files_gt = parseInt(threshold[1], 10)
|
||||
j++
|
||||
continue
|
||||
}
|
||||
const reviewersLine = /^\s{4}github_reviewers:\s*(.*)$/.exec(l)
|
||||
if (reviewersLine) {
|
||||
let value = (reviewersLine[1] || '').trim()
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
const inner = value.slice(1, -1).trim()
|
||||
if (inner.length > 0) {
|
||||
inner.split(',').forEach((s) => {
|
||||
const u = s.trim().replace(/^"|"$/g, '').replace(/^'|'$/g, '')
|
||||
if (u) rule.reviewers.push(u)
|
||||
})
|
||||
}
|
||||
}
|
||||
j++
|
||||
continue
|
||||
}
|
||||
const dash = /^\s{6}-\s*(["']?)([^"']*)\1\s*$/.exec(l)
|
||||
if (dash) {
|
||||
const u = dash[2].trim()
|
||||
if (u) rule.reviewers.push(u)
|
||||
j++
|
||||
continue
|
||||
}
|
||||
if (/^\s{2}[a-zA-Z0-9_-]+:\s*$/.test(l)) break
|
||||
j++
|
||||
}
|
||||
rule.reviewers = Array.from(new Set(rule.reviewers))
|
||||
result.__rules.large_change = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Failed to load .github/pr-modules.yml:', e.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended reviewers based on PR category
|
||||
* This is a helper for Claude to suggest appropriate reviewers
|
||||
* @param {string} category - PR category
|
||||
* @param {Map<string, string>} userMapping - GitHub to Feishu user mapping
|
||||
* @returns {string[]} List of Feishu user IDs to notify
|
||||
*/
|
||||
function getRecommendedReviewersByCategory(category, userMapping, configGithubReviewersMap) {
|
||||
// Fallback mapping when config not provided
|
||||
const fallback = {
|
||||
backend: ['kangfenmao'],
|
||||
ai_core: ['kangfenmao'],
|
||||
'build-config': ['kangfenmao'],
|
||||
multiple: ['kangfenmao']
|
||||
}
|
||||
|
||||
const configUsers = (configGithubReviewersMap && configGithubReviewersMap.get(category)) || []
|
||||
const fallbackUsers = fallback[category] || []
|
||||
const githubUsers = Array.from(new Set([...configUsers, ...fallbackUsers]))
|
||||
return githubUsers.map((gh) => userMapping.get(gh)).filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Feishu card message from PR data
|
||||
* @param {object} prData - GitHub PR data
|
||||
* @param {Map<string, string>} userMapping - GitHub to Feishu user mapping
|
||||
* @returns {object} Feishu card content
|
||||
*/
|
||||
function createPRCard(prData, userMapping, configGithubReviewersMap) {
|
||||
const {
|
||||
prUrl,
|
||||
prNumber,
|
||||
prTitle,
|
||||
prSummary,
|
||||
prAuthor,
|
||||
labels,
|
||||
reviewers,
|
||||
assignees,
|
||||
category,
|
||||
changedFiles,
|
||||
additions,
|
||||
deletions,
|
||||
vendorAdded
|
||||
} = prData
|
||||
|
||||
const categoryInfo = getCategoryInfo(category)
|
||||
|
||||
// Build labels section
|
||||
const labelElements =
|
||||
labels && labels.length > 0
|
||||
? [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**🏷️ Labels:** ${labels.map((l) => `\`${l}\``).join(' ')}`
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
// Build stats section
|
||||
const statsContent = [
|
||||
`📁 ${changedFiles || 0} files`,
|
||||
`<font color='green'>+${additions || 0}</font>`,
|
||||
`<font color='red'>-${deletions || 0}</font>`
|
||||
].join(' · ')
|
||||
|
||||
// Build mention content for reviewers and assignees
|
||||
const mentions = []
|
||||
const mentionedUsers = new Set()
|
||||
|
||||
// Add reviewers
|
||||
if (reviewers && reviewers.length > 0) {
|
||||
reviewers.forEach((reviewer) => {
|
||||
const feishuId = userMapping.get(reviewer)
|
||||
if (feishuId && !mentionedUsers.has(feishuId)) {
|
||||
mentions.push(`<at id="${feishuId}"></at>`)
|
||||
mentionedUsers.add(feishuId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add assignees
|
||||
if (assignees && assignees.length > 0) {
|
||||
assignees.forEach((assignee) => {
|
||||
const feishuId = userMapping.get(assignee)
|
||||
if (feishuId && !mentionedUsers.has(feishuId)) {
|
||||
mentions.push(`<at id="${feishuId}"></at>`)
|
||||
mentionedUsers.add(feishuId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add category-based experts (if not already mentioned)
|
||||
const categoryExperts = getRecommendedReviewersByCategory(category, userMapping, configGithubReviewersMap)
|
||||
categoryExperts.forEach((feishuId) => {
|
||||
if (feishuId && !mentionedUsers.has(feishuId)) {
|
||||
mentions.push(`<at id="${feishuId}"></at>`)
|
||||
mentionedUsers.add(feishuId)
|
||||
}
|
||||
})
|
||||
|
||||
// Enforce mandatory reviewers based on rules
|
||||
const mandatoryGithubUsers = []
|
||||
const rules = configGithubReviewersMap.__rules || {
|
||||
vendor_added: [],
|
||||
large_change: { changed_files_gt: 30, reviewers: [] }
|
||||
}
|
||||
if (vendorAdded) {
|
||||
mandatoryGithubUsers.push(...(rules.vendor_added || ['Yinsen-Ho']))
|
||||
}
|
||||
const changedFilesNum = Number(changedFiles) || 0
|
||||
const threshold = (rules.large_change && rules.large_change.changed_files_gt) || 30
|
||||
if (changedFilesNum > threshold) {
|
||||
const reviewers = (rules.large_change && rules.large_change.reviewers) || ['kangfenmao']
|
||||
mandatoryGithubUsers.push(...reviewers)
|
||||
}
|
||||
|
||||
mandatoryGithubUsers.forEach((gh) => {
|
||||
const feishuId = userMapping.get(gh)
|
||||
if (feishuId && !mentionedUsers.has(feishuId)) {
|
||||
mentions.push(`<at id="${feishuId}"></at>`)
|
||||
mentionedUsers.add(feishuId)
|
||||
}
|
||||
})
|
||||
|
||||
// Build mentions section
|
||||
const mentionElements =
|
||||
mentions.length > 0
|
||||
? [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**👥 请关注:** ${mentions.join(' ')}`
|
||||
}
|
||||
}
|
||||
]
|
||||
: []
|
||||
|
||||
// Build reviewer and assignee info
|
||||
const reviewerInfo = []
|
||||
if (reviewers && reviewers.length > 0) {
|
||||
reviewerInfo.push({
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**👀 Reviewers:** ${reviewers.map((r) => `\`${r}\``).join(', ')}`
|
||||
}
|
||||
})
|
||||
}
|
||||
if (assignees && assignees.length > 0) {
|
||||
reviewerInfo.push({
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**👤 Assignees:** ${assignees.map((a) => `\`${a}\``).join(', ')}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
elements: [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**🔀 New Pull Request #${prNumber}**`
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'hr'
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**${categoryInfo.emoji} 类型:** ${categoryInfo.name}`
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**📝 Title:** ${prTitle}`
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**👤 Author:** \`${prAuthor}\``
|
||||
}
|
||||
},
|
||||
...reviewerInfo,
|
||||
...labelElements,
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**📊 Changes:** ${statsContent}`
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'hr'
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
tag: 'lark_md',
|
||||
content: `**📋 Summary:**\n${prSummary}`
|
||||
}
|
||||
},
|
||||
...mentionElements,
|
||||
{
|
||||
tag: 'hr'
|
||||
},
|
||||
{
|
||||
tag: 'action',
|
||||
actions: [
|
||||
{
|
||||
tag: 'button',
|
||||
text: {
|
||||
tag: 'plain_text',
|
||||
content: '🔗 View PR'
|
||||
},
|
||||
type: 'primary',
|
||||
url: prUrl
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
header: {
|
||||
template: categoryInfo.color,
|
||||
title: {
|
||||
tag: 'plain_text',
|
||||
content: `${categoryInfo.emoji} Cherry Studio - New PR [${categoryInfo.name}]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
try {
|
||||
// Get environment variables
|
||||
const webhookUrl = process.env.FEISHU_WEBHOOK_URL
|
||||
const secret = process.env.FEISHU_WEBHOOK_SECRET
|
||||
const userMappingStr = process.env.FEISHU_USER_MAPPING || ''
|
||||
|
||||
const prUrl = process.env.PR_URL
|
||||
const prNumber = process.env.PR_NUMBER
|
||||
const prTitle = process.env.PR_TITLE
|
||||
const prSummary = process.env.PR_SUMMARY
|
||||
const prAuthor = process.env.PR_AUTHOR
|
||||
const labelsStr = process.env.PR_LABELS || ''
|
||||
const reviewersStr = process.env.PR_REVIEWERS || ''
|
||||
const assigneesStr = process.env.PR_ASSIGNEES || ''
|
||||
const category = process.env.PR_CATEGORY || 'multiple'
|
||||
const vendorAdded = String(process.env.PR_VENDOR_ADDED || 'false').toLowerCase() === 'true'
|
||||
const changedFiles = process.env.PR_CHANGED_FILES || '0'
|
||||
const additions = process.env.PR_ADDITIONS || '0'
|
||||
const deletions = process.env.PR_DELETIONS || '0'
|
||||
|
||||
// Validate required environment variables
|
||||
if (!webhookUrl) {
|
||||
throw new Error('FEISHU_WEBHOOK_URL environment variable is required')
|
||||
}
|
||||
if (!secret) {
|
||||
throw new Error('FEISHU_WEBHOOK_SECRET environment variable is required')
|
||||
}
|
||||
if (!prUrl || !prNumber || !prTitle || !prSummary) {
|
||||
throw new Error('PR data environment variables are required')
|
||||
}
|
||||
|
||||
// Parse data
|
||||
const userMapping = parseUserMapping(userMappingStr)
|
||||
const configGithubReviewersMap = loadConfigGithubReviewersByCategory()
|
||||
|
||||
const labels = labelsStr
|
||||
? labelsStr
|
||||
.split(',')
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
const reviewers = reviewersStr
|
||||
? reviewersStr
|
||||
.split(',')
|
||||
.map((r) => r.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
const assignees = assigneesStr
|
||||
? assigneesStr
|
||||
.split(',')
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
// Create PR data object
|
||||
const prData = {
|
||||
prUrl,
|
||||
prNumber,
|
||||
prTitle,
|
||||
prSummary,
|
||||
prAuthor: prAuthor || 'Unknown',
|
||||
labels,
|
||||
reviewers,
|
||||
assignees,
|
||||
category,
|
||||
vendorAdded,
|
||||
changedFiles,
|
||||
additions,
|
||||
deletions
|
||||
}
|
||||
|
||||
console.log('📤 Sending PR notification to Feishu...')
|
||||
console.log(`PR #${prNumber}: ${prTitle}`)
|
||||
console.log(`Category: ${category}`)
|
||||
console.log(`Vendor added: ${vendorAdded}`)
|
||||
console.log(`Reviewers: ${reviewers.join(', ') || 'None'}`)
|
||||
console.log(`Assignees: ${assignees.join(', ') || 'None'}`)
|
||||
console.log(`User mapping entries: ${userMapping.size}`)
|
||||
|
||||
// Create card content
|
||||
const card = createPRCard(prData, userMapping, configGithubReviewersMap)
|
||||
|
||||
// Send to Feishu
|
||||
await sendToFeishu(webhookUrl, secret, card)
|
||||
|
||||
console.log('✅ PR notification sent successfully!')
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run main function
|
||||
main()
|
||||
@@ -1,277 +0,0 @@
|
||||
/**
|
||||
* Stats major contributors per module based on .github/pr-modules.yml
|
||||
* Output a markdown summary and write JSON to .github/reviewer-suggestions.json
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/stats-contributors.js [--top 3] [--since 1.year] [--mode auto|shortlog|log|blame] [--blame-sample 30]
|
||||
*/
|
||||
|
||||
const { spawnSync } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
function readText(file) {
|
||||
try {
|
||||
return fs.readFileSync(file, 'utf8')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2)
|
||||
const out = { top: 3, since: '', mode: 'auto', blameSample: 30 }
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--top' && i + 1 < args.length) {
|
||||
out.top = parseInt(args[++i], 10) || 3
|
||||
} else if (args[i] === '--since' && i + 1 < args.length) {
|
||||
out.since = String(args[++i])
|
||||
} else if (args[i] === '--mode' && i + 1 < args.length) {
|
||||
out.mode = String(args[++i])
|
||||
} else if (args[i] === '--blame-sample' && i + 1 < args.length) {
|
||||
out.blameSample = parseInt(args[++i], 10) || 30
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Minimal YAML parser for categories/globs in .github/pr-modules.yml
|
||||
function parseModulesConfig(configPath) {
|
||||
const text = readText(configPath)
|
||||
if (!text) throw new Error(`Cannot read ${configPath}`)
|
||||
const lines = text.split(/\r?\n/)
|
||||
const categories = []
|
||||
let inCategories = false
|
||||
let current = null
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (!inCategories) {
|
||||
if (/^categories:\s*$/.test(line)) inCategories = true
|
||||
continue
|
||||
}
|
||||
|
||||
// New category key
|
||||
const catMatch = /^\s{2}([a-zA-Z0-9_-]+):\s*$/.exec(line)
|
||||
if (catMatch) {
|
||||
if (current) categories.push(current)
|
||||
current = { key: catMatch[1], name: '', globs: [] }
|
||||
continue
|
||||
}
|
||||
|
||||
if (!current) continue
|
||||
|
||||
const nameMatch = /^\s{4}name:\s*"?([^"]+)"?\s*$/.exec(line)
|
||||
if (nameMatch) {
|
||||
current.name = nameMatch[1].trim()
|
||||
continue
|
||||
}
|
||||
|
||||
// Enter globs list, then collect dash items
|
||||
const globsHeader = /^\s{4}globs:\s*$/.exec(line)
|
||||
if (globsHeader) {
|
||||
let j = i + 1
|
||||
while (j < lines.length) {
|
||||
const l = lines[j]
|
||||
const item = /^\s{6}-\s*"?([^"]+)"?\s*$/.exec(l)
|
||||
if (!item) break
|
||||
current.globs.push(item[1].trim())
|
||||
j++
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (current) categories.push(current)
|
||||
return categories
|
||||
}
|
||||
|
||||
function git(args, cwd) {
|
||||
const res = spawnSync('git', args, { cwd, encoding: 'utf8' })
|
||||
if (res.status !== 0) {
|
||||
const msg = (res.stderr || '').trim() || `git ${args.join(' ')} failed`
|
||||
throw new Error(msg)
|
||||
}
|
||||
return res.stdout
|
||||
}
|
||||
|
||||
function buildPathspecs(globs) {
|
||||
// Use pathspec magic :(glob)pattern so that ** works and we avoid shell expansion
|
||||
return globs.map((g) => `:(glob)${g}`)
|
||||
}
|
||||
|
||||
function lsFilesForGlobs(globs, repoRoot) {
|
||||
const pathspecs = buildPathspecs(globs)
|
||||
if (pathspecs.length === 0) return []
|
||||
try {
|
||||
const stdout = git(['ls-files', '--', ...pathspecs], repoRoot)
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
} catch (e) {
|
||||
// No matched files or pathspec error → treat as empty
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function shortlogFor(globs, repoRoot, since) {
|
||||
const files = lsFilesForGlobs(globs, repoRoot)
|
||||
if (files.length === 0) return []
|
||||
const base = ['shortlog', '-sne']
|
||||
if (since) base.push(`--since=${since}`)
|
||||
const stdout = git([...base, '--', ...files], repoRoot)
|
||||
const lines = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
const rows = []
|
||||
for (const l of lines) {
|
||||
// e.g. " 42 John Doe <john@example.com>"
|
||||
const m = /^(\d+)\s+(.+?)\s+<([^>]+)>$/.exec(l)
|
||||
if (!m) continue
|
||||
const commits = parseInt(m[1], 10)
|
||||
const name = m[2]
|
||||
const email = m[3]
|
||||
const gh = extractGithubUsername(name, email)
|
||||
rows.push({ commits, name, email, github: gh })
|
||||
}
|
||||
rows.sort((a, b) => b.commits - a.commits)
|
||||
return rows
|
||||
}
|
||||
|
||||
function logAuthorsFor(globs, repoRoot, since) {
|
||||
const files = lsFilesForGlobs(globs, repoRoot)
|
||||
if (files.length === 0) return []
|
||||
const base = ['log', '--format=%an <%ae>']
|
||||
if (since) base.push(`--since=${since}`)
|
||||
const stdout = git([...base, '--', ...files], repoRoot)
|
||||
const lines = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
const map = new Map()
|
||||
for (const l of lines) {
|
||||
const m = /^(.+?)\s+<([^>]+)>$/.exec(l)
|
||||
if (!m) continue
|
||||
const name = m[1]
|
||||
const email = m[2]
|
||||
const gh = extractGithubUsername(name, email)
|
||||
const key = `${name} <${email}>`
|
||||
map.set(key, (map.get(key) || 0) + 1)
|
||||
}
|
||||
const out = []
|
||||
for (const [key, commits] of map.entries()) {
|
||||
const m = /^(.+?)\s+<([^>]+)>$/.exec(key)
|
||||
out.push({ commits, name: m[1], email: m[2], github: extractGithubUsername(m[1], m[2]) })
|
||||
}
|
||||
out.sort((a, b) => b.commits - a.commits)
|
||||
return out
|
||||
}
|
||||
|
||||
function blameAuthorsSample(globs, repoRoot, sample) {
|
||||
const files = lsFilesForGlobs(globs, repoRoot)
|
||||
if (files.length === 0) return []
|
||||
const pick = files.slice(0, Math.max(1, sample))
|
||||
const map = new Map()
|
||||
for (const f of pick) {
|
||||
let stdout = ''
|
||||
try {
|
||||
stdout = git(['blame', '--line-porcelain', '--', f], repoRoot)
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
const lines = stdout.split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
// author and author-mail lines
|
||||
const am = /^author-mail\s+<([^>]+)>$/.exec(line)
|
||||
if (am) {
|
||||
const email = am[1]
|
||||
// We do not rely on index; we just keep email-based identity
|
||||
const gh = extractGithubUsername('', email)
|
||||
const key = `${gh || ''}<${email}>`
|
||||
map.set(key, (map.get(key) || 0) + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
const out = []
|
||||
for (const [key, commits] of map.entries()) {
|
||||
const m = /^(.*?)<([^>]+)>$/.exec(key)
|
||||
const email = m ? m[2] : ''
|
||||
const gh = extractGithubUsername('', email)
|
||||
out.push({ commits, name: gh || email, email, github: gh })
|
||||
}
|
||||
out.sort((a, b) => b.commits - a.commits)
|
||||
return out
|
||||
}
|
||||
|
||||
function extractGithubUsername(name, email) {
|
||||
// Try noreply forms: 12345+user@users.noreply.github.com or user@users.noreply.github.com
|
||||
const noreply = /^(?:\d+\+)?([A-Za-z0-9-]+)@users\.noreply\.github\.com$/.exec(email)
|
||||
if (noreply) return noreply[1]
|
||||
// If name itself looks like a probable GitHub handle
|
||||
if (/^[A-Za-z0-9-]{3,}$/.test(name)) return name
|
||||
return ''
|
||||
}
|
||||
|
||||
function main() {
|
||||
const repoRoot = process.cwd()
|
||||
const { top, since, mode, blameSample } = parseArgs()
|
||||
const configPath = path.join(repoRoot, '.github', 'pr-modules.yml')
|
||||
const categories = parseModulesConfig(configPath)
|
||||
|
||||
const suggestions = {}
|
||||
const markdownLines = []
|
||||
markdownLines.push('| Module | Top Contributors (commits) |')
|
||||
markdownLines.push('|---|---|')
|
||||
|
||||
for (const cat of categories) {
|
||||
let rows = []
|
||||
try {
|
||||
if (mode === 'shortlog' || mode === 'auto') rows = shortlogFor(cat.globs, repoRoot, since)
|
||||
if (rows.length === 0 && (mode === 'log' || mode === 'auto')) rows = logAuthorsFor(cat.globs, repoRoot, since)
|
||||
if (rows.length === 0 && (mode === 'blame' || mode === 'auto'))
|
||||
rows = blameAuthorsSample(cat.globs, repoRoot, blameSample)
|
||||
} catch (e) {
|
||||
// Fallback to next method if one fails
|
||||
if (mode === 'auto') {
|
||||
try {
|
||||
rows = logAuthorsFor(cat.globs, repoRoot, since)
|
||||
} catch (e2) {
|
||||
// ignore and continue
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
try {
|
||||
rows = blameAuthorsSample(cat.globs, repoRoot, blameSample)
|
||||
} catch (e3) {
|
||||
// ignore and continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-auto mode: report empty on error
|
||||
rows = []
|
||||
}
|
||||
}
|
||||
const topRows = rows.slice(0, top)
|
||||
suggestions[cat.key] = topRows.map((r) => ({
|
||||
github: r.github,
|
||||
name: r.name,
|
||||
email: r.email,
|
||||
commits: r.commits
|
||||
}))
|
||||
const cell = topRows
|
||||
.map((r) => {
|
||||
const id = r.github ? `@${r.github}` : r.name
|
||||
return `${id} (${r.commits})`
|
||||
})
|
||||
.join(', ')
|
||||
markdownLines.push(`| ${cat.key} | ${cell || '-'} |`)
|
||||
}
|
||||
|
||||
const outJsonPath = path.join(repoRoot, '.github', 'reviewer-suggestions.json')
|
||||
fs.writeFileSync(outJsonPath, JSON.stringify({ generatedAt: new Date().toISOString(), suggestions }, null, 2))
|
||||
|
||||
console.log(markdownLines.join('\n'))
|
||||
console.log(`\nSaved JSON: ${path.relative(repoRoot, outJsonPath)}`)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -5,7 +5,7 @@ import { sortedObjectByKeys } from './sort'
|
||||
|
||||
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
|
||||
const baseLocale = process.env.TRANSLATION_BASE_LOCALE ?? 'en-us'
|
||||
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseFilePath = path.join(localesDir, baseFileName)
|
||||
|
||||
@@ -13,45 +13,45 @@ type I18NValue = string | { [key: string]: I18NValue }
|
||||
type I18N = { [key: string]: I18NValue }
|
||||
|
||||
/**
|
||||
* Recursively sync target object to match template object structure
|
||||
* 1. Add keys that exist in template but missing in target (with '[to be translated]')
|
||||
* 2. Remove keys that exist in target but not in template
|
||||
* 3. Recursively sync nested objects
|
||||
* 递归同步 target 对象,使其与 template 对象保持一致
|
||||
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||||
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||||
* 3. 对于子对象,递归同步
|
||||
*
|
||||
* @param target Target object (language object to be updated)
|
||||
* @param template Base locale object (Chinese)
|
||||
* @returns Returns whether target was updated
|
||||
* @param target 目标对象(需要更新的语言对象)
|
||||
* @param template 主模板对象(中文)
|
||||
* @returns 返回是否对 target 进行了更新
|
||||
*/
|
||||
function syncRecursively(target: I18N, template: I18N): void {
|
||||
// Add keys that exist in template but missing in target
|
||||
// 添加 template 中存在但 target 中缺少的 key
|
||||
for (const key in template) {
|
||||
if (!(key in target)) {
|
||||
target[key] =
|
||||
typeof template[key] === 'object' && template[key] !== null ? {} : `[to be translated]:${template[key]}`
|
||||
console.log(`Added new property: ${key}`)
|
||||
console.log(`添加新属性:${key}`)
|
||||
}
|
||||
if (typeof template[key] === 'object' && template[key] !== null) {
|
||||
if (typeof target[key] !== 'object' || target[key] === null) {
|
||||
target[key] = {}
|
||||
}
|
||||
// Recursively sync nested objects
|
||||
// 递归同步子对象
|
||||
syncRecursively(target[key], template[key])
|
||||
}
|
||||
}
|
||||
|
||||
// Remove keys that exist in target but not in template
|
||||
// 删除 target 中存在但 template 中没有的 key
|
||||
for (const targetKey in target) {
|
||||
if (!(targetKey in template)) {
|
||||
console.log(`Removed excess property: ${targetKey}`)
|
||||
console.log(`移除多余属性:${targetKey}`)
|
||||
delete target[targetKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check JSON object for duplicate keys and collect all duplicates
|
||||
* @param obj Object to check
|
||||
* @returns Returns array of duplicate keys (empty array if no duplicates)
|
||||
* 检查 JSON 对象中是否存在重复键,并收集所有重复键
|
||||
* @param obj 要检查的对象
|
||||
* @returns 返回重复键的数组(若无重复则返回空数组)
|
||||
*/
|
||||
function checkDuplicateKeys(obj: I18N): string[] {
|
||||
const keys = new Set<string>()
|
||||
@@ -62,7 +62,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
|
||||
const fullPath = path ? `${path}.${key}` : key
|
||||
|
||||
if (keys.has(fullPath)) {
|
||||
// When duplicate key found, add to array (avoid duplicate additions)
|
||||
// 发现重复键时,添加到数组中(避免重复添加)
|
||||
if (!duplicateKeys.includes(fullPath)) {
|
||||
duplicateKeys.push(fullPath)
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
|
||||
keys.add(fullPath)
|
||||
}
|
||||
|
||||
// Recursively check nested objects
|
||||
// 递归检查子对象
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
checkObject(obj[key], fullPath)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
|
||||
|
||||
function syncTranslations() {
|
||||
if (!fs.existsSync(baseFilePath)) {
|
||||
console.error(`Base locale file ${baseFileName} does not exist, please check path or filename`)
|
||||
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,24 +92,24 @@ function syncTranslations() {
|
||||
try {
|
||||
baseJson = JSON.parse(baseContent)
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${baseFileName}. ${error}`)
|
||||
console.error(`解析 ${baseFileName} 出错。${error}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if base locale has duplicate keys
|
||||
// 检查主模板是否存在重复键
|
||||
const duplicateKeys = checkDuplicateKeys(baseJson)
|
||||
if (duplicateKeys.length > 0) {
|
||||
throw new Error(`Base locale file ${baseFileName} has the following duplicate keys:\n${duplicateKeys.join('\n')}`)
|
||||
throw new Error(`主模板文件 ${baseFileName} 存在以下重复键:\n${duplicateKeys.join('\n')}`)
|
||||
}
|
||||
|
||||
// Sort base locale
|
||||
// 为主模板排序
|
||||
const sortedJson = sortedObjectByKeys(baseJson)
|
||||
if (JSON.stringify(baseJson) !== JSON.stringify(sortedJson)) {
|
||||
try {
|
||||
fs.writeFileSync(baseFilePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
|
||||
console.log(`Base locale has been sorted`)
|
||||
console.log(`主模板已排序`)
|
||||
} catch (error) {
|
||||
console.error(`Error writing ${baseFilePath}.`, error)
|
||||
console.error(`写入 ${baseFilePath} 出错。`, error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ function syncTranslations() {
|
||||
.map((filename) => path.join(translateDir, filename))
|
||||
const files = [...localeFiles, ...translateFiles]
|
||||
|
||||
// Sync keys
|
||||
// 同步键
|
||||
for (const filePath of files) {
|
||||
const filename = path.basename(filePath)
|
||||
let targetJson: I18N = {}
|
||||
@@ -132,7 +132,7 @@ function syncTranslations() {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
targetJson = JSON.parse(fileContent)
|
||||
} catch (error) {
|
||||
console.error(`Error parsing ${filename}, skipping this file.`, error)
|
||||
console.error(`解析 ${filename} 出错,跳过此文件。`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -142,9 +142,9 @@ function syncTranslations() {
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
|
||||
console.log(`File ${filename} has been sorted and synced to match base locale content`)
|
||||
console.log(`文件 ${filename} 已排序并同步更新为主模板的内容`)
|
||||
} catch (error) {
|
||||
console.error(`Error writing ${filename}. ${error}`)
|
||||
console.error(`写入 ${filename} 出错。${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
144
src/main/ipc.ts
144
src/main/ipc.ts
@@ -11,7 +11,6 @@ import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import type { PluginError } from '@types'
|
||||
import {
|
||||
AgentPersistedMessage,
|
||||
FileMetadata,
|
||||
@@ -47,7 +46,6 @@ import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ocrService } from './services/ocr/OcrService'
|
||||
import OvmsManager from './services/OvmsManager'
|
||||
import { PluginService } from './services/PluginService'
|
||||
import { proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||
@@ -95,18 +93,6 @@ const vertexAIService = VertexAIService.getInstance()
|
||||
const memoryService = MemoryService.getInstance()
|
||||
const dxtService = new DxtService()
|
||||
const ovmsManager = new OvmsManager()
|
||||
const pluginService = PluginService.getInstance()
|
||||
|
||||
function normalizeError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
function extractPluginError(error: unknown): PluginError | null {
|
||||
if (error && typeof error === 'object' && 'type' in error && typeof (error as { type: unknown }).type === 'string') {
|
||||
return error as PluginError
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater()
|
||||
@@ -545,6 +531,23 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.File_StopWatcher, fileManager.stopFileWatcher.bind(fileManager))
|
||||
ipcMain.handle(IpcChannel.File_ShowInFolder, fileManager.showInFolder.bind(fileManager))
|
||||
|
||||
// Image export specific handlers
|
||||
ipcMain.handle(IpcChannel.File_ReadBinary, async (_, filePath: string) => {
|
||||
return fs.promises.readFile(filePath)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.File_WriteBinary, async (_, filePath: string, buffer: Buffer) => {
|
||||
return fs.promises.writeFile(filePath, buffer)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.File_CopyFile, async (_, sourcePath: string, destPath: string) => {
|
||||
return fs.promises.copyFile(sourcePath, destPath)
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.File_CreateDirectory, async (_, dirPath: string) => {
|
||||
return fs.promises.mkdir(dirPath, { recursive: true })
|
||||
})
|
||||
|
||||
// file service
|
||||
ipcMain.handle(IpcChannel.FileService_Upload, async (_, provider: Provider, file: FileMetadata) => {
|
||||
const service = FileServiceManager.getInstance().getService(provider)
|
||||
@@ -904,117 +907,4 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// CherryAI
|
||||
ipcMain.handle(IpcChannel.Cherryai_GetSignature, (_, params) => generateSignature(params))
|
||||
|
||||
// Claude Code Plugins
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListAvailable, async () => {
|
||||
try {
|
||||
const data = await pluginService.listAvailable()
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list available plugins', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list available plugins', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-available',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Install, async (_, options) => {
|
||||
try {
|
||||
const data = await pluginService.install(options)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to install plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_Uninstall, async (_, options) => {
|
||||
try {
|
||||
await pluginService.uninstall(options)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to uninstall plugin', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ListInstalled, async (_, agentId: string) => {
|
||||
try {
|
||||
const data = await pluginService.listInstalled(agentId)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to list installed plugins', { agentId, error: pluginError })
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to list installed plugins', { agentId, error: err })
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'list-installed',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_InvalidateCache, async () => {
|
||||
try {
|
||||
pluginService.invalidateCache()
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
const pluginError = extractPluginError(error)
|
||||
if (pluginError) {
|
||||
logger.error('Failed to invalidate plugin cache', pluginError)
|
||||
return { success: false, error: pluginError }
|
||||
}
|
||||
|
||||
const err = normalizeError(error)
|
||||
logger.error('Failed to invalidate plugin cache', err)
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type: 'TRANSACTION_FAILED',
|
||||
operation: 'invalidate-cache',
|
||||
reason: err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_ReadContent, async (_, sourcePath: string) => {
|
||||
try {
|
||||
const data = await pluginService.readContent(sourcePath)
|
||||
return { success: true, data }
|
||||
} catch (error) {
|
||||
logger.error('Failed to read plugin content', { sourcePath, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannel.ClaudeCodePlugin_WriteContent, async (_, options) => {
|
||||
try {
|
||||
await pluginService.writeContent(options.agentId, options.filename, options.type, options.content)
|
||||
return { success: true, data: undefined }
|
||||
} catch (error) {
|
||||
logger.error('Failed to write plugin content', { options, error })
|
||||
return { success: false, error }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
|
||||
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
|
||||
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
|
||||
import { ApiClient } from '@types'
|
||||
|
||||
import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
@@ -8,7 +9,7 @@ import { VoyageEmbeddings } from './VoyageEmbeddings'
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ embedApiClient, dimensions }: { embedApiClient: ApiClient; dimensions?: number }): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
const { model, provider, apiKey, baseURL } = embedApiClient
|
||||
const { model, provider, apiKey, apiVersion, baseURL } = embedApiClient
|
||||
if (provider === 'voyageai') {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
@@ -37,7 +38,16 @@ export default class EmbeddingsFactory {
|
||||
}
|
||||
})
|
||||
}
|
||||
// NOTE: Azure OpenAI 也走 OpenAIEmbeddings, baseURL是https://xxxx.openai.azure.com/openai/v1
|
||||
if (apiVersion !== undefined) {
|
||||
return new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIEndpoint: baseURL,
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { fileStorage } from '@main/services/FileStorage'
|
||||
import { FileMetadata, PreprocessProvider } from '@types'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { net } from 'electron'
|
||||
import FormData from 'form-data'
|
||||
|
||||
import BasePreprocessProvider from './BasePreprocessProvider'
|
||||
|
||||
const logger = loggerService.withContext('MineruPreprocessProvider')
|
||||
|
||||
export default class OpenMineruPreprocessProvider extends BasePreprocessProvider {
|
||||
constructor(provider: PreprocessProvider, userId?: string) {
|
||||
super(provider, userId)
|
||||
}
|
||||
|
||||
public async parseFile(
|
||||
sourceId: string,
|
||||
file: FileMetadata
|
||||
): Promise<{ processedFile: FileMetadata; quota: number }> {
|
||||
try {
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
logger.info(`Open MinerU preprocess processing started: ${filePath}`)
|
||||
await this.validateFile(filePath)
|
||||
|
||||
// 1. Update progress
|
||||
await this.sendPreprocessProgress(sourceId, 50)
|
||||
logger.info(`File ${file.name} is starting processing...`)
|
||||
|
||||
// 2. Upload file and extract
|
||||
const { path: outputPath } = await this.uploadFileAndExtract(file)
|
||||
|
||||
// 3. Check quota
|
||||
const quota = await this.checkQuota()
|
||||
|
||||
// 4. Create processed file info
|
||||
return {
|
||||
processedFile: this.createProcessedFileInfo(file, outputPath),
|
||||
quota
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Open MinerU preprocess processing failed for:`, error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async checkQuota() {
|
||||
// self-hosted version always has enough quota
|
||||
return Infinity
|
||||
}
|
||||
|
||||
private async validateFile(filePath: string): Promise<void> {
|
||||
const pdfBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const doc = await this.readPdf(pdfBuffer)
|
||||
|
||||
// File page count must be less than 600 pages
|
||||
if (doc.numPages >= 600) {
|
||||
throw new Error(`PDF page count (${doc.numPages}) exceeds the limit of 600 pages`)
|
||||
}
|
||||
// File size must be less than 200MB
|
||||
if (pdfBuffer.length >= 200 * 1024 * 1024) {
|
||||
const fileSizeMB = Math.round(pdfBuffer.length / (1024 * 1024))
|
||||
throw new Error(`PDF file size (${fileSizeMB}MB) exceeds the limit of 200MB`)
|
||||
}
|
||||
}
|
||||
|
||||
private createProcessedFileInfo(file: FileMetadata, outputPath: string): FileMetadata {
|
||||
// Find the main file after extraction
|
||||
let finalPath = ''
|
||||
let finalName = file.origin_name.replace('.pdf', '.md')
|
||||
// Find the corresponding folder by file name
|
||||
outputPath = path.join(outputPath, `${file.origin_name.replace('.pdf', '')}`)
|
||||
try {
|
||||
const files = fs.readdirSync(outputPath)
|
||||
|
||||
const mdFile = files.find((f) => f.endsWith('.md'))
|
||||
if (mdFile) {
|
||||
const originalMdPath = path.join(outputPath, mdFile)
|
||||
const newMdPath = path.join(outputPath, finalName)
|
||||
|
||||
// Rename file to original file name
|
||||
try {
|
||||
fs.renameSync(originalMdPath, newMdPath)
|
||||
finalPath = newMdPath
|
||||
logger.info(`Renamed markdown file from ${mdFile} to ${finalName}`)
|
||||
} catch (renameError) {
|
||||
logger.warn(`Failed to rename file ${mdFile} to ${finalName}: ${renameError}`)
|
||||
// If rename fails, use the original file
|
||||
finalPath = originalMdPath
|
||||
finalName = mdFile
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to read output directory ${outputPath}:`, error as Error)
|
||||
finalPath = path.join(outputPath, `${file.id}.md`)
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
name: finalName,
|
||||
path: finalPath,
|
||||
ext: '.md',
|
||||
size: fs.existsSync(finalPath) ? fs.statSync(finalPath).size : 0
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFileAndExtract(
|
||||
file: FileMetadata,
|
||||
maxRetries: number = 5,
|
||||
intervalMs: number = 5000
|
||||
): Promise<{ path: string }> {
|
||||
let retries = 0
|
||||
|
||||
const endpoint = `${this.provider.apiHost}/file_parse`
|
||||
|
||||
// Get file stream
|
||||
const filePath = fileStorage.getFilePathById(file)
|
||||
const fileBuffer = await fs.promises.readFile(filePath)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('return_md', 'true')
|
||||
formData.append('response_format_zip', 'true')
|
||||
formData.append('files', fileBuffer, {
|
||||
filename: file.origin_name
|
||||
})
|
||||
|
||||
while (retries < maxRetries) {
|
||||
let zipPath: string | undefined
|
||||
|
||||
try {
|
||||
const response = await net.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
token: this.userId ?? '',
|
||||
...(this.provider.apiKey ? { Authorization: `Bearer ${this.provider.apiKey}` } : {}),
|
||||
...formData.getHeaders()
|
||||
},
|
||||
body: formData.getBuffer()
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Check if response header is application/zip
|
||||
if (response.headers.get('content-type') !== 'application/zip') {
|
||||
throw new Error(`Downloaded ZIP file has unexpected content-type: ${response.headers.get('content-type')}`)
|
||||
}
|
||||
|
||||
const dirPath = this.storageDir
|
||||
|
||||
zipPath = path.join(dirPath, `${file.id}.zip`)
|
||||
const extractPath = path.join(dirPath, `${file.id}`)
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
fs.writeFileSync(zipPath, Buffer.from(arrayBuffer))
|
||||
logger.info(`Downloaded ZIP file: ${zipPath}`)
|
||||
|
||||
// Ensure extraction directory exists
|
||||
if (!fs.existsSync(extractPath)) {
|
||||
fs.mkdirSync(extractPath, { recursive: true })
|
||||
}
|
||||
|
||||
// Extract files
|
||||
const zip = new AdmZip(zipPath)
|
||||
zip.extractAllTo(extractPath, true)
|
||||
logger.info(`Extracted files to: ${extractPath}`)
|
||||
|
||||
return { path: extractPath }
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to upload and extract file: ${(error as Error).message}, retry ${retries + 1}/${maxRetries}`
|
||||
)
|
||||
if (retries === maxRetries - 1) {
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
// Delete temporary ZIP file
|
||||
if (zipPath && fs.existsSync(zipPath)) {
|
||||
try {
|
||||
fs.unlinkSync(zipPath)
|
||||
logger.info(`Deleted temporary ZIP file: ${zipPath}`)
|
||||
} catch (deleteError) {
|
||||
logger.warn(`Failed to delete temporary ZIP file ${zipPath}:`, deleteError as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retries++
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalMs))
|
||||
}
|
||||
|
||||
throw new Error(`Processing timeout for file: ${file.id}`)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import DefaultPreprocessProvider from './DefaultPreprocessProvider'
|
||||
import Doc2xPreprocessProvider from './Doc2xPreprocessProvider'
|
||||
import MineruPreprocessProvider from './MineruPreprocessProvider'
|
||||
import MistralPreprocessProvider from './MistralPreprocessProvider'
|
||||
import OpenMineruPreprocessProvider from './OpenMineruPreprocessProvider'
|
||||
export default class PreprocessProviderFactory {
|
||||
static create(provider: PreprocessProvider, userId?: string): BasePreprocessProvider {
|
||||
switch (provider.id) {
|
||||
@@ -15,8 +14,6 @@ export default class PreprocessProviderFactory {
|
||||
return new MistralPreprocessProvider(provider)
|
||||
case 'mineru':
|
||||
return new MineruPreprocessProvider(provider, userId)
|
||||
case 'open-mineru':
|
||||
return new OpenMineruPreprocessProvider(provider, userId)
|
||||
default:
|
||||
return new DefaultPreprocessProvider(provider)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
import { CanUseTool, McpHttpServerConfig, Options, query, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { McpHttpServerConfig, Options, query, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { config as apiConfigService } from '@main/apiServer/config'
|
||||
import { validateModelId } from '@main/apiServer/utils'
|
||||
@@ -11,23 +11,10 @@ import { app } from 'electron'
|
||||
|
||||
import { GetAgentSessionResponse } from '../..'
|
||||
import { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||
import { promptForToolApproval } from './tool-permissions'
|
||||
import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform'
|
||||
|
||||
const require_ = createRequire(import.meta.url)
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
const DEFAULT_AUTO_ALLOW_TOOLS = new Set(['Read', 'Glob', 'Grep'])
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type UserInputMessage = {
|
||||
type: 'user'
|
||||
parent_tool_use_id: string | null
|
||||
session_id: string
|
||||
message: {
|
||||
role: 'user'
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
class ClaudeCodeStream extends EventEmitter implements AgentStream {
|
||||
declare emit: (event: 'data', data: AgentStreamEvent) => boolean
|
||||
@@ -112,41 +99,6 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
const errorChunks: string[] = []
|
||||
|
||||
const sessionAllowedTools = new Set<string>(session.allowed_tools ?? [])
|
||||
const autoAllowTools = new Set<string>([...DEFAULT_AUTO_ALLOW_TOOLS, ...sessionAllowedTools])
|
||||
const normalizeToolName = (name: string) => (name.startsWith('builtin_') ? name.slice('builtin_'.length) : name)
|
||||
|
||||
const canUseTool: CanUseTool = async (toolName, input, options) => {
|
||||
logger.info('Handling tool permission check', {
|
||||
toolName,
|
||||
suggestionCount: options.suggestions?.length ?? 0
|
||||
})
|
||||
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('Auto-approving tool due to CHERRY_AUTO_ALLOW_TOOLS flag', { toolName })
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
if (options.signal.aborted) {
|
||||
logger.debug('Permission request signal already aborted; denying tool', { toolName })
|
||||
return {
|
||||
behavior: 'deny',
|
||||
message: 'Tool request was cancelled before prompting the user'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedToolName = normalizeToolName(toolName)
|
||||
if (autoAllowTools.has(toolName) || autoAllowTools.has(normalizedToolName)) {
|
||||
logger.debug('Auto-allowing tool from allowed list', {
|
||||
toolName,
|
||||
normalizedToolName
|
||||
})
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
return promptForToolApproval(toolName, input, options)
|
||||
}
|
||||
|
||||
// Build SDK options from parameters
|
||||
const options: Options = {
|
||||
abortController,
|
||||
@@ -169,8 +121,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
includePartialMessages: true,
|
||||
permissionMode: session.configuration?.permission_mode,
|
||||
maxTurns: session.configuration?.max_turns,
|
||||
allowedTools: session.allowed_tools,
|
||||
canUseTool
|
||||
allowedTools: session.allowed_tools
|
||||
}
|
||||
|
||||
if (session.accessible_paths.length > 1) {
|
||||
@@ -209,14 +160,9 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
resume: options.resume
|
||||
})
|
||||
|
||||
const { stream: userInputStream, close: closeUserStream } = this.createUserMessageStream(
|
||||
prompt,
|
||||
abortController.signal
|
||||
)
|
||||
|
||||
// Start async processing on the next tick so listeners can subscribe first
|
||||
setImmediate(() => {
|
||||
this.processSDKQuery(userInputStream, closeUserStream, options, aiStream, errorChunks).catch((error) => {
|
||||
this.processSDKQuery(prompt, options, aiStream, errorChunks).catch((error) => {
|
||||
logger.error('Unhandled Claude Code stream error', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
@@ -230,90 +176,17 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
return aiStream
|
||||
}
|
||||
|
||||
private createUserMessageStream(initialPrompt: string, abortSignal: AbortSignal) {
|
||||
const queue: Array<UserInputMessage | null> = []
|
||||
const waiters: Array<(value: UserInputMessage | null) => void> = []
|
||||
let closed = false
|
||||
|
||||
const flushWaiters = (value: UserInputMessage | null) => {
|
||||
const resolve = waiters.shift()
|
||||
if (resolve) {
|
||||
resolve(value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const enqueue = (value: UserInputMessage | null) => {
|
||||
if (closed) return
|
||||
if (value === null) {
|
||||
closed = true
|
||||
}
|
||||
if (!flushWaiters(value)) {
|
||||
queue.push(value)
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (closed) return
|
||||
enqueue(null)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
close()
|
||||
}
|
||||
|
||||
if (abortSignal.aborted) {
|
||||
close()
|
||||
} else {
|
||||
abortSignal.addEventListener('abort', onAbort, { once: true })
|
||||
}
|
||||
|
||||
const iterator = (async function* () {
|
||||
try {
|
||||
while (true) {
|
||||
let value: UserInputMessage | null
|
||||
if (queue.length > 0) {
|
||||
value = queue.shift() ?? null
|
||||
} else if (closed) {
|
||||
break
|
||||
} else {
|
||||
// Wait for next message or close signal
|
||||
value = await new Promise<UserInputMessage | null>((resolve) => {
|
||||
waiters.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
break
|
||||
}
|
||||
|
||||
yield value
|
||||
}
|
||||
} finally {
|
||||
closed = true
|
||||
abortSignal.removeEventListener('abort', onAbort)
|
||||
while (waiters.length > 0) {
|
||||
const resolve = waiters.shift()
|
||||
resolve?.(null)
|
||||
private async *userMessages(prompt: string) {
|
||||
{
|
||||
yield {
|
||||
type: 'user' as const,
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user' as const,
|
||||
content: prompt
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
enqueue({
|
||||
type: 'user',
|
||||
parent_tool_use_id: null,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initialPrompt
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stream: iterator,
|
||||
enqueue,
|
||||
close
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,8 +194,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
* Process SDK query and emit stream events
|
||||
*/
|
||||
private async processSDKQuery(
|
||||
promptStream: AsyncIterable<UserInputMessage>,
|
||||
closePromptStream: () => void,
|
||||
prompt: string,
|
||||
options: Options,
|
||||
stream: ClaudeCodeStream,
|
||||
errorChunks: string[]
|
||||
@@ -330,10 +202,14 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
const jsonOutput: SDKMessage[] = []
|
||||
let hasCompleted = false
|
||||
const startTime = Date.now()
|
||||
const streamState = new ClaudeStreamState()
|
||||
|
||||
const streamState = new ClaudeStreamState()
|
||||
try {
|
||||
for await (const message of query({ prompt: promptStream, options })) {
|
||||
// Process streaming responses using SDK query
|
||||
for await (const message of query({
|
||||
prompt: this.userMessages(prompt),
|
||||
options
|
||||
})) {
|
||||
if (hasCompleted) break
|
||||
|
||||
jsonOutput.push(message)
|
||||
@@ -344,10 +220,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
content: JSON.stringify(message.message.content)
|
||||
})
|
||||
} else if (message.type === 'stream_event') {
|
||||
// logger.silly('Claude stream event', {
|
||||
// message,
|
||||
// event: JSON.stringify(message.event)
|
||||
// })
|
||||
logger.silly('Claude stream event', {
|
||||
message,
|
||||
event: JSON.stringify(message.event)
|
||||
})
|
||||
} else {
|
||||
logger.silly('Claude response', {
|
||||
message,
|
||||
@@ -355,6 +231,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
})
|
||||
}
|
||||
|
||||
// Transform SDKMessage to UIMessageChunks
|
||||
const chunks = transformSDKMessageToStreamParts(message, streamState)
|
||||
for (const chunk of chunks) {
|
||||
stream.emit('data', {
|
||||
@@ -364,6 +241,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// Successfully completed
|
||||
hasCompleted = true
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
@@ -372,6 +250,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
messageCount: jsonOutput.length
|
||||
})
|
||||
|
||||
// Emit completion event
|
||||
stream.emit('data', {
|
||||
type: 'complete'
|
||||
})
|
||||
@@ -380,6 +259,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
hasCompleted = true
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
// Check if this is an abort error
|
||||
const errorObj = error as any
|
||||
const isAborted =
|
||||
errorObj?.name === 'AbortError' ||
|
||||
@@ -388,6 +269,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
|
||||
if (isAborted) {
|
||||
logger.info('SDK query aborted by client disconnect', { duration })
|
||||
// Simply cleanup and return - don't emit error events
|
||||
stream.emit('data', {
|
||||
type: 'cancelled',
|
||||
error: new Error('Request aborted by client')
|
||||
@@ -402,13 +284,11 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
error: errorObj instanceof Error ? { name: errorObj.name, message: errorObj.message } : String(errorObj),
|
||||
stderr: errorChunks
|
||||
})
|
||||
|
||||
// Emit error event
|
||||
stream.emit('data', {
|
||||
type: 'error',
|
||||
error: new Error(errorMessage)
|
||||
})
|
||||
} finally {
|
||||
closePromptStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { PermissionResult, PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { loggerService } from '@logger'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { windowService } from '../../../WindowService'
|
||||
import { builtinTools } from './tools'
|
||||
|
||||
const logger = loggerService.withContext('ClaudeCodeService')
|
||||
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = 30_000
|
||||
const MAX_PREVIEW_LENGTH = 2_000
|
||||
const shouldAutoApproveTools = process.env.CHERRY_AUTO_ALLOW_TOOLS === '1'
|
||||
|
||||
type ToolPermissionBehavior = 'allow' | 'deny'
|
||||
|
||||
type ToolPermissionResponsePayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
updatedInput?: unknown
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type PendingPermissionRequest = {
|
||||
fulfill: (update: PermissionResult) => void
|
||||
timeout: NodeJS.Timeout
|
||||
signal?: AbortSignal
|
||||
abortListener?: () => void
|
||||
originalInput: Record<string, unknown>
|
||||
toolName: string
|
||||
}
|
||||
|
||||
type RendererPermissionRequestPayload = {
|
||||
requestId: string
|
||||
toolName: string
|
||||
toolId: string
|
||||
description?: string
|
||||
requiresPermissions: boolean
|
||||
input: Record<string, unknown>
|
||||
inputPreview: string
|
||||
createdAt: number
|
||||
expiresAt: number
|
||||
suggestions: PermissionUpdate[]
|
||||
}
|
||||
|
||||
type RendererPermissionResultPayload = {
|
||||
requestId: string
|
||||
behavior: ToolPermissionBehavior
|
||||
message?: string
|
||||
reason: 'response' | 'timeout' | 'aborted' | 'no-window'
|
||||
}
|
||||
|
||||
const pendingRequests = new Map<string, PendingPermissionRequest>()
|
||||
let ipcHandlersInitialized = false
|
||||
|
||||
const jsonReplacer = (_key: string, value: unknown) => {
|
||||
if (typeof value === 'bigint') return value.toString()
|
||||
if (value instanceof Map) return Object.fromEntries(value.entries())
|
||||
if (value instanceof Set) return Array.from(value.values())
|
||||
if (value instanceof Date) return value.toISOString()
|
||||
if (typeof value === 'function') return undefined
|
||||
if (value === undefined) return undefined
|
||||
return value
|
||||
}
|
||||
|
||||
const sanitizeStructuredData = <T>(value: T): T => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value, jsonReplacer)) as T
|
||||
} catch (error) {
|
||||
logger.warn('Failed to sanitize structured data for tool permission payload', {
|
||||
error: error instanceof Error ? { name: error.name, message: error.message } : String(error)
|
||||
})
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const buildInputPreview = (value: unknown): string => {
|
||||
let preview: string
|
||||
|
||||
try {
|
||||
preview = JSON.stringify(value, null, 2)
|
||||
} catch (error) {
|
||||
preview = typeof value === 'string' ? value : String(value)
|
||||
}
|
||||
|
||||
if (preview.length > MAX_PREVIEW_LENGTH) {
|
||||
preview = `${preview.slice(0, MAX_PREVIEW_LENGTH)}...`
|
||||
}
|
||||
|
||||
return preview
|
||||
}
|
||||
|
||||
const broadcastToRenderer = (
|
||||
channel: IpcChannel,
|
||||
payload: RendererPermissionRequestPayload | RendererPermissionResultPayload
|
||||
): boolean => {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Unable to send agent tool permission payload – main window unavailable', {
|
||||
channel,
|
||||
requestId: 'requestId' in payload ? payload.requestId : undefined
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
mainWindow.webContents.send(channel, payload)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const finalizeRequest = (
|
||||
requestId: string,
|
||||
update: PermissionResult,
|
||||
reason: RendererPermissionResultPayload['reason']
|
||||
) => {
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.debug('Attempted to finalize unknown tool permission request', { requestId, reason })
|
||||
return false
|
||||
}
|
||||
|
||||
logger.debug('Finalizing tool permission request', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior: update.behavior,
|
||||
reason
|
||||
})
|
||||
|
||||
pendingRequests.delete(requestId)
|
||||
clearTimeout(pending.timeout)
|
||||
|
||||
if (pending.signal && pending.abortListener) {
|
||||
pending.signal.removeEventListener('abort', pending.abortListener)
|
||||
}
|
||||
|
||||
pending.fulfill(update)
|
||||
|
||||
const resultPayload: RendererPermissionResultPayload = {
|
||||
requestId,
|
||||
behavior: update.behavior,
|
||||
message: update.behavior === 'deny' ? update.message : undefined,
|
||||
reason
|
||||
}
|
||||
|
||||
const dispatched = broadcastToRenderer(IpcChannel.AgentToolPermission_Result, resultPayload)
|
||||
|
||||
logger.debug('Sent tool permission result to renderer', {
|
||||
requestId,
|
||||
dispatched
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const ensureIpcHandlersRegistered = () => {
|
||||
if (ipcHandlersInitialized) return
|
||||
|
||||
ipcHandlersInitialized = true
|
||||
|
||||
ipcMain.handle(IpcChannel.AgentToolPermission_Response, async (_event, payload: ToolPermissionResponsePayload) => {
|
||||
logger.debug('main received AgentToolPermission_Response', payload)
|
||||
const { requestId, behavior, updatedInput, message } = payload
|
||||
const pending = pendingRequests.get(requestId)
|
||||
|
||||
if (!pending) {
|
||||
logger.warn('Received renderer tool permission response for unknown request', { requestId })
|
||||
return { success: false, error: 'unknown-request' }
|
||||
}
|
||||
|
||||
logger.debug('Received renderer response for tool permission', {
|
||||
requestId,
|
||||
toolName: pending.toolName,
|
||||
behavior,
|
||||
hasUpdatedPermissions: Array.isArray(payload.updatedPermissions) && payload.updatedPermissions.length > 0
|
||||
})
|
||||
|
||||
const maybeUpdatedInput =
|
||||
updatedInput && typeof updatedInput === 'object' && !Array.isArray(updatedInput)
|
||||
? (updatedInput as Record<string, unknown>)
|
||||
: pending.originalInput
|
||||
|
||||
const sanitizedUpdatedPermissions = Array.isArray(payload.updatedPermissions)
|
||||
? payload.updatedPermissions.map((perm) => sanitizeStructuredData(perm))
|
||||
: undefined
|
||||
|
||||
const finalUpdate: PermissionResult =
|
||||
behavior === 'allow'
|
||||
? {
|
||||
behavior: 'allow',
|
||||
updatedInput: sanitizeStructuredData(maybeUpdatedInput),
|
||||
updatedPermissions: sanitizedUpdatedPermissions
|
||||
}
|
||||
: {
|
||||
behavior: 'deny',
|
||||
message: message ?? 'User denied permission for this tool'
|
||||
}
|
||||
|
||||
finalizeRequest(requestId, finalUpdate, 'response')
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
|
||||
export async function promptForToolApproval(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
options?: { signal: AbortSignal; suggestions?: PermissionUpdate[] }
|
||||
): Promise<PermissionResult> {
|
||||
if (shouldAutoApproveTools) {
|
||||
logger.debug('promptForToolApproval auto-approving tool for test', {
|
||||
toolName
|
||||
})
|
||||
|
||||
return { behavior: 'allow', updatedInput: input }
|
||||
}
|
||||
|
||||
ensureIpcHandlersRegistered()
|
||||
|
||||
if (options?.signal?.aborted) {
|
||||
logger.info('Skipping tool approval prompt because request signal is already aborted', { toolName })
|
||||
return { behavior: 'deny', message: 'Tool request was cancelled before prompting the user' }
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!mainWindow) {
|
||||
logger.warn('Denying tool usage because no renderer window is available to obtain approval', { toolName })
|
||||
return { behavior: 'deny', message: 'Unable to request approval – renderer not ready' }
|
||||
}
|
||||
|
||||
const toolMetadata = builtinTools.find((tool) => tool.name === toolName || tool.id === toolName)
|
||||
const sanitizedInput = sanitizeStructuredData(input)
|
||||
const inputPreview = buildInputPreview(sanitizedInput)
|
||||
const sanitizedSuggestions = (options?.suggestions ?? []).map((suggestion) => sanitizeStructuredData(suggestion))
|
||||
|
||||
const requestId = randomUUID()
|
||||
const createdAt = Date.now()
|
||||
const expiresAt = createdAt + TOOL_APPROVAL_TIMEOUT_MS
|
||||
|
||||
logger.info('Requesting user approval for tool usage', {
|
||||
requestId,
|
||||
toolName,
|
||||
description: toolMetadata?.description
|
||||
})
|
||||
|
||||
const requestPayload: RendererPermissionRequestPayload = {
|
||||
requestId,
|
||||
toolName,
|
||||
toolId: toolMetadata?.id ?? toolName,
|
||||
description: toolMetadata?.description,
|
||||
requiresPermissions: toolMetadata?.requirePermissions ?? false,
|
||||
input: sanitizedInput,
|
||||
inputPreview,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
suggestions: sanitizedSuggestions
|
||||
}
|
||||
|
||||
const defaultDenyUpdate: PermissionResult = { behavior: 'deny', message: 'Tool request aborted before user decision' }
|
||||
|
||||
logger.debug('Registering tool permission request', {
|
||||
requestId,
|
||||
toolName,
|
||||
requiresPermissions: requestPayload.requiresPermissions,
|
||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||
suggestionCount: sanitizedSuggestions.length
|
||||
})
|
||||
|
||||
return new Promise<PermissionResult>((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
logger.info('User tool permission request timed out', { requestId, toolName })
|
||||
finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout')
|
||||
}, TOOL_APPROVAL_TIMEOUT_MS)
|
||||
|
||||
const pending: PendingPermissionRequest = {
|
||||
fulfill: resolve,
|
||||
timeout,
|
||||
originalInput: sanitizedInput,
|
||||
toolName,
|
||||
signal: options?.signal
|
||||
}
|
||||
|
||||
if (options?.signal) {
|
||||
const abortListener = () => {
|
||||
logger.info('Tool permission request aborted before user responded', { requestId, toolName })
|
||||
finalizeRequest(requestId, defaultDenyUpdate, 'aborted')
|
||||
}
|
||||
|
||||
pending.abortListener = abortListener
|
||||
options.signal.addEventListener('abort', abortListener, { once: true })
|
||||
}
|
||||
|
||||
pendingRequests.set(requestId, pending)
|
||||
|
||||
logger.debug('Pending tool permission request count', {
|
||||
count: pendingRequests.size
|
||||
})
|
||||
|
||||
const sent = broadcastToRenderer(IpcChannel.AgentToolPermission_Request, requestPayload)
|
||||
|
||||
logger.debug('Broadcasted tool permission request to renderer', {
|
||||
requestId,
|
||||
toolName,
|
||||
sent
|
||||
})
|
||||
|
||||
if (!sent) {
|
||||
finalizeRequest(
|
||||
requestId,
|
||||
{
|
||||
behavior: 'deny',
|
||||
message: 'Unable to request approval because the renderer window is unavailable'
|
||||
},
|
||||
'no-window'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
|
||||
import { isPathInside } from './file'
|
||||
|
||||
const logger = loggerService.withContext('Utils:FileOperations')
|
||||
|
||||
const MAX_RECURSION_DEPTH = 1000
|
||||
|
||||
/**
|
||||
* Recursively copy a directory and all its contents
|
||||
* @param source - Source directory path (must be absolute)
|
||||
* @param destination - Destination directory path (must be absolute)
|
||||
* @param options - Copy options
|
||||
* @param depth - Current recursion depth (internal use)
|
||||
* @throws If copy operation fails or paths are invalid
|
||||
*/
|
||||
export async function copyDirectoryRecursive(
|
||||
source: string,
|
||||
destination: string,
|
||||
options?: { allowedBasePath?: string },
|
||||
depth = 0
|
||||
): Promise<void> {
|
||||
// Input validation
|
||||
if (!source || !destination) {
|
||||
throw new TypeError('Source and destination paths are required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(source) || !path.isAbsolute(destination)) {
|
||||
throw new Error('Source and destination paths must be absolute')
|
||||
}
|
||||
|
||||
// Depth limit to prevent stack overflow
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
throw new Error(`Maximum recursion depth exceeded: ${MAX_RECURSION_DEPTH}`)
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(source, options.allowedBasePath)) {
|
||||
throw new Error(`Source path is outside allowed directory: ${source}`)
|
||||
}
|
||||
if (!isPathInside(destination, options.allowedBasePath)) {
|
||||
throw new Error(`Destination path is outside allowed directory: ${destination}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify source exists and is a directory
|
||||
const sourceStats = await fs.promises.lstat(source)
|
||||
if (!sourceStats.isDirectory()) {
|
||||
throw new Error(`Source is not a directory: ${source}`)
|
||||
}
|
||||
|
||||
// Create destination directory
|
||||
await fs.promises.mkdir(destination, { recursive: true })
|
||||
logger.debug('Created destination directory', { destination })
|
||||
|
||||
// Read source directory
|
||||
const entries = await fs.promises.readdir(source, { withFileTypes: true })
|
||||
|
||||
// Copy each entry
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(source, entry.name)
|
||||
const destPath = path.join(destination, entry.name)
|
||||
|
||||
// Use lstat to detect symlinks and prevent following them
|
||||
const entryStats = await fs.promises.lstat(sourcePath)
|
||||
|
||||
if (entryStats.isSymbolicLink()) {
|
||||
logger.warn('Skipping symlink for security', { path: sourcePath })
|
||||
continue
|
||||
}
|
||||
|
||||
if (entryStats.isDirectory()) {
|
||||
// Recursively copy subdirectory
|
||||
await copyDirectoryRecursive(sourcePath, destPath, options, depth + 1)
|
||||
} else if (entryStats.isFile()) {
|
||||
// Copy file with error handling for race conditions
|
||||
try {
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
// Preserve file permissions
|
||||
await fs.promises.chmod(destPath, entryStats.mode)
|
||||
logger.debug('Copied file', { from: sourcePath, to: destPath })
|
||||
} catch (error) {
|
||||
// Handle race condition where file was deleted during copy
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
logger.warn('File disappeared during copy', { sourcePath })
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// Skip special files (pipes, sockets, devices, etc.)
|
||||
logger.debug('Skipping special file', { path: sourcePath })
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Directory copied successfully', { from: source, to: destination, depth })
|
||||
} catch (error) {
|
||||
logger.error('Failed to copy directory', { source, destination, depth, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a directory and all its contents
|
||||
* @param dirPath - Directory path to delete (must be absolute)
|
||||
* @param options - Delete options
|
||||
* @throws If deletion fails or path is invalid
|
||||
*/
|
||||
export async function deleteDirectoryRecursive(dirPath: string, options?: { allowedBasePath?: string }): Promise<void> {
|
||||
// Input validation
|
||||
if (!dirPath) {
|
||||
throw new TypeError('Directory path is required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(dirPath)) {
|
||||
throw new Error('Directory path must be absolute')
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(dirPath, options.allowedBasePath)) {
|
||||
throw new Error(`Path is outside allowed directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify path exists before attempting deletion
|
||||
try {
|
||||
const stats = await fs.promises.lstat(dirPath)
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`Path is not a directory: ${dirPath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
logger.warn('Directory already deleted', { dirPath })
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Node.js 14.14+ has fs.rm with recursive option
|
||||
await fs.promises.rm(dirPath, { recursive: true, force: true })
|
||||
logger.info('Directory deleted successfully', { dirPath })
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete directory', { dirPath, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total size of a directory (in bytes)
|
||||
* @param dirPath - Directory path (must be absolute)
|
||||
* @param options - Size calculation options
|
||||
* @param depth - Current recursion depth (internal use)
|
||||
* @returns Total size in bytes
|
||||
* @throws If size calculation fails or path is invalid
|
||||
*/
|
||||
export async function getDirectorySize(
|
||||
dirPath: string,
|
||||
options?: { allowedBasePath?: string },
|
||||
depth = 0
|
||||
): Promise<number> {
|
||||
// Input validation
|
||||
if (!dirPath) {
|
||||
throw new TypeError('Directory path is required')
|
||||
}
|
||||
|
||||
if (!path.isAbsolute(dirPath)) {
|
||||
throw new Error('Directory path must be absolute')
|
||||
}
|
||||
|
||||
// Depth limit to prevent stack overflow
|
||||
if (depth > MAX_RECURSION_DEPTH) {
|
||||
throw new Error(`Maximum recursion depth exceeded: ${MAX_RECURSION_DEPTH}`)
|
||||
}
|
||||
|
||||
// Path validation - ensure operations stay within allowed boundaries
|
||||
if (options?.allowedBasePath) {
|
||||
if (!isPathInside(dirPath, options.allowedBasePath)) {
|
||||
throw new Error(`Path is outside allowed directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
let totalSize = 0
|
||||
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dirPath, entry.name)
|
||||
|
||||
// Use lstat to detect symlinks and prevent following them
|
||||
const entryStats = await fs.promises.lstat(entryPath)
|
||||
|
||||
if (entryStats.isSymbolicLink()) {
|
||||
logger.debug('Skipping symlink in size calculation', { path: entryPath })
|
||||
continue
|
||||
}
|
||||
|
||||
if (entryStats.isDirectory()) {
|
||||
// Recursively get size of subdirectory
|
||||
totalSize += await getDirectorySize(entryPath, options, depth + 1)
|
||||
} else if (entryStats.isFile()) {
|
||||
// Get file size from lstat (already have it)
|
||||
totalSize += entryStats.size
|
||||
} else {
|
||||
// Skip special files
|
||||
logger.debug('Skipping special file in size calculation', { path: entryPath })
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Calculated directory size', { dirPath, size: totalSize, depth })
|
||||
return totalSize
|
||||
} catch (error) {
|
||||
logger.error('Failed to calculate directory size', { dirPath, depth, error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import type { PluginError, PluginMetadata } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
import * as fs from 'fs'
|
||||
import matter from 'gray-matter'
|
||||
import * as yaml from 'js-yaml'
|
||||
import * as path from 'path'
|
||||
|
||||
import { getDirectorySize } from './fileOperations'
|
||||
|
||||
const logger = loggerService.withContext('Utils:MarkdownParser')
|
||||
|
||||
/**
|
||||
* Parse plugin metadata from a markdown file with frontmatter
|
||||
* @param filePath Absolute path to the markdown file
|
||||
* @param sourcePath Relative source path from plugins directory
|
||||
* @param category Category name derived from parent folder
|
||||
* @param type Plugin type (agent or command)
|
||||
* @returns PluginMetadata object with parsed frontmatter and file info
|
||||
*/
|
||||
export async function parsePluginMetadata(
|
||||
filePath: string,
|
||||
sourcePath: string,
|
||||
category: string,
|
||||
type: 'agent' | 'command'
|
||||
): Promise<PluginMetadata> {
|
||||
const content = await fs.promises.readFile(filePath, 'utf8')
|
||||
const stats = await fs.promises.stat(filePath)
|
||||
|
||||
// Parse frontmatter safely with FAILSAFE_SCHEMA to prevent deserialization attacks
|
||||
const { data } = matter(content, {
|
||||
engines: {
|
||||
yaml: (s) => yaml.load(s, { schema: yaml.FAILSAFE_SCHEMA }) as object
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate content hash for integrity checking
|
||||
const contentHash = crypto.createHash('sha256').update(content).digest('hex')
|
||||
|
||||
// Extract filename
|
||||
const filename = path.basename(filePath)
|
||||
|
||||
// Parse allowed_tools - handle both array and comma-separated string
|
||||
let allowedTools: string[] | undefined
|
||||
if (data['allowed-tools'] || data.allowed_tools) {
|
||||
const toolsData = data['allowed-tools'] || data.allowed_tools
|
||||
if (Array.isArray(toolsData)) {
|
||||
allowedTools = toolsData
|
||||
} else if (typeof toolsData === 'string') {
|
||||
allowedTools = toolsData
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tools - similar handling
|
||||
let tools: string[] | undefined
|
||||
if (data.tools) {
|
||||
if (Array.isArray(data.tools)) {
|
||||
tools = data.tools
|
||||
} else if (typeof data.tools === 'string') {
|
||||
tools = data.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
let tags: string[] | undefined
|
||||
if (data.tags) {
|
||||
if (Array.isArray(data.tags)) {
|
||||
tags = data.tags
|
||||
} else if (typeof data.tags === 'string') {
|
||||
tags = data.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sourcePath,
|
||||
filename,
|
||||
name: data.name || filename.replace(/\.md$/, ''),
|
||||
description: data.description,
|
||||
allowed_tools: allowedTools,
|
||||
tools,
|
||||
category,
|
||||
type,
|
||||
tags,
|
||||
version: data.version,
|
||||
author: data.author,
|
||||
size: stats.size,
|
||||
contentHash
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all directories containing SKILL.md
|
||||
*
|
||||
* @param dirPath - Directory to search in
|
||||
* @param basePath - Base path for calculating relative source paths
|
||||
* @param maxDepth - Maximum depth to search (default: 10 to prevent infinite loops)
|
||||
* @param currentDepth - Current search depth (used internally)
|
||||
* @returns Array of objects with absolute folder path and relative source path
|
||||
*/
|
||||
export async function findAllSkillDirectories(
|
||||
dirPath: string,
|
||||
basePath: string,
|
||||
maxDepth = 10,
|
||||
currentDepth = 0
|
||||
): Promise<Array<{ folderPath: string; sourcePath: string }>> {
|
||||
const results: Array<{ folderPath: string; sourcePath: string }> = []
|
||||
|
||||
// Prevent excessive recursion
|
||||
if (currentDepth > maxDepth) {
|
||||
return results
|
||||
}
|
||||
|
||||
// Check if current directory contains SKILL.md
|
||||
const skillMdPath = path.join(dirPath, 'SKILL.md')
|
||||
|
||||
try {
|
||||
await fs.promises.stat(skillMdPath)
|
||||
// Found SKILL.md in this directory
|
||||
const relativePath = path.relative(basePath, dirPath)
|
||||
results.push({
|
||||
folderPath: dirPath,
|
||||
sourcePath: relativePath
|
||||
})
|
||||
return results
|
||||
} catch {
|
||||
// SKILL.md not in current directory
|
||||
}
|
||||
|
||||
// Only search subdirectories if current directory doesn't have SKILL.md
|
||||
try {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDirPath = path.join(dirPath, entry.name)
|
||||
const subResults = await findAllSkillDirectories(subDirPath, basePath, maxDepth, currentDepth + 1)
|
||||
results.push(...subResults)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Ignore errors when reading subdirectories (e.g., permission denied)
|
||||
logger.debug('Failed to read subdirectory during skill search', {
|
||||
dirPath,
|
||||
error: error.message
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse metadata from SKILL.md within a skill folder
|
||||
*
|
||||
* @param skillFolderPath - Absolute path to skill folder (must be absolute and contain SKILL.md)
|
||||
* @param sourcePath - Relative path from plugins base (e.g., "skills/my-skill")
|
||||
* @param category - Category name (typically "skills" for flat structure)
|
||||
* @returns PluginMetadata with folder name as filename (no extension)
|
||||
* @throws PluginError if SKILL.md not found or parsing fails
|
||||
*/
|
||||
export async function parseSkillMetadata(
|
||||
skillFolderPath: string,
|
||||
sourcePath: string,
|
||||
category: string
|
||||
): Promise<PluginMetadata> {
|
||||
// Input validation
|
||||
if (!skillFolderPath || !path.isAbsolute(skillFolderPath)) {
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: 'Skill folder path must be absolute',
|
||||
path: skillFolderPath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Look for SKILL.md directly in this folder (no recursion)
|
||||
const skillMdPath = path.join(skillFolderPath, 'SKILL.md')
|
||||
|
||||
// Check if SKILL.md exists
|
||||
try {
|
||||
await fs.promises.stat(skillMdPath)
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ENOENT') {
|
||||
logger.error('SKILL.md not found in skill folder', { skillMdPath })
|
||||
throw {
|
||||
type: 'FILE_NOT_FOUND',
|
||||
path: skillMdPath,
|
||||
message: 'SKILL.md not found in skill folder'
|
||||
} as PluginError
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Read SKILL.md content
|
||||
let content: string
|
||||
try {
|
||||
content = await fs.promises.readFile(skillMdPath, 'utf8')
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to read SKILL.md', { skillMdPath, error })
|
||||
throw {
|
||||
type: 'READ_FAILED',
|
||||
path: skillMdPath,
|
||||
reason: error.message || 'Unknown error'
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Parse frontmatter safely with FAILSAFE_SCHEMA to prevent deserialization attacks
|
||||
let data: any
|
||||
try {
|
||||
const parsed = matter(content, {
|
||||
engines: {
|
||||
yaml: (s) => yaml.load(s, { schema: yaml.FAILSAFE_SCHEMA }) as object
|
||||
}
|
||||
})
|
||||
data = parsed.data
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to parse SKILL.md frontmatter', { skillMdPath, error })
|
||||
throw {
|
||||
type: 'INVALID_METADATA',
|
||||
reason: `Failed to parse frontmatter: ${error.message}`,
|
||||
path: skillMdPath
|
||||
} as PluginError
|
||||
}
|
||||
|
||||
// Calculate hash of SKILL.md only (not entire folder)
|
||||
// Note: This means changes to other files in the skill won't trigger cache invalidation
|
||||
// This is intentional - only SKILL.md metadata changes should trigger updates
|
||||
const contentHash = crypto.createHash('sha256').update(content).digest('hex')
|
||||
|
||||
// Get folder name as identifier (NO EXTENSION)
|
||||
const folderName = path.basename(skillFolderPath)
|
||||
|
||||
// Get total folder size
|
||||
let folderSize: number
|
||||
try {
|
||||
folderSize = await getDirectorySize(skillFolderPath)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to calculate skill folder size', { skillFolderPath, error })
|
||||
// Use 0 as fallback instead of failing completely
|
||||
folderSize = 0
|
||||
}
|
||||
|
||||
// Parse tools (skills use 'tools', not 'allowed_tools')
|
||||
let tools: string[] | undefined
|
||||
if (data.tools) {
|
||||
if (Array.isArray(data.tools)) {
|
||||
// Validate all elements are strings
|
||||
tools = data.tools.filter((t) => typeof t === 'string')
|
||||
} else if (typeof data.tools === 'string') {
|
||||
tools = data.tools
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
let tags: string[] | undefined
|
||||
if (data.tags) {
|
||||
if (Array.isArray(data.tags)) {
|
||||
// Validate all elements are strings
|
||||
tags = data.tags.filter((t) => typeof t === 'string')
|
||||
} else if (typeof data.tags === 'string') {
|
||||
tags = data.tags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and sanitize name
|
||||
const name = typeof data.name === 'string' && data.name.trim() ? data.name.trim() : folderName
|
||||
|
||||
// Validate and sanitize description
|
||||
const description =
|
||||
typeof data.description === 'string' && data.description.trim() ? data.description.trim() : undefined
|
||||
|
||||
// Validate version and author
|
||||
const version = typeof data.version === 'string' ? data.version : undefined
|
||||
const author = typeof data.author === 'string' ? data.author : undefined
|
||||
|
||||
logger.debug('Successfully parsed skill metadata', {
|
||||
skillFolderPath,
|
||||
folderName,
|
||||
size: folderSize
|
||||
})
|
||||
|
||||
return {
|
||||
sourcePath, // e.g., "skills/my-skill"
|
||||
filename: folderName, // e.g., "my-skill" (folder name, NO .md extension)
|
||||
name,
|
||||
description,
|
||||
tools,
|
||||
category, // "skills" for flat structure
|
||||
type: 'skill',
|
||||
tags,
|
||||
version,
|
||||
author,
|
||||
size: folderSize,
|
||||
contentHash // Hash of SKILL.md content only
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
||||
import { SpanContext } from '@opentelemetry/api'
|
||||
@@ -36,15 +35,6 @@ import {
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
|
||||
import { CreateDirectoryOptions } from 'webdav'
|
||||
|
||||
import type {
|
||||
InstalledPlugin,
|
||||
InstallPluginOptions,
|
||||
ListAvailablePluginsResult,
|
||||
PluginMetadata,
|
||||
PluginResult,
|
||||
UninstallPluginOptions,
|
||||
WritePluginContentOptions
|
||||
} from '../renderer/src/types/plugin'
|
||||
import type { ActionItem } from '../renderer/src/types/selectionTypes'
|
||||
|
||||
export function tracedInvoke(channel: string, spanContext: SpanContext | undefined, ...args: any[]) {
|
||||
@@ -215,7 +205,14 @@ const api = {
|
||||
ipcRenderer.on('file-change', listener)
|
||||
return () => ipcRenderer.off('file-change', listener)
|
||||
},
|
||||
showInFolder: (path: string): Promise<void> => ipcRenderer.invoke(IpcChannel.File_ShowInFolder, path)
|
||||
showInFolder: (path: string): Promise<void> => ipcRenderer.invoke(IpcChannel.File_ShowInFolder, path),
|
||||
// Image export specific methods
|
||||
readBinary: (filePath: string): Promise<Buffer> => ipcRenderer.invoke(IpcChannel.File_ReadBinary, filePath),
|
||||
writeBinary: (filePath: string, buffer: Buffer): Promise<void> =>
|
||||
ipcRenderer.invoke(IpcChannel.File_WriteBinary, filePath, buffer),
|
||||
copyFile: (sourcePath: string, destPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke(IpcChannel.File_CopyFile, sourcePath, destPath),
|
||||
createDirectory: (dirPath: string): Promise<void> => ipcRenderer.invoke(IpcChannel.File_CreateDirectory, dirPath)
|
||||
},
|
||||
fs: {
|
||||
read: (pathOrUrl: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, pathOrUrl, encoding),
|
||||
@@ -439,15 +436,6 @@ const api = {
|
||||
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
|
||||
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
|
||||
},
|
||||
agentTools: {
|
||||
respondToPermission: (payload: {
|
||||
requestId: string
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}) => ipcRenderer.invoke(IpcChannel.AgentToolPermission_Response, payload)
|
||||
},
|
||||
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
|
||||
setDisableHardwareAcceleration: (isDisable: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
|
||||
@@ -526,21 +514,6 @@ const api = {
|
||||
start: (): Promise<StartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Start),
|
||||
restart: (): Promise<RestartApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Restart),
|
||||
stop: (): Promise<StopApiServerStatusResult> => ipcRenderer.invoke(IpcChannel.ApiServer_Stop)
|
||||
},
|
||||
claudeCodePlugin: {
|
||||
listAvailable: (): Promise<PluginResult<ListAvailablePluginsResult>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListAvailable),
|
||||
install: (options: InstallPluginOptions): Promise<PluginResult<PluginMetadata>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Install, options),
|
||||
uninstall: (options: UninstallPluginOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_Uninstall, options),
|
||||
listInstalled: (agentId: string): Promise<PluginResult<InstalledPlugin[]>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ListInstalled, agentId),
|
||||
invalidateCache: (): Promise<PluginResult<void>> => ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_InvalidateCache),
|
||||
readContent: (sourcePath: string): Promise<PluginResult<string>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_ReadContent, sourcePath),
|
||||
writeContent: (options: WritePluginContentOptions): Promise<PluginResult<void>> =>
|
||||
ipcRenderer.invoke(IpcChannel.ClaudeCodePlugin_WriteContent, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,7 @@
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { processKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import {
|
||||
BaseTool,
|
||||
MCPCallToolResponse,
|
||||
MCPTool,
|
||||
MCPToolResponse,
|
||||
MCPToolResultContent,
|
||||
NormalToolResponse
|
||||
} from '@renderer/types'
|
||||
import { BaseTool, MCPTool, MCPToolResponse, NormalToolResponse } from '@renderer/types'
|
||||
import { Chunk, ChunkType } from '@renderer/types/chunk'
|
||||
import type { ToolSet, TypedToolCall, TypedToolError, TypedToolResult } from 'ai'
|
||||
|
||||
@@ -261,7 +254,6 @@ export class ToolCallChunkHandler {
|
||||
type: 'tool-result'
|
||||
} & TypedToolResult<ToolSet>
|
||||
): void {
|
||||
// TODO: 基于AI SDK为供应商内置工具做更好的展示和类型安全处理
|
||||
const { toolCallId, output, input } = chunk
|
||||
|
||||
if (!toolCallId) {
|
||||
@@ -307,7 +299,12 @@ export class ToolCallChunkHandler {
|
||||
responses: [toolResponse]
|
||||
})
|
||||
|
||||
const images = extractImagesFromToolOutput(toolResponse.response)
|
||||
const images: string[] = []
|
||||
for (const content of toolResponse.response?.content || []) {
|
||||
if (content.type === 'image' && content.data) {
|
||||
images.push(`data:${content.mimeType};base64,${content.data}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (images.length) {
|
||||
this.onChunk({
|
||||
@@ -354,41 +351,3 @@ export class ToolCallChunkHandler {
|
||||
}
|
||||
|
||||
export const addActiveToolCall = ToolCallChunkHandler.addActiveToolCall.bind(ToolCallChunkHandler)
|
||||
|
||||
function extractImagesFromToolOutput(output: unknown): string[] {
|
||||
if (!output) {
|
||||
return []
|
||||
}
|
||||
|
||||
const contents: unknown[] = []
|
||||
|
||||
if (isMcpCallToolResponse(output)) {
|
||||
contents.push(...output.content)
|
||||
} else if (Array.isArray(output)) {
|
||||
contents.push(...output)
|
||||
} else if (hasContentArray(output)) {
|
||||
contents.push(...output.content)
|
||||
}
|
||||
|
||||
return contents
|
||||
.filter(isMcpImageContent)
|
||||
.map((content) => `data:${content.mimeType ?? 'image/png'};base64,${content.data}`)
|
||||
}
|
||||
|
||||
function isMcpCallToolResponse(value: unknown): value is MCPCallToolResponse {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as MCPCallToolResponse).content)
|
||||
}
|
||||
|
||||
function hasContentArray(value: unknown): value is { content: unknown[] } {
|
||||
return typeof value === 'object' && value !== null && Array.isArray((value as { content?: unknown }).content)
|
||||
}
|
||||
|
||||
function isMcpImageContent(content: unknown): content is MCPToolResultContent & { data: string } {
|
||||
if (typeof content !== 'object' || content === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const resultContent = content as MCPToolResultContent
|
||||
|
||||
return resultContent.type === 'image' && typeof resultContent.data === 'string'
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { addSpan, endSpan } from '@renderer/services/SpanManagerService'
|
||||
import { StartSpanParams } from '@renderer/trace/types/ModelSpanEntity'
|
||||
import type { Assistant, GenerateImageParams, Model, Provider } from '@renderer/types'
|
||||
import type { AiSdkModel, StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||
import { SUPPORTED_IMAGE_ENDPOINT_LIST } from '@renderer/utils'
|
||||
import { buildClaudeCodeSystemModelMessage } from '@shared/anthropic'
|
||||
import { type ImageModel, type LanguageModel, type Provider as AiSdkProvider, wrapLanguageModel } from 'ai'
|
||||
|
||||
@@ -78,7 +77,7 @@ export default class ModernAiProvider {
|
||||
return this.actualProvider
|
||||
}
|
||||
|
||||
public async completions(modelId: string, params: StreamTextParams, providerConfig: ModernAiProviderConfig) {
|
||||
public async completions(modelId: string, params: StreamTextParams, config: ModernAiProviderConfig) {
|
||||
// 检查model是否存在
|
||||
if (!this.model) {
|
||||
throw new Error('Model is required for completions. Please use constructor with model parameter.')
|
||||
@@ -86,10 +85,7 @@ export default class ModernAiProvider {
|
||||
|
||||
// 每次请求时重新生成配置以确保API key轮换生效
|
||||
this.config = providerToAiSdkConfig(this.actualProvider, this.model)
|
||||
logger.debug('Generated provider config for completions', this.config)
|
||||
if (SUPPORTED_IMAGE_ENDPOINT_LIST.includes(this.config.options.endpoint)) {
|
||||
providerConfig.isImageGenerationEndpoint = true
|
||||
}
|
||||
|
||||
// 准备特殊配置
|
||||
await prepareSpecialProviderConfig(this.actualProvider, this.config)
|
||||
|
||||
@@ -100,13 +96,12 @@ export default class ModernAiProvider {
|
||||
|
||||
// 提前构建中间件
|
||||
const middlewares = buildAiSdkMiddlewares({
|
||||
...providerConfig,
|
||||
provider: this.actualProvider,
|
||||
assistant: providerConfig.assistant
|
||||
...config,
|
||||
provider: this.actualProvider
|
||||
})
|
||||
logger.debug('Built middlewares in completions', {
|
||||
middlewareCount: middlewares.length,
|
||||
isImageGeneration: providerConfig.isImageGenerationEndpoint
|
||||
isImageGeneration: config.isImageGenerationEndpoint
|
||||
})
|
||||
if (!this.localProvider) {
|
||||
throw new Error('Local provider not created')
|
||||
@@ -114,7 +109,7 @@ export default class ModernAiProvider {
|
||||
|
||||
// 根据endpoint类型创建对应的模型
|
||||
let model: AiSdkModel | undefined
|
||||
if (providerConfig.isImageGenerationEndpoint) {
|
||||
if (config.isImageGenerationEndpoint) {
|
||||
model = this.localProvider.imageModel(modelId)
|
||||
} else {
|
||||
model = this.localProvider.languageModel(modelId)
|
||||
@@ -130,15 +125,15 @@ export default class ModernAiProvider {
|
||||
params.messages = [...claudeCodeSystemMessage, ...(params.messages || [])]
|
||||
}
|
||||
|
||||
if (providerConfig.topicId && getEnableDeveloperMode()) {
|
||||
if (config.topicId && getEnableDeveloperMode()) {
|
||||
// TypeScript类型窄化:确保topicId是string类型
|
||||
const traceConfig = {
|
||||
...providerConfig,
|
||||
topicId: providerConfig.topicId
|
||||
...config,
|
||||
topicId: config.topicId
|
||||
}
|
||||
return await this._completionsForTrace(model, params, traceConfig)
|
||||
} else {
|
||||
return await this._completionsOrImageGeneration(model, params, providerConfig)
|
||||
return await this._completionsOrImageGeneration(model, params, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { isOpenAIProvider } from '@renderer/utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { AihubmixAPIClient } from '../aihubmix/AihubmixAPIClient'
|
||||
@@ -201,4 +202,36 @@ describe('ApiClientFactory', () => {
|
||||
expect(client).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOpenAIProvider', () => {
|
||||
it('should return true for openai type', () => {
|
||||
const provider = createTestProvider('openai', 'openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for azure-openai type', () => {
|
||||
const provider = createTestProvider('azure-openai', 'azure-openai')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true for unknown type (fallback to OpenAI)', () => {
|
||||
const provider = createTestProvider('unknown', 'unknown')
|
||||
expect(isOpenAIProvider(provider)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for vertexai type', () => {
|
||||
const provider = createTestProvider('vertex', 'vertexai')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for anthropic type', () => {
|
||||
const provider = createTestProvider('anthropic', 'anthropic')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false for gemini type', () => {
|
||||
const provider = createTestProvider('gemini', 'gemini')
|
||||
expect(isOpenAIProvider(provider)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -342,28 +342,29 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
|
||||
}
|
||||
}
|
||||
switch (message.type) {
|
||||
case 'function_call_output': {
|
||||
let str = ''
|
||||
if (typeof message.output === 'string') {
|
||||
str = message.output
|
||||
} else {
|
||||
for (const part of message.output) {
|
||||
switch (part.type) {
|
||||
case 'input_text':
|
||||
str += part.text
|
||||
break
|
||||
case 'input_image':
|
||||
str += part.image_url || ''
|
||||
break
|
||||
case 'input_file':
|
||||
str += part.file_data || ''
|
||||
break
|
||||
case 'function_call_output':
|
||||
{
|
||||
let str = ''
|
||||
if (typeof message.output === 'string') {
|
||||
str = message.output
|
||||
} else {
|
||||
for (const part of message.output) {
|
||||
switch (part.type) {
|
||||
case 'input_text':
|
||||
str += part.text
|
||||
break
|
||||
case 'input_image':
|
||||
str += part.image_url || ''
|
||||
break
|
||||
case 'input_file':
|
||||
str += part.file_data || ''
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sum += estimateTextTokens(str)
|
||||
}
|
||||
sum += estimateTextTokens(str)
|
||||
break
|
||||
}
|
||||
case 'function_call':
|
||||
sum += estimateTextTokens(message.arguments)
|
||||
break
|
||||
|
||||
@@ -78,12 +78,6 @@ export const ImageGenerationMiddleware: CompletionsMiddleware =
|
||||
const options = { signal, timeout: defaultTimeout }
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
const model = assistant.model
|
||||
const provider = context.apiClientInstance.provider
|
||||
// https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/dall-e?tabs=gpt-image-1#call-the-image-edit-api
|
||||
if (model.id.toLowerCase().includes('gpt-image-1-mini') && provider.type === 'azure-openai') {
|
||||
throw new Error('Azure OpenAI GPT-Image-1-Mini model does not support image editing.')
|
||||
}
|
||||
response = await sdk.images.edit(
|
||||
{
|
||||
model: assistant.model.id,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
|
||||
import { isSupportEnableThinkingProvider } from '@renderer/config/providers'
|
||||
import { type Assistant, MCPTool, type Message, type Model, type Provider } from '@renderer/types'
|
||||
import type { MCPTool, Message, Model, Provider } from '@renderer/types'
|
||||
import type { Chunk } from '@renderer/types/chunk'
|
||||
import { extractReasoningMiddleware, LanguageModelMiddleware, simulateStreamingMiddleware } from 'ai'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import { isOpenRouterGeminiGenerateImageModel } from '../utils/image'
|
||||
import { noThinkMiddleware } from './noThinkMiddleware'
|
||||
import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware'
|
||||
import { qwenThinkingMiddleware } from './qwenThinkingMiddleware'
|
||||
import { toolChoiceMiddleware } from './toolChoiceMiddleware'
|
||||
|
||||
const logger = loggerService.withContext('AiSdkMiddlewareBuilder')
|
||||
@@ -23,7 +17,6 @@ export interface AiSdkMiddlewareConfig {
|
||||
onChunk?: (chunk: Chunk) => void
|
||||
model?: Model
|
||||
provider?: Provider
|
||||
assistant?: Assistant
|
||||
enableReasoning: boolean
|
||||
// 是否开启提示词工具调用
|
||||
isPromptToolUse: boolean
|
||||
@@ -132,7 +125,7 @@ export function buildAiSdkMiddlewares(config: AiSdkMiddlewareConfig): LanguageMo
|
||||
const builder = new AiSdkMiddlewareBuilder()
|
||||
|
||||
// 0. 知识库强制调用中间件(必须在最前面,确保第一轮强制调用知识库)
|
||||
if (!isEmpty(config.assistant?.knowledge_bases?.map((base) => base.id)) && config.knowledgeRecognition !== 'on') {
|
||||
if (config.knowledgeRecognition === 'off') {
|
||||
builder.add({
|
||||
name: 'force-knowledge-first',
|
||||
middleware: toolChoiceMiddleware('builtin_knowledge_search')
|
||||
@@ -220,31 +213,15 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
|
||||
/**
|
||||
* 添加模型特定的中间件
|
||||
*/
|
||||
function addModelSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config: AiSdkMiddlewareConfig): void {
|
||||
if (!config.model || !config.provider) return
|
||||
|
||||
// Qwen models on providers that don't support enable_thinking parameter (like Ollama, LM Studio, NVIDIA)
|
||||
// Use /think or /no_think suffix to control thinking mode
|
||||
if (
|
||||
config.provider &&
|
||||
isSupportedThinkingTokenQwenModel(config.model) &&
|
||||
!isSupportEnableThinkingProvider(config.provider)
|
||||
) {
|
||||
const enableThinking = config.assistant?.settings?.reasoning_effort !== undefined
|
||||
builder.add({
|
||||
name: 'qwen-thinking-control',
|
||||
middleware: qwenThinkingMiddleware(enableThinking)
|
||||
})
|
||||
logger.debug(`Added Qwen thinking middleware with thinking ${enableThinking ? 'enabled' : 'disabled'}`)
|
||||
}
|
||||
function addModelSpecificMiddlewares(_: AiSdkMiddlewareBuilder, config: AiSdkMiddlewareConfig): void {
|
||||
if (!config.model) return
|
||||
|
||||
// 可以根据模型ID或特性添加特定中间件
|
||||
// 例如:图像生成模型、多模态模型等
|
||||
if (isOpenRouterGeminiGenerateImageModel(config.model, config.provider)) {
|
||||
builder.add({
|
||||
name: 'openrouter-gemini-image-generation',
|
||||
middleware: openrouterGenerateImageMiddleware()
|
||||
})
|
||||
|
||||
// 示例:某些模型需要特殊处理
|
||||
if (config.model.id.includes('dalle') || config.model.id.includes('midjourney')) {
|
||||
// 图像生成相关中间件
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
/**
|
||||
* Returns a LanguageModelMiddleware that ensures the OpenRouter provider is configured to support both
|
||||
* image and text modalities.
|
||||
* https://openrouter.ai/docs/features/multimodal/image-generation
|
||||
*
|
||||
* Remarks:
|
||||
* - The middleware declares middlewareVersion as 'v2'.
|
||||
* - transformParams asynchronously clones the incoming params and sets
|
||||
* providerOptions.openrouter.modalities = ['image', 'text'], preserving other providerOptions and
|
||||
* openrouter fields when present.
|
||||
* - Intended to ensure the provider can handle image and text generation without altering other
|
||||
* parameter values.
|
||||
*
|
||||
* @returns LanguageModelMiddleware - a middleware that augments providerOptions for OpenRouter to include image and text modalities.
|
||||
*/
|
||||
export function openrouterGenerateImageMiddleware(): LanguageModelMiddleware {
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
transformedParams.providerOptions = {
|
||||
...transformedParams.providerOptions,
|
||||
openrouter: { ...transformedParams.providerOptions?.openrouter, modalities: ['image', 'text'] }
|
||||
}
|
||||
transformedParams
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
/**
|
||||
* Qwen Thinking Middleware
|
||||
* Controls thinking mode for Qwen models on providers that don't support enable_thinking parameter (like Ollama)
|
||||
* Appends '/think' or '/no_think' suffix to user messages based on reasoning_effort setting
|
||||
* @param enableThinking - Whether thinking mode is enabled (based on reasoning_effort !== undefined)
|
||||
* @returns LanguageModelMiddleware
|
||||
*/
|
||||
export function qwenThinkingMiddleware(enableThinking: boolean): LanguageModelMiddleware {
|
||||
const suffix = enableThinking ? ' /think' : ' /no_think'
|
||||
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
|
||||
transformParams: async ({ params }) => {
|
||||
const transformedParams = { ...params }
|
||||
// Process messages in prompt
|
||||
if (transformedParams.prompt && Array.isArray(transformedParams.prompt)) {
|
||||
transformedParams.prompt = transformedParams.prompt.map((message) => {
|
||||
// Only process user messages
|
||||
if (message.role === 'user') {
|
||||
// Process content array
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const part of message.content) {
|
||||
if (part.type === 'text' && !part.text.endsWith('/think') && !part.text.endsWith('/no_think')) {
|
||||
part.text += suffix
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
}
|
||||
|
||||
return transformedParams
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AiPlugin } from '@cherrystudio/ai-core'
|
||||
import { createPromptToolUsePlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { createPromptToolUsePlugin, googleToolsPlugin, webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
import { loggerService } from '@logger'
|
||||
import { getEnableDeveloperMode } from '@renderer/hooks/useSettings'
|
||||
import { Assistant } from '@renderer/types'
|
||||
@@ -68,9 +68,9 @@ export function buildPlugins(
|
||||
)
|
||||
}
|
||||
|
||||
// if (middlewareConfig.enableUrlContext && middlewareConfig.) {
|
||||
// plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
// }
|
||||
if (middlewareConfig.enableUrlContext) {
|
||||
plugins.push(googleToolsPlugin({ urlContext: true }))
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
'Final plugin list:',
|
||||
|
||||
@@ -49,7 +49,7 @@ class AdapterTracer {
|
||||
this.cachedParentContext = undefined
|
||||
}
|
||||
|
||||
logger.debug('AdapterTracer created with parent context info', {
|
||||
logger.info('AdapterTracer created with parent context info', {
|
||||
topicId,
|
||||
modelName,
|
||||
parentTraceId: this.parentSpanContext?.traceId,
|
||||
@@ -62,7 +62,7 @@ class AdapterTracer {
|
||||
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, fn: F): ReturnType<F>
|
||||
startActiveSpan<F extends (span: Span) => any>(name: string, options: any, context: any, fn: F): ReturnType<F>
|
||||
startActiveSpan<F extends (span: Span) => any>(name: string, arg2?: any, arg3?: any, arg4?: any): ReturnType<F> {
|
||||
logger.debug('AdapterTracer.startActiveSpan called', {
|
||||
logger.info('AdapterTracer.startActiveSpan called', {
|
||||
spanName: name,
|
||||
topicId: this.topicId,
|
||||
modelName: this.modelName,
|
||||
@@ -88,7 +88,7 @@ class AdapterTracer {
|
||||
// 包装span的end方法
|
||||
const originalEnd = span.end.bind(span)
|
||||
span.end = (endTime?: any) => {
|
||||
logger.debug('AI SDK span.end() called in startActiveSpan - about to convert span', {
|
||||
logger.info('AI SDK span.end() called in startActiveSpan - about to convert span', {
|
||||
spanName: name,
|
||||
spanId: span.spanContext().spanId,
|
||||
traceId: span.spanContext().traceId,
|
||||
@@ -101,14 +101,14 @@ class AdapterTracer {
|
||||
|
||||
// 转换并保存 span 数据
|
||||
try {
|
||||
logger.debug('Converting AI SDK span to SpanEntity (from startActiveSpan)', {
|
||||
logger.info('Converting AI SDK span to SpanEntity (from startActiveSpan)', {
|
||||
spanName: name,
|
||||
spanId: span.spanContext().spanId,
|
||||
traceId: span.spanContext().traceId,
|
||||
topicId: this.topicId,
|
||||
modelName: this.modelName
|
||||
})
|
||||
logger.silly('span', span)
|
||||
logger.info('span', span)
|
||||
const spanEntity = AiSdkSpanAdapter.convertToSpanEntity({
|
||||
span,
|
||||
topicId: this.topicId,
|
||||
@@ -118,7 +118,7 @@ class AdapterTracer {
|
||||
// 保存转换后的数据
|
||||
window.api.trace.saveEntity(spanEntity)
|
||||
|
||||
logger.debug('AI SDK span converted and saved successfully (from startActiveSpan)', {
|
||||
logger.info('AI SDK span converted and saved successfully (from startActiveSpan)', {
|
||||
spanName: name,
|
||||
spanId: span.spanContext().spanId,
|
||||
traceId: span.spanContext().traceId,
|
||||
@@ -151,7 +151,7 @@ class AdapterTracer {
|
||||
if (this.parentSpanContext) {
|
||||
try {
|
||||
const ctx = trace.setSpanContext(otelContext.active(), this.parentSpanContext)
|
||||
logger.debug('Created active context with parent SpanContext for startActiveSpan', {
|
||||
logger.info('Created active context with parent SpanContext for startActiveSpan', {
|
||||
spanName: name,
|
||||
parentTraceId: this.parentSpanContext.traceId,
|
||||
parentSpanId: this.parentSpanContext.spanId,
|
||||
@@ -218,7 +218,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
|
||||
if (effectiveTopicId) {
|
||||
try {
|
||||
// 从 SpanManagerService 获取当前的 span
|
||||
logger.debug('Attempting to find parent span', {
|
||||
logger.info('Attempting to find parent span', {
|
||||
topicId: effectiveTopicId,
|
||||
requestId: context.requestId,
|
||||
modelName: modelName,
|
||||
@@ -230,7 +230,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
|
||||
if (parentSpan) {
|
||||
// 直接使用父 span 的 SpanContext,避免手动拼装字段遗漏
|
||||
parentSpanContext = parentSpan.spanContext()
|
||||
logger.debug('Found active parent span for AI SDK', {
|
||||
logger.info('Found active parent span for AI SDK', {
|
||||
parentSpanId: parentSpanContext.spanId,
|
||||
parentTraceId: parentSpanContext.traceId,
|
||||
topicId: effectiveTopicId,
|
||||
@@ -302,7 +302,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
|
||||
logger.debug('Updated active context with parent span')
|
||||
})
|
||||
|
||||
logger.debug('Set parent context for AI SDK spans', {
|
||||
logger.info('Set parent context for AI SDK spans', {
|
||||
parentSpanId: parentSpanContext?.spanId,
|
||||
parentTraceId: parentSpanContext?.traceId,
|
||||
hasActiveContext: !!activeContext,
|
||||
@@ -313,7 +313,7 @@ export function createTelemetryPlugin(config: TelemetryPluginConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('Injecting AI SDK telemetry config with adapter', {
|
||||
logger.info('Injecting AI SDK telemetry config with adapter', {
|
||||
requestId: context.requestId,
|
||||
topicId: effectiveTopicId,
|
||||
modelId: context.modelId,
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function handleGeminiFileUpload(file: FileMetadata, model: Model):
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理OpenAI兼容大文件上传
|
||||
* 处理OpenAI大文件上传
|
||||
*/
|
||||
export async function handleOpenAILargeFileUpload(
|
||||
file: FileMetadata,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { isImageEnhancementModel, isVisionModel } from '@renderer/config/models'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import type { Message, Model } from '@renderer/types'
|
||||
import { FileMessageBlock, ImageMessageBlock, ThinkingMessageBlock } from '@renderer/types/newMessage'
|
||||
import {
|
||||
@@ -47,41 +47,6 @@ export async function convertMessageToSdkParam(
|
||||
}
|
||||
}
|
||||
|
||||
async function convertImageBlockToImagePart(imageBlocks: ImageMessageBlock[]): Promise<Array<ImagePart>> {
|
||||
const parts: Array<ImagePart> = []
|
||||
for (const imageBlock of imageBlocks) {
|
||||
if (imageBlock.file) {
|
||||
try {
|
||||
const image = await window.api.file.base64Image(imageBlock.file.id + imageBlock.file.ext)
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: image.base64,
|
||||
mediaType: image.mime
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn('Failed to load image:', error as Error)
|
||||
}
|
||||
} else if (imageBlock.url) {
|
||||
const isBase64 = imageBlock.url.startsWith('data:')
|
||||
if (isBase64) {
|
||||
const base64 = imageBlock.url.match(/^data:[^;]*;base64,(.+)$/)![1]
|
||||
const mimeMatch = imageBlock.url.match(/^data:([^;]+)/)
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: base64,
|
||||
mediaType: mimeMatch ? mimeMatch[1] : 'image/png'
|
||||
})
|
||||
} else {
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: imageBlock.url
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为用户模型消息
|
||||
*/
|
||||
@@ -99,7 +64,25 @@ async function convertMessageToUserModelMessage(
|
||||
|
||||
// 处理图片(仅在支持视觉的模型中)
|
||||
if (isVisionModel) {
|
||||
parts.push(...(await convertImageBlockToImagePart(imageBlocks)))
|
||||
for (const imageBlock of imageBlocks) {
|
||||
if (imageBlock.file) {
|
||||
try {
|
||||
const image = await window.api.file.base64Image(imageBlock.file.id + imageBlock.file.ext)
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: image.base64,
|
||||
mediaType: image.mime
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn('Failed to load image:', error as Error)
|
||||
}
|
||||
} else if (imageBlock.url) {
|
||||
parts.push({
|
||||
type: 'image',
|
||||
image: imageBlock.url
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理文件
|
||||
for (const fileBlock of fileBlocks) {
|
||||
@@ -189,27 +172,7 @@ async function convertMessageToAssistantModelMessage(
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of messages to SDK-compatible model messages.
|
||||
*
|
||||
* This function processes messages and transforms them into the format required by the SDK.
|
||||
* It handles special cases for vision models and image enhancement models.
|
||||
*
|
||||
* @param messages - Array of messages to convert. Must contain at least 2 messages when using image enhancement models.
|
||||
* @param model - The model configuration that determines conversion behavior
|
||||
*
|
||||
* @returns A promise that resolves to an array of SDK-compatible model messages
|
||||
*
|
||||
* @remarks
|
||||
* For image enhancement models with 2+ messages:
|
||||
* - Expects the second-to-last message (index length-2) to be an assistant message containing image blocks
|
||||
* - Expects the last message (index length-1) to be a user message
|
||||
* - Extracts images from the assistant message and appends them to the user message content
|
||||
* - Returns only the last two processed messages [assistantSdkMessage, userSdkMessage]
|
||||
*
|
||||
* For other models:
|
||||
* - Returns all converted messages in order
|
||||
*
|
||||
* The function automatically detects vision model capabilities and adjusts conversion accordingly.
|
||||
* 转换 Cherry Studio 消息数组为 AI SDK 消息数组
|
||||
*/
|
||||
export async function convertMessagesToSdkMessages(messages: Message[], model: Model): Promise<ModelMessage[]> {
|
||||
const sdkMessages: ModelMessage[] = []
|
||||
@@ -219,31 +182,6 @@ export async function convertMessagesToSdkMessages(messages: Message[], model: M
|
||||
const sdkMessage = await convertMessageToSdkParam(message, isVision, model)
|
||||
sdkMessages.push(...(Array.isArray(sdkMessage) ? sdkMessage : [sdkMessage]))
|
||||
}
|
||||
// Special handling for image enhancement models
|
||||
// Only keep the last two messages and merge images into the user message
|
||||
// [system?, user, assistant, user]
|
||||
if (isImageEnhancementModel(model) && messages.length >= 3) {
|
||||
const needUpdatedMessages = messages.slice(-2)
|
||||
const needUpdatedSdkMessages = sdkMessages.slice(-2)
|
||||
const assistantMessage = needUpdatedMessages.filter((m) => m.role === 'assistant')[0]
|
||||
const assistantSdkMessage = needUpdatedSdkMessages.filter((m) => m.role === 'assistant')[0]
|
||||
const userSdkMessage = needUpdatedSdkMessages.filter((m) => m.role === 'user')[0]
|
||||
const systemSdkMessages = sdkMessages.filter((m) => m.role === 'system')
|
||||
const imageBlocks = findImageBlocks(assistantMessage)
|
||||
const imageParts = await convertImageBlockToImagePart(imageBlocks)
|
||||
const parts: Array<TextPart | ImagePart | FilePart> = []
|
||||
if (typeof userSdkMessage.content === 'string') {
|
||||
parts.push({ type: 'text', text: userSdkMessage.content })
|
||||
parts.push(...imageParts)
|
||||
userSdkMessage.content = parts
|
||||
} else {
|
||||
userSdkMessage.content.push(...imageParts)
|
||||
}
|
||||
if (systemSdkMessages.length > 0) {
|
||||
return [systemSdkMessages[0], assistantSdkMessage, userSdkMessage]
|
||||
}
|
||||
return [assistantSdkMessage, userSdkMessage]
|
||||
}
|
||||
|
||||
return sdkMessages
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
isClaude45ReasoningModel,
|
||||
isClaudeReasoningModel,
|
||||
isNotSupportTemperatureAndTopP,
|
||||
isSupportedFlexServiceTier
|
||||
@@ -20,10 +19,7 @@ export function getTemperature(assistant: Assistant, model: Model): number | und
|
||||
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
isNotSupportTemperatureAndTopP(model) ||
|
||||
(isClaude45ReasoningModel(model) && assistant.settings?.enableTopP && !assistant.settings?.enableTemperature)
|
||||
) {
|
||||
if (isNotSupportTemperatureAndTopP(model)) {
|
||||
return undefined
|
||||
}
|
||||
const assistantSettings = getAssistantSettings(assistant)
|
||||
@@ -37,10 +33,7 @@ export function getTopP(assistant: Assistant, model: Model): number | undefined
|
||||
if (assistant.settings?.reasoning_effort && isClaudeReasoningModel(model)) {
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
isNotSupportTemperatureAndTopP(model) ||
|
||||
(isClaude45ReasoningModel(model) && assistant.settings?.enableTemperature)
|
||||
) {
|
||||
if (isNotSupportTemperatureAndTopP(model)) {
|
||||
return undefined
|
||||
}
|
||||
const assistantSettings = getAssistantSettings(assistant)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* 构建AI SDK的流式和非流式参数
|
||||
*/
|
||||
|
||||
import { anthropic } from '@ai-sdk/anthropic'
|
||||
import { google } from '@ai-sdk/google'
|
||||
import { vertexAnthropic } from '@ai-sdk/google-vertex/anthropic/edge'
|
||||
import { vertex } from '@ai-sdk/google-vertex/edge'
|
||||
import { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
|
||||
@@ -99,6 +97,10 @@ export async function buildStreamTextParams(
|
||||
|
||||
let tools = setupToolsConfig(mcpTools)
|
||||
|
||||
// if (webSearchProviderId) {
|
||||
// tools['builtin_web_search'] = webSearchTool(webSearchProviderId)
|
||||
// }
|
||||
|
||||
// 构建真正的 providerOptions
|
||||
const webSearchConfig: CherryWebSearchConfig = {
|
||||
maxResults: store.getState().websearch.maxResults,
|
||||
@@ -141,34 +143,12 @@ export async function buildStreamTextParams(
|
||||
}
|
||||
}
|
||||
|
||||
if (enableUrlContext) {
|
||||
// google-vertex
|
||||
if (enableUrlContext && aiSdkProviderId === 'google-vertex') {
|
||||
if (!tools) {
|
||||
tools = {}
|
||||
}
|
||||
const blockedDomains = mapRegexToPatterns(webSearchConfig.excludeDomains)
|
||||
|
||||
switch (aiSdkProviderId) {
|
||||
case 'google-vertex':
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'google':
|
||||
tools.url_context = google.tools.urlContext({}) as ProviderDefinedTool
|
||||
break
|
||||
case 'anthropic':
|
||||
case 'google-vertex-anthropic':
|
||||
tools.web_fetch = (
|
||||
aiSdkProviderId === 'anthropic'
|
||||
? anthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
: vertexAnthropic.tools.webFetch_20250910({
|
||||
maxUses: webSearchConfig.maxResults,
|
||||
blockedDomains: blockedDomains.length > 0 ? blockedDomains : undefined
|
||||
})
|
||||
) as ProviderDefinedTool
|
||||
break
|
||||
}
|
||||
tools.url_context = vertex.tools.urlContext({}) as ProviderDefinedTool
|
||||
}
|
||||
|
||||
// 构建基础参数
|
||||
|
||||
@@ -32,8 +32,7 @@ const AIHUBMIX_RULES: RuleSet = {
|
||||
match: (model) =>
|
||||
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
|
||||
!model.id.endsWith('-nothink') &&
|
||||
!model.id.endsWith('-search') &&
|
||||
!model.id.includes('embedding'),
|
||||
!model.id.endsWith('-search'),
|
||||
provider: (provider: Provider) => {
|
||||
return extraProviderConfig({
|
||||
...provider,
|
||||
|
||||
@@ -6,28 +6,26 @@ import {
|
||||
type ProviderSettingsMap
|
||||
} from '@cherrystudio/ai-core/provider'
|
||||
import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
|
||||
import {
|
||||
isAnthropicProvider,
|
||||
isAzureOpenAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
} from '@renderer/config/providers'
|
||||
import { isNewApiProvider } from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
getAwsBedrockRegion,
|
||||
getAwsBedrockSecretAccessKey
|
||||
} from '@renderer/hooks/useAwsBedrock'
|
||||
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
|
||||
import { createVertexProvider, isVertexAIConfigured } from '@renderer/hooks/useVertexAI'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { loggerService } from '@renderer/services/LoggerService'
|
||||
import store from '@renderer/store'
|
||||
import { isSystemProvider, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { formatApiHost, formatAzureOpenAIApiHost, formatVertexApiHost, routeToEndpoint } from '@renderer/utils/api'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { isSystemProvider, type Model, type Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { cloneDeep, trim } from 'lodash'
|
||||
|
||||
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
|
||||
import { COPILOT_DEFAULT_HEADERS } from './constants'
|
||||
import { getAiSdkProviderId } from './factory'
|
||||
|
||||
const logger = loggerService.withContext('ProviderConfigProcessor')
|
||||
|
||||
/**
|
||||
* 获取轮询的API key
|
||||
* 复用legacy架构的多key轮询逻辑
|
||||
@@ -58,6 +56,13 @@ function getRotatedApiKey(provider: Provider): string {
|
||||
* 处理特殊provider的转换逻辑
|
||||
*/
|
||||
function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
// if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
|
||||
// if (!isVertexAIConfigured()) {
|
||||
// throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
|
||||
// }
|
||||
// return createVertexProvider(provider)
|
||||
// }
|
||||
|
||||
if (isNewApiProvider(provider)) {
|
||||
return newApiResolverCreator(model, provider)
|
||||
}
|
||||
@@ -74,30 +79,43 @@ function handleSpecialProviders(model: Model, provider: Provider): Provider {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主要用来对齐AISdk的BaseURL格式
|
||||
* @param provider
|
||||
* @returns
|
||||
* 格式化provider的API Host
|
||||
*/
|
||||
function formatAnthropicApiHost(host: string): string {
|
||||
const trimmedHost = host?.trim()
|
||||
|
||||
if (!trimmedHost) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/')) {
|
||||
return trimmedHost
|
||||
}
|
||||
|
||||
if (trimmedHost.endsWith('/v1')) {
|
||||
return `${trimmedHost}/`
|
||||
}
|
||||
|
||||
return formatApiHost(trimmedHost)
|
||||
}
|
||||
|
||||
function formatProviderApiHost(provider: Provider): Provider {
|
||||
const formatted = { ...provider }
|
||||
if (formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatApiHost(formatted.anthropicApiHost)
|
||||
formatted.anthropicApiHost = formatAnthropicApiHost(formatted.anthropicApiHost)
|
||||
}
|
||||
|
||||
if (isAnthropicProvider(provider)) {
|
||||
if (formatted.type === 'anthropic') {
|
||||
const baseHost = formatted.anthropicApiHost || formatted.apiHost
|
||||
formatted.apiHost = formatApiHost(baseHost)
|
||||
formatted.apiHost = formatAnthropicApiHost(baseHost)
|
||||
if (!formatted.anthropicApiHost) {
|
||||
formatted.anthropicApiHost = formatted.apiHost
|
||||
}
|
||||
} else if (formatted.id === SystemProviderIds.copilot || formatted.id === SystemProviderIds.github) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isGeminiProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, true, 'v1beta')
|
||||
} else if (isAzureOpenAIProvider(formatted)) {
|
||||
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
|
||||
} else if (isVertexProvider(formatted)) {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else if (formatted.id === 'copilot') {
|
||||
const trimmed = trim(formatted.apiHost)
|
||||
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
|
||||
} else if (formatted.type === 'gemini') {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
@@ -131,15 +149,15 @@ export function providerToAiSdkConfig(
|
||||
options: ProviderSettingsMap[keyof ProviderSettingsMap]
|
||||
} {
|
||||
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
|
||||
logger.debug('providerToAiSdkConfig', { aiSdkProviderId })
|
||||
|
||||
// 构建基础配置
|
||||
const { baseURL, endpoint } = routeToEndpoint(actualProvider.apiHost)
|
||||
const baseConfig = {
|
||||
baseURL: baseURL,
|
||||
baseURL: trim(actualProvider.apiHost),
|
||||
apiKey: getRotatedApiKey(actualProvider)
|
||||
}
|
||||
|
||||
const isCopilotProvider = actualProvider.id === SystemProviderIds.copilot
|
||||
const isCopilotProvider = actualProvider.id === 'copilot'
|
||||
if (isCopilotProvider) {
|
||||
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
|
||||
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
|
||||
@@ -160,7 +178,6 @@ export function providerToAiSdkConfig(
|
||||
|
||||
// 处理OpenAI模式
|
||||
const extraOptions: any = {}
|
||||
extraOptions.endpoint = endpoint
|
||||
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
|
||||
extraOptions.mode = 'responses'
|
||||
} else if (aiSdkProviderId === 'openai') {
|
||||
@@ -182,11 +199,13 @@ export function providerToAiSdkConfig(
|
||||
}
|
||||
// azure
|
||||
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
|
||||
// extraOptions.apiVersion = actualProvider.apiVersion 默认使用v1,不使用azure endpoint
|
||||
extraOptions.apiVersion = actualProvider.apiVersion
|
||||
baseConfig.baseURL += '/openai'
|
||||
if (actualProvider.apiVersion === 'preview') {
|
||||
extraOptions.mode = 'responses'
|
||||
} else {
|
||||
extraOptions.mode = 'chat'
|
||||
extraOptions.useDeploymentBasedUrls = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +227,22 @@ export function providerToAiSdkConfig(
|
||||
...googleCredentials,
|
||||
privateKey: formatPrivateKey(googleCredentials.privateKey)
|
||||
}
|
||||
baseConfig.baseURL += aiSdkProviderId === 'google-vertex' ? '/publishers/google' : '/publishers/anthropic/models'
|
||||
// extraOptions.headers = window.api.vertexAI.getAuthHeaders({
|
||||
// projectId: project,
|
||||
// serviceAccount: {
|
||||
// privateKey: googleCredentials.privateKey,
|
||||
// clientEmail: googleCredentials.clientEmail
|
||||
// }
|
||||
// })
|
||||
if (baseConfig.baseURL.endsWith('/v1/')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -4)
|
||||
} else if (baseConfig.baseURL.endsWith('/v1')) {
|
||||
baseConfig.baseURL = baseConfig.baseURL.slice(0, -3)
|
||||
}
|
||||
|
||||
if (baseConfig.baseURL && !baseConfig.baseURL.includes('publishers/google')) {
|
||||
baseConfig.baseURL = `${baseConfig.baseURL}/v1/projects/${project}/locations/${location}/publishers/google`
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
|
||||
|
||||
@@ -63,14 +63,6 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
|
||||
creatorFunctionName: 'createMistral',
|
||||
supportsImageGeneration: false,
|
||||
aliases: ['mistral']
|
||||
},
|
||||
{
|
||||
id: 'huggingface',
|
||||
name: 'HuggingFace',
|
||||
import: () => import('@ai-sdk/huggingface'),
|
||||
creatorFunctionName: 'createHuggingFace',
|
||||
supportsImageGeneration: true,
|
||||
aliases: ['hf', 'hugging-face']
|
||||
}
|
||||
] as const
|
||||
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { isSystemProvider, Model, Provider, SystemProviderIds } from '@renderer/types'
|
||||
|
||||
export function buildGeminiGenerateImageParams(): Record<string, any> {
|
||||
return {
|
||||
responseModalities: ['TEXT', 'IMAGE']
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpenRouterGeminiGenerateImageModel(model: Model, provider: Provider): boolean {
|
||||
return (
|
||||
model.id.includes('gemini-2.5-flash-image') &&
|
||||
isSystemProvider(provider) &&
|
||||
provider.id === SystemProviderIds.openrouter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,9 +90,7 @@ export function buildProviderOptions(
|
||||
serviceTier: serviceTierSetting
|
||||
}
|
||||
break
|
||||
case 'huggingface':
|
||||
providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
|
||||
case 'anthropic':
|
||||
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||
break
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
isGrok4FastReasoningModel,
|
||||
isGrokReasoningModel,
|
||||
isOpenAIDeepResearchModel,
|
||||
isOpenAIModel,
|
||||
isOpenAIReasoningModel,
|
||||
isQwenAlwaysThinkModel,
|
||||
isQwenReasoningModel,
|
||||
@@ -67,8 +66,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
isGrokReasoningModel(model) ||
|
||||
isOpenAIReasoningModel(model) ||
|
||||
isQwenAlwaysThinkModel(model) ||
|
||||
model.id.includes('seed-oss') ||
|
||||
model.id.includes('minimax-m2')
|
||||
model.id.includes('seed-oss')
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
@@ -201,7 +199,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
||||
}
|
||||
}
|
||||
|
||||
// OpenRouter models, use reasoning
|
||||
// OpenRouter models, use thinking
|
||||
if (model.provider === SystemProviderIds.openrouter) {
|
||||
if (isSupportedReasoningEffortModel(model) || isSupportedThinkingTokenModel(model)) {
|
||||
return {
|
||||
@@ -320,20 +318,6 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re
|
||||
if (!isReasoningModel(model)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
let reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (!reasoningEffort) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// 非OpenAI模型,但是Provider类型是responses/azure openai的情况
|
||||
if (!isOpenAIModel(model)) {
|
||||
return {
|
||||
reasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
const openAI = getStoreSetting('openAI') as SettingsState['openAI']
|
||||
const summaryText = openAI?.summaryText || 'off'
|
||||
|
||||
@@ -345,10 +329,16 @@ export function getOpenAIReasoningParams(assistant: Assistant, model: Model): Re
|
||||
reasoningSummary = summaryText
|
||||
}
|
||||
|
||||
let reasoningEffort = assistant?.settings?.reasoning_effort
|
||||
|
||||
if (isOpenAIDeepResearchModel(model)) {
|
||||
reasoningEffort = 'medium'
|
||||
}
|
||||
|
||||
if (!reasoningEffort) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// OpenAI 推理参数
|
||||
if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.0006 25.9992C13.8266 25.999 11.7118 25.2901 9.97686 23.9799C8.2419 22.6698 6.98127 20.8298 6.38599 18.7388C5.79071 16.6478 5.89323 14.4198 6.678 12.3923C7.46278 10.3648 8.88705 8.64837 10.735 7.50308C12.5829 6.35779 14.7538 5.84606 16.9187 6.04544C19.0837 6.24481 21.1246 7.14442 22.7323 8.60795C24.34 10.0715 25.4268 12.0192 25.8281 14.1559C26.2293 16.2926 25.9232 18.5019 24.9561 20.449C24.7703 20.8042 24.7223 21.2155 24.8211 21.604L25.4211 23.8316C25.4803 24.0518 25.4805 24.2837 25.4216 24.5039C25.3627 24.7242 25.2468 24.925 25.0856 25.0862C24.9244 25.2474 24.7235 25.3633 24.5033 25.4222C24.283 25.4811 24.0512 25.4809 23.831 25.4217L21.6034 24.8217C21.2172 24.7248 20.809 24.7729 20.4558 24.9567C19.0683 25.6467 17.5457 26.0068 16.0006 26.0068V25.9992Z" fill="black"/>
|
||||
<path d="M9.62598 16.0013C9.62598 15.3799 10.1294 14.8765 10.7508 14.8765C11.3721 14.8765 11.8756 15.3799 11.8756 16.0013C11.8756 17.0953 12.3102 18.1448 13.0838 18.9184C13.8574 19.692 14.9069 20.1266 16.001 20.1267C17.095 20.1267 18.1445 19.692 18.9181 18.9184C19.6918 18.1448 20.1264 17.0953 20.1264 16.0013C20.1264 15.3799 20.6299 14.8765 21.2512 14.8765C21.8725 14.8765 22.3759 15.3799 22.3759 16.0013C22.3759 17.6921 21.7046 19.3137 20.509 20.5093C19.3134 21.7049 17.6918 22.3762 16.001 22.3762C14.3102 22.3762 12.6885 21.7049 11.4929 20.5093C10.2974 19.3137 9.62598 17.6921 9.62598 16.0013Z" fill="white"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
||||
<path
|
||||
fill="#FFD21E"
|
||||
d="M4 15.55C4 9.72 8.72 5 14.55 5h4.11a9.34 9.34 0 1 1 0 18.68H7.58l-2.89 2.8a.41.41 0 0 1-.69-.3V15.55Z"
|
||||
/>
|
||||
<path
|
||||
fill="#32343D"
|
||||
d="M19.63 12.48c.37.14.52.9.9.7.71-.38.98-1.27.6-1.98a1.46 1.46 0 0 0-1.98-.61 1.47 1.47 0 0 0-.6 1.99c.17.34.74-.21 1.08-.1ZM12.72 12.48c-.37.14-.52.9-.9.7a1.47 1.47 0 0 1-.6-1.98 1.46 1.46 0 0 1 1.98-.61c.71.38.98 1.27.6 1.99-.18.34-.74-.21-1.08-.1ZM16.24 19.55c2.89 0 3.82-2.58 3.82-3.9 0-1.33-1.71.7-3.82.7-2.1 0-3.8-2.03-3.8-.7 0 1.32.92 3.9 3.8 3.9Z"
|
||||
/>
|
||||
<path
|
||||
fill="#FF323D"
|
||||
d="M18.56 18.8c-.57.44-1.33.75-2.32.75-.92 0-1.65-.27-2.2-.68.3-.63.87-1.11 1.55-1.32.12-.03.24.17.36.38.12.2.24.4.37.4s.26-.2.39-.4.26-.4.38-.36a2.56 2.56 0 0 1 1.47 1.23Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 810 B |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@@ -5,16 +5,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// mock @hello-pangea/dnd 组件
|
||||
vi.mock('@hello-pangea/dnd', () => {
|
||||
return {
|
||||
|
||||
@@ -3,16 +3,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { DraggableVirtualList } from '../'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock 依赖项
|
||||
vi.mock('@hello-pangea/dnd', () => ({
|
||||
__esModule: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { restoreFromLocal } from '@renderer/services/BackupService'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, message, Modal, Space, Table, Tooltip } from 'antd'
|
||||
import { Button, message, Modal, Table, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -214,26 +214,6 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
}
|
||||
}
|
||||
|
||||
const footerContent = (
|
||||
<Space align="center">
|
||||
<Button key="refresh" icon={<ReloadOutlined />} onClick={fetchBackupFiles} disabled={loading}>
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
key="delete"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}
|
||||
loading={deleting}>
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('settings.data.local.backup.manager.title')}
|
||||
@@ -242,7 +222,23 @@ export function LocalBackupManager({ visible, onClose, localBackupDir, restoreMe
|
||||
width={800}
|
||||
centered
|
||||
transitionName="animation-move-down"
|
||||
footer={footerContent}>
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} onClick={fetchBackupFiles} disabled={loading}>
|
||||
{t('settings.data.local.backup.manager.refresh')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0 || deleting}
|
||||
loading={deleting}>
|
||||
{t('settings.data.local.backup.manager.delete.selected')} ({selectedRowKeys.length})
|
||||
</Button>,
|
||||
<Button key="close" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}>
|
||||
<Table
|
||||
rowKey="fileName"
|
||||
columns={columns}
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { permissionModeCards } from '@renderer/constants/permissionModes'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils/image'
|
||||
import { makeSvgSizeAdaptive } from '@renderer/utils'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,6 @@ interface BaseSelectorProps<V = string | number> {
|
||||
options: SelectorOption<V>[]
|
||||
placeholder?: string
|
||||
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight' | 'top' | 'bottom'
|
||||
style?: React.CSSProperties
|
||||
/** 字体大小 */
|
||||
size?: number
|
||||
/** 是否禁用 */
|
||||
@@ -44,7 +43,6 @@ const Selector = <V extends string | number>({
|
||||
placement = 'bottomRight',
|
||||
size = 13,
|
||||
placeholder,
|
||||
style,
|
||||
disabled = false,
|
||||
multiple = false
|
||||
}: SelectorProps<V>) => {
|
||||
@@ -137,7 +135,7 @@ const Selector = <V extends string | number>({
|
||||
placement={placement}
|
||||
open={open && !disabled}
|
||||
onOpenChange={handleOpenChange}>
|
||||
<Label style={style} $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
<Label $size={size} $open={open} $disabled={disabled} $isPlaceholder={label === placeholder}>
|
||||
{label}
|
||||
<LabelIcon size={size + 3} />
|
||||
</Label>
|
||||
|
||||
@@ -23,16 +23,6 @@ const mocks = vi.hoisted(() => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock antd components to prevent flaky snapshot tests
|
||||
vi.mock('antd', () => {
|
||||
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({
|
||||
|
||||
@@ -65,7 +65,7 @@ const NavbarContainer = styled.div<{ $isFullScreen: boolean }>`
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: ${({ $isFullScreen }) => (!$isFullScreen && isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)')};
|
||||
min-height: ${isMac ? 'env(titlebar-area-height)' : 'var(--navbar-height)'};
|
||||
max-height: var(--navbar-height);
|
||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1 + 2px)' : 0};
|
||||
padding-left: ${({ $isFullScreen }) =>
|
||||
|
||||
@@ -18,15 +18,6 @@ describe('Qwen Model Detection', () => {
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: vi.fn().mockReturnValue({ id: 'cherryai' })
|
||||
}))
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
test('isQwenReasoningModel', () => {
|
||||
expect(isQwenReasoningModel({ id: 'qwen3-thinking' } as Model)).toBe(true)
|
||||
|
||||
@@ -2,16 +2,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { isDoubaoSeedAfter251015, isDoubaoThinkingAutoModel, isLingReasoningModel } from '../models/reasoning'
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
default: {
|
||||
getState: () => ({
|
||||
llm: {
|
||||
settings: {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// FIXME: Idk why it's imported. Maybe circular dependency somewhere
|
||||
vi.mock('@renderer/services/AssistantService.ts', () => ({
|
||||
getDefaultAssistant: () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
|
||||
import { AgentBase, AgentType } from '@renderer/types'
|
||||
import { PermissionModeCard } from '@renderer/types/agent'
|
||||
|
||||
// base agent config. no default config for now.
|
||||
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
@@ -20,47 +19,3 @@ export const getAgentTypeAvatar = (type: AgentType): string => {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const permissionModeCards: PermissionModeCard[] = [
|
||||
{
|
||||
mode: 'default',
|
||||
// t('agent.settings.tooling.permissionMode.default.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.default.title',
|
||||
titleFallback: 'Default (ask before continuing)',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
|
||||
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
|
||||
behaviorFallback: 'Read-only tools are pre-approved automatically.'
|
||||
},
|
||||
{
|
||||
mode: 'plan',
|
||||
// t('agent.settings.tooling.permissionMode.plan.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
|
||||
titleFallback: 'Planning mode',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
|
||||
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
|
||||
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
|
||||
},
|
||||
{
|
||||
mode: 'acceptEdits',
|
||||
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
|
||||
titleFallback: 'Auto-accept file edits',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
|
||||
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
|
||||
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
|
||||
},
|
||||
{
|
||||
mode: 'bypassPermissions',
|
||||
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
|
||||
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
|
||||
titleFallback: 'Bypass permission checks',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
|
||||
descriptionFallback: 'All permission prompts are skipped — use with caution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
|
||||
behaviorFallback: 'Every tool is pre-approved automatically.',
|
||||
caution: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -22,7 +22,6 @@ import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?
|
||||
import GoogleAppLogo from '@renderer/assets/images/apps/google.svg?url'
|
||||
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
||||
import GrokXAppLogo from '@renderer/assets/images/apps/grok-x.png?url'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
|
||||
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
|
||||
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
|
||||
@@ -472,16 +471,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
style: {
|
||||
padding: 6
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'huggingchat',
|
||||
name: 'HuggingChat',
|
||||
url: 'https://huggingface.co/chat/',
|
||||
logo: HuggingChatLogo,
|
||||
bodered: true,
|
||||
style: {
|
||||
padding: 6
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1741,7 +1741,6 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
id: 'DeepSeek-R1',
|
||||
provider: 'cephalon',
|
||||
name: 'DeepSeek-R1满血版',
|
||||
capabilities: [{ type: 'reasoning' }],
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
@@ -1838,6 +1837,5 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
provider: 'longcat',
|
||||
group: 'LongCat'
|
||||
}
|
||||
],
|
||||
huggingface: []
|
||||
]
|
||||
}
|
||||
|
||||
@@ -361,12 +361,6 @@ export function isSupportedThinkingTokenDoubaoModel(model?: Model): boolean {
|
||||
return DOUBAO_THINKING_MODEL_REGEX.test(modelId) || DOUBAO_THINKING_MODEL_REGEX.test(model.name)
|
||||
}
|
||||
|
||||
export function isClaude45ReasoningModel(model: Model): boolean {
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
const regex = /claude-(sonnet|opus|haiku)-4(-|.)5(?:-[\w-]+)?$/i
|
||||
return regex.test(modelId)
|
||||
}
|
||||
|
||||
export function isClaudeReasoningModel(model?: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
@@ -461,14 +455,6 @@ export const isStepReasoningModel = (model?: Model): boolean => {
|
||||
return modelId.includes('step-3') || modelId.includes('step-r1-v-mini')
|
||||
}
|
||||
|
||||
export const isMiniMaxReasoningModel = (model?: Model): boolean => {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
return (['minimax-m1', 'minimax-m2'] as const).some((id) => modelId.includes(id))
|
||||
}
|
||||
|
||||
export function isReasoningModel(model?: Model): boolean {
|
||||
if (!model || isEmbeddingModel(model) || isRerankModel(model) || isTextToImageModel(model)) {
|
||||
return false
|
||||
@@ -503,8 +489,8 @@ export function isReasoningModel(model?: Model): boolean {
|
||||
isStepReasoningModel(model) ||
|
||||
isDeepSeekHybridInferenceModel(model) ||
|
||||
isLingReasoningModel(model) ||
|
||||
isMiniMaxReasoningModel(model) ||
|
||||
modelId.includes('magistral') ||
|
||||
modelId.includes('minimax-m1') ||
|
||||
modelId.includes('pangu-pro-moe') ||
|
||||
modelId.includes('seed-oss')
|
||||
) {
|
||||
|
||||
@@ -27,9 +27,8 @@ export const FUNCTION_CALLING_MODELS = [
|
||||
'doubao-seed-1[.-]6(?:-[\\w-]+)?',
|
||||
'kimi-k2(?:-[\\w-]+)?',
|
||||
'ling-\\w+(?:-[\\w-]+)?',
|
||||
'ring-\\w+(?:-[\\w-]+)?',
|
||||
'minimax-m2'
|
||||
] as const
|
||||
'ring-\\w+(?:-[\\w-]+)?'
|
||||
]
|
||||
|
||||
const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
||||
'aqa(?:-[\\w-]+)?',
|
||||
|
||||
@@ -83,7 +83,7 @@ export const IMAGE_ENHANCEMENT_MODELS = [
|
||||
'grok-2-image(?:-[\\w-]+)?',
|
||||
'qwen-image-edit',
|
||||
'gpt-image-1',
|
||||
'gemini-2.5-flash-image(?:-[\\w-]+)?',
|
||||
'gemini-2.5-flash-image',
|
||||
'gemini-2.0-flash-preview-image-generation'
|
||||
]
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { Model, SystemProviderIds } from '@renderer/types'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getLowerBaseModelName, isUserSelectedModelType } from '@renderer/utils'
|
||||
|
||||
import { isGeminiProvider, isNewApiProvider, isOpenAICompatibleProvider, isOpenAIProvider } from '../providers'
|
||||
import { isEmbeddingModel, isRerankModel } from './embedding'
|
||||
import { isAnthropicModel } from './utils'
|
||||
import { isPureGenerateImageModel, isTextToImageModel } from './vision'
|
||||
@@ -66,16 +65,12 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
|
||||
const modelId = getLowerBaseModelName(model.id, '/')
|
||||
|
||||
// bedrock和vertex不支持
|
||||
if (
|
||||
isAnthropicModel(model) &&
|
||||
(provider.id === SystemProviderIds['aws-bedrock'] || provider.id === SystemProviderIds.vertexai)
|
||||
) {
|
||||
// 不管哪个供应商都判断了
|
||||
if (isAnthropicModel(model)) {
|
||||
return CLAUDE_SUPPORTED_WEBSEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
// TODO: 当其他供应商采用Response端点时,这个地方逻辑需要改进
|
||||
if (isOpenAIProvider(provider)) {
|
||||
if (provider.type === 'openai-response') {
|
||||
if (isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
@@ -83,11 +78,11 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === SystemProviderIds.perplexity) {
|
||||
if (provider.id === 'perplexity') {
|
||||
return PERPLEXITY_SEARCH_MODELS.includes(modelId)
|
||||
}
|
||||
|
||||
if (provider.id === SystemProviderIds.aihubmix) {
|
||||
if (provider.id === 'aihubmix') {
|
||||
// modelId 不以-search结尾
|
||||
if (!modelId.endsWith('-search') && GEMINI_SEARCH_REGEX.test(modelId)) {
|
||||
return true
|
||||
@@ -100,13 +95,13 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (isOpenAICompatibleProvider(provider) || isNewApiProvider(provider)) {
|
||||
if (provider?.type === 'openai') {
|
||||
if (GEMINI_SEARCH_REGEX.test(modelId) || isOpenAIWebSearchModel(model)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (isGeminiProvider(provider) || provider.id === SystemProviderIds.vertexai) {
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini' || provider.type === 'vertexai') {
|
||||
return GEMINI_SEARCH_REGEX.test(modelId)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ export function getPreprocessProviderLogo(providerId: PreprocessProviderId) {
|
||||
return MistralLogo
|
||||
case 'mineru':
|
||||
return MinerULogo
|
||||
case 'open-mineru':
|
||||
return MinerULogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@@ -38,11 +36,5 @@ export const PREPROCESS_PROVIDER_CONFIG: Record<PreprocessProviderId, Preprocess
|
||||
official: 'https://mineru.net/',
|
||||
apiKey: 'https://mineru.net/apiManage'
|
||||
}
|
||||
},
|
||||
'open-mineru': {
|
||||
websites: {
|
||||
official: 'https://github.com/opendatalab/MinerU/',
|
||||
apiKey: 'https://github.com/opendatalab/MinerU/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import GoogleProviderLogo from '@renderer/assets/images/providers/google.png'
|
||||
import GPUStackProviderLogo from '@renderer/assets/images/providers/gpustack.svg'
|
||||
import GrokProviderLogo from '@renderer/assets/images/providers/grok.png'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import HuggingfaceProviderLogo from '@renderer/assets/images/providers/huggingface.webp'
|
||||
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
|
||||
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
|
||||
import IntelOvmsLogo from '@renderer/assets/images/providers/intel.png'
|
||||
@@ -58,7 +57,6 @@ import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import {
|
||||
AtLeast,
|
||||
AzureOpenAIProvider,
|
||||
isSystemProvider,
|
||||
OpenAIServiceTiers,
|
||||
Provider,
|
||||
@@ -356,7 +354,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
name: 'VertexAI',
|
||||
type: 'vertexai',
|
||||
apiKey: '',
|
||||
apiHost: '',
|
||||
apiHost: 'https://aiplatform.googleapis.com',
|
||||
models: SYSTEM_MODELS.vertexai,
|
||||
isSystem: true,
|
||||
enabled: false,
|
||||
@@ -420,7 +418,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/apps/anthropic',
|
||||
anthropicApiHost: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy',
|
||||
models: SYSTEM_MODELS.dashscope,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
@@ -655,16 +653,6 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
||||
models: SYSTEM_MODELS.longcat,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
huggingface: {
|
||||
id: 'huggingface',
|
||||
name: 'Hugging Face',
|
||||
type: 'openai-response',
|
||||
apiKey: '',
|
||||
apiHost: 'https://router.huggingface.co/v1/',
|
||||
models: [],
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -729,8 +717,7 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
|
||||
'aws-bedrock': AwsProviderLogo,
|
||||
poe: 'poe', // use svg icon component
|
||||
aionly: AiOnlyProviderLogo,
|
||||
longcat: LongCatProviderLogo,
|
||||
huggingface: HuggingfaceProviderLogo
|
||||
longcat: LongCatProviderLogo
|
||||
} as const
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
@@ -1296,7 +1283,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
},
|
||||
vertexai: {
|
||||
api: {
|
||||
url: ''
|
||||
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://cloud.google.com/vertex-ai',
|
||||
@@ -1357,17 +1344,6 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||
docs: 'https://longcat.chat/platform/docs/zh/',
|
||||
models: 'https://longcat.chat/platform/docs/zh/APIDocs.html'
|
||||
}
|
||||
},
|
||||
huggingface: {
|
||||
api: {
|
||||
url: 'https://router.huggingface.co/v1/'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://huggingface.co/',
|
||||
apiKey: 'https://huggingface.co/settings/tokens',
|
||||
docs: 'https://huggingface.co/docs',
|
||||
models: 'https://huggingface.co/models'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,8 +1352,7 @@ const NOT_SUPPORT_ARRAY_CONTENT_PROVIDERS = [
|
||||
'baichuan',
|
||||
'minimax',
|
||||
'xirang',
|
||||
'poe',
|
||||
'cephalon'
|
||||
'poe'
|
||||
] as const satisfies SystemProviderId[]
|
||||
|
||||
/**
|
||||
@@ -1442,15 +1417,10 @@ export const isSupportServiceTierProvider = (provider: Provider) => {
|
||||
)
|
||||
}
|
||||
|
||||
const SUPPORT_URL_CONTEXT_PROVIDER_TYPES = [
|
||||
'gemini',
|
||||
'vertexai',
|
||||
'anthropic',
|
||||
'new-api'
|
||||
] as const satisfies ProviderType[]
|
||||
const SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES = ['gemini', 'vertexai'] as const satisfies ProviderType[]
|
||||
|
||||
export const isSupportUrlContextProvider = (provider: Provider) => {
|
||||
return SUPPORT_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
return SUPPORT_GEMINI_URL_CONTEXT_PROVIDER_TYPES.some((type) => type === provider.type)
|
||||
}
|
||||
|
||||
const SUPPORT_GEMINI_NATIVE_WEB_SEARCH_PROVIDERS = ['gemini', 'vertexai'] as const satisfies SystemProviderId[]
|
||||
@@ -1463,37 +1433,3 @@ export const isGeminiWebSearchProvider = (provider: Provider) => {
|
||||
export const isNewApiProvider = (provider: Provider) => {
|
||||
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
* @returns {boolean} 是否为 OpenAI 兼容提供商
|
||||
*/
|
||||
export function isOpenAICompatibleProvider(provider: Provider): boolean {
|
||||
return ['openai', 'new-api', 'mistral'].includes(provider.type)
|
||||
}
|
||||
|
||||
export function isAzureOpenAIProvider(provider: Provider): provider is AzureOpenAIProvider {
|
||||
return provider.type === 'azure-openai'
|
||||
}
|
||||
|
||||
export function isOpenAIProvider(provider: Provider): boolean {
|
||||
return provider.type === 'openai-response'
|
||||
}
|
||||
|
||||
export function isAnthropicProvider(provider: Provider): boolean {
|
||||
return provider.type === 'anthropic'
|
||||
}
|
||||
|
||||
export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
return !NOT_SUPPORT_API_VERSION_PROVIDERS.some((pid) => pid === provider.id)
|
||||
}
|
||||
return provider.apiOptions?.isNotSupportAPIVersion !== false
|
||||
}
|
||||
|
||||
53
src/renderer/src/constants/permissionModes.ts
Normal file
53
src/renderer/src/constants/permissionModes.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { PermissionMode } from '@renderer/types'
|
||||
|
||||
export type PermissionModeCard = {
|
||||
mode: PermissionMode
|
||||
titleKey: string
|
||||
titleFallback: string
|
||||
descriptionKey: string
|
||||
descriptionFallback: string
|
||||
behaviorKey: string
|
||||
behaviorFallback: string
|
||||
caution?: boolean
|
||||
unsupported?: boolean
|
||||
}
|
||||
|
||||
export const permissionModeCards: PermissionModeCard[] = [
|
||||
{
|
||||
mode: 'default',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.default.title',
|
||||
titleFallback: 'Default (ask before continuing)',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
|
||||
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
|
||||
behaviorFallback: 'Read-only tools are pre-approved automatically.'
|
||||
},
|
||||
{
|
||||
mode: 'plan',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
|
||||
titleFallback: 'Planning mode',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
|
||||
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
|
||||
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
|
||||
},
|
||||
{
|
||||
mode: 'acceptEdits',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
|
||||
titleFallback: 'Auto-accept file edits',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
|
||||
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
|
||||
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
|
||||
},
|
||||
{
|
||||
mode: 'bypassPermissions',
|
||||
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
|
||||
titleFallback: 'Bypass permission checks',
|
||||
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
|
||||
descriptionFallback: 'All permission prompts are skipped — use with caution.',
|
||||
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
|
||||
behaviorFallback: 'Every tool is pre-approved automatically.',
|
||||
caution: true
|
||||
}
|
||||
]
|
||||
10
src/renderer/src/env.d.ts
vendored
10
src/renderer/src/env.d.ts
vendored
@@ -1,6 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { HookAPI } from 'antd/es/modal/useModal'
|
||||
@@ -35,14 +34,5 @@ declare global {
|
||||
info: typeof info
|
||||
loading: typeof loading
|
||||
}
|
||||
agentTools: {
|
||||
respondToPermission: (payload: {
|
||||
requestId: string
|
||||
behavior: 'allow' | 'deny'
|
||||
updatedInput?: Record<string, unknown>
|
||||
message?: string
|
||||
updatedPermissions?: PermissionUpdate[]
|
||||
}) => Promise<{ success: boolean }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveSessionIdAction, setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { CreateSessionForm } from '@renderer/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/**
|
||||
* Returns a stable callback that creates a default agent session and updates UI state.
|
||||
*/
|
||||
export const useCreateDefaultSession = (agentId: string | null) => {
|
||||
const { agent } = useAgent(agentId)
|
||||
const { createSession } = useSessions(agentId)
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
const [creatingSession, setCreatingSession] = useState(false)
|
||||
|
||||
const createDefaultSession = useCallback(async () => {
|
||||
if (!agentId || !agent || creatingSession) {
|
||||
return null
|
||||
}
|
||||
|
||||
setCreatingSession(true)
|
||||
try {
|
||||
const session = {
|
||||
...agent,
|
||||
id: undefined,
|
||||
name: t('common.unnamed')
|
||||
} satisfies CreateSessionForm
|
||||
|
||||
const created = await createSession(session)
|
||||
|
||||
if (created) {
|
||||
dispatch(setActiveSessionIdAction({ agentId, sessionId: created.id }))
|
||||
dispatch(setActiveTopicOrSessionAction('session'))
|
||||
}
|
||||
|
||||
return created
|
||||
} finally {
|
||||
setCreatingSession(false)
|
||||
}
|
||||
}, [agentId, agent, createSession, creatingSession, dispatch, t])
|
||||
|
||||
return {
|
||||
createDefaultSession,
|
||||
creatingSession
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,12 @@ import { useAppSelector } from '@renderer/store'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { selectMemoryConfig } from '@renderer/store/memory'
|
||||
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import {
|
||||
type ToolPermissionRequestPayload,
|
||||
type ToolPermissionResultPayload,
|
||||
toolPermissionsActions
|
||||
} from '@renderer/store/toolPermissions'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { checkDataLimit } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import useFullScreenNotice from './useFullScreenNotice'
|
||||
@@ -33,7 +27,6 @@ import useUpdateHandler from './useUpdateHandler'
|
||||
const logger = loggerService.withContext('useAppInit')
|
||||
|
||||
export function useAppInit() {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
const {
|
||||
proxyUrl,
|
||||
@@ -155,64 +148,6 @@ export function useAppInit() {
|
||||
}
|
||||
}, [customCss])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron?.ipcRenderer) return
|
||||
|
||||
const requestListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionRequestPayload) => {
|
||||
logger.debug('Renderer received tool permission request', {
|
||||
requestId: payload.requestId,
|
||||
toolName: payload.toolName,
|
||||
expiresAt: payload.expiresAt,
|
||||
suggestionCount: payload.suggestions.length
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestReceived(payload))
|
||||
}
|
||||
|
||||
const resultListener = (_event: Electron.IpcRendererEvent, payload: ToolPermissionResultPayload) => {
|
||||
logger.debug('Renderer received tool permission result', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
dispatch(toolPermissionsActions.requestResolved(payload))
|
||||
|
||||
if (payload.behavior === 'deny') {
|
||||
const message =
|
||||
payload.reason === 'timeout'
|
||||
? (payload.message ?? t('agent.toolPermission.toast.timeout'))
|
||||
: (payload.message ?? t('agent.toolPermission.toast.denied'))
|
||||
|
||||
if (payload.reason === 'no-window') {
|
||||
logger.debug('Displaying deny toast for tool permission', {
|
||||
requestId: payload.requestId,
|
||||
behavior: payload.behavior,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.error?.(message)
|
||||
} else if (payload.reason === 'timeout') {
|
||||
logger.debug('Displaying timeout toast for tool permission', {
|
||||
requestId: payload.requestId
|
||||
})
|
||||
window.toast?.warning?.(message)
|
||||
} else {
|
||||
logger.debug('Displaying info toast for tool permission deny', {
|
||||
requestId: payload.requestId,
|
||||
reason: payload.reason
|
||||
})
|
||||
window.toast?.info?.(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: init data collection
|
||||
}, [enableDataCollection])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addAssistantPreset,
|
||||
@@ -9,22 +8,8 @@ import {
|
||||
} from '@renderer/store/assistants'
|
||||
import { AssistantPreset, AssistantSettings } from '@renderer/types'
|
||||
|
||||
const logger = loggerService.withContext('useAssistantPresets')
|
||||
|
||||
function ensurePresetsArray(storedPresets: unknown): AssistantPreset[] {
|
||||
if (Array.isArray(storedPresets)) {
|
||||
return storedPresets
|
||||
}
|
||||
logger.warn('Unexpected data type from state.assistants.presets, falling back to empty list.', {
|
||||
type: typeof storedPresets,
|
||||
value: storedPresets
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
export function useAssistantPresets() {
|
||||
const storedPresets = useAppSelector((state) => state.assistants.presets)
|
||||
const presets = ensurePresetsArray(storedPresets)
|
||||
const presets = useAppSelector((state) => state.assistants.presets)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
@@ -36,23 +21,14 @@ export function useAssistantPresets() {
|
||||
}
|
||||
|
||||
export function useAssistantPreset(id: string) {
|
||||
const storedPresets = useAppSelector((state) => state.assistants.presets)
|
||||
const presets = ensurePresetsArray(storedPresets)
|
||||
const preset = presets.find((a) => a.id === id)
|
||||
// FIXME: undefined is not handled
|
||||
const preset = useAppSelector((state) => state.assistants.presets.find((a) => a.id === id) as AssistantPreset)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
if (!preset) {
|
||||
logger.warn(`Assistant preset with id ${id} not found in state.`)
|
||||
}
|
||||
|
||||
return {
|
||||
preset: preset,
|
||||
preset,
|
||||
updateAssistantPreset: (preset: AssistantPreset) => dispatch(updateAssistantPreset(preset)),
|
||||
updateAssistantPresetSettings: (settings: Partial<AssistantSettings>) => {
|
||||
if (!preset) {
|
||||
logger.warn(`Failed to update assistant preset settings because preset with id ${id} is missing.`)
|
||||
return
|
||||
}
|
||||
dispatch(updateAssistantPresetSettings({ assistantId: preset.id, settings }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
saveEdit()
|
||||
} else if (e.key === 'Escape') {
|
||||
|
||||
@@ -57,7 +57,7 @@ export const useKnowledgeBaseForm = (base?: KnowledgeBase) => {
|
||||
label: t('settings.tool.preprocess.provider'),
|
||||
title: t('settings.tool.preprocess.provider'),
|
||||
options: preprocessProviders
|
||||
.filter((p) => p.apiKey !== '' || ['mineru', 'open-mineru'].includes(p.id))
|
||||
.filter((p) => p.apiKey !== '' || p.id === 'mineru')
|
||||
.map((p) => ({ value: p.id, label: p.name }))
|
||||
}
|
||||
return [preprocessOptions]
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import type { InstalledPlugin, PluginError, PluginMetadata } from '@renderer/types/plugin'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Helper to extract error message from PluginError union type
|
||||
*/
|
||||
function getPluginErrorMessage(error: PluginError, defaultMessage: string): string {
|
||||
if ('message' in error && error.message) return error.message
|
||||
if ('reason' in error) return error.reason
|
||||
if ('path' in error) return `Error with file: ${error.path}`
|
||||
return defaultMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch and cache available plugins from the resources directory
|
||||
* @returns Object containing available agents, commands, skills, loading state, and error
|
||||
*/
|
||||
export function useAvailablePlugins() {
|
||||
const [agents, setAgents] = useState<PluginMetadata[]>([])
|
||||
const [commands, setCommands] = useState<PluginMetadata[]>([])
|
||||
const [skills, setSkills] = useState<PluginMetadata[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailablePlugins = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listAvailable()
|
||||
|
||||
if (result.success) {
|
||||
setAgents(result.data.agents)
|
||||
setCommands(result.data.commands)
|
||||
setSkills(result.data.skills)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load available plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchAvailablePlugins()
|
||||
}, [])
|
||||
|
||||
return { agents, commands, skills, loading, error }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch installed plugins for a specific agent
|
||||
* @param agentId - The ID of the agent to fetch plugins for
|
||||
* @returns Object containing installed plugins, loading state, error, and refresh function
|
||||
*/
|
||||
export function useInstalledPlugins(agentId: string | undefined) {
|
||||
const [plugins, setPlugins] = useState<InstalledPlugin[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!agentId) {
|
||||
setPlugins([])
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.listInstalled(agentId)
|
||||
|
||||
if (result.success) {
|
||||
setPlugins(result.data)
|
||||
} else {
|
||||
setError(getPluginErrorMessage(result.error, 'Failed to load installed plugins'))
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [agentId])
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
return { plugins, loading, error, refresh }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to provide install and uninstall actions for plugins
|
||||
* @param agentId - The ID of the agent to perform actions for
|
||||
* @param onSuccess - Optional callback to be called on successful operations
|
||||
* @returns Object containing install, uninstall functions and their loading states
|
||||
*/
|
||||
export function usePluginActions(agentId: string, onSuccess?: () => void) {
|
||||
const [installing, setInstalling] = useState<boolean>(false)
|
||||
const [uninstalling, setUninstalling] = useState<boolean>(false)
|
||||
|
||||
const install = useCallback(
|
||||
async (sourcePath: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setInstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.install({
|
||||
agentId,
|
||||
sourcePath,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const, data: result.data }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to install plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setInstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
const uninstall = useCallback(
|
||||
async (filename: string, type: 'agent' | 'command' | 'skill') => {
|
||||
setUninstalling(true)
|
||||
|
||||
try {
|
||||
const result = await window.api.claudeCodePlugin.uninstall({
|
||||
agentId,
|
||||
filename,
|
||||
type
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
onSuccess?.()
|
||||
return { success: true as const }
|
||||
} else {
|
||||
const errorMessage = getPluginErrorMessage(result.error, 'Failed to uninstall plugin')
|
||||
return { success: false as const, error: errorMessage }
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
|
||||
return { success: false as const, error: errorMessage }
|
||||
} finally {
|
||||
setUninstalling(false)
|
||||
}
|
||||
},
|
||||
[agentId, onSuccess]
|
||||
)
|
||||
|
||||
return { install, uninstall, installing, uninstalling }
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function getVertexAIServiceAccount() {
|
||||
* 类型守卫:检查 Provider 是否为 VertexProvider
|
||||
*/
|
||||
export function isVertexProvider(provider: Provider): provider is VertexProvider {
|
||||
return provider.type === 'vertexai'
|
||||
return provider.type === 'vertexai' && 'googleCredentials' in provider
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,9 +88,7 @@ const providerKeyMap = {
|
||||
zhinao: 'provider.zhinao',
|
||||
zhipu: 'provider.zhipu',
|
||||
poe: 'provider.poe',
|
||||
aionly: 'provider.aionly',
|
||||
longcat: 'provider.longcat',
|
||||
huggingface: 'provider.huggingface'
|
||||
aionly: 'provider.aionly'
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -165,21 +163,9 @@ export const getThemeModeLabel = (key: string): string => {
|
||||
return getLabel(themeModeKeyMap, key)
|
||||
}
|
||||
|
||||
// const sidebarIconKeyMap = {
|
||||
// assistants: t('assistants.title'),
|
||||
// store: t('assistants.presets.title'),
|
||||
// paintings: t('paintings.title'),
|
||||
// translate: t('translate.title'),
|
||||
// minapp: t('minapp.title'),
|
||||
// knowledge: t('knowledge.title'),
|
||||
// files: t('files.title'),
|
||||
// code_tools: t('code.title'),
|
||||
// notes: t('notes.title')
|
||||
// } as const
|
||||
|
||||
const sidebarIconKeyMap = {
|
||||
assistants: 'assistants.title',
|
||||
store: 'assistants.presets.title',
|
||||
agents: 'agents.title',
|
||||
paintings: 'paintings.title',
|
||||
translate: 'translate.title',
|
||||
minapp: 'minapp.title',
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Advanced Settings"
|
||||
},
|
||||
"essential": "Essential Settings",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Available Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Are you sure you want to uninstall this plugin?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "No plugins found matching your filters. Try adjusting your search or category filters."
|
||||
},
|
||||
"error": {
|
||||
"install": "Failed to install plugin",
|
||||
"load": "Failed to load plugins",
|
||||
"uninstall": "Failed to uninstall plugin"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All Categories"
|
||||
},
|
||||
"install": "Install",
|
||||
"installed": {
|
||||
"empty": "No plugins installed yet. Browse available plugins to get started.",
|
||||
"title": "Installed Plugins"
|
||||
},
|
||||
"installing": "Installing...",
|
||||
"results": "{{count}} plugin(s) found",
|
||||
"search": {
|
||||
"placeholder": "Search plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin installed successfully",
|
||||
"uninstall": "Plugin uninstalled successfully"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agents",
|
||||
"all": "All",
|
||||
"command": "Command",
|
||||
"commands": "Commands",
|
||||
"skills": "Skills"
|
||||
},
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"prompt": "Prompt Settings",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Allow tool request",
|
||||
"denyRequest": "Deny tool request",
|
||||
"hideDetails": "Hide tool details",
|
||||
"runWithOptions": "Run with additional options",
|
||||
"showDetails": "Show tool details"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancel",
|
||||
"run": "Run"
|
||||
},
|
||||
"confirmation": "Are you sure you want to run this Claude tool?",
|
||||
"defaultDenyMessage": "User denied permission for this tool.",
|
||||
"defaultDescription": "Executes code or system actions in your environment. Make sure the command looks safe before running it.",
|
||||
"error": {
|
||||
"sendFailed": "Failed to send your decision. Please try again."
|
||||
},
|
||||
"expired": "Expired",
|
||||
"inputPreview": "Tool input preview",
|
||||
"pending": "Pending ({{seconds}}s)",
|
||||
"permissionExpired": "Permission request expired. Waiting for new instructions...",
|
||||
"requiresElevatedPermissions": "This tool requires elevated permissions.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Approving may update multiple session permissions if you chose to always allow this tool.",
|
||||
"permissionUpdateSingle": "Approving may update your session permissions if you chose to always allow this tool."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool request was denied.",
|
||||
"timeout": "Tool request timed out before receiving approval."
|
||||
},
|
||||
"waiting": "Waiting for tool permission decision..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"unknown": "Unknown Type"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Controls upscaling randomness"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Actions",
|
||||
"agents": "Agents",
|
||||
"all_categories": "All Categories",
|
||||
"all_types": "All",
|
||||
"category": "Category",
|
||||
"commands": "Commands",
|
||||
"confirm_uninstall": "Are you sure you want to uninstall {{name}}?",
|
||||
"install": "Install",
|
||||
"install_plugins_from_browser": "Browse available plugins to get started",
|
||||
"installing": "Installing...",
|
||||
"name": "Name",
|
||||
"no_description": "No description available",
|
||||
"no_installed_plugins": "No plugins installed yet",
|
||||
"no_results": "No plugins found",
|
||||
"search_placeholder": "Search plugins...",
|
||||
"showing_results": "Showing {{count}} plugin",
|
||||
"showing_results_one": "Showing {{count}} plugin",
|
||||
"showing_results_other": "Showing {{count}} plugins",
|
||||
"showing_results_plural": "Showing {{count}} plugins",
|
||||
"skills": "Skills",
|
||||
"try_different_search": "Try adjusting your search or category filters",
|
||||
"type": "Type",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstalling": "Uninstalling..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copy as image"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "Infini",
|
||||
"jina": "Jina",
|
||||
"lanyun": "LANYUN",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "LongCat AI",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API Host",
|
||||
"anthropic_api_host_preview": "Anthropic preview: {{url}}",
|
||||
"anthropic_api_host_tip": "Only configure this when your provider exposes an Anthropic-compatible endpoint. Ending with / ignores v1, ending with # forces use of input address.",
|
||||
"anthropic_api_host_tooltip": "Use only when the provider offers a Claude-compatible base URL.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Preview: {{url}}",
|
||||
"reset": "Reset",
|
||||
"tip": "ending with # forces use of input address"
|
||||
"tip": "Ending with / ignores v1, ending with # forces use of input address"
|
||||
}
|
||||
},
|
||||
"api_host": "API Host",
|
||||
"api_host_no_valid": "API address is invalid",
|
||||
"api_host_preview": "Preview: {{url}}",
|
||||
"api_host_tooltip": "Override only when your provider requires a custom OpenAI-compatible endpoint.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "高级设置"
|
||||
},
|
||||
"essential": "基础设置",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用插件"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "确定要卸载此插件吗?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到匹配的插件。请尝试调整搜索或类别筛选。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安装插件失败",
|
||||
"load": "加载插件失败",
|
||||
"uninstall": "卸载插件失败"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有类别"
|
||||
},
|
||||
"install": "安装",
|
||||
"installed": {
|
||||
"empty": "尚未安装任何插件。浏览可用插件以开始使用。",
|
||||
"title": "已安装插件"
|
||||
},
|
||||
"installing": "安装中...",
|
||||
"results": "找到 {{count}} 个插件",
|
||||
"search": {
|
||||
"placeholder": "搜索插件..."
|
||||
},
|
||||
"success": {
|
||||
"install": "插件安装成功",
|
||||
"uninstall": "插件卸载成功"
|
||||
},
|
||||
"tab": "插件",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "命令",
|
||||
"commands": "命令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"prompt": "提示词设置",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允许工具请求",
|
||||
"denyRequest": "拒绝工具请求",
|
||||
"hideDetails": "隐藏工具详情",
|
||||
"runWithOptions": "带选项运行",
|
||||
"showDetails": "显示工具详情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "运行"
|
||||
},
|
||||
"confirmation": "确定要运行此 Claude 工具吗?",
|
||||
"defaultDenyMessage": "用户拒绝了该工具的权限。",
|
||||
"defaultDescription": "在您的环境中执行代码或系统操作。运行前请确保命令安全。",
|
||||
"error": {
|
||||
"sendFailed": "发送您的决定失败,请重试。"
|
||||
},
|
||||
"expired": "已过期",
|
||||
"inputPreview": "工具输入预览",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "权限请求已过期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要更高权限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您选择总是允许此工具,批准可能会更新多个会话权限。",
|
||||
"permissionUpdateSingle": "如果您选择总是允许此工具,批准可能会更新您的会话权限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具请求已被拒绝。",
|
||||
"timeout": "工具请求在收到批准前超时。"
|
||||
},
|
||||
"waiting": "等待工具权限决定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "智能体类型",
|
||||
"unknown": "未知类型"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "控制放大结果的随机性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有类别",
|
||||
"all_types": "全部",
|
||||
"category": "类别",
|
||||
"commands": "命令",
|
||||
"confirm_uninstall": "确定要卸载 {{name}} 吗?",
|
||||
"install": "安装",
|
||||
"install_plugins_from_browser": "浏览可用插件以开始使用",
|
||||
"installing": "安装中...",
|
||||
"name": "名称",
|
||||
"no_description": "无描述",
|
||||
"no_installed_plugins": "尚未安装任何插件",
|
||||
"no_results": "未找到插件",
|
||||
"search_placeholder": "搜索插件...",
|
||||
"showing_results": "显示 {{count}} 个插件",
|
||||
"showing_results_one": "显示 {{count}} 个插件",
|
||||
"showing_results_other": "显示 {{count}} 个插件",
|
||||
"showing_results_plural": "显示 {{count}} 个插件",
|
||||
"skills": "技能",
|
||||
"try_different_search": "请尝试调整搜索或类别筛选",
|
||||
"type": "类型",
|
||||
"uninstall": "卸载",
|
||||
"uninstalling": "卸载中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "复制为图片"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "腾讯混元",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "无问芯穹",
|
||||
"jina": "Jina",
|
||||
"lanyun": "蓝耘科技",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "龙猫",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope 魔搭",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 地址",
|
||||
"anthropic_api_host_preview": "Anthropic 预览:{{url}}",
|
||||
"anthropic_api_host_tip": "仅在服务商提供兼容 Anthropic 的地址时填写。以 / 结尾会忽略自动追加的 v1,以 # 结尾则强制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "仅当服务商提供 Claude 兼容的基础地址时填写。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "预览: {{url}}",
|
||||
"reset": "重置",
|
||||
"tip": "# 结尾强制使用输入地址"
|
||||
"tip": "/ 结尾忽略 v1 版本,# 结尾强制使用输入地址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 地址",
|
||||
"api_host_no_valid": "API 地址不合法",
|
||||
"api_host_preview": "预览:{{url}}",
|
||||
"api_host_tooltip": "仅在服务商需要自定义的 OpenAI 兼容地址时覆盖。",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "進階設定"
|
||||
},
|
||||
"essential": "必要設定",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "可用外掛"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "確定要解除安裝此外掛嗎?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "未找到符合的外掛。請嘗試調整搜尋或類別篩選。"
|
||||
},
|
||||
"error": {
|
||||
"install": "安裝外掛失敗",
|
||||
"load": "載入外掛失敗",
|
||||
"uninstall": "解除安裝外掛失敗"
|
||||
},
|
||||
"filter": {
|
||||
"all": "所有類別"
|
||||
},
|
||||
"install": "安裝",
|
||||
"installed": {
|
||||
"empty": "尚未安裝任何外掛。瀏覽可用外掛以開始使用。",
|
||||
"title": "已安裝外掛"
|
||||
},
|
||||
"installing": "安裝中...",
|
||||
"results": "找到 {{count}} 個外掛",
|
||||
"search": {
|
||||
"placeholder": "搜尋外掛..."
|
||||
},
|
||||
"success": {
|
||||
"install": "外掛安裝成功",
|
||||
"uninstall": "外掛解除安裝成功"
|
||||
},
|
||||
"tab": "外掛",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "指令",
|
||||
"commands": "指令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"prompt": "提示設定",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "允許工具請求",
|
||||
"denyRequest": "拒絕工具請求",
|
||||
"hideDetails": "隱藏工具詳情",
|
||||
"runWithOptions": "帶選項執行",
|
||||
"showDetails": "顯示工具詳情"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "取消",
|
||||
"run": "執行"
|
||||
},
|
||||
"confirmation": "確定要執行此 Claude 工具嗎?",
|
||||
"defaultDenyMessage": "使用者拒絕了該工具的權限。",
|
||||
"defaultDescription": "在您的環境中執行程式碼或系統操作。執行前請確保指令安全。",
|
||||
"error": {
|
||||
"sendFailed": "傳送您的決定失敗,請重試。"
|
||||
},
|
||||
"expired": "已過期",
|
||||
"inputPreview": "工具輸入預覽",
|
||||
"pending": "等待中 ({{seconds}}秒)",
|
||||
"permissionExpired": "權限請求已過期。等待新指令...",
|
||||
"requiresElevatedPermissions": "此工具需要提升的權限。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "如果您選擇總是允許此工具,核准可能會更新多個工作階段權限。",
|
||||
"permissionUpdateSingle": "如果您選擇總是允許此工具,核准可能會更新您的工作階段權限。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "工具請求已被拒絕。",
|
||||
"timeout": "工具請求在收到核准前逾時。"
|
||||
},
|
||||
"waiting": "等待工具權限決定..."
|
||||
},
|
||||
"type": {
|
||||
"label": "代理類型",
|
||||
"unknown": "未知類型"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "控制放大結果的隨機性"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "所有類別",
|
||||
"all_types": "全部",
|
||||
"category": "類別",
|
||||
"commands": "指令",
|
||||
"confirm_uninstall": "確定要解除安裝 {{name}} 嗎?",
|
||||
"install": "安裝",
|
||||
"install_plugins_from_browser": "瀏覽可用外掛以開始使用",
|
||||
"installing": "安裝中...",
|
||||
"name": "名稱",
|
||||
"no_description": "無描述",
|
||||
"no_installed_plugins": "尚未安裝任何外掛",
|
||||
"no_results": "未找到外掛",
|
||||
"search_placeholder": "搜尋外掛...",
|
||||
"showing_results": "顯示 {{count}} 個外掛",
|
||||
"showing_results_one": "顯示 {{count}} 個外掛",
|
||||
"showing_results_other": "顯示 {{count}} 個外掛",
|
||||
"showing_results_plural": "顯示 {{count}} 個外掛",
|
||||
"skills": "技能",
|
||||
"try_different_search": "請嘗試調整搜尋或類別篩選",
|
||||
"type": "類型",
|
||||
"uninstall": "解除安裝",
|
||||
"uninstalling": "解除安裝中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "複製為圖片"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "騰訊混元",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "無問芯穹",
|
||||
"jina": "Jina",
|
||||
"lanyun": "藍耘",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "龍貓",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope 魔搭",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API 主機地址",
|
||||
"anthropic_api_host_preview": "Anthropic 預覽:{{url}}",
|
||||
"anthropic_api_host_tip": "僅在服務商提供與 Anthropic 相容的網址時設定。以 / 結尾會忽略自動附加的 v1,以 # 結尾則強制使用原始地址。",
|
||||
"anthropic_api_host_tooltip": "僅在服務商提供 Claude 相容的基礎網址時設定。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "預覽:{{url}}",
|
||||
"reset": "重設",
|
||||
"tip": "# 結尾強制使用輸入位址"
|
||||
"tip": "/ 結尾忽略 v1 版本,# 結尾強制使用輸入位址"
|
||||
}
|
||||
},
|
||||
"api_host": "API 主機地址",
|
||||
"api_host_no_valid": "API 位址不合法",
|
||||
"api_host_preview": "預覽:{{url}}",
|
||||
"api_host_tooltip": "僅在服務商需要自訂的 OpenAI 相容端點時才覆蓋。",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Erweiterte Einstellungen"
|
||||
},
|
||||
"essential": "Grundeinstellungen",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Verfügbare Plugins"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Sind Sie sicher, dass Sie dieses Plugin deinstallieren möchten?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Keine Plugins gefunden, die deinen Filtern entsprechen. Versuche, deine Such- oder Kategoriefilter anzupassen."
|
||||
},
|
||||
"error": {
|
||||
"install": "Fehler beim Installieren des Plugins",
|
||||
"load": "Fehler beim Laden der Plugins",
|
||||
"uninstall": "Fehler beim Deinstallieren des Plugins"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Alle Kategorien"
|
||||
},
|
||||
"install": "Installieren",
|
||||
"installed": {
|
||||
"empty": "Noch keine Plugins installiert. Durchsuche verfügbare Plugins, um loszulegen.",
|
||||
"title": "Installierte Plugins"
|
||||
},
|
||||
"installing": "Wird installiert...",
|
||||
"results": "{{count}} Plugin(s) gefunden",
|
||||
"search": {
|
||||
"placeholder": "Such-Plugins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin erfolgreich installiert",
|
||||
"uninstall": "Plugin erfolgreich deinstalliert"
|
||||
},
|
||||
"tab": "Plugins",
|
||||
"type": {
|
||||
"agent": "Agent",
|
||||
"agents": "Agenten",
|
||||
"all": "Alle",
|
||||
"command": "Befehl",
|
||||
"commands": "Befehle",
|
||||
"skills": "Fähigkeiten"
|
||||
},
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"prompt": "Prompt-Einstellungen",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Werkzeuganfrage zulassen",
|
||||
"denyRequest": "Werkzeuganfrage ablehnen",
|
||||
"hideDetails": "Werkzeugdetails ausblenden",
|
||||
"runWithOptions": "Mit zusätzlichen Optionen ausführen",
|
||||
"showDetails": "Zeige Werkzeugdetails"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Abbrechen",
|
||||
"run": "Laufen"
|
||||
},
|
||||
"confirmation": "Bist du sicher, dass du dieses Claude-Tool ausführen möchtest?",
|
||||
"defaultDenyMessage": "Der Benutzer hat die Berechtigung für dieses Tool verweigert.",
|
||||
"defaultDescription": "Führt Code oder Systemaktionen in Ihrer Umgebung aus. Vergewissern Sie sich, dass der Befehl sicher aussieht, bevor Sie ihn ausführen.",
|
||||
"error": {
|
||||
"sendFailed": "Ihre Entscheidung konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"expired": "Abgelaufen",
|
||||
"inputPreview": "Vorschau der Werkzeugeingabe",
|
||||
"pending": "Ausstehend ({{seconds}}s)",
|
||||
"permissionExpired": "Berechtigungsanfrage abgelaufen. Warte auf neue Anweisungen...",
|
||||
"requiresElevatedPermissions": "Dieses Tool erfordert erhöhte Berechtigungen.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Das Genehmigen kann mehrere Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen.",
|
||||
"permissionUpdateSingle": "Das Genehmigen kann Ihre Sitzungsberechtigungen aktualisieren, wenn Sie sich entschieden haben, dieses Tool immer zuzulassen."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Tool-Anfrage wurde abgelehnt.",
|
||||
"timeout": "Tool-Anfrage ist abgelaufen, bevor eine Genehmigung eingegangen ist."
|
||||
},
|
||||
"waiting": "Warten auf Entscheidung über Tool-Berechtigung..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent-Typ",
|
||||
"unknown": "Unbekannter Typ"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Kontrolle der Zufälligkeit des Upscale-Ergebnisses"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Aktionen",
|
||||
"agents": "Agenten",
|
||||
"all_categories": "Alle Kategorien",
|
||||
"all_types": "Alle",
|
||||
"category": "Kategorie",
|
||||
"commands": "Befehle",
|
||||
"confirm_uninstall": "Sind Sie sicher, dass Sie {{name}} deinstallieren möchten?",
|
||||
"install": "Installieren",
|
||||
"install_plugins_from_browser": "Durchsuche verfügbare Plugins, um loszulegen",
|
||||
"installing": "Installiere…",
|
||||
"name": "Name",
|
||||
"no_description": "Keine Beschreibung verfügbar",
|
||||
"no_installed_plugins": "Noch keine Plugins installiert",
|
||||
"no_results": "Keine Plugins gefunden",
|
||||
"search_placeholder": "Such-Plugins...",
|
||||
"showing_results": "{{count}} Plugin anzeigen",
|
||||
"showing_results_one": "{{count}} Plugin anzeigen",
|
||||
"showing_results_other": "Zeige {{count}} Plugins",
|
||||
"showing_results_plural": "{{count}} Plugins anzeigen",
|
||||
"skills": "Fähigkeiten",
|
||||
"try_different_search": "Versuchen Sie, Ihre Suche oder die Kategoriefilter anzupassen.",
|
||||
"type": "Typ",
|
||||
"uninstall": "Deinstallieren",
|
||||
"uninstalling": "Deinstallation läuft..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Als Bild kopieren"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "Infini-AI",
|
||||
"jina": "Jina",
|
||||
"lanyun": "Lanyun Technologie",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "Meißner Riesenhamster",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic API-Adresse",
|
||||
"anthropic_api_host_preview": "Anthropic-Vorschau: {{url}}",
|
||||
"anthropic_api_host_tip": "Nur bei Anbietern, die ein Anthropic-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"anthropic_api_host_tooltip": "Nur bei Anbietern, die ein Claude-kompatibles Basis-Endpunkt anbieten.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Vorschau: {{url}}",
|
||||
"reset": "Zurücksetzen",
|
||||
"tip": "# am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
"tip": "/ am Ende ignorieren v1-Version, # am Ende erzwingt die Verwendung der Eingabe-Adresse"
|
||||
}
|
||||
},
|
||||
"api_host": "API-Adresse",
|
||||
"api_host_no_valid": "API-Adresse ist ungültig",
|
||||
"api_host_preview": "Vorschau: {{url}}",
|
||||
"api_host_tooltip": "Nur bei Anbietern, die ein OpenAI-kompatibles Endpunkt anbieten. Eine / am Ende ignoriert automatisch hinzugefügtes v1, ein # am Ende erzwingt die Verwendung der ursprünglichen Adresse.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Ρυθμίσεις για προχωρημένους"
|
||||
},
|
||||
"essential": "Βασικές Ρυθμίσεις",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Διαθέσιμα πρόσθετα"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Είστε βέβαιοι ότι θέλετε να απεγκαταστήσετε αυτό το πρόσθετο;"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Δεν βρέθηκε συμβατό πρόσθετο. Δοκιμάστε να προσαρμόσετε την αναζήτηση ή το φίλτρο κατηγοριών."
|
||||
},
|
||||
"error": {
|
||||
"install": "Η εγκατάσταση του πρόσθετου απέτυχε",
|
||||
"load": "Η φόρτωση του πρόσθετου απέτυχε",
|
||||
"uninstall": "Η απεγκατάσταση του πρόσθετου απέτυχε"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Όλες οι κατηγορίες"
|
||||
},
|
||||
"install": "εγκατάσταση",
|
||||
"installed": {
|
||||
"empty": "Δεν έχει εγκατασταθεί κανένα πρόσθετο. Περιηγηθείτε στα διαθέσιμα πρόσθετα για να ξεκινήσετε.",
|
||||
"title": "Έχει εγκατασταθεί το πρόσθετο"
|
||||
},
|
||||
"installing": "Εγκατάσταση...",
|
||||
"results": "Βρέθηκαν {{count}} πρόσθετα",
|
||||
"search": {
|
||||
"placeholder": "Αναζήτηση πρόσθετου..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Η εγκατάσταση του πρόσθετου ολοκληρώθηκε με επιτυχία",
|
||||
"uninstall": "Η απεγκατάσταση του πρόσθετου ολοκληρώθηκε με επιτυχία"
|
||||
},
|
||||
"tab": "Πρόσθετο",
|
||||
"type": {
|
||||
"agent": "αντιπρόσωπος",
|
||||
"agents": "αντιπρόσωπος",
|
||||
"all": "όλα",
|
||||
"command": "εντολή",
|
||||
"commands": "εντολή",
|
||||
"skills": "δεξιότητα"
|
||||
},
|
||||
"uninstall": "απεγκατάσταση",
|
||||
"uninstalling": "Απεγκατάσταση..."
|
||||
},
|
||||
"prompt": "Ρυθμίσεις Προτροπής",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Επίτρεψη αίτησης εργαλείου",
|
||||
"denyRequest": "Απόρριψη αιτήματος εργαλείου",
|
||||
"hideDetails": "Απόκρυψη λεπτομερειών εργαλείου",
|
||||
"runWithOptions": "Εκτέλεση με επιπλέον επιλογές",
|
||||
"showDetails": "Εμφάνιση λεπτομερειών εργαλείου"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Ακύρωση",
|
||||
"run": "Τρέξε"
|
||||
},
|
||||
"confirmation": "Είσαι σίγουρος ότι θέλεις να εκτελέσεις αυτό το εργαλείο Claude;",
|
||||
"defaultDenyMessage": "Ο χρήστης αρνήθηκε την άδεια για αυτό το εργαλείο.",
|
||||
"defaultDescription": "Εκτελεί κώδικα ή ενέργειες συστήματος στο περιβάλλον σας. Βεβαιωθείτε ότι η εντολή φαίνεται ασφαλής πριν την εκτελέσετε.",
|
||||
"error": {
|
||||
"sendFailed": "Αποτυχία αποστολής της απόφασής σας. Προσπαθήστε ξανά."
|
||||
},
|
||||
"expired": "Ληγμένο",
|
||||
"inputPreview": "Προεπισκόπηση εισόδου εργαλείου",
|
||||
"pending": "Εκκρεμεί ({{seconds}}δ)",
|
||||
"permissionExpired": "Το αίτημα άδειας έληξε. Αναμονή για νέες οδηγίες...",
|
||||
"requiresElevatedPermissions": "Αυτό το εργαλείο απαιτεί αυξημένα δικαιώματα.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Η έγκριση μπορεί να ενημερώσει πολλές άδειες συνεδρίας αν επιλέξατε να επιτρέπετε πάντα αυτό το εργαλείο.",
|
||||
"permissionUpdateSingle": "Η έγκριση ενδέχεται να ενημερώσει τα δικαιώματα περιόδου σύνδεσής σας, εάν επιλέξατε να επιτρέπετε πάντα αυτό το εργαλείο."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Το αίτημα για εργαλείο απορρίφθηκε.",
|
||||
"timeout": "Το αίτημα για το εργαλείο έληξε πριν λάβει έγκριση."
|
||||
},
|
||||
"waiting": "Αναμονή για απόφαση άδειας εργαλείου..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Τύπος Πράκτορα",
|
||||
"unknown": "Άγνωστος Τύπος"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Ελέγχει την τυχαιότητα του αποτελέσματος μεγέθυνσης"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Λειτουργία",
|
||||
"agents": "αντιπρόσωπος",
|
||||
"all_categories": "Όλες οι κατηγορίες",
|
||||
"all_types": "ολόκληρο",
|
||||
"category": "Κατηγορία",
|
||||
"commands": "εντολή",
|
||||
"confirm_uninstall": "Είστε σίγουροι ότι θέλετε να απεγκαταστήσετε το {{name}};",
|
||||
"install": "εγκατάσταση",
|
||||
"install_plugins_from_browser": "Περιηγηθείτε στα διαθέσιμα πρόσθετα για να ξεκινήσετε",
|
||||
"installing": "Εγκατάσταση...",
|
||||
"name": "Όνομα",
|
||||
"no_description": "Χωρίς περιγραφή",
|
||||
"no_installed_plugins": "Δεν έχει εγκατασταθεί κανένα πρόσθετο",
|
||||
"no_results": "Δεν βρέθηκε πρόσθετο",
|
||||
"search_placeholder": "Πρόσθετο αναζήτησης...",
|
||||
"showing_results": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_one": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_other": "Εμφάνιση {{count}} προσθέτων",
|
||||
"showing_results_plural": "Εμφάνιση {{count}} πρόσθετων",
|
||||
"skills": "δεξιότητα",
|
||||
"try_different_search": "Προσπαθήστε να προσαρμόσετε την αναζήτηση ή το φίλτρο κατηγοριών",
|
||||
"type": "τύπος",
|
||||
"uninstall": "κατάργηση εγκατάστασης",
|
||||
"uninstalling": "Απεγκατάσταση..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Αντιγραφή ως εικόνα"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Υπερβολικός",
|
||||
"infini": "Χωρίς Ερώτημα Xin Qiong",
|
||||
"jina": "Jina",
|
||||
"lanyun": "Λανιούν Τεχνολογία",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "Τσίρο",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope Magpie",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Διεύθυνση API Anthropic",
|
||||
"anthropic_api_host_preview": "Προεπισκόπηση Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Συμπληρώστε μόνο εάν ο πάροχος προσφέρει συμβατή με Anthropic διεύθυνση. Η λήξη με / αγνοεί το v1 που προστίθεται αυτόματα, η λήξη με # επιβάλλει τη χρήση της αρχικής διεύθυνσης.",
|
||||
"anthropic_api_host_tooltip": "Συμπληρώστε μόνο όταν ο πάροχος παρέχει βασική διεύθυνση συμβατή με Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Προεπισκόπηση: {{url}}",
|
||||
"reset": "Επαναφορά",
|
||||
"tip": "#τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
"tip": "/τέλος αγνόηση v1 έκδοσης, #τέλος ενδεχόμενη χρήση της εισαγωγής διευθύνσεως"
|
||||
}
|
||||
},
|
||||
"api_host": "Διεύθυνση API",
|
||||
"api_host_no_valid": "Η διεύθυνση API δεν είναι έγκυρη",
|
||||
"api_host_preview": "Προεπισκόπηση: {{url}}",
|
||||
"api_host_tooltip": "Αντικατάσταση μόνο όταν ο πάροχος απαιτεί προσαρμοσμένη διεύθυνση συμβατή με OpenAI.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Configuración avanzada"
|
||||
},
|
||||
"essential": "Configuraciones esenciales",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Complementos disponibles"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "¿Estás seguro de que quieres desinstalar este complemento?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "No se encontró ningún complemento que coincida. Intenta ajustar la búsqueda o los filtros de categoría."
|
||||
},
|
||||
"error": {
|
||||
"install": "Error al instalar el complemento",
|
||||
"load": "Error al cargar el complemento",
|
||||
"uninstall": "Error al desinstalar el complemento"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Todas las categorías"
|
||||
},
|
||||
"install": "instalación",
|
||||
"installed": {
|
||||
"empty": "Aún no se ha instalado ningún complemento. Explora los complementos disponibles para comenzar.",
|
||||
"title": "Complemento instalado"
|
||||
},
|
||||
"installing": "Instalando...",
|
||||
"results": "Encontrados {{count}} complementos",
|
||||
"search": {
|
||||
"placeholder": "Buscar complemento..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Complemento instalado con éxito",
|
||||
"uninstall": "Complemento desinstalado correctamente"
|
||||
},
|
||||
"tab": "complemento",
|
||||
"type": {
|
||||
"agent": "agente",
|
||||
"agents": "Agente",
|
||||
"all": "todo",
|
||||
"command": "comando",
|
||||
"commands": "comando",
|
||||
"skills": "habilidad"
|
||||
},
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"prompt": "Configuración de indicaciones",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Permitir solicitud de herramienta",
|
||||
"denyRequest": "Denegar solicitud de herramienta",
|
||||
"hideDetails": "Ocultar detalles de la herramienta",
|
||||
"runWithOptions": "Ejecutar con opciones adicionales",
|
||||
"showDetails": "Mostrar detalles de la herramienta"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancelar",
|
||||
"run": "Correr"
|
||||
},
|
||||
"confirmation": "¿Estás seguro de que quieres ejecutar esta herramienta de Claude?",
|
||||
"defaultDenyMessage": "El usuario denegó el permiso para esta herramienta.",
|
||||
"defaultDescription": "Ejecuta código o acciones del sistema en tu entorno. Asegúrate de que el comando parezca seguro antes de ejecutarlo.",
|
||||
"error": {
|
||||
"sendFailed": "No se pudo enviar tu decisión. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"expired": "Caducado",
|
||||
"inputPreview": "Vista previa de entrada de herramienta",
|
||||
"pending": "Pendiente ({{seconds}}s)",
|
||||
"permissionExpired": "Solicitud de permiso expirada. Esperando nuevas instrucciones...",
|
||||
"requiresElevatedPermissions": "Esta herramienta requiere permisos elevados.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Aprobar puede actualizar varios permisos de sesión si elegiste permitir siempre esta herramienta.",
|
||||
"permissionUpdateSingle": "Aprobar puede actualizar los permisos de tu sesión si elegiste permitir siempre esta herramienta."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "La solicitud de herramienta fue denegada.",
|
||||
"timeout": "La solicitud de herramienta expiró antes de recibir la aprobación."
|
||||
},
|
||||
"waiting": "Esperando la decisión de permiso de la herramienta..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo desconocido"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Controla la aleatoriedad del resultado de la ampliación"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Operación",
|
||||
"agents": "Agente",
|
||||
"all_categories": "Todas las categorías",
|
||||
"all_types": "todo",
|
||||
"category": "Categoría",
|
||||
"commands": "comando",
|
||||
"confirm_uninstall": "¿Estás seguro de que quieres desinstalar {{name}}?",
|
||||
"install": "instalación",
|
||||
"install_plugins_from_browser": "Explora los complementos disponibles para empezar a usar",
|
||||
"installing": "Instalando...",
|
||||
"name": "Nombre",
|
||||
"no_description": "Sin descripción",
|
||||
"no_installed_plugins": "Aún no se ha instalado ningún complemento",
|
||||
"no_results": "No se encontró el complemento",
|
||||
"search_placeholder": "Buscar complemento...",
|
||||
"showing_results": "Mostrar {{count}} complementos",
|
||||
"showing_results_one": "Mostrar {{count}} complementos",
|
||||
"showing_results_other": "Mostrar {{count}} complementos",
|
||||
"showing_results_plural": "Mostrar {{count}} complementos",
|
||||
"skills": "habilidad",
|
||||
"try_different_search": "Por favor, intenta ajustar la búsqueda o los filtros de categoría.",
|
||||
"type": "tipo",
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copiar como imagen"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hùnyuán",
|
||||
"hyperbolic": "Hiperbólico",
|
||||
"infini": "Infini",
|
||||
"jina": "Jina",
|
||||
"lanyun": "Tecnología Lanyun",
|
||||
"lmstudio": "Estudio LM",
|
||||
"longcat": "Totoro",
|
||||
"minimax": "Minimax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope Módulo",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Dirección API de Anthropic",
|
||||
"anthropic_api_host_preview": "Vista previa de Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Rellenar solo si el proveedor ofrece una dirección compatible con Anthropic. Terminar con / ignora el v1 añadido automáticamente, terminar con # fuerza el uso de la dirección original.",
|
||||
"anthropic_api_host_tooltip": "Rellenar solo cuando el proveedor proporcione una dirección base compatible con Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Vista previa: {{url}}",
|
||||
"reset": "Restablecer",
|
||||
"tip": "forzar uso de dirección de entrada con # al final"
|
||||
"tip": "Ignorar v1 al final con /, forzar uso de dirección de entrada con # al final"
|
||||
}
|
||||
},
|
||||
"api_host": "Dirección API",
|
||||
"api_host_no_valid": "La dirección de la API no es válida",
|
||||
"api_host_preview": "Vista previa: {{url}}",
|
||||
"api_host_tooltip": "Sobrescribir solo cuando el proveedor necesite una dirección compatible con OpenAI personalizada.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Paramètres avancés"
|
||||
},
|
||||
"essential": "Paramètres essentiels",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Plugins disponibles"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Êtes-vous sûr de vouloir désinstaller ce plugin ?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Aucun plugin correspondant trouvé. Veuillez essayer d’ajuster la recherche ou les filtres de catégorie."
|
||||
},
|
||||
"error": {
|
||||
"install": "Échec de l'installation du plugin",
|
||||
"load": "Échec du chargement du plugin",
|
||||
"uninstall": "Échec de la désinstallation du plugin"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Toutes les catégories"
|
||||
},
|
||||
"install": "Installation",
|
||||
"installed": {
|
||||
"empty": "Aucun plugin n'est encore installé. Parcourez les plugins disponibles pour commencer.",
|
||||
"title": "Extension installée"
|
||||
},
|
||||
"installing": "Installation en cours...",
|
||||
"results": "{{count}} modules complémentaires trouvés",
|
||||
"search": {
|
||||
"placeholder": "Recherche de plug-ins..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Installation du plugin réussie",
|
||||
"uninstall": "Désinstallation du plugin réussie"
|
||||
},
|
||||
"tab": "Module d'extension",
|
||||
"type": {
|
||||
"agent": "mandataire",
|
||||
"agents": "mandataire",
|
||||
"all": "Tout",
|
||||
"command": "commande",
|
||||
"commands": "commande",
|
||||
"skills": "compétence"
|
||||
},
|
||||
"uninstall": "Désinstaller",
|
||||
"uninstalling": "Désinstallation en cours..."
|
||||
},
|
||||
"prompt": "Paramètres de l'invite",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Autoriser la demande d'outil",
|
||||
"denyRequest": "Refuser la demande d'outil",
|
||||
"hideDetails": "Masquer les détails de l'outil",
|
||||
"runWithOptions": "Exécuter avec des options supplémentaires",
|
||||
"showDetails": "Afficher les détails de l'outil"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Annuler",
|
||||
"run": "Courir"
|
||||
},
|
||||
"confirmation": "Êtes-vous sûr de vouloir exécuter cet outil Claude ?",
|
||||
"defaultDenyMessage": "L'utilisateur a refusé l'autorisation pour cet outil.",
|
||||
"defaultDescription": "Exécute du code ou des actions système dans votre environnement. Assurez-vous que la commande semble sûre avant de l’exécuter.",
|
||||
"error": {
|
||||
"sendFailed": "Échec de l'envoi de votre décision. Veuillez réessayer."
|
||||
},
|
||||
"expired": "Expiré",
|
||||
"inputPreview": "Aperçu de l'entrée de l'outil",
|
||||
"pending": "En attente ({{seconds}}s)",
|
||||
"permissionExpired": "Demande de permission expirée. En attente de nouvelles instructions...",
|
||||
"requiresElevatedPermissions": "Cet outil nécessite des autorisations élevées.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Approuver peut mettre à jour plusieurs autorisations de session si vous avez choisi de toujours autoriser cet outil.",
|
||||
"permissionUpdateSingle": "Approuver peut mettre à jour vos permissions de session si vous avez choisi de toujours autoriser cet outil."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "La demande d'outil a été refusée.",
|
||||
"timeout": "La demande d'outil a expiré avant d'obtenir l'approbation."
|
||||
},
|
||||
"waiting": "En attente de la décision d'autorisation de l'outil..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Type d'agent",
|
||||
"unknown": "Type inconnu"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Contrôle la randomisation du résultat d'agrandissement"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Opération",
|
||||
"agents": "mandataire",
|
||||
"all_categories": "Toutes les catégories",
|
||||
"all_types": "Tout",
|
||||
"category": "Catégorie",
|
||||
"commands": "commande",
|
||||
"confirm_uninstall": "Êtes-vous sûr de vouloir désinstaller {{name}} ?",
|
||||
"install": "Installation",
|
||||
"install_plugins_from_browser": "Parcourir les plugins disponibles pour commencer",
|
||||
"installing": "Installation en cours...",
|
||||
"name": "Nom",
|
||||
"no_description": "Sans description",
|
||||
"no_installed_plugins": "Aucun plugin n’est encore installé",
|
||||
"no_results": "Aucun plugin trouvé",
|
||||
"search_placeholder": "Rechercher des modules d'extension...",
|
||||
"showing_results": "Afficher {{count}} extensions",
|
||||
"showing_results_one": "Afficher {{count}} modules d’extension",
|
||||
"showing_results_other": "Afficher {{count}} modules d'extension",
|
||||
"showing_results_plural": "Afficher {{count}} modules d'extension",
|
||||
"skills": "compétence",
|
||||
"try_different_search": "Veuillez essayer d’ajuster la recherche ou le filtre de catégorie.",
|
||||
"type": "type",
|
||||
"uninstall": "Désinstaller",
|
||||
"uninstalling": "Désinstallation en cours..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copier en tant qu'image"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent HunYuan",
|
||||
"hyperbolic": "Hyperbolique",
|
||||
"infini": "Sans Frontières Céleste",
|
||||
"jina": "Jina",
|
||||
"lanyun": "Technologie Lan Yun",
|
||||
"lmstudio": "Studio LM",
|
||||
"longcat": "Mon voisin Totoro",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope MoDa",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Adresse API Anthropic",
|
||||
"anthropic_api_host_preview": "Aperçu Anthropic : {{url}}",
|
||||
"anthropic_api_host_tip": "Remplir seulement si le fournisseur propose une adresse compatible Anthropic. Se terminant par / ignore le v1 ajouté automatiquement, se terminant par # force l'utilisation de l'adresse originale.",
|
||||
"anthropic_api_host_tooltip": "Remplir seulement lorsque le fournisseur propose une adresse de base compatible Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Aperçu : {{url}}",
|
||||
"reset": "Réinitialiser",
|
||||
"tip": "forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
"tip": "Ignorer la version v1 si terminé par /, forcer l'utilisation de l'adresse d'entrée si terminé par #"
|
||||
}
|
||||
},
|
||||
"api_host": "Adresse API",
|
||||
"api_host_no_valid": "Adresse API invalide",
|
||||
"api_host_preview": "Aperçu : {{url}}",
|
||||
"api_host_tooltip": "Remplacer seulement lorsque le fournisseur nécessite une adresse compatible OpenAI personnalisée.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "高級設定"
|
||||
},
|
||||
"essential": "必須設定",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "利用可能なプラグイン"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "このプラグインをアンインストールしてもよろしいですか?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "一致するプラグインが見つかりませんでした。検索キーワードやカテゴリフィルターを調整してみてください。"
|
||||
},
|
||||
"error": {
|
||||
"install": "プラグインのインストールに失敗しました",
|
||||
"load": "プラグインの読み込みに失敗しました",
|
||||
"uninstall": "プラグインのアンインストールに失敗しました"
|
||||
},
|
||||
"filter": {
|
||||
"all": "すべてのカテゴリー"
|
||||
},
|
||||
"install": "インストール",
|
||||
"installed": {
|
||||
"empty": "まだプラグインがインストールされていません。利用可能なプラグインを見てみましょう。",
|
||||
"title": "インストール済みプラグイン"
|
||||
},
|
||||
"installing": "インストール中...",
|
||||
"results": "{{count}} 個のプラグインが見つかりました",
|
||||
"search": {
|
||||
"placeholder": "検索プラグイン..."
|
||||
},
|
||||
"success": {
|
||||
"install": "プラグインのインストールが成功しました",
|
||||
"uninstall": "プラグインのアンインストールが成功しました"
|
||||
},
|
||||
"tab": "プラグイン",
|
||||
"type": {
|
||||
"agent": "代理",
|
||||
"agents": "代理",
|
||||
"all": "全部",
|
||||
"command": "命令",
|
||||
"commands": "命令",
|
||||
"skills": "技能"
|
||||
},
|
||||
"uninstall": "アンインストール",
|
||||
"uninstalling": "アンインストール中..."
|
||||
},
|
||||
"prompt": "プロンプト設定",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "ツールリクエストを許可",
|
||||
"denyRequest": "ツールリクエストを拒否",
|
||||
"hideDetails": "ツールの詳細を非表示",
|
||||
"runWithOptions": "追加オプションで実行",
|
||||
"showDetails": "ツールの詳細を表示"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "キャンセル",
|
||||
"run": "走る"
|
||||
},
|
||||
"confirmation": "このClaudeツールを実行してもよろしいですか?",
|
||||
"defaultDenyMessage": "ユーザーはこのツールの使用を拒否しました。",
|
||||
"defaultDescription": "環境内でコードまたはシステムアクションを実行します。実行前にコマンドが安全であることを確認してください。",
|
||||
"error": {
|
||||
"sendFailed": "決定の送信に失敗しました。もう一度お試しください。"
|
||||
},
|
||||
"expired": "期限切れ",
|
||||
"inputPreview": "ツール入力プレビュー",
|
||||
"pending": "保留中({{seconds}}秒)",
|
||||
"permissionExpired": "許可リクエストの期限が切れました。新しい指示を待っています...",
|
||||
"requiresElevatedPermissions": "このツールは昇格した権限が必要です。",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "承認すると、このツールを常に許可することを選択した場合、複数のセッション権限が更新されることがあります。",
|
||||
"permissionUpdateSingle": "承認すると、このツールを常に許可することを選択した場合、セッションの権限が更新されることがあります。"
|
||||
},
|
||||
"toast": {
|
||||
"denied": "ツールリクエストは拒否されました。",
|
||||
"timeout": "ツールリクエストは承認を受ける前にタイムアウトしました。"
|
||||
},
|
||||
"waiting": "ツールの許可決定を待っています..."
|
||||
},
|
||||
"type": {
|
||||
"label": "エージェントタイプ",
|
||||
"unknown": "不明なタイプ"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "拡大結果のランダム性を制御します"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "操作",
|
||||
"agents": "代理",
|
||||
"all_categories": "すべてのカテゴリー",
|
||||
"all_types": "全部",
|
||||
"category": "カテゴリー",
|
||||
"commands": "命令",
|
||||
"confirm_uninstall": "{{name}}をアンインストールしてもよろしいですか?",
|
||||
"install": "インストール",
|
||||
"install_plugins_from_browser": "利用可能なプラグインを閲覧して、使用を開始してください",
|
||||
"installing": "インストール中...",
|
||||
"name": "名称",
|
||||
"no_description": "説明なし",
|
||||
"no_installed_plugins": "まだプラグインがインストールされていません",
|
||||
"no_results": "プラグインが見つかりません",
|
||||
"search_placeholder": "検索プラグイン...",
|
||||
"showing_results": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_one": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_other": "{{count}} 個のプラグインを表示",
|
||||
"showing_results_plural": "{{count}} 個のプラグインを表示",
|
||||
"skills": "スキル",
|
||||
"try_different_search": "検索またはカテゴリフィルターを調整してみてください",
|
||||
"type": "タイプ",
|
||||
"uninstall": "アンインストール",
|
||||
"uninstalling": "アンインストール中..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "画像としてコピー"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "ハギングフェイス",
|
||||
"hunyuan": "腾讯混元",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "Infini",
|
||||
"jina": "Jina",
|
||||
"lanyun": "LANYUN",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "トトロ",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Anthropic APIアドレス",
|
||||
"anthropic_api_host_preview": "Anthropic プレビュー:{{url}}",
|
||||
"anthropic_api_host_tip": "サービスプロバイダーがAnthropic互換のアドレスを提供する場合のみ入力してください。/で終わる場合は自動追加されるv1を無視し、#で終わる場合は元のアドレスを強制的に使用します。",
|
||||
"anthropic_api_host_tooltip": "サービスプロバイダーがClaude互換のベースアドレスを提供する場合のみ入力してください。",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "プレビュー: {{url}}",
|
||||
"reset": "リセット",
|
||||
"tip": "#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
"tip": "/で終わる場合、v1を無視します。#で終わる場合、入力されたアドレスを強制的に使用します"
|
||||
}
|
||||
},
|
||||
"api_host": "APIホスト",
|
||||
"api_host_no_valid": "APIアドレスが無効です",
|
||||
"api_host_preview": "プレビュー:{{url}}",
|
||||
"api_host_tooltip": "サービスプロバイダーがカスタムOpenAI互換アドレスを必要とする場合のみ上書きしてください。",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Configurações avançadas"
|
||||
},
|
||||
"essential": "Configurações Essenciais",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Plugins disponíveis"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Tem certeza de que deseja desinstalar este plugin?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Nenhum plugin correspondente encontrado. Tente ajustar a pesquisa ou os filtros de categoria."
|
||||
},
|
||||
"error": {
|
||||
"install": "Falha na instalação do plugin",
|
||||
"load": "Falha ao carregar o plugin",
|
||||
"uninstall": "Falha ao desinstalar o plug-in"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Todas as categorias"
|
||||
},
|
||||
"install": "Instalação",
|
||||
"installed": {
|
||||
"empty": "Nenhum plugin foi instalado ainda. Explore os plugins disponíveis para começar.",
|
||||
"title": "Plugin instalado"
|
||||
},
|
||||
"installing": "Instalando...",
|
||||
"results": "Encontrados {{count}} plugins",
|
||||
"search": {
|
||||
"placeholder": "Pesquisar extensão..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Plugin instalado com sucesso",
|
||||
"uninstall": "插件 desinstalado com sucesso"
|
||||
},
|
||||
"tab": "plug-in",
|
||||
"type": {
|
||||
"agent": "agente",
|
||||
"agents": "agente",
|
||||
"all": "tudo",
|
||||
"command": "comando",
|
||||
"commands": "comando",
|
||||
"skills": "habilidade"
|
||||
},
|
||||
"uninstall": "desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"prompt": "Configurações de Prompt",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Permitir solicitação de ferramenta",
|
||||
"denyRequest": "Negar solicitação de ferramenta",
|
||||
"hideDetails": "Ocultar detalhes da ferramenta",
|
||||
"runWithOptions": "Executar com opções adicionais",
|
||||
"showDetails": "Mostrar detalhes da ferramenta"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Cancelar",
|
||||
"run": "Correr"
|
||||
},
|
||||
"confirmation": "Tem certeza de que quer executar esta ferramenta Claude?",
|
||||
"defaultDenyMessage": "Usuário negou permissão para esta ferramenta.",
|
||||
"defaultDescription": "Executa código ou ações do sistema no seu ambiente. Certifique-se de que o comando parece seguro antes de executá-lo.",
|
||||
"error": {
|
||||
"sendFailed": "Falha ao enviar sua decisão. Por favor, tente novamente."
|
||||
},
|
||||
"expired": "Expirado",
|
||||
"inputPreview": "Pré-visualização da entrada da ferramenta",
|
||||
"pending": "Pendente ({{seconds}}s)",
|
||||
"permissionExpired": "Solicitação de permissão expirou. Aguardando novas instruções...",
|
||||
"requiresElevatedPermissions": "Esta ferramenta requer permissões elevadas.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Aprovar pode atualizar várias permissões de sessão se você escolheu sempre permitir esta ferramenta.",
|
||||
"permissionUpdateSingle": "Aprovar pode atualizar as permissões da sua sessão se você escolheu sempre permitir esta ferramenta."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Solicitação de ferramenta foi negada.",
|
||||
"timeout": "A solicitação da ferramenta expirou antes de receber aprovação."
|
||||
},
|
||||
"waiting": "Aguardando decisão de permissão da ferramenta..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo Desconhecido"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Controla a aleatoriedade do resultado de ampliação"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Operação",
|
||||
"agents": "agente",
|
||||
"all_categories": "Todas as categorias",
|
||||
"all_types": "Tudo",
|
||||
"category": "categoria",
|
||||
"commands": "comando",
|
||||
"confirm_uninstall": "Tem certeza de que deseja desinstalar {{name}}?",
|
||||
"install": "Instalação",
|
||||
"install_plugins_from_browser": "Navegue pelos plugins disponíveis para começar a usar",
|
||||
"installing": "Instalando...",
|
||||
"name": "Nome",
|
||||
"no_description": "Sem descrição",
|
||||
"no_installed_plugins": "Nenhum plugin foi instalado ainda",
|
||||
"no_results": "Plugin não encontrado",
|
||||
"search_placeholder": "Pesquisar plugin...",
|
||||
"showing_results": "Exibir {{count}} extensões",
|
||||
"showing_results_one": "Mostrar {{count}} extensões",
|
||||
"showing_results_other": "Exibir {{count}} extensões",
|
||||
"showing_results_plural": "Exibir {{count}} extensões",
|
||||
"skills": "habilidade",
|
||||
"try_different_search": "Por favor, tente ajustar a pesquisa ou os filtros de categoria.",
|
||||
"type": "tipo",
|
||||
"uninstall": "Desinstalar",
|
||||
"uninstalling": "Desinstalando..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Copiar como imagem"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Compreender",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hún Yuán",
|
||||
"hyperbolic": "Hiperbólico",
|
||||
"infini": "Infinito",
|
||||
"jina": "Jina",
|
||||
"lanyun": "Lanyun Tecnologia",
|
||||
"lmstudio": "Estúdio LM",
|
||||
"longcat": "Totoro",
|
||||
"minimax": "Minimax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope MôDá",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Endereço da API Anthropic",
|
||||
"anthropic_api_host_preview": "Pré-visualização Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Preencher apenas se o fornecedor oferecer um endereço compatível com Anthropic. Terminar com / ignora o v1 adicionado automaticamente, terminar com # força o uso do endereço original.",
|
||||
"anthropic_api_host_tooltip": "Preencher apenas quando o fornecedor fornece um endereço base compatível com Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Pré-visualização: {{url}}",
|
||||
"reset": "Redefinir",
|
||||
"tip": "e forçar o uso do endereço original quando terminar com '#'"
|
||||
"tip": "Ignorar v1 na versão finalizada com /, usar endereço de entrada forçado se terminar com #"
|
||||
}
|
||||
},
|
||||
"api_host": "Endereço API",
|
||||
"api_host_no_valid": "O endereço da API é inválido",
|
||||
"api_host_preview": "Pré-visualização: {{url}}",
|
||||
"api_host_tooltip": "Substituir apenas quando o fornecedor necessita de um endereço compatível com OpenAI personalizado.",
|
||||
"api_key": {
|
||||
|
||||
@@ -107,50 +107,6 @@
|
||||
"title": "Расширенные настройки"
|
||||
},
|
||||
"essential": "Основные настройки",
|
||||
"plugins": {
|
||||
"available": {
|
||||
"title": "Доступные плагины"
|
||||
},
|
||||
"confirm": {
|
||||
"uninstall": "Вы уверены, что хотите удалить этот плагин?"
|
||||
},
|
||||
"empty": {
|
||||
"available": "Совпадающие плагины не найдены. Попробуйте изменить поиск или фильтр категорий."
|
||||
},
|
||||
"error": {
|
||||
"install": "Ошибка установки плагина",
|
||||
"load": "Ошибка загрузки плагина",
|
||||
"uninstall": "Не удалось удалить плагин"
|
||||
},
|
||||
"filter": {
|
||||
"all": "Все категории"
|
||||
},
|
||||
"install": "установка",
|
||||
"installed": {
|
||||
"empty": "Плагины ещё не установлены. Просмотрите доступные плагины, чтобы начать.",
|
||||
"title": "Установленный плагин"
|
||||
},
|
||||
"installing": "Установка...",
|
||||
"results": "Найдено {{count}} плагинов",
|
||||
"search": {
|
||||
"placeholder": "Поиск плагинов..."
|
||||
},
|
||||
"success": {
|
||||
"install": "Плагин успешно установлен",
|
||||
"uninstall": "Плагин успешно удалён"
|
||||
},
|
||||
"tab": "плагин",
|
||||
"type": {
|
||||
"agent": "агент",
|
||||
"agents": "Прокси",
|
||||
"all": "всё",
|
||||
"command": "команда",
|
||||
"commands": "команда",
|
||||
"skills": "навык"
|
||||
},
|
||||
"uninstall": "Удаление",
|
||||
"uninstalling": "Удаление..."
|
||||
},
|
||||
"prompt": "Настройки подсказки",
|
||||
"tooling": {
|
||||
"mcp": {
|
||||
@@ -235,39 +191,6 @@
|
||||
"toggle": "{{defaultValue}}"
|
||||
}
|
||||
},
|
||||
"toolPermission": {
|
||||
"aria": {
|
||||
"allowRequest": "Разрешить запрос инструмента",
|
||||
"denyRequest": "Отклонить запрос на инструмент",
|
||||
"hideDetails": "Скрыть сведения об инструменте",
|
||||
"runWithOptions": "Запустить с дополнительными параметрами",
|
||||
"showDetails": "Показать сведения об инструменте"
|
||||
},
|
||||
"button": {
|
||||
"cancel": "Отмена",
|
||||
"run": "Беги"
|
||||
},
|
||||
"confirmation": "Вы уверены, что хотите запустить этот инструмент Claude?",
|
||||
"defaultDenyMessage": "Пользователь отказал в разрешении на использование этого инструмента.",
|
||||
"defaultDescription": "Выполняет код или системные действия в вашей среде. Убедитесь, что команда выглядит безопасно, прежде чем запускать её.",
|
||||
"error": {
|
||||
"sendFailed": "Не удалось отправить ваше решение. Попробуйте ещё раз."
|
||||
},
|
||||
"expired": "Истёк",
|
||||
"inputPreview": "Предварительный просмотр ввода инструмента",
|
||||
"pending": "Ожидание ({{seconds}}с)",
|
||||
"permissionExpired": "Срок действия запроса на разрешение истёк. Ожидание новых инструкций...",
|
||||
"requiresElevatedPermissions": "Этому инструменту требуются повышенные разрешения.",
|
||||
"suggestion": {
|
||||
"permissionUpdateMultiple": "Одобрение может обновить разрешения для нескольких сеансов, если вы выбрали всегда разрешать использование этого инструмента.",
|
||||
"permissionUpdateSingle": "Одобрение может обновить разрешения вашей сессии, если вы выбрали всегда разрешать использование этого инструмента."
|
||||
},
|
||||
"toast": {
|
||||
"denied": "Запрос на инструмент был отклонён.",
|
||||
"timeout": "Запрос на инструмент превысил время ожидания до получения подтверждения."
|
||||
},
|
||||
"waiting": "Ожидание решения о разрешении на использование инструмента..."
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип агента",
|
||||
"unknown": "Неизвестный тип"
|
||||
@@ -2376,32 +2299,6 @@
|
||||
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"actions": "Операция",
|
||||
"agents": "агент",
|
||||
"all_categories": "Все категории",
|
||||
"all_types": "всё",
|
||||
"category": "категория",
|
||||
"commands": "команда",
|
||||
"confirm_uninstall": "Вы уверены, что хотите удалить {{name}}?",
|
||||
"install": "установка",
|
||||
"install_plugins_from_browser": "Просмотрите доступные плагины, чтобы начать работу",
|
||||
"installing": "Установка...",
|
||||
"name": "название",
|
||||
"no_description": "Без описания",
|
||||
"no_installed_plugins": "Плагины ещё не установлены",
|
||||
"no_results": "Плагин не найден",
|
||||
"search_placeholder": "Поиск плагинов...",
|
||||
"showing_results": "Отображено {{count}} плагинов",
|
||||
"showing_results_one": "Отображено {{count}} плагинов",
|
||||
"showing_results_other": "Отображено {{count}} плагинов",
|
||||
"showing_results_plural": "Отображение {{count}} плагинов",
|
||||
"skills": "навык",
|
||||
"try_different_search": "Пожалуйста, попробуйте изменить поиск или фильтры категорий",
|
||||
"type": "тип",
|
||||
"uninstall": "Удаление",
|
||||
"uninstalling": "Удаление..."
|
||||
},
|
||||
"preview": {
|
||||
"copy": {
|
||||
"image": "Скопировать как изображение"
|
||||
@@ -2448,14 +2345,12 @@
|
||||
"gpustack": "GPUStack",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
"huggingface": "Hugging Face",
|
||||
"hunyuan": "Tencent Hunyuan",
|
||||
"hyperbolic": "Hyperbolic",
|
||||
"infini": "Infini",
|
||||
"jina": "Jina",
|
||||
"lanyun": "LANYUN",
|
||||
"lmstudio": "LM Studio",
|
||||
"longcat": "Тоторо",
|
||||
"minimax": "MiniMax",
|
||||
"mistral": "Mistral",
|
||||
"modelscope": "ModelScope",
|
||||
@@ -4148,6 +4043,7 @@
|
||||
},
|
||||
"anthropic_api_host": "Адрес API Anthropic",
|
||||
"anthropic_api_host_preview": "Предпросмотр Anthropic: {{url}}",
|
||||
"anthropic_api_host_tip": "Заполняйте только если провайдер предоставляет совместимый с Anthropic адрес. Окончание на / игнорирует автоматически добавляемое v1, окончание на # принудительно использует оригинальный адрес.",
|
||||
"anthropic_api_host_tooltip": "Заполняйте только когда провайдер предоставляет базовый адрес, совместимый с Claude.",
|
||||
"api": {
|
||||
"key": {
|
||||
@@ -4192,11 +4088,10 @@
|
||||
"url": {
|
||||
"preview": "Предпросмотр: {{url}}",
|
||||
"reset": "Сброс",
|
||||
"tip": "заканчивая на # принудительно использует введенный адрес"
|
||||
"tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес"
|
||||
}
|
||||
},
|
||||
"api_host": "Хост API",
|
||||
"api_host_no_valid": "Недопустимый адрес API",
|
||||
"api_host_preview": "Предпросмотр: {{url}}",
|
||||
"api_host_tooltip": "Переопределяйте только когда провайдер требует пользовательский адрес, совместимый с OpenAI.",
|
||||
"api_key": {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
|
||||
},
|
||||
dashscope: {
|
||||
anthropic: {
|
||||
api_base_url: 'https://dashscope.aliyuncs.com/apps/anthropic'
|
||||
api_base_url: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy'
|
||||
}
|
||||
},
|
||||
modelscope: {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
@@ -53,8 +52,6 @@ const Chat: FC<Props> = (props) => {
|
||||
const { activeTopicOrSession, activeAgentId, activeSessionIdMap } = chat
|
||||
const activeSessionId = activeAgentId ? activeSessionIdMap[activeAgentId] : null
|
||||
const { apiServer } = useSettings()
|
||||
const sessionAgentId = activeTopicOrSession === 'session' ? activeAgentId : null
|
||||
const { createDefaultSession } = useCreateDefaultSession(sessionAgentId)
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@@ -93,21 +90,6 @@ const Chat: FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut(
|
||||
'new_topic',
|
||||
() => {
|
||||
if (activeTopicOrSession !== 'session' || !activeAgentId) {
|
||||
return
|
||||
}
|
||||
void createDefaultSession()
|
||||
},
|
||||
{
|
||||
enabled: activeTopicOrSession === 'session',
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true
|
||||
}
|
||||
)
|
||||
|
||||
const contentSearchFilter: NodeFilter = {
|
||||
acceptNode(node) {
|
||||
const container = node.parentElement?.closest('.message-content-container')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user