mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +08:00
merge main into v2
This commit is contained in:
commit
ad37f0991a
27
.github/workflows/auto-i18n.yml
vendored
27
.github/workflows/auto-i18n.yml
vendored
@ -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}"
|
||||
|
||||
88
.github/workflows/github-issue-tracker.yml
vendored
88
.github/workflows/github-issue-tracker.yml
vendored
@ -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. **移除标签**
|
||||
|
||||
34
.github/workflows/sync-to-gitcode.yml
vendored
34
.github/workflows/sync-to-gitcode.yml
vendored
@ -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}"
|
||||
|
||||
155
docs/en/references/feishu-notify.md
Normal file
155
docs/en/references/feishu-notify.md
Normal 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()`
|
||||
155
docs/zh/references/feishu-notify.md
Normal file
155
docs/zh/references/feishu-notify.md
Normal 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()` 注册命令
|
||||
@ -144,34 +144,30 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
Cherry Studio 1.7.11 - 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
|
||||
- [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.11 - 新功能与问题修复
|
||||
Cherry Studio 1.7.13 - 安全与问题修复
|
||||
|
||||
✨ 新功能
|
||||
- [MCP] 新增 MCP Hub 智能模式,可自动管理和调用多个 MCP 服务器工具
|
||||
🔒 安全修复
|
||||
- [插件] 修复 Windows 系统 DXT 插件的安全漏洞
|
||||
|
||||
🐛 问题修复
|
||||
- [对话] 修复部分代理模型的推理过程无法正确显示的问题
|
||||
- [对话] 修复操作按钮重复显示加载状态的问题
|
||||
- [编辑器] 修复段落手柄和加号按钮无法点击的问题
|
||||
- [绘图] 修复 TokenFlux 模型在绘图面板不显示的问题
|
||||
- [翻译] 修复翻译功能初始化后卡住的问题
|
||||
- [错误] 修复查看包含大图片的错误详情时应用卡死的问题
|
||||
- [笔记] 修复文件夹遮挡网页预览的问题
|
||||
- [对话] 修复停止生成时思考时间显示问题
|
||||
- [Agent] 修复系统未安装 Node.js 时 Agent 功能无法使用的问题
|
||||
- [对话] 修复打开某些智能体时应用崩溃的问题
|
||||
- [对话] 修复部分服务商推理过程无法正确显示的问题
|
||||
- [对话] 修复流式对话时的内存泄漏问题
|
||||
- [MCP] 修复 MCP 配置的 timeout 字段不支持字符串格式的问题
|
||||
- [设置] 关于页面新增招聘入口
|
||||
<!--LANG:END-->
|
||||
|
||||
28
package.json
28
package.json
@ -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",
|
||||
@ -271,6 +271,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",
|
||||
@ -456,7 +457,8 @@
|
||||
"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-npm-1.0.28-5705188855.patch",
|
||||
"@anthropic-ai/claude-agent-sdk@0.1.76": "patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@kangfenmao/keyv-storage",
|
||||
@ -484,5 +486,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"
|
||||
}
|
||||
}
|
||||
|
||||
33
patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch
Normal file
33
patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch
Normal 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) => {
|
||||
253
pnpm-lock.yaml
253
pnpm-lock.yaml
@ -34,6 +34,9 @@ patchedDependencies:
|
||||
'@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
|
||||
@ -86,7 +89,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
|
||||
@ -688,6 +691,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
|
||||
@ -1123,6 +1129,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:
|
||||
@ -4237,6 +4304,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==}
|
||||
|
||||
'@oxc-project/types@0.95.0':
|
||||
resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==}
|
||||
|
||||
@ -5333,6 +5403,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.45':
|
||||
resolution: {integrity: sha512-xjCv4CRVsSnnIxTuyH1RDJl5OEQ1c9JYOwfDAHddjJDxCw46ZX9q80+xq7Eok7KC4bRSZudMJllkvOKv0T9SeA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5351,6 +5427,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.45':
|
||||
resolution: {integrity: sha512-ddcO9TD3D/CLUa/l8GO8LHzBOaZqWg5ClMy3jICoxwCuoz47h9dtqPsIeTiB6yR501LQTeDsjA4lIFd7u3Ljfw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5369,6 +5451,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.45':
|
||||
resolution: {integrity: sha512-MBTWdrzW9w+UMYDUvnEuh0pQvLENkl2Sis15fHTfHVW7ClbGuez+RWopZudIDEGkpZXdeI4CkRXk+vdIIebrmg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5387,6 +5475,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.45':
|
||||
resolution: {integrity: sha512-4YgoCFiki1HR6oSg+GxxfzfnVCesQxLF1LEnw9uXS/MpBmuog0EOO2rYfy69rWP4tFZL9IWp6KEfGZLrZ7aUog==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5405,6 +5499,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.45':
|
||||
resolution: {integrity: sha512-LE1gjAwQRrbCOorJJ7LFr10s5vqYf5a00V5Ea9wXcT2+56n5YosJkcp8eQ12FxRBv2YX8dsdQJb+ZTtYJwb6XQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5423,6 +5523,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.45':
|
||||
resolution: {integrity: sha512-tdy8ThO/fPp40B81v0YK3QC+KODOmzJzSUOO37DinQxzlTJ026gqUSOM8tzlVixRbQJltgVDCTYF8HNPRErQTA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5441,6 +5547,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.45':
|
||||
resolution: {integrity: sha512-lS082ROBWdmOyVY/0YB3JmsiClaWoxvC+dA8/rbhyB9VLkvVEaihLEOr4CYmrMse151C4+S6hCw6oa1iewox7g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5459,6 +5571,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.45':
|
||||
resolution: {integrity: sha512-Hi73aYY0cBkr1/SvNQqH8Cd+rSV6S9RB5izCv0ySBcRnd/Wfn5plguUoGYwBnhHgFbh6cPw9m2dUVBR6BG1gxA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5477,6 +5595,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.45':
|
||||
resolution: {integrity: sha512-fljEqbO7RHHogNDxYtTzr+GNjlfOx21RUyGmF+NrkebZ8emYYiIqzPxsaMZuRx0rgZmVmliOzEp86/CQFDKhJQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5495,6 +5619,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.45':
|
||||
resolution: {integrity: sha512-ZJDB7lkuZE9XUnWQSYrBObZxczut+8FZ5pdanm8nNS1DAo8zsrPuvGwn+U3fwU98WaiFsNrA4XHngesCGr8tEQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -5510,6 +5640,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.45':
|
||||
resolution: {integrity: sha512-zyzAjItHPUmxg6Z8SyRhLdXlJn3/D9KL5b9mObUrBHhWS/GwRH4665xCiFqeuktAhhWutqfc+rOV2LjK4VYQGQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5528,6 +5663,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-ia32-msvc@1.0.0-beta.45':
|
||||
resolution: {integrity: sha512-wODcGzlfxqS6D7BR0srkJk3drPwXYLu7jPHN27ce2c4PUnVVmJnp9mJzUQGT4LpmHmmVdMZ+P6hKvyTGBzc1CA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -5552,6 +5693,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==}
|
||||
|
||||
@ -5564,6 +5711,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/pluginutils@5.3.0':
|
||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -6040,6 +6190,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@strongtz/win32-arm64-msvc@0.4.7':
|
||||
resolution: {integrity: sha512-LMU0CVfKGxKzIUawkDw6pwCNFb64DSwqEQhVAfEdLGMZcvVIdJghWB9yMW+72DHzALpwHscIssAJGhYAT4AbfA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0':
|
||||
resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==}
|
||||
engines: {node: '>=14'}
|
||||
@ -8077,6 +8232,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==}
|
||||
|
||||
@ -12695,6 +12854,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'}
|
||||
@ -14611,7 +14775,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:
|
||||
@ -18405,6 +18569,8 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.106.0': {}
|
||||
|
||||
'@oxc-project/types@0.107.0': {}
|
||||
|
||||
'@oxc-project/types@0.95.0': {}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.2.1':
|
||||
@ -19811,6 +19977,9 @@ 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.45':
|
||||
optional: true
|
||||
|
||||
@ -19820,6 +19989,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19829,6 +20001,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19838,6 +20013,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19847,6 +20025,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19856,6 +20037,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19865,6 +20049,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19874,6 +20061,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19883,6 +20073,9 @@ snapshots:
|
||||
'@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.45':
|
||||
optional: true
|
||||
|
||||
@ -19892,6 +20085,9 @@ snapshots:
|
||||
'@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.45':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
@ -19907,6 +20103,11 @@ 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.45':
|
||||
optional: true
|
||||
|
||||
@ -19916,6 +20117,9 @@ snapshots:
|
||||
'@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-ia32-msvc@1.0.0-beta.45':
|
||||
optional: true
|
||||
|
||||
@ -19928,6 +20132,9 @@ snapshots:
|
||||
'@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.45': {}
|
||||
@ -19936,6 +20143,8 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.58': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.59': {}
|
||||
|
||||
'@rollup/pluginutils@5.3.0(rollup@4.55.1)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
@ -20511,6 +20720,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@strongtz/win32-arm64-msvc@0.4.7':
|
||||
optional: true
|
||||
|
||||
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@ -23190,6 +23402,8 @@ snapshots:
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.2: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@5.1.0: {}
|
||||
@ -28590,7 +28804,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
|
||||
@ -28600,7 +28814,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
|
||||
@ -28608,7 +28822,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
|
||||
@ -28618,7 +28832,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
|
||||
@ -28736,6 +28950,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(rollup@4.55.1):
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
@ -29665,8 +29898,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
|
||||
@ -29689,8 +29922,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
|
||||
|
||||
@ -1,2 +1,8 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
|
||||
supportedArchitectures:
|
||||
os:
|
||||
- current
|
||||
cpu:
|
||||
- current
|
||||
|
||||
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
421
scripts/feishu-notify.ts
Normal 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()
|
||||
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +87,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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1088,18 +1088,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
|
||||
}
|
||||
|
||||
|
||||
202
src/main/services/__tests__/DxtService.test.ts
Normal file
202
src/main/services/__tests__/DxtService.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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': {
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Anthropic Prompt Caching Middleware
|
||||
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
|
||||
*/
|
||||
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: unknown): number {
|
||||
if (typeof content === 'string') return estimateTextTokens(content)
|
||||
if (Array.isArray(content)) {
|
||||
return content.reduce((acc, part) => {
|
||||
if (typeof part === 'object' && part !== null && 'text' in part) {
|
||||
return acc + estimateTextTokens(part.text as string)
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function addCacheToContentParts(content: unknown): unknown {
|
||||
if (typeof content === 'string') {
|
||||
return [{ type: 'text', text: content, providerOptions: cacheProviderOptions }]
|
||||
}
|
||||
if (Array.isArray(content) && content.length > 0) {
|
||||
const result = [...content]
|
||||
const last = result[result.length - 1]
|
||||
if (typeof last === 'object' && last !== null) {
|
||||
result[result.length - 1] = { ...last, providerOptions: cacheProviderOptions }
|
||||
}
|
||||
return result
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
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 any
|
||||
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) {
|
||||
for (let i = messages.length - 1; i >= 0 && cachedCount < cacheLastNMessages; i--) {
|
||||
const msg = messages[i] as any
|
||||
if (msg.role !== 'system' && estimateContentTokens(msg.content) >= tokenThreshold) {
|
||||
messages[i] = { ...msg, content: addCacheToContentParts(msg.content) }
|
||||
cachedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...params, prompt: messages }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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": "Υποστήριξη για περιεχόμενο μηνύματος με μορφή πίνακα"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 (à l’exclusion 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"
|
||||
|
||||
@ -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": "配列形式のメッセージコンテンツをサポート"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -3115,6 +3115,10 @@
|
||||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"careers": {
|
||||
"button": "Vedere",
|
||||
"title": "Carieră"
|
||||
},
|
||||
"checkUpdate": {
|
||||
"available": "Actualizare",
|
||||
"label": "Verifică actualizări"
|
||||
@ -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"
|
||||
|
||||
@ -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": "поддержка формата массива для содержимого сообщения"
|
||||
|
||||
@ -16,7 +16,7 @@ import { ThemeMode, UpgradeChannel } from '@shared/data/preference/preferenceTyp
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Avatar, Progress, Radio, Row, Tag } 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'
|
||||
@ -324,6 +324,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} />
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { ColFlex, RowFlex } from '@cherrystudio/ui'
|
||||
import { Switch } from '@cherrystudio/ui'
|
||||
import { ColFlex, RowFlex, Switch } from '@cherrystudio/ui'
|
||||
import { InfoTooltip } from '@cherrystudio/ui'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import type { Provider } from '@renderer/types'
|
||||
import { type AnthropicCacheControlSettings, type Provider } from '@renderer/types'
|
||||
import { isSupportAnthropicPromptCacheProvider } from '@renderer/utils/provider'
|
||||
import { Divider, InputNumber } from 'antd'
|
||||
import { startTransition, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -114,11 +115,32 @@ 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 (
|
||||
<ColFlex className="gap-4">
|
||||
{options.map((item) => (
|
||||
<RowFlex key={item.key} className="justify-between">
|
||||
<RowFlex className="items-center gap-1.5">
|
||||
<RowFlex className="items-center gap-2">
|
||||
<label style={{ cursor: 'pointer' }} htmlFor={item.key}>
|
||||
{item.label}
|
||||
</label>
|
||||
@ -127,6 +149,52 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
|
||||
<Switch id={item.key} checked={item.checked} onCheckedChange={item.onChange} />
|
||||
</RowFlex>
|
||||
))}
|
||||
|
||||
{isSupportAnthropicPromptCache && (
|
||||
<>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<RowFlex className="justify-between">
|
||||
<RowFlex className="items-center gap-2">
|
||||
<span>{t('settings.provider.api.options.anthropic_cache.token_threshold')}</span>
|
||||
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.token_threshold_help')} />
|
||||
</RowFlex>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100000}
|
||||
value={cacheSettings.tokenThreshold}
|
||||
onChange={(v) => updateCacheSettings({ tokenThreshold: v ?? 0 })}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</RowFlex>
|
||||
{cacheSettings.tokenThreshold > 0 && (
|
||||
<>
|
||||
<RowFlex className="justify-between">
|
||||
<RowFlex className="items-center gap-2">
|
||||
<span>{t('settings.provider.api.options.anthropic_cache.cache_system')}</span>
|
||||
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.cache_system_help')} />
|
||||
</RowFlex>
|
||||
<Switch
|
||||
checked={cacheSettings.cacheSystemMessage}
|
||||
onCheckedChange={(v) => updateCacheSettings({ cacheSystemMessage: v })}
|
||||
/>
|
||||
</RowFlex>
|
||||
<RowFlex className="justify-between">
|
||||
<RowFlex className="items-center gap-2">
|
||||
<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')} />
|
||||
</RowFlex>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={10}
|
||||
value={cacheSettings.cacheLastNMessages}
|
||||
onChange={(v) => updateCacheSettings({ cacheLastNMessages: v ?? 0 })}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</RowFlex>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ColFlex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
isOllamaProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
isOpenAIProvider,
|
||||
isSupportAnthropicPromptCacheProvider,
|
||||
isVertexProvider
|
||||
} from '@renderer/utils/provider'
|
||||
import { Divider, Input, Select, Space } from 'antd'
|
||||
@ -403,7 +404,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{!isSystemProvider(provider) && (
|
||||
{(!isSystemProvider(provider) || isSupportAnthropicPromptCacheProvider(provider)) && (
|
||||
<Tooltip content={t('settings.provider.api.options.label')}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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包的版本。
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user