Compare commits
2 Commits
refactor/a
...
feat/claud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd6d6bd56e | ||
|
|
7a23386de4 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -1,4 +0,0 @@
|
||||
/src/renderer/src/store/ @0xfullex
|
||||
/src/main/services/ConfigManager.ts @0xfullex
|
||||
/packages/shared/IpcChannel.ts @0xfullex
|
||||
/src/main/ipc.ts @0xfullex
|
||||
94
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
94
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['BUG']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间填写此错误报告!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我的问题不是 [常见问题](https://github.com/CherryHQ/cherry-studio/issues/3881) 中的内容。
|
||||
required: true
|
||||
- label: 我已经查看了 **置顶 Issue** 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
- label: 我确认我正在使用最新版本的 Cherry Studio。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 错误描述
|
||||
description: 描述问题时请尽可能详细。请尽可能提供截图或屏幕录制,以帮助我们更好地理解问题。
|
||||
placeholder: 告诉我们发生了什么...(记得附上截图/录屏,如果适用)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: 重现步骤
|
||||
description: 提供详细的重现步骤,以便于我们的开发人员可以准确地重现问题。请尽可能为每个步骤提供截图或屏幕录制。
|
||||
placeholder: |
|
||||
1. 转到 '...'
|
||||
2. 点击 '....'
|
||||
3. 向下滚动到 '....'
|
||||
4. 看到错误
|
||||
|
||||
记得尽可能为每个步骤附上截图/录屏!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: 预期行为
|
||||
description: 清晰简洁地描述您期望发生的事情
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: 相关日志输出
|
||||
description: 请复制并粘贴任何相关的日志输出
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
||||
76
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/#1_feature_request.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['feature']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出新的功能建议!
|
||||
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
||||
required: true
|
||||
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: 您的功能建议是否与某个问题/issue相关?
|
||||
description: 请简明扼要地描述您遇到的问题
|
||||
placeholder: 我总是感到沮丧,因为...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: 请描述您希望实现的解决方案
|
||||
description: 请简明扼要地描述您希望发生的情况
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: 请描述您考虑过的其他方案
|
||||
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 其他补充信息
|
||||
description: 在此添加任何其他与功能建议相关的上下文或截图
|
||||
77
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
77
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: ❓ 提问 & 讨论 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['discussion', 'help wanted']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Issue 检查清单
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 您的问题
|
||||
description: 请详细描述您的问题
|
||||
placeholder: 请尽可能清楚地说明您的问题...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供一些背景信息,帮助我们更好地理解您的问题
|
||||
placeholder: 例如:使用场景、已尝试的解决方案等
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息
|
||||
description: 任何其他相关的信息、截图或代码示例
|
||||
render: shell
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: 优先级
|
||||
description: 这个问题对您来说有多紧急?
|
||||
options:
|
||||
- 低 (有空再看)
|
||||
- 中 (希望尽快得到答复)
|
||||
- 高 (阻碍工作进行)
|
||||
validations:
|
||||
required: true
|
||||
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 🤔 其他问题 (中文)
|
||||
description: 提交不属于错误报告或功能需求的问题
|
||||
title: '[其他]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出问题!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是"一个问题"、"求助"等。
|
||||
required: true
|
||||
- label: 我的问题不属于错误报告或功能需求类别。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 请详细描述您的问题或疑问
|
||||
placeholder: 我想了解有关...的更多信息
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供与您的问题相关的任何背景信息或上下文
|
||||
placeholder: 我尝试实现...时遇到了疑问
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempts
|
||||
attributes:
|
||||
label: 您已尝试的方法
|
||||
description: 请描述您为解决问题已经尝试过的方法(如果有)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
66
.github/workflows/auto-i18n.yml
vendored
66
.github/workflows/auto-i18n.yml
vendored
@@ -1,66 +0,0 @@
|
||||
name: Auto I18N
|
||||
|
||||
env:
|
||||
API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
MODEL: ${{ vars.MODEL || 'deepseek/deepseek-v3.1'}}
|
||||
BASE_URL: ${{ vars.BASE_URL || 'https://api.ppinfra.com/openai'}}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-i18n:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == 'CherryHQ/cherry-studio'
|
||||
name: Auto I18N
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: 🐈⬛ Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
run: |
|
||||
# 在临时目录安装依赖
|
||||
mkdir -p /tmp/translation-deps
|
||||
cd /tmp/translation-deps
|
||||
echo '{"dependencies": {"openai": "^5.12.2", "cli-progress": "^3.12.0", "tsx": "^4.20.3", "@biomejs/biome": "2.2.4"}}' > package.json
|
||||
npm install --no-package-lock
|
||||
|
||||
# 设置 NODE_PATH 让项目能找到这些依赖
|
||||
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
|
||||
|
||||
- name: 🏃♀️ Translate
|
||||
run: npx tsx scripts/auto-translate-i18n.ts
|
||||
|
||||
- name: 🔍 Format
|
||||
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/
|
||||
|
||||
- name: 🔄 Commit changes
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "fix(i18n): Auto update translations for PR #${{ github.event.pull_request.number }}"
|
||||
fi
|
||||
|
||||
- name: 🚀 Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.event.pull_request.head.ref }}
|
||||
56
.github/workflows/claude-code-review.yml
vendored
56
.github/workflows/claude-code-review.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Only trigger code review for PRs from the main repository due to upstream OIDC issues
|
||||
# https://github.com/anthropics/claude-code-action/issues/542
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||
(github.event.pull_request.draft == false)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
Please review this pull request and provide feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Performance considerations
|
||||
- Security concerns
|
||||
- Test coverage
|
||||
|
||||
PR number: ${{ github.event.number }}
|
||||
Repo: ${{ github.repository }}
|
||||
|
||||
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
||||
|
||||
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
||||
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
||||
107
.github/workflows/claude-translator.yml
vendored
107
.github/workflows/claude-translator.yml
vendored
@@ -1,107 +0,0 @@
|
||||
name: Claude Translator
|
||||
concurrency:
|
||||
group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
pull_request_review:
|
||||
types: [submitted, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
if: |
|
||||
(github.event_name == 'issues') ||
|
||||
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write # 编辑issues/comments
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude for translation
|
||||
uses: anthropics/claude-code-action@main
|
||||
id: claude
|
||||
with:
|
||||
# Warning: Permissions should have been controlled by workflow permission.
|
||||
# Now `contents: read` is safe for files, but we could make a fine-grained token to control it.
|
||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||
allowed_non_write_users: "*"
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
|
||||
prompt: |
|
||||
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||
|
||||
- issues
|
||||
- issue_comment
|
||||
- pull_request_review
|
||||
- pull_request_review_comment
|
||||
|
||||
请完成以下任务:
|
||||
|
||||
1. 获取当前事件的完整信息。
|
||||
|
||||
- 如果当前事件是 issues,就获取该 issues 的信息。
|
||||
- 如果当前事件是 issue_comment,就获取该 comment 的信息。
|
||||
- 如果当前事件是 pull_request_review,就获取该 review 的信息。
|
||||
- 如果当前事件是 pull_request_review_comment,就获取该 comment 的信息。
|
||||
|
||||
2. 智能检测内容。
|
||||
|
||||
- 如果获取到的信息是已经遵循格式要求翻译过的内容,则检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求;
|
||||
- 如果获取到的信息是未翻译过的内容,检查其内容语言。若不是英文,则翻译成英文;
|
||||
- 如果获取到的信息是部分翻译为英文的内容,则将其翻译为英文;
|
||||
- 如果获取到的信息包含了对已翻译内容的引用,则将引用内容清理为仅含英文的内容。引用的内容不能够包含"This xxx was translated by Claude"和"Original Content`等内容。
|
||||
- 如果获取到的信息包含了其他类型的引用,即对非 Claude 翻译的内容的引用,则直接照原样引用,不进行翻译。
|
||||
- 如果获取到的信息是通过邮件回复的内容,则在翻译时应当将邮件内容的引用放到最后。在原始内容和翻译内容中只需要回复的内容本身,不要包含对邮件内容的引用。
|
||||
- 如果获取到的信息本身不需要任何处理,则跳过任务。
|
||||
|
||||
3. 格式要求:
|
||||
|
||||
- 标题:英文翻译(如果非英文)
|
||||
- 内容格式:
|
||||
> [!NOTE]
|
||||
> This issue/comment/review was translated by Claude.
|
||||
|
||||
[翻译内容]
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>Original Content</summary>
|
||||
[原始内容]
|
||||
</details>
|
||||
|
||||
4. 使用gh工具更新:
|
||||
|
||||
- 根据环境信息中的Event类型选择正确的命令:
|
||||
- 如果 Event 是 'issues': gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]"
|
||||
- 如果 Event 是 'issue_comment': gh api -X PATCH /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]"
|
||||
- 如果 Event 是 'pull_request_review': gh api -X PUT /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ github.event.review.id }} -f body="[翻译内容]"
|
||||
- 如果 Event 是 'pull_request_review_comment': gh api -X PATCH /repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]"
|
||||
|
||||
环境信息:
|
||||
- Event: ${{ github.event_name }}
|
||||
- Issue Number: ${{ github.event.issue.number }}
|
||||
- Repository: ${{ github.repository }}
|
||||
- (Review) Comment ID: ${{ github.event.comment.id || 'N/A' }}
|
||||
- Pull Request Number: ${{ github.event.pull_request.number || 'N/A' }}
|
||||
- Review ID: ${{ github.event.review.id || 'N/A' }}
|
||||
|
||||
|
||||
使用以下命令获取完整信息:
|
||||
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||
60
.github/workflows/claude.yml
vendored
60
.github/workflows/claude.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment'
|
||||
&& contains(github.event.comment.body, '@claude')
|
||||
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
|
||||
||
|
||||
(github.event_name == 'pull_request_review_comment'
|
||||
&& contains(github.event.comment.body, '@claude')
|
||||
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
|
||||
||
|
||||
(github.event_name == 'pull_request_review'
|
||||
&& contains(github.event.review.body, '@claude')
|
||||
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.review.author_association))
|
||||
||
|
||||
(github.event_name == 'issues'
|
||||
&& (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))
|
||||
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.issue.author_association))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
|
||||
4
.github/workflows/pr-ci.yml
vendored
4
.github/workflows/pr-ci.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -49,9 +48,6 @@ jobs:
|
||||
- name: Lint Check
|
||||
run: yarn test:lint
|
||||
|
||||
- name: Format Check
|
||||
run: yarn format:check
|
||||
|
||||
- name: Type Check
|
||||
run: yarn typecheck
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -37,7 +37,6 @@ dist
|
||||
out
|
||||
mcp_server
|
||||
stats.html
|
||||
.eslintcache
|
||||
|
||||
# ENV
|
||||
.env
|
||||
@@ -69,5 +68,3 @@ playwright-report
|
||||
test-results
|
||||
|
||||
YOUR_MEMORY_FILE_PATH
|
||||
|
||||
.sessions/
|
||||
|
||||
215
.oxlintrc.json
215
.oxlintrc.json
@@ -1,215 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"categories": {},
|
||||
"env": {
|
||||
"es2022": true
|
||||
},
|
||||
"globals": {},
|
||||
"ignorePatterns": [
|
||||
"node_modules/**",
|
||||
"build/**",
|
||||
"dist/**",
|
||||
"out/**",
|
||||
"local/**",
|
||||
".yarn/**",
|
||||
".gitignore",
|
||||
"scripts/cloudflare-worker.js",
|
||||
"src/main/integration/nutstore/sso/lib/**",
|
||||
"src/main/integration/cherryin/index.js",
|
||||
"src/main/integration/nutstore/sso/lib/**",
|
||||
"src/renderer/src/ui/**",
|
||||
"packages/**/dist",
|
||||
"eslint.config.mjs"
|
||||
],
|
||||
"overrides": [
|
||||
// set different env
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts"]
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"files": [
|
||||
"src/renderer/**/*.{ts,tsx}",
|
||||
"packages/aiCore/**",
|
||||
"packages/extension-table-plus/**",
|
||||
"resources/js/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"node": true,
|
||||
"vitest": true
|
||||
},
|
||||
"files": ["**/__tests__/*.test.{ts,tsx}", "tests/**"]
|
||||
},
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"files": ["src/preload/**"]
|
||||
}
|
||||
],
|
||||
// We don't use the React plugin here because its behavior differs slightly from that of ESLint's React plugin.
|
||||
"plugins": ["unicorn", "typescript", "oxc", "import"],
|
||||
"rules": {
|
||||
"constructor-super": "error",
|
||||
"for-direction": "error",
|
||||
"getter-return": "error",
|
||||
"no-array-constructor": "off",
|
||||
// "import/no-cycle": "error", // tons of error, bro
|
||||
"no-async-promise-executor": "error",
|
||||
"no-caller": "warn",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-args": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-empty-static-block": "error",
|
||||
"no-eval": "warn",
|
||||
"no-ex-assign": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-fallthrough": "warn",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-import-assign": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-new-native-nonconstructor": "error",
|
||||
"no-nonoctal-decimal-escape": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-octal": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-setter-return": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-unassigned-vars": "warn",
|
||||
"no-undef": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-expressions": "off", // this rule disallow us to use expression to call function, like `condition && fn()`
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"no-unused-vars": ["error", { "caughtErrors": "none" }],
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-useless-rename": "warn",
|
||||
"no-with": "error",
|
||||
"oxc/bad-array-method-on-arguments": "warn",
|
||||
"oxc/bad-char-at-comparison": "warn",
|
||||
"oxc/bad-comparison-sequence": "warn",
|
||||
"oxc/bad-min-max-func": "warn",
|
||||
"oxc/bad-object-literal-comparison": "warn",
|
||||
"oxc/bad-replace-all-arg": "warn",
|
||||
"oxc/const-comparisons": "warn",
|
||||
"oxc/double-comparisons": "warn",
|
||||
"oxc/erasing-op": "warn",
|
||||
"oxc/missing-throw": "warn",
|
||||
"oxc/number-arg-out-of-range": "warn",
|
||||
"oxc/only-used-in-recursion": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||
"oxc/uninvoked-array-callback": "warn",
|
||||
"require-yield": "error",
|
||||
"typescript/await-thenable": "warn",
|
||||
// "typescript/ban-ts-comment": "error",
|
||||
"typescript/no-array-constructor": "error",
|
||||
// "typescript/consistent-type-imports": "error",
|
||||
"typescript/no-array-delete": "warn",
|
||||
"typescript/no-base-to-string": "warn",
|
||||
"typescript/no-duplicate-enum-values": "error",
|
||||
"typescript/no-duplicate-type-constituents": "warn",
|
||||
"typescript/no-empty-object-type": "off",
|
||||
"typescript/no-explicit-any": "off", // not safe but too many errors
|
||||
"typescript/no-extra-non-null-assertion": "error",
|
||||
"typescript/no-floating-promises": "warn",
|
||||
"typescript/no-for-in-array": "warn",
|
||||
"typescript/no-implied-eval": "warn",
|
||||
"typescript/no-meaningless-void-operator": "warn",
|
||||
"typescript/no-misused-new": "error",
|
||||
"typescript/no-misused-spread": "warn",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-non-null-asserted-optional-chain": "off", // it's off now. but may turn it on.
|
||||
"typescript/no-redundant-type-constituents": "warn",
|
||||
"typescript/no-require-imports": "off",
|
||||
"typescript/no-this-alias": "error",
|
||||
"typescript/no-unnecessary-parameter-property-assignment": "warn",
|
||||
"typescript/no-unnecessary-type-constraint": "error",
|
||||
"typescript/no-unsafe-declaration-merging": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-unsafe-unary-minus": "warn",
|
||||
"typescript/no-useless-empty-export": "warn",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/prefer-namespace-keyword": "error",
|
||||
"typescript/require-array-sort-compare": "warn",
|
||||
"typescript/restrict-template-expressions": "warn",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
"typescript/unbound-method": "warn",
|
||||
"unicorn/no-await-in-promise-methods": "warn",
|
||||
"unicorn/no-empty-file": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||
"unicorn/no-invalid-fetch-options": "warn",
|
||||
"unicorn/no-invalid-remove-event-listener": "warn",
|
||||
"unicorn/no-new-array": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||
"unicorn/no-single-promise-in-promise-methods": "warn",
|
||||
"unicorn/no-thenable": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||
"unicorn/no-unnecessary-await": "warn",
|
||||
"unicorn/no-useless-fallback-in-spread": "warn",
|
||||
"unicorn/no-useless-length-check": "warn",
|
||||
"unicorn/no-useless-spread": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||
"unicorn/prefer-set-size": "warn",
|
||||
"unicorn/prefer-string-starts-ends-with": "warn",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error"
|
||||
},
|
||||
"settings": {
|
||||
"jsdoc": {
|
||||
"augmentsExtendsReplacesDocs": false,
|
||||
"exemptDestructuredRootsFromChecks": false,
|
||||
"ignoreInternal": false,
|
||||
"ignorePrivate": false,
|
||||
"ignoreReplacesDocs": true,
|
||||
"implementsReplacesDocs": false,
|
||||
"overrideReplacesDocs": true,
|
||||
"tagNamePreference": {}
|
||||
},
|
||||
"jsx-a11y": {
|
||||
"attributes": {},
|
||||
"components": {},
|
||||
"polymorphicPropName": null
|
||||
},
|
||||
"next": {
|
||||
"rootDir": []
|
||||
},
|
||||
"react": {
|
||||
"formComponents": [],
|
||||
"linkComponents": []
|
||||
}
|
||||
}
|
||||
}
|
||||
11
.prettierignore
Normal file
11
.prettierignore
Normal file
@@ -0,0 +1,11 @@
|
||||
out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
CHANGELOG*.md
|
||||
agents.json
|
||||
src/renderer/src/integration/nutstore/sso/lib
|
||||
AGENT.md
|
||||
src/main/integration/cherryin/index.js
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"bracketSameLine": true,
|
||||
"endOfLine": "lf",
|
||||
"jsonRecursiveSort": true,
|
||||
"jsonSortOrder": "{\"*\": \"lexical\"}",
|
||||
"plugins": ["prettier-plugin-sort-json"],
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,11 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"editorconfig.editorconfig",
|
||||
"lokalise.i18n-ally",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"vitest.explorer",
|
||||
"oxc.oxc-vscode",
|
||||
"biomejs.biome"
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
||||
|
||||
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@@ -1,43 +1,38 @@
|
||||
{
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[markdown]": {
|
||||
"files.trimTrailingWhitespace": false
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
|
||||
"i18n-ally.displayLanguage": "zh-cn",
|
||||
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
|
||||
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
|
||||
"i18n-ally.fullReloadOnChanged": true,
|
||||
"i18n-ally.fullReloadOnChanged": true, // 界面显示语言
|
||||
"i18n-ally.keystyle": "nested", // 翻译路径格式
|
||||
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
|
||||
// "i18n-ally.namespace": true, // 开启命名空间
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
diff --git a/sdk.mjs b/sdk.mjs
|
||||
index e2dbafb4e2faa1bf2b6b02f0009a2b9bbf57c757..3f07a1d5c2949a246fe5414e69ab45942fa605a2 100644
|
||||
--- a/sdk.mjs
|
||||
+++ b/sdk.mjs
|
||||
@@ -6355,11 +6355,11 @@ class ProcessTransport {
|
||||
prompt,
|
||||
additionalDirectories = [],
|
||||
cwd,
|
||||
- executable = isRunningWithBun() ? "bun" : "node",
|
||||
+ executable = process.execPath,
|
||||
executableArgs = [],
|
||||
extraArgs = {},
|
||||
pathToClaudeCodeExecutable,
|
||||
- env = { ...process.env },
|
||||
+ env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
stderr,
|
||||
customSystemPrompt,
|
||||
appendSystemPrompt,
|
||||
@@ -5,5 +5,3 @@ httpTimeout: 300000
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
npmRegistryServer: https://registry.npmjs.org
|
||||
npmPublishRegistry: https://registry.npmjs.org
|
||||
|
||||
146
CLAUDE.md
146
CLAUDE.md
@@ -1,52 +1,120 @@
|
||||
# AI Assistant Guide
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to AI coding assistants when working with code in this repository. Adherence to these guidelines is crucial for maintaining code quality and consistency.
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
- **Clarity and Simplicity**: Write code that is easy to understand and maintain.
|
||||
- **Consistency**: Follow existing patterns and conventions in the codebase.
|
||||
- **Correctness**: Ensure code is correct, well-tested, and robust.
|
||||
- **Efficiency**: Write performant code and use resources judiciously.
|
||||
|
||||
## MUST Follow Rules
|
||||
|
||||
1. **Code Search**: Use `ast-grep` for semantic code pattern searches when available. Fallback to `rg` (ripgrep) or `grep` for text-based searches.
|
||||
2. **UI Framework**: Exclusively use **HeroUI** for all new UI components. The use of `antd` or `styled-components` is strictly **PROHIBITED**.
|
||||
3. **Quality Assurance**: **Always** run `yarn build:check` before finalizing your work or making any commits. This ensures code quality (linting, testing, and type checking).
|
||||
4. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`.
|
||||
5. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand.
|
||||
6. **Code Reviews**: Always seek a code review from a human developer before merging significant changes. This ensures adherence to project standards and catches potential issues.
|
||||
7. **Documentation**: Update or create documentation for any new features, modules, or significant changes to existing functionality.
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
- **Install**: `yarn install`
|
||||
- **Development**: `yarn dev` - Runs Electron app in development mode
|
||||
- **Debug**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
||||
- **Build Check**: `yarn build:check` - REQUIRED before commits (lint + test + typecheck)
|
||||
- **Test**: `yarn test` - Run all tests (Vitest)
|
||||
- **Single Test**: `yarn test:main` or `yarn test:renderer`
|
||||
- **Lint**: `yarn lint` - Fix linting issues and run typecheck
|
||||
### Environment Setup
|
||||
|
||||
## Project Architecture
|
||||
- **Prerequisites**: Node.js v22.x.x or higher, Yarn 4.9.1
|
||||
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.9.1 --activate`
|
||||
- **Install Dependencies**: `yarn install`
|
||||
|
||||
### Electron Structure
|
||||
- **Main Process** (`src/main/`): Node.js backend with services (MCP, Knowledge, Storage, etc.)
|
||||
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
||||
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
||||
### Development
|
||||
|
||||
### Key Components
|
||||
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
|
||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
||||
- **Start Development**: `yarn dev` - Runs Electron app in development mode
|
||||
- **Debug Mode**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
||||
|
||||
### Testing & Quality
|
||||
|
||||
- **Run Tests**: `yarn test` - Runs all tests (Vitest)
|
||||
- **Run E2E Tests**: `yarn test:e2e` - Playwright end-to-end tests
|
||||
- **Type Check**: `yarn typecheck` - Checks TypeScript for both node and web
|
||||
- **Lint**: `yarn lint` - ESLint with auto-fix
|
||||
- **Format**: `yarn format` - Prettier formatting
|
||||
|
||||
### Build & Release
|
||||
|
||||
- **Build**: `yarn build` - Builds for production (includes typecheck)
|
||||
- **Platform-specific builds**:
|
||||
- Windows: `yarn build:win`
|
||||
- macOS: `yarn build:mac`
|
||||
- Linux: `yarn build:linux`
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Electron Multi-Process Architecture
|
||||
|
||||
- **Main Process** (`src/main/`): Node.js backend handling system integration, file operations, and services
|
||||
- **Renderer Process** (`src/renderer/`): React-based UI running in Chromium
|
||||
- **Preload Scripts** (`src/preload/`): Secure bridge between main and renderer processes
|
||||
|
||||
### Key Architectural Components
|
||||
|
||||
#### Main Process Services (`src/main/services/`)
|
||||
|
||||
- **MCPService**: Model Context Protocol server management
|
||||
- **KnowledgeService**: Document processing and knowledge base management
|
||||
- **FileStorage/S3Storage/WebDav**: Multiple storage backends
|
||||
- **WindowService**: Multi-window management (main, mini, selection windows)
|
||||
- **ProxyManager**: Network proxy handling
|
||||
- **SearchService**: Full-text search capabilities
|
||||
|
||||
#### AI Core (`src/renderer/src/aiCore/`)
|
||||
|
||||
- **Middleware System**: Composable pipeline for AI request processing
|
||||
- **Client Factory**: Supports multiple AI providers (OpenAI, Anthropic, Gemini, etc.)
|
||||
- **Stream Processing**: Real-time response handling
|
||||
|
||||
#### State Management (`src/renderer/src/store/`)
|
||||
|
||||
- **Redux Toolkit**: Centralized state management
|
||||
- **Persistent Storage**: Redux-persist for data persistence
|
||||
- **Thunks**: Async actions for complex operations
|
||||
|
||||
#### Knowledge Management
|
||||
|
||||
- **Embeddings**: Vector search with multiple providers (OpenAI, Voyage, etc.)
|
||||
- **OCR**: Document text extraction (system OCR, Doc2x, Mineru)
|
||||
- **Preprocessing**: Document preparation pipeline
|
||||
- **Loaders**: Support for various file formats (PDF, DOCX, EPUB, etc.)
|
||||
|
||||
### Build System
|
||||
|
||||
- **Electron-Vite**: Development and build tooling (v4.0.0)
|
||||
- **Rolldown-Vite**: Using experimental rolldown-vite instead of standard vite
|
||||
- **Workspaces**: Monorepo structure with `packages/` directory
|
||||
- **Multiple Entry Points**: Main app, mini window, selection toolbar
|
||||
- **Styled Components**: CSS-in-JS styling with SWC optimization
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- **Vitest**: Unit and integration testing
|
||||
- **Playwright**: End-to-end testing
|
||||
- **Component Testing**: React Testing Library
|
||||
- **Coverage**: Available via `yarn test:coverage`
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **IPC Communication**: Secure main-renderer communication via preload scripts
|
||||
- **Service Layer**: Clear separation between UI and business logic
|
||||
- **Plugin Architecture**: Extensible via MCP servers and middleware
|
||||
- **Multi-language Support**: i18n with dynamic loading
|
||||
- **Theme System**: Light/dark themes with custom CSS variables
|
||||
|
||||
## Logging Standards
|
||||
|
||||
### Usage
|
||||
|
||||
### Logging
|
||||
```typescript
|
||||
// Main process
|
||||
import { loggerService } from '@logger'
|
||||
const logger = loggerService.withContext('moduleName')
|
||||
// Renderer: loggerService.initWindowSource('windowName') first
|
||||
|
||||
// Renderer process (set window source first)
|
||||
loggerService.initWindowSource('windowName')
|
||||
const logger = loggerService.withContext('moduleName')
|
||||
|
||||
// Logging
|
||||
logger.info('message', CONTEXT)
|
||||
logger.error('message', new Error('error'), CONTEXT)
|
||||
```
|
||||
|
||||
### Log Levels (highest to lowest)
|
||||
|
||||
- `error` - Critical errors causing crash/unusable functionality
|
||||
- `warn` - Potential issues that don't affect core functionality
|
||||
- `info` - Application lifecycle and key user actions
|
||||
- `verbose` - Detailed flow information for feature tracing
|
||||
- `debug` - Development diagnostic info (not for production)
|
||||
- `silly` - Extreme debugging, low-level information
|
||||
|
||||
45
LICENSE
45
LICENSE
@@ -1,3 +1,48 @@
|
||||
**许可协议 (Licensing)**
|
||||
|
||||
本项目采用**区分用户的双重许可 (User-Segmented Dual Licensing)** 模式。
|
||||
|
||||
**核心原则:**
|
||||
|
||||
* **个人用户 和 10人及以下企业/组织:** 默认适用 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)**。
|
||||
* **超过10人的企业/组织:** **必须** 获取 **商业许可证 (Commercial License)**。
|
||||
|
||||
定义:“10人及以下”
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
|
||||
---
|
||||
|
||||
**1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织**
|
||||
|
||||
* 如果您是个人用户,或者您的组织满足上述“10人及以下”的定义,您可以在 **AGPLv3** 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
|
||||
* **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3 许可证向接收者提供相应的**完整源代码**。即使您符合“10人及以下”的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。
|
||||
* 使用前请务必仔细阅读并理解 AGPLv3 的所有条款。
|
||||
|
||||
**2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3 义务的用户**
|
||||
|
||||
* **强制要求:** 如果您的组织**不**满足上述“10人及以下”的定义(即有11人或更多人可以访问、使用或受益于本软件),您**必须**联系我们获取并签署一份商业许可证才能使用 Cherry Studio。
|
||||
* **自愿选择:** 即使您的组织满足“10人及以下”的条件,但如果您的使用场景**无法满足 AGPLv3 的条款要求**(特别是关于**源代码公开**的义务),或者您需要 AGPLv3 **未提供**的特定商业条款(如保证、赔偿、无 Copyleft 限制等),您也**必须**联系我们获取并签署一份商业许可证。
|
||||
* **需要商业许可证的常见情况包括(但不限于):**
|
||||
* 您的组织规模超过10人。
|
||||
* (无论组织规模)您希望分发修改过的 Cherry Studio 版本,但**不希望**根据 AGPLv3 公开您修改部分的源代码。
|
||||
* (无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但**不希望**根据 AGPLv3 向服务使用者提供修改后的源代码。
|
||||
* (无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
|
||||
* 商业许可证将为您提供豁免 AGPLv3 义务(如源代码公开)的权利,并可能包含额外的商业保障条款。
|
||||
* **获取商业许可:** 请通过邮箱 **bd@cherry-ai.com** 联系 Cherry Studio 开发团队洽谈商业授权事宜。
|
||||
|
||||
**3. 贡献 (Contributions)**
|
||||
|
||||
* 我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 **AGPLv3** 许可证下提供。
|
||||
* 通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
||||
* 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。
|
||||
|
||||
**4. 其他条款 (Other Terms)**
|
||||
|
||||
* 关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。
|
||||
* 项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
||||
|
||||
---
|
||||
|
||||
**Licensing**
|
||||
|
||||
This project employs a **User-Segmented Dual Licensing** model.
|
||||
|
||||
@@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl
|
||||
1. **Diverse LLM Provider Support**:
|
||||
|
||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||
- 🔗 AI Web Service Integration: Claude, Perplexity, Poe, and others
|
||||
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
|
||||
- 💻 Local Model Support with Ollama, LM Studio
|
||||
|
||||
2. **AI Assistants & Conversations**:
|
||||
|
||||
97
biome.jsonc
97
biome.jsonc
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
|
||||
"assist": {
|
||||
// to sort json
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on",
|
||||
"useSortedKeys": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"sortOrder": "lexicographic"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabled": true,
|
||||
"includes": ["**/*.json", "!*.json", "!**/package.json"]
|
||||
},
|
||||
"css": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single"
|
||||
}
|
||||
},
|
||||
"files": { "ignoreUnknown": false },
|
||||
"formatter": {
|
||||
"attributePosition": "auto",
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"enabled": true,
|
||||
"expand": "auto",
|
||||
"formatWithErrors": true,
|
||||
"includes": [
|
||||
"**",
|
||||
"!out/**",
|
||||
"!**/dist/**",
|
||||
"!build/**",
|
||||
"!.yarn/**",
|
||||
"!.github/**",
|
||||
"!.husky/**",
|
||||
"!.vscode/**",
|
||||
"!*.yaml",
|
||||
"!*.yml",
|
||||
"!*.mjs",
|
||||
"!*.cjs",
|
||||
"!*.md",
|
||||
"!*.json",
|
||||
"!src/main/integration/**",
|
||||
"!**/tailwind.css",
|
||||
"!**/package.json"
|
||||
],
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 120,
|
||||
"useEditorconfig": true
|
||||
},
|
||||
"html": { "formatter": { "selfCloseVoidElements": "always" } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"arrowParentheses": "always",
|
||||
"attributePosition": "auto",
|
||||
// To minimize changes in this PR as much as possible, it's set to true. However, setting it to false would make it more convenient to add attributes at the end.
|
||||
"bracketSameLine": true,
|
||||
"bracketSpacing": true,
|
||||
"jsxQuoteStyle": "double",
|
||||
"quoteProperties": "asNeeded",
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded",
|
||||
"trailingCommas": "none"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"parser": {
|
||||
"allowComments": true
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"includes": ["!**/tailwind.css", "src/renderer/**/*.{tsx,ts}"],
|
||||
// only enable sorted tailwind css rule. used as formatter instead of linter
|
||||
"rules": {
|
||||
"nursery": {
|
||||
// to sort tailwind css classes
|
||||
"useSortedClasses": {
|
||||
"fix": "safe",
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"functions": ["cn"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"recommended": false,
|
||||
"suspicious": "off"
|
||||
}
|
||||
},
|
||||
"vcs": { "clientKind": "git", "enabled": false, "useIgnoreFile": false }
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"aliases": {
|
||||
"components": "@renderer/ui/third-party",
|
||||
"hooks": "@renderer/hooks",
|
||||
"lib": "@renderer/lib",
|
||||
"ui": "@renderer/ui",
|
||||
"utils": "@renderer/utils"
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rsc": false,
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"baseColor": "zinc",
|
||||
"config": "",
|
||||
"css": "src/renderer/src/assets/styles/tailwind.css",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"tsx": true
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=fr">Français</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=de">Deutsch</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=es">Español</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=it">Italiano</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=it">Itapano</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ru">Русский</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=pt">Português</a></p>
|
||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=nl">Nederlands</a></p>
|
||||
@@ -89,7 +89,7 @@ https://docs.cherry-ai.com
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
- 🔗 集成流行 AI Web 服务:Claude、Perplexity、Poe、腾讯元宝、知乎直答等
|
||||
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
||||
- 💻 支持 Ollama、LM Studio 本地模型部署
|
||||
|
||||
2. **智能助手与对话**:
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
## IDE Setup
|
||||
|
||||
- Editor: [Cursor](https://www.cursor.com/), etc. Any VS Code compatible editor.
|
||||
- Linter: [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
- Formatter: [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome)
|
||||
[Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
|
||||
## Project Setup
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
| 字段名 | 类型 | 是否主键 | 索引 | 说明 |
|
||||
| ---------- | ------ | -------- | ---- | ------------------------------------------------------------------------ |
|
||||
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
|
||||
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
|
||||
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
|
||||
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji,用户输入 |
|
||||
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
|
||||
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
|
||||
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
|
||||
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji,用户输入 |
|
||||
|
||||
> `langCode` 虽非主键,但在业务层应当避免重复插入相同语言代码。
|
||||
|
||||
@@ -17,52 +17,52 @@ protocols:
|
||||
schemes:
|
||||
- cherrystudio
|
||||
files:
|
||||
- "**/*"
|
||||
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}}"
|
||||
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
|
||||
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
|
||||
- "!**/{.editorconfig,.jekyll-metadata}"
|
||||
- "!src"
|
||||
- "!scripts"
|
||||
- "!local"
|
||||
- "!docs"
|
||||
- "!packages"
|
||||
- "!.swc"
|
||||
- "!.bin"
|
||||
- "!._*"
|
||||
- "!*.log"
|
||||
- "!stats.html"
|
||||
- "!*.md"
|
||||
- "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}"
|
||||
- "!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}"
|
||||
- "!**/{test,tests,__tests__,powered-test,coverage}/**"
|
||||
- "!**/{example,examples}/**"
|
||||
- "!**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||
- "!**/*.min.*.map"
|
||||
- "!**/*.d.ts"
|
||||
- "!**/dist/es6/**"
|
||||
- "!**/dist/demo/**"
|
||||
- "!**/amd/**"
|
||||
- "!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}"
|
||||
- "!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}"
|
||||
- "!node_modules/rollup-plugin-visualizer"
|
||||
- "!node_modules/js-tiktoken"
|
||||
- "!node_modules/@tavily/core/node_modules/js-tiktoken"
|
||||
- "!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}"
|
||||
- "!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}"
|
||||
- "!node_modules/selection-hook/prebuilds/**/*" # we rebuild .node, don't use prebuilds
|
||||
- "!node_modules/selection-hook/node_modules" # we don't need what in the node_modules dir
|
||||
- "!node_modules/selection-hook/src" # we don't need source files
|
||||
- "!node_modules/tesseract.js-core/{tesseract-core.js,tesseract-core.wasm,tesseract-core.wasm.js}" # we don't need source files
|
||||
- "!node_modules/tesseract.js-core/{tesseract-core-lstm.js,tesseract-core-lstm.wasm,tesseract-core-lstm.wasm.js}" # we don't need source files
|
||||
- "!node_modules/tesseract.js-core/{tesseract-core-simd-lstm.js,tesseract-core-simd-lstm.wasm,tesseract-core-simd-lstm.wasm.js}" # we don't need source files
|
||||
- "!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}" # filter .node build files
|
||||
- '**/*'
|
||||
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}}'
|
||||
- '!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}'
|
||||
- '!**/{.editorconfig,.jekyll-metadata}'
|
||||
- '!src'
|
||||
- '!scripts'
|
||||
- '!local'
|
||||
- '!docs'
|
||||
- '!packages'
|
||||
- '!.swc'
|
||||
- '!.bin'
|
||||
- '!._*'
|
||||
- '!*.log'
|
||||
- '!stats.html'
|
||||
- '!*.md'
|
||||
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
|
||||
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
|
||||
- '!**/{test,tests,__tests__,powered-test,coverage}/**'
|
||||
- '!**/{example,examples}/**'
|
||||
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||||
- '!**/*.min.*.map'
|
||||
- '!**/*.d.ts'
|
||||
- '!**/dist/es6/**'
|
||||
- '!**/dist/demo/**'
|
||||
- '!**/amd/**'
|
||||
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
|
||||
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
|
||||
- '!node_modules/rollup-plugin-visualizer'
|
||||
- '!node_modules/js-tiktoken'
|
||||
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
|
||||
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
|
||||
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||
- '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds
|
||||
- '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir
|
||||
- '!node_modules/selection-hook/src' # we don't need source files
|
||||
- '!node_modules/tesseract.js-core/{tesseract-core.js,tesseract-core.wasm,tesseract-core.wasm.js}' # we don't need source files
|
||||
- '!node_modules/tesseract.js-core/{tesseract-core-lstm.js,tesseract-core-lstm.wasm,tesseract-core-lstm.wasm.js}' # we don't need source files
|
||||
- '!node_modules/tesseract.js-core/{tesseract-core-simd-lstm.js,tesseract-core-simd-lstm.wasm,tesseract-core-simd-lstm.wasm.js}' # we don't need source files
|
||||
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
- "**/*.{metal,exp,lib}"
|
||||
- "node_modules/@img/sharp-libvips-*/**"
|
||||
- '**/*.{metal,exp,lib}'
|
||||
- 'node_modules/@img/sharp-libvips-*/**'
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
@@ -88,7 +88,7 @@ mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
minimumSystemVersion: "20.1.0" # 最低支持 macOS 11.0
|
||||
minimumSystemVersion: '20.1.0' # 最低支持 macOS 11.0
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -110,10 +110,6 @@ linux:
|
||||
StartupWMClass: CherryStudio
|
||||
mimeTypes:
|
||||
- x-scheme-handler/cherrystudio
|
||||
rpm:
|
||||
# Workaround for electron build issue on rpm package:
|
||||
# https://github.com/electron/forge/issues/3594
|
||||
fpm: ["--rpm-rpmbuild-define=_build_id_links none"]
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://releases.cherry-ai.com
|
||||
@@ -125,16 +121,12 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
🔧 性能优化:
|
||||
- 优化AI服务连接方式,提升响应速度和稳定性
|
||||
- 改进模型列表获取功能,减少不必要的网络请求
|
||||
- 增强各AI服务商的兼容性和连接可靠性
|
||||
|
||||
🐛 问题修复:
|
||||
- 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理
|
||||
- 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题
|
||||
- 修复 MiniWindow 高度问题
|
||||
|
||||
🚀 性能优化:
|
||||
- 优化输入栏提及模型状态缓存,在渲染间保持状态
|
||||
- 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持
|
||||
|
||||
🔧 重构改进:
|
||||
- 更新 HeroUIProvider 导入路径,改善上下文管理
|
||||
- 更新依赖项和 VSCode 开发环境配置
|
||||
- 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17
|
||||
- 修复部分AI服务商连接失败的问题
|
||||
- 修复模型配置加载时的潜在错误
|
||||
- 提升应用整体稳定性和容错能力
|
||||
|
||||
@@ -4,9 +4,7 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
// assert not supported by biome
|
||||
// import pkg from './package.json' assert { type: 'json' }
|
||||
import pkg from './package.json'
|
||||
import pkg from './package.json' assert { type: 'json' }
|
||||
|
||||
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
||||
@@ -25,7 +23,10 @@ export default defineConfig({
|
||||
'@shared': resolve('packages/shared'),
|
||||
'@logger': resolve('src/main/services/LoggerService'),
|
||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node')
|
||||
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node'),
|
||||
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
|
||||
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
|
||||
'@cherrystudio/ai-core': resolve('packages/aiCore/src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
@@ -62,7 +63,6 @@ export default defineConfig({
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||
react({
|
||||
tsDecorators: true,
|
||||
plugins: [
|
||||
@@ -84,7 +84,6 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@renderer': resolve('src/renderer/src'),
|
||||
'@shared': resolve('packages/shared'),
|
||||
'@types': resolve('src/renderer/src/types'),
|
||||
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import electronConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
||||
import tseslint from '@electron-toolkit/eslint-config-ts'
|
||||
import eslint from '@eslint/js'
|
||||
import eslintReact from '@eslint-react/eslint-plugin'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
@@ -10,6 +10,7 @@ import unusedImports from 'eslint-plugin-unused-imports'
|
||||
export default defineConfig([
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
electronConfigPrettier,
|
||||
eslintReact.configs['recommended-typescript'],
|
||||
reactHooks.configs['recommended-latest'],
|
||||
{
|
||||
@@ -25,6 +26,7 @@ export default defineConfig([
|
||||
'simple-import-sort/exports': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'@eslint-react/no-prop-types': 'error',
|
||||
'prettier/prettier': ['error']
|
||||
}
|
||||
},
|
||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||
@@ -51,7 +53,7 @@ export default defineConfig([
|
||||
{
|
||||
// LoggerService Custom Rules - only apply to src directory
|
||||
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
||||
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
|
||||
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
process.env.PRCI ? 'error' : 'warn',
|
||||
@@ -121,14 +123,7 @@ export default defineConfig([
|
||||
'.gitignore',
|
||||
'scripts/cloudflare-worker.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/main/integration/cherryin/index.js',
|
||||
'src/main/integration/nutstore/sso/lib/**',
|
||||
'src/renderer/src/ui/**',
|
||||
'packages/**/dist'
|
||||
'src/main/integration/cherryin/index.js'
|
||||
]
|
||||
},
|
||||
// turn off oxlint supported rules.
|
||||
...oxlint.configs['flat/eslint'],
|
||||
...oxlint.configs['flat/typescript'],
|
||||
...oxlint.configs['flat/unicorn']
|
||||
}
|
||||
])
|
||||
|
||||
112
package.json
112
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.6.0-rc.2",
|
||||
"version": "1.6.0-beta.6",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -27,7 +27,6 @@
|
||||
"scripts": {
|
||||
"start": "electron-vite preview",
|
||||
"dev": "dotenv electron-vite dev",
|
||||
"dev:main": "dotenv electron-vite dev --watch",
|
||||
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build:check": "yarn lint && yarn test",
|
||||
@@ -44,18 +43,15 @@
|
||||
"release": "node scripts/version.js",
|
||||
"publish": "yarn build:check && yarn release patch push",
|
||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||
"agents:generate": "NODE_ENV='development' drizzle-kit generate --config src/main/services/agents/drizzle.config.ts",
|
||||
"agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts",
|
||||
"agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts",
|
||||
"agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts",
|
||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
|
||||
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
||||
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
||||
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"check:i18n": "tsx scripts/check-i18n.ts",
|
||||
"sync:i18n": "tsx scripts/sync-i18n.ts",
|
||||
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
||||
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
||||
"update:languages": "tsx scripts/update-languages.ts",
|
||||
@@ -67,37 +63,26 @@
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "yarn playwright test",
|
||||
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
||||
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
||||
"test:scripts": "vitest scripts",
|
||||
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n",
|
||||
"format": "biome format --write && biome lint --write",
|
||||
"format:check": "biome format && biome lint",
|
||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
||||
"claude": "dotenv -e .env -- claude",
|
||||
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
||||
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix && yarn typecheck && yarn check:i18n",
|
||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-code": "patch:@anthropic-ai/claude-code@npm%3A1.0.118#~/.yarn/patches/@anthropic-ai-claude-code-npm-1.0.118-bbf4e9e59f.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",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"ai-sdk-provider-claude-code": "^1.1.3",
|
||||
"express": "^5.1.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"font-list": "^2.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jsdom": "26.1.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.2.0",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"selection-hook": "^1.0.12",
|
||||
"selection-hook": "^1.0.11",
|
||||
"sharp": "^0.34.3",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
@@ -105,18 +90,16 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.21",
|
||||
"@ai-sdk/google-vertex": "^3.0.27",
|
||||
"@ai-sdk/mistral": "^2.0.14",
|
||||
"@ai-sdk/perplexity": "^2.0.9",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.0",
|
||||
"@ai-sdk/google-vertex": "^3.0.0",
|
||||
"@ai-sdk/mistral": "^2.0.0",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.41.0",
|
||||
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||
"@aws-sdk/client-bedrock": "^3.840.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||
"@aws-sdk/client-s3": "^3.840.0",
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
|
||||
"@cherrystudio/ai-core": "workspace:*",
|
||||
"@cherrystudio/embedjs": "^0.1.31",
|
||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
||||
@@ -134,6 +117,7 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
@@ -144,11 +128,11 @@
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@langchain/community": "^0.3.50",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@langchain/ollama": "^0.2.1",
|
||||
"@mistralai/mistralai": "^1.7.5",
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@openrouter/ai-sdk-provider": "^1.1.2",
|
||||
@@ -159,11 +143,9 @@
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.12.0",
|
||||
"@swc/plugin-styled-components": "^8.0.4",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
@@ -189,30 +171,21 @@
|
||||
"@truto/turndown-plugin-gfm": "^1.0.2",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/cli-progress": "^3",
|
||||
"@types/content-type": "^1.1.9",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/diff": "^7",
|
||||
"@types/express": "^5",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/he": "^1",
|
||||
"@types/html-to-text": "^9",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/mime-types": "^3",
|
||||
"@types/node": "^22.17.1",
|
||||
"@types/pako": "^1.0.2",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1",
|
||||
"@types/swagger-jsdoc": "^6",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/word-extractor": "^1",
|
||||
"@typescript/native-preview": "latest",
|
||||
"@uiw/codemirror-extensions-langs": "^4.25.1",
|
||||
"@uiw/codemirror-themes-all": "^4.25.1",
|
||||
"@uiw/react-codemirror": "^4.25.1",
|
||||
@@ -224,18 +197,15 @@
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"ai": "^5.0.44",
|
||||
"ai": "^5.0.29",
|
||||
"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",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"chardet": "^2.1.0",
|
||||
"check-disk-space": "3.4.0",
|
||||
"cheerio": "^1.1.2",
|
||||
"chokidar": "^4.0.3",
|
||||
"cli-progress": "^3.12.0",
|
||||
"clsx": "^2.1.1",
|
||||
"code-inspector-plugin": "^0.20.14",
|
||||
"color": "^5.0.0",
|
||||
"concurrently": "^9.2.1",
|
||||
@@ -247,11 +217,9 @@
|
||||
"docx": "^9.0.2",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"electron": "37.4.0",
|
||||
"electron-builder": "26.0.15",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-reload": "^2.0.0-alpha.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "6.6.4",
|
||||
"electron-vite": "4.0.0",
|
||||
@@ -260,21 +228,18 @@
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-oxlint": "^1.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"fast-diff": "^1.3.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"fetch-socks": "1.3.2",
|
||||
"framer-motion": "^12.23.12",
|
||||
"franc-min": "^6.2.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"he": "^1.2.0",
|
||||
"html-tags": "^5.1.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"html-to-text": "^9.0.5",
|
||||
"htmlparser2": "^10.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"i18next": "^23.11.5",
|
||||
@@ -291,17 +256,15 @@
|
||||
"markdown-it": "^14.1.0",
|
||||
"mermaid": "^11.10.1",
|
||||
"mime": "^4.0.4",
|
||||
"mime-types": "^3.0.1",
|
||||
"motion": "^12.10.5",
|
||||
"notion-helper": "^1.3.22",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
||||
"oxlint": "^1.15.0",
|
||||
"oxlint-tsgolint": "^0.2.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"playwright": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -311,7 +274,6 @@
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-player": "^3.3.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
@@ -331,23 +293,21 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"remove-markdown": "^0.6.2",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.88.0",
|
||||
"shiki": "^3.12.0",
|
||||
"strict-url-sanitise": "^0.0.1",
|
||||
"string-width": "^7.2.0",
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swr": "^2.3.6",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"tokenx": "^1.1.0",
|
||||
"tsx": "^4.20.3",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "~5.8.2",
|
||||
"typescript": "^5.6.2",
|
||||
"undici": "6.21.2",
|
||||
"unified": "^11.0.5",
|
||||
"uuid": "^13.0.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vitest": "^3.2.4",
|
||||
"webdav": "^5.8.0",
|
||||
@@ -355,11 +315,9 @@
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"word-extractor": "^1.0.4",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yaml": "^2.8.1",
|
||||
"yjs": "^13.6.27",
|
||||
"youtubei.js": "^15.0.1",
|
||||
"zipread": "^1.3.3",
|
||||
"zod": "^4.1.5"
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"resolutions": {
|
||||
"@codemirror/language": "6.11.3",
|
||||
@@ -380,16 +338,22 @@
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"undici": "6.21.2",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch"
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.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"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
||||
"biome format --write --no-errors-on-unmatched",
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{json,yml,yaml,css,html}": [
|
||||
"biome format --write --no-errors-on-unmatched"
|
||||
"*.{json,yml,yaml,css,scss,html}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
103
packages/aiCore/examples/hub-provider-usage.ts
Normal file
103
packages/aiCore/examples/hub-provider-usage.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Hub Provider 使用示例
|
||||
*
|
||||
* 演示如何使用简化后的Hub Provider功能来路由到多个底层provider
|
||||
*/
|
||||
|
||||
import { createHubProvider, initializeProvider, providerRegistry } from '../src/index'
|
||||
|
||||
async function demonstrateHubProvider() {
|
||||
try {
|
||||
// 1. 初始化底层providers
|
||||
console.log('📦 初始化底层providers...')
|
||||
|
||||
initializeProvider('openai', {
|
||||
apiKey: process.env.OPENAI_API_KEY || 'sk-test-key'
|
||||
})
|
||||
|
||||
initializeProvider('anthropic', {
|
||||
apiKey: process.env.ANTHROPIC_API_KEY || 'sk-ant-test-key'
|
||||
})
|
||||
|
||||
// 2. 创建Hub Provider(自动包含所有已初始化的providers)
|
||||
console.log('🌐 创建Hub Provider...')
|
||||
|
||||
const aihubmixProvider = createHubProvider({
|
||||
hubId: 'aihubmix',
|
||||
debug: true
|
||||
})
|
||||
|
||||
// 3. 注册Hub Provider
|
||||
providerRegistry.registerProvider('aihubmix', aihubmixProvider)
|
||||
|
||||
console.log('✅ Hub Provider "aihubmix" 注册成功')
|
||||
|
||||
// 4. 使用Hub Provider访问不同的模型
|
||||
console.log('\n🚀 使用Hub模型...')
|
||||
|
||||
// 通过Hub路由到OpenAI
|
||||
const openaiModel = providerRegistry.languageModel('aihubmix:openai:gpt-4')
|
||||
console.log('✓ OpenAI模型已获取:', openaiModel.modelId)
|
||||
|
||||
// 通过Hub路由到Anthropic
|
||||
const anthropicModel = providerRegistry.languageModel('aihubmix:anthropic:claude-3.5-sonnet')
|
||||
console.log('✓ Anthropic模型已获取:', anthropicModel.modelId)
|
||||
|
||||
// 5. 演示错误处理
|
||||
console.log('\n❌ 演示错误处理...')
|
||||
|
||||
try {
|
||||
// 尝试访问未初始化的provider
|
||||
providerRegistry.languageModel('aihubmix:google:gemini-pro')
|
||||
} catch (error) {
|
||||
console.log('预期错误:', error.message)
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试使用错误的模型ID格式
|
||||
providerRegistry.languageModel('aihubmix:invalid-format')
|
||||
} catch (error) {
|
||||
console.log('预期错误:', error.message)
|
||||
}
|
||||
|
||||
// 6. 多个Hub Provider示例
|
||||
console.log('\n🔄 创建多个Hub Provider...')
|
||||
|
||||
const localHubProvider = createHubProvider({
|
||||
hubId: 'local-ai'
|
||||
})
|
||||
|
||||
providerRegistry.registerProvider('local-ai', localHubProvider)
|
||||
console.log('✅ Hub Provider "local-ai" 注册成功')
|
||||
|
||||
console.log('\n🎉 Hub Provider演示完成!')
|
||||
} catch (error) {
|
||||
console.error('💥 演示过程中发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 演示简化的使用方式
|
||||
function simplifiedUsageExample() {
|
||||
console.log('\n📝 简化使用示例:')
|
||||
console.log(`
|
||||
// 1. 初始化providers
|
||||
initializeProvider('openai', { apiKey: 'sk-xxx' })
|
||||
initializeProvider('anthropic', { apiKey: 'sk-ant-xxx' })
|
||||
|
||||
// 2. 创建并注册Hub Provider
|
||||
const hubProvider = createHubProvider({ hubId: 'aihubmix' })
|
||||
providerRegistry.registerProvider('aihubmix', hubProvider)
|
||||
|
||||
// 3. 直接使用
|
||||
const model1 = providerRegistry.languageModel('aihubmix:openai:gpt-4')
|
||||
const model2 = providerRegistry.languageModel('aihubmix:anthropic:claude-3.5-sonnet')
|
||||
`)
|
||||
}
|
||||
|
||||
// 运行演示
|
||||
if (require.main === module) {
|
||||
demonstrateHubProvider()
|
||||
simplifiedUsageExample()
|
||||
}
|
||||
|
||||
export { demonstrateHubProvider, simplifiedUsageExample }
|
||||
167
packages/aiCore/examples/image-generation.ts
Normal file
167
packages/aiCore/examples/image-generation.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Image Generation Example
|
||||
* 演示如何使用 aiCore 的文生图功能
|
||||
*/
|
||||
|
||||
import { createExecutor, generateImage } from '../src/index'
|
||||
|
||||
async function main() {
|
||||
// 方式1: 使用执行器实例
|
||||
console.log('📸 创建 OpenAI 图像生成执行器...')
|
||||
const executor = createExecutor('openai', {
|
||||
apiKey: process.env.OPENAI_API_KEY!
|
||||
})
|
||||
|
||||
try {
|
||||
console.log('🎨 使用执行器生成图像...')
|
||||
const result1 = await executor.generateImage('dall-e-3', {
|
||||
prompt: 'A futuristic cityscape at sunset with flying cars',
|
||||
size: '1024x1024',
|
||||
n: 1
|
||||
})
|
||||
|
||||
console.log('✅ 图像生成成功!')
|
||||
console.log('📊 结果:', {
|
||||
imagesCount: result1.images.length,
|
||||
mediaType: result1.image.mediaType,
|
||||
hasBase64: !!result1.image.base64,
|
||||
providerMetadata: result1.providerMetadata
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 执行器生成失败:', error)
|
||||
}
|
||||
|
||||
// 方式2: 使用直接调用 API
|
||||
try {
|
||||
console.log('🎨 使用直接 API 生成图像...')
|
||||
const result2 = await generateImage('openai', { apiKey: process.env.OPENAI_API_KEY! }, 'dall-e-3', {
|
||||
prompt: 'A magical forest with glowing mushrooms and fairy lights',
|
||||
aspectRatio: '16:9',
|
||||
providerOptions: {
|
||||
openai: {
|
||||
quality: 'hd',
|
||||
style: 'vivid'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('✅ 直接 API 生成成功!')
|
||||
console.log('📊 结果:', {
|
||||
imagesCount: result2.images.length,
|
||||
mediaType: result2.image.mediaType,
|
||||
hasBase64: !!result2.image.base64
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 直接 API 生成失败:', error)
|
||||
}
|
||||
|
||||
// 方式3: 支持其他提供商 (Google Imagen)
|
||||
if (process.env.GOOGLE_API_KEY) {
|
||||
try {
|
||||
console.log('🎨 使用 Google Imagen 生成图像...')
|
||||
const googleExecutor = createExecutor('google', {
|
||||
apiKey: process.env.GOOGLE_API_KEY!
|
||||
})
|
||||
|
||||
const result3 = await googleExecutor.generateImage('imagen-3.0-generate-002', {
|
||||
prompt: 'A serene mountain lake at dawn with mist rising from the water',
|
||||
aspectRatio: '1:1'
|
||||
})
|
||||
|
||||
console.log('✅ Google Imagen 生成成功!')
|
||||
console.log('📊 结果:', {
|
||||
imagesCount: result3.images.length,
|
||||
mediaType: result3.image.mediaType,
|
||||
hasBase64: !!result3.image.base64
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Google Imagen 生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 方式4: 支持插件系统
|
||||
const pluginExample = async () => {
|
||||
console.log('🔌 演示插件系统...')
|
||||
|
||||
// 创建一个示例插件,用于修改提示词
|
||||
const promptEnhancerPlugin = {
|
||||
name: 'prompt-enhancer',
|
||||
transformParams: async (params: any) => {
|
||||
console.log('🔧 插件: 增强提示词...')
|
||||
return {
|
||||
...params,
|
||||
prompt: `${params.prompt}, highly detailed, cinematic lighting, 4K resolution`
|
||||
}
|
||||
},
|
||||
transformResult: async (result: any) => {
|
||||
console.log('🔧 插件: 处理结果...')
|
||||
return {
|
||||
...result,
|
||||
enhanced: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const executorWithPlugin = createExecutor(
|
||||
'openai',
|
||||
{
|
||||
apiKey: process.env.OPENAI_API_KEY!
|
||||
},
|
||||
[promptEnhancerPlugin]
|
||||
)
|
||||
|
||||
try {
|
||||
const result4 = await executorWithPlugin.generateImage('dall-e-3', {
|
||||
prompt: 'A cute robot playing in a garden'
|
||||
})
|
||||
|
||||
console.log('✅ 插件系统生成成功!')
|
||||
console.log('📊 结果:', {
|
||||
imagesCount: result4.images.length,
|
||||
enhanced: (result4 as any).enhanced,
|
||||
mediaType: result4.image.mediaType
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 插件系统生成失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
await pluginExample()
|
||||
}
|
||||
|
||||
// 错误处理演示
|
||||
async function errorHandlingExample() {
|
||||
console.log('⚠️ 演示错误处理...')
|
||||
|
||||
try {
|
||||
const executor = createExecutor('openai', {
|
||||
apiKey: 'invalid-key'
|
||||
})
|
||||
|
||||
await executor.generateImage('dall-e-3', {
|
||||
prompt: 'Test image'
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.log('✅ 成功捕获错误:', error.constructor.name)
|
||||
console.log('📋 错误信息:', error.message)
|
||||
console.log('🏷️ 提供商ID:', error.providerId)
|
||||
console.log('🏷️ 模型ID:', error.modelId)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行示例
|
||||
if (require.main === module) {
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('🎉 所有示例完成!')
|
||||
return errorHandlingExample()
|
||||
})
|
||||
.then(() => {
|
||||
console.log('🎯 示例程序结束')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 程序执行出错:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cherrystudio/ai-core",
|
||||
"version": "1.0.0-alpha.18",
|
||||
"version": "1.0.0-alpha.11",
|
||||
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
@@ -36,16 +36,16 @@
|
||||
"ai": "^5.0.26"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^2.0.17",
|
||||
"@ai-sdk/azure": "^2.0.30",
|
||||
"@ai-sdk/deepseek": "^1.0.17",
|
||||
"@ai-sdk/google": "^2.0.14",
|
||||
"@ai-sdk/openai": "^2.0.30",
|
||||
"@ai-sdk/openai-compatible": "^1.0.17",
|
||||
"@ai-sdk/anthropic": "^2.0.5",
|
||||
"@ai-sdk/azure": "^2.0.16",
|
||||
"@ai-sdk/deepseek": "^1.0.9",
|
||||
"@ai-sdk/google": "^2.0.7",
|
||||
"@ai-sdk/openai": "^2.0.19",
|
||||
"@ai-sdk/openai-compatible": "^1.0.9",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@ai-sdk/provider-utils": "^3.0.9",
|
||||
"@ai-sdk/xai": "^2.0.18",
|
||||
"zod": "^4.1.5"
|
||||
"@ai-sdk/provider-utils": "^3.0.4",
|
||||
"@ai-sdk/xai": "^2.0.9",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsdown": "^0.12.9",
|
||||
|
||||
@@ -84,6 +84,7 @@ export class ModelResolver {
|
||||
*/
|
||||
private resolveTraditionalModel(providerId: string, modelId: string): LanguageModelV2 {
|
||||
const fullModelId = `${providerId}${DEFAULT_SEPARATOR}${modelId}`
|
||||
console.log('fullModelId', fullModelId)
|
||||
return globalRegistryManagement.languageModel(fullModelId as any)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export function createGoogleOptions(options: ExtractProviderOptions<'google'>) {
|
||||
/**
|
||||
* 创建OpenRouter供应商选项的便捷函数
|
||||
*/
|
||||
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'> | Record<string, any>) {
|
||||
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'>) {
|
||||
return createProviderOptions('openrouter', options)
|
||||
}
|
||||
|
||||
|
||||
38
packages/aiCore/src/core/options/openrouter.ts
Normal file
38
packages/aiCore/src/core/options/openrouter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export type OpenRouterProviderOptions = {
|
||||
models?: string[]
|
||||
|
||||
/**
|
||||
* https://openrouter.ai/docs/use-cases/reasoning-tokens
|
||||
* One of `max_tokens` or `effort` is required.
|
||||
* If `exclude` is true, reasoning will be removed from the response. Default is false.
|
||||
*/
|
||||
reasoning?: {
|
||||
exclude?: boolean
|
||||
} & (
|
||||
| {
|
||||
max_tokens: number
|
||||
}
|
||||
| {
|
||||
effort: 'high' | 'medium' | 'low'
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A unique identifier representing your end-user, which can
|
||||
* help OpenRouter to monitor and detect abuse.
|
||||
*/
|
||||
user?: string
|
||||
|
||||
extraBody?: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Enable usage accounting to get detailed token usage information.
|
||||
* https://openrouter.ai/docs/use-cases/usage-accounting
|
||||
*/
|
||||
usage?: {
|
||||
/**
|
||||
* When true, includes token usage information in the response.
|
||||
*/
|
||||
include: boolean
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import { type AnthropicProviderOptions } from '@ai-sdk/anthropic'
|
||||
import { type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
|
||||
import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
|
||||
import { type SharedV2ProviderMetadata } from '@ai-sdk/provider'
|
||||
import { type XaiProviderOptions } from '@ai-sdk/xai'
|
||||
import { type OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider'
|
||||
|
||||
import { type OpenRouterProviderOptions } from './openrouter'
|
||||
import { type XaiProviderOptions } from './xai'
|
||||
|
||||
export type ProviderOptions<T extends keyof SharedV2ProviderMetadata> = SharedV2ProviderMetadata[T]
|
||||
|
||||
|
||||
86
packages/aiCore/src/core/options/xai.ts
Normal file
86
packages/aiCore/src/core/options/xai.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
// copy from @ai-sdk/xai/xai-chat-options.ts
|
||||
// 如果@ai-sdk/xai暴露出了xaiProviderOptions就删除这个文件
|
||||
|
||||
import * as z from 'zod/v4'
|
||||
|
||||
const webSourceSchema = z.object({
|
||||
type: z.literal('web'),
|
||||
country: z.string().length(2).optional(),
|
||||
excludedWebsites: z.array(z.string()).max(5).optional(),
|
||||
allowedWebsites: z.array(z.string()).max(5).optional(),
|
||||
safeSearch: z.boolean().optional()
|
||||
})
|
||||
|
||||
const xSourceSchema = z.object({
|
||||
type: z.literal('x'),
|
||||
xHandles: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
const newsSourceSchema = z.object({
|
||||
type: z.literal('news'),
|
||||
country: z.string().length(2).optional(),
|
||||
excludedWebsites: z.array(z.string()).max(5).optional(),
|
||||
safeSearch: z.boolean().optional()
|
||||
})
|
||||
|
||||
const rssSourceSchema = z.object({
|
||||
type: z.literal('rss'),
|
||||
links: z.array(z.url()).max(1) // currently only supports one RSS link
|
||||
})
|
||||
|
||||
const searchSourceSchema = z.discriminatedUnion('type', [
|
||||
webSourceSchema,
|
||||
xSourceSchema,
|
||||
newsSourceSchema,
|
||||
rssSourceSchema
|
||||
])
|
||||
|
||||
export const xaiProviderOptions = z.object({
|
||||
/**
|
||||
* reasoning effort for reasoning models
|
||||
* only supported by grok-3-mini and grok-3-mini-fast models
|
||||
*/
|
||||
reasoningEffort: z.enum(['low', 'high']).optional(),
|
||||
|
||||
searchParameters: z
|
||||
.object({
|
||||
/**
|
||||
* search mode preference
|
||||
* - "off": disables search completely
|
||||
* - "auto": model decides whether to search (default)
|
||||
* - "on": always enables search
|
||||
*/
|
||||
mode: z.enum(['off', 'auto', 'on']),
|
||||
|
||||
/**
|
||||
* whether to return citations in the response
|
||||
* defaults to true
|
||||
*/
|
||||
returnCitations: z.boolean().optional(),
|
||||
|
||||
/**
|
||||
* start date for search data (ISO8601 format: YYYY-MM-DD)
|
||||
*/
|
||||
fromDate: z.string().optional(),
|
||||
|
||||
/**
|
||||
* end date for search data (ISO8601 format: YYYY-MM-DD)
|
||||
*/
|
||||
toDate: z.string().optional(),
|
||||
|
||||
/**
|
||||
* maximum number of search results to consider
|
||||
* defaults to 20
|
||||
*/
|
||||
maxSearchResults: z.number().min(1).max(50).optional(),
|
||||
|
||||
/**
|
||||
* data sources to search from
|
||||
* defaults to ["web", "x"] if not specified
|
||||
*/
|
||||
sources: z.array(searchSourceSchema).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
export type XaiProviderOptions = z.infer<typeof xaiProviderOptions>
|
||||
@@ -1,38 +0,0 @@
|
||||
import { google } from '@ai-sdk/google'
|
||||
|
||||
import { definePlugin } from '../../'
|
||||
import type { AiRequestContext } from '../../types'
|
||||
|
||||
const toolNameMap = {
|
||||
googleSearch: 'google_search',
|
||||
urlContext: 'url_context',
|
||||
codeExecution: 'code_execution'
|
||||
} as const
|
||||
|
||||
type ToolConfigKey = keyof typeof toolNameMap
|
||||
type ToolConfig = { googleSearch?: boolean; urlContext?: boolean; codeExecution?: boolean }
|
||||
|
||||
export const googleToolsPlugin = (config?: ToolConfig) =>
|
||||
definePlugin({
|
||||
name: 'googleToolsPlugin',
|
||||
transformParams: <T>(params: T, context: AiRequestContext): T => {
|
||||
const { providerId } = context
|
||||
if (providerId === 'google' && config) {
|
||||
if (typeof params === 'object' && params !== null) {
|
||||
const typedParams = params as T & { tools?: Record<string, unknown> }
|
||||
|
||||
if (!typedParams.tools) {
|
||||
typedParams.tools = {}
|
||||
}
|
||||
// 使用类型安全的方式遍历配置
|
||||
;(Object.keys(config) as ToolConfigKey[]).forEach((key) => {
|
||||
if (config[key] && key in toolNameMap && key in google.tools) {
|
||||
const toolName = toolNameMap[key]
|
||||
typedParams.tools![toolName] = google.tools[key]({})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
})
|
||||
@@ -4,12 +4,7 @@
|
||||
*/
|
||||
export const BUILT_IN_PLUGIN_PREFIX = 'built-in:'
|
||||
|
||||
export { googleToolsPlugin } from './googleToolsPlugin'
|
||||
export { createLoggingPlugin } from './logging'
|
||||
export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin'
|
||||
export type {
|
||||
PromptToolUseConfig,
|
||||
ToolUseRequestContext,
|
||||
ToolUseResult
|
||||
} from './toolUsePlugin/type'
|
||||
export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin'
|
||||
export type { PromptToolUseConfig, ToolUseRequestContext, ToolUseResult } from './toolUsePlugin/type'
|
||||
export { webSearchPlugin } from './webSearchPlugin'
|
||||
|
||||
@@ -27,20 +27,10 @@ export class StreamEventManager {
|
||||
/**
|
||||
* 发送步骤完成事件
|
||||
*/
|
||||
sendStepFinishEvent(
|
||||
controller: StreamController,
|
||||
chunk: any,
|
||||
context: AiRequestContext,
|
||||
finishReason: string = 'stop'
|
||||
): void {
|
||||
// 累加当前步骤的 usage
|
||||
if (chunk.usage && context.accumulatedUsage) {
|
||||
this.accumulateUsage(context.accumulatedUsage, chunk.usage)
|
||||
}
|
||||
|
||||
sendStepFinishEvent(controller: StreamController, chunk: any): void {
|
||||
controller.enqueue({
|
||||
type: 'finish-step',
|
||||
finishReason,
|
||||
finishReason: 'stop',
|
||||
response: chunk.response,
|
||||
usage: chunk.usage,
|
||||
providerMetadata: chunk.providerMetadata
|
||||
@@ -53,32 +43,28 @@ export class StreamEventManager {
|
||||
async handleRecursiveCall(
|
||||
controller: StreamController,
|
||||
recursiveParams: any,
|
||||
context: AiRequestContext
|
||||
context: AiRequestContext,
|
||||
stepId: string
|
||||
): Promise<void> {
|
||||
// try {
|
||||
// 重置工具执行状态,准备处理新的步骤
|
||||
context.hasExecutedToolsInCurrentStep = false
|
||||
try {
|
||||
console.log('[MCP Prompt] Starting recursive call after tool execution...')
|
||||
|
||||
const recursiveResult = await context.recursiveCall(recursiveParams)
|
||||
const recursiveResult = await context.recursiveCall(recursiveParams)
|
||||
|
||||
if (recursiveResult && recursiveResult.fullStream) {
|
||||
await this.pipeRecursiveStream(controller, recursiveResult.fullStream, context)
|
||||
} else {
|
||||
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
|
||||
if (recursiveResult && recursiveResult.fullStream) {
|
||||
await this.pipeRecursiveStream(controller, recursiveResult.fullStream)
|
||||
} else {
|
||||
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleRecursiveCallError(controller, error, stepId)
|
||||
}
|
||||
// } catch (error) {
|
||||
// this.handleRecursiveCallError(controller, error, stepId)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 将递归流的数据传递到当前流
|
||||
*/
|
||||
private async pipeRecursiveStream(
|
||||
controller: StreamController,
|
||||
recursiveStream: ReadableStream,
|
||||
context?: AiRequestContext
|
||||
): Promise<void> {
|
||||
private async pipeRecursiveStream(controller: StreamController, recursiveStream: ReadableStream): Promise<void> {
|
||||
const reader = recursiveStream.getReader()
|
||||
try {
|
||||
while (true) {
|
||||
@@ -87,16 +73,9 @@ export class StreamEventManager {
|
||||
break
|
||||
}
|
||||
if (value.type === 'finish') {
|
||||
// 迭代的流不发finish,但需要累加其 usage
|
||||
if (value.usage && context?.accumulatedUsage) {
|
||||
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
||||
}
|
||||
// 迭代的流不发finish
|
||||
break
|
||||
}
|
||||
// 对于 finish-step 类型,累加其 usage
|
||||
if (value.type === 'finish-step' && value.usage && context?.accumulatedUsage) {
|
||||
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
||||
}
|
||||
// 将递归流的数据传递到当前流
|
||||
controller.enqueue(value)
|
||||
}
|
||||
@@ -108,25 +87,25 @@ export class StreamEventManager {
|
||||
/**
|
||||
* 处理递归调用错误
|
||||
*/
|
||||
// private handleRecursiveCallError(controller: StreamController, error: unknown): void {
|
||||
// console.error('[MCP Prompt] Recursive call failed:', error)
|
||||
private handleRecursiveCallError(controller: StreamController, error: unknown, stepId: string): void {
|
||||
console.error('[MCP Prompt] Recursive call failed:', error)
|
||||
|
||||
// // 使用 AI SDK 标准错误格式,但不中断流
|
||||
// controller.enqueue({
|
||||
// type: 'error',
|
||||
// error: {
|
||||
// message: error instanceof Error ? error.message : String(error),
|
||||
// name: error instanceof Error ? error.name : 'RecursiveCallError'
|
||||
// }
|
||||
// })
|
||||
// 使用 AI SDK 标准错误格式,但不中断流
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
name: error instanceof Error ? error.name : 'RecursiveCallError'
|
||||
}
|
||||
})
|
||||
|
||||
// // // 继续发送文本增量,保持流的连续性
|
||||
// // controller.enqueue({
|
||||
// // type: 'text-delta',
|
||||
// // id: stepId,
|
||||
// // text: '\n\n[工具执行后递归调用失败,继续对话...]'
|
||||
// // })
|
||||
// }
|
||||
// 继续发送文本增量,保持流的连续性
|
||||
controller.enqueue({
|
||||
type: 'text-delta',
|
||||
id: stepId,
|
||||
text: '\n\n[工具执行后递归调用失败,继续对话...]'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建递归调用的参数
|
||||
@@ -157,18 +136,4 @@ export class StreamEventManager {
|
||||
|
||||
return recursiveParams
|
||||
}
|
||||
|
||||
/**
|
||||
* 累加 usage 数据
|
||||
*/
|
||||
private accumulateUsage(target: any, source: any): void {
|
||||
if (!target || !source) return
|
||||
|
||||
// 累加各种 token 类型
|
||||
target.inputTokens = (target.inputTokens || 0) + (source.inputTokens || 0)
|
||||
target.outputTokens = (target.outputTokens || 0) + (source.outputTokens || 0)
|
||||
target.totalTokens = (target.totalTokens || 0) + (source.totalTokens || 0)
|
||||
target.reasoningTokens = (target.reasoningTokens || 0) + (source.reasoningTokens || 0)
|
||||
target.cachedInputTokens = (target.cachedInputTokens || 0) + (source.cachedInputTokens || 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 负责工具的执行、结果格式化和相关事件发送
|
||||
* 从 promptToolUsePlugin.ts 中提取出来以降低复杂度
|
||||
*/
|
||||
import type { ToolSet, TypedToolError } from 'ai'
|
||||
import type { ToolSet } from 'ai'
|
||||
|
||||
import type { ToolUseResult } from './type'
|
||||
|
||||
@@ -38,6 +38,7 @@ export class ToolExecutor {
|
||||
controller: StreamController
|
||||
): Promise<ExecutedResult[]> {
|
||||
const executedResults: ExecutedResult[] = []
|
||||
|
||||
for (const toolUse of toolUses) {
|
||||
try {
|
||||
const tool = tools[toolUse.toolName]
|
||||
@@ -45,12 +46,17 @@ export class ToolExecutor {
|
||||
throw new Error(`Tool "${toolUse.toolName}" has no execute method`)
|
||||
}
|
||||
|
||||
// 发送工具调用开始事件
|
||||
this.sendToolStartEvents(controller, toolUse)
|
||||
|
||||
console.log(`[MCP Prompt Stream] Executing tool: ${toolUse.toolName}`, toolUse.arguments)
|
||||
|
||||
// 发送 tool-call 事件
|
||||
controller.enqueue({
|
||||
type: 'tool-call',
|
||||
toolCallId: toolUse.id,
|
||||
toolName: toolUse.toolName,
|
||||
input: toolUse.arguments
|
||||
input: tool.inputSchema
|
||||
})
|
||||
|
||||
const result = await tool.execute(toolUse.arguments, {
|
||||
@@ -105,46 +111,45 @@ export class ToolExecutor {
|
||||
/**
|
||||
* 发送工具调用开始相关事件
|
||||
*/
|
||||
// private sendToolStartEvents(controller: StreamController, toolUse: ToolUseResult): void {
|
||||
// // 发送 tool-input-start 事件
|
||||
// controller.enqueue({
|
||||
// type: 'tool-input-start',
|
||||
// id: toolUse.id,
|
||||
// toolName: toolUse.toolName
|
||||
// })
|
||||
// }
|
||||
private sendToolStartEvents(controller: StreamController, toolUse: ToolUseResult): void {
|
||||
// 发送 tool-input-start 事件
|
||||
controller.enqueue({
|
||||
type: 'tool-input-start',
|
||||
id: toolUse.id,
|
||||
toolName: toolUse.toolName
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理工具执行错误
|
||||
*/
|
||||
private handleToolError<T extends ToolSet>(
|
||||
private handleToolError(
|
||||
toolUse: ToolUseResult,
|
||||
error: unknown,
|
||||
controller: StreamController
|
||||
// _tools: ToolSet
|
||||
): ExecutedResult {
|
||||
// 使用 AI SDK 标准错误格式
|
||||
const toolError: TypedToolError<T> = {
|
||||
type: 'tool-error',
|
||||
toolCallId: toolUse.id,
|
||||
toolName: toolUse.toolName,
|
||||
input: toolUse.arguments,
|
||||
error
|
||||
}
|
||||
|
||||
controller.enqueue(toolError)
|
||||
|
||||
// 发送标准错误事件
|
||||
// controller.enqueue({
|
||||
// const toolError: TypedToolError<typeof _tools> = {
|
||||
// type: 'tool-error',
|
||||
// toolCallId: toolUse.id,
|
||||
// error: error instanceof Error ? error.message : String(error),
|
||||
// input: toolUse.arguments
|
||||
// })
|
||||
// toolName: toolUse.toolName,
|
||||
// input: toolUse.arguments,
|
||||
// error: error instanceof Error ? error.message : String(error)
|
||||
// }
|
||||
|
||||
// controller.enqueue(toolError)
|
||||
|
||||
// 发送标准错误事件
|
||||
controller.enqueue({
|
||||
type: 'error',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
|
||||
return {
|
||||
toolCallId: toolUse.id,
|
||||
toolName: toolUse.toolName,
|
||||
result: error,
|
||||
result: error instanceof Error ? error.message : String(error),
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,9 @@ import type { TextStreamPart, ToolSet } from 'ai'
|
||||
import { definePlugin } from '../../index'
|
||||
import type { AiRequestContext } from '../../types'
|
||||
import { StreamEventManager } from './StreamEventManager'
|
||||
import { type TagConfig, TagExtractor } from './tagExtraction'
|
||||
import { ToolExecutor } from './ToolExecutor'
|
||||
import { PromptToolUseConfig, ToolUseResult } from './type'
|
||||
|
||||
/**
|
||||
* 工具使用标签配置
|
||||
*/
|
||||
const TOOL_USE_TAG_CONFIG: TagConfig = {
|
||||
openingTag: '<tool_use>',
|
||||
closingTag: '</tool_use>',
|
||||
separator: '\n'
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认系统提示符模板(提取自 Cherry Studio)
|
||||
*/
|
||||
@@ -156,10 +146,8 @@ Assistant: The population of Shanghai is 26 million, while Guangzhou has a popul
|
||||
/**
|
||||
* 构建可用工具部分(提取自 Cherry Studio)
|
||||
*/
|
||||
function buildAvailableTools(tools: ToolSet): string | null {
|
||||
function buildAvailableTools(tools: ToolSet): string {
|
||||
const availableTools = Object.keys(tools)
|
||||
if (availableTools.length === 0) return null
|
||||
const result = availableTools
|
||||
.map((toolName: string) => {
|
||||
const tool = tools[toolName]
|
||||
return `
|
||||
@@ -174,7 +162,7 @@ function buildAvailableTools(tools: ToolSet): string | null {
|
||||
})
|
||||
.join('\n')
|
||||
return `<tools>
|
||||
${result}
|
||||
${availableTools}
|
||||
</tools>`
|
||||
}
|
||||
|
||||
@@ -183,7 +171,6 @@ ${result}
|
||||
*/
|
||||
function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet): string {
|
||||
const availableTools = buildAvailableTools(tools)
|
||||
if (availableTools === null) return userSystemPrompt
|
||||
|
||||
const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES)
|
||||
.replace('{{ AVAILABLE_TOOLS }}', availableTools)
|
||||
@@ -262,11 +249,13 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
||||
}
|
||||
|
||||
context.mcpTools = params.tools
|
||||
console.log('tools stored in context', params.tools)
|
||||
|
||||
// 构建系统提示符
|
||||
const userSystemPrompt = typeof params.system === 'string' ? params.system : ''
|
||||
const systemPrompt = buildSystemPrompt(userSystemPrompt, params.tools)
|
||||
let systemMessage: string | null = systemPrompt
|
||||
console.log('config.context', context)
|
||||
if (config.createSystemMessage) {
|
||||
// 🎯 如果用户提供了自定义处理函数,使用它
|
||||
systemMessage = config.createSystemMessage(systemPrompt, params, context)
|
||||
@@ -279,40 +268,20 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
||||
tools: undefined
|
||||
}
|
||||
context.originalParams = transformedParams
|
||||
console.log('transformedParams', transformedParams)
|
||||
return transformedParams
|
||||
},
|
||||
transformStream: (_: any, context: AiRequestContext) => () => {
|
||||
let textBuffer = ''
|
||||
// let stepId = ''
|
||||
let stepId = ''
|
||||
|
||||
if (!context.mcpTools) {
|
||||
throw new Error('No tools available')
|
||||
}
|
||||
|
||||
// 从 context 中获取或初始化 usage 累加器
|
||||
if (!context.accumulatedUsage) {
|
||||
context.accumulatedUsage = {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
reasoningTokens: 0,
|
||||
cachedInputTokens: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工具执行器、流事件管理器和标签提取器
|
||||
// 创建工具执行器和流事件管理器
|
||||
const toolExecutor = new ToolExecutor()
|
||||
const streamEventManager = new StreamEventManager()
|
||||
const tagExtractor = new TagExtractor(TOOL_USE_TAG_CONFIG)
|
||||
|
||||
// 在context中初始化工具执行状态,避免递归调用时状态丢失
|
||||
if (!context.hasExecutedToolsInCurrentStep) {
|
||||
context.hasExecutedToolsInCurrentStep = false
|
||||
}
|
||||
|
||||
// 用于hold text-start事件,直到确认有非工具标签内容
|
||||
let pendingTextStart: TextStreamPart<TOOLS> | null = null
|
||||
let hasStartedText = false
|
||||
|
||||
type TOOLS = NonNullable<typeof context.mcpTools>
|
||||
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
|
||||
@@ -320,106 +289,83 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
||||
chunk: TextStreamPart<TOOLS>,
|
||||
controller: TransformStreamDefaultController<TextStreamPart<TOOLS>>
|
||||
) {
|
||||
// Hold住text-start事件,直到确认有非工具标签内容
|
||||
if ((chunk as any).type === 'text-start') {
|
||||
pendingTextStart = chunk
|
||||
return
|
||||
}
|
||||
|
||||
// text-delta阶段:收集文本内容并过滤工具标签
|
||||
// 收集文本内容
|
||||
if (chunk.type === 'text-delta') {
|
||||
textBuffer += chunk.text || ''
|
||||
// stepId = chunk.id || ''
|
||||
|
||||
// 使用TagExtractor过滤工具标签,只传递非标签内容到UI层
|
||||
const extractionResults = tagExtractor.processText(chunk.text || '')
|
||||
|
||||
for (const result of extractionResults) {
|
||||
// 只传递非标签内容到UI层
|
||||
if (!result.isTagContent && result.content) {
|
||||
// 如果还没有发送text-start且有pending的text-start,先发送它
|
||||
if (!hasStartedText && pendingTextStart) {
|
||||
controller.enqueue(pendingTextStart)
|
||||
hasStartedText = true
|
||||
pendingTextStart = null
|
||||
}
|
||||
|
||||
const filteredChunk = {
|
||||
...chunk,
|
||||
text: result.content
|
||||
}
|
||||
controller.enqueue(filteredChunk)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (chunk.type === 'text-end') {
|
||||
// 只有当已经发送了text-start时才发送text-end
|
||||
if (hasStartedText) {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (chunk.type === 'finish-step') {
|
||||
// 统一在finish-step阶段检查并执行工具调用
|
||||
const tools = context.mcpTools
|
||||
if (tools && Object.keys(tools).length > 0 && !context.hasExecutedToolsInCurrentStep) {
|
||||
// 解析完整的textBuffer来检测工具调用
|
||||
const { results: parsedTools } = parseToolUse(textBuffer, tools)
|
||||
const validToolUses = parsedTools.filter((t) => t.status === 'pending')
|
||||
|
||||
if (validToolUses.length > 0) {
|
||||
context.hasExecutedToolsInCurrentStep = true
|
||||
|
||||
// 执行工具调用(不需要手动发送 start-step,外部流已经处理)
|
||||
const executedResults = await toolExecutor.executeTools(validToolUses, tools, controller)
|
||||
|
||||
// 发送步骤完成事件,使用 tool-calls 作为 finishReason
|
||||
streamEventManager.sendStepFinishEvent(controller, chunk, context, 'tool-calls')
|
||||
|
||||
// 处理递归调用
|
||||
const toolResultsText = toolExecutor.formatToolResults(executedResults)
|
||||
const recursiveParams = streamEventManager.buildRecursiveParams(
|
||||
context,
|
||||
textBuffer,
|
||||
toolResultsText,
|
||||
tools
|
||||
)
|
||||
|
||||
await streamEventManager.handleRecursiveCall(controller, recursiveParams, context)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有执行工具调用,直接传递原始finish-step事件
|
||||
stepId = chunk.id || ''
|
||||
controller.enqueue(chunk)
|
||||
return
|
||||
}
|
||||
|
||||
if (chunk.type === 'text-end' || chunk.type === 'finish-step') {
|
||||
const tools = context.mcpTools
|
||||
if (!tools || Object.keys(tools).length === 0) {
|
||||
controller.enqueue(chunk)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析工具调用
|
||||
const { results: parsedTools, content: parsedContent } = parseToolUse(textBuffer, tools)
|
||||
const validToolUses = parsedTools.filter((t) => t.status === 'pending')
|
||||
|
||||
// 如果没有有效的工具调用,直接传递原始事件
|
||||
if (validToolUses.length === 0) {
|
||||
controller.enqueue(chunk)
|
||||
return
|
||||
}
|
||||
|
||||
if (chunk.type === 'text-end') {
|
||||
controller.enqueue({
|
||||
type: 'text-end',
|
||||
id: stepId,
|
||||
providerMetadata: {
|
||||
text: {
|
||||
value: parsedContent
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
controller.enqueue({
|
||||
...chunk,
|
||||
finishReason: 'tool-calls'
|
||||
})
|
||||
|
||||
// 发送步骤开始事件
|
||||
streamEventManager.sendStepStartEvent(controller)
|
||||
|
||||
// 执行工具调用
|
||||
const executedResults = await toolExecutor.executeTools(validToolUses, tools, controller)
|
||||
|
||||
// 发送步骤完成事件
|
||||
streamEventManager.sendStepFinishEvent(controller, chunk)
|
||||
|
||||
// 处理递归调用
|
||||
if (validToolUses.length > 0) {
|
||||
const toolResultsText = toolExecutor.formatToolResults(executedResults)
|
||||
const recursiveParams = streamEventManager.buildRecursiveParams(
|
||||
context,
|
||||
textBuffer,
|
||||
toolResultsText,
|
||||
tools
|
||||
)
|
||||
|
||||
await streamEventManager.handleRecursiveCall(controller, recursiveParams, context, stepId)
|
||||
}
|
||||
|
||||
// 清理状态
|
||||
textBuffer = ''
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 finish 类型,使用累加后的 totalUsage
|
||||
if (chunk.type === 'finish') {
|
||||
controller.enqueue({
|
||||
...chunk,
|
||||
totalUsage: context.accumulatedUsage
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 对于其他类型的事件,直接传递(不包括text-start,已在上面处理)
|
||||
if ((chunk as any).type !== 'text-start') {
|
||||
controller.enqueue(chunk)
|
||||
}
|
||||
// 对于其他类型的事件,直接传递
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
|
||||
flush() {
|
||||
// 清理pending状态
|
||||
pendingTextStart = null
|
||||
hasStartedText = false
|
||||
// 流结束时的清理工作
|
||||
console.log('[MCP Prompt] Stream ended, cleaning up...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@ import { google } from '@ai-sdk/google'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
|
||||
import { ProviderOptionsMap } from '../../../options/types'
|
||||
import { OpenRouterSearchConfig } from './openrouter'
|
||||
|
||||
/**
|
||||
* 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。
|
||||
*/
|
||||
export type OpenAISearchConfig = NonNullable<Parameters<typeof openai.tools.webSearch>[0]>
|
||||
export type OpenAISearchPreviewConfig = NonNullable<Parameters<typeof openai.tools.webSearchPreview>[0]>
|
||||
export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tools.webSearch_20250305>[0]>
|
||||
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
|
||||
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
|
||||
type OpenAISearchConfig = Parameters<typeof openai.tools.webSearchPreview>[0]
|
||||
type AnthropicSearchConfig = Parameters<typeof anthropic.tools.webSearch_20250305>[0]
|
||||
type GoogleSearchConfig = Parameters<typeof google.tools.googleSearch>[0]
|
||||
|
||||
/**
|
||||
* 插件初始化时接收的完整配置对象
|
||||
@@ -21,12 +18,10 @@ export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParam
|
||||
*/
|
||||
export interface WebSearchPluginConfig {
|
||||
openai?: OpenAISearchConfig
|
||||
'openai-chat'?: OpenAISearchPreviewConfig
|
||||
anthropic?: AnthropicSearchConfig
|
||||
xai?: ProviderOptionsMap['xai']['searchParameters']
|
||||
google?: GoogleSearchConfig
|
||||
'google-vertex'?: GoogleSearchConfig
|
||||
openrouter?: OpenRouterSearchConfig
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +31,6 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
||||
google: {},
|
||||
'google-vertex': {},
|
||||
openai: {},
|
||||
'openai-chat': {},
|
||||
xai: {
|
||||
mode: 'on',
|
||||
returnCitations: true,
|
||||
@@ -45,14 +39,6 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
||||
},
|
||||
anthropic: {
|
||||
maxUses: 5
|
||||
},
|
||||
openrouter: {
|
||||
plugins: [
|
||||
{
|
||||
id: 'web',
|
||||
max_results: 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { anthropic } from '@ai-sdk/anthropic'
|
||||
import { google } from '@ai-sdk/google'
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
|
||||
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
||||
import { createXaiOptions, mergeProviderOptions } from '../../../options'
|
||||
import { definePlugin } from '../../'
|
||||
import type { AiRequestContext } from '../../types'
|
||||
import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper'
|
||||
@@ -27,14 +27,7 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
|
||||
case 'openai': {
|
||||
if (config.openai) {
|
||||
if (!params.tools) params.tools = {}
|
||||
params.tools.web_search = openai.tools.webSearch(config.openai)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'openai-chat': {
|
||||
if (config['openai-chat']) {
|
||||
if (!params.tools) params.tools = {}
|
||||
params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat'])
|
||||
params.tools.web_search_preview = openai.tools.webSearchPreview(config.openai)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -63,14 +56,6 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'openrouter': {
|
||||
if (config.openrouter) {
|
||||
const searchOptions = createOpenRouterOptions(config.openrouter)
|
||||
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export type OpenRouterSearchConfig = {
|
||||
plugins?: Array<{
|
||||
id: 'web'
|
||||
/**
|
||||
* Maximum number of search results to include (default: 5)
|
||||
*/
|
||||
max_results?: number
|
||||
/**
|
||||
* Custom search prompt to guide the search query
|
||||
*/
|
||||
search_prompt?: string
|
||||
}>
|
||||
/**
|
||||
* Built-in web search options for models that support native web search
|
||||
*/
|
||||
web_search_options?: {
|
||||
/**
|
||||
* Maximum number of search results to include
|
||||
*/
|
||||
max_results?: number
|
||||
/**
|
||||
* Custom search prompt to guide the search query
|
||||
*/
|
||||
search_prompt?: string
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// 核心类型和接口
|
||||
export type { AiPlugin, AiRequestContext, HookResult, PluginManagerConfig } from './types'
|
||||
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||
import type { LanguageModel } from 'ai'
|
||||
|
||||
import type { ProviderId } from '../providers'
|
||||
import type { AiPlugin, AiRequestContext } from './types'
|
||||
|
||||
@@ -12,16 +9,16 @@ export { PluginManager } from './manager'
|
||||
// 工具函数
|
||||
export function createContext<T extends ProviderId>(
|
||||
providerId: T,
|
||||
model: LanguageModel | ImageModelV2,
|
||||
modelId: string,
|
||||
originalParams: any
|
||||
): AiRequestContext {
|
||||
return {
|
||||
providerId,
|
||||
model,
|
||||
modelId,
|
||||
originalParams,
|
||||
metadata: {},
|
||||
startTime: Date.now(),
|
||||
requestId: `${providerId}-${typeof model === 'string' ? model : model?.modelId}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
requestId: `${providerId}-${modelId}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
// 占位
|
||||
recursiveCall: () => Promise.resolve(null)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export type RecursiveCallFn = (newParams: any) => Promise<any>
|
||||
*/
|
||||
export interface AiRequestContext {
|
||||
providerId: ProviderId
|
||||
model: LanguageModel | ImageModelV2
|
||||
modelId: string
|
||||
originalParams: any
|
||||
metadata: Record<string, any>
|
||||
startTime: number
|
||||
|
||||
@@ -9,11 +9,9 @@ import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||
import { LanguageModelV2 } from '@ai-sdk/provider'
|
||||
import { createXai } from '@ai-sdk/xai'
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||
import { customProvider, Provider } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { customProvider, type Provider } from 'ai'
|
||||
import * as z from 'zod'
|
||||
|
||||
/**
|
||||
* 基础 Provider IDs
|
||||
@@ -27,8 +25,7 @@ export const baseProviderIds = [
|
||||
'xai',
|
||||
'azure',
|
||||
'azure-responses',
|
||||
'deepseek',
|
||||
'openrouter'
|
||||
'deepseek'
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -41,16 +38,14 @@ export const baseProviderIdSchema = z.enum(baseProviderIds)
|
||||
*/
|
||||
export type BaseProviderId = z.infer<typeof baseProviderIdSchema>
|
||||
|
||||
export const isBaseProvider = (id: ProviderId): id is BaseProviderId => {
|
||||
return baseProviderIdSchema.safeParse(id).success
|
||||
}
|
||||
export const baseProviderSchema = z.object({
|
||||
id: baseProviderIdSchema,
|
||||
name: z.string(),
|
||||
creator: z.function().args(z.any()).returns(z.any()) as z.ZodType<(options: any) => Provider>,
|
||||
supportsImageGeneration: z.boolean()
|
||||
})
|
||||
|
||||
type BaseProvider = {
|
||||
id: BaseProviderId
|
||||
name: string
|
||||
creator: (options: any) => Provider | LanguageModelV2
|
||||
supportsImageGeneration: boolean
|
||||
}
|
||||
export type BaseProvider = z.infer<typeof baseProviderSchema>
|
||||
|
||||
/**
|
||||
* 基础 Providers 定义
|
||||
@@ -126,12 +121,6 @@ export const baseProviders = [
|
||||
name: 'DeepSeek',
|
||||
creator: createDeepSeek,
|
||||
supportsImageGeneration: false
|
||||
},
|
||||
{
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
creator: createOpenRouter,
|
||||
supportsImageGeneration: true
|
||||
}
|
||||
] as const satisfies BaseProvider[]
|
||||
|
||||
@@ -159,12 +148,7 @@ export const providerConfigSchema = z
|
||||
.object({
|
||||
id: customProviderIdSchema, // 只允许自定义ID
|
||||
name: z.string().min(1),
|
||||
creator: z
|
||||
.function({
|
||||
input: z.any(),
|
||||
output: z.any()
|
||||
})
|
||||
.optional(),
|
||||
creator: z.function().optional(),
|
||||
import: z.function().optional(),
|
||||
creatorFunctionName: z.string().optional(),
|
||||
supportsImageGeneration: z.boolean().default(false),
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
import { ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||
import {
|
||||
experimental_generateImage as _generateImage,
|
||||
generateObject as _generateObject,
|
||||
generateText as _generateText,
|
||||
experimental_generateImage as generateImage,
|
||||
generateObject,
|
||||
generateText,
|
||||
LanguageModel,
|
||||
streamObject as _streamObject,
|
||||
streamText as _streamText
|
||||
streamObject,
|
||||
streamText
|
||||
} from 'ai'
|
||||
|
||||
import { globalModelResolver } from '../models'
|
||||
@@ -18,14 +18,7 @@ import { type AiPlugin, type AiRequestContext, definePlugin } from '../plugins'
|
||||
import { type ProviderId } from '../providers'
|
||||
import { ImageGenerationError, ImageModelResolutionError } from './errors'
|
||||
import { PluginEngine } from './pluginEngine'
|
||||
import type {
|
||||
generateImageParams,
|
||||
generateObjectParams,
|
||||
generateTextParams,
|
||||
RuntimeConfig,
|
||||
streamObjectParams,
|
||||
streamTextParams
|
||||
} from './types'
|
||||
import { type RuntimeConfig } from './types'
|
||||
|
||||
export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
public pluginEngine: PluginEngine<T>
|
||||
@@ -82,12 +75,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
* 流式文本生成
|
||||
*/
|
||||
async streamText(
|
||||
params: streamTextParams,
|
||||
params: Parameters<typeof streamText>[0],
|
||||
options?: {
|
||||
middlewares?: LanguageModelV2Middleware[]
|
||||
}
|
||||
): Promise<ReturnType<typeof _streamText>> {
|
||||
const { model } = params
|
||||
): Promise<ReturnType<typeof streamText>> {
|
||||
const { model, ...restParams } = params
|
||||
|
||||
// 根据 model 类型决定插件配置
|
||||
if (typeof model === 'string') {
|
||||
@@ -101,16 +94,19 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
|
||||
return this.pluginEngine.executeStreamWithPlugins(
|
||||
'streamText',
|
||||
params,
|
||||
(resolvedModel, transformedParams, streamTransforms) => {
|
||||
model,
|
||||
restParams,
|
||||
async (resolvedModel, transformedParams, streamTransforms) => {
|
||||
const experimental_transform =
|
||||
params?.experimental_transform ?? (streamTransforms.length > 0 ? streamTransforms : undefined)
|
||||
|
||||
return _streamText({
|
||||
...transformedParams,
|
||||
const finalParams = {
|
||||
model: resolvedModel,
|
||||
...transformedParams,
|
||||
experimental_transform
|
||||
})
|
||||
} as Parameters<typeof streamText>[0]
|
||||
|
||||
return await streamText(finalParams)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -121,12 +117,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
* 生成文本
|
||||
*/
|
||||
async generateText(
|
||||
params: generateTextParams,
|
||||
params: Parameters<typeof generateText>[0],
|
||||
options?: {
|
||||
middlewares?: LanguageModelV2Middleware[]
|
||||
}
|
||||
): Promise<ReturnType<typeof _generateText>> {
|
||||
const { model } = params
|
||||
): Promise<ReturnType<typeof generateText>> {
|
||||
const { model, ...restParams } = params
|
||||
|
||||
// 根据 model 类型决定插件配置
|
||||
if (typeof model === 'string') {
|
||||
@@ -138,10 +134,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||
}
|
||||
|
||||
return this.pluginEngine.executeWithPlugins<Parameters<typeof _generateText>[0], ReturnType<typeof _generateText>>(
|
||||
return this.pluginEngine.executeWithPlugins(
|
||||
'generateText',
|
||||
params,
|
||||
(resolvedModel, transformedParams) => _generateText({ ...transformedParams, model: resolvedModel })
|
||||
model,
|
||||
restParams,
|
||||
async (resolvedModel, transformedParams) =>
|
||||
generateText({ model: resolvedModel, ...transformedParams } as Parameters<typeof generateText>[0])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,12 +147,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
* 生成结构化对象
|
||||
*/
|
||||
async generateObject(
|
||||
params: generateObjectParams,
|
||||
params: Parameters<typeof generateObject>[0],
|
||||
options?: {
|
||||
middlewares?: LanguageModelV2Middleware[]
|
||||
}
|
||||
): Promise<ReturnType<typeof _generateObject>> {
|
||||
const { model } = params
|
||||
): Promise<ReturnType<typeof generateObject>> {
|
||||
const { model, ...restParams } = params
|
||||
|
||||
// 根据 model 类型决定插件配置
|
||||
if (typeof model === 'string') {
|
||||
@@ -166,23 +164,25 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||
}
|
||||
|
||||
return this.pluginEngine.executeWithPlugins<generateObjectParams, ReturnType<typeof _generateObject>>(
|
||||
return this.pluginEngine.executeWithPlugins(
|
||||
'generateObject',
|
||||
params,
|
||||
async (resolvedModel, transformedParams) => _generateObject({ ...transformedParams, model: resolvedModel })
|
||||
model,
|
||||
restParams,
|
||||
async (resolvedModel, transformedParams) =>
|
||||
generateObject({ model: resolvedModel, ...transformedParams } as Parameters<typeof generateObject>[0])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式生成结构化对象
|
||||
*/
|
||||
streamObject(
|
||||
params: streamObjectParams,
|
||||
async streamObject(
|
||||
params: Parameters<typeof streamObject>[0],
|
||||
options?: {
|
||||
middlewares?: LanguageModelV2Middleware[]
|
||||
}
|
||||
): Promise<ReturnType<typeof _streamObject>> {
|
||||
const { model } = params
|
||||
): Promise<ReturnType<typeof streamObject>> {
|
||||
const { model, ...restParams } = params
|
||||
|
||||
// 根据 model 类型决定插件配置
|
||||
if (typeof model === 'string') {
|
||||
@@ -194,17 +194,23 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||
}
|
||||
|
||||
return this.pluginEngine.executeStreamWithPlugins('streamObject', params, (resolvedModel, transformedParams) =>
|
||||
_streamObject({ ...transformedParams, model: resolvedModel })
|
||||
return this.pluginEngine.executeWithPlugins(
|
||||
'streamObject',
|
||||
model,
|
||||
restParams,
|
||||
async (resolvedModel, transformedParams) =>
|
||||
streamObject({ model: resolvedModel, ...transformedParams } as Parameters<typeof streamObject>[0])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图像
|
||||
*/
|
||||
generateImage(params: generateImageParams): Promise<ReturnType<typeof _generateImage>> {
|
||||
async generateImage(
|
||||
params: Omit<Parameters<typeof generateImage>[0], 'model'> & { model: string | ImageModelV2 }
|
||||
): Promise<ReturnType<typeof generateImage>> {
|
||||
try {
|
||||
const { model } = params
|
||||
const { model, ...restParams } = params
|
||||
|
||||
// 根据 model 类型决定插件配置
|
||||
if (typeof model === 'string') {
|
||||
@@ -213,8 +219,13 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||
}
|
||||
|
||||
return this.pluginEngine.executeImageWithPlugins('generateImage', params, (resolvedModel, transformedParams) =>
|
||||
_generateImage({ ...transformedParams, model: resolvedModel })
|
||||
return await this.pluginEngine.executeImageWithPlugins(
|
||||
'generateImage',
|
||||
model,
|
||||
restParams,
|
||||
async (resolvedModel, transformedParams) => {
|
||||
return await generateImage({ model: resolvedModel, ...transformedParams })
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @eslint-react/naming-convention/context-name */
|
||||
import { ImageModelV2 } from '@ai-sdk/provider'
|
||||
import { experimental_generateImage, generateObject, generateText, LanguageModel, streamObject, streamText } from 'ai'
|
||||
import { LanguageModel } from 'ai'
|
||||
|
||||
import { type AiPlugin, createContext, PluginManager } from '../plugins'
|
||||
import { type ProviderId } from '../providers/types'
|
||||
@@ -62,19 +62,17 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
* 执行带插件的操作(非流式)
|
||||
* 提供给AiExecutor使用
|
||||
*/
|
||||
async executeWithPlugins<
|
||||
TParams extends Parameters<typeof generateText | typeof generateObject>[0],
|
||||
TResult extends ReturnType<typeof generateText | typeof generateObject>
|
||||
>(
|
||||
async executeWithPlugins<TParams, TResult>(
|
||||
methodName: string,
|
||||
model: LanguageModel,
|
||||
params: TParams,
|
||||
executor: (model: LanguageModel, transformedParams: TParams) => TResult,
|
||||
executor: (model: LanguageModel, transformedParams: TParams) => Promise<TResult>,
|
||||
_context?: ReturnType<typeof createContext>
|
||||
): Promise<TResult> {
|
||||
// 统一处理模型解析
|
||||
let resolvedModel: LanguageModel | undefined
|
||||
let modelId: string
|
||||
const { model } = params
|
||||
|
||||
if (typeof model === 'string') {
|
||||
// 字符串:需要通过插件解析
|
||||
modelId = model
|
||||
@@ -85,13 +83,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
}
|
||||
|
||||
// 使用正确的createContext创建请求上下文
|
||||
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||
const context = _context ? _context : createContext(this.providerId, modelId, params)
|
||||
|
||||
// 🔥 为上下文添加递归调用能力
|
||||
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||
// 递归调用自身,重新走完整的插件流程
|
||||
context.isRecursiveCall = true
|
||||
const result = await this.executeWithPlugins(methodName, newParams, executor, context)
|
||||
const result = await this.executeWithPlugins(methodName, model, newParams, executor, context)
|
||||
context.isRecursiveCall = false
|
||||
return result
|
||||
}
|
||||
@@ -140,19 +138,17 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
* 执行带插件的图像生成操作
|
||||
* 提供给AiExecutor使用
|
||||
*/
|
||||
async executeImageWithPlugins<
|
||||
TParams extends Omit<Parameters<typeof experimental_generateImage>[0], 'model'> & { model: string | ImageModelV2 },
|
||||
TResult extends ReturnType<typeof experimental_generateImage>
|
||||
>(
|
||||
async executeImageWithPlugins<TParams, TResult>(
|
||||
methodName: string,
|
||||
model: ImageModelV2 | string,
|
||||
params: TParams,
|
||||
executor: (model: ImageModelV2, transformedParams: TParams) => TResult,
|
||||
executor: (model: ImageModelV2, transformedParams: TParams) => Promise<TResult>,
|
||||
_context?: ReturnType<typeof createContext>
|
||||
): Promise<TResult> {
|
||||
// 统一处理模型解析
|
||||
let resolvedModel: ImageModelV2 | undefined
|
||||
let modelId: string
|
||||
const { model } = params
|
||||
|
||||
if (typeof model === 'string') {
|
||||
// 字符串:需要通过插件解析
|
||||
modelId = model
|
||||
@@ -163,13 +159,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
}
|
||||
|
||||
// 使用正确的createContext创建请求上下文
|
||||
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||
const context = _context ? _context : createContext(this.providerId, modelId, params)
|
||||
|
||||
// 🔥 为上下文添加递归调用能力
|
||||
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||
// 递归调用自身,重新走完整的插件流程
|
||||
context.isRecursiveCall = true
|
||||
const result = await this.executeImageWithPlugins(methodName, newParams, executor, context)
|
||||
const result = await this.executeImageWithPlugins(methodName, model, newParams, executor, context)
|
||||
context.isRecursiveCall = false
|
||||
return result
|
||||
}
|
||||
@@ -218,19 +214,17 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
* 执行流式调用的通用逻辑(支持流转换器)
|
||||
* 提供给AiExecutor使用
|
||||
*/
|
||||
async executeStreamWithPlugins<
|
||||
TParams extends Parameters<typeof streamText | typeof streamObject>[0],
|
||||
TResult extends ReturnType<typeof streamText | typeof streamObject>
|
||||
>(
|
||||
async executeStreamWithPlugins<TParams, TResult>(
|
||||
methodName: string,
|
||||
model: LanguageModel,
|
||||
params: TParams,
|
||||
executor: (model: LanguageModel, transformedParams: TParams, streamTransforms: any[]) => TResult,
|
||||
executor: (model: LanguageModel, transformedParams: TParams, streamTransforms: any[]) => Promise<TResult>,
|
||||
_context?: ReturnType<typeof createContext>
|
||||
): Promise<TResult> {
|
||||
// 统一处理模型解析
|
||||
let resolvedModel: LanguageModel | undefined
|
||||
let modelId: string
|
||||
const { model } = params
|
||||
|
||||
if (typeof model === 'string') {
|
||||
// 字符串:需要通过插件解析
|
||||
modelId = model
|
||||
@@ -241,13 +235,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||
}
|
||||
|
||||
// 创建请求上下文
|
||||
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||
const context = _context ? _context : createContext(this.providerId, modelId, params)
|
||||
|
||||
// 🔥 为上下文添加递归调用能力
|
||||
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||
// 递归调用自身,重新走完整的插件流程
|
||||
context.isRecursiveCall = true
|
||||
const result = await this.executeStreamWithPlugins(methodName, newParams, executor, context)
|
||||
const result = await this.executeStreamWithPlugins(methodName, model, newParams, executor, context)
|
||||
context.isRecursiveCall = false
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/**
|
||||
* Runtime 层类型定义
|
||||
*/
|
||||
import { ImageModelV2 } from '@ai-sdk/provider'
|
||||
import { experimental_generateImage, generateObject, generateText, streamObject, streamText } from 'ai'
|
||||
|
||||
import { type ModelConfig } from '../models/types'
|
||||
import { type AiPlugin } from '../plugins'
|
||||
import { type ProviderId } from '../providers/types'
|
||||
@@ -16,11 +13,3 @@ export interface RuntimeConfig<T extends ProviderId = ProviderId> {
|
||||
providerSettings: ModelConfig<T>['providerSettings'] & { mode?: 'chat' | 'responses' }
|
||||
plugins?: AiPlugin[]
|
||||
}
|
||||
|
||||
export type generateImageParams = Omit<Parameters<typeof experimental_generateImage>[0], 'model'> & {
|
||||
model: string | ImageModelV2
|
||||
}
|
||||
export type generateObjectParams = Parameters<typeof generateObject>[0]
|
||||
export type generateTextParams = Parameters<typeof generateText>[0]
|
||||
export type streamObjectParams = Parameters<typeof streamObject>[0]
|
||||
export type streamTextParams = Parameters<typeof streamText>[0]
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmitOnError": false,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"resolveJsonModule": true,
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "ES2020"
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmitOnError": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -67,13 +67,13 @@
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@tiptap/core": "^3.2.0",
|
||||
"@tiptap/pm": "^3.2.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"prettier": "^3.5.3",
|
||||
"tsdown": "^0.13.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -87,7 +87,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"lint": "biome format ./src/ --write && eslint --fix ./src/"
|
||||
"lint": "prettier ./src/ --write && eslint --fix ./src/"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1"
|
||||
}
|
||||
|
||||
@@ -35,10 +35,8 @@ export enum IpcChannel {
|
||||
App_InstallBunBinary = 'app:install-bun-binary',
|
||||
App_LogToMain = 'app:log-to-main',
|
||||
App_SaveData = 'app:save-data',
|
||||
App_GetDiskInfo = 'app:get-disk-info',
|
||||
App_SetFullScreen = 'app:set-full-screen',
|
||||
App_IsFullScreen = 'app:is-full-screen',
|
||||
App_GetSystemFonts = 'app:get-system-fonts',
|
||||
|
||||
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
|
||||
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
|
||||
@@ -85,13 +83,10 @@ export enum IpcChannel {
|
||||
Mcp_UploadDxt = 'mcp:upload-dxt',
|
||||
Mcp_AbortTool = 'mcp:abort-tool',
|
||||
Mcp_GetServerVersion = 'mcp:get-server-version',
|
||||
Mcp_Progress = 'mcp:progress',
|
||||
|
||||
// Python
|
||||
Python_Execute = 'python:execute',
|
||||
|
||||
// agent messages
|
||||
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
||||
|
||||
//copilot
|
||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||
@@ -128,12 +123,6 @@ export enum IpcChannel {
|
||||
Windows_SetMinimumSize = 'window:set-minimum-size',
|
||||
Windows_Resize = 'window:resize',
|
||||
Windows_GetSize = 'window:get-size',
|
||||
Windows_Minimize = 'window:minimize',
|
||||
Windows_Maximize = 'window:maximize',
|
||||
Windows_Unmaximize = 'window:unmaximize',
|
||||
Windows_Close = 'window:close',
|
||||
Windows_IsMaximized = 'window:is-maximized',
|
||||
Windows_MaximizedChanged = 'window:maximized-changed',
|
||||
|
||||
KnowledgeBase_Create = 'knowledge-base:create',
|
||||
KnowledgeBase_Reset = 'knowledge-base:reset',
|
||||
@@ -261,6 +250,7 @@ export enum IpcChannel {
|
||||
|
||||
// Provider
|
||||
Provider_AddKey = 'provider:add-key',
|
||||
Provider_GetClaudeCodePort = 'provider:get-claude-code-port',
|
||||
|
||||
//Selection Assistant
|
||||
Selection_TextSelected = 'selection:text-selected',
|
||||
@@ -307,21 +297,6 @@ export enum IpcChannel {
|
||||
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
|
||||
|
||||
// API Server
|
||||
ApiServer_Start = 'api-server:start',
|
||||
ApiServer_Stop = 'api-server:stop',
|
||||
ApiServer_Restart = 'api-server:restart',
|
||||
ApiServer_GetStatus = 'api-server:get-status',
|
||||
ApiServer_GetConfig = 'api-server:get-config',
|
||||
|
||||
// Anthropic OAuth
|
||||
Anthropic_StartOAuthFlow = 'anthropic:start-oauth-flow',
|
||||
Anthropic_CompleteOAuthWithCode = 'anthropic:complete-oauth-with-code',
|
||||
Anthropic_CancelOAuthFlow = 'anthropic:cancel-oauth-flow',
|
||||
Anthropic_GetAccessToken = 'anthropic:get-access-token',
|
||||
Anthropic_HasCredentials = 'anthropic:has-credentials',
|
||||
Anthropic_ClearCredentials = 'anthropic:clear-credentials',
|
||||
|
||||
// CodeTools
|
||||
CodeTools_Run = 'code-tools:run',
|
||||
|
||||
|
||||
@@ -216,6 +216,5 @@ export enum codeTools {
|
||||
qwenCode = 'qwen-code',
|
||||
claudeCode = 'claude-code',
|
||||
geminiCli = 'gemini-cli',
|
||||
openaiCodex = 'openai-codex',
|
||||
iFlowCli = 'iflow-cli'
|
||||
openaiCodex = 'openai-codex'
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export type LoaderReturn = {
|
||||
loaderType: string
|
||||
status?: ProcessingStatus
|
||||
message?: string
|
||||
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
||||
messageSource?: 'preprocess' | 'embedding'
|
||||
}
|
||||
|
||||
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
||||
@@ -17,8 +17,3 @@ export type FileChangeEvent = {
|
||||
filePath: string
|
||||
watchPath: string
|
||||
}
|
||||
|
||||
export type MCPProgressEvent = {
|
||||
callId: string
|
||||
progress: number // 0-1 range
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export const defaultAppHeaders = () => {
|
||||
return {
|
||||
'HTTP-Referer': 'https://cherry-ai.com',
|
||||
'X-Title': 'Cherry Studio'
|
||||
}
|
||||
}
|
||||
@@ -1,274 +1,199 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>许可协议 | License Agreement</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50">
|
||||
<div class="mx-auto max-w-4xl px-4 py-8">
|
||||
<!-- 中文版本 -->
|
||||
<div class="mb-12">
|
||||
<h1 class="mb-8 text-3xl font-bold text-gray-900">许可协议</h1>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>许可协议 | License Agreement</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<p class="mb-6 text-gray-700">
|
||||
本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。
|
||||
<body class="bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<!-- 中文版本 -->
|
||||
<div class="mb-12">
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">核心原则</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用 <strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。</li>
|
||||
<li><strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取 <strong>商业许可证 (Commercial License)</strong>。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">定义:"10人及以下"</h2>
|
||||
<p class="text-gray-700">
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
||||
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">核心原则</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
<strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用
|
||||
<strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。
|
||||
</li>
|
||||
<li>
|
||||
<strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取
|
||||
<strong>商业许可证 (Commercial License)</strong>。
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
||||
</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a> 获取。
|
||||
</li>
|
||||
<li><strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
||||
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。</li>
|
||||
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">定义:"10人及以下"</h2>
|
||||
<p class="text-gray-700">
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
||||
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3
|
||||
义务的用户</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>强制要求:</strong>
|
||||
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
||||
Cherry Studio。</li>
|
||||
<li><strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong>无法满足 AGPLv3
|
||||
的条款要求</strong>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3 <strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
||||
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。</li>
|
||||
<li><strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>您的组织规模超过10人。</li>
|
||||
<li>(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3 公开您修改部分的源代码。</li>
|
||||
<li>(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据 AGPLv3 向服务使用者提供修改后的源代码。</li>
|
||||
<li>(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>获取商业许可:</strong> 请通过邮箱 <a href="mailto:bd@cherry-ai.com"
|
||||
class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry Studio 开发团队洽谈商业授权事宜。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||
1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
||||
</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在
|
||||
<strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html" class="text-blue-600 hover:underline"
|
||||
>https://www.gnu.org/licenses/agpl-3.0.html</a
|
||||
>
|
||||
获取。
|
||||
</li>
|
||||
<li>
|
||||
<strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio
|
||||
并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
||||
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。
|
||||
</li>
|
||||
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. 贡献 (Contributions)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 <strong>AGPLv3</strong> 许可证下提供。</li>
|
||||
<li>通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。</li>
|
||||
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||
2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3 义务的用户
|
||||
</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
<strong>强制要求:</strong>
|
||||
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
||||
Cherry Studio。
|
||||
</li>
|
||||
<li>
|
||||
<strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong
|
||||
>无法满足 AGPLv3 的条款要求</strong
|
||||
>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3
|
||||
<strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
||||
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。
|
||||
</li>
|
||||
<li>
|
||||
<strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-6">
|
||||
<li>您的组织规模超过10人。</li>
|
||||
<li>
|
||||
(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3
|
||||
公开您修改部分的源代码。
|
||||
</li>
|
||||
<li>
|
||||
(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据
|
||||
AGPLv3 向服务使用者提供修改后的源代码。
|
||||
</li>
|
||||
<li>
|
||||
(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>获取商业许可:</strong> 请通过邮箱
|
||||
<a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry
|
||||
Studio 开发团队洽谈商业授权事宜。
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">3. 贡献 (Contributions)</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在
|
||||
<strong>AGPLv3</strong> 许可证下提供。
|
||||
</li>
|
||||
<li>
|
||||
通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3
|
||||
许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
||||
</li>
|
||||
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">4. 其他条款 (Other Terms)</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
||||
<li>
|
||||
项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<hr class="my-12 border-gray-300" />
|
||||
|
||||
<!-- English Version -->
|
||||
<div>
|
||||
<h1 class="mb-8 text-3xl font-bold text-gray-900">Licensing</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">Core Principle</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
<strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
||||
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
||||
<strong>Commercial License</strong>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
||||
<p class="text-gray-700">
|
||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
||||
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly
|
||||
benefit from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is
|
||||
not limited to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||
1. Open Source License: AGPLv3 - For Individuals and Organizations of 10 or Fewer
|
||||
</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
||||
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
||||
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html" class="text-blue-600 hover:underline"
|
||||
>https://www.gnu.org/licenses/agpl-3.0.html</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
||||
make it available over a network, or distribute the modified version, you must provide the
|
||||
<strong>complete corresponding source code</strong> under the AGPLv3 license to the recipients. Even if
|
||||
you qualify under the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure
|
||||
obligation, you will need to obtain a Commercial License (see below).
|
||||
</li>
|
||||
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||
2. Commercial License - For Organizations with More Than 10 Individuals, or Users Needing to Avoid AGPLv3
|
||||
Obligations
|
||||
</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
<strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
||||
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
||||
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
||||
Studio.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
||||
condition, if your intended use case
|
||||
<strong>cannot comply with the terms of the AGPLv3</strong> (particularly the obligations regarding
|
||||
<strong>source code disclosure</strong>), or if you require specific commercial terms
|
||||
<strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom from copyleft
|
||||
restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial License.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-6">
|
||||
<li>
|
||||
Your organization has more than 10 individuals who can access, use, or benefit from the software.
|
||||
</li>
|
||||
<li>
|
||||
(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
||||
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
||||
</li>
|
||||
<li>
|
||||
(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
||||
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
||||
of the service under AGPLv3.
|
||||
</li>
|
||||
<li>
|
||||
(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
||||
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and
|
||||
confidentiality.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
||||
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
||||
discuss commercial licensing options.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">3. Contributions</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
||||
considered to be offered under the <strong>AGPLv3</strong> license.
|
||||
</li>
|
||||
<li>
|
||||
By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
||||
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
||||
operate under AGPLv3 or a Commercial License).
|
||||
</li>
|
||||
<li>
|
||||
You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
||||
offered under our commercial license.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">4. Other Terms</h2>
|
||||
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||
<li>
|
||||
The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
||||
agreement signed by both parties.
|
||||
</li>
|
||||
<li>
|
||||
The project maintainers reserve the right to update this licensing policy (including the definition and
|
||||
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
||||
code repository, official website).
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. 其他条款 (Other Terms)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
||||
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<hr class="my-12 border-gray-300">
|
||||
|
||||
<!-- English Version -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">Licensing</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Core Principle</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
||||
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.</li>
|
||||
<li><strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
||||
<strong>Commercial License</strong>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
||||
<p class="text-gray-700">
|
||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
||||
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit
|
||||
from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited
|
||||
to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. Open Source License: AGPLv3 - For Individuals and
|
||||
Organizations of 10 or Fewer</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
||||
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
||||
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at <a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a>.
|
||||
</li>
|
||||
<li><strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
||||
make it available over a network, or distribute the modified version, you must provide the <strong>complete
|
||||
corresponding source code</strong> under the AGPLv3 license to the recipients. Even if you qualify under
|
||||
the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will
|
||||
need to obtain a Commercial License (see below).</li>
|
||||
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. Commercial License - For Organizations with More Than 10
|
||||
Individuals, or Users Needing to Avoid AGPLv3 Obligations</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
||||
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
||||
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
||||
Studio.</li>
|
||||
<li><strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
||||
condition, if your intended use case <strong>cannot comply with the terms of the AGPLv3</strong>
|
||||
(particularly the obligations regarding <strong>source code disclosure</strong>), or if you require specific
|
||||
commercial terms <strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom
|
||||
from copyleft restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial
|
||||
License.</li>
|
||||
<li><strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>Your organization has more than 10 individuals who can access, use, or benefit from the software.</li>
|
||||
<li>(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
||||
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
||||
</li>
|
||||
<li>(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
||||
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
||||
of the service under AGPLv3.</li>
|
||||
<li>(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
||||
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
||||
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
||||
discuss commercial licensing options.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. Contributions</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
||||
considered to be offered under the <strong>AGPLv3</strong> license.</li>
|
||||
<li>By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
||||
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
||||
operate under AGPLv3 or a Commercial License).</li>
|
||||
<li>You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
||||
offered under our commercial license.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. Other Terms</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
||||
agreement signed by both parties.</li>
|
||||
<li>The project maintainers reserve the right to update this licensing policy (including the definition and
|
||||
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
||||
code repository, official website).</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -12,18 +12,18 @@
|
||||
|
||||
<body id="app">
|
||||
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||
<div class="mx-auto max-w-3xl px-4 py-12">
|
||||
<h1 class="mb-8 text-3xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||
|
||||
<!-- Loading状态 -->
|
||||
<div v-if="loading" class="py-8 text-center">
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<div
|
||||
class="inline-block h-8 w-8 animate-spin rounded-full border-4"
|
||||
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error 状态 -->
|
||||
<div v-else-if="error" class="py-8 text-center text-red-500">{{ error }}</div>
|
||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
||||
|
||||
<!-- Release 列表 -->
|
||||
<div v-else class="space-y-8">
|
||||
@@ -32,21 +32,21 @@
|
||||
:key="release.id"
|
||||
class="relative pl-8"
|
||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||
<div class="absolute top-0 -left-2 h-4 w-4 rounded-full bg-green-500"></div>
|
||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
||||
<div
|
||||
class="rounded-lg p-6 shadow-sm transition-shadow"
|
||||
class="rounded-lg shadow-sm p-6 transition-shadow"
|
||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ release.name || release.tag_name }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||
{{ formatDate(release.published_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||
{{ release.tag_name }}
|
||||
</span>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,53 +0,0 @@
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `migrations` (
|
||||
`version` integer PRIMARY KEY NOT NULL,
|
||||
`tag` text NOT NULL,
|
||||
`executed_at` integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `agents` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`accessible_paths` text,
|
||||
`instructions` text,
|
||||
`model` text NOT NULL,
|
||||
`plan_model` text,
|
||||
`small_model` text,
|
||||
`mcps` text,
|
||||
`allowed_tools` text,
|
||||
`configuration` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`agent_type` text NOT NULL,
|
||||
`agent_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`accessible_paths` text,
|
||||
`instructions` text,
|
||||
`model` text NOT NULL,
|
||||
`plan_model` text,
|
||||
`small_model` text,
|
||||
`mcps` text,
|
||||
`allowed_tools` text,
|
||||
`configuration` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `session_messages` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`session_id` text NOT NULL,
|
||||
`role` text NOT NULL,
|
||||
`content` text NOT NULL,
|
||||
`metadata` text,
|
||||
`created_at` text NOT NULL,
|
||||
`updated_at` text NOT NULL
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE `session_messages` ADD `agent_session_id` text DEFAULT '';
|
||||
@@ -1,331 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "35efb412-0230-4767-9c76-7b7c4d40369f",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"agents": {
|
||||
"name": "agents",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"accessible_paths": {
|
||||
"name": "accessible_paths",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"instructions": {
|
||||
"name": "instructions",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"plan_model": {
|
||||
"name": "plan_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"small_model": {
|
||||
"name": "small_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mcps": {
|
||||
"name": "mcps",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_tools": {
|
||||
"name": "allowed_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"configuration": {
|
||||
"name": "configuration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session_messages": {
|
||||
"name": "session_messages",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"migrations": {
|
||||
"name": "migrations",
|
||||
"columns": {
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tag": {
|
||||
"name": "tag",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"executed_at": {
|
||||
"name": "executed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_type": {
|
||||
"name": "agent_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"accessible_paths": {
|
||||
"name": "accessible_paths",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"instructions": {
|
||||
"name": "instructions",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"plan_model": {
|
||||
"name": "plan_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"small_model": {
|
||||
"name": "small_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mcps": {
|
||||
"name": "mcps",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_tools": {
|
||||
"name": "allowed_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"configuration": {
|
||||
"name": "configuration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "dabab6db-a2cd-4e96-b06e-6cb87d445a87",
|
||||
"prevId": "35efb412-0230-4767-9c76-7b7c4d40369f",
|
||||
"tables": {
|
||||
"agents": {
|
||||
"name": "agents",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"accessible_paths": {
|
||||
"name": "accessible_paths",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"instructions": {
|
||||
"name": "instructions",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"plan_model": {
|
||||
"name": "plan_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"small_model": {
|
||||
"name": "small_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mcps": {
|
||||
"name": "mcps",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_tools": {
|
||||
"name": "allowed_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"configuration": {
|
||||
"name": "configuration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session_messages": {
|
||||
"name": "session_messages",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_session_id": {
|
||||
"name": "agent_session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"migrations": {
|
||||
"name": "migrations",
|
||||
"columns": {
|
||||
"version": {
|
||||
"name": "version",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tag": {
|
||||
"name": "tag",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"executed_at": {
|
||||
"name": "executed_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_type": {
|
||||
"name": "agent_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"accessible_paths": {
|
||||
"name": "accessible_paths",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"instructions": {
|
||||
"name": "instructions",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"plan_model": {
|
||||
"name": "plan_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"small_model": {
|
||||
"name": "small_model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"mcps": {
|
||||
"name": "mcps",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"allowed_tools": {
|
||||
"name": "allowed_tools",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"configuration": {
|
||||
"name": "configuration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1758091173882,
|
||||
"tag": "0000_confused_wendigo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1758187378775,
|
||||
"tag": "0001_woozy_captain_flint",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import * as path from 'path'
|
||||
|
||||
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
|
||||
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
|
||||
const baseLocale = 'zh-cn'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseLocalePath = path.join(__dirname, '../src/renderer/src/i18n/locales', baseFileName)
|
||||
|
||||
type I18NValue = string | { [key: string]: I18NValue }
|
||||
type I18N = { [key: string]: I18NValue }
|
||||
@@ -106,9 +105,6 @@ const translateRecursively = async (originObj: I18N, systemPrompt: string): Prom
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
if (!fs.existsSync(baseLocalePath)) {
|
||||
throw new Error(`${baseLocalePath} not found.`)
|
||||
}
|
||||
const localeFiles = fs
|
||||
.readdirSync(localesDir)
|
||||
.filter((file) => file.endsWith('.json') && file !== baseFileName)
|
||||
|
||||
@@ -35,9 +35,6 @@ const allX64 = {
|
||||
'@napi-rs/system-ocr-win32-x64-msvc': '1.0.2'
|
||||
}
|
||||
|
||||
const claudeCodeVenderPath = '@anthropic-ai/claude-code/vendor'
|
||||
const claudeCodeVenders = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
|
||||
|
||||
const platformToArch = {
|
||||
mac: 'darwin',
|
||||
windows: 'win32',
|
||||
@@ -49,6 +46,9 @@ exports.default = async function (context) {
|
||||
const archType = arch === Arch.arm64 ? 'arm64' : 'x64'
|
||||
const platform = context.packager.platform.name
|
||||
|
||||
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
|
||||
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
|
||||
|
||||
const downloadPackages = async (packages) => {
|
||||
console.log('downloading packages ......')
|
||||
const downloadPromises = []
|
||||
@@ -67,39 +67,25 @@ exports.default = async function (context) {
|
||||
await Promise.all(downloadPromises)
|
||||
}
|
||||
|
||||
const changeFilters = async (filtersToExclude, filtersToInclude) => {
|
||||
const changeFilters = async (packages, filtersToExclude, filtersToInclude) => {
|
||||
await downloadPackages(packages)
|
||||
// remove filters for the target architecture (allow inclusion)
|
||||
|
||||
let filters = context.packager.config.files[0].filter
|
||||
filters = filters.filter((filter) => !filtersToInclude.includes(filter))
|
||||
|
||||
// add filters for other architectures (exclude them)
|
||||
filters.push(...filtersToExclude)
|
||||
|
||||
context.packager.config.files[0].filter = filters
|
||||
}
|
||||
|
||||
await downloadPackages(arch === Arch.arm64 ? allArm64 : allX64)
|
||||
|
||||
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
|
||||
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
|
||||
const excludeClaudeCodeRipgrepFilters = claudeCodeVenders
|
||||
.filter((f) => f !== `${archType}-${platformToArch[platform]}`)
|
||||
.map((f) => '!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + f + '/**')
|
||||
const excludeClaudeCodeJBPlutins = ['!node_modules/' + claudeCodeVenderPath + '/' + 'claude-code-jetbrains-plugin']
|
||||
|
||||
const includeClaudeCodeFilters = [
|
||||
'!node_modules/' + claudeCodeVenderPath + '/' + `${archType}-${platformToArch[platform]}/**`
|
||||
]
|
||||
|
||||
if (arch === Arch.arm64) {
|
||||
await changeFilters(
|
||||
[...x64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
|
||||
[...arm64Filters, ...includeClaudeCodeFilters]
|
||||
)
|
||||
} else {
|
||||
await changeFilters(
|
||||
[...arm64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
|
||||
[...x64Filters, ...includeClaudeCodeFilters]
|
||||
)
|
||||
await changeFilters(allArm64, x64Filters, arm64Filters)
|
||||
return
|
||||
}
|
||||
|
||||
if (arch === Arch.x64) {
|
||||
await changeFilters(allX64, arm64Filters, x64Filters)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from 'path'
|
||||
import { sortedObjectByKeys } from './sort'
|
||||
|
||||
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
|
||||
const baseLocale = 'zh-cn'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseFilePath = path.join(translationsDir, baseFileName)
|
||||
|
||||
|
||||
@@ -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.BASE_LOCALE ?? 'zh-cn'
|
||||
const baseLocale = 'zh-cn'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseFilePath = path.join(localesDir, baseFileName)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exec } from 'child_process'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as linguistLanguages from 'linguist-languages'
|
||||
import linguistLanguages from 'linguist-languages'
|
||||
import * as path from 'path'
|
||||
import { promisify } from 'util'
|
||||
|
||||
@@ -75,17 +75,17 @@ export const languages: Record<string, LanguageData> = ${languagesObjectString};
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a file using Biome.
|
||||
* Formats a file using Prettier.
|
||||
* @param filePath The path to the file to format.
|
||||
*/
|
||||
async function format(filePath: string): Promise<void> {
|
||||
console.log('🎨 Formatting file with Biome...')
|
||||
async function formatWithPrettier(filePath: string): Promise<void> {
|
||||
console.log('🎨 Formatting file with Prettier...')
|
||||
try {
|
||||
await execAsync(`yarn biome format --write ${filePath}`)
|
||||
console.log('✅ Biome formatting complete.')
|
||||
await execAsync(`yarn prettier --write ${filePath}`)
|
||||
console.log('✅ Prettier formatting complete.')
|
||||
} catch (e: any) {
|
||||
console.error('❌ Biome formatting failed:', e.stdout || e.stderr)
|
||||
throw new Error('Biome formatting failed.')
|
||||
console.error('❌ Prettier formatting failed:', e.stdout || e.stderr)
|
||||
throw new Error('Prettier formatting failed.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ async function updateLanguagesFile(): Promise<void> {
|
||||
await fs.writeFile(LANGUAGES_FILE_PATH, fileContent, 'utf-8')
|
||||
console.log(`✅ Successfully wrote to ${LANGUAGES_FILE_PATH}`)
|
||||
|
||||
await format(LANGUAGES_FILE_PATH)
|
||||
await formatWithPrettier(LANGUAGES_FILE_PATH)
|
||||
await checkTypeScript(LANGUAGES_FILE_PATH)
|
||||
|
||||
console.log('🎉 Successfully updated languages.ts file!')
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { loggerService } from '@main/services/LoggerService'
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { authMiddleware } from './middleware/auth'
|
||||
import { errorHandler } from './middleware/error'
|
||||
import { setupOpenAPIDocumentation } from './middleware/openapi'
|
||||
import { agentsRoutes } from './routes/agents'
|
||||
import { chatRoutes } from './routes/chat'
|
||||
import { mcpRoutes } from './routes/mcp'
|
||||
import { messagesRoutes } from './routes/messages'
|
||||
import { modelsRoutes } from './routes/models'
|
||||
|
||||
const logger = loggerService.withContext('ApiServer')
|
||||
|
||||
const app = express()
|
||||
|
||||
// Global middleware
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now()
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start
|
||||
logger.info(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`)
|
||||
})
|
||||
next()
|
||||
})
|
||||
|
||||
app.use((_req, res, next) => {
|
||||
res.setHeader('X-Request-ID', uuidv4())
|
||||
next()
|
||||
})
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: '*',
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /health:
|
||||
* get:
|
||||
* summary: Health check endpoint
|
||||
* description: Check server status (no authentication required)
|
||||
* tags: [Health]
|
||||
* security: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Server is healthy
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* example: ok
|
||||
* timestamp:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* version:
|
||||
* type: string
|
||||
* example: 1.0.0
|
||||
*/
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || '1.0.0'
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /:
|
||||
* get:
|
||||
* summary: API information
|
||||
* description: Get basic API information and available endpoints
|
||||
* tags: [General]
|
||||
* security: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: API information
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* example: Cherry Studio API
|
||||
* version:
|
||||
* type: string
|
||||
* example: 1.0.0
|
||||
* endpoints:
|
||||
* type: object
|
||||
*/
|
||||
app.get('/', (_req, res) => {
|
||||
res.json({
|
||||
name: 'Cherry Studio API',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
health: 'GET /health'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// API v1 routes with auth
|
||||
const apiRouter = express.Router()
|
||||
apiRouter.use(authMiddleware)
|
||||
apiRouter.use(express.json())
|
||||
// Mount routes
|
||||
apiRouter.use('/chat', chatRoutes)
|
||||
apiRouter.use('/mcps', mcpRoutes)
|
||||
apiRouter.use('/messages', messagesRoutes)
|
||||
apiRouter.use('/models', modelsRoutes)
|
||||
apiRouter.use('/agents', agentsRoutes)
|
||||
app.use('/v1', apiRouter)
|
||||
|
||||
// Setup OpenAPI documentation
|
||||
setupOpenAPIDocumentation(app)
|
||||
|
||||
// Error handling (must be last)
|
||||
app.use(errorHandler)
|
||||
|
||||
export { app }
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ApiServerConfig } from '@types'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { loggerService } from '../services/LoggerService'
|
||||
import { reduxService } from '../services/ReduxService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerConfig')
|
||||
|
||||
const defaultHost = 'localhost'
|
||||
const defaultPort = 23333
|
||||
|
||||
class ConfigManager {
|
||||
private _config: ApiServerConfig | null = null
|
||||
|
||||
private generateApiKey(): string {
|
||||
return `cs-sk-${uuidv4()}`
|
||||
}
|
||||
|
||||
async load(): Promise<ApiServerConfig> {
|
||||
try {
|
||||
const settings = await reduxService.select('state.settings')
|
||||
const serverSettings = settings?.apiServer
|
||||
let apiKey = serverSettings?.apiKey
|
||||
if (!apiKey || apiKey.trim() === '') {
|
||||
apiKey = this.generateApiKey()
|
||||
await reduxService.dispatch({
|
||||
type: 'settings/setApiServerApiKey',
|
||||
payload: apiKey
|
||||
})
|
||||
}
|
||||
this._config = {
|
||||
enabled: serverSettings?.enabled ?? false,
|
||||
port: serverSettings?.port ?? defaultPort,
|
||||
host: defaultHost,
|
||||
apiKey: apiKey
|
||||
}
|
||||
return this._config
|
||||
} catch (error: any) {
|
||||
logger.warn('Failed to load config from Redux, using defaults:', error)
|
||||
this._config = {
|
||||
enabled: false,
|
||||
port: defaultPort,
|
||||
host: defaultHost,
|
||||
apiKey: this.generateApiKey()
|
||||
}
|
||||
return this._config
|
||||
}
|
||||
}
|
||||
|
||||
async get(): Promise<ApiServerConfig> {
|
||||
if (!this._config) {
|
||||
await this.load()
|
||||
}
|
||||
if (!this._config) {
|
||||
throw new Error('Failed to load API server configuration')
|
||||
}
|
||||
return this._config
|
||||
}
|
||||
|
||||
async reload(): Promise<ApiServerConfig> {
|
||||
return await this.load()
|
||||
}
|
||||
}
|
||||
|
||||
export const config = new ConfigManager()
|
||||
@@ -1,2 +0,0 @@
|
||||
export { config } from './config'
|
||||
export { apiServer } from './server'
|
||||
@@ -1,62 +0,0 @@
|
||||
import crypto from 'crypto'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
import { config } from '../config'
|
||||
|
||||
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const auth = req.header('Authorization') || ''
|
||||
const xApiKey = req.header('x-api-key') || ''
|
||||
|
||||
// Fast rejection if neither credential header provided
|
||||
if (!auth && !xApiKey) {
|
||||
return res.status(401).json({ error: 'Unauthorized: missing credentials' })
|
||||
}
|
||||
|
||||
let token: string | undefined
|
||||
|
||||
// Prefer Bearer if well‑formed
|
||||
if (auth) {
|
||||
const trimmed = auth.trim()
|
||||
const bearerPrefix = /^Bearer\s+/i
|
||||
if (bearerPrefix.test(trimmed)) {
|
||||
const candidate = trimmed.replace(bearerPrefix, '').trim()
|
||||
if (!candidate) {
|
||||
return res.status(401).json({ error: 'Unauthorized: empty bearer token' })
|
||||
}
|
||||
token = candidate
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to x-api-key if token still not resolved
|
||||
if (!token && xApiKey) {
|
||||
if (!xApiKey.trim()) {
|
||||
return res.status(401).json({ error: 'Unauthorized: empty x-api-key' })
|
||||
}
|
||||
token = xApiKey.trim()
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
// At this point we had at least one header, but none yielded a usable token
|
||||
return res.status(401).json({ error: 'Unauthorized: invalid credentials format' })
|
||||
}
|
||||
|
||||
const { apiKey } = await config.get()
|
||||
|
||||
if (!apiKey) {
|
||||
// If server not configured, treat as forbidden (or could be 500). Choose 403 to avoid leaking config state.
|
||||
return res.status(403).json({ error: 'Forbidden' })
|
||||
}
|
||||
|
||||
// Timing-safe compare when lengths match, else immediate forbidden
|
||||
if (token.length !== apiKey.length) {
|
||||
return res.status(403).json({ error: 'Forbidden' })
|
||||
}
|
||||
|
||||
const tokenBuf = Buffer.from(token)
|
||||
const keyBuf = Buffer.from(apiKey)
|
||||
if (!crypto.timingSafeEqual(tokenBuf, keyBuf)) {
|
||||
return res.status(403).json({ error: 'Forbidden' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerErrorHandler')
|
||||
|
||||
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
logger.error('API Server Error:', err)
|
||||
|
||||
// Don't expose internal errors in production
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: isDev ? err.message : 'Internal server error',
|
||||
type: 'server_error',
|
||||
...(isDev && { stack: err.stack })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
import { Express } from 'express'
|
||||
import swaggerJSDoc from 'swagger-jsdoc'
|
||||
import swaggerUi from 'swagger-ui-express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('OpenAPIMiddleware')
|
||||
|
||||
const swaggerOptions: swaggerJSDoc.Options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'Cherry Studio API',
|
||||
version: '1.0.0',
|
||||
description: 'OpenAI-compatible API for Cherry Studio with additional Cherry-specific endpoints',
|
||||
contact: {
|
||||
name: 'Cherry Studio',
|
||||
url: 'https://github.com/CherryHQ/cherry-studio'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:23333',
|
||||
description: 'Local development server'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
BearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'Use the API key from Cherry Studio settings'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
code: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ChatMessage: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['system', 'user', 'assistant', 'tool']
|
||||
},
|
||||
content: {
|
||||
oneOf: [
|
||||
{ type: 'string' },
|
||||
{
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string' },
|
||||
text: { type: 'string' },
|
||||
image_url: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
name: { type: 'string' },
|
||||
tool_calls: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
function: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
arguments: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ChatCompletionRequest: {
|
||||
type: 'object',
|
||||
required: ['model', 'messages'],
|
||||
properties: {
|
||||
model: {
|
||||
type: 'string',
|
||||
description: 'The model to use for completion, in format provider:model-id'
|
||||
},
|
||||
messages: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/ChatMessage' }
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
maximum: 2,
|
||||
default: 1
|
||||
},
|
||||
max_tokens: {
|
||||
type: 'integer',
|
||||
minimum: 1
|
||||
},
|
||||
stream: {
|
||||
type: 'boolean',
|
||||
default: false
|
||||
},
|
||||
tools: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string' },
|
||||
function: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
parameters: { type: 'object' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
object: { type: 'string', enum: ['model'] },
|
||||
created: { type: 'integer' },
|
||||
owned_by: { type: 'string' }
|
||||
}
|
||||
},
|
||||
MCPServer: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
command: { type: 'string' },
|
||||
args: {
|
||||
type: 'array',
|
||||
items: { type: 'string' }
|
||||
},
|
||||
env: { type: 'object' },
|
||||
disabled: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
BearerAuth: []
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
|
||||
}
|
||||
|
||||
export function setupOpenAPIDocumentation(app: Express) {
|
||||
try {
|
||||
const specs = swaggerJSDoc(swaggerOptions)
|
||||
|
||||
// Serve OpenAPI JSON
|
||||
app.get('/api-docs.json', (_req, res) => {
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(specs)
|
||||
})
|
||||
|
||||
// Serve Swagger UI
|
||||
app.use(
|
||||
'/api-docs',
|
||||
swaggerUi.serve,
|
||||
swaggerUi.setup(specs, {
|
||||
customCss: `
|
||||
.swagger-ui .topbar { display: none; }
|
||||
.swagger-ui .info .title { color: #1890ff; }
|
||||
`,
|
||||
customSiteTitle: 'Cherry Studio API Documentation'
|
||||
})
|
||||
)
|
||||
|
||||
logger.info('OpenAPI documentation setup complete')
|
||||
logger.info('Documentation available at /api-docs')
|
||||
logger.info('OpenAPI spec available at /api-docs.json')
|
||||
} catch (error) {
|
||||
logger.error('Failed to setup OpenAPI documentation:', error as Error)
|
||||
}
|
||||
}
|
||||
@@ -1,532 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { AgentModelValidationError, agentService } from '@main/services/agents'
|
||||
import { ListAgentsResponse,type ReplaceAgentRequest, type UpdateAgentRequest } from '@types'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerAgentsHandlers')
|
||||
|
||||
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
|
||||
error: {
|
||||
message: `Invalid ${error.context.field}: ${error.detail.message}`,
|
||||
type: 'invalid_request_error',
|
||||
code: error.detail.code
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents:
|
||||
* post:
|
||||
* summary: Create a new agent
|
||||
* description: Creates a new autonomous agent with the specified configuration
|
||||
* tags: [Agents]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateAgentRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Agent created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Validation error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const createAgent = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
logger.info('Creating new agent')
|
||||
logger.debug('Agent data:', req.body)
|
||||
|
||||
const agent = await agentService.createAgent(req.body)
|
||||
|
||||
logger.info(`Agent created successfully: ${agent.id}`)
|
||||
return res.status(201).json(agent)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Agent model validation error during create:', {
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error creating agent:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Failed to create agent: ${error.message}`,
|
||||
type: 'internal_error',
|
||||
code: 'agent_creation_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents:
|
||||
* get:
|
||||
* summary: List all agents
|
||||
* description: Retrieves a paginated list of all agents
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 20
|
||||
* description: Number of agents to return
|
||||
* - in: query
|
||||
* name: offset
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* default: 0
|
||||
* description: Number of agents to skip
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of agents
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* total:
|
||||
* type: integer
|
||||
* description: Total number of agents
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: Number of agents returned
|
||||
* offset:
|
||||
* type: integer
|
||||
* description: Number of agents skipped
|
||||
* 400:
|
||||
* description: Validation error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const listAgents = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
|
||||
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
|
||||
|
||||
logger.info(`Listing agents with limit=${limit}, offset=${offset}`)
|
||||
|
||||
const result = await agentService.listAgents({ limit, offset })
|
||||
|
||||
logger.info(`Retrieved ${result.agents.length} agents (total: ${result.total})`)
|
||||
return res.json({
|
||||
data: result.agents,
|
||||
total: result.total,
|
||||
limit,
|
||||
offset
|
||||
} satisfies ListAgentsResponse)
|
||||
} catch (error: any) {
|
||||
logger.error('Error listing agents:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to list agents',
|
||||
type: 'internal_error',
|
||||
code: 'agent_list_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents/{agentId}:
|
||||
* get:
|
||||
* summary: Get agent by ID
|
||||
* description: Retrieves a specific agent by its ID
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const getAgent = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId } = req.params
|
||||
logger.info(`Getting agent: ${agentId}`)
|
||||
|
||||
const agent = await agentService.getAgent(agentId)
|
||||
|
||||
if (!agent) {
|
||||
logger.warn(`Agent not found: ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Agent not found',
|
||||
type: 'not_found',
|
||||
code: 'agent_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Agent retrieved successfully: ${agentId}`)
|
||||
return res.json(agent)
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting agent:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to get agent',
|
||||
type: 'internal_error',
|
||||
code: 'agent_get_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents/{agentId}:
|
||||
* put:
|
||||
* summary: Update agent
|
||||
* description: Updates an existing agent with the provided data
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateAgentRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Validation error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const updateAgent = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { agentId } = req.params
|
||||
try {
|
||||
logger.info(`Updating agent: ${agentId}`)
|
||||
logger.debug('Update data:', req.body)
|
||||
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const replacePayload = (validatedBody ?? {}) as ReplaceAgentRequest
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, replacePayload, { replace: true })
|
||||
|
||||
if (!agent) {
|
||||
logger.warn(`Agent not found for update: ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Agent not found',
|
||||
type: 'not_found',
|
||||
code: 'agent_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Agent updated successfully: ${agentId}`)
|
||||
return res.json(agent)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Agent model validation error during update:', {
|
||||
agentId,
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error updating agent:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to update agent: ' + error.message,
|
||||
type: 'internal_error',
|
||||
code: 'agent_update_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents/{agentId}:
|
||||
* patch:
|
||||
* summary: Partially update agent
|
||||
* description: Partially updates an existing agent with only the provided fields
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: Agent name
|
||||
* description:
|
||||
* type: string
|
||||
* description: Agent description
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: Agent avatar URL
|
||||
* instructions:
|
||||
* type: string
|
||||
* description: System prompt/instructions
|
||||
* model:
|
||||
* type: string
|
||||
* description: Main model ID
|
||||
* plan_model:
|
||||
* type: string
|
||||
* description: Optional planning model ID
|
||||
* small_model:
|
||||
* type: string
|
||||
* description: Optional small/fast model ID
|
||||
* built_in_tools:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Built-in tool IDs
|
||||
* mcps:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: MCP tool IDs
|
||||
* knowledges:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Knowledge base IDs
|
||||
* configuration:
|
||||
* type: object
|
||||
* description: Extensible settings
|
||||
* accessible_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Accessible directory paths
|
||||
* permission_mode:
|
||||
* type: string
|
||||
* enum: [readOnly, acceptEdits, bypassPermissions]
|
||||
* description: Permission mode
|
||||
* max_steps:
|
||||
* type: integer
|
||||
* description: Maximum steps the agent can take
|
||||
* description: Only include the fields you want to update
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Validation error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const patchAgent = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { agentId } = req.params
|
||||
try {
|
||||
logger.info(`Partially updating agent: ${agentId}`)
|
||||
logger.debug('Partial update data:', req.body)
|
||||
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const updatePayload = (validatedBody ?? {}) as UpdateAgentRequest
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, updatePayload)
|
||||
|
||||
if (!agent) {
|
||||
logger.warn(`Agent not found for partial update: ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Agent not found',
|
||||
type: 'not_found',
|
||||
code: 'agent_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Agent partially updated successfully: ${agentId}`)
|
||||
return res.json(agent)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Agent model validation error during partial update:', {
|
||||
agentId,
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error partially updating agent:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Failed to partially update agent: ${error.message}`,
|
||||
type: 'internal_error',
|
||||
code: 'agent_patch_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/agents/{agentId}:
|
||||
* delete:
|
||||
* summary: Delete agent
|
||||
* description: Deletes an agent and all associated sessions and logs
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Agent deleted successfully
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
export const deleteAgent = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId } = req.params
|
||||
logger.info(`Deleting agent: ${agentId}`)
|
||||
|
||||
const deleted = await agentService.deleteAgent(agentId)
|
||||
|
||||
if (!deleted) {
|
||||
logger.warn(`Agent not found for deletion: ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Agent not found',
|
||||
type: 'not_found',
|
||||
code: 'agent_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Agent deleted successfully: ${agentId}`)
|
||||
return res.status(204).send()
|
||||
} catch (error: any) {
|
||||
logger.error('Error deleting agent:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to delete agent',
|
||||
type: 'internal_error',
|
||||
code: 'agent_delete_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * as agentHandlers from './agents'
|
||||
export * as messageHandlers from './messages'
|
||||
export * as sessionHandlers from './sessions'
|
||||
@@ -1,230 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { agentService, sessionMessageService, sessionService } from '../../../../services/agents'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMessagesHandlers')
|
||||
|
||||
// Helper function to verify agent and session exist and belong together
|
||||
const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
|
||||
const agentExists = await agentService.agentExists(agentId)
|
||||
if (!agentExists) {
|
||||
throw { status: 404, code: 'agent_not_found', message: 'Agent not found' }
|
||||
}
|
||||
|
||||
const session = await sessionService.getSession(agentId, sessionId)
|
||||
if (!session) {
|
||||
throw { status: 404, code: 'session_not_found', message: 'Session not found' }
|
||||
}
|
||||
|
||||
if (session.agent_id !== agentId) {
|
||||
throw { status: 404, code: 'session_not_found', message: 'Session not found for this agent' }
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
export const createMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { agentId, sessionId } = req.params
|
||||
|
||||
const session = await verifyAgentAndSession(agentId, sessionId)
|
||||
|
||||
const messageData = req.body
|
||||
|
||||
logger.info(`Creating streaming message for session: ${sessionId}`)
|
||||
logger.debug('Streaming message data:', messageData)
|
||||
|
||||
// Set SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control')
|
||||
|
||||
const abortController = new AbortController()
|
||||
const { stream, completion } = await sessionMessageService.createSessionMessage(
|
||||
session,
|
||||
messageData,
|
||||
abortController
|
||||
)
|
||||
const reader = stream.getReader()
|
||||
|
||||
// Track stream lifecycle so we keep the SSE connection open until persistence finishes
|
||||
let responseEnded = false
|
||||
let streamFinished = false
|
||||
|
||||
const finalizeResponse = () => {
|
||||
if (responseEnded) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!streamFinished) {
|
||||
return
|
||||
}
|
||||
|
||||
responseEnded = true
|
||||
try {
|
||||
// res.write('data: {"type":"finish"}\n\n')
|
||||
res.write('data: [DONE]\n\n')
|
||||
} catch (writeError) {
|
||||
logger.error('Error writing final sentinel to SSE stream:', { error: writeError as Error })
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
|
||||
/**
|
||||
* Client Disconnect Detection for Server-Sent Events (SSE)
|
||||
*
|
||||
* We monitor multiple HTTP events to reliably detect when a client disconnects
|
||||
* from the streaming response. This is crucial for:
|
||||
* - Aborting long-running Claude Code processes
|
||||
* - Cleaning up resources and preventing memory leaks
|
||||
* - Avoiding orphaned processes
|
||||
*
|
||||
* Event Priority & Behavior:
|
||||
* 1. res.on('close') - Most common for SSE client disconnects (browser tab close, curl Ctrl+C)
|
||||
* 2. req.on('aborted') - Explicit request abortion
|
||||
* 3. req.on('close') - Request object closure (less common with SSE)
|
||||
*
|
||||
* When any disconnect event fires, we:
|
||||
* - Abort the Claude Code SDK process via abortController
|
||||
* - Clean up event listeners to prevent memory leaks
|
||||
* - Mark the response as ended to prevent further writes
|
||||
*/
|
||||
const handleDisconnect = () => {
|
||||
if (responseEnded) return
|
||||
logger.info(`Client disconnected from streaming message for session: ${sessionId}`)
|
||||
responseEnded = true
|
||||
abortController.abort('Client disconnected')
|
||||
reader.cancel('Client disconnected').catch(() => {})
|
||||
}
|
||||
|
||||
req.on('close', handleDisconnect)
|
||||
req.on('aborted', handleDisconnect)
|
||||
res.on('close', handleDisconnect)
|
||||
|
||||
const pumpStream = async () => {
|
||||
try {
|
||||
while (!responseEnded) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify(value)}\n\n`)
|
||||
}
|
||||
|
||||
streamFinished = true
|
||||
finalizeResponse()
|
||||
} catch (error) {
|
||||
if (responseEnded) return
|
||||
logger.error('Error reading agent stream:', { error })
|
||||
try {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: (error as Error).message || 'Stream processing error',
|
||||
type: 'stream_error',
|
||||
code: 'stream_processing_failed'
|
||||
}
|
||||
})}\n\n`
|
||||
)
|
||||
} catch (writeError) {
|
||||
logger.error('Error writing stream error to SSE:', { error: writeError })
|
||||
}
|
||||
responseEnded = true
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
pumpStream().catch((error) => {
|
||||
logger.error('Pump stream failure:', { error })
|
||||
})
|
||||
|
||||
completion
|
||||
.then(() => {
|
||||
streamFinished = true
|
||||
finalizeResponse()
|
||||
})
|
||||
.catch((error) => {
|
||||
if (responseEnded) return
|
||||
logger.error(`Streaming message error for session: ${sessionId}:`, error)
|
||||
try {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: (error as { message?: string })?.message || 'Stream processing error',
|
||||
type: 'stream_error',
|
||||
code: 'stream_processing_failed'
|
||||
}
|
||||
})}\n\n`
|
||||
)
|
||||
} catch (writeError) {
|
||||
logger.error('Error writing completion error to SSE stream:', { error: writeError })
|
||||
}
|
||||
responseEnded = true
|
||||
res.end()
|
||||
})
|
||||
|
||||
// Set a timeout to prevent hanging indefinitely
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
if (!responseEnded) {
|
||||
logger.error(`Streaming message timeout for session: ${sessionId}`)
|
||||
try {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: 'Stream timeout',
|
||||
type: 'timeout_error',
|
||||
code: 'stream_timeout'
|
||||
}
|
||||
})}\n\n`
|
||||
)
|
||||
} catch (writeError) {
|
||||
logger.error('Error writing timeout to SSE stream:', { error: writeError })
|
||||
}
|
||||
abortController.abort('stream timeout')
|
||||
reader.cancel('stream timeout').catch(() => {})
|
||||
responseEnded = true
|
||||
res.end()
|
||||
}
|
||||
},
|
||||
10 * 60 * 1000
|
||||
) // 10 minutes timeout
|
||||
|
||||
// Clear timeout when response ends
|
||||
res.on('close', () => clearTimeout(timeout))
|
||||
res.on('finish', () => clearTimeout(timeout))
|
||||
} catch (error: any) {
|
||||
logger.error('Error in streaming message handler:', error)
|
||||
|
||||
// Send error as SSE if possible
|
||||
if (!res.headersSent) {
|
||||
res.setHeader('Content-Type', 'text/event-stream')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
}
|
||||
|
||||
try {
|
||||
const errorResponse = {
|
||||
type: 'error',
|
||||
error: {
|
||||
message: error.status ? error.message : 'Failed to create streaming message',
|
||||
type: error.status ? 'not_found' : 'internal_error',
|
||||
code: error.status ? error.code : 'stream_creation_failed'
|
||||
}
|
||||
}
|
||||
|
||||
res.write(`data: ${JSON.stringify(errorResponse)}\n\n`)
|
||||
} catch (writeError) {
|
||||
logger.error('Error writing initial error to SSE stream:', { error: writeError })
|
||||
}
|
||||
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
import { loggerService } from '@logger'
|
||||
import {
|
||||
AgentModelValidationError,
|
||||
sessionMessageService,
|
||||
sessionService
|
||||
} from '@main/services/agents'
|
||||
import {
|
||||
CreateSessionResponse,
|
||||
ListAgentSessionsResponse,
|
||||
type ReplaceSessionRequest,
|
||||
UpdateSessionResponse
|
||||
} from '@types'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerSessionsHandlers')
|
||||
|
||||
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
|
||||
error: {
|
||||
message: `Invalid ${error.context.field}: ${error.detail.message}`,
|
||||
type: 'invalid_request_error',
|
||||
code: error.detail.code
|
||||
}
|
||||
})
|
||||
|
||||
export const createSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { agentId } = req.params
|
||||
try {
|
||||
const sessionData = req.body
|
||||
|
||||
logger.info(`Creating new session for agent: ${agentId}`)
|
||||
logger.debug('Session data:', sessionData)
|
||||
|
||||
const session = (await sessionService.createSession(agentId, sessionData)) satisfies CreateSessionResponse
|
||||
|
||||
logger.info(`Session created successfully: ${session.id}`)
|
||||
return res.status(201).json(session)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Session model validation error during create:', {
|
||||
agentId,
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error creating session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Failed to create session: ${error.message}`,
|
||||
type: 'internal_error',
|
||||
code: 'session_creation_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const listSessions = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId } = req.params
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
|
||||
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
|
||||
const status = req.query.status as any
|
||||
|
||||
logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${status}`)
|
||||
|
||||
const result = await sessionService.listSessions(agentId, { limit, offset })
|
||||
|
||||
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total}) for agent: ${agentId}`)
|
||||
return res.json({
|
||||
data: result.sessions,
|
||||
total: result.total,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error listing sessions:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to list sessions',
|
||||
type: 'internal_error',
|
||||
code: 'session_list_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId, sessionId } = req.params
|
||||
logger.info(`Getting session: ${sessionId} for agent: ${agentId}`)
|
||||
|
||||
const session = await sessionService.getSession(agentId, sessionId)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found: ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// // Verify session belongs to the agent
|
||||
// logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`)
|
||||
// return res.status(404).json({
|
||||
// error: {
|
||||
// message: 'Session not found for this agent',
|
||||
// type: 'not_found',
|
||||
// code: 'session_not_found'
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// Fetch session messages
|
||||
logger.info(`Fetching messages for session: ${sessionId}`)
|
||||
const { messages } = await sessionMessageService.listSessionMessages(sessionId)
|
||||
|
||||
// Add messages to session
|
||||
const sessionWithMessages = {
|
||||
...session,
|
||||
messages: messages
|
||||
}
|
||||
|
||||
logger.info(`Session retrieved successfully: ${sessionId} with ${messages.length} messages`)
|
||||
return res.json(sessionWithMessages)
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to get session',
|
||||
type: 'internal_error',
|
||||
code: 'session_get_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const updateSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { agentId, sessionId } = req.params
|
||||
try {
|
||||
logger.info(`Updating session: ${sessionId} for agent: ${agentId}`)
|
||||
logger.debug('Update data:', req.body)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found for this agent',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const replacePayload = (validatedBody ?? {}) as ReplaceSessionRequest
|
||||
|
||||
const session = await sessionService.updateSession(agentId, sessionId, replacePayload)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found for update: ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Session updated successfully: ${sessionId}`)
|
||||
return res.json(session satisfies UpdateSessionResponse)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Session model validation error during update:', {
|
||||
agentId,
|
||||
sessionId,
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error updating session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Failed to update session: ${error.message}`,
|
||||
type: 'internal_error',
|
||||
code: 'session_update_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const patchSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
const { agentId, sessionId } = req.params
|
||||
try {
|
||||
logger.info(`Patching session: ${sessionId} for agent: ${agentId}`)
|
||||
logger.debug('Patch data:', req.body)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found for this agent',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateSession = { ...existingSession, ...req.body }
|
||||
const session = await sessionService.updateSession(agentId, sessionId, updateSession)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found for patch: ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Session patched successfully: ${sessionId}`)
|
||||
return res.json(session)
|
||||
} catch (error: any) {
|
||||
if (error instanceof AgentModelValidationError) {
|
||||
logger.warn('Session model validation error during patch:', {
|
||||
agentId,
|
||||
sessionId,
|
||||
agentType: error.context.agentType,
|
||||
field: error.context.field,
|
||||
model: error.context.model,
|
||||
detail: error.detail
|
||||
})
|
||||
return res.status(400).json(modelValidationErrorBody(error))
|
||||
}
|
||||
|
||||
logger.error('Error patching session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: `Failed to patch session, ${error.message}`,
|
||||
type: 'internal_error',
|
||||
code: 'session_patch_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { agentId, sessionId } = req.params
|
||||
logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found for this agent',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleted = await sessionService.deleteSession(agentId, sessionId)
|
||||
|
||||
if (!deleted) {
|
||||
logger.warn(`Session not found for deletion: ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Session deleted successfully: ${sessionId}`)
|
||||
return res.status(204).send()
|
||||
} catch (error: any) {
|
||||
logger.error('Error deleting session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to delete session',
|
||||
type: 'internal_error',
|
||||
code: 'session_delete_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience endpoints for sessions without agent context
|
||||
export const listAllSessions = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
|
||||
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
|
||||
const status = req.query.status as any
|
||||
|
||||
logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`)
|
||||
|
||||
const result = await sessionService.listSessions(undefined, { limit, offset })
|
||||
|
||||
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`)
|
||||
return res.json({
|
||||
data: result.sessions,
|
||||
total: result.total,
|
||||
limit,
|
||||
offset
|
||||
} satisfies ListAgentSessionsResponse)
|
||||
} catch (error: any) {
|
||||
logger.error('Error listing all sessions:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to list sessions',
|
||||
type: 'internal_error',
|
||||
code: 'session_list_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getSessionById = async (req: Request, res: Response): Promise<Response> => {
|
||||
try {
|
||||
const { sessionId } = req.params
|
||||
logger.info(`Getting session: ${sessionId}`)
|
||||
|
||||
const session = await sessionService.getSessionById(sessionId)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found: ${sessionId}`)
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
message: 'Session not found',
|
||||
type: 'not_found',
|
||||
code: 'session_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`Session retrieved successfully: ${sessionId}`)
|
||||
return res.json(session)
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting session:', error)
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to get session',
|
||||
type: 'internal_error',
|
||||
code: 'session_get_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,927 +0,0 @@
|
||||
import express from 'express'
|
||||
|
||||
import { agentHandlers, messageHandlers, sessionHandlers } from './handlers'
|
||||
import { checkAgentExists, handleValidationErrors } from './middleware'
|
||||
import {
|
||||
validateAgent,
|
||||
validateAgentId,
|
||||
validateAgentReplace,
|
||||
validateAgentUpdate,
|
||||
validatePagination,
|
||||
validateSession,
|
||||
validateSessionId,
|
||||
validateSessionMessage,
|
||||
validateSessionReplace,
|
||||
validateSessionUpdate
|
||||
} from './validators'
|
||||
|
||||
// Create main agents router
|
||||
const agentsRouter = express.Router()
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* PermissionMode:
|
||||
* type: string
|
||||
* enum: [default, acceptEdits, bypassPermissions, plan]
|
||||
* description: Permission mode for agent operations
|
||||
*
|
||||
* AgentType:
|
||||
* type: string
|
||||
* enum: [claude-code]
|
||||
* description: Type of agent
|
||||
*
|
||||
* AgentConfiguration:
|
||||
* type: object
|
||||
* properties:
|
||||
* permission_mode:
|
||||
* $ref: '#/components/schemas/PermissionMode'
|
||||
* default: default
|
||||
* max_turns:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: Maximum number of interaction turns
|
||||
* additionalProperties: true
|
||||
*
|
||||
* AgentBase:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: Agent name
|
||||
* description:
|
||||
* type: string
|
||||
* description: Agent description
|
||||
* accessible_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of directory paths the agent can access
|
||||
* instructions:
|
||||
* type: string
|
||||
* description: System prompt/instructions
|
||||
* model:
|
||||
* type: string
|
||||
* description: Main model ID
|
||||
* plan_model:
|
||||
* type: string
|
||||
* description: Optional planning model ID
|
||||
* small_model:
|
||||
* type: string
|
||||
* description: Optional small/fast model ID
|
||||
* mcps:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of MCP tool IDs
|
||||
* allowed_tools:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of allowed tool IDs (whitelist)
|
||||
* configuration:
|
||||
* $ref: '#/components/schemas/AgentConfiguration'
|
||||
* required:
|
||||
* - model
|
||||
* - accessible_paths
|
||||
*
|
||||
* AgentEntity:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/AgentBase'
|
||||
* - type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: Unique agent identifier
|
||||
* type:
|
||||
* $ref: '#/components/schemas/AgentType'
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: ISO timestamp of creation
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: ISO timestamp of last update
|
||||
* required:
|
||||
* - id
|
||||
* - type
|
||||
* - created_at
|
||||
* - updated_at
|
||||
* CreateAgentRequest:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/AgentBase'
|
||||
* - type: object
|
||||
* properties:
|
||||
* type:
|
||||
* $ref: '#/components/schemas/AgentType'
|
||||
* name:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* description: Agent name (required)
|
||||
* model:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* description: Main model ID (required)
|
||||
* required:
|
||||
* - type
|
||||
* - name
|
||||
* - model
|
||||
*
|
||||
* UpdateAgentRequest:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: Agent name
|
||||
* description:
|
||||
* type: string
|
||||
* description: Agent description
|
||||
* accessible_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of directory paths the agent can access
|
||||
* instructions:
|
||||
* type: string
|
||||
* description: System prompt/instructions
|
||||
* model:
|
||||
* type: string
|
||||
* description: Main model ID
|
||||
* plan_model:
|
||||
* type: string
|
||||
* description: Optional planning model ID
|
||||
* small_model:
|
||||
* type: string
|
||||
* description: Optional small/fast model ID
|
||||
* mcps:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of MCP tool IDs
|
||||
* allowed_tools:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of allowed tool IDs (whitelist)
|
||||
* configuration:
|
||||
* $ref: '#/components/schemas/AgentConfiguration'
|
||||
* description: Partial update - all fields are optional
|
||||
*
|
||||
* ReplaceAgentRequest:
|
||||
* $ref: '#/components/schemas/AgentBase'
|
||||
*
|
||||
* SessionEntity:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/AgentBase'
|
||||
* - type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: Unique session identifier
|
||||
* agent_id:
|
||||
* type: string
|
||||
* description: Primary agent ID for the session
|
||||
* agent_type:
|
||||
* $ref: '#/components/schemas/AgentType'
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: ISO timestamp of creation
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: ISO timestamp of last update
|
||||
* required:
|
||||
* - id
|
||||
* - agent_id
|
||||
* - agent_type
|
||||
* - created_at
|
||||
* - updated_at
|
||||
*
|
||||
* CreateSessionRequest:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/AgentBase'
|
||||
* - type: object
|
||||
* properties:
|
||||
* model:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* description: Main model ID (required)
|
||||
* required:
|
||||
* - model
|
||||
*
|
||||
* UpdateSessionRequest:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: Session name
|
||||
* description:
|
||||
* type: string
|
||||
* description: Session description
|
||||
* accessible_paths:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of directory paths the agent can access
|
||||
* instructions:
|
||||
* type: string
|
||||
* description: System prompt/instructions
|
||||
* model:
|
||||
* type: string
|
||||
* description: Main model ID
|
||||
* plan_model:
|
||||
* type: string
|
||||
* description: Optional planning model ID
|
||||
* small_model:
|
||||
* type: string
|
||||
* description: Optional small/fast model ID
|
||||
* mcps:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of MCP tool IDs
|
||||
* allowed_tools:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: Array of allowed tool IDs (whitelist)
|
||||
* configuration:
|
||||
* $ref: '#/components/schemas/AgentConfiguration'
|
||||
* description: Partial update - all fields are optional
|
||||
*
|
||||
* ReplaceSessionRequest:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/AgentBase'
|
||||
* - type: object
|
||||
* properties:
|
||||
* model:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* description: Main model ID (required)
|
||||
* required:
|
||||
* - model
|
||||
*
|
||||
* CreateSessionMessageRequest:
|
||||
* type: object
|
||||
* properties:
|
||||
* content:
|
||||
* type: string
|
||||
* minLength: 1
|
||||
* description: Message content
|
||||
* required:
|
||||
* - content
|
||||
*
|
||||
* PaginationQuery:
|
||||
* type: object
|
||||
* properties:
|
||||
* limit:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 20
|
||||
* description: Number of items to return
|
||||
* offset:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* default: 0
|
||||
* description: Number of items to skip
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [idle, running, completed, failed, stopped]
|
||||
* description: Filter by session status
|
||||
*
|
||||
* ListAgentsResponse:
|
||||
* type: object
|
||||
* properties:
|
||||
* agents:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* total:
|
||||
* type: integer
|
||||
* description: Total number of agents
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: Number of items returned
|
||||
* offset:
|
||||
* type: integer
|
||||
* description: Number of items skipped
|
||||
* required:
|
||||
* - agents
|
||||
* - total
|
||||
* - limit
|
||||
* - offset
|
||||
*
|
||||
* ListSessionsResponse:
|
||||
* type: object
|
||||
* properties:
|
||||
* sessions:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/SessionEntity'
|
||||
* total:
|
||||
* type: integer
|
||||
* description: Total number of sessions
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: Number of items returned
|
||||
* offset:
|
||||
* type: integer
|
||||
* description: Number of items skipped
|
||||
* required:
|
||||
* - sessions
|
||||
* - total
|
||||
* - limit
|
||||
* - offset
|
||||
*
|
||||
* ErrorResponse:
|
||||
* type: object
|
||||
* properties:
|
||||
* error:
|
||||
* type: object
|
||||
* properties:
|
||||
* message:
|
||||
* type: string
|
||||
* description: Error message
|
||||
* type:
|
||||
* type: string
|
||||
* description: Error type
|
||||
* code:
|
||||
* type: string
|
||||
* description: Error code
|
||||
* required:
|
||||
* - message
|
||||
* - type
|
||||
* - code
|
||||
* required:
|
||||
* - error
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents:
|
||||
* post:
|
||||
* summary: Create a new agent
|
||||
* tags: [Agents]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateAgentRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Agent created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
// Agent CRUD routes
|
||||
agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents:
|
||||
* get:
|
||||
* summary: List all agents with pagination
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 20
|
||||
* description: Number of agents to return
|
||||
* - in: query
|
||||
* name: offset
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* default: 0
|
||||
* description: Number of agents to skip
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [idle, running, completed, failed, stopped]
|
||||
* description: Filter by agent status
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of agents
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ListAgentsResponse'
|
||||
*/
|
||||
agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* get:
|
||||
* summary: Get agent by ID
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* put:
|
||||
* summary: Replace agent (full update)
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ReplaceAgentRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* patch:
|
||||
* summary: Update agent (partial update)
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/UpdateAgentRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Agent updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/AgentEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}:
|
||||
* delete:
|
||||
* summary: Delete agent
|
||||
* tags: [Agents]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Agent deleted successfully
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent)
|
||||
|
||||
// Create sessions router with agent context
|
||||
const createSessionsRouter = (): express.Router => {
|
||||
const sessionsRouter = express.Router({ mergeParams: true })
|
||||
|
||||
// Session CRUD routes (nested under agent)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions:
|
||||
* post:
|
||||
* summary: Create a new session for an agent
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateSessionRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Session created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SessionEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.post('/', validateSession, handleValidationErrors, sessionHandlers.createSession)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions:
|
||||
* get:
|
||||
* summary: List sessions for an agent
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 20
|
||||
* description: Number of sessions to return
|
||||
* - in: query
|
||||
* name: offset
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* default: 0
|
||||
* description: Number of sessions to skip
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [idle, running, completed, failed, stopped]
|
||||
* description: Filter by session status
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of sessions
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ListSessionsResponse'
|
||||
* 404:
|
||||
* description: Agent not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* get:
|
||||
* summary: Get session by ID
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Session details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SessionEntity'
|
||||
* 404:
|
||||
* description: Agent or session not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* put:
|
||||
* summary: Replace session (full update)
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ReplaceSessionRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Session updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SessionEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent or session not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.put(
|
||||
'/:sessionId',
|
||||
validateSessionId,
|
||||
validateSessionReplace,
|
||||
handleValidationErrors,
|
||||
sessionHandlers.updateSession
|
||||
)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* patch:
|
||||
* summary: Update session (partial update)
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/UpdateSessionRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Session updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/SessionEntity'
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent or session not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.patch(
|
||||
'/:sessionId',
|
||||
validateSessionId,
|
||||
validateSessionUpdate,
|
||||
handleValidationErrors,
|
||||
sessionHandlers.patchSession
|
||||
)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}:
|
||||
* delete:
|
||||
* summary: Delete session
|
||||
* tags: [Sessions]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* responses:
|
||||
* 204:
|
||||
* description: Session deleted successfully
|
||||
* 404:
|
||||
* description: Agent or session not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
sessionsRouter.delete('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.deleteSession)
|
||||
|
||||
return sessionsRouter
|
||||
}
|
||||
|
||||
// Create messages router with agent and session context
|
||||
const createMessagesRouter = (): express.Router => {
|
||||
const messagesRouter = express.Router({ mergeParams: true })
|
||||
|
||||
// Message CRUD routes (nested under agent/session)
|
||||
/**
|
||||
* @swagger
|
||||
* /api/agents/{agentId}/sessions/{sessionId}/messages:
|
||||
* post:
|
||||
* summary: Create a new message in a session
|
||||
* tags: [Messages]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: agentId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Agent ID
|
||||
* - in: path
|
||||
* name: sessionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateSessionMessageRequest'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Message created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: number
|
||||
* description: Message ID
|
||||
* session_id:
|
||||
* type: string
|
||||
* description: Session ID
|
||||
* role:
|
||||
* type: string
|
||||
* enum: [assistant, user, system, tool]
|
||||
* description: Message role
|
||||
* content:
|
||||
* type: object
|
||||
* description: Message content (AI SDK format)
|
||||
* agent_session_id:
|
||||
* type: string
|
||||
* description: Agent session ID for resuming
|
||||
* metadata:
|
||||
* type: object
|
||||
* description: Additional metadata
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* 400:
|
||||
* description: Invalid request body
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
* 404:
|
||||
* description: Agent or session not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ErrorResponse'
|
||||
*/
|
||||
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
|
||||
return messagesRouter
|
||||
}
|
||||
|
||||
// Mount nested resources with clear hierarchy
|
||||
const sessionsRouter = createSessionsRouter()
|
||||
const messagesRouter = createMessagesRouter()
|
||||
|
||||
// Mount sessions under specific agent
|
||||
agentsRouter.use('/:agentId/sessions', validateAgentId, checkAgentExists, handleValidationErrors, sessionsRouter)
|
||||
|
||||
// Mount messages under specific agent/session
|
||||
agentsRouter.use(
|
||||
'/:agentId/sessions/:sessionId/messages',
|
||||
validateAgentId,
|
||||
validateSessionId,
|
||||
handleValidationErrors,
|
||||
messagesRouter
|
||||
)
|
||||
|
||||
// Export main router and convenience router
|
||||
export const agentsRoutes = agentsRouter
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { agentService } from '../../../../services/agents'
|
||||
import { loggerService } from '../../../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMiddleware')
|
||||
|
||||
// Since Zod validators handle their own errors, this is now a pass-through
|
||||
export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => {
|
||||
next()
|
||||
}
|
||||
|
||||
// Middleware to check if agent exists
|
||||
export const checkAgentExists = async (req: Request, res: Response, next: any): Promise<void> => {
|
||||
try {
|
||||
const { agentId } = req.params
|
||||
const exists = await agentService.agentExists(agentId)
|
||||
|
||||
if (!exists) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
message: 'Agent not found',
|
||||
type: 'not_found',
|
||||
code: 'agent_not_found'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
logger.error('Error checking agent existence:', error as Error)
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Failed to validate agent',
|
||||
type: 'internal_error',
|
||||
code: 'agent_validation_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './common'
|
||||
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
AgentIdParamSchema,
|
||||
CreateAgentRequestSchema,
|
||||
ReplaceAgentRequestSchema,
|
||||
UpdateAgentRequestSchema
|
||||
} from '@types'
|
||||
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateAgent = createZodValidator({
|
||||
body: CreateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentReplace = createZodValidator({
|
||||
body: ReplaceAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentUpdate = createZodValidator({
|
||||
body: UpdateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentId = createZodValidator({
|
||||
params: AgentIdParamSchema
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import { PaginationQuerySchema } from '@types'
|
||||
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validatePagination = createZodValidator({
|
||||
query: PaginationQuerySchema
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './agents'
|
||||
export * from './common'
|
||||
export * from './messages'
|
||||
export * from './sessions'
|
||||
@@ -1,7 +0,0 @@
|
||||
import { CreateSessionMessageRequestSchema } from '@types'
|
||||
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionMessage = createZodValidator({
|
||||
body: CreateSessionMessageRequestSchema
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import {
|
||||
CreateSessionRequestSchema,
|
||||
ReplaceSessionRequestSchema,
|
||||
SessionIdParamSchema,
|
||||
UpdateSessionRequestSchema
|
||||
} from '@types'
|
||||
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSession = createZodValidator({
|
||||
body: CreateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionReplace = createZodValidator({
|
||||
body: ReplaceSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionUpdate = createZodValidator({
|
||||
body: UpdateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionId = createZodValidator({
|
||||
params: SessionIdParamSchema
|
||||
})
|
||||
@@ -1,68 +0,0 @@
|
||||
import { NextFunction,Request, Response } from 'express'
|
||||
import { ZodError, ZodType } from 'zod'
|
||||
|
||||
export interface ValidationRequest extends Request {
|
||||
validatedBody?: any
|
||||
validatedParams?: any
|
||||
validatedQuery?: any
|
||||
}
|
||||
|
||||
export interface ZodValidationConfig {
|
||||
body?: ZodType
|
||||
params?: ZodType
|
||||
query?: ZodType
|
||||
}
|
||||
|
||||
export const createZodValidator = (config: ZodValidationConfig) => {
|
||||
return (req: ValidationRequest, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
if (config.body && req.body) {
|
||||
req.validatedBody = config.body.parse(req.body)
|
||||
}
|
||||
|
||||
if (config.params && req.params) {
|
||||
req.validatedParams = config.params.parse(req.params)
|
||||
}
|
||||
|
||||
if (config.query && req.query) {
|
||||
req.validatedQuery = config.query.parse(req.query)
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const validationErrors = error.issues.map((err) => ({
|
||||
type: 'field',
|
||||
value: err.input,
|
||||
msg: err.message,
|
||||
path: err.path.map(p => String(p)).join('.'),
|
||||
location: getLocationFromPath(err.path, config)
|
||||
}))
|
||||
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
details: validationErrors
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Internal validation error',
|
||||
type: 'internal_error',
|
||||
code: 'validation_processing_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationFromPath(path: (string | number | symbol)[], config: ZodValidationConfig): string {
|
||||
if (config.body && path.length > 0) return 'body'
|
||||
if (config.params && path.length > 0) return 'params'
|
||||
if (config.query && path.length > 0) return 'query'
|
||||
return 'unknown'
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { ChatCompletionCreateParams } from 'openai/resources'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import {
|
||||
ChatCompletionModelError,
|
||||
chatCompletionService,
|
||||
ChatCompletionValidationError
|
||||
} from '../services/chat-completion'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerChatRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
interface ErrorResponseBody {
|
||||
error: {
|
||||
message: string
|
||||
type: string
|
||||
code: string
|
||||
}
|
||||
}
|
||||
|
||||
const mapChatCompletionError = (error: unknown): { status: number; body: ErrorResponseBody } => {
|
||||
if (error instanceof ChatCompletionValidationError) {
|
||||
logger.warn('Chat completion validation error:', {
|
||||
errors: error.errors
|
||||
})
|
||||
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: {
|
||||
message: error.errors.join('; '),
|
||||
type: 'invalid_request_error',
|
||||
code: 'validation_failed'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ChatCompletionModelError) {
|
||||
logger.warn('Chat completion model error:', error.error)
|
||||
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
error: {
|
||||
message: error.error.message,
|
||||
type: 'invalid_request_error',
|
||||
code: error.error.code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
let statusCode = 500
|
||||
let errorType = 'server_error'
|
||||
let errorCode = 'internal_error'
|
||||
|
||||
if (error.message.includes('API key') || error.message.includes('authentication')) {
|
||||
statusCode = 401
|
||||
errorType = 'authentication_error'
|
||||
errorCode = 'invalid_api_key'
|
||||
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
|
||||
statusCode = 429
|
||||
errorType = 'rate_limit_error'
|
||||
errorCode = 'rate_limit_exceeded'
|
||||
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
|
||||
statusCode = 502
|
||||
errorType = 'server_error'
|
||||
errorCode = 'upstream_error'
|
||||
}
|
||||
|
||||
logger.error('Chat completion error:', { error })
|
||||
|
||||
return {
|
||||
status: statusCode,
|
||||
body: {
|
||||
error: {
|
||||
message: error.message || 'Internal server error',
|
||||
type: errorType,
|
||||
code: errorCode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error('Chat completion unknown error:', { error })
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: {
|
||||
message: 'Internal server error',
|
||||
type: 'server_error',
|
||||
code: 'internal_error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/chat/completions:
|
||||
* post:
|
||||
* summary: Create chat completion
|
||||
* description: Create a chat completion response, compatible with OpenAI API
|
||||
* tags: [Chat]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/ChatCompletionRequest'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Chat completion response
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* object:
|
||||
* type: string
|
||||
* example: chat.completion
|
||||
* created:
|
||||
* type: integer
|
||||
* model:
|
||||
* type: string
|
||||
* choices:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* index:
|
||||
* type: integer
|
||||
* message:
|
||||
* $ref: '#/components/schemas/ChatMessage'
|
||||
* finish_reason:
|
||||
* type: string
|
||||
* usage:
|
||||
* type: object
|
||||
* properties:
|
||||
* prompt_tokens:
|
||||
* type: integer
|
||||
* completion_tokens:
|
||||
* type: integer
|
||||
* total_tokens:
|
||||
* type: integer
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Server-sent events stream (when stream=true)
|
||||
* 400:
|
||||
* description: Bad request
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 429:
|
||||
* description: Rate limit exceeded
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.post('/completions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const request: ChatCompletionCreateParams = req.body
|
||||
|
||||
if (!request) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Request body is required',
|
||||
type: 'invalid_request_error',
|
||||
code: 'missing_body'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info('Chat completion request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages?.length || 0,
|
||||
stream: request.stream,
|
||||
temperature: request.temperature
|
||||
})
|
||||
|
||||
const isStreaming = !!request.stream
|
||||
|
||||
if (isStreaming) {
|
||||
const { stream } = await chatCompletionService.processStreamingCompletion(request)
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
res.setHeader('X-Accel-Buffering', 'no')
|
||||
res.flushHeaders()
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
|
||||
}
|
||||
res.write('data: [DONE]\n\n')
|
||||
} catch (streamError: any) {
|
||||
logger.error('Stream error:', streamError)
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
error: {
|
||||
message: 'Stream processing error',
|
||||
type: 'server_error',
|
||||
code: 'stream_error'
|
||||
}
|
||||
})}\n\n`
|
||||
)
|
||||
} finally {
|
||||
res.end()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const { response } = await chatCompletionService.processCompletion(request)
|
||||
return res.json(response)
|
||||
} catch (error: unknown) {
|
||||
const { status, body } = mapChatCompletionError(error)
|
||||
return res.status(status).json(body)
|
||||
}
|
||||
})
|
||||
|
||||
export { router as chatRoutes }
|
||||
@@ -1,153 +0,0 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { mcpApiService } from '../services/mcp'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMCPRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/mcps:
|
||||
* get:
|
||||
* summary: List MCP servers
|
||||
* description: Get a list of all configured Model Context Protocol servers
|
||||
* tags: [MCP]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of MCP servers
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/MCPServer'
|
||||
* 503:
|
||||
* description: Service unavailable
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* error:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
logger.info('Get all MCP servers request received')
|
||||
const servers = await mcpApiService.getAllServers(req)
|
||||
return res.json({
|
||||
success: true,
|
||||
data: servers
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching MCP servers:', error)
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to retrieve MCP servers: ${error.message}`,
|
||||
type: 'service_unavailable',
|
||||
code: 'servers_unavailable'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/mcps/{server_id}:
|
||||
* get:
|
||||
* summary: Get MCP server info
|
||||
* description: Get detailed information about a specific MCP server
|
||||
* tags: [MCP]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: server_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: MCP server ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: MCP server information
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/MCPServer'
|
||||
* 404:
|
||||
* description: MCP server not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: false
|
||||
* error:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
router.get('/:server_id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
logger.info('Get MCP server info request received')
|
||||
const server = await mcpApiService.getServerInfo(req.params.server_id)
|
||||
if (!server) {
|
||||
logger.warn('MCP server not found')
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'MCP server not found',
|
||||
type: 'not_found',
|
||||
code: 'server_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
return res.json({
|
||||
success: true,
|
||||
data: server
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching MCP server info:', error)
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to retrieve MCP server info: ${error.message}`,
|
||||
type: 'service_unavailable',
|
||||
code: 'server_info_unavailable'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Connect to MCP server
|
||||
router.all('/:server_id/mcp', async (req: Request, res: Response) => {
|
||||
const server = await mcpApiService.getServerById(req.params.server_id)
|
||||
if (!server) {
|
||||
logger.warn('MCP server not found')
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'MCP server not found',
|
||||
type: 'not_found',
|
||||
code: 'server_not_found'
|
||||
}
|
||||
})
|
||||
}
|
||||
return await mcpApiService.handleRequest(req, res, server)
|
||||
})
|
||||
|
||||
export { router as mcpRoutes }
|
||||
@@ -1,290 +0,0 @@
|
||||
import { MessageCreateParams } from '@anthropic-ai/sdk/resources'
|
||||
import express, { Request, Response } from 'express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { messagesService } from '../services/messages'
|
||||
import { validateModelId } from '../utils'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMessagesRoutes')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/messages:
|
||||
* post:
|
||||
* summary: Create message
|
||||
* description: Create a message response using Anthropic's API format
|
||||
* tags: [Messages]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - model
|
||||
* - max_tokens
|
||||
* - messages
|
||||
* properties:
|
||||
* model:
|
||||
* type: string
|
||||
* description: Model ID in format "provider:model_id"
|
||||
* example: "my-anthropic:claude-3-5-sonnet-20241022"
|
||||
* max_tokens:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: Maximum number of tokens to generate
|
||||
* example: 1024
|
||||
* messages:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* role:
|
||||
* type: string
|
||||
* enum: [user, assistant]
|
||||
* content:
|
||||
* oneOf:
|
||||
* - type: string
|
||||
* - type: array
|
||||
* system:
|
||||
* type: string
|
||||
* description: System message
|
||||
* temperature:
|
||||
* type: number
|
||||
* minimum: 0
|
||||
* maximum: 1
|
||||
* description: Sampling temperature
|
||||
* top_p:
|
||||
* type: number
|
||||
* minimum: 0
|
||||
* maximum: 1
|
||||
* description: Nucleus sampling
|
||||
* top_k:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* description: Top-k sampling
|
||||
* stream:
|
||||
* type: boolean
|
||||
* description: Whether to stream the response
|
||||
* tools:
|
||||
* type: array
|
||||
* description: Available tools for the model
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Message response
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* example: message
|
||||
* role:
|
||||
* type: string
|
||||
* example: assistant
|
||||
* content:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* model:
|
||||
* type: string
|
||||
* stop_reason:
|
||||
* type: string
|
||||
* stop_sequence:
|
||||
* type: string
|
||||
* usage:
|
||||
* type: object
|
||||
* properties:
|
||||
* input_tokens:
|
||||
* type: integer
|
||||
* output_tokens:
|
||||
* type: integer
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Server-sent events stream (when stream=true)
|
||||
* 400:
|
||||
* description: Bad request
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* example: error
|
||||
* error:
|
||||
* type: object
|
||||
* properties:
|
||||
* type:
|
||||
* type: string
|
||||
* message:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* 429:
|
||||
* description: Rate limit exceeded
|
||||
* 500:
|
||||
* description: Internal server error
|
||||
*/
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const request: MessageCreateParams = req.body
|
||||
|
||||
if (!request) {
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: 'Request body is required'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logger.info('Anthropic message request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages?.length || 0,
|
||||
stream: request.stream,
|
||||
max_tokens: request.max_tokens,
|
||||
temperature: request.temperature
|
||||
})
|
||||
|
||||
// Validate model ID and get provider
|
||||
const modelValidation = await validateModelId(request.model)
|
||||
if (!modelValidation.valid) {
|
||||
const error = modelValidation.error!
|
||||
logger.warn(`Model validation failed for '${request.model}':`, error)
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: error.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const provider = modelValidation.provider!
|
||||
|
||||
// Ensure provider is Anthropic type
|
||||
if (provider.type !== 'anthropic') {
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: `Invalid provider type '${provider.type}' for messages endpoint. Expected 'anthropic' provider.`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modelId = modelValidation.modelId!
|
||||
request.model = modelId
|
||||
|
||||
logger.info('Model validation successful:', {
|
||||
provider: provider.id,
|
||||
providerType: provider.type,
|
||||
modelId: modelId,
|
||||
fullModelId: request.model
|
||||
})
|
||||
|
||||
// Validate request
|
||||
const validation = messagesService.validateRequest(request)
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'invalid_request_error',
|
||||
message: validation.errors.join('; ')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle streaming
|
||||
if (request.stream) {
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
res.setHeader('X-Accel-Buffering', 'no')
|
||||
res.flushHeaders()
|
||||
|
||||
try {
|
||||
for await (const chunk of messagesService.processStreamingMessage(request, provider)) {
|
||||
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
|
||||
}
|
||||
res.write('data: [DONE]\n\n')
|
||||
} catch (streamError: any) {
|
||||
logger.error('Stream error:', streamError)
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'api_error',
|
||||
message: 'Stream processing error'
|
||||
}
|
||||
})}\n\n`
|
||||
)
|
||||
} finally {
|
||||
res.end()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle non-streaming
|
||||
const response = await messagesService.processMessage(request, provider)
|
||||
return res.json(response)
|
||||
} catch (error: any) {
|
||||
logger.error('Anthropic message error:', error)
|
||||
|
||||
let statusCode = 500
|
||||
let errorType = 'api_error'
|
||||
let errorMessage = 'Internal server error'
|
||||
|
||||
const anthropicStatus = typeof error?.status === 'number' ? error.status : undefined
|
||||
const anthropicError = error?.error
|
||||
|
||||
if (anthropicStatus) {
|
||||
statusCode = anthropicStatus
|
||||
}
|
||||
|
||||
if (anthropicError?.type) {
|
||||
errorType = anthropicError.type
|
||||
}
|
||||
|
||||
if (anthropicError?.message) {
|
||||
errorMessage = anthropicError.message
|
||||
} else if (error instanceof Error && error.message) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
if (!anthropicStatus && error instanceof Error) {
|
||||
if (error.message.includes('API key') || error.message.includes('authentication')) {
|
||||
statusCode = 401
|
||||
errorType = 'authentication_error'
|
||||
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
|
||||
statusCode = 429
|
||||
errorType = 'rate_limit_error'
|
||||
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
|
||||
statusCode = 502
|
||||
errorType = 'api_error'
|
||||
} else if (error.message.includes('validation') || error.message.includes('invalid')) {
|
||||
statusCode = 400
|
||||
errorType = 'invalid_request_error'
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(statusCode).json({
|
||||
type: 'error',
|
||||
error: {
|
||||
type: errorType,
|
||||
message: errorMessage,
|
||||
requestId: error?.request_id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export { router as messagesRoutes }
|
||||
@@ -1,130 +0,0 @@
|
||||
import { ApiModelsFilterSchema, ApiModelsResponse } from '@types'
|
||||
import express, { Request, Response } from 'express'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { modelsService } from '../services/models'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerModelsRoutes')
|
||||
|
||||
const router = express
|
||||
.Router()
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /v1/models:
|
||||
* get:
|
||||
* summary: List available models
|
||||
* description: Returns a list of available AI models from all configured providers with optional filtering
|
||||
* tags: [Models]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: providerType
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [openai, openai-response, anthropic, gemini]
|
||||
* description: Filter models by provider type
|
||||
* - in: query
|
||||
* name: offset
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* default: 0
|
||||
* description: Pagination offset
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: Maximum number of models to return
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of available models
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* object:
|
||||
* type: string
|
||||
* example: list
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Model'
|
||||
* total:
|
||||
* type: integer
|
||||
* description: Total number of models (when using pagination)
|
||||
* offset:
|
||||
* type: integer
|
||||
* description: Current offset (when using pagination)
|
||||
* limit:
|
||||
* type: integer
|
||||
* description: Current limit (when using pagination)
|
||||
* 400:
|
||||
* description: Invalid query parameters
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 503:
|
||||
* description: Service unavailable
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
logger.info('Models list request received', { query: req.query })
|
||||
|
||||
// Validate query parameters using Zod schema
|
||||
const filterResult = ApiModelsFilterSchema.safeParse(req.query)
|
||||
|
||||
if (!filterResult.success) {
|
||||
logger.warn('Invalid query parameters:', filterResult.error.issues)
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
message: 'Invalid query parameters',
|
||||
type: 'invalid_request_error',
|
||||
code: 'invalid_parameters',
|
||||
details: filterResult.error.issues.map((issue) => ({
|
||||
field: issue.path.join('.'),
|
||||
message: issue.message
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filter = filterResult.data
|
||||
const response = await modelsService.getModels(filter)
|
||||
|
||||
if (response.data.length === 0) {
|
||||
logger.warn(
|
||||
'No models available from providers. This may be because no OpenAI/Anthropic providers are configured or enabled.',
|
||||
{ filter }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`Returning ${response.data.length} models`, {
|
||||
filter,
|
||||
total: response.total
|
||||
})
|
||||
logger.debug(
|
||||
'Model IDs:',
|
||||
response.data.map((m) => m.id)
|
||||
)
|
||||
|
||||
return res.json(response satisfies ApiModelsResponse)
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching models:', error)
|
||||
return res.status(503).json({
|
||||
error: {
|
||||
message: 'Failed to retrieve models from available providers',
|
||||
type: 'service_unavailable',
|
||||
code: 'models_unavailable'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export { router as modelsRoutes }
|
||||
@@ -1,71 +0,0 @@
|
||||
import { createServer } from 'node:http'
|
||||
|
||||
import { agentService } from '../services/agents'
|
||||
import { loggerService } from '../services/LoggerService'
|
||||
import { app } from './app'
|
||||
import { config } from './config'
|
||||
|
||||
const logger = loggerService.withContext('ApiServer')
|
||||
|
||||
export class ApiServer {
|
||||
private server: ReturnType<typeof createServer> | null = null
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.server) {
|
||||
logger.warn('Server already running')
|
||||
return
|
||||
}
|
||||
|
||||
// Load config
|
||||
const { port, host, apiKey } = await config.load()
|
||||
|
||||
// Initialize AgentService
|
||||
logger.info('Initializing AgentService...')
|
||||
await agentService.initialize()
|
||||
logger.info('AgentService initialized successfully')
|
||||
|
||||
// Create server with Express app
|
||||
this.server = createServer(app)
|
||||
|
||||
// Start server
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.listen(port, host, () => {
|
||||
logger.info(`API Server started at http://${host}:${port}`)
|
||||
logger.info(`API Key: ${apiKey}`)
|
||||
resolve()
|
||||
})
|
||||
|
||||
this.server!.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.server) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.server!.close(() => {
|
||||
logger.info('API Server stopped')
|
||||
this.server = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await this.stop()
|
||||
await config.reload()
|
||||
await this.start()
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
const hasServer = this.server !== null
|
||||
const isListening = this.server?.listening || false
|
||||
const result = hasServer && isListening
|
||||
|
||||
logger.debug('isRunning check:', { hasServer, isListening, result })
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export const apiServer = new ApiServer()
|
||||
@@ -1,248 +0,0 @@
|
||||
import { Provider } from '@types'
|
||||
import OpenAI from 'openai'
|
||||
import { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from 'openai/resources'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { ModelValidationError, validateModelId } from '../utils'
|
||||
|
||||
const logger = loggerService.withContext('ChatCompletionService')
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export class ChatCompletionValidationError extends Error {
|
||||
constructor(public readonly errors: string[]) {
|
||||
super(`Request validation failed: ${errors.join('; ')}`)
|
||||
this.name = 'ChatCompletionValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatCompletionModelError extends Error {
|
||||
constructor(public readonly error: ModelValidationError) {
|
||||
super(`Model validation failed: ${error.message}`)
|
||||
this.name = 'ChatCompletionModelError'
|
||||
}
|
||||
}
|
||||
|
||||
export type PrepareRequestResult =
|
||||
| { status: 'validation_error'; errors: string[] }
|
||||
| { status: 'model_error'; error: ModelValidationError }
|
||||
| {
|
||||
status: 'ok'
|
||||
provider: Provider
|
||||
modelId: string
|
||||
client: OpenAI
|
||||
providerRequest: ChatCompletionCreateParams
|
||||
}
|
||||
|
||||
export class ChatCompletionService {
|
||||
async resolveProviderContext(model: string): Promise<
|
||||
| { ok: false; error: ModelValidationError }
|
||||
| { ok: true; provider: Provider; modelId: string; client: OpenAI }
|
||||
> {
|
||||
const modelValidation = await validateModelId(model)
|
||||
if (!modelValidation.valid) {
|
||||
return {
|
||||
ok: false,
|
||||
error: modelValidation.error!
|
||||
}
|
||||
}
|
||||
|
||||
const provider = modelValidation.provider!
|
||||
|
||||
if (provider.type !== 'openai') {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'unsupported_provider_type',
|
||||
message: `Provider '${provider.id}' of type '${provider.type}' is not supported for OpenAI chat completions`,
|
||||
code: 'unsupported_provider_type'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modelId = modelValidation.modelId!
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: provider.apiHost,
|
||||
apiKey: provider.apiKey
|
||||
})
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
provider,
|
||||
modelId,
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
async prepareRequest(request: ChatCompletionCreateParams, stream: boolean): Promise<PrepareRequestResult> {
|
||||
const requestValidation = this.validateRequest(request)
|
||||
if (!requestValidation.isValid) {
|
||||
return {
|
||||
status: 'validation_error',
|
||||
errors: requestValidation.errors
|
||||
}
|
||||
}
|
||||
|
||||
const providerContext = await this.resolveProviderContext(request.model!)
|
||||
if (!providerContext.ok) {
|
||||
return {
|
||||
status: 'model_error',
|
||||
error: providerContext.error
|
||||
}
|
||||
}
|
||||
|
||||
const { provider, modelId, client } = providerContext
|
||||
|
||||
logger.info('Model validation successful:', {
|
||||
provider: provider.id,
|
||||
providerType: provider.type,
|
||||
modelId,
|
||||
fullModelId: request.model
|
||||
})
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
provider,
|
||||
modelId,
|
||||
client,
|
||||
providerRequest: stream
|
||||
? {
|
||||
...request,
|
||||
model: modelId,
|
||||
stream: true as const
|
||||
}
|
||||
: {
|
||||
...request,
|
||||
model: modelId,
|
||||
stream: false as const
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateRequest(request: ChatCompletionCreateParams): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate messages
|
||||
if (!request.messages) {
|
||||
errors.push('Messages array is required')
|
||||
} else if (!Array.isArray(request.messages)) {
|
||||
errors.push('Messages must be an array')
|
||||
} else if (request.messages.length === 0) {
|
||||
errors.push('Messages array cannot be empty')
|
||||
} else {
|
||||
// Validate each message
|
||||
request.messages.forEach((message, index) => {
|
||||
if (!message.role) {
|
||||
errors.push(`Message ${index}: role is required`)
|
||||
}
|
||||
if (!message.content) {
|
||||
errors.push(`Message ${index}: content is required`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Validate optional parameters
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
async processCompletion(request: ChatCompletionCreateParams): Promise<{
|
||||
provider: Provider
|
||||
modelId: string
|
||||
response: OpenAI.Chat.Completions.ChatCompletion
|
||||
}> {
|
||||
try {
|
||||
logger.info('Processing chat completion request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages.length,
|
||||
stream: request.stream
|
||||
})
|
||||
|
||||
const preparation = await this.prepareRequest(request, false)
|
||||
if (preparation.status === 'validation_error') {
|
||||
throw new ChatCompletionValidationError(preparation.errors)
|
||||
}
|
||||
|
||||
if (preparation.status === 'model_error') {
|
||||
throw new ChatCompletionModelError(preparation.error)
|
||||
}
|
||||
|
||||
const { provider, modelId, client, providerRequest } = preparation
|
||||
|
||||
logger.debug('Sending request to provider:', {
|
||||
provider: provider.id,
|
||||
model: modelId,
|
||||
apiHost: provider.apiHost
|
||||
})
|
||||
|
||||
const response = (await client.chat.completions.create(providerRequest)) as OpenAI.Chat.Completions.ChatCompletion
|
||||
|
||||
logger.info('Successfully processed chat completion')
|
||||
return {
|
||||
provider,
|
||||
modelId,
|
||||
response
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error processing chat completion:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async processStreamingCompletion(
|
||||
request: ChatCompletionCreateParams
|
||||
): Promise<{
|
||||
provider: Provider
|
||||
modelId: string
|
||||
stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>
|
||||
}> {
|
||||
try {
|
||||
logger.info('Processing streaming chat completion request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages.length
|
||||
})
|
||||
|
||||
const preparation = await this.prepareRequest(request, true)
|
||||
if (preparation.status === 'validation_error') {
|
||||
throw new ChatCompletionValidationError(preparation.errors)
|
||||
}
|
||||
|
||||
if (preparation.status === 'model_error') {
|
||||
throw new ChatCompletionModelError(preparation.error)
|
||||
}
|
||||
|
||||
const { provider, modelId, client, providerRequest } = preparation
|
||||
|
||||
logger.debug('Sending streaming request to provider:', {
|
||||
provider: provider.id,
|
||||
model: modelId,
|
||||
apiHost: provider.apiHost
|
||||
})
|
||||
|
||||
const streamRequest = providerRequest as ChatCompletionCreateParamsStreaming
|
||||
const stream = (await client.chat.completions.create(streamRequest)) as AsyncIterable<
|
||||
OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
>
|
||||
|
||||
logger.info('Successfully started streaming chat completion')
|
||||
return {
|
||||
provider,
|
||||
modelId,
|
||||
stream
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error processing streaming chat completion:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const chatCompletionService = new ChatCompletionService()
|
||||
@@ -1,226 +0,0 @@
|
||||
import mcpService from '@main/services/MCPService'
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp'
|
||||
import {
|
||||
isJSONRPCRequest,
|
||||
JSONRPCMessage,
|
||||
JSONRPCMessageSchema,
|
||||
MessageExtraInfo
|
||||
} from '@modelcontextprotocol/sdk/types'
|
||||
import { MCPServer } from '@types'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { EventEmitter } from 'events'
|
||||
import { Request, Response } from 'express'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { getMcpServerById, getMCPServersFromRedux } from '../utils/mcp'
|
||||
|
||||
const logger = loggerService.withContext('MCPApiService')
|
||||
const transports: Record<string, StreamableHTTPServerTransport> = {}
|
||||
|
||||
interface McpServerDTO {
|
||||
id: MCPServer['id']
|
||||
name: MCPServer['name']
|
||||
type: MCPServer['type']
|
||||
description: MCPServer['description']
|
||||
url: string
|
||||
}
|
||||
|
||||
interface McpServersResp {
|
||||
servers: Record<string, McpServerDTO>
|
||||
}
|
||||
|
||||
/**
|
||||
* MCPApiService - API layer for MCP server management
|
||||
*
|
||||
* This service provides a REST API interface for MCP servers while integrating
|
||||
* with the existing application architecture:
|
||||
*
|
||||
* 1. Uses ReduxService to access the renderer's Redux store directly
|
||||
* 2. Syncs changes back to the renderer via Redux actions
|
||||
* 3. Leverages existing MCPService for actual server connections
|
||||
* 4. Provides session management for API clients
|
||||
*/
|
||||
class MCPApiService extends EventEmitter {
|
||||
private transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID()
|
||||
})
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.initMcpServer()
|
||||
logger.silly('MCPApiService initialized')
|
||||
}
|
||||
|
||||
private initMcpServer() {
|
||||
this.transport.onmessage = this.onMessage
|
||||
}
|
||||
|
||||
// get all activated servers
|
||||
async getAllServers(req: Request): Promise<McpServersResp> {
|
||||
try {
|
||||
const servers = await getMCPServersFromRedux()
|
||||
logger.silly(`Returning ${servers.length} servers`)
|
||||
const resp: McpServersResp = {
|
||||
servers: {}
|
||||
}
|
||||
for (const server of servers) {
|
||||
if (server.isActive) {
|
||||
resp.servers[server.id] = {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
type: 'streamableHttp',
|
||||
description: server.description,
|
||||
url: `${req.protocol}://${req.host}/v1/mcps/${server.id}/mcp`
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to get all servers:', error)
|
||||
throw new Error('Failed to retrieve servers')
|
||||
}
|
||||
}
|
||||
|
||||
// get server by id
|
||||
async getServerById(id: string): Promise<MCPServer | null> {
|
||||
try {
|
||||
logger.silly(`getServerById called with id: ${id}`)
|
||||
const servers = await getMCPServersFromRedux()
|
||||
const server = servers.find((s) => s.id === id)
|
||||
if (!server) {
|
||||
logger.warn(`Server with id ${id} not found`)
|
||||
return null
|
||||
}
|
||||
logger.silly(`Returning server with id ${id}`)
|
||||
return server
|
||||
} catch (error: any) {
|
||||
logger.error(`Failed to get server with id ${id}:`, error)
|
||||
throw new Error('Failed to retrieve server')
|
||||
}
|
||||
}
|
||||
|
||||
async getServerInfo(id: string): Promise<any> {
|
||||
try {
|
||||
logger.silly(`getServerInfo called with id: ${id}`)
|
||||
const server = await this.getServerById(id)
|
||||
if (!server) {
|
||||
logger.warn(`Server with id ${id} not found`)
|
||||
return null
|
||||
}
|
||||
logger.silly(`Returning server info for id ${id}`)
|
||||
|
||||
const client = await mcpService.initClient(server)
|
||||
const tools = await client.listTools()
|
||||
|
||||
logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) })
|
||||
|
||||
// const [version, tools, prompts, resources] = await Promise.all([
|
||||
// () => {
|
||||
// try {
|
||||
// return client.getServerVersion()
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to get server version for id ${id}:`, { error: error })
|
||||
// return '1.0.0'
|
||||
// }
|
||||
// },
|
||||
// (() => {
|
||||
// try {
|
||||
// return client.listTools()
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to list tools for id ${id}:`, { error: error })
|
||||
// return []
|
||||
// }
|
||||
// })(),
|
||||
// (() => {
|
||||
// try {
|
||||
// return client.listPrompts()
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to list prompts for id ${id}:`, { error: error })
|
||||
// return []
|
||||
// }
|
||||
// })(),
|
||||
// (() => {
|
||||
// try {
|
||||
// return client.listResources()
|
||||
// } catch (error) {
|
||||
// logger.error(`Failed to list resources for id ${id}:`, { error: error })
|
||||
// return []
|
||||
// }
|
||||
// })()
|
||||
// ])
|
||||
|
||||
return {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
type: server.type,
|
||||
description: server.description,
|
||||
tools
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`Failed to get server info with id ${id}:`, error)
|
||||
throw new Error('Failed to retrieve server info')
|
||||
}
|
||||
}
|
||||
|
||||
async handleRequest(req: Request, res: Response, server: MCPServer) {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined
|
||||
logger.silly(`Handling request for server with sessionId ${sessionId}`)
|
||||
let transport: StreamableHTTPServerTransport
|
||||
if (sessionId && transports[sessionId]) {
|
||||
transport = transports[sessionId]
|
||||
} else {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
transports[sessionId] = transport
|
||||
}
|
||||
})
|
||||
|
||||
transport.onclose = () => {
|
||||
logger.info(`Transport for sessionId ${sessionId} closed`)
|
||||
if (transport.sessionId) {
|
||||
delete transports[transport.sessionId]
|
||||
}
|
||||
}
|
||||
const mcpServer = await getMcpServerById(server.id)
|
||||
if (mcpServer) {
|
||||
await mcpServer.connect(transport)
|
||||
}
|
||||
}
|
||||
const jsonpayload = req.body
|
||||
const messages: JSONRPCMessage[] = []
|
||||
|
||||
if (Array.isArray(jsonpayload)) {
|
||||
for (const payload of jsonpayload) {
|
||||
const message = JSONRPCMessageSchema.parse(payload)
|
||||
messages.push(message)
|
||||
}
|
||||
} else {
|
||||
const message = JSONRPCMessageSchema.parse(jsonpayload)
|
||||
messages.push(message)
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
if (isJSONRPCRequest(message)) {
|
||||
if (!message.params) {
|
||||
message.params = {}
|
||||
}
|
||||
if (!message.params._meta) {
|
||||
message.params._meta = {}
|
||||
}
|
||||
message.params._meta.serverId = server.id
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Request body`, { rawBody: req.body, messages: JSON.stringify(messages) })
|
||||
await transport.handleRequest(req as IncomingMessage, res as ServerResponse, messages)
|
||||
}
|
||||
|
||||
private onMessage(message: JSONRPCMessage, extra?: MessageExtraInfo) {
|
||||
logger.info(`Received message: ${JSON.stringify(message)}`, extra)
|
||||
// Handle message here
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpApiService = new MCPApiService()
|
||||
@@ -1,106 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { Message, MessageCreateParams, RawMessageStreamEvent } from '@anthropic-ai/sdk/resources'
|
||||
import { Provider } from '@types'
|
||||
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('MessagesService')
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export class MessagesService {
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
validateRequest(request: MessageCreateParams): ValidationResult {
|
||||
// TODO: Implement comprehensive request validation
|
||||
const errors: string[] = []
|
||||
|
||||
if (!request.model) {
|
||||
errors.push('Model is required')
|
||||
}
|
||||
|
||||
if (!request.max_tokens || request.max_tokens < 1) {
|
||||
errors.push('max_tokens is required and must be at least 1')
|
||||
}
|
||||
|
||||
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
||||
errors.push('messages is required and must be a non-empty array')
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
async processMessage(request: MessageCreateParams, provider: Provider): Promise<Message> {
|
||||
logger.info('Processing Anthropic message request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages.length,
|
||||
stream: request.stream,
|
||||
max_tokens: request.max_tokens
|
||||
})
|
||||
|
||||
// Create Anthropic client for the provider
|
||||
const client = new Anthropic({
|
||||
baseURL: provider.apiHost,
|
||||
apiKey: provider.apiKey
|
||||
})
|
||||
|
||||
// Prepare request with the actual model ID
|
||||
const anthropicRequest: MessageCreateParams = {
|
||||
...request,
|
||||
stream: false
|
||||
}
|
||||
|
||||
logger.debug('Sending request to Anthropic provider:', {
|
||||
provider: provider.id,
|
||||
apiHost: provider.apiHost
|
||||
})
|
||||
|
||||
const response = await client.messages.create(anthropicRequest)
|
||||
|
||||
logger.info('Successfully processed Anthropic message')
|
||||
return response
|
||||
}
|
||||
|
||||
async *processStreamingMessage(
|
||||
request: MessageCreateParams,
|
||||
provider: Provider
|
||||
): AsyncIterable<RawMessageStreamEvent> {
|
||||
logger.info('Processing streaming Anthropic message request:', {
|
||||
model: request.model,
|
||||
messageCount: request.messages.length
|
||||
})
|
||||
|
||||
// Create Anthropic client for the provider
|
||||
const client = new Anthropic({
|
||||
baseURL: provider.apiHost,
|
||||
apiKey: provider.apiKey
|
||||
})
|
||||
|
||||
// Prepare streaming request
|
||||
const streamingRequest: MessageCreateParams = {
|
||||
...request,
|
||||
stream: true
|
||||
}
|
||||
|
||||
logger.debug('Sending streaming request to Anthropic provider:', {
|
||||
provider: provider.id,
|
||||
apiHost: provider.apiHost
|
||||
})
|
||||
|
||||
const stream = client.messages.stream(streamingRequest)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
yield chunk
|
||||
}
|
||||
|
||||
logger.info('Successfully completed streaming Anthropic message')
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const messagesService = new MessagesService()
|
||||
@@ -1,93 +0,0 @@
|
||||
import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
|
||||
import { loggerService } from '../../services/LoggerService'
|
||||
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
|
||||
|
||||
const logger = loggerService.withContext('ModelsService')
|
||||
|
||||
// Re-export for backward compatibility
|
||||
|
||||
export type ModelsFilter = ApiModelsFilter
|
||||
|
||||
export class ModelsService {
|
||||
async getModels(filter: ModelsFilter): Promise<ApiModelsResponse> {
|
||||
try {
|
||||
logger.debug('Getting available models from providers', { filter })
|
||||
|
||||
const models = await listAllAvailableModels()
|
||||
const providers = await getAvailableProviders()
|
||||
|
||||
// Use Map to deduplicate models by their full ID (provider:model_id)
|
||||
const uniqueModels = new Map<string, ApiModel>()
|
||||
|
||||
for (const model of models) {
|
||||
const openAIModel = transformModelToOpenAI(model, providers)
|
||||
const fullModelId = openAIModel.id // This is already in format "provider:model_id"
|
||||
|
||||
// Only add if not already present (first occurrence wins)
|
||||
if (!uniqueModels.has(fullModelId)) {
|
||||
uniqueModels.set(fullModelId, openAIModel)
|
||||
} else {
|
||||
logger.debug(`Skipping duplicate model: ${fullModelId}`)
|
||||
}
|
||||
}
|
||||
|
||||
let modelData = Array.from(uniqueModels.values())
|
||||
if (filter.providerType) {
|
||||
// Apply filters
|
||||
const providerType = filter.providerType
|
||||
modelData = modelData.filter((model) => {
|
||||
// Find the provider for this model and check its type
|
||||
return model.provider_type === providerType
|
||||
})
|
||||
logger.debug(`Filtered by provider type '${providerType}': ${modelData.length} models`)
|
||||
}
|
||||
|
||||
const total = modelData.length
|
||||
|
||||
// Apply pagination
|
||||
const offset = filter?.offset || 0
|
||||
const limit = filter?.limit
|
||||
|
||||
if (limit !== undefined) {
|
||||
modelData = modelData.slice(offset, offset + limit)
|
||||
logger.debug(
|
||||
`Applied pagination: offset=${offset}, limit=${limit}, showing ${modelData.length} of ${total} models`
|
||||
)
|
||||
} else if (offset > 0) {
|
||||
modelData = modelData.slice(offset)
|
||||
logger.debug(`Applied offset: offset=${offset}, showing ${modelData.length} of ${total} models`)
|
||||
}
|
||||
|
||||
logger.info(`Successfully retrieved ${modelData.length} models from ${models.length} total models`)
|
||||
|
||||
if (models.length > total) {
|
||||
logger.debug(`Filtered out ${models.length - total} models after deduplication and filtering`)
|
||||
}
|
||||
|
||||
const response: ApiModelsResponse = {
|
||||
object: 'list',
|
||||
data: modelData
|
||||
}
|
||||
|
||||
// Add pagination metadata if applicable
|
||||
if (filter?.limit !== undefined || filter?.offset !== undefined) {
|
||||
response.total = total
|
||||
response.offset = offset
|
||||
if (filter?.limit !== undefined) {
|
||||
response.limit = filter.limit
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting models:', error)
|
||||
return {
|
||||
object: 'list',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const modelsService = new ModelsService()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user