Compare commits

...

32 Commits

Author SHA1 Message Date
SuYao
9414f13f6d
fix: 修改请求体字段名 (#12430) 2026-01-12 16:38:55 +08:00
flt6
cbeda03acb
use cumsum in anthropic cache (#12419)
* use cumsum in anthropic cache

* fix types and refactor addCache in anthropicCacheMiddleware
2026-01-12 15:49:33 +08:00
George·Dong
cea36d170b
fix(qwen-code): format baseUrl with /v1 for OpenAI-compatible tools (#12418)
The Qwen Code tool was failing with 'Model stream ended without a finish reason'
because the OPENAI_BASE_URL environment variable was not properly formatted.

This fix adds /v1 suffix to the baseUrl when it's missing for OpenAI-compatible
tools (qwenCode, openaiCodex, iFlowCli).

Changes:
- Import formatApiHost from @renderer/utils/api
- Use formatApiHost to format baseUrl before passing to environment variables
- Add unit tests for the URL formatting behavior
2026-01-12 13:41:27 +08:00
Nicolae Fericitu
d84b84eb2f
i18n: Major improvements to Romanian (ro-RO) localization (#12428)
* fix(i18n): update and refine Romanian translation

I have corrected several typos and refined the terminology in the ro-ro.json file for better linguistic accuracy. This update ensures translation consistency throughout the user interface.

* i18n: Update and fix Romanian localization (ro-RO)

The Romanian localization file has been updated. 

Necessary corrections have been applied to address issues identified during an interface review, ensuring consistent terminology and improved message clarity.

* i18n: Capitalize "Users" label for UI consistency

Updated the "users" key in ro-ro.json to use an uppercase initial. This ensures visual consistency with other menu items in the settings section (User Management).
2026-01-12 10:58:38 +08:00
SuYao
c7c380d706
fix: disable strict JSON schema for OpenRouter to support MCP tools (#12415)
* fix: update dependencies and patch files for strict JSON schema compliance

- Updated `@ai-sdk/openai-compatible` to include version 1.0.30 and adjusted related patch files.
- Removed obsolete patch for `@ai-sdk/openai-compatible@1.0.28`.
- Added new patch for `@openrouter/ai-sdk-provider` to support strict JSON schema options.
- Modified `options.ts` to set `strictJsonSchema` to false for OpenAI models.
- Enhanced OpenAI compatible provider options to include `sendReasoning` and `strictJsonSchema`.
- Updated lockfile to reflect changes in patched dependencies and their hashes.

* fix: filter strictJsonSchema from request body in OpenRouter patch

- Destructure and remove strictJsonSchema from openrouterOptions before spreading into request body
- This prevents sending the internal option to the OpenRouter API

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:55:09 +08:00
Copilot
622e3f0db6
feat: Add year to topic timestamp and improve unpin UX (#12408)
* Initial plan

* feat: Add year to topic time display format

Co-authored-by: GeorgeDong32 <98630204+GeorgeDong32@users.noreply.github.com>

* fix: improve topic unpin UX by moving to top of unpinned list

- Unpinned topics now move to the top of the unpinned section
- List automatically scrolls to the unpinned topic position
- Keeps the requirement to unpin before deleting pinned topics

Closes #12398

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: GeorgeDong32 <98630204+GeorgeDong32@users.noreply.github.com>
Co-authored-by: George·Dong <GeorgeDong32@qq.com>
2026-01-10 21:37:37 +08:00
fullex
e5a2980da8
fix(logger): allow logging with unknown window source (#12406)
Some checks failed
Auto I18N Weekly / Auto I18N (push) Has been cancelled
* fix(logger): allow logging with unknown window source

Previously, LoggerService would block all logging when window source
was not initialized, which could swallow important business errors.
Now it uses 'UNKNOWN' as the window source and continues logging.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(logger): update documentation links for LoggerService eslint

Updated the ESLint configuration to point to the correct documentation for the unified LoggerService, ensuring users have access to the latest guides in both English and Chinese.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 15:12:00 +08:00
George·Dong
5b5e190132
feat(models): add Qwen text-embedding models to defaults (#12410)
* feat(models): add Qwen text-embedding models to defaults

Add four Qwen text-embedding models (v4, v3, v2, v1) to the
default model list in the renderer config. Group them under
"Qwen-text-embedding" and set the provider to "dashscope".

This ensures embedding models are available by default for
features that require text embeddings.

* fix(models): normalize qwen embedding group name

Change group value for Dashscope Qwen embedding models from
'Qwen-text-embedding' to 'qwen-text-embedding' in the default models
configuration. This makes the group naming consistent with other Qwen
model groups (lowercase) and avoids potential mismatches or lookup
errors caused by case differences.

* feat(dashscope): add qwen3-rerank model

Add qwen3-rerank model for Alibaba Cloud Bailian rerank API.
2026-01-10 15:11:08 +08:00
kangfenmao
e8e8f028f3 chore(release): v1.7.13
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 21:03:45 +08:00
kangfenmao
8ab082ceb5 feat(i18n): add careers section to AboutSettings and update translations
- Updated package.json to remove redundant i18n:check command.
- Added a "careers" section in the AboutSettings component with a button linking to the careers page.
- Introduced translations for the "careers" section in multiple languages including English, Chinese, German, Spanish, French, Japanese, Portuguese, Romanian, and Russian.
- Updated cache-related translations across various languages to provide localized support.
2026-01-09 20:57:59 +08:00
Phantom
864eda68fb
ci(workflows): fix pnpm installation and improve issue tracker (#12388)
* ci(workflows): add pnpm caching to improve build performance

Cache pnpm dependencies to reduce installation time in CI workflows

* ci(github-actions): change claude-code-action version to v1

* ci(workflows): improve issue tracker formatting

Format issue body as markdown code block for better readability in workflow output

* ci(workflows): unify claude-code-action version to v1 for both jobs

Changed process-pending-issues job from @main to @v1 to maintain consistency across all jobs and avoid potential version conflicts.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(workflows): remove --help from allowed-tools to avoid upstream parsing bug

The `--help` flags in claude_args were being incorrectly parsed as actual
command-line arguments by claude-code-action, causing JSON parsing errors
("Unexpected identifier 'Usage'").

Switched to wildcard pattern `pnpm tsx scripts/feishu-notify.ts*` which:
- Allows all feishu-notify.ts commands including --help
- Avoids triggering the upstream argument parsing bug
- Simplifies the allowed-tools configuration

This addresses the root cause identified by Anthropic engineers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(workflows): remove --help references from prompts

Remove `--help` references from prompts to avoid potential parsing issues.
The example commands are already comprehensive enough without needing to
mention the help flag.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 20:47:39 +08:00
亢奋猫
c5ea42ca3a
fix(security): prevent path traversal vulnerability in DXT plugin system (#12377)
* fix(security): prevent path traversal vulnerability in DXT plugin system

Add input validation to prevent path traversal attacks in DXT plugin handling:

- Add sanitizeName() to filter dangerous characters from manifest.name
- Add validateCommand() to reject commands with path traversal sequences
- Add validateArgs() to validate command arguments
- Remove unsafe fallback logic in cleanupDxtServer()

The vulnerability allowed attackers to write files to arbitrary locations
on Windows by crafting malicious DXT packages with path traversal sequences
(e.g., "..\\..\\Windows\\System32\\") in manifest.name or command fields.

* refactor: use path validation instead of input sanitization

---------

Co-authored-by: defi-failure <159208748+defi-failure@users.noreply.github.com>
2026-01-09 20:47:14 +08:00
Sun
bdf8f103c8
fix(mcp): 修复 MCP 配置 timeout 字段不支持字符串类型的问题 (#12384)
fix(mcp): allow string input for timeout in mcp config

Co-authored-by: Sun <10309831+x_taiyang@user.noreply.gitee.com>
2026-01-09 17:24:08 +08:00
SuYao
7a7089e315
fix: normalize topics in useAssistant and assistants slice to prevent errors (#12319) 2026-01-09 17:21:20 +08:00
defi-failure
9b8420f9b9
fix: restore patch for claude-agent-sdk (#12391)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 16:26:39 +08:00
SuYao
29d8c4a7ed
fix(aiCore): only apply sendReasoning for openai-compatible SDK providers (#12387)
sendReasoning is a patch specific to @ai-sdk/openai-compatible package.
Previously it was incorrectly applied to all providers in buildGenericProviderOptions,
including those with dedicated SDK packages (e.g., cerebras, deepseek, openrouter).

Now it only applies when the provider will actually use openai-compatible SDK:
- No dedicated SDK registered (!hasProviderConfig(providerId))
- OR explicitly openai-compatible (providerId === 'openai-compatible')

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:00:31 +08:00
Phantom
76cc196667
ci(workflows): add Feishu notification for workflow failures (#12375)
* ci(workflows): add feishu notification for failed sync jobs

Add Feishu webhook notification when sync-to-gitcode workflow fails or is cancelled. The notification includes tag name, status and run URL for quick debugging.

* ci(workflow): add feishu notification for failed or cancelled jobs
2026-01-09 11:35:17 +08:00
SuYao
61aae7376a
fix: add dispose method to prevent abort listener leak (#12269)
* fix: add dispose method to prevent abort listener leak

Add dispose() method to StreamAbortController that explicitly removes
the abort event listener when stream ends normally. Previously, the
listener would only be removed when abort was triggered ({ once: true }),
but if the stream completed normally without abort, the listener would
remain attached until garbage collection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: format code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 17:55:15 +08:00
kangfenmao
74e1d0887d chore: release v1.7.12
Updated version number to 1.7.12 in package.json and electron-builder.yml. Added release notes detailing the introduction of the MCP Hub with Auto mode and new cache control options for the Anthropic provider, along with various bug fixes.
2026-01-08 17:47:05 +08:00
Phantom
2a1722bb52
fix(workflows): add pnpm installing and caching (#12374) 2026-01-08 17:42:59 +08:00
fullex
7ff6955870
fix(SelectionService): add macOS key code support for modifier key detection (#12355) 2026-01-08 17:42:17 +08:00
pippobj
008df2d4b7
feat(baichuan):add baichuan models (#12364)
Co-authored-by: roberto <roberto@baichuan-inc.com>
2026-01-08 17:10:07 +08:00
Chen Yichi
8223c9fbfd
fix(Tray): set X11 window class and name to cherry-studio (#12348)
fix: set X11 window class and name to cherry-studio

Set window class and name for Linux X11 to ensure system tray and
window manager identify the app correctly instead of using default
'electron' identifier.
2026-01-08 17:08:51 +08:00
beyondkmp
153c1024f6
refactor: use pnpm install instead of manual download for prebuild packages (#12358)
* refactor: use pnpm install instead of manual download for prebuild packages

Replace manual tgz download with pnpm install for architecture-specific
prebuild binaries, simplifying the build process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* delete utils

* update after pack

* udpate before pack

* use optional deps

* refactor: use js-yaml to modify pnpm-workspace.yaml for cross-platform builds

- Add all prebuild packages to optionalDependencies in package.json
- Use js-yaml to parse and modify pnpm-workspace.yaml
- Add target platform to supportedArchitectures.os and cpu
- Restore original config after pnpm install

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix version

* refactor: streamline package management and filtering logic in before… (#12370)

refactor: streamline package management and filtering logic in before-pack.js

- Consolidated architecture-specific package definitions into a single array for better maintainability.
- Simplified the logic for determining target platform and architecture.
- Enhanced the filtering process for excluding and including packages based on architecture and platform.
- Improved console logging for clarity during package installation.

This refactor aims to improve the readability and efficiency of the prebuild package handling process.

* refactor: update package filtering logic in before-pack.js to read from electron-builder.yml

- Modified the package filtering process to load configuration directly from electron-builder.yml, reducing potential errors from multiple overrides.
- Enhanced maintainability by centralizing the file configuration management.

This change aims to streamline the prebuild package handling and improve configuration clarity.

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2026-01-08 17:07:41 +08:00
Phantom
43a48a4a38
feat(scripts): migrate feishu-notify to TypeScript CLI tool with subcommands (#12371)
* build: add commander package as dependency

* refactor(scripts): migrate feishu-notify to TypeScript with CLI interface

- Convert JavaScript implementation to TypeScript with proper type definitions
- Add CLI interface using commander for better usability
- Improve error handling and input validation
- Add version management and subcommand support

* ci(workflows): update feishu notification command and add pnpm install step

Update the feishu notification command to use CLI tool with proper arguments instead of direct node script execution
Add pnpm install step to ensure dependencies are available before running the workflow

* docs: add feishu notification script documentation

Add Chinese and English documentation for the feishu-notify.ts CLI tool

* feat(notify): add generic send command to feishu-notify

Add a new 'send' subcommand to send simple notifications to Feishu with customizable title, description and header color. This provides a more flexible way to send notifications without being tied to specific business logic like the existing 'issue' command.

The implementation includes:
- New send command handler and options interface
- Simple card creation function
- Zod schema for header color validation
- Documentation updates in both Chinese and English
2026-01-08 16:55:46 +08:00
kangfenmao
0cb3bd8311 fix: add claude code sdk support for arm version windows
- Enhanced the logic to determine included Claude code vendors based on architecture and platform.
- Adjusted filters for excluding and including Claude code vendors to improve compatibility, particularly for Windows ARM64.
- Removed unnecessary variables and streamlined the filter application process.
2026-01-08 00:05:35 +08:00
kangfenmao
2f67b63057 chore: update package.json and pnpm-lock.yaml for dependency management
- Removed outdated dependencies: js-yaml, bonjour-service, and emoji-picker-element-data from devDependencies.
- Added js-yaml, bonjour-service, and emoji-picker-element-data back to their respective locations in dependencies.
- Introduced optionalDependencies for @strongtz/win32-arm64-msvc with version ^0.4.7.
- Updated pnpm-lock.yaml to reflect the changes in package.json and added new package versions.
2026-01-07 23:12:37 +08:00
花月喵梦
81ea847989
Add Anthropic Cache (#12333)
* add anthropic cache

* i18n: sync

* fix: condition judgment

* lg

* ag

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2026-01-07 23:05:30 +08:00
defi-failure
1d07e89e38
fix: remove blockmap handling after differentialPackage disabled (#12351) 2026-01-07 22:48:31 +08:00
kangfenmao
90cd06d23d chore: release v1.7.11
Updated version number to 1.7.11 in package.json and electron-builder.yml. Added release notes highlighting the introduction of the MCP Hub with Auto mode and various bug fixes, including improvements to the Chat and Editor components.
2026-01-07 18:50:28 +08:00
kangfenmao
8d56bf80dd chore: update GitHub Actions workflow to enable corepack for pnpm installation
Replaced the pnpm action setup with a corepack enable command to streamline dependency management in the workflow.
2026-01-07 18:46:07 +08:00
kangfenmao
7766438853 Revert "fix(SearchService): Fix inability to retrieve search results from Bing, Baidu, and Google"
This reverts commit b83fbc0ace.
2026-01-07 18:40:39 +08:00
58 changed files with 2593 additions and 654 deletions

View File

@ -90,3 +90,30 @@ jobs:
- name: 📢 Notify if no changes
if: steps.git_status.outputs.has_changes != 'true'
run: echo "Bot script ran, but no changes were detected. No PR created."
- name: Send failure notification to Feishu
if: always() && (failure() || cancelled())
shell: bash
env:
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
JOB_STATUS: ${{ job.status }}
run: |
# Determine status and color
if [ "$JOB_STATUS" = "cancelled" ]; then
STATUS_TEXT="已取消"
COLOR="orange"
else
STATUS_TEXT="失败"
COLOR="red"
fi
# Build description using printf
DESCRIPTION=$(printf "**状态:** %s\n\n**工作流:** [查看详情](%s)" "$STATUS_TEXT" "$RUN_URL")
# Send notification
pnpm tsx scripts/feishu-notify.ts send \
-t "自动国际化${STATUS_TEXT}" \
-d "$DESCRIPTION" \
-c "${COLOR}"

View File

@ -58,14 +58,34 @@ jobs:
with:
node-version: 22
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm dependencies
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install dependencies
if: steps.check_time.outputs.should_delay == 'false'
run: pnpm install
- name: Process issue with Claude
if: steps.check_time.outputs.should_delay == 'false'
uses: anthropics/claude-code-action@main
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: "*"
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
claude_args: "--allowed-tools Bash(gh issue:*),Bash(node scripts/feishu-notify.js)"
claude_args: "--allowed-tools Bash(gh issue:*),Bash(pnpm tsx scripts/feishu-notify.ts*)"
prompt: |
你是一个GitHub Issue自动化处理助手。请完成以下任务
@ -74,9 +94,14 @@ jobs:
- 标题:${{ github.event.issue.title }}
- 作者:${{ github.event.issue.user.login }}
- URL${{ github.event.issue.html_url }}
- 内容:${{ github.event.issue.body }}
- 标签:${{ join(github.event.issue.labels.*.name, ', ') }}
### Issue body
`````md
${{ github.event.issue.body }}
`````
## 任务步骤
1. **分析并总结issue**
@ -86,20 +111,20 @@ jobs:
- 重要的技术细节
2. **发送飞书通知**
使用以下命令发送飞书通知注意ISSUE_SUMMARY需要用引号包裹
使用CLI工具发送飞书通知参考以下示例
```bash
ISSUE_URL="${{ github.event.issue.html_url }}" \
ISSUE_NUMBER="${{ github.event.issue.number }}" \
ISSUE_TITLE="${{ github.event.issue.title }}" \
ISSUE_AUTHOR="${{ github.event.issue.user.login }}" \
ISSUE_LABELS="${{ join(github.event.issue.labels.*.name, ',') }}" \
ISSUE_SUMMARY="<你生成的中文总结>" \
node scripts/feishu-notify.js
pnpm tsx scripts/feishu-notify.ts issue \
-u "${{ github.event.issue.html_url }}" \
-n "${{ github.event.issue.number }}" \
-t "${{ github.event.issue.title }}" \
-a "${{ github.event.issue.user.login }}" \
-l "${{ join(github.event.issue.labels.*.name, ',') }}" \
-m "<你生成的中文总结>"
```
## 注意事项
- 总结必须使用简体中文
- ISSUE_SUMMARY 在传递给 node 命令时需要正确转义特殊字符
- 命令行参数需要正确转义特殊字符
- 如果issue内容为空也要提供一个简短的说明
请开始执行任务!
@ -125,13 +150,32 @@ jobs:
with:
node-version: 22
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm dependencies
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install
- name: Process pending issues with Claude
uses: anthropics/claude-code-action@main
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
allowed_non_write_users: "*"
github_token: ${{ secrets.GITHUB_TOKEN }}
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:*),Bash(node scripts/feishu-notify.js)"
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:*),Bash(pnpm tsx scripts/feishu-notify.ts*)"
prompt: |
你是一个GitHub Issue自动化处理助手。请完成以下任务
@ -153,15 +197,15 @@ jobs:
- 重要的技术细节
3. **发送飞书通知**
对于每个issue使用以下命令发送飞书通知
使用CLI工具发送飞书通知参考以下示例
```bash
ISSUE_URL="<issue的html_url>" \
ISSUE_NUMBER="<issue编号>" \
ISSUE_TITLE="<issue标题>" \
ISSUE_AUTHOR="<issue作者>" \
ISSUE_LABELS="<逗号分隔的标签列表排除pending-feishu-notification>" \
ISSUE_SUMMARY="<你生成的中文总结>" \
node scripts/feishu-notify.js
pnpm tsx scripts/feishu-notify.ts issue \
-u "<issue的html_url>" \
-n "<issue编号>" \
-t "<issue标题>" \
-a "<issue作者>" \
-l "<逗号分隔的标签列表排除pending-feishu-notification>" \
-m "<你生成的中文总结>"
```
4. **移除标签**

View File

@ -79,7 +79,7 @@ jobs:
shell: bash
run: |
echo "Built Windows artifacts:"
ls -la dist/*.exe dist/*.blockmap dist/latest*.yml
ls -la dist/*.exe dist/latest*.yml
- name: Download GitHub release assets
shell: bash
@ -112,12 +112,10 @@ jobs:
fi
# Remove unsigned Windows files from downloaded assets
# *.exe, *.exe.blockmap, latest.yml (Windows only)
rm -f release-assets/*.exe release-assets/*.exe.blockmap release-assets/latest.yml 2>/dev/null || true
rm -f release-assets/*.exe release-assets/latest.yml 2>/dev/null || true
# Copy signed Windows files with error checking
cp dist/*.exe release-assets/ || { echo "ERROR: Failed to copy .exe files"; exit 1; }
cp dist/*.exe.blockmap release-assets/ || { echo "ERROR: Failed to copy .blockmap files"; exit 1; }
cp dist/latest.yml release-assets/ || { echo "ERROR: Failed to copy latest.yml"; exit 1; }
echo "Final release assets:"
@ -302,3 +300,31 @@ jobs:
run: |
rm -f /tmp/release_payload.json /tmp/upload_headers.txt release_body.txt
rm -rf release-assets/
- name: Send failure notification to Feishu
if: always() && (failure() || cancelled())
shell: bash
env:
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
JOB_STATUS: ${{ job.status }}
run: |
# Determine status and color
if [ "$JOB_STATUS" = "cancelled" ]; then
STATUS_TEXT="已取消"
COLOR="orange"
else
STATUS_TEXT="失败"
COLOR="red"
fi
# Build description using printf
DESCRIPTION=$(printf "**标签:** %s\n\n**状态:** %s\n\n**工作流:** [查看详情](%s)" "$TAG_NAME" "$STATUS_TEXT" "$RUN_URL")
# Send notification
pnpm tsx scripts/feishu-notify.ts send \
-t "GitCode 同步${STATUS_TEXT}" \
-d "$DESCRIPTION" \
-c "${COLOR}"

View File

@ -154,9 +154,10 @@ jobs:
with:
node-version: 22
- name: Install pnpm
- name: Enable corepack
if: steps.check.outputs.should_run == 'true'
uses: pnpm/action-setup@v4
working-directory: main
run: corepack enable pnpm
- name: Install dependencies
if: steps.check.outputs.should_run == 'true'

View File

@ -0,0 +1,155 @@
# Feishu Notification Script
`scripts/feishu-notify.ts` is a CLI tool for sending notifications to Feishu (Lark) Webhook. This script is primarily used in GitHub Actions workflows to enable automatic notifications.
## Features
- Subcommand-based CLI structure for different notification types
- HMAC-SHA256 signature verification
- Sends Feishu interactive card messages
- Full TypeScript type support
- Credentials via environment variables for security
## Usage
### Prerequisites
```bash
pnpm install
```
### CLI Structure
```bash
pnpm tsx scripts/feishu-notify.ts [command] [options]
```
### Environment Variables (Required)
| Variable | Description |
|----------|-------------|
| `FEISHU_WEBHOOK_URL` | Feishu Webhook URL |
| `FEISHU_WEBHOOK_SECRET` | Feishu Webhook signing secret |
## Commands
### `send` - Send Simple Notification
Send a generic notification without business-specific logic.
```bash
pnpm tsx scripts/feishu-notify.ts send [options]
```
| Option | Short | Description | Required |
|--------|-------|-------------|----------|
| `--title` | `-t` | Card title | Yes |
| `--description` | `-d` | Card description (supports markdown) | Yes |
| `--color` | `-c` | Header color template | No (default: turquoise) |
**Available colors:** `blue`, `wathet`, `turquoise`, `green`, `yellow`, `orange`, `red`, `carmine`, `violet`, `purple`, `indigo`, `grey`, `default`
#### Example
```bash
# Use $'...' syntax for proper newlines
pnpm tsx scripts/feishu-notify.ts send \
-t "Deployment Completed" \
-d $'**Status:** Success\n\n**Environment:** Production\n\n**Version:** v1.2.3' \
-c green
```
```bash
# Send an error alert (red color)
pnpm tsx scripts/feishu-notify.ts send \
-t "Error Alert" \
-d $'**Error Type:** Connection failed\n\n**Severity:** High\n\nPlease check the system status' \
-c red
```
**Note:** For proper newlines in the description, use bash's `$'...'` syntax. Do not use literal `\n` in double quotes, as it will be displayed as-is in the Feishu card.
### `issue` - Send GitHub Issue Notification
```bash
pnpm tsx scripts/feishu-notify.ts issue [options]
```
| Option | Short | Description | Required |
|--------|-------|-------------|----------|
| `--url` | `-u` | GitHub issue URL | Yes |
| `--number` | `-n` | Issue number | Yes |
| `--title` | `-t` | Issue title | Yes |
| `--summary` | `-m` | Issue summary | Yes |
| `--author` | `-a` | Issue author | No (default: "Unknown") |
| `--labels` | `-l` | Issue labels (comma-separated) | No |
#### Example
```bash
pnpm tsx scripts/feishu-notify.ts issue \
-u "https://github.com/owner/repo/issues/123" \
-n "123" \
-t "Bug: Something is broken" \
-m "This is a bug report about a feature" \
-a "username" \
-l "bug,high-priority"
```
## Usage in GitHub Actions
This script is primarily used in `.github/workflows/github-issue-tracker.yml`:
```yaml
- name: Install dependencies
run: pnpm install
- name: Send notification
run: |
pnpm tsx scripts/feishu-notify.ts issue \
-u "${{ github.event.issue.html_url }}" \
-n "${{ github.event.issue.number }}" \
-t "${{ github.event.issue.title }}" \
-a "${{ github.event.issue.user.login }}" \
-l "${{ join(github.event.issue.labels.*.name, ',') }}" \
-m "Issue summary content"
env:
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
```
## Feishu Card Message Format
The `issue` command sends an interactive card containing:
- **Header**: `#<issue_number> - <issue_title>`
- **Author**: Issue creator
- **Labels**: Issue labels (if any)
- **Summary**: Issue content summary
- **Action Button**: "View Issue" button linking to the GitHub Issue page
## Configuring Feishu Webhook
1. Add a custom bot to your Feishu group
2. Obtain the Webhook URL and signing secret
3. Configure them in GitHub Secrets:
- `FEISHU_WEBHOOK_URL`: Webhook address
- `FEISHU_WEBHOOK_SECRET`: Signing secret
## Error Handling
The script exits with a non-zero code when:
- Required environment variables are missing (`FEISHU_WEBHOOK_URL`, `FEISHU_WEBHOOK_SECRET`)
- Required command options are missing
- Feishu API returns a non-2xx status code
- Network request fails
## Extending with New Commands
The CLI is designed to support multiple notification types. To add a new command:
1. Define the command options interface
2. Create a card builder function
3. Add a new command handler
4. Register the command with `program.command()`

View File

@ -0,0 +1,155 @@
# 飞书通知脚本
`scripts/feishu-notify.ts` 是一个 CLI 工具,用于向飞书 Webhook 发送通知。该脚本主要在 GitHub Actions 工作流中使用,实现自动通知功能。
## 功能特性
- 基于子命令的 CLI 结构,支持不同类型的通知
- 使用 HMAC-SHA256 签名验证
- 发送飞书交互式卡片消息
- 完整的 TypeScript 类型支持
- 通过环境变量传递凭证,确保安全性
## 使用方式
### 前置依赖
```bash
pnpm install
```
### CLI 结构
```bash
pnpm tsx scripts/feishu-notify.ts [command] [options]
```
### 环境变量(必需)
| 变量 | 说明 |
|------|------|
| `FEISHU_WEBHOOK_URL` | 飞书 Webhook URL |
| `FEISHU_WEBHOOK_SECRET` | 飞书 Webhook 签名密钥 |
## 命令
### `send` - 发送简单通知
发送通用通知,不涉及具体业务逻辑。
```bash
pnpm tsx scripts/feishu-notify.ts send [options]
```
| 参数 | 短选项 | 说明 | 必需 |
|------|--------|------|------|
| `--title` | `-t` | 卡片标题 | 是 |
| `--description` | `-d` | 卡片描述(支持 markdown | 是 |
| `--color` | `-c` | 标题栏颜色模板 | 否默认turquoise |
**可用颜色:** `blue`(蓝色), `wathet`(浅蓝), `turquoise`(青绿), `green`(绿色), `yellow`(黄色), `orange`(橙色), `red`(红色), `carmine`(深红), `violet`(紫罗兰), `purple`(紫色), `indigo`(靛蓝), `grey`(灰色), `default`(默认)
#### 示例
```bash
# 使用 $'...' 语法实现正确的换行
pnpm tsx scripts/feishu-notify.ts send \
-t "部署完成" \
-d $'**状态:** 成功\n\n**环境:** 生产环境\n\n**版本:** v1.2.3' \
-c green
```
```bash
# 发送错误警报(红色)
pnpm tsx scripts/feishu-notify.ts send \
-t "错误警报" \
-d $'**错误类型:** 连接失败\n\n**严重程度:** 高\n\n请及时检查系统状态' \
-c red
```
**注意:** 如需在描述中换行,请使用 bash 的 `$'...'` 语法。不要在双引号中使用字面量 `\n`,否则会原样显示在飞书卡片中。
### `issue` - 发送 GitHub Issue 通知
```bash
pnpm tsx scripts/feishu-notify.ts issue [options]
```
| 参数 | 短选项 | 说明 | 必需 |
|------|--------|------|------|
| `--url` | `-u` | GitHub Issue URL | 是 |
| `--number` | `-n` | Issue 编号 | 是 |
| `--title` | `-t` | Issue 标题 | 是 |
| `--summary` | `-m` | Issue 摘要 | 是 |
| `--author` | `-a` | Issue 作者 | 否(默认:"Unknown" |
| `--labels` | `-l` | Issue 标签(逗号分隔) | 否 |
#### 示例
```bash
pnpm tsx scripts/feishu-notify.ts issue \
-u "https://github.com/owner/repo/issues/123" \
-n "123" \
-t "Bug: Something is broken" \
-m "这是一个关于某功能的 bug 报告" \
-a "username" \
-l "bug,high-priority"
```
## 在 GitHub Actions 中使用
该脚本主要在 `.github/workflows/github-issue-tracker.yml` 工作流中使用:
```yaml
- name: Install dependencies
run: pnpm install
- name: Send notification
run: |
pnpm tsx scripts/feishu-notify.ts issue \
-u "${{ github.event.issue.html_url }}" \
-n "${{ github.event.issue.number }}" \
-t "${{ github.event.issue.title }}" \
-a "${{ github.event.issue.user.login }}" \
-l "${{ join(github.event.issue.labels.*.name, ',') }}" \
-m "Issue 摘要内容"
env:
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
```
## 飞书卡片消息格式
`issue` 命令发送的交互式卡片包含以下内容:
- **标题**: `#<issue编号> - <issue标题>`
- **作者**: Issue 创建者
- **标签**: Issue 标签列表(如有)
- **摘要**: Issue 内容摘要
- **操作按钮**: "View Issue" 按钮,点击跳转到 GitHub Issue 页面
## 配置飞书 Webhook
1. 在飞书群组中添加自定义机器人
2. 获取 Webhook URL 和签名密钥
3. 将 URL 和密钥配置到 GitHub Secrets
- `FEISHU_WEBHOOK_URL`: Webhook 地址
- `FEISHU_WEBHOOK_SECRET`: 签名密钥
## 错误处理
脚本在以下情况会返回非零退出码:
- 缺少必需的环境变量(`FEISHU_WEBHOOK_URL`、`FEISHU_WEBHOOK_SECRET`
- 缺少必需的命令参数
- 飞书 API 返回非 2xx 状态码
- 网络请求失败
## 扩展新命令
CLI 设计支持多种通知类型。添加新命令的步骤:
1. 定义命令选项接口
2. 创建卡片构建函数
3. 添加新的命令处理函数
4. 使用 `program.command()` 注册命令

View File

@ -143,36 +143,30 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
<!--LANG:en-->
Cherry Studio 1.7.10 - New Features & Bug Fixes
Cherry Studio 1.7.13 - Security & Bug Fixes
✨ New Features
- [MCP] Add MCP Hub with Auto mode for intelligent multi-server tool orchestration
🔒 Security
- [Plugin] Fix security vulnerability in DXT plugin system on Windows
🐛 Bug Fixes
- [Search] Fix Bing, Baidu, and Google search results not retrieving
- [Chat] Fix reasoning process not displaying correctly for some proxy models
- [Chat] Fix duplicate loading spinners on action buttons
- [Editor] Fix paragraph handle and plus button not clickable
- [Drawing] Fix TokenFlux models not showing in drawing panel
- [Translate] Fix translation stalling after initialization
- [Error] Fix app freeze when viewing error details with large images
- [Notes] Fix folder overlay blocking webview preview
- [Chat] Fix thinking time display when stopping generation
- [Agent] Fix Agent not working when Node.js is not installed on system
- [Chat] Fix app crash when opening certain agents
- [Chat] Fix reasoning process not displaying correctly for some providers
- [Chat] Fix memory leak issue during streaming conversations
- [MCP] Fix timeout field not accepting string format in MCP configuration
- [Settings] Add careers section in About page
<!--LANG:zh-CN-->
Cherry Studio 1.7.10 - 新功能与问题修复
Cherry Studio 1.7.13 - 安全与问题修复
✨ 新功能
- [MCP] 新增 MCP Hub 智能模式,可自动管理和调用多个 MCP 服务器工具
🔒 安全修复
- [插件] 修复 Windows 系统 DXT 插件的安全漏洞
🐛 问题修复
- [搜索] 修复必应、百度、谷歌搜索无法获取结果的问题
- [对话] 修复部分代理模型的推理过程无法正确显示的问题
- [对话] 修复操作按钮重复显示加载状态的问题
- [编辑器] 修复段落手柄和加号按钮无法点击的问题
- [绘图] 修复 TokenFlux 模型在绘图面板不显示的问题
- [翻译] 修复翻译功能初始化后卡住的问题
- [错误] 修复查看包含大图片的错误详情时应用卡死的问题
- [笔记] 修复文件夹遮挡网页预览的问题
- [对话] 修复停止生成时思考时间显示问题
- [Agent] 修复系统未安装 Node.js 时 Agent 功能无法使用的问题
- [对话] 修复打开某些智能体时应用崩溃的问题
- [对话] 修复部分服务商推理过程无法正确显示的问题
- [对话] 修复流式对话时的内存泄漏问题
- [MCP] 修复 MCP 配置的 timeout 字段不支持字符串格式的问题
- [设置] 关于页面新增招聘入口
<!--LANG:END-->

View File

@ -84,7 +84,7 @@ export default defineConfig([
{
selector: 'CallExpression[callee.object.name="console"]',
message:
'❗CherryStudio uses unified LoggerService: 📖 docs/technical/how-to-use-logger-en.md\n❗CherryStudio 使用统一的日志服务:📖 docs/technical/how-to-use-logger-zh.md\n\n'
'❗CherryStudio uses unified LoggerService: 📖 docs/en/guides/logging.md\n❗CherryStudio 使用统一的日志服务:📖 docs/zh/guides/logging.md\n\n'
}
]
}

View File

@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.7.10",
"version": "1.7.13",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@ -42,7 +42,7 @@
"i18n:check": "dotenv -e .env -- tsx scripts/check-i18n.ts",
"i18n:sync": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
"i18n:translate": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
"i18n:all": "pnpm i18n:check && pnpm i18n:sync && pnpm i18n:translate",
"i18n:all": "pnpm i18n:sync && pnpm i18n:translate",
"update:languages": "tsx scripts/update-languages.ts",
"update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts",
"test": "vitest run --silent",
@ -87,9 +87,6 @@
"turndown": "7.2.0"
},
"devDependencies": {
"js-yaml": "4.1.0",
"bonjour-service": "1.3.0",
"emoji-picker-element-data": "1",
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
@ -254,6 +251,7 @@
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"axios": "^1.7.3",
"bonjour-service": "1.3.0",
"browser-image-compression": "^2.0.2",
"builder-util-runtime": "9.5.0",
"chalk": "4.1.2",
@ -267,6 +265,7 @@
"code-inspector-plugin": "^0.20.14",
"codemirror-lang-mermaid": "0.5.0",
"color": "^5.0.0",
"commander": "^14.0.2",
"concurrently": "^9.2.1",
"cors": "2.8.5",
"country-flag-emoji-polyfill": "0.1.8",
@ -290,6 +289,7 @@
"electron-window-state": "^5.0.3",
"emittery": "^1.0.3",
"emoji-picker-element": "^1.22.1",
"emoji-picker-element-data": "1",
"epub": "1.3.0",
"eslint": "^9.22.0",
"eslint-plugin-import-zod": "^1.2.0",
@ -319,6 +319,7 @@
"jaison": "^2.0.2",
"jest-styled-components": "^7.2.0",
"js-base64": "3.7.7",
"js-yaml": "4.1.0",
"json-schema": "0.4.0",
"katex": "0.16.22",
"ky": "1.8.1",
@ -432,7 +433,8 @@
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3",
"@langchain/core": "1.0.2",
"@ai-sdk/openai-compatible@1.0.27": "1.0.28"
"@ai-sdk/openai-compatible@1.0.27": "1.0.28",
"@ai-sdk/openai-compatible@1.0.30": "1.0.28"
},
"patchedDependencies": {
"@napi-rs/system-ocr@1.0.2": "patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
@ -452,7 +454,9 @@
"file-stream-rotator@0.6.1": "patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
"libsql@0.4.7": "patches/libsql-npm-0.4.7-444e260fb1.patch",
"pdf-parse@1.1.1": "patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
"@ai-sdk/openai-compatible@1.0.28": "patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch"
"@ai-sdk/openai-compatible@1.0.28": "patches/@ai-sdk__openai-compatible@1.0.28.patch",
"@anthropic-ai/claude-agent-sdk@0.1.76": "patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch",
"@openrouter/ai-sdk-provider": "patches/@openrouter__ai-sdk-provider.patch"
},
"onlyBuiltDependencies": [
"@kangfenmao/keyv-storage",
@ -480,5 +484,27 @@
"*.{json,yml,yaml,css,html}": [
"biome format --write --no-errors-on-unmatched"
]
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3",
"@libsql/darwin-arm64": "0.4.7",
"@libsql/darwin-x64": "0.4.7",
"@libsql/linux-arm64-gnu": "0.4.7",
"@libsql/linux-x64-gnu": "0.4.7",
"@libsql/win32-x64-msvc": "0.4.7",
"@napi-rs/system-ocr-darwin-arm64": "1.0.2",
"@napi-rs/system-ocr-darwin-x64": "1.0.2",
"@napi-rs/system-ocr-win32-arm64-msvc": "1.0.2",
"@napi-rs/system-ocr-win32-x64-msvc": "1.0.2",
"@strongtz/win32-arm64-msvc": "0.4.7"
}
}

View File

@ -11,7 +11,7 @@ index 48e2f6263c6ee4c75d7e5c28733e64f6ebe92200..00d0729c4a3cbf9a48e8e1e962c7e2b2
type OpenAICompatibleProviderOptions = z.infer<typeof openaiCompatibleProviderOptions>;
diff --git a/dist/index.js b/dist/index.js
index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e5bfe0f9a 100644
index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..88349c614a69a268a2e4f3b157cb5e328ca1d347 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -41,7 +41,7 @@ function getOpenAIMetadata(message) {
@ -52,17 +52,38 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
...metadata
});
@@ -200,7 +208,8 @@ var openaiCompatibleProviderOptions = import_v4.z.object({
@@ -200,7 +208,9 @@ var openaiCompatibleProviderOptions = import_v4.z.object({
/**
* Controls the verbosity of the generated text. Defaults to `medium`.
*/
- textVerbosity: import_v4.z.string().optional()
+ textVerbosity: import_v4.z.string().optional(),
+ sendReasoning: import_v4.z.boolean().optional()
+ sendReasoning: import_v4.z.boolean().optional(),
+ strictJsonSchema: z.boolean().optional()
});
// src/openai-compatible-error.ts
@@ -378,7 +387,7 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -225,7 +235,8 @@ var defaultOpenAICompatibleErrorStructure = {
var import_provider2 = require("@ai-sdk/provider");
function prepareTools({
tools,
- toolChoice
+ toolChoice,
+ strictJsonSchema
}) {
tools = (tools == null ? void 0 : tools.length) ? tools : void 0;
const toolWarnings = [];
@@ -242,7 +253,8 @@ function prepareTools({
function: {
name: tool.name,
description: tool.description,
- parameters: tool.inputSchema
+ parameters: tool.inputSchema,
+ strict: strictJsonSchema
}
});
}
@@ -378,7 +390,7 @@ var OpenAICompatibleChatLanguageModel = class {
reasoning_effort: compatibleOptions.reasoningEffort,
verbosity: compatibleOptions.textVerbosity,
// messages:
@ -71,7 +92,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
// tools:
tools: openaiTools,
tool_choice: openaiToolChoice
@@ -421,6 +430,17 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -421,6 +433,17 @@ var OpenAICompatibleChatLanguageModel = class {
text: reasoning
});
}
@ -89,7 +110,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
if (choice.message.tool_calls != null) {
for (const toolCall of choice.message.tool_calls) {
content.push({
@@ -598,6 +618,17 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -598,6 +621,17 @@ var OpenAICompatibleChatLanguageModel = class {
delta: delta.content
});
}
@ -107,7 +128,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
if (delta.tool_calls != null) {
for (const toolCallDelta of delta.tool_calls) {
const index = toolCallDelta.index;
@@ -765,6 +796,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
@@ -765,6 +799,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
arguments: import_v43.z.string()
})
})
@ -122,7 +143,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
).nullish()
}),
finish_reason: import_v43.z.string().nullish()
@@ -795,6 +834,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
@@ -795,6 +837,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
arguments: import_v43.z.string().nullish()
})
})
@ -138,7 +159,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
}).nullish(),
finish_reason: import_v43.z.string().nullish()
diff --git a/dist/index.mjs b/dist/index.mjs
index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5700264de 100644
index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..fca65c04000ce4c01fb90e93326ac179c2378055 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -23,7 +23,7 @@ function getOpenAIMetadata(message) {
@ -179,17 +200,38 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
...metadata
});
@@ -182,7 +190,8 @@ var openaiCompatibleProviderOptions = z.object({
@@ -182,7 +190,9 @@ var openaiCompatibleProviderOptions = z.object({
/**
* Controls the verbosity of the generated text. Defaults to `medium`.
*/
- textVerbosity: z.string().optional()
+ textVerbosity: z.string().optional(),
+ sendReasoning: z.boolean().optional()
+ sendReasoning: z.boolean().optional(),
+ strictJsonSchema: z.boolean().optional()
});
// src/openai-compatible-error.ts
@@ -362,7 +371,7 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -209,7 +219,8 @@ import {
} from "@ai-sdk/provider";
function prepareTools({
tools,
- toolChoice
+ toolChoice,
+ strictJsonSchema
}) {
tools = (tools == null ? void 0 : tools.length) ? tools : void 0;
const toolWarnings = [];
@@ -226,7 +237,8 @@ function prepareTools({
function: {
name: tool.name,
description: tool.description,
- parameters: tool.inputSchema
+ parameters: tool.inputSchema,
+ strict: strictJsonSchema
}
});
}
@@ -362,7 +374,7 @@ var OpenAICompatibleChatLanguageModel = class {
reasoning_effort: compatibleOptions.reasoningEffort,
verbosity: compatibleOptions.textVerbosity,
// messages:
@ -198,7 +240,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
// tools:
tools: openaiTools,
tool_choice: openaiToolChoice
@@ -405,6 +414,17 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -405,6 +417,17 @@ var OpenAICompatibleChatLanguageModel = class {
text: reasoning
});
}
@ -216,7 +258,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
if (choice.message.tool_calls != null) {
for (const toolCall of choice.message.tool_calls) {
content.push({
@@ -582,6 +602,17 @@ var OpenAICompatibleChatLanguageModel = class {
@@ -582,6 +605,17 @@ var OpenAICompatibleChatLanguageModel = class {
delta: delta.content
});
}
@ -234,7 +276,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
if (delta.tool_calls != null) {
for (const toolCallDelta of delta.tool_calls) {
const index = toolCallDelta.index;
@@ -749,6 +780,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
@@ -749,6 +783,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
arguments: z3.string()
})
})
@ -249,7 +291,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
).nullish()
}),
finish_reason: z3.string().nullish()
@@ -779,6 +818,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
@@ -779,6 +821,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
arguments: z3.string().nullish()
})
})

View File

@ -0,0 +1,33 @@
diff --git a/sdk.mjs b/sdk.mjs
index 1e1c3e4e3f81db622fb2789d17f3d421f212306e..5d193cdb6a43c7799fd5eff2d8af80827bfbdf1e 100755
--- a/sdk.mjs
+++ b/sdk.mjs
@@ -11985,7 +11985,7 @@ function createAbortController(maxListeners = DEFAULT_MAX_LISTENERS) {
}
// ../src/transport/ProcessTransport.ts
-import { spawn } from "child_process";
+import { fork } from "child_process";
import { createInterface } from "readline";
// ../src/utils/fsOperations.ts
@@ -12999,14 +12999,14 @@ class ProcessTransport {
return isRunningWithBun() ? "bun" : "node";
}
spawnLocalProcess(spawnOptions) {
- const { command, args, cwd: cwd2, env, signal } = spawnOptions;
+ const { args, cwd: cwd2, env, signal } = spawnOptions;
const stderrMode = env.DEBUG_CLAUDE_AGENT_SDK || this.options.stderr ? "pipe" : "ignore";
- const childProcess = spawn(command, args, {
+ logForSdkDebugging(`Forking Claude Code Node.js process: ${args[0]} ${args.slice(1).join(" ")}`);
+ const childProcess = fork(args[0], args.slice(1), {
cwd: cwd2,
- stdio: ["pipe", "pipe", stderrMode],
+ stdio: stderrMode === "pipe" ? ["pipe", "pipe", "pipe", "ipc"] : ["pipe", "pipe", "ignore", "ipc"],
signal,
- env,
- windowsHide: true
+ env
});
if (env.DEBUG_CLAUDE_AGENT_SDK || this.options.stderr) {
childProcess.stderr.on("data", (data) => {

View File

@ -0,0 +1,140 @@
diff --git a/dist/index.js b/dist/index.js
index f33510a50d11a2cb92a90ea70cc0ac84c89f29b9..db0af7e2cc05c47baeb29c0a3974a155316fbd05 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1050,7 +1050,8 @@ var OpenRouterProviderMetadataSchema = import_v43.z.object({
var OpenRouterProviderOptionsSchema = import_v43.z.object({
openrouter: import_v43.z.object({
reasoning_details: import_v43.z.array(ReasoningDetailUnionSchema).optional(),
- annotations: import_v43.z.array(FileAnnotationSchema).optional()
+ annotations: import_v43.z.array(FileAnnotationSchema).optional(),
+ strictJsonSchema: import_v43.z.boolean().optional()
}).optional()
}).optional();
@@ -1658,7 +1659,8 @@ var OpenRouterChatLanguageModel = class {
responseFormat,
topK,
tools,
- toolChoice
+ toolChoice,
+ providerOptions
}) {
var _a15;
const baseArgs = __spreadValues(__spreadValues({
@@ -1712,7 +1714,8 @@ var OpenRouterChatLanguageModel = class {
function: {
name: tool.name,
description: tool.description,
- parameters: tool.inputSchema
+ parameters: tool.inputSchema,
+ strict: providerOptions?.openrouter?.strictJsonSchema
}
}));
return __spreadProps(__spreadValues({}, baseArgs), {
@@ -1725,7 +1728,7 @@ var OpenRouterChatLanguageModel = class {
async doGenerate(options) {
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: responseValue, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -1931,7 +1934,7 @@ var OpenRouterChatLanguageModel = class {
async doStream(options) {
var _a15;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -2564,7 +2567,7 @@ var OpenRouterCompletionLanguageModel = class {
async doGenerate(options) {
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -2623,7 +2626,7 @@ var OpenRouterCompletionLanguageModel = class {
}
async doStream(options) {
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({
diff --git a/dist/index.mjs b/dist/index.mjs
index 8a688331b88b4af738ee4ca8062b5f24124d3d81..a2aa299a44352addc26f8891d839ea31a2150ee2 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1015,7 +1015,8 @@ var OpenRouterProviderMetadataSchema = z3.object({
var OpenRouterProviderOptionsSchema = z3.object({
openrouter: z3.object({
reasoning_details: z3.array(ReasoningDetailUnionSchema).optional(),
- annotations: z3.array(FileAnnotationSchema).optional()
+ annotations: z3.array(FileAnnotationSchema).optional(),
+ strictJsonSchema: z3.boolean().optional()
}).optional()
}).optional();
@@ -1623,7 +1624,8 @@ var OpenRouterChatLanguageModel = class {
responseFormat,
topK,
tools,
- toolChoice
+ toolChoice,
+ providerOptions
}) {
var _a15;
const baseArgs = __spreadValues(__spreadValues({
@@ -1677,7 +1679,8 @@ var OpenRouterChatLanguageModel = class {
function: {
name: tool.name,
description: tool.description,
- parameters: tool.inputSchema
+ parameters: tool.inputSchema,
+ strict: providerOptions?.openrouter?.strictJsonSchema
}
}));
return __spreadProps(__spreadValues({}, baseArgs), {
@@ -1690,7 +1693,7 @@ var OpenRouterChatLanguageModel = class {
async doGenerate(options) {
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: responseValue, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -1896,7 +1899,7 @@ var OpenRouterChatLanguageModel = class {
async doStream(options) {
var _a15;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -2529,7 +2532,7 @@ var OpenRouterCompletionLanguageModel = class {
async doGenerate(options) {
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({
@@ -2588,7 +2591,7 @@ var OpenRouterCompletionLanguageModel = class {
}
async doStream(options) {
const providerOptions = options.providerOptions || {};
- const openrouterOptions = providerOptions.openrouter || {};
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
const { value: response, responseHeaders } = await postJsonToApi({
url: this.config.url({

295
pnpm-lock.yaml generated
View File

@ -23,17 +23,21 @@ overrides:
'@img/sharp-win32-x64': 0.34.3
'@langchain/core': 1.0.2
'@ai-sdk/openai-compatible@1.0.27': 1.0.28
'@ai-sdk/openai-compatible@1.0.30': 1.0.28
patchedDependencies:
'@ai-sdk/google@2.0.49':
hash: 279e9d43f675e4b979b32b78954dd37acc3026aa36ae2dd7701b5bad2f061522
path: patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch
'@ai-sdk/openai-compatible@1.0.28':
hash: 66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1
path: patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch
hash: 5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda
path: patches/@ai-sdk__openai-compatible@1.0.28.patch
'@ai-sdk/openai@2.0.85':
hash: f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b
path: patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch
'@anthropic-ai/claude-agent-sdk@0.1.76':
hash: e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5
path: patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch
'@anthropic-ai/vertex-sdk@0.11.4':
hash: 12e3275df5632dfe717d4db64df70e9b0128dfac86195da27722effe4749662f
path: patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch
@ -49,6 +53,9 @@ patchedDependencies:
'@napi-rs/system-ocr@1.0.2':
hash: aa1a73e445ee644774745b620589bb99d85bee6c95cc2a91fe9137e580da5bde
path: patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch
'@openrouter/ai-sdk-provider':
hash: 508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52
path: patches/@openrouter__ai-sdk-provider.patch
'@tiptap/extension-drag-handle@3.2.0':
hash: 8432665d4553fb9ba8ff2a126a9181c3ccfee06ae57688aa14f65aa560e52fce
path: patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch
@ -86,7 +93,7 @@ importers:
dependencies:
'@anthropic-ai/claude-agent-sdk':
specifier: 0.1.76
version: 0.1.76(zod@4.3.4)
version: 0.1.76(patch_hash=e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5)(zod@4.3.4)
'@libsql/client':
specifier: 0.14.0
version: 0.14.0
@ -354,7 +361,7 @@ importers:
version: 2.3.0(encoding@0.1.13)
'@openrouter/ai-sdk-provider':
specifier: ^1.2.8
version: 1.5.4(ai@5.0.117(zod@4.3.4))(zod@4.3.4)
version: 1.5.4(patch_hash=508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52)(ai@5.0.117(zod@4.3.4))(zod@4.3.4)
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.0
@ -673,6 +680,9 @@ importers:
color:
specifier: ^5.0.0
version: 5.0.3
commander:
specifier: ^14.0.2
version: 14.0.2
concurrently:
specifier: ^9.2.1
version: 9.2.1
@ -1114,6 +1124,67 @@ importers:
zod:
specifier: ^4.1.5
version: 4.3.4
optionalDependencies:
'@img/sharp-darwin-arm64':
specifier: 0.34.3
version: 0.34.3
'@img/sharp-darwin-x64':
specifier: 0.34.3
version: 0.34.3
'@img/sharp-libvips-darwin-arm64':
specifier: 1.2.0
version: 1.2.0
'@img/sharp-libvips-darwin-x64':
specifier: 1.2.0
version: 1.2.0
'@img/sharp-libvips-linux-arm64':
specifier: 1.2.0
version: 1.2.0
'@img/sharp-libvips-linux-x64':
specifier: 1.2.0
version: 1.2.0
'@img/sharp-linux-arm64':
specifier: 0.34.3
version: 0.34.3
'@img/sharp-linux-x64':
specifier: 0.34.3
version: 0.34.3
'@img/sharp-win32-arm64':
specifier: 0.34.3
version: 0.34.3
'@img/sharp-win32-x64':
specifier: 0.34.3
version: 0.34.3
'@libsql/darwin-arm64':
specifier: 0.4.7
version: 0.4.7
'@libsql/darwin-x64':
specifier: 0.4.7
version: 0.4.7
'@libsql/linux-arm64-gnu':
specifier: 0.4.7
version: 0.4.7
'@libsql/linux-x64-gnu':
specifier: 0.4.7
version: 0.4.7
'@libsql/win32-x64-msvc':
specifier: 0.4.7
version: 0.4.7
'@napi-rs/system-ocr-darwin-arm64':
specifier: 1.0.2
version: 1.0.2
'@napi-rs/system-ocr-darwin-x64':
specifier: 1.0.2
version: 1.0.2
'@napi-rs/system-ocr-win32-arm64-msvc':
specifier: 1.0.2
version: 1.0.2
'@napi-rs/system-ocr-win32-x64-msvc':
specifier: 1.0.2
version: 1.0.2
'@strongtz/win32-arm64-msvc':
specifier: 0.4.7
version: 0.4.7
packages/ai-sdk-provider:
dependencies:
@ -1128,7 +1199,7 @@ importers:
version: 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.5)
'@ai-sdk/openai-compatible':
specifier: 1.0.28
version: 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.5)
version: 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.5)
'@ai-sdk/provider':
specifier: ^2.0.0
version: 2.0.1
@ -1168,7 +1239,7 @@ importers:
version: 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)
'@ai-sdk/openai-compatible':
specifier: 1.0.28
version: 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
version: 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider':
specifier: ^2.0.0
version: 2.0.1
@ -1331,12 +1402,6 @@ packages:
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai-compatible@1.0.30':
resolution: {integrity: sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/openai@2.0.85':
resolution: {integrity: sha512-3pzr7qVhsOXwjPAfmvFNZz3sRWCuyMOc3GgLHe7sWY0t8J4hA5mwQ4LISTKYI3iIr8IXzAQn9MUrC8Hiji9RpA==}
engines: {node: '>=18'}
@ -3473,6 +3538,9 @@ packages:
'@oxc-project/types@0.106.0':
resolution: {integrity: sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==}
'@oxc-project/types@0.107.0':
resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==}
'@oxlint-tsgolint/darwin-arm64@0.2.1':
resolution: {integrity: sha512-1cjcZkgpkWTfkFx111weVsvlnDG3+a7Y3qei+VCbr55LwKTHSzGq9tVNakIHRJ6NZfX7bQKYU4yef3p6AKoteg==}
cpu: [arm64]
@ -3979,6 +4047,12 @@ packages:
cpu: [arm64]
os: [android]
'@rolldown/binding-android-arm64@1.0.0-beta.59':
resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -3991,6 +4065,12 @@ packages:
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-arm64@1.0.0-beta.59':
resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-beta.53':
resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4003,6 +4083,12 @@ packages:
cpu: [x64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-beta.59':
resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-beta.53':
resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4015,6 +4101,12 @@ packages:
cpu: [x64]
os: [freebsd]
'@rolldown/binding-freebsd-x64@1.0.0-beta.59':
resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53':
resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4027,6 +4119,12 @@ packages:
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59':
resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53':
resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4039,6 +4137,12 @@ packages:
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59':
resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4051,6 +4155,12 @@ packages:
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.59':
resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4063,6 +4173,12 @@ packages:
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.59':
resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4075,6 +4191,12 @@ packages:
cpu: [x64]
os: [linux]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.59':
resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4087,6 +4209,12 @@ packages:
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.59':
resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-beta.53':
resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==}
engines: {node: '>=14.0.0'}
@ -4097,6 +4225,11 @@ packages:
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-wasm32-wasi@1.0.0-beta.59':
resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53':
resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4109,6 +4242,12 @@ packages:
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59':
resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.53':
resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -4121,6 +4260,12 @@ packages:
cpu: [x64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.59':
resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-beta.27':
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
@ -4130,6 +4275,9 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.58':
resolution: {integrity: sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==}
'@rolldown/pluginutils@1.0.0-beta.59':
resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==}
'@rollup/rollup-linux-x64-gnu@4.45.1':
resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==}
cpu: [x64]
@ -4403,6 +4551,11 @@ packages:
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@strongtz/win32-arm64-msvc@0.4.7':
resolution: {integrity: sha512-LMU0CVfKGxKzIUawkDw6pwCNFb64DSwqEQhVAfEdLGMZcvVIdJghWB9yMW+72DHzALpwHscIssAJGhYAT4AbfA==}
cpu: [arm64]
os: [win32]
'@svta/common-media-library@0.17.4':
resolution: {integrity: sha512-nP/KThzQW5FZKdc9V7ICTa9/A7xGw66VQoLPYOEwwMZTTrISp1zIQAX4KAYJw2PN/VPnxJQJXIYbzZTXgMHctw==}
engines: {node: '>=20'}
@ -6225,6 +6378,10 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@ -10652,6 +10809,11 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rolldown@1.0.0-beta.59:
resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rollup-plugin-visualizer@5.14.0:
resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==}
engines: {node: '>=18'}
@ -12156,7 +12318,7 @@ snapshots:
'@ai-sdk/cerebras@1.0.34(zod@4.3.4)':
dependencies:
'@ai-sdk/openai-compatible': 1.0.30(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
zod: 4.3.4
@ -12212,7 +12374,7 @@ snapshots:
'@ai-sdk/huggingface@0.0.10(zod@4.3.4)':
dependencies:
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.17(zod@4.3.4)
zod: 4.3.4
@ -12223,24 +12385,18 @@ snapshots:
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/openai-compatible@1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)':
'@ai-sdk/openai-compatible@1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/openai-compatible@1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.5)':
'@ai-sdk/openai-compatible@1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.5)':
dependencies:
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.18(zod@4.3.5)
zod: 4.3.5
'@ai-sdk/openai-compatible@1.0.30(zod@4.3.4)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/openai@2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)':
dependencies:
'@ai-sdk/provider': 2.0.0
@ -12335,14 +12491,14 @@ snapshots:
'@ai-sdk/xai@2.0.36(zod@4.3.4)':
dependencies:
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider': 2.0.0
'@ai-sdk/provider-utils': 3.0.17(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/xai@2.0.43(zod@4.3.4)':
dependencies:
'@ai-sdk/openai-compatible': 1.0.30(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
zod: 4.3.4
@ -12412,7 +12568,7 @@ snapshots:
package-manager-detector: 1.6.0
tinyexec: 1.0.2
'@anthropic-ai/claude-agent-sdk@0.1.76(zod@4.3.4)':
'@anthropic-ai/claude-agent-sdk@0.1.76(patch_hash=e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5)(zod@4.3.4)':
dependencies:
zod: 4.3.4
optionalDependencies:
@ -14995,7 +15151,7 @@ snapshots:
'@open-draft/until@2.1.0': {}
'@openrouter/ai-sdk-provider@1.5.4(ai@5.0.117(zod@4.3.4))(zod@4.3.4)':
'@openrouter/ai-sdk-provider@1.5.4(patch_hash=508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52)(ai@5.0.117(zod@4.3.4))(zod@4.3.4)':
dependencies:
'@openrouter/sdk': 0.1.27
ai: 5.0.117(zod@4.3.4)
@ -15112,7 +15268,7 @@ snapshots:
'@opeoginni/github-copilot-openai-compatible@0.1.22(zod@4.3.4)':
dependencies:
'@ai-sdk/openai': 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
'@ai-sdk/provider': 2.1.0-beta.5
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
transitivePeerDependencies:
@ -15124,6 +15280,8 @@ snapshots:
'@oxc-project/types@0.106.0': {}
'@oxc-project/types@0.107.0': {}
'@oxlint-tsgolint/darwin-arm64@0.2.1':
optional: true
@ -15573,60 +15731,90 @@ snapshots:
'@rolldown/binding-android-arm64@1.0.0-beta.58':
optional: true
'@rolldown/binding-android-arm64@1.0.0-beta.59':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.53':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.58':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-beta.59':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.53':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.58':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-beta.59':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.53':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.58':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-beta.59':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.58':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.59':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.58':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.59':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.58':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-beta.59':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.58':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-beta.59':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.53':
dependencies:
'@napi-rs/wasm-runtime': 1.1.1
@ -15637,24 +15825,37 @@ snapshots:
'@napi-rs/wasm-runtime': 1.1.1
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-beta.59':
dependencies:
'@napi-rs/wasm-runtime': 1.1.1
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.53':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.58':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.59':
optional: true
'@rolldown/pluginutils@1.0.0-beta.27': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rolldown/pluginutils@1.0.0-beta.58': {}
'@rolldown/pluginutils@1.0.0-beta.59': {}
'@rollup/rollup-linux-x64-gnu@4.45.1':
optional: true
@ -16054,6 +16255,9 @@ snapshots:
'@standard-schema/utils@0.3.0': {}
'@strongtz/win32-arm64-msvc@0.4.7':
optional: true
'@svta/common-media-library@0.17.4': {}
'@swc/core-darwin-arm64@1.15.8':
@ -18459,6 +18663,8 @@ snapshots:
commander@13.1.0: {}
commander@14.0.2: {}
commander@2.20.3: {}
commander@5.1.0: {}
@ -23676,7 +23882,7 @@ snapshots:
- oxc-resolver
- supports-color
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.8.3):
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.8.3):
dependencies:
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
@ -23686,7 +23892,7 @@ snapshots:
debug: 4.4.3
dts-resolver: 2.1.3
get-tsconfig: 4.13.0
rolldown: 1.0.0-beta.58
rolldown: 1.0.0-beta.59
optionalDependencies:
'@typescript/native-preview': 7.0.0-dev.20260104.1
typescript: 5.8.3
@ -23694,7 +23900,7 @@ snapshots:
- oxc-resolver
- supports-color
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.9.2):
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.9.2):
dependencies:
'@babel/generator': 7.28.5
'@babel/parser': 7.28.5
@ -23704,7 +23910,7 @@ snapshots:
debug: 4.4.3
dts-resolver: 2.1.3
get-tsconfig: 4.13.0
rolldown: 1.0.0-beta.58
rolldown: 1.0.0-beta.59
optionalDependencies:
'@typescript/native-preview': 7.0.0-dev.20260104.1
typescript: 5.9.2
@ -23784,6 +23990,25 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58
rolldown@1.0.0-beta.59:
dependencies:
'@oxc-project/types': 0.107.0
'@rolldown/pluginutils': 1.0.0-beta.59
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-beta.59
'@rolldown/binding-darwin-arm64': 1.0.0-beta.59
'@rolldown/binding-darwin-x64': 1.0.0-beta.59
'@rolldown/binding-freebsd-x64': 1.0.0-beta.59
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.59
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.59
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.59
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59
rollup-plugin-visualizer@5.14.0:
dependencies:
open: 8.4.2
@ -24600,8 +24825,8 @@ snapshots:
diff: 8.0.2
empathic: 2.0.0
hookable: 5.5.3
rolldown: 1.0.0-beta.58
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.8.3)
rolldown: 1.0.0-beta.59
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.8.3)
semver: 7.7.3
tinyexec: 1.0.2
tinyglobby: 0.2.15
@ -24624,8 +24849,8 @@ snapshots:
diff: 8.0.2
empathic: 2.0.0
hookable: 5.5.3
rolldown: 1.0.0-beta.58
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.9.2)
rolldown: 1.0.0-beta.59
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.9.2)
semver: 7.7.3
tinyexec: 1.0.2
tinyglobby: 0.2.15

View File

@ -1,2 +1,8 @@
packages:
- 'packages/*'
supportedArchitectures:
os:
- current
cpu:
- current

View File

@ -1,42 +1,35 @@
const { Arch } = require('electron-builder')
const { downloadNpmPackage } = require('./utils')
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const workspaceConfigPath = path.join(__dirname, '..', 'pnpm-workspace.yaml')
// if you want to add new prebuild binaries packages with different architectures, you can add them here
// please add to allX64 and allArm64 from pnpm-lock.yaml
const allArm64 = {
'@img/sharp-darwin-arm64': '0.34.3',
'@img/sharp-win32-arm64': '0.34.3',
'@img/sharp-linux-arm64': '0.34.3',
'@img/sharp-libvips-darwin-arm64': '1.2.4',
'@img/sharp-libvips-linux-arm64': '1.2.4',
'@libsql/darwin-arm64': '0.4.7',
'@libsql/linux-arm64-gnu': '0.4.7',
'@strongtz/win32-arm64-msvc': '0.4.7',
'@napi-rs/system-ocr-darwin-arm64': '1.0.2',
'@napi-rs/system-ocr-win32-arm64-msvc': '1.0.2'
}
const allX64 = {
'@img/sharp-darwin-x64': '0.34.3',
'@img/sharp-linux-x64': '0.34.3',
'@img/sharp-win32-x64': '0.34.3',
'@img/sharp-libvips-darwin-x64': '1.2.4',
'@img/sharp-libvips-linux-x64': '1.2.4',
'@libsql/darwin-x64': '0.4.7',
'@libsql/linux-x64-gnu': '0.4.7',
'@libsql/win32-x64-msvc': '0.4.7',
'@napi-rs/system-ocr-darwin-x64': '1.0.2',
'@napi-rs/system-ocr-win32-x64-msvc': '1.0.2'
}
const claudeCodeVenderPath = '@anthropic-ai/claude-agent-sdk/vendor'
const claudeCodeVenders = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
const packages = [
'@img/sharp-darwin-arm64',
'@img/sharp-darwin-x64',
'@img/sharp-linux-arm64',
'@img/sharp-linux-x64',
'@img/sharp-win32-arm64',
'@img/sharp-win32-x64',
'@img/sharp-libvips-darwin-arm64',
'@img/sharp-libvips-darwin-x64',
'@img/sharp-libvips-linux-arm64',
'@img/sharp-libvips-linux-x64',
'@libsql/darwin-arm64',
'@libsql/darwin-x64',
'@libsql/linux-arm64-gnu',
'@libsql/linux-x64-gnu',
'@libsql/win32-x64-msvc',
'@napi-rs/system-ocr-darwin-arm64',
'@napi-rs/system-ocr-darwin-x64',
'@napi-rs/system-ocr-win32-arm64-msvc',
'@napi-rs/system-ocr-win32-x64-msvc',
'@strongtz/win32-arm64-msvc'
]
const platformToArch = {
mac: 'darwin',
@ -45,61 +38,82 @@ const platformToArch = {
}
exports.default = async function (context) {
const arch = context.arch
const archType = arch === Arch.arm64 ? 'arm64' : 'x64'
const platform = context.packager.platform.name
const arch = context.arch === Arch.arm64 ? 'arm64' : 'x64'
const platformName = context.packager.platform.name
const platform = platformToArch[platformName]
const downloadPackages = async (packages) => {
console.log('downloading packages ......')
const downloadPromises = []
for (const name of Object.keys(packages)) {
if (name.includes(`${platformToArch[platform]}`) && name.includes(`-${archType}`)) {
downloadPromises.push(
downloadNpmPackage(
name,
`https://registry.npmjs.org/${name}/-/${name.split('/').pop()}-${packages[name]}.tgz`
)
)
}
const downloadPackages = async () => {
// Skip if target platform and architecture match current system
if (platform === process.platform && arch === process.arch) {
console.log(`Skipping install: target (${platform}/${arch}) matches current system`)
return
}
await Promise.all(downloadPromises)
console.log(`Installing packages for target platform=${platform} arch=${arch}...`)
// Backup and modify pnpm-workspace.yaml to add target platform support
const originalWorkspaceConfig = fs.readFileSync(workspaceConfigPath, 'utf-8')
const workspaceConfig = yaml.load(originalWorkspaceConfig)
// Add target platform to supportedArchitectures.os
if (!workspaceConfig.supportedArchitectures.os.includes(platform)) {
workspaceConfig.supportedArchitectures.os.push(platform)
}
// Add target architecture to supportedArchitectures.cpu
if (!workspaceConfig.supportedArchitectures.cpu.includes(arch)) {
workspaceConfig.supportedArchitectures.cpu.push(arch)
}
const modifiedWorkspaceConfig = yaml.dump(workspaceConfig)
console.log('Modified workspace config:', modifiedWorkspaceConfig)
fs.writeFileSync(workspaceConfigPath, modifiedWorkspaceConfig)
try {
execSync(`pnpm install`, { stdio: 'inherit' })
} finally {
// Restore original pnpm-workspace.yaml
fs.writeFileSync(workspaceConfigPath, originalWorkspaceConfig)
}
}
const changeFilters = async (filtersToExclude, filtersToInclude) => {
// remove filters for the target architecture (allow inclusion)
let filters = context.packager.config.files[0].filter
filters = filters.filter((filter) => !filtersToInclude.includes(filter))
await downloadPackages()
const excludePackages = async (packagesToExclude) => {
// 从项目根目录的 electron-builder.yml 读取 files 配置,避免多次覆盖配置导致出错
const electronBuilderConfigPath = path.join(__dirname, '..', 'electron-builder.yml')
const electronBuilderConfig = yaml.load(fs.readFileSync(electronBuilderConfigPath, 'utf-8'))
let filters = electronBuilderConfig.files
// add filters for other architectures (exclude them)
filters.push(...filtersToExclude)
filters.push(...packagesToExclude)
context.packager.config.files[0].filter = filters
}
await downloadPackages(arch === Arch.arm64 ? allArm64 : allX64)
const arm64KeepPackages = packages.filter((p) => p.includes('arm64') && p.includes(platform))
const arm64ExcludePackages = packages
.filter((p) => !arm64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
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 x64KeepPackages = packages.filter((p) => p.includes('x64') && p.includes(platform))
const x64ExcludePackages = packages
.filter((p) => !x64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
const includeClaudeCodeFilters = [
'!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + `${archType}-${platformToArch[platform]}/**`
]
const excludeRipgrepFilters = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
.filter((f) => {
// On Windows ARM64, also keep x64-win32 for emulation compatibility
if (platform === 'win32' && context.arch === Arch.arm64 && f === 'x64-win32') {
return false
}
return f !== `${arch}-${platform}`
})
.map((f) => '!node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep/' + f + '/**')
if (arch === Arch.arm64) {
await changeFilters(
[...x64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...arm64Filters, ...includeClaudeCodeFilters]
)
if (context.arch === Arch.arm64) {
await excludePackages([...arm64ExcludePackages, ...excludeRipgrepFilters])
} else {
await changeFilters(
[...arm64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...x64Filters, ...includeClaudeCodeFilters]
)
await excludePackages([...x64ExcludePackages, ...excludeRipgrepFilters])
}
}

View File

@ -1,211 +0,0 @@
/**
* Feishu (Lark) Webhook Notification Script
* Sends GitHub issue summaries to Feishu with signature verification
*/
const crypto = require('crypto')
const https = require('https')
/**
* Generate Feishu webhook signature
* @param {string} secret - Feishu webhook secret
* @param {number} timestamp - Unix timestamp in seconds
* @returns {string} Base64 encoded signature
*/
function generateSignature(secret, timestamp) {
const stringToSign = `${timestamp}\n${secret}`
const hmac = crypto.createHmac('sha256', stringToSign)
return hmac.digest('base64')
}
/**
* Send message to Feishu webhook
* @param {string} webhookUrl - Feishu webhook URL
* @param {string} secret - Feishu webhook secret
* @param {object} content - Message content
* @returns {Promise<void>}
*/
function sendToFeishu(webhookUrl, secret, content) {
return new Promise((resolve, reject) => {
const timestamp = Math.floor(Date.now() / 1000)
const sign = generateSignature(secret, timestamp)
const payload = JSON.stringify({
timestamp: timestamp.toString(),
sign: sign,
msg_type: 'interactive',
card: content
})
const url = new URL(webhookUrl)
const options = {
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
}
}
const req = https.request(options, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
console.log('✅ Successfully sent to Feishu:', data)
resolve()
} else {
reject(new Error(`Feishu API error: ${res.statusCode} - ${data}`))
}
})
})
req.on('error', (error) => {
reject(error)
})
req.write(payload)
req.end()
})
}
/**
* Create Feishu card message from issue data
* @param {object} issueData - GitHub issue data
* @returns {object} Feishu card content
*/
function createIssueCard(issueData) {
const { issueUrl, issueNumber, issueTitle, issueSummary, issueAuthor, labels } = issueData
// Build labels section if labels exist
const labelElements =
labels && labels.length > 0
? labels.map((label) => ({
tag: 'markdown',
content: `\`${label}\``
}))
: []
return {
elements: [
{
tag: 'div',
text: {
tag: 'lark_md',
content: `**👤 Author:** ${issueAuthor}`
}
},
...(labelElements.length > 0
? [
{
tag: 'div',
text: {
tag: 'lark_md',
content: `**🏷️ Labels:** ${labels.join(', ')}`
}
}
]
: []),
{
tag: 'hr'
},
{
tag: 'div',
text: {
tag: 'lark_md',
content: `**📋 Summary:**\n${issueSummary}`
}
},
{
tag: 'hr'
},
{
tag: 'action',
actions: [
{
tag: 'button',
text: {
tag: 'plain_text',
content: '🔗 View Issue'
},
type: 'primary',
url: issueUrl
}
]
}
],
header: {
template: 'blue',
title: {
tag: 'plain_text',
content: `#${issueNumber} - ${issueTitle}`
}
}
}
}
/**
* Main function
*/
async function main() {
try {
// Get environment variables
const webhookUrl = process.env.FEISHU_WEBHOOK_URL
const secret = process.env.FEISHU_WEBHOOK_SECRET
const issueUrl = process.env.ISSUE_URL
const issueNumber = process.env.ISSUE_NUMBER
const issueTitle = process.env.ISSUE_TITLE
const issueSummary = process.env.ISSUE_SUMMARY
const issueAuthor = process.env.ISSUE_AUTHOR
const labelsStr = process.env.ISSUE_LABELS || ''
// Validate required environment variables
if (!webhookUrl) {
throw new Error('FEISHU_WEBHOOK_URL environment variable is required')
}
if (!secret) {
throw new Error('FEISHU_WEBHOOK_SECRET environment variable is required')
}
if (!issueUrl || !issueNumber || !issueTitle || !issueSummary) {
throw new Error('Issue data environment variables are required')
}
// Parse labels
const labels = labelsStr
? labelsStr
.split(',')
.map((l) => l.trim())
.filter(Boolean)
: []
// Create issue data object
const issueData = {
issueUrl,
issueNumber,
issueTitle,
issueSummary,
issueAuthor: issueAuthor || 'Unknown',
labels
}
// Create card content
const card = createIssueCard(issueData)
console.log('📤 Sending notification to Feishu...')
console.log(`Issue #${issueNumber}: ${issueTitle}`)
// Send to Feishu
await sendToFeishu(webhookUrl, secret, card)
console.log('✅ Notification sent successfully!')
} catch (error) {
console.error('❌ Error:', error.message)
process.exit(1)
}
}
// Run main function
main()

421
scripts/feishu-notify.ts Normal file
View File

@ -0,0 +1,421 @@
#!/usr/bin/env npx tsx
/**
* @fileoverview Feishu (Lark) Webhook Notification CLI Tool
* @description Sends notifications to Feishu with signature verification.
* Supports subcommands for different notification types.
* @module feishu-notify
* @example
* // Send GitHub issue notification
* pnpm tsx feishu-notify.ts issue -u "https://..." -n "123" -t "Title" -m "Summary"
*
* // Using environment variables for credentials
* FEISHU_WEBHOOK_URL="..." FEISHU_WEBHOOK_SECRET="..." pnpm tsx feishu-notify.ts issue ...
*/
import { Command } from 'commander'
import crypto from 'crypto'
import dotenv from 'dotenv'
import https from 'https'
import * as z from 'zod'
// Load environment variables from .env file
dotenv.config()
/** CLI tool version */
const VERSION = '1.0.0'
/** GitHub issue data structure */
interface IssueData {
/** GitHub issue URL */
issueUrl: string
/** Issue number */
issueNumber: string
/** Issue title */
issueTitle: string
/** Issue summary/description */
issueSummary: string
/** Issue author username */
issueAuthor: string
/** Issue labels */
labels: string[]
}
/** Feishu card text element */
interface FeishuTextElement {
tag: 'div'
text: {
tag: 'lark_md'
content: string
}
}
/** Feishu card horizontal rule element */
interface FeishuHrElement {
tag: 'hr'
}
/** Feishu card action button */
interface FeishuActionElement {
tag: 'action'
actions: Array<{
tag: 'button'
text: {
tag: 'plain_text'
content: string
}
type: 'primary' | 'default'
url: string
}>
}
/** Feishu card element union type */
type FeishuCardElement = FeishuTextElement | FeishuHrElement | FeishuActionElement
/** Zod schema for Feishu header color template */
const FeishuHeaderTemplateSchema = z.enum([
'blue',
'wathet',
'turquoise',
'green',
'yellow',
'orange',
'red',
'carmine',
'violet',
'purple',
'indigo',
'grey',
'default'
])
/** Feishu card header color template (inferred from schema) */
type FeishuHeaderTemplate = z.infer<typeof FeishuHeaderTemplateSchema>
/** Feishu interactive card structure */
interface FeishuCard {
elements: FeishuCardElement[]
header: {
template: FeishuHeaderTemplate
title: {
tag: 'plain_text'
content: string
}
}
}
/** Feishu webhook request payload */
interface FeishuPayload {
timestamp: string
sign: string
msg_type: 'interactive'
card: FeishuCard
}
/** Issue subcommand options */
interface IssueOptions {
url: string
number: string
title: string
summary: string
author?: string
labels?: string
}
/** Send subcommand options */
interface SendOptions {
title: string
description: string
color?: string
}
/**
* Generate Feishu webhook signature using HMAC-SHA256
* @param secret - Feishu webhook secret
* @param timestamp - Unix timestamp in seconds
* @returns Base64 encoded signature
*/
function generateSignature(secret: string, timestamp: number): string {
const stringToSign = `${timestamp}\n${secret}`
const hmac = crypto.createHmac('sha256', stringToSign)
return hmac.digest('base64')
}
/**
* Send message to Feishu webhook
* @param webhookUrl - Feishu webhook URL
* @param secret - Feishu webhook secret
* @param content - Feishu card message content
* @returns Resolves when message is sent successfully
* @throws When Feishu API returns non-2xx status code or network error occurs
*/
function sendToFeishu(webhookUrl: string, secret: string, content: FeishuCard): Promise<void> {
return new Promise((resolve, reject) => {
const timestamp = Math.floor(Date.now() / 1000)
const sign = generateSignature(secret, timestamp)
const payload: FeishuPayload = {
timestamp: timestamp.toString(),
sign,
msg_type: 'interactive',
card: content
}
const payloadStr = JSON.stringify(payload)
const url = new URL(webhookUrl)
const options: https.RequestOptions = {
hostname: url.hostname,
path: url.pathname + url.search,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payloadStr)
}
}
const req = https.request(options, (res) => {
let data = ''
res.on('data', (chunk: Buffer) => {
data += chunk.toString()
})
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
console.log('Successfully sent to Feishu:', data)
resolve()
} else {
reject(new Error(`Feishu API error: ${res.statusCode} - ${data}`))
}
})
})
req.on('error', (error: Error) => {
reject(error)
})
req.write(payloadStr)
req.end()
})
}
/**
* Create Feishu card message from issue data
* @param issueData - GitHub issue data
* @returns Feishu card content
*/
function createIssueCard(issueData: IssueData): FeishuCard {
const { issueUrl, issueNumber, issueTitle, issueSummary, issueAuthor, labels } = issueData
const elements: FeishuCardElement[] = [
{
tag: 'div',
text: {
tag: 'lark_md',
content: `**Author:** ${issueAuthor}`
}
}
]
if (labels.length > 0) {
elements.push({
tag: 'div',
text: {
tag: 'lark_md',
content: `**Labels:** ${labels.join(', ')}`
}
})
}
elements.push(
{ tag: 'hr' },
{
tag: 'div',
text: {
tag: 'lark_md',
content: `**Summary:**\n${issueSummary}`
}
},
{ tag: 'hr' },
{
tag: 'action',
actions: [
{
tag: 'button',
text: {
tag: 'plain_text',
content: 'View Issue'
},
type: 'primary',
url: issueUrl
}
]
}
)
return {
elements,
header: {
template: 'blue',
title: {
tag: 'plain_text',
content: `#${issueNumber} - ${issueTitle}`
}
}
}
}
/**
* Create a simple Feishu card message
* @param title - Card title
* @param description - Card description content
* @param color - Header color template (default: 'turquoise')
* @returns Feishu card content
*/
function createSimpleCard(title: string, description: string, color: FeishuHeaderTemplate = 'turquoise'): FeishuCard {
return {
elements: [
{
tag: 'div',
text: {
tag: 'lark_md',
content: description
}
}
],
header: {
template: color,
title: {
tag: 'plain_text',
content: title
}
}
}
}
/**
* Get Feishu credentials from environment variables
*/
function getCredentials(): { webhookUrl: string; secret: string } {
const webhookUrl = process.env.FEISHU_WEBHOOK_URL
const secret = process.env.FEISHU_WEBHOOK_SECRET
if (!webhookUrl) {
console.error('Error: FEISHU_WEBHOOK_URL environment variable is required')
process.exit(1)
}
if (!secret) {
console.error('Error: FEISHU_WEBHOOK_SECRET environment variable is required')
process.exit(1)
}
return { webhookUrl, secret }
}
/**
* Handle send subcommand
*/
async function handleSendCommand(options: SendOptions): Promise<void> {
const { webhookUrl, secret } = getCredentials()
const { title, description, color = 'turquoise' } = options
// Validate color parameter
const colorValidation = FeishuHeaderTemplateSchema.safeParse(color)
if (!colorValidation.success) {
console.error(`Error: Invalid color "${color}". Valid colors: ${FeishuHeaderTemplateSchema.options.join(', ')}`)
process.exit(1)
}
const card = createSimpleCard(title, description, colorValidation.data)
console.log('Sending notification to Feishu...')
console.log(`Title: ${title}`)
await sendToFeishu(webhookUrl, secret, card)
console.log('Notification sent successfully!')
}
/**
* Handle issue subcommand
*/
async function handleIssueCommand(options: IssueOptions): Promise<void> {
const { webhookUrl, secret } = getCredentials()
const { url, number, title, summary, author = 'Unknown', labels: labelsStr = '' } = options
if (!url || !number || !title || !summary) {
console.error('Error: --url, --number, --title, and --summary are required')
process.exit(1)
}
const labels = labelsStr
? labelsStr
.split(',')
.map((l) => l.trim())
.filter(Boolean)
: []
const issueData: IssueData = {
issueUrl: url,
issueNumber: number,
issueTitle: title,
issueSummary: summary,
issueAuthor: author,
labels
}
const card = createIssueCard(issueData)
console.log('Sending notification to Feishu...')
console.log(`Issue #${number}: ${title}`)
await sendToFeishu(webhookUrl, secret, card)
console.log('Notification sent successfully!')
}
// Configure CLI
const program = new Command()
program.name('feishu-notify').description('Send notifications to Feishu webhook').version(VERSION)
// Send subcommand (generic)
program
.command('send')
.description('Send a simple notification to Feishu')
.requiredOption('-t, --title <title>', 'Card title')
.requiredOption('-d, --description <description>', 'Card description (supports markdown)')
.option(
'-c, --color <color>',
`Header color template (default: turquoise). Options: ${FeishuHeaderTemplateSchema.options.join(', ')}`,
'turquoise'
)
.action(async (options: SendOptions) => {
try {
await handleSendCommand(options)
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : error)
process.exit(1)
}
})
// Issue subcommand
program
.command('issue')
.description('Send GitHub issue notification to Feishu')
.requiredOption('-u, --url <url>', 'GitHub issue URL')
.requiredOption('-n, --number <number>', 'Issue number')
.requiredOption('-t, --title <title>', 'Issue title')
.requiredOption('-m, --summary <summary>', 'Issue summary')
.option('-a, --author <author>', 'Issue author', 'Unknown')
.option('-l, --labels <labels>', 'Issue labels, comma-separated')
.action(async (options: IssueOptions) => {
try {
await handleIssueCommand(options)
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : error)
process.exit(1)
}
})
program.parse()

View File

@ -1,64 +0,0 @@
const fs = require('fs')
const path = require('path')
const os = require('os')
const zlib = require('zlib')
const tar = require('tar')
const { pipeline } = require('stream/promises')
async function downloadNpmPackage(packageName, url) {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'npm-download-'))
const targetDir = path.join('./node_modules/', packageName)
const filename = path.join(tempDir, packageName.replace('/', '-') + '.tgz')
const extractDir = path.join(tempDir, 'extract')
// Skip if directory already exists
if (fs.existsSync(targetDir)) {
console.log(`${targetDir} already exists, skipping download...`)
return
}
try {
console.log(`Downloading ${packageName}...`, url)
// Download file using fetch API
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const fileStream = fs.createWriteStream(filename)
await pipeline(response.body, fileStream)
console.log(`Extracting ${filename}...`)
// Create extraction directory
fs.mkdirSync(extractDir, { recursive: true })
// Extract tar.gz file using Node.js streams
await pipeline(fs.createReadStream(filename), zlib.createGunzip(), tar.extract({ cwd: extractDir }))
// Remove the downloaded file
fs.rmSync(filename, { force: true })
// Create target directory
fs.mkdirSync(targetDir, { recursive: true })
// Move extracted package contents to target directory
const packageDir = path.join(extractDir, 'package')
if (fs.existsSync(packageDir)) {
fs.cpSync(packageDir, targetDir, { recursive: true })
}
} catch (error) {
console.error(`Error processing ${packageName}: ${error.message}`)
throw error
} finally {
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true })
}
}
}
module.exports = {
downloadNpmPackage
}

View File

@ -1,6 +1,10 @@
import { loggerService } from '@logger'
import { MESSAGE_STREAM_TIMEOUT_MS } from '@main/apiServer/config/timeouts'
import { createStreamAbortController, STREAM_TIMEOUT_REASON } from '@main/apiServer/utils/createStreamAbortController'
import {
createStreamAbortController,
STREAM_TIMEOUT_REASON,
type StreamAbortController
} from '@main/apiServer/utils/createStreamAbortController'
import { agentService, sessionMessageService, sessionService } from '@main/services/agents'
import type { Request, Response } from 'express'
@ -26,7 +30,7 @@ const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
}
export const createMessage = async (req: Request, res: Response): Promise<void> => {
let clearAbortTimeout: (() => void) | undefined
let streamController: StreamAbortController | undefined
try {
const { agentId, sessionId } = req.params
@ -45,14 +49,10 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control')
const {
abortController,
registerAbortHandler,
clearAbortTimeout: helperClearAbortTimeout
} = createStreamAbortController({
streamController = createStreamAbortController({
timeoutMs: MESSAGE_STREAM_TIMEOUT_MS
})
clearAbortTimeout = helperClearAbortTimeout
const { abortController, registerAbortHandler, dispose } = streamController
const { stream, completion } = await sessionMessageService.createSessionMessage(
session,
messageData,
@ -64,8 +64,8 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
let responseEnded = false
let streamFinished = false
const cleanupAbortTimeout = () => {
clearAbortTimeout?.()
const cleanup = () => {
dispose()
}
const finalizeResponse = () => {
@ -78,7 +78,7 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
}
responseEnded = true
cleanupAbortTimeout()
cleanup()
try {
// res.write('data: {"type":"finish"}\n\n')
res.write('data: [DONE]\n\n')
@ -108,7 +108,7 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
* - Mark the response as ended to prevent further writes
*/
registerAbortHandler((abortReason) => {
cleanupAbortTimeout()
cleanup()
if (responseEnded) return
@ -189,7 +189,7 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
logger.error('Error writing stream error to SSE', { error: writeError })
}
responseEnded = true
cleanupAbortTimeout()
cleanup()
res.end()
}
}
@ -221,14 +221,14 @@ export const createMessage = async (req: Request, res: Response): Promise<void>
logger.error('Error writing completion error to SSE stream', { error: writeError })
}
responseEnded = true
cleanupAbortTimeout()
cleanup()
res.end()
})
// Clear timeout when response ends
res.on('close', cleanupAbortTimeout)
res.on('finish', cleanupAbortTimeout)
res.on('close', cleanup)
res.on('finish', cleanup)
} catch (error: any) {
clearAbortTimeout?.()
streamController?.dispose()
logger.error('Error in streaming message handler', {
error,
agentId: req.params.agentId,

View File

@ -4,6 +4,7 @@ export interface StreamAbortController {
abortController: AbortController
registerAbortHandler: (handler: StreamAbortHandler) => void
clearAbortTimeout: () => void
dispose: () => void
}
export const STREAM_TIMEOUT_REASON = 'stream timeout'
@ -40,6 +41,15 @@ export const createStreamAbortController = (options: CreateStreamAbortController
signal.addEventListener('abort', handleAbort, { once: true })
let disposed = false
const dispose = () => {
if (disposed) return
disposed = true
clearAbortTimeout()
signal.removeEventListener('abort', handleAbort)
}
const registerAbortHandler = (handler: StreamAbortHandler) => {
abortHandler = handler
@ -59,6 +69,7 @@ export const createStreamAbortController = (options: CreateStreamAbortController
return {
abortController,
registerAbortHandler,
clearAbortTimeout
clearAbortTimeout,
dispose
}
}

View File

@ -75,6 +75,15 @@ if (isLinux && process.env.XDG_SESSION_TYPE === 'wayland') {
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
}
/**
* Set window class and name for X11
* This ensures the system tray and window manager identify the app correctly
*/
if (isLinux) {
app.commandLine.appendSwitch('class', 'cherry-studio')
app.commandLine.appendSwitch('name', 'cherry-studio')
}
// DocumentPolicyIncludeJSCallStacksInCrashReports: Enable features for unresponsive renderer js call stacks
// EarlyEstablishGpuChannel,EstablishGpuChannelAsync: Enable features for early establish gpu channel
// speed up the startup time

View File

@ -8,6 +8,27 @@ import { v4 as uuidv4 } from 'uuid'
const logger = loggerService.withContext('DxtService')
/**
* Ensure a target path is within the base directory to prevent path traversal attacks.
* This is the correct approach: validate the final resolved path rather than sanitizing input.
*
* @param basePath - The base directory that the target must be within
* @param targetPath - The target path to validate
* @returns The resolved target path if valid
* @throws Error if the target path escapes the base directory
*/
export function ensurePathWithin(basePath: string, targetPath: string): string {
const resolvedBase = path.resolve(basePath)
const resolvedTarget = path.resolve(path.normalize(targetPath))
// Must be direct child of base directory, no subdirectories allowed
if (path.dirname(resolvedTarget) !== resolvedBase) {
throw new Error('Path traversal detected: target path must be direct child of base directory')
}
return resolvedTarget
}
// Type definitions
export interface DxtManifest {
dxt_version: string
@ -68,6 +89,76 @@ export interface DxtUploadResult {
error?: string
}
/**
* Validate and sanitize a command to prevent path traversal attacks.
* Commands should be either:
* 1. Simple command names (e.g., "node", "python", "npx") - looked up in PATH
* 2. Absolute paths (e.g., "/usr/bin/node", "C:\\Program Files\\node\\node.exe")
* 3. Relative paths starting with ./ or .\ (relative to extractDir)
*
* Rejects commands containing path traversal sequences (..)
*
* @param command - The command to validate
* @returns The validated command
* @throws Error if command contains path traversal or is invalid
*/
export function validateCommand(command: string): string {
if (!command || typeof command !== 'string') {
throw new Error('Invalid command: command must be a non-empty string')
}
const trimmed = command.trim()
if (!trimmed) {
throw new Error('Invalid command: command cannot be empty')
}
// Check for path traversal sequences
// This catches: .., ../, ..\, /../, \..\, etc.
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(trimmed) || trimmed === '..') {
throw new Error(`Invalid command: path traversal detected in "${command}"`)
}
// Check for null bytes
if (trimmed.includes('\0')) {
throw new Error('Invalid command: null byte detected')
}
return trimmed
}
/**
* Validate command arguments to prevent injection attacks.
* Rejects arguments containing path traversal sequences.
*
* @param args - The arguments array to validate
* @returns The validated arguments array
* @throws Error if any argument contains path traversal
*/
export function validateArgs(args: string[]): string[] {
if (!Array.isArray(args)) {
throw new Error('Invalid args: must be an array')
}
return args.map((arg, index) => {
if (typeof arg !== 'string') {
throw new Error(`Invalid args: argument at index ${index} must be a string`)
}
// Check for null bytes
if (arg.includes('\0')) {
throw new Error(`Invalid args: null byte detected in argument at index ${index}`)
}
// Check for path traversal in arguments that look like paths
// Only validate if the arg contains path separators (indicating it's meant to be a path)
if ((arg.includes('/') || arg.includes('\\')) && /(?:^|[/\\])\.\.(?:[/\\]|$)/.test(arg)) {
throw new Error(`Invalid args: path traversal detected in argument at index ${index}`)
}
return arg
})
}
export function performVariableSubstitution(
value: string,
extractDir: string,
@ -134,12 +225,16 @@ export function applyPlatformOverrides(mcpConfig: any, extractDir: string, userC
// Apply variable substitution to all string values
if (resolvedConfig.command) {
resolvedConfig.command = performVariableSubstitution(resolvedConfig.command, extractDir, userConfig)
// Validate command after substitution to prevent path traversal attacks
resolvedConfig.command = validateCommand(resolvedConfig.command)
}
if (resolvedConfig.args) {
resolvedConfig.args = resolvedConfig.args.map((arg: string) =>
performVariableSubstitution(arg, extractDir, userConfig)
)
// Validate args after substitution to prevent path traversal attacks
resolvedConfig.args = validateArgs(resolvedConfig.args)
}
if (resolvedConfig.env) {
@ -271,10 +366,8 @@ class DxtService {
}
// Use server name as the final extract directory for automatic version management
// Sanitize the name to prevent creating subdirectories
const sanitizedName = manifest.name.replace(/\//g, '-')
const serverDirName = `server-${sanitizedName}`
const finalExtractDir = path.join(this.mcpDir, serverDirName)
const serverDirName = `server-${manifest.name}`
const finalExtractDir = ensurePathWithin(this.mcpDir, path.join(this.mcpDir, serverDirName))
// Clean up any existing version of this server
if (fs.existsSync(finalExtractDir)) {
@ -354,27 +447,15 @@ class DxtService {
public cleanupDxtServer(serverName: string): boolean {
try {
// Handle server names that might contain slashes (e.g., "anthropic/sequential-thinking")
// by replacing slashes with the same separator used during installation
const sanitizedName = serverName.replace(/\//g, '-')
const serverDirName = `server-${sanitizedName}`
const serverDir = path.join(this.mcpDir, serverDirName)
const serverDirName = `server-${serverName}`
const serverDir = ensurePathWithin(this.mcpDir, path.join(this.mcpDir, serverDirName))
// First try the sanitized path
if (fs.existsSync(serverDir)) {
logger.debug(`Removing DXT server directory: ${serverDir}`)
fs.rmSync(serverDir, { recursive: true, force: true })
return true
}
// Fallback: try with original name in case it was stored differently
const originalServerDir = path.join(this.mcpDir, `server-${serverName}`)
if (fs.existsSync(originalServerDir)) {
logger.debug(`Removing DXT server directory: ${originalServerDir}`)
fs.rmSync(originalServerDir, { recursive: true, force: true })
return true
}
logger.warn(`Server directory not found: ${serverDir}`)
return false
} catch (error) {

View File

@ -22,8 +22,7 @@ export class SearchService {
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
devTools: is.dev,
offscreen: true // 启用离屏渲染
devTools: is.dev
}
})
@ -69,8 +68,7 @@ export class SearchService {
// Wait for the page to fully load before getting the content
await new Promise<void>((resolve) => {
const loadTimeout = setTimeout(() => resolve(), 10000) // 10 second timeout
window.once('ready-to-show', () => {
//让网页加载完成后执行,原来的.webContents.once('did-finish-load'会导致网页抖动
window.webContents.once('did-finish-load', () => {
clearTimeout(loadTimeout)
// Small delay to ensure JavaScript has executed
setTimeout(resolve, 500)
@ -78,9 +76,7 @@ export class SearchService {
})
// Get the page content after ensuring it's fully loaded
const executeJavaScript = await window.webContents.executeJavaScript('document.documentElement.outerHTML')
// logger.info(executeJavaScript)
return executeJavaScript
return await window.webContents.executeJavaScript('document.documentElement.outerHTML')
}
}

View File

@ -1083,18 +1083,33 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1
}
//check if the key is ctrl key
// Check if the key is ctrl key
// Windows: VK_LCONTROL(162), VK_RCONTROL(163)
// macOS: kVK_Control(59), kVK_RightControl(62)
private isCtrlkey(vkCode: number) {
if (isMac) {
return vkCode === 59 || vkCode === 62
}
return vkCode === 162 || vkCode === 163
}
//check if the key is shift key
// Check if the key is shift key
// Windows: VK_LSHIFT(160), VK_RSHIFT(161)
// macOS: kVK_Shift(56), kVK_RightShift(60)
private isShiftkey(vkCode: number) {
if (isMac) {
return vkCode === 56 || vkCode === 60
}
return vkCode === 160 || vkCode === 161
}
//check if the key is alt key
// Check if the key is alt/option key
// Windows: VK_LMENU(164), VK_RMENU(165)
// macOS: kVK_Option(58), kVK_RightOption(61)
private isAltkey(vkCode: number) {
if (isMac) {
return vkCode === 58 || vkCode === 61
}
return vkCode === 164 || vkCode === 165
}

View File

@ -0,0 +1,202 @@
import path from 'path'
import { describe, expect, it } from 'vitest'
import { ensurePathWithin, validateArgs, validateCommand } from '../DxtService'
describe('ensurePathWithin', () => {
const baseDir = '/home/user/mcp'
describe('valid paths', () => {
it('should accept direct child paths', () => {
expect(ensurePathWithin(baseDir, '/home/user/mcp/server-test')).toBe('/home/user/mcp/server-test')
expect(ensurePathWithin(baseDir, '/home/user/mcp/my-server')).toBe('/home/user/mcp/my-server')
})
it('should accept paths with unicode characters', () => {
expect(ensurePathWithin(baseDir, '/home/user/mcp/服务器')).toBe('/home/user/mcp/服务器')
expect(ensurePathWithin(baseDir, '/home/user/mcp/サーバー')).toBe('/home/user/mcp/サーバー')
})
})
describe('path traversal prevention', () => {
it('should reject paths that escape base directory', () => {
expect(() => ensurePathWithin(baseDir, '/home/user/mcp/../../../etc')).toThrow('Path traversal detected')
expect(() => ensurePathWithin(baseDir, '/etc/passwd')).toThrow('Path traversal detected')
expect(() => ensurePathWithin(baseDir, '/home/user')).toThrow('Path traversal detected')
})
it('should reject subdirectories', () => {
expect(() => ensurePathWithin(baseDir, '/home/user/mcp/sub/dir')).toThrow('Path traversal detected')
expect(() => ensurePathWithin(baseDir, '/home/user/mcp/a/b/c')).toThrow('Path traversal detected')
})
it('should reject Windows-style path traversal', () => {
const winBase = 'C:\\Users\\user\\mcp'
expect(() => ensurePathWithin(winBase, 'C:\\Users\\user\\mcp\\..\\..\\Windows\\System32')).toThrow(
'Path traversal detected'
)
})
it('should reject null byte attacks', () => {
const maliciousPath = path.join(baseDir, 'server\x00/../../../etc/passwd')
expect(() => ensurePathWithin(baseDir, maliciousPath)).toThrow('Path traversal detected')
})
it('should handle encoded traversal attempts', () => {
expect(() => ensurePathWithin(baseDir, '/home/user/mcp/../escape')).toThrow('Path traversal detected')
})
})
describe('edge cases', () => {
it('should reject base directory itself', () => {
expect(() => ensurePathWithin(baseDir, '/home/user/mcp')).toThrow('Path traversal detected')
})
it('should handle relative path construction', () => {
const target = path.join(baseDir, 'server-name')
expect(ensurePathWithin(baseDir, target)).toBe('/home/user/mcp/server-name')
})
})
})
describe('validateCommand', () => {
describe('valid commands', () => {
it('should accept simple command names', () => {
expect(validateCommand('node')).toBe('node')
expect(validateCommand('python')).toBe('python')
expect(validateCommand('npx')).toBe('npx')
expect(validateCommand('uvx')).toBe('uvx')
})
it('should accept absolute paths', () => {
expect(validateCommand('/usr/bin/node')).toBe('/usr/bin/node')
expect(validateCommand('/usr/local/bin/python3')).toBe('/usr/local/bin/python3')
expect(validateCommand('C:\\Program Files\\nodejs\\node.exe')).toBe('C:\\Program Files\\nodejs\\node.exe')
})
it('should accept relative paths starting with ./', () => {
expect(validateCommand('./node_modules/.bin/tsc')).toBe('./node_modules/.bin/tsc')
expect(validateCommand('.\\scripts\\run.bat')).toBe('.\\scripts\\run.bat')
})
it('should trim whitespace', () => {
expect(validateCommand(' node ')).toBe('node')
expect(validateCommand('\tpython\n')).toBe('python')
})
})
describe('path traversal prevention', () => {
it('should reject commands with path traversal (Unix style)', () => {
expect(() => validateCommand('../../../bin/sh')).toThrow('path traversal detected')
expect(() => validateCommand('../../etc/passwd')).toThrow('path traversal detected')
expect(() => validateCommand('/usr/../../../bin/sh')).toThrow('path traversal detected')
})
it('should reject commands with path traversal (Windows style)', () => {
expect(() => validateCommand('..\\..\\..\\Windows\\System32\\cmd.exe')).toThrow('path traversal detected')
expect(() => validateCommand('..\\..\\Windows\\System32\\calc.exe')).toThrow('path traversal detected')
expect(() => validateCommand('C:\\..\\..\\Windows\\System32\\cmd.exe')).toThrow('path traversal detected')
})
it('should reject just ".."', () => {
expect(() => validateCommand('..')).toThrow('path traversal detected')
})
it('should reject mixed style path traversal', () => {
expect(() => validateCommand('../..\\mixed/..\\attack')).toThrow('path traversal detected')
})
})
describe('null byte injection', () => {
it('should reject commands with null bytes', () => {
expect(() => validateCommand('node\x00.exe')).toThrow('null byte detected')
expect(() => validateCommand('python\0')).toThrow('null byte detected')
})
})
describe('edge cases', () => {
it('should reject empty strings', () => {
expect(() => validateCommand('')).toThrow('command must be a non-empty string')
expect(() => validateCommand(' ')).toThrow('command cannot be empty')
})
it('should reject non-string input', () => {
// @ts-expect-error - testing runtime behavior
expect(() => validateCommand(null)).toThrow('command must be a non-empty string')
// @ts-expect-error - testing runtime behavior
expect(() => validateCommand(undefined)).toThrow('command must be a non-empty string')
// @ts-expect-error - testing runtime behavior
expect(() => validateCommand(123)).toThrow('command must be a non-empty string')
})
})
describe('real-world attack scenarios', () => {
it('should prevent Windows system32 command injection', () => {
expect(() => validateCommand('../../../../Windows/System32/cmd.exe')).toThrow('path traversal detected')
expect(() => validateCommand('..\\..\\..\\..\\Windows\\System32\\powershell.exe')).toThrow(
'path traversal detected'
)
})
it('should prevent Unix bin injection', () => {
expect(() => validateCommand('../../../../bin/bash')).toThrow('path traversal detected')
expect(() => validateCommand('../../../usr/bin/curl')).toThrow('path traversal detected')
})
})
})
describe('validateArgs', () => {
describe('valid arguments', () => {
it('should accept normal arguments', () => {
expect(validateArgs(['--version'])).toEqual(['--version'])
expect(validateArgs(['-y', '@anthropic/mcp-server'])).toEqual(['-y', '@anthropic/mcp-server'])
expect(validateArgs(['install', 'package-name'])).toEqual(['install', 'package-name'])
})
it('should accept arguments with safe paths', () => {
expect(validateArgs(['./src/index.ts'])).toEqual(['./src/index.ts'])
expect(validateArgs(['/absolute/path/file.js'])).toEqual(['/absolute/path/file.js'])
})
it('should accept empty array', () => {
expect(validateArgs([])).toEqual([])
})
})
describe('path traversal prevention', () => {
it('should reject arguments with path traversal', () => {
expect(() => validateArgs(['../../../etc/passwd'])).toThrow('path traversal detected')
expect(() => validateArgs(['--config', '../../secrets.json'])).toThrow('path traversal detected')
expect(() => validateArgs(['..\\..\\Windows\\System32\\config'])).toThrow('path traversal detected')
})
it('should only check path-like arguments', () => {
// Arguments without path separators should pass even with dots
expect(validateArgs(['..version'])).toEqual(['..version'])
expect(validateArgs(['test..name'])).toEqual(['test..name'])
})
})
describe('null byte injection', () => {
it('should reject arguments with null bytes', () => {
expect(() => validateArgs(['file\x00.txt'])).toThrow('null byte detected')
expect(() => validateArgs(['--config', 'path\0name'])).toThrow('null byte detected')
})
})
describe('edge cases', () => {
it('should reject non-array input', () => {
// @ts-expect-error - testing runtime behavior
expect(() => validateArgs('not an array')).toThrow('must be an array')
// @ts-expect-error - testing runtime behavior
expect(() => validateArgs(null)).toThrow('must be an array')
})
it('should reject non-string elements', () => {
// @ts-expect-error - testing runtime behavior
expect(() => validateArgs([123])).toThrow('must be a string')
// @ts-expect-error - testing runtime behavior
expect(() => validateArgs(['valid', null])).toThrow('must be a string')
})
})
})

View File

@ -1,6 +1,6 @@
import type { WebSearchPluginConfig } from '@cherrystudio/ai-core/built-in/plugins'
import { loggerService } from '@logger'
import { isGemini3Model, isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
import { isAnthropicModel, isGemini3Model, isSupportedThinkingTokenQwenModel } from '@renderer/config/models'
import type { McpMode, MCPTool } from '@renderer/types'
import { type Assistant, type Message, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk'
@ -10,6 +10,7 @@ import { extractReasoningMiddleware, simulateStreamingMiddleware } from 'ai'
import { getAiSdkProviderId } from '../provider/factory'
import { isOpenRouterGeminiGenerateImageModel } from '../utils/image'
import { anthropicCacheMiddleware } from './anthropicCacheMiddleware'
import { noThinkMiddleware } from './noThinkMiddleware'
import { openrouterGenerateImageMiddleware } from './openrouterGenerateImageMiddleware'
import { openrouterReasoningMiddleware } from './openrouterReasoningMiddleware'
@ -179,7 +180,12 @@ function addProviderSpecificMiddlewares(builder: AiSdkMiddlewareBuilder, config:
// 根据不同provider添加特定中间件
switch (config.provider.type) {
case 'anthropic':
// Anthropic特定中间件
if (isAnthropicModel(config.model) && config.provider.anthropicCacheControl?.tokenThreshold) {
builder.add({
name: 'anthropic-cache',
middleware: anthropicCacheMiddleware(config.provider)
})
}
break
case 'openai':
case 'azure-openai': {

View File

@ -0,0 +1,85 @@
/**
* Anthropic Prompt Caching Middleware
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
*/
import type { LanguageModelV2Message } from '@ai-sdk/provider'
import { estimateTextTokens } from '@renderer/services/TokenService'
import type { Provider } from '@renderer/types'
import type { LanguageModelMiddleware } from 'ai'
const cacheProviderOptions = {
anthropic: { cacheControl: { type: 'ephemeral' } }
}
function estimateContentTokens(content: LanguageModelV2Message['content']): number {
if (typeof content === 'string') return estimateTextTokens(content)
if (Array.isArray(content)) {
return content.reduce((acc, part) => {
if (part.type === 'text') {
return acc + estimateTextTokens(part.text as string)
}
return acc
}, 0)
}
return 0
}
export function anthropicCacheMiddleware(provider: Provider): LanguageModelMiddleware {
return {
middlewareVersion: 'v2',
transformParams: async ({ params }) => {
const settings = provider.anthropicCacheControl
if (!settings?.tokenThreshold || !Array.isArray(params.prompt) || params.prompt.length === 0) {
return params
}
const { tokenThreshold, cacheSystemMessage, cacheLastNMessages } = settings
const messages = [...params.prompt]
let cachedCount = 0
// Cache system message (providerOptions on message object)
if (cacheSystemMessage) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i] as LanguageModelV2Message
if (msg.role === 'system' && estimateContentTokens(msg.content) >= tokenThreshold) {
messages[i] = { ...msg, providerOptions: cacheProviderOptions }
break
}
}
}
// Cache last N non-system messages (providerOptions on content parts)
if (cacheLastNMessages > 0) {
const cumsumTokens = [] as Array<number>
let tokenSum = 0 as number
for (let i = 0; i < messages.length; i++) {
const msg = messages[i] as LanguageModelV2Message
tokenSum += estimateContentTokens(msg.content)
cumsumTokens.push(tokenSum)
}
for (let i = messages.length - 1; i >= 0 && cachedCount < cacheLastNMessages; i--) {
const msg = messages[i] as LanguageModelV2Message
if (msg.role === 'system' || cumsumTokens[i] < tokenThreshold || msg.content.length === 0) {
continue
}
const newContent = [...msg.content]
const lastIndex = newContent.length - 1
newContent[lastIndex] = {
...newContent[lastIndex],
providerOptions: cacheProviderOptions
}
messages[i] = {
...msg,
content: newContent
} as LanguageModelV2Message
cachedCount++
}
}
return { ...params, prompt: messages }
}
}
}

View File

@ -325,9 +325,9 @@ function createDeveloperToSystemFetch(originalFetch?: typeof fetch): typeof fetc
if (options?.body && typeof options.body === 'string') {
try {
const body = JSON.parse(options.body)
if (body.messages && Array.isArray(body.messages)) {
if (body.input && Array.isArray(body.input)) {
let hasChanges = false
body.messages = body.messages.map((msg: { role: string }) => {
body.input = body.input.map((msg: { role: string }) => {
if (msg.role === 'developer') {
hasChanges = true
return { ...msg, role: 'system' }

View File

@ -3,7 +3,7 @@ import { type AnthropicProviderOptions } from '@ai-sdk/anthropic'
import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
import type { XaiProviderOptions } from '@ai-sdk/xai'
import { baseProviderIdSchema, customProviderIdSchema } from '@cherrystudio/ai-core/provider'
import { baseProviderIdSchema, customProviderIdSchema, hasProviderConfig } from '@cherrystudio/ai-core/provider'
import { loggerService } from '@logger'
import {
getModelSupportedVerbosity,
@ -616,9 +616,14 @@ function buildGenericProviderOptions(
}
if (enableReasoning) {
if (isInterleavedThinkingModel(model)) {
providerOptions = {
...providerOptions,
sendReasoning: true
// sendReasoning is a patch specific to @ai-sdk/openai-compatible
// Only apply when provider will actually use openai-compatible SDK
// (i.e., no dedicated SDK registered OR explicitly openai-compatible)
if (!hasProviderConfig(providerId) || providerId === 'openai-compatible') {
providerOptions = {
...providerOptions,
sendReasoning: true
}
}
}
}
@ -648,6 +653,10 @@ function buildGenericProviderOptions(
}
}
if (isOpenAIModel(model)) {
providerOptions.strictJsonSchema = false
}
return {
[providerId]: providerOptions
}

View File

@ -1368,7 +1368,9 @@ describe('findTokenLimit', () => {
{ modelId: 'qwen-plus-ultra', expected: { min: 0, max: 81_920 } },
{ modelId: 'qwen-turbo-pro', expected: { min: 0, max: 38_912 } },
{ modelId: 'qwen-flash-lite', expected: { min: 0, max: 81_920 } },
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } }
{ modelId: 'qwen3-7b', expected: { min: 1_024, max: 38_912 } },
{ modelId: 'Baichuan-M2', expected: { min: 0, max: 30_000 } },
{ modelId: 'baichuan-m2', expected: { min: 0, max: 30_000 } }
]
it.each(cases)('returns correct limits for $modelId', ({ modelId, expected }) => {

View File

@ -713,6 +713,30 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
provider: 'baichuan',
name: 'Baichuan3 Turbo 128k',
group: 'Baichuan3'
},
{
id: 'Baichuan4-Turbo',
provider: 'baichuan',
name: 'Baichuan4 Turbo',
group: 'Baichuan4'
},
{
id: 'Baichuan4-Air',
provider: 'baichuan',
name: 'Baichuan4 Air',
group: 'Baichuan4'
},
{
id: 'Baichuan-M2',
provider: 'baichuan',
name: 'Baichuan M2',
group: 'Baichuan-M2'
},
{
id: 'Baichuan-M2-Plus',
provider: 'baichuan',
name: 'Baichuan M2 Plus',
group: 'Baichuan-M2'
}
],
modelscope: [
@ -753,7 +777,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
{ id: 'qwen-flash', name: 'qwen-flash', provider: 'dashscope', group: 'qwen-flash', owned_by: 'system' },
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
{ id: 'qwen3-max', name: 'qwen3-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' }
{ id: 'qwen3-max', name: 'qwen3-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
{ id: 'text-embedding-v4', name: 'text-embedding-v4', provider: 'dashscope', group: 'qwen-text-embedding' },
{ id: 'text-embedding-v3', name: 'text-embedding-v3', provider: 'dashscope', group: 'qwen-text-embedding' },
{ id: 'text-embedding-v2', name: 'text-embedding-v2', provider: 'dashscope', group: 'qwen-text-embedding' },
{ id: 'text-embedding-v1', name: 'text-embedding-v1', provider: 'dashscope', group: 'qwen-text-embedding' },
{ id: 'qwen3-rerank', name: 'qwen3-rerank', provider: 'dashscope', group: 'qwen-rerank' }
],
stepfun: [
{

View File

@ -640,6 +640,16 @@ export const isMiniMaxReasoningModel = (model?: Model): boolean => {
return (['minimax-m1', 'minimax-m2', 'minimax-m2.1'] as const).some((id) => modelId.includes(id))
}
export const isBaichuanReasoningModel = (model?: Model): boolean => {
if (!model) {
return false
}
const modelId = getLowerBaseModelName(model.id, '/')
// 只有 Baichuan-M2 是推理模型注意M2-Plus 不是推理模型)
return modelId.includes('baichuan-m2') && !modelId.includes('plus')
}
export function isReasoningModel(model?: Model): boolean {
if (!model || isEmbeddingModel(model) || isRerankModel(model) || isTextToImageModel(model)) {
return false
@ -675,6 +685,7 @@ export function isReasoningModel(model?: Model): boolean {
isLingReasoningModel(model) ||
isMiniMaxReasoningModel(model) ||
isMiMoReasoningModel(model) ||
isBaichuanReasoningModel(model) ||
modelId.includes('magistral') ||
modelId.includes('pangu-pro-moe') ||
modelId.includes('seed-oss') ||
@ -718,7 +729,10 @@ const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
'(?:anthropic\\.)?claude-opus-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
min: 1024,
max: 32_000
}
},
// Baichuan models
'baichuan-m2$': { min: 0, max: 30_000 }
}
export const findTokenLimit = (modelId: string): { min: number; max: number } | undefined => {

View File

@ -1025,7 +1025,7 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
official: 'https://www.baichuan-ai.com/',
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
docs: 'https://platform.baichuan-ai.com/docs',
models: 'https://platform.baichuan-ai.com/price'
models: 'https://platform.baichuan-ai.com/prices'
}
},
modelscope: {

View File

@ -83,7 +83,14 @@ export function useAssistant(id: string) {
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
}
const assistantWithModel = useMemo(() => ({ ...assistant, model }), [assistant, model])
const normalizedTopics = useMemo(
() => (Array.isArray(assistant?.topics) ? assistant.topics : []),
[assistant?.topics]
)
const assistantWithModel = useMemo(
() => ({ ...assistant, model, topics: normalizedTopics }),
[assistant, model, normalizedTopics]
)
const settingsRef = useRef(assistant?.settings)

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "View",
"title": "Careers"
},
"checkUpdate": {
"available": "Update",
"label": "Check Update"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Cache Last N Messages",
"cache_last_n_help": "Cache the last N conversation messages (excluding system messages)",
"cache_system": "Cache System Message",
"cache_system_help": "Whether to cache the system prompt",
"token_threshold": "Cache Token Threshold",
"token_threshold_help": "Messages exceeding this token count will be cached. Set to 0 to disable caching."
},
"array_content": {
"help": "Does the provider support the content field of the message being of array type?",
"label": "Supports array format message content"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "查看",
"title": "加入我们"
},
"checkUpdate": {
"available": "立即更新",
"label": "检查更新"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "缓存最后 N 条消息",
"cache_last_n_help": "缓存最后的 N 条对话消息(不含系统消息)",
"cache_system": "缓存系统消息",
"cache_system_help": "是否缓存系统提示词",
"token_threshold": "缓存 Token 阈值",
"token_threshold_help": "消息超过此 Token 数才会被缓存,设为 0 禁用缓存"
},
"array_content": {
"help": "该提供商是否支持 message 的 content 字段为 array 类型",
"label": "支持数组格式的 message content"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "查看",
"title": "加入我們"
},
"checkUpdate": {
"available": "立即更新",
"label": "檢查更新"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "快取最近 N 則訊息",
"cache_last_n_help": "快取最後 N 則對話訊息(排除系統訊息)",
"cache_system": "快取系統訊息",
"cache_system_help": "是否快取系統提示",
"token_threshold": "快取權杖閾值",
"token_threshold_help": "超過此標記數量的訊息將被快取。設為 0 以停用快取。"
},
"array_content": {
"help": "該供應商是否支援 message 的 content 欄位為 array 類型",
"label": "支援陣列格式的 message content"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Ansicht",
"title": "Karriere"
},
"checkUpdate": {
"available": "Jetzt aktualisieren",
"label": "Auf Updates prüfen"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Letzte N Nachrichten zwischenspeichern",
"cache_last_n_help": "Zwischen die letzten N Gesprächsnachrichten (ohne Systemnachrichten) zwischenspeichern",
"cache_system": "Cache-Systemnachricht",
"cache_system_help": "Ob der System-Prompt zwischengespeichert werden soll",
"token_threshold": "Cache-Token-Schwellenwert",
"token_threshold_help": "Nachrichten, die diese Token-Anzahl überschreiten, werden zwischengespeichert. Auf 0 setzen, um das Caching zu deaktivieren."
},
"array_content": {
"help": "Unterstützt Array-Format für message content",
"label": "Unterstützt Array-Format für message content"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Προβολή",
"title": "Καριέρα"
},
"checkUpdate": {
"available": "Άμεση ενημέρωση",
"label": "Έλεγχος ενημερώσεων"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Κρύψτε τα τελευταία N μηνύματα",
"cache_last_n_help": "Αποθηκεύστε στην κρυφή μνήμη τα τελευταία N μηνύματα της συνομιλίας (εξαιρουμένων των μηνυμάτων συστήματος)",
"cache_system": "Μήνυμα Συστήματος Κρυφής Μνήμης",
"cache_system_help": "Εάν θα αποθηκευτεί προσωρινά το σύστημα εντολών",
"token_threshold": "Κατώφλι Διακριτικού Κρυφής Μνήμης",
"token_threshold_help": "Μηνύματα που υπερβαίνουν αυτό το όριο token θα αποθηκεύονται στην cache. Ορίστε το σε 0 για να απενεργοποιήσετε την προσωρινή αποθήκευση."
},
"array_content": {
"help": "Εάν ο πάροχος υποστηρίζει το πεδίο περιεχομένου του μηνύματος ως τύπο πίνακα",
"label": "Υποστήριξη για περιεχόμενο μηνύματος με μορφή πίνακα"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Vista",
"title": "Carreras"
},
"checkUpdate": {
"available": "Actualizar ahora",
"label": "Comprobar actualizaciones"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Caché de los últimos N mensajes",
"cache_last_n_help": "Almacenar en caché los últimos N mensajes de la conversación (excluyendo los mensajes del sistema)",
"cache_system": "Mensaje del Sistema de Caché",
"cache_system_help": "Si se debe almacenar en caché el mensaje del sistema",
"token_threshold": "Umbral de Token de Caché",
"token_threshold_help": "Los mensajes que superen este recuento de tokens se almacenarán en caché. Establecer en 0 para desactivar el almacenamiento en caché."
},
"array_content": {
"help": "¿Admite el proveedor que el campo content del mensaje sea de tipo array?",
"label": "Contenido del mensaje compatible con formato de matriz"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Vue",
"title": "Carrières"
},
"checkUpdate": {
"available": "Mettre à jour maintenant",
"label": "Vérifier les mises à jour"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Mettre en cache les N derniers messages",
"cache_last_n_help": "Mettre en cache les N derniers messages de conversation (à lexclusion des messages système)",
"cache_system": "Message du système de cache",
"cache_system_help": "S'il faut mettre en cache l'invite système",
"token_threshold": "Seuil de jeton de cache",
"token_threshold_help": "Les messages dépassant ce nombre de jetons seront mis en cache. Mettre à 0 pour désactiver la mise en cache."
},
"array_content": {
"help": "Ce fournisseur prend-il en charge le champ content du message sous forme de tableau ?",
"label": "Prise en charge du format de tableau pour le contenu du message"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "表示",
"title": "キャリア"
},
"checkUpdate": {
"available": "今すぐ更新",
"label": "更新を確認"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "最後のN件のメッセージをキャッシュ",
"cache_last_n_help": "最後のN件の会話メッセージをキャッシュするシステムメッセージは除く",
"cache_system": "キャッシュシステムメッセージ",
"cache_system_help": "システムプロンプトをキャッシュするかどうか",
"token_threshold": "キャッシュトークン閾値",
"token_threshold_help": "このトークン数を超えるメッセージはキャッシュされます。キャッシュを無効にするには0を設定してください。"
},
"array_content": {
"help": "このプロバイダーは、message の content フィールドが配列型であることをサポートしていますか",
"label": "配列形式のメッセージコンテンツをサポート"

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Visualizar",
"title": "Carreiras"
},
"checkUpdate": {
"available": "Atualizar agora",
"label": "Verificar atualizações"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Cache Últimas N Mensagens",
"cache_last_n_help": "Armazenar em cache as últimas N mensagens da conversa (excluindo mensagens do sistema)",
"cache_system": "Mensagem do Sistema de Cache",
"cache_system_help": "Se deve armazenar em cache o prompt do sistema",
"token_threshold": "Limite de Token de Cache",
"token_threshold_help": "Mensagens que excederem essa contagem de tokens serão armazenadas em cache. Defina como 0 para desativar o cache."
},
"array_content": {
"help": "O fornecedor suporta que o campo content da mensagem seja do tipo array?",
"label": "suporta o formato de matriz do conteúdo da mensagem"

View File

@ -378,7 +378,7 @@
"about": "Despre",
"close": "Închide fereastra",
"copy": "Copiază",
"cut": "Taie",
"cut": "Decupează",
"delete": "Șterge",
"documentation": "Documentație",
"edit": "Editare",
@ -1191,7 +1191,7 @@
"agent_one": "Agent",
"agent_other": "Agenți",
"and": "și",
"assistant": "Agent",
"assistant": "Asistent",
"assistant_one": "Asistent",
"assistant_other": "Asistenți",
"avatar": "Avatar",
@ -1208,7 +1208,7 @@
"copy": "Copiază",
"copy_failed": "Copiere eșuată",
"current": "Curent",
"cut": "Taie",
"cut": "Decupează",
"default": "Implicit",
"delete": "Șterge",
"delete_confirm": "Ești sigur că vrei să ștergi?",
@ -1222,10 +1222,10 @@
"duplicate": "Duplică",
"edit": "Editează",
"enabled": "Activat",
"error": "eroare",
"error": "Eroare",
"errors": {
"create_message": "Nu s-a putut crea mesajul",
"validation": "Verificarea a eșuat"
"validation": "Validarea a eșuat"
},
"expand": "Extinde",
"file": {
@ -1611,7 +1611,7 @@
"title": "Setări bază de cunoștințe"
},
"sitemap_added": "Adăugat cu succes",
"sitemap_placeholder": "Introdu URL-ul hărții site-ului",
"sitemap_placeholder": "Introdu URL-ul sitemap-ului",
"sitemaps": "Site-uri web",
"source": "Sursă",
"status": "Stare",
@ -1623,7 +1623,7 @@
"status_pending": "În așteptare",
"status_preprocess_completed": "Preprocesare finalizată",
"status_preprocess_failed": "Preprocesare eșuată",
"status_processing": "Se procesează",
"status_processing": "În procesare",
"subtitle_file": "fișier subtitrare",
"threshold": "Prag de potrivire",
"threshold_placeholder": "Nesetat",
@ -1633,9 +1633,9 @@
"topN": "Număr rezultate returnate",
"topN_placeholder": "Nesetat",
"topN_too_large_or_small": "Numărul de rezultate returnate nu poate fi mai mare de 30 sau mai mic de 1.",
"topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și mai mulți tokeni consumați.",
"topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și un consum mai mare de tokeni.",
"url_added": "URL adăugat",
"url_placeholder": "Introdu URL, separă URL-urile multiple prin Enter",
"url_placeholder": "Introdu URL-ul; separă mai multe URL-uri prin Enter",
"urls": "URL-uri",
"videos": "video",
"videos_file": "fișier video"
@ -1760,7 +1760,7 @@
"switch_user_confirm": "Schimbi contextul de utilizator la {{user}}?",
"time": "Timp",
"title": "Amintiri",
"total_memories": "total amintiri",
"total_memories": "Total amintiri",
"try_different_filters": "Încearcă să ajustezi criteriile de căutare",
"update_failed": "Nu s-a putut actualiza amintirea",
"update_success": "Amintire actualizată cu succes",
@ -1779,7 +1779,7 @@
"user_memories_reset": "Toate amintirile pentru {{user}} au fost resetate",
"user_switch_failed": "Nu s-a putut schimba utilizatorul",
"user_switched": "Contextul de utilizator a fost schimbat la {{user}}",
"users": "utilizatori"
"users": "Utilizatori"
},
"message": {
"agents": {
@ -2196,7 +2196,7 @@
"navbar": {
"expand": "Extinde dialogul",
"hide_sidebar": "Ascunde bara laterală",
"show_sidebar": "Arată bara laterală",
"show_sidebar": "Afișează bara laterală",
"window": {
"close": "Închide",
"maximize": "Maximizează",
@ -2216,27 +2216,27 @@
},
"characters": "Caractere",
"collapse": "Restrânge",
"content_placeholder": "Te rugăm să introduci conținutul notiței...",
"content_placeholder": "Introdu conținutul notiței...",
"copyContent": "Copiază conținutul",
"crossPlatformRestoreWarning": "Configurația multi-platformă a fost restaurată, dar directorul de notițe este gol. Te rugăm să copiezi fișierele notițelor în: {{path}}",
"delete": "șterge",
"delete": "Șterge",
"delete_confirm": "Ești sigur că vrei să ștergi acest {{type}}?",
"delete_folder_confirm": "Ești sigur că vrei să ștergi dosarul \"{{name}}\" și tot conținutul său?",
"delete_note_confirm": "Ești sigur că vrei să ștergi notița \"{{name}}\"?",
"drop_markdown_hint": "Trage fișiere sau dosare .md aici pentru a importa",
"empty": "Încă nu există notițe disponibile",
"expand": "desfășoară",
"expand": "Extinde",
"export_failed": "Exportul în baza de cunoștințe a eșuat",
"export_knowledge": "Exportă notițele în baza de cunoștințe",
"export_success": "Exportat cu succes în baza de cunoștințe",
"folder": "dosar",
"folder": "Dosar",
"new_folder": "Dosar nou",
"new_note": "Creează o notiță nouă",
"no_content_to_copy": "Niciun conținut de copiat",
"no_file_selected": "Te rugăm să selectezi fișierul de încărcat",
"no_valid_files": "Nu a fost încărcat niciun fișier valid",
"open_folder": "Deschide un dosar extern",
"open_outside": "Deschide din exterior",
"open_outside": "Deschide extern",
"rename": "Redenumește",
"rename_changed": "Din cauza politicilor de securitate, numele fișierului a fost schimbat din {{original}} în {{final}}",
"save": "Salvează în Notițe",
@ -2275,7 +2275,7 @@
"font_size_small": "Mic",
"font_title": "Setări font",
"serif_font": "Font cu serife",
"show_table_of_contents": "Arată cuprinsul",
"show_table_of_contents": "Afișează cuprinsul",
"show_table_of_contents_description": "Afișează o bară laterală cu cuprinsul pentru o navigare ușoară în documente",
"title": "Setări afișare"
},
@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Vedere",
"title": "Carieră"
},
"checkUpdate": {
"available": "Actualizare",
"label": "Verifică actualizări"
@ -4258,7 +4262,7 @@
"display_title": "Setări afișare mini-aplicații",
"empty": "Trage mini-aplicațiile din stânga pentru a le ascunde",
"open_link_external": {
"title": "Deschide linkurile de fereastră nouă în browser"
"title": "Deschide în browser linkurile care deschid ferestre noi"
},
"reset_tooltip": "Resetează la implicit",
"sidebar_description": "Arată mini-aplicațiile active în bara laterală",
@ -4358,7 +4362,7 @@
"description": "Model folosit pentru sarcini simple, cum ar fi numirea subiectelor și extragerea cuvintelor cheie",
"label": "Model rapid",
"setting_title": "Configurare model rapid",
"tooltip": "Se recomandă alegerea unui model ușor și nu se recomandă alegerea unui model de gândire."
"tooltip": "Se recomandă alegerea unui model ușor, nu a unui model de raționament complex."
},
"topic_naming": {
"auto": "Numire automată subiect",
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Cache Ultimelor N Mesaje",
"cache_last_n_help": "Stochează ultimele N mesaje din conversație (excluzând mesajele de sistem)",
"cache_system": "Mesaj de sistem Cache",
"cache_system_help": "Dacă să se memoreze în cache promptul de sistem",
"token_threshold": "Prag de Token Cache",
"token_threshold_help": "Mesajele care depășesc acest număr de tokeni vor fi memorate în cache. Setați la 0 pentru a dezactiva memorarea în cache."
},
"array_content": {
"help": "Furnizorul acceptă ca câmpul content al mesajului să fie de tip array?",
"label": "Acceptă conținut mesaj în format array"
@ -4690,7 +4702,7 @@
},
"shortcuts": {
"action": "Acțiune",
"actions": "operațiune",
"actions": "Comandă",
"clear_shortcut": "Șterge comanda rapidă",
"clear_topic": "Șterge mesajele",
"copy_last_message": "Copiază ultimul mesaj",
@ -4721,8 +4733,8 @@
},
"theme": {
"color_primary": "Culoare primară",
"dark": "Întunecat",
"light": "Luminos",
"dark": "Întunecată",
"light": "Luminoasă",
"system": "Sistem",
"title": "Temă",
"window": {
@ -4884,21 +4896,21 @@
"translate": {
"custom": {
"delete": {
"description": "Ești sigur că vrei să ștergi?",
"description": "Ești sigur că vrei să ștergi această limbă?",
"title": "Șterge limba personalizată"
},
"error": {
"add": "Adăugarea a eșuat",
"delete": "Ștergerea a eșuat",
"langCode": {
"builtin": "Limba are suport integrat",
"empty": "Codul limbii este gol",
"builtin": "Limbă deja integrată",
"empty": "Codul limbii lipsește",
"exists": "Limba există deja",
"invalid": "Cod limbă invalid"
},
"update": "Actualizarea a eșuat",
"value": {
"empty": "Numele limbii nu poate fi gol",
"empty": "Numele limbii este obligatoriu",
"too_long": "Numele limbii este prea lung"
}
},
@ -4908,13 +4920,13 @@
"placeholder": "en-us"
},
"success": {
"add": "Adăugat cu succes",
"delete": "Șters cu succes",
"update": "Actualizare reușită"
"add": "Adăugată cu succes",
"delete": "Ștearsă cu succes",
"update": "Actualiza cu succes"
},
"table": {
"action": {
"title": "Operațiune"
"title": "Acțiuni"
}
},
"value": {
@ -4946,7 +4958,7 @@
"mcp-servers": "Servere MCP",
"memories": "Amintiri",
"notes": "Notițe",
"paintings": "Picturi",
"paintings": "Imagini",
"settings": "Setări",
"store": "Bibliotecă asistenți",
"translate": "Traducere"
@ -4990,7 +5002,7 @@
"detect": {
"method": {
"algo": {
"label": "algoritm",
"label": "Algoritm",
"tip": "Folosește biblioteca franc pentru detectarea limbii"
},
"auto": {
@ -5097,7 +5109,7 @@
"tray": {
"quit": "Ieșire",
"show_mini_window": "Asistent rapid",
"show_window": "Arată fereastra"
"show_window": "Afișează fereastra"
},
"update": {
"install": "Instalează",
@ -5113,7 +5125,7 @@
"words": {
"knowledgeGraph": "Grafic de cunoștințe",
"quit": "Ieșire",
"show_window": "Arată fereastra",
"show_window": "Afișează fereastra",
"visualization": "Vizualizare"
}
}

View File

@ -3115,6 +3115,10 @@
},
"settings": {
"about": {
"careers": {
"button": "Вид",
"title": "Карьера"
},
"checkUpdate": {
"available": "Обновить",
"label": "Проверить обновления"
@ -4475,6 +4479,14 @@
}
},
"options": {
"anthropic_cache": {
"cache_last_n": "Кэшировать последние N сообщений",
"cache_last_n_help": "Кэшировать последние N сообщений разговора (исключая системные сообщения)",
"cache_system": "Сообщение системы кэша",
"cache_system_help": "Кэшировать ли системный промпт",
"token_threshold": "Порог токена кэша",
"token_threshold_help": "Сообщения, превышающие это количество токенов, будут кэшироваться. Установите значение 0, чтобы отключить кэширование."
},
"array_content": {
"help": "Поддерживает ли данный провайдер тип массива для поля content в сообщении",
"label": "поддержка формата массива для содержимого сообщения"

View File

@ -0,0 +1,231 @@
import type { Model, Provider } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Mock CodeToolsPage which is the default export
vi.mock('../CodeToolsPage', () => ({ default: () => null }))
// Mock dependencies needed by CodeToolsPage
vi.mock('@renderer/hooks/useCodeTools', () => ({
useCodeTools: () => ({
selectedCliTool: codeTools.qwenCode,
selectedModel: null,
selectedTerminal: 'systemDefault',
environmentVariables: '',
directories: [],
currentDirectory: '',
canLaunch: true,
setCliTool: vi.fn(),
setModel: vi.fn(),
setTerminal: vi.fn(),
setEnvVars: vi.fn(),
setCurrentDir: vi.fn(),
removeDir: vi.fn(),
selectFolder: vi.fn()
})
}))
vi.mock('@renderer/hooks/useProvider', () => ({
useProviders: () => ({ providers: [] }),
useAllProviders: () => []
}))
vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn()
}))
vi.mock('@renderer/services/LoggerService', () => ({
loggerService: {
withContext: () => ({
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
warn: vi.fn()
})
}
}))
vi.mock('@renderer/store', () => ({
useAppDispatch: () => vi.fn(),
useAppSelector: () => false
}))
vi.mock('@renderer/aiCore', () => ({
default: class {
getBaseURL() {
return ''
}
getApiKey() {
return ''
}
}
}))
vi.mock('@renderer/utils/api', () => ({
formatApiHost: vi.fn((host) => {
if (!host) return ''
const normalized = host.replace(/\/$/, '').trim()
if (normalized.endsWith('#')) {
return normalized.replace(/#$/, '')
}
if (/\/v\d+(?:alpha|beta)?(?=\/|$)/i.test(normalized)) {
return normalized
}
return `${normalized}/v1`
})
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key })
}))
describe('generateToolEnvironment', () => {
const createMockModel = (id: string, provider: string): Model => ({
id,
name: id,
provider,
group: provider
})
const createMockProvider = (id: string, apiHost: string): Provider => ({
id,
type: 'openai',
name: id,
apiKey: 'test-key',
apiHost,
models: [],
isSystem: true
})
beforeEach(() => {
vi.clearAllMocks()
})
it('should format baseUrl with /v1 for qwenCode when missing', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-turbo', 'dashscope')
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode'
})
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
})
it('should not duplicate /v1 when already present for qwenCode', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-turbo', 'dashscope')
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
})
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
})
it('should handle empty baseUrl gracefully', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-turbo', 'dashscope')
const provider = createMockProvider('dashscope', '')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: ''
})
expect(env.OPENAI_BASE_URL).toBe('')
})
it('should preserve other API versions when present', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-plus', 'dashscope')
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://dashscope.aliyuncs.com/v2'
})
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2')
})
it('should format baseUrl with /v1 for openaiCodex when missing', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('gpt-4', 'openai')
const provider = createMockProvider('openai', 'https://api.openai.com')
const env = generateToolEnvironment({
tool: codeTools.openaiCodex,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://api.openai.com'
})
expect(env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
})
it('should format baseUrl with /v1 for iFlowCli when missing', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('gpt-4', 'iflow')
const provider = createMockProvider('iflow', 'https://api.iflow.cn')
const env = generateToolEnvironment({
tool: codeTools.iFlowCli,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://api.iflow.cn'
})
expect(env.IFLOW_BASE_URL).toBe('https://api.iflow.cn/v1')
})
it('should handle trailing slash correctly', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-turbo', 'dashscope')
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/'
})
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
})
it('should handle v2beta version correctly', async () => {
const { generateToolEnvironment } = await import('../index')
const model = createMockModel('qwen-plus', 'dashscope')
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2beta')
const env = generateToolEnvironment({
tool: codeTools.qwenCode,
model,
modelProvider: provider,
apiKey: 'test-key',
baseUrl: 'https://dashscope.aliyuncs.com/v2beta'
})
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2beta')
})
})

View File

@ -1,4 +1,5 @@
import { type EndpointType, type Model, type Provider, SystemProviderIds } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { codeTools } from '@shared/config/constant'
export interface LaunchValidationResult {
@ -145,6 +146,7 @@ export const generateToolEnvironment = ({
baseUrl: string
}): Record<string, string> => {
const env: Record<string, string> = {}
const formattedBaseUrl = formatApiHost(baseUrl)
switch (tool) {
case codeTools.claudeCode:
@ -169,19 +171,19 @@ export const generateToolEnvironment = ({
case codeTools.qwenCode:
env.OPENAI_API_KEY = apiKey
env.OPENAI_BASE_URL = baseUrl
env.OPENAI_BASE_URL = formattedBaseUrl
env.OPENAI_MODEL = model.id
break
case codeTools.openaiCodex:
env.OPENAI_API_KEY = apiKey
env.OPENAI_BASE_URL = baseUrl
env.OPENAI_BASE_URL = formattedBaseUrl
env.OPENAI_MODEL = model.id
env.OPENAI_MODEL_PROVIDER = modelProvider.id
break
case codeTools.iFlowCli:
env.IFLOW_API_KEY = apiKey
env.IFLOW_BASE_URL = baseUrl
env.IFLOW_BASE_URL = formattedBaseUrl
env.IFLOW_MODEL_NAME = model.id
break

View File

@ -1,4 +1,5 @@
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
import type { DraggableVirtualListRef } from '@renderer/components/DraggableList'
import { DraggableVirtualList } from '@renderer/components/DraggableList'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
@ -85,6 +86,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
const listRef = useRef<DraggableVirtualListRef>(null)
// 管理模式状态
const manageState = useTopicManageMode()
@ -168,10 +170,46 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
const onPinTopic = useCallback(
(topic: Topic) => {
let newIndex = 0
if (topic.pinned) {
// 取消固定:将话题移到未固定话题的顶部
const pinnedTopics = assistant.topics.filter((t) => t.pinned)
const unpinnedTopics = assistant.topics.filter((t) => !t.pinned)
// 构建新顺序:其他固定话题 + 取消固定的话题(移到顶部) + 其他未固定话题
const reorderedTopics = [
...pinnedTopics.filter((t) => t.id !== topic.id), // 其他固定话题
topic, // 取消固定的话题移到顶部
...unpinnedTopics // 其他未固定话题
]
newIndex = pinnedTopics.length - 1 // 最后一个固定话题的索引 + 1 = 第一个未固定的索引
updateTopics(reorderedTopics)
} else {
// 固定话题:移到固定区域顶部
const pinnedTopics = assistant.topics.filter((t) => t.pinned)
const unpinnedTopics = assistant.topics.filter((t) => !t.pinned)
const reorderedTopics = [
topic, // 新固定的话题移到顶部
...pinnedTopics, // 其他固定话题
...unpinnedTopics.filter((t) => t.id !== topic.id) // 其他未固定话题(排除 topic
]
newIndex = 0
updateTopics(reorderedTopics)
}
const updatedTopic = { ...topic, pinned: !topic.pinned }
updateTopic(updatedTopic)
// 延迟滚动到话题位置(等待渲染完成)
setTimeout(() => {
listRef.current?.scrollToIndex(newIndex, { align: 'auto' })
}, 50)
},
[updateTopic]
[assistant.topics, updateTopic, updateTopics]
)
const onDeleteTopic = useCallback(
@ -529,6 +567,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
return (
<>
<DraggableVirtualList
ref={listRef}
className="topics-tab"
list={filteredTopics}
onUpdate={updateTopics}
@ -663,7 +702,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
</TopicPromptText>
)}
{showTopicTime && (
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
<TopicTime className="time">{dayjs(topic.createdAt).format('YYYY/MM/DD HH:mm')}</TopicTime>
)}
</TopicListItem>
</Dropdown>

View File

@ -15,7 +15,7 @@ import { runAsyncFunction } from '@renderer/utils'
import { UpgradeChannel } from '@shared/config/constant'
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
import { Briefcase, Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
import { BadgeQuestionMark } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
@ -327,6 +327,16 @@ const AboutSettings: FC = () => {
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<Briefcase size={18} />
{t('settings.about.careers.title')}
</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://www.cherry-ai.com/careers')}>
{t('settings.about.careers.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
<Bug size={18} />

View File

@ -1,8 +1,9 @@
import { HStack } from '@renderer/components/Layout'
import { InfoTooltip } from '@renderer/components/TooltipIcons'
import { useProvider } from '@renderer/hooks/useProvider'
import type { Provider } from '@renderer/types'
import { Flex, Switch } from 'antd'
import { type AnthropicCacheControlSettings, type Provider } from '@renderer/types'
import { isSupportAnthropicPromptCacheProvider } from '@renderer/utils/provider'
import { Divider, Flex, InputNumber, Switch } from 'antd'
import { startTransition, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -114,6 +115,27 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
return items
}, [openAIOptions, provider.apiOptions, provider.type, t, updateProviderTransition])
const isSupportAnthropicPromptCache = isSupportAnthropicPromptCacheProvider(provider)
const cacheSettings = useMemo(
() =>
provider.anthropicCacheControl ?? {
tokenThreshold: 0,
cacheSystemMessage: true,
cacheLastNMessages: 0
},
[provider.anthropicCacheControl]
)
const updateCacheSettings = useCallback(
(updates: Partial<AnthropicCacheControlSettings>) => {
updateProviderTransition({
anthropicCacheControl: { ...cacheSettings, ...updates }
})
},
[cacheSettings, updateProviderTransition]
)
return (
<Flex vertical gap="middle">
{options.map((item) => (
@ -127,6 +149,52 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
<Switch id={item.key} checked={item.checked} onChange={item.onChange} />
</HStack>
))}
{isSupportAnthropicPromptCache && (
<>
<Divider style={{ margin: '8px 0' }} />
<HStack justifyContent="space-between">
<HStack alignItems="center" gap={6}>
<span>{t('settings.provider.api.options.anthropic_cache.token_threshold')}</span>
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.token_threshold_help')} />
</HStack>
<InputNumber
min={0}
max={100000}
value={cacheSettings.tokenThreshold}
onChange={(v) => updateCacheSettings({ tokenThreshold: v ?? 0 })}
style={{ width: 100 }}
/>
</HStack>
{cacheSettings.tokenThreshold > 0 && (
<>
<HStack justifyContent="space-between">
<HStack alignItems="center" gap={6}>
<span>{t('settings.provider.api.options.anthropic_cache.cache_system')}</span>
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.cache_system_help')} />
</HStack>
<Switch
checked={cacheSettings.cacheSystemMessage}
onChange={(v) => updateCacheSettings({ cacheSystemMessage: v })}
/>
</HStack>
<HStack justifyContent="space-between">
<HStack alignItems="center" gap={6}>
<span>{t('settings.provider.api.options.anthropic_cache.cache_last_n')}</span>
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.cache_last_n_help')} />
</HStack>
<InputNumber
min={0}
max={10}
value={cacheSettings.cacheLastNMessages}
onChange={(v) => updateCacheSettings({ cacheLastNMessages: v ?? 0 })}
style={{ width: 100 }}
/>
</HStack>
</>
)}
</>
)}
</Flex>
)
}

View File

@ -31,6 +31,7 @@ import {
isOllamaProvider,
isOpenAICompatibleProvider,
isOpenAIProvider,
isSupportAnthropicPromptCacheProvider,
isVertexProvider
} from '@renderer/utils/provider'
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
@ -400,7 +401,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
<Button type="text" size="small" icon={<SquareArrowOutUpRight size={14} />} />
</Link>
)}
{!isSystemProvider(provider) && (
{(!isSystemProvider(provider) || isSupportAnthropicPromptCacheProvider(provider)) && (
<Tooltip title={t('settings.provider.api.options.label')}>
<Button
type="text"

View File

@ -113,9 +113,10 @@ class LoggerService {
* @param data - Additional data to log
*/
private processLog(level: LogLevel, message: string, data: any[]): void {
let windowSource = this.window
if (!this.window) {
console.error('[LoggerService] window source not initialized, please initialize window source first')
return
windowSource = 'UNKNOWN'
}
const currentLevel = LEVEL_MAP[level]
@ -164,7 +165,7 @@ class LoggerService {
if (currentLevel >= LEVEL_MAP[this.logToMainLevel] || forceLogToMain) {
const source: LogSourceWithContext = {
process: 'renderer',
window: this.window,
window: windowSource,
module: this.module
}

View File

@ -43,6 +43,8 @@ const initialState: AssistantsState = {
unifiedListOrder: []
}
const normalizeTopics = (topics: unknown): Topic[] => (Array.isArray(topics) ? topics : [])
const assistantsSlice = createSlice({
name: 'assistants',
initialState,
@ -127,7 +129,7 @@ const assistantsSlice = createSlice({
assistant.id === action.payload.assistantId
? {
...assistant,
topics: uniqBy([topic, ...assistant.topics], 'id')
topics: uniqBy([topic, ...normalizeTopics(assistant.topics)], 'id')
}
: assistant
)
@ -137,7 +139,7 @@ const assistantsSlice = createSlice({
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.filter(({ id }) => id !== action.payload.topic.id)
topics: normalizeTopics(assistant.topics).filter(({ id }) => id !== action.payload.topic.id)
}
: assistant
)
@ -149,7 +151,7 @@ const assistantsSlice = createSlice({
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.map((topic) => {
topics: normalizeTopics(assistant.topics).map((topic) => {
const _topic = topic.id === newTopic.id ? newTopic : topic
_topic.messages = []
return _topic
@ -173,7 +175,7 @@ const assistantsSlice = createSlice({
removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => {
state.assistants = state.assistants.map((assistant) => {
if (assistant.id === action.payload.assistantId) {
assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id))
normalizeTopics(assistant.topics).forEach((topic) => TopicManager.removeTopic(topic.id))
return {
...assistant,
topics: [getDefaultTopic(assistant.id)]
@ -184,7 +186,7 @@ const assistantsSlice = createSlice({
},
updateTopicUpdatedAt: (state, action: PayloadAction<{ topicId: string }>) => {
outer: for (const assistant of state.assistants) {
for (const topic of assistant.topics) {
for (const topic of normalizeTopics(assistant.topics)) {
if (topic.id === action.payload.topicId) {
topic.updatedAt = new Date().toISOString()
break outer
@ -268,7 +270,7 @@ export const {
} = assistantsSlice.actions
export const selectAllTopics = createSelector([(state: RootState) => state.assistants.assistants], (assistants) =>
assistants.flatMap((assistant: Assistant) => assistant.topics)
assistants.flatMap((assistant: Assistant) => normalizeTopics(assistant.topics))
)
export const selectTopicsMap = createSelector([selectAllTopics], (topics) => {

View File

@ -123,7 +123,15 @@ export const McpServerConfigSchema = z
*
* 60
*/
timeout: z.number().optional().describe('Timeout in seconds for requests to this server'),
timeout: z
.preprocess((val) => {
if (typeof val === 'string' && val.trim() !== '') {
const parsed = Number(val)
return isNaN(parsed) ? val : parsed
}
return val
}, z.number().optional())
.describe('Timeout in seconds for requests to this server'),
/**
* DXT包版本号
* DXT包的版本

View File

@ -79,6 +79,12 @@ export function isGroqServiceTier(tier: string | undefined | null): tier is Groq
export type ServiceTier = OpenAIServiceTier | GroqServiceTier
export type AnthropicCacheControlSettings = {
tokenThreshold: number
cacheSystemMessage: boolean
cacheLastNMessages: number
}
export function isServiceTier(tier: string | null | undefined): tier is ServiceTier {
return isGroqServiceTier(tier) || isOpenAIServiceTier(tier)
}
@ -127,6 +133,9 @@ export type Provider = {
isVertex?: boolean
notes?: string
extra_headers?: Record<string, string>
// Anthropic prompt caching settings
anthropicCacheControl?: AnthropicCacheControlSettings
}
export const SystemProviderIdSchema = z.enum([

View File

@ -181,47 +181,6 @@ describe('fetch', () => {
consoleSpy.mockRestore()
})
it('should throttle requests to the same domain', async () => {
const fetchCallTimes: number[] = []
vi.mocked(global.fetch).mockImplementation(async () => {
fetchCallTimes.push(Date.now())
return createMockResponse()
})
// 3 URLs from the same domain
const urls = ['https://zhihu.com/a', 'https://zhihu.com/b', 'https://zhihu.com/c']
await fetchWebContents(urls)
expect(fetchCallTimes).toHaveLength(3)
// Verify that requests are spaced out (at least 400ms apart due to 500ms interval)
if (fetchCallTimes.length >= 2) {
const timeDiff1 = fetchCallTimes[1] - fetchCallTimes[0]
expect(timeDiff1).toBeGreaterThanOrEqual(400)
}
if (fetchCallTimes.length >= 3) {
const timeDiff2 = fetchCallTimes[2] - fetchCallTimes[1]
expect(timeDiff2).toBeGreaterThanOrEqual(400)
}
})
it('should allow parallel requests to different domains', async () => {
const fetchCallTimes: Map<string, number> = new Map()
vi.mocked(global.fetch).mockImplementation(async (url) => {
fetchCallTimes.set(url as string, Date.now())
return createMockResponse()
})
// URLs from different domains
const urls = ['https://zhihu.com/a', 'https://douban.com/b', 'https://github.com/c']
await fetchWebContents(urls)
expect(fetchCallTimes.size).toBe(3)
// Different domains should start nearly simultaneously (within 100ms)
const times = Array.from(fetchCallTimes.values())
const maxDiff = Math.max(...times) - Math.min(...times)
expect(maxDiff).toBeLessThan(100)
})
})
describe('fetchRedirectUrl', () => {

View File

@ -4,7 +4,6 @@ import { nanoid } from '@reduxjs/toolkit'
import type { WebSearchProviderResult } from '@renderer/types'
import { createAbortPromise } from '@renderer/utils/abortController'
import { isAbortError } from '@renderer/utils/error'
import PQueue from 'p-queue'
import TurndownService from 'turndown'
const logger = loggerService.withContext('Utils:fetch')
@ -14,33 +13,6 @@ export const noContent = 'No content found'
type ResponseFormat = 'markdown' | 'html' | 'text'
// Domain queue management for throttling requests to the same domain
const domainQueues = new Map<string, PQueue>()
const DOMAIN_CONCURRENCY = 1
const DOMAIN_INTERVAL = 500 // ms between requests to the same domain
function getDomainQueue(domain: string): PQueue {
if (!domainQueues.has(domain)) {
domainQueues.set(
domain,
new PQueue({
concurrency: DOMAIN_CONCURRENCY,
interval: DOMAIN_INTERVAL,
intervalCap: 1
})
)
}
return domainQueues.get(domain)!
}
function getDomain(url: string): string {
try {
return new URL(url).hostname
} catch {
return 'unknown'
}
}
/**
* Validates if the string is a properly formatted URL
*/
@ -59,15 +31,10 @@ export async function fetchWebContents(
usingBrowser: boolean = false,
httpOptions: RequestInit = {}
): Promise<WebSearchProviderResult[]> {
const results = await Promise.allSettled(
urls.map((url) => {
const domain = getDomain(url)
const queue = getDomainQueue(domain)
return queue.add(() => fetchWebContent(url, format, usingBrowser, httpOptions), { throwOnTimeout: true })
})
)
// parallel using fetchWebContent
const results = await Promise.allSettled(urls.map((url) => fetchWebContent(url, format, usingBrowser, httpOptions)))
return results.map((result, index) => {
if (result.status === 'fulfilled' && result.value) {
if (result.status === 'fulfilled') {
return result.value
} else {
return {

View File

@ -198,3 +198,13 @@ export const NOT_SUPPORT_API_KEY_PROVIDERS: readonly SystemProviderId[] = [
]
export const NOT_SUPPORT_API_KEY_PROVIDER_TYPES: readonly ProviderType[] = ['vertexai', 'aws-bedrock']
// https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration
export const isSupportAnthropicPromptCacheProvider = (provider: Provider) => {
return (
provider.type === 'anthropic' ||
isNewApiProvider(provider) ||
provider.id === SystemProviderIds.aihubmix ||
isAzureOpenAIProvider(provider)
)
}