mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-13 21:57:30 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9414f13f6d | ||
|
|
cbeda03acb | ||
|
|
cea36d170b | ||
|
|
d84b84eb2f | ||
|
|
c7c380d706 | ||
|
|
622e3f0db6 | ||
|
|
e5a2980da8 | ||
|
|
5b5e190132 | ||
|
|
e8e8f028f3 | ||
|
|
8ab082ceb5 | ||
|
|
864eda68fb | ||
|
|
c5ea42ca3a | ||
|
|
bdf8f103c8 | ||
|
|
7a7089e315 | ||
|
|
9b8420f9b9 | ||
|
|
29d8c4a7ed | ||
|
|
76cc196667 | ||
|
|
61aae7376a | ||
|
|
74e1d0887d | ||
|
|
2a1722bb52 | ||
|
|
7ff6955870 | ||
|
|
008df2d4b7 | ||
|
|
8223c9fbfd | ||
|
|
153c1024f6 | ||
|
|
43a48a4a38 | ||
|
|
0cb3bd8311 | ||
|
|
2f67b63057 | ||
|
|
81ea847989 | ||
|
|
1d07e89e38 |
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()` 注册命令
|
||||
@ -143,34 +143,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-->
|
||||
|
||||
@ -84,7 +84,7 @@ export default defineConfig([
|
||||
{
|
||||
selector: 'CallExpression[callee.object.name="console"]',
|
||||
message:
|
||||
'❗CherryStudio uses unified LoggerService: 📖 docs/technical/how-to-use-logger-en.md\n❗CherryStudio 使用统一的日志服务:📖 docs/technical/how-to-use-logger-zh.md\n\n'
|
||||
'❗CherryStudio uses unified LoggerService: 📖 docs/en/guides/logging.md\n❗CherryStudio 使用统一的日志服务:📖 docs/zh/guides/logging.md\n\n'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
40
package.json
40
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.7.11",
|
||||
"version": "1.7.13",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@ -42,7 +42,7 @@
|
||||
"i18n:check": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
||||
"i18n:sync": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
||||
"i18n:translate": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
||||
"i18n:all": "pnpm i18n:check && pnpm i18n:sync && pnpm i18n:translate",
|
||||
"i18n:all": "pnpm i18n:sync && pnpm i18n:translate",
|
||||
"update:languages": "tsx scripts/update-languages.ts",
|
||||
"update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts",
|
||||
"test": "vitest run --silent",
|
||||
@ -87,9 +87,6 @@
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"js-yaml": "4.1.0",
|
||||
"bonjour-service": "1.3.0",
|
||||
"emoji-picker-element-data": "1",
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
@ -254,6 +251,7 @@
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.7.3",
|
||||
"bonjour-service": "1.3.0",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"builder-util-runtime": "9.5.0",
|
||||
"chalk": "4.1.2",
|
||||
@ -267,6 +265,7 @@
|
||||
"code-inspector-plugin": "^0.20.14",
|
||||
"codemirror-lang-mermaid": "0.5.0",
|
||||
"color": "^5.0.0",
|
||||
"commander": "^14.0.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "2.8.5",
|
||||
"country-flag-emoji-polyfill": "0.1.8",
|
||||
@ -290,6 +289,7 @@
|
||||
"electron-window-state": "^5.0.3",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"emoji-picker-element-data": "1",
|
||||
"epub": "1.3.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-import-zod": "^1.2.0",
|
||||
@ -319,6 +319,7 @@
|
||||
"jaison": "^2.0.2",
|
||||
"jest-styled-components": "^7.2.0",
|
||||
"js-base64": "3.7.7",
|
||||
"js-yaml": "4.1.0",
|
||||
"json-schema": "0.4.0",
|
||||
"katex": "0.16.22",
|
||||
"ky": "1.8.1",
|
||||
@ -432,7 +433,8 @@
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3",
|
||||
"@langchain/core": "1.0.2",
|
||||
"@ai-sdk/openai-compatible@1.0.27": "1.0.28"
|
||||
"@ai-sdk/openai-compatible@1.0.27": "1.0.28",
|
||||
"@ai-sdk/openai-compatible@1.0.30": "1.0.28"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@napi-rs/system-ocr@1.0.2": "patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
@ -452,7 +454,9 @@
|
||||
"file-stream-rotator@0.6.1": "patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
||||
"libsql@0.4.7": "patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"pdf-parse@1.1.1": "patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
"@ai-sdk/openai-compatible@1.0.28": "patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch"
|
||||
"@ai-sdk/openai-compatible@1.0.28": "patches/@ai-sdk__openai-compatible@1.0.28.patch",
|
||||
"@anthropic-ai/claude-agent-sdk@0.1.76": "patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch",
|
||||
"@openrouter/ai-sdk-provider": "patches/@openrouter__ai-sdk-provider.patch"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@kangfenmao/keyv-storage",
|
||||
@ -480,5 +484,27 @@
|
||||
"*.{json,yml,yaml,css,html}": [
|
||||
"biome format --write --no-errors-on-unmatched"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.0",
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-arm64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3",
|
||||
"@libsql/darwin-arm64": "0.4.7",
|
||||
"@libsql/darwin-x64": "0.4.7",
|
||||
"@libsql/linux-arm64-gnu": "0.4.7",
|
||||
"@libsql/linux-x64-gnu": "0.4.7",
|
||||
"@libsql/win32-x64-msvc": "0.4.7",
|
||||
"@napi-rs/system-ocr-darwin-arm64": "1.0.2",
|
||||
"@napi-rs/system-ocr-darwin-x64": "1.0.2",
|
||||
"@napi-rs/system-ocr-win32-arm64-msvc": "1.0.2",
|
||||
"@napi-rs/system-ocr-win32-x64-msvc": "1.0.2",
|
||||
"@strongtz/win32-arm64-msvc": "0.4.7"
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ index 48e2f6263c6ee4c75d7e5c28733e64f6ebe92200..00d0729c4a3cbf9a48e8e1e962c7e2b2
|
||||
type OpenAICompatibleProviderOptions = z.infer<typeof openaiCompatibleProviderOptions>;
|
||||
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e5bfe0f9a 100644
|
||||
index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..88349c614a69a268a2e4f3b157cb5e328ca1d347 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -41,7 +41,7 @@ function getOpenAIMetadata(message) {
|
||||
@ -52,17 +52,38 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
|
||||
...metadata
|
||||
});
|
||||
@@ -200,7 +208,8 @@ var openaiCompatibleProviderOptions = import_v4.z.object({
|
||||
@@ -200,7 +208,9 @@ var openaiCompatibleProviderOptions = import_v4.z.object({
|
||||
/**
|
||||
* Controls the verbosity of the generated text. Defaults to `medium`.
|
||||
*/
|
||||
- textVerbosity: import_v4.z.string().optional()
|
||||
+ textVerbosity: import_v4.z.string().optional(),
|
||||
+ sendReasoning: import_v4.z.boolean().optional()
|
||||
+ sendReasoning: import_v4.z.boolean().optional(),
|
||||
+ strictJsonSchema: z.boolean().optional()
|
||||
});
|
||||
|
||||
// src/openai-compatible-error.ts
|
||||
@@ -378,7 +387,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -225,7 +235,8 @@ var defaultOpenAICompatibleErrorStructure = {
|
||||
var import_provider2 = require("@ai-sdk/provider");
|
||||
function prepareTools({
|
||||
tools,
|
||||
- toolChoice
|
||||
+ toolChoice,
|
||||
+ strictJsonSchema
|
||||
}) {
|
||||
tools = (tools == null ? void 0 : tools.length) ? tools : void 0;
|
||||
const toolWarnings = [];
|
||||
@@ -242,7 +253,8 @@ function prepareTools({
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
- parameters: tool.inputSchema
|
||||
+ parameters: tool.inputSchema,
|
||||
+ strict: strictJsonSchema
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -378,7 +390,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
reasoning_effort: compatibleOptions.reasoningEffort,
|
||||
verbosity: compatibleOptions.textVerbosity,
|
||||
// messages:
|
||||
@ -71,7 +92,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
// tools:
|
||||
tools: openaiTools,
|
||||
tool_choice: openaiToolChoice
|
||||
@@ -421,6 +430,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -421,6 +433,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
text: reasoning
|
||||
});
|
||||
}
|
||||
@ -89,7 +110,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
if (choice.message.tool_calls != null) {
|
||||
for (const toolCall of choice.message.tool_calls) {
|
||||
content.push({
|
||||
@@ -598,6 +618,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -598,6 +621,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
delta: delta.content
|
||||
});
|
||||
}
|
||||
@ -107,7 +128,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
if (delta.tool_calls != null) {
|
||||
for (const toolCallDelta of delta.tool_calls) {
|
||||
const index = toolCallDelta.index;
|
||||
@@ -765,6 +796,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
|
||||
@@ -765,6 +799,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
|
||||
arguments: import_v43.z.string()
|
||||
})
|
||||
})
|
||||
@ -122,7 +143,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
).nullish()
|
||||
}),
|
||||
finish_reason: import_v43.z.string().nullish()
|
||||
@@ -795,6 +834,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
|
||||
@@ -795,6 +837,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
|
||||
arguments: import_v43.z.string().nullish()
|
||||
})
|
||||
})
|
||||
@ -138,7 +159,7 @@ index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..b3060fbaf010e30b64df55302807828e
|
||||
}).nullish(),
|
||||
finish_reason: import_v43.z.string().nullish()
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5700264de 100644
|
||||
index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..fca65c04000ce4c01fb90e93326ac179c2378055 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -23,7 +23,7 @@ function getOpenAIMetadata(message) {
|
||||
@ -179,17 +200,38 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
|
||||
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
|
||||
...metadata
|
||||
});
|
||||
@@ -182,7 +190,8 @@ var openaiCompatibleProviderOptions = z.object({
|
||||
@@ -182,7 +190,9 @@ var openaiCompatibleProviderOptions = z.object({
|
||||
/**
|
||||
* Controls the verbosity of the generated text. Defaults to `medium`.
|
||||
*/
|
||||
- textVerbosity: z.string().optional()
|
||||
+ textVerbosity: z.string().optional(),
|
||||
+ sendReasoning: z.boolean().optional()
|
||||
+ sendReasoning: z.boolean().optional(),
|
||||
+ strictJsonSchema: z.boolean().optional()
|
||||
});
|
||||
|
||||
// src/openai-compatible-error.ts
|
||||
@@ -362,7 +371,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -209,7 +219,8 @@ import {
|
||||
} from "@ai-sdk/provider";
|
||||
function prepareTools({
|
||||
tools,
|
||||
- toolChoice
|
||||
+ toolChoice,
|
||||
+ strictJsonSchema
|
||||
}) {
|
||||
tools = (tools == null ? void 0 : tools.length) ? tools : void 0;
|
||||
const toolWarnings = [];
|
||||
@@ -226,7 +237,8 @@ function prepareTools({
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
- parameters: tool.inputSchema
|
||||
+ parameters: tool.inputSchema,
|
||||
+ strict: strictJsonSchema
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -362,7 +374,7 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
reasoning_effort: compatibleOptions.reasoningEffort,
|
||||
verbosity: compatibleOptions.textVerbosity,
|
||||
// messages:
|
||||
@ -198,7 +240,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
|
||||
// tools:
|
||||
tools: openaiTools,
|
||||
tool_choice: openaiToolChoice
|
||||
@@ -405,6 +414,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -405,6 +417,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
text: reasoning
|
||||
});
|
||||
}
|
||||
@ -216,7 +258,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
|
||||
if (choice.message.tool_calls != null) {
|
||||
for (const toolCall of choice.message.tool_calls) {
|
||||
content.push({
|
||||
@@ -582,6 +602,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
@@ -582,6 +605,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||
delta: delta.content
|
||||
});
|
||||
}
|
||||
@ -234,7 +276,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
|
||||
if (delta.tool_calls != null) {
|
||||
for (const toolCallDelta of delta.tool_calls) {
|
||||
const index = toolCallDelta.index;
|
||||
@@ -749,6 +780,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
|
||||
@@ -749,6 +783,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
|
||||
arguments: z3.string()
|
||||
})
|
||||
})
|
||||
@ -249,7 +291,7 @@ index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..565b605eeacd9830b2b0e817e58ad0c5
|
||||
).nullish()
|
||||
}),
|
||||
finish_reason: z3.string().nullish()
|
||||
@@ -779,6 +818,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
|
||||
@@ -779,6 +821,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
|
||||
arguments: z3.string().nullish()
|
||||
})
|
||||
})
|
||||
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) => {
|
||||
140
patches/@openrouter__ai-sdk-provider.patch
Normal file
140
patches/@openrouter__ai-sdk-provider.patch
Normal file
@ -0,0 +1,140 @@
|
||||
diff --git a/dist/index.js b/dist/index.js
|
||||
index f33510a50d11a2cb92a90ea70cc0ac84c89f29b9..db0af7e2cc05c47baeb29c0a3974a155316fbd05 100644
|
||||
--- a/dist/index.js
|
||||
+++ b/dist/index.js
|
||||
@@ -1050,7 +1050,8 @@ var OpenRouterProviderMetadataSchema = import_v43.z.object({
|
||||
var OpenRouterProviderOptionsSchema = import_v43.z.object({
|
||||
openrouter: import_v43.z.object({
|
||||
reasoning_details: import_v43.z.array(ReasoningDetailUnionSchema).optional(),
|
||||
- annotations: import_v43.z.array(FileAnnotationSchema).optional()
|
||||
+ annotations: import_v43.z.array(FileAnnotationSchema).optional(),
|
||||
+ strictJsonSchema: import_v43.z.boolean().optional()
|
||||
}).optional()
|
||||
}).optional();
|
||||
|
||||
@@ -1658,7 +1659,8 @@ var OpenRouterChatLanguageModel = class {
|
||||
responseFormat,
|
||||
topK,
|
||||
tools,
|
||||
- toolChoice
|
||||
+ toolChoice,
|
||||
+ providerOptions
|
||||
}) {
|
||||
var _a15;
|
||||
const baseArgs = __spreadValues(__spreadValues({
|
||||
@@ -1712,7 +1714,8 @@ var OpenRouterChatLanguageModel = class {
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
- parameters: tool.inputSchema
|
||||
+ parameters: tool.inputSchema,
|
||||
+ strict: providerOptions?.openrouter?.strictJsonSchema
|
||||
}
|
||||
}));
|
||||
return __spreadProps(__spreadValues({}, baseArgs), {
|
||||
@@ -1725,7 +1728,7 @@ var OpenRouterChatLanguageModel = class {
|
||||
async doGenerate(options) {
|
||||
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: responseValue, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -1931,7 +1934,7 @@ var OpenRouterChatLanguageModel = class {
|
||||
async doStream(options) {
|
||||
var _a15;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -2564,7 +2567,7 @@ var OpenRouterCompletionLanguageModel = class {
|
||||
async doGenerate(options) {
|
||||
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -2623,7 +2626,7 @@ var OpenRouterCompletionLanguageModel = class {
|
||||
}
|
||||
async doStream(options) {
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index 8a688331b88b4af738ee4ca8062b5f24124d3d81..a2aa299a44352addc26f8891d839ea31a2150ee2 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -1015,7 +1015,8 @@ var OpenRouterProviderMetadataSchema = z3.object({
|
||||
var OpenRouterProviderOptionsSchema = z3.object({
|
||||
openrouter: z3.object({
|
||||
reasoning_details: z3.array(ReasoningDetailUnionSchema).optional(),
|
||||
- annotations: z3.array(FileAnnotationSchema).optional()
|
||||
+ annotations: z3.array(FileAnnotationSchema).optional(),
|
||||
+ strictJsonSchema: z3.boolean().optional()
|
||||
}).optional()
|
||||
}).optional();
|
||||
|
||||
@@ -1623,7 +1624,8 @@ var OpenRouterChatLanguageModel = class {
|
||||
responseFormat,
|
||||
topK,
|
||||
tools,
|
||||
- toolChoice
|
||||
+ toolChoice,
|
||||
+ providerOptions
|
||||
}) {
|
||||
var _a15;
|
||||
const baseArgs = __spreadValues(__spreadValues({
|
||||
@@ -1677,7 +1679,8 @@ var OpenRouterChatLanguageModel = class {
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
- parameters: tool.inputSchema
|
||||
+ parameters: tool.inputSchema,
|
||||
+ strict: providerOptions?.openrouter?.strictJsonSchema
|
||||
}
|
||||
}));
|
||||
return __spreadProps(__spreadValues({}, baseArgs), {
|
||||
@@ -1690,7 +1693,7 @@ var OpenRouterChatLanguageModel = class {
|
||||
async doGenerate(options) {
|
||||
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: responseValue, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -1896,7 +1899,7 @@ var OpenRouterChatLanguageModel = class {
|
||||
async doStream(options) {
|
||||
var _a15;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -2529,7 +2532,7 @@ var OpenRouterCompletionLanguageModel = class {
|
||||
async doGenerate(options) {
|
||||
var _a15, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
@@ -2588,7 +2591,7 @@ var OpenRouterCompletionLanguageModel = class {
|
||||
}
|
||||
async doStream(options) {
|
||||
const providerOptions = options.providerOptions || {};
|
||||
- const openrouterOptions = providerOptions.openrouter || {};
|
||||
+ const { strictJsonSchema: _strictJsonSchema, ...openrouterOptions } = providerOptions.openrouter || {};
|
||||
const args = __spreadValues(__spreadValues({}, this.getArgs(options)), openrouterOptions);
|
||||
const { value: response, responseHeaders } = await postJsonToApi({
|
||||
url: this.config.url({
|
||||
295
pnpm-lock.yaml
generated
295
pnpm-lock.yaml
generated
@ -23,17 +23,21 @@ overrides:
|
||||
'@img/sharp-win32-x64': 0.34.3
|
||||
'@langchain/core': 1.0.2
|
||||
'@ai-sdk/openai-compatible@1.0.27': 1.0.28
|
||||
'@ai-sdk/openai-compatible@1.0.30': 1.0.28
|
||||
|
||||
patchedDependencies:
|
||||
'@ai-sdk/google@2.0.49':
|
||||
hash: 279e9d43f675e4b979b32b78954dd37acc3026aa36ae2dd7701b5bad2f061522
|
||||
path: patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch
|
||||
'@ai-sdk/openai-compatible@1.0.28':
|
||||
hash: 66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1
|
||||
path: patches/@ai-sdk-openai-compatible-npm-1.0.28-5705188855.patch
|
||||
hash: 5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda
|
||||
path: patches/@ai-sdk__openai-compatible@1.0.28.patch
|
||||
'@ai-sdk/openai@2.0.85':
|
||||
hash: f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b
|
||||
path: patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.76':
|
||||
hash: e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5
|
||||
path: patches/@anthropic-ai__claude-agent-sdk@0.1.76.patch
|
||||
'@anthropic-ai/vertex-sdk@0.11.4':
|
||||
hash: 12e3275df5632dfe717d4db64df70e9b0128dfac86195da27722effe4749662f
|
||||
path: patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch
|
||||
@ -49,6 +53,9 @@ patchedDependencies:
|
||||
'@napi-rs/system-ocr@1.0.2':
|
||||
hash: aa1a73e445ee644774745b620589bb99d85bee6c95cc2a91fe9137e580da5bde
|
||||
path: patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch
|
||||
'@openrouter/ai-sdk-provider':
|
||||
hash: 508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52
|
||||
path: patches/@openrouter__ai-sdk-provider.patch
|
||||
'@tiptap/extension-drag-handle@3.2.0':
|
||||
hash: 8432665d4553fb9ba8ff2a126a9181c3ccfee06ae57688aa14f65aa560e52fce
|
||||
path: patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch
|
||||
@ -86,7 +93,7 @@ importers:
|
||||
dependencies:
|
||||
'@anthropic-ai/claude-agent-sdk':
|
||||
specifier: 0.1.76
|
||||
version: 0.1.76(zod@4.3.4)
|
||||
version: 0.1.76(patch_hash=e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5)(zod@4.3.4)
|
||||
'@libsql/client':
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0
|
||||
@ -354,7 +361,7 @@ importers:
|
||||
version: 2.3.0(encoding@0.1.13)
|
||||
'@openrouter/ai-sdk-provider':
|
||||
specifier: ^1.2.8
|
||||
version: 1.5.4(ai@5.0.117(zod@4.3.4))(zod@4.3.4)
|
||||
version: 1.5.4(patch_hash=508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52)(ai@5.0.117(zod@4.3.4))(zod@4.3.4)
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
@ -673,6 +680,9 @@ importers:
|
||||
color:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.3
|
||||
commander:
|
||||
specifier: ^14.0.2
|
||||
version: 14.0.2
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
@ -1114,6 +1124,67 @@ importers:
|
||||
zod:
|
||||
specifier: ^4.1.5
|
||||
version: 4.3.4
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@img/sharp-darwin-x64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@img/sharp-libvips-darwin-arm64':
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
'@img/sharp-libvips-darwin-x64':
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
'@img/sharp-libvips-linux-arm64':
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
'@img/sharp-libvips-linux-x64':
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
'@img/sharp-linux-arm64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@img/sharp-linux-x64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@img/sharp-win32-arm64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@img/sharp-win32-x64':
|
||||
specifier: 0.34.3
|
||||
version: 0.34.3
|
||||
'@libsql/darwin-arm64':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
'@libsql/darwin-x64':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
'@libsql/linux-arm64-gnu':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
'@libsql/linux-x64-gnu':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
'@libsql/win32-x64-msvc':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
'@napi-rs/system-ocr-darwin-arm64':
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
'@napi-rs/system-ocr-darwin-x64':
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
'@napi-rs/system-ocr-win32-arm64-msvc':
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
'@napi-rs/system-ocr-win32-x64-msvc':
|
||||
specifier: 1.0.2
|
||||
version: 1.0.2
|
||||
'@strongtz/win32-arm64-msvc':
|
||||
specifier: 0.4.7
|
||||
version: 0.4.7
|
||||
|
||||
packages/ai-sdk-provider:
|
||||
dependencies:
|
||||
@ -1128,7 +1199,7 @@ importers:
|
||||
version: 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.5)
|
||||
'@ai-sdk/openai-compatible':
|
||||
specifier: 1.0.28
|
||||
version: 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.5)
|
||||
version: 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.5)
|
||||
'@ai-sdk/provider':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.1
|
||||
@ -1168,7 +1239,7 @@ importers:
|
||||
version: 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible':
|
||||
specifier: 1.0.28
|
||||
version: 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
|
||||
version: 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.1
|
||||
@ -1331,12 +1402,6 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.30':
|
||||
resolution: {integrity: sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai@2.0.85':
|
||||
resolution: {integrity: sha512-3pzr7qVhsOXwjPAfmvFNZz3sRWCuyMOc3GgLHe7sWY0t8J4hA5mwQ4LISTKYI3iIr8IXzAQn9MUrC8Hiji9RpA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -3473,6 +3538,9 @@ packages:
|
||||
'@oxc-project/types@0.106.0':
|
||||
resolution: {integrity: sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==}
|
||||
|
||||
'@oxc-project/types@0.107.0':
|
||||
resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.2.1':
|
||||
resolution: {integrity: sha512-1cjcZkgpkWTfkFx111weVsvlnDG3+a7Y3qei+VCbr55LwKTHSzGq9tVNakIHRJ6NZfX7bQKYU4yef3p6AKoteg==}
|
||||
cpu: [arm64]
|
||||
@ -3979,6 +4047,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -3991,6 +4065,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4003,6 +4083,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4015,6 +4101,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4027,6 +4119,12 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4039,6 +4137,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4051,6 +4155,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4063,6 +4173,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4075,6 +4191,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4087,6 +4209,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -4097,6 +4225,11 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4109,6 +4242,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.53':
|
||||
resolution: {integrity: sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@ -4121,6 +4260,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
@ -4130,6 +4275,9 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-beta.58':
|
||||
resolution: {integrity: sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.59':
|
||||
resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==}
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.45.1':
|
||||
resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==}
|
||||
cpu: [x64]
|
||||
@ -4403,6 +4551,11 @@ packages:
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@strongtz/win32-arm64-msvc@0.4.7':
|
||||
resolution: {integrity: sha512-LMU0CVfKGxKzIUawkDw6pwCNFb64DSwqEQhVAfEdLGMZcvVIdJghWB9yMW+72DHzALpwHscIssAJGhYAT4AbfA==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@svta/common-media-library@0.17.4':
|
||||
resolution: {integrity: sha512-nP/KThzQW5FZKdc9V7ICTa9/A7xGw66VQoLPYOEwwMZTTrISp1zIQAX4KAYJw2PN/VPnxJQJXIYbzZTXgMHctw==}
|
||||
engines: {node: '>=20'}
|
||||
@ -6225,6 +6378,10 @@ packages:
|
||||
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
commander@14.0.2:
|
||||
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
@ -10652,6 +10809,11 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rolldown@1.0.0-beta.59:
|
||||
resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-visualizer@5.14.0:
|
||||
resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -12156,7 +12318,7 @@ snapshots:
|
||||
|
||||
'@ai-sdk/cerebras@1.0.34(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible': 1.0.30(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
@ -12212,7 +12374,7 @@ snapshots:
|
||||
|
||||
'@ai-sdk/huggingface@0.0.10(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.17(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
@ -12223,24 +12385,18 @@ snapshots:
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)':
|
||||
'@ai-sdk/openai-compatible@1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.5)':
|
||||
'@ai-sdk/openai-compatible@1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.5)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.18(zod@4.3.5)
|
||||
zod: 4.3.5
|
||||
|
||||
'@ai-sdk/openai-compatible@1.0.30(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
|
||||
'@ai-sdk/openai@2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
@ -12335,14 +12491,14 @@ snapshots:
|
||||
|
||||
'@ai-sdk/xai@2.0.36(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.17(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
|
||||
'@ai-sdk/xai@2.0.43(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible': 1.0.30(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider': 2.0.1
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
|
||||
zod: 4.3.4
|
||||
@ -12412,7 +12568,7 @@ snapshots:
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.0.2
|
||||
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.76(zod@4.3.4)':
|
||||
'@anthropic-ai/claude-agent-sdk@0.1.76(patch_hash=e063a8ede82d78f452f7f1290b9d4d9323866159b5624679163caa8edd4928d5)(zod@4.3.4)':
|
||||
dependencies:
|
||||
zod: 4.3.4
|
||||
optionalDependencies:
|
||||
@ -14995,7 +15151,7 @@ snapshots:
|
||||
|
||||
'@open-draft/until@2.1.0': {}
|
||||
|
||||
'@openrouter/ai-sdk-provider@1.5.4(ai@5.0.117(zod@4.3.4))(zod@4.3.4)':
|
||||
'@openrouter/ai-sdk-provider@1.5.4(patch_hash=508e8e662b8547de93410cb7c3b1336077f34c6bf79c520ef5273962ea777c52)(ai@5.0.117(zod@4.3.4))(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@openrouter/sdk': 0.1.27
|
||||
ai: 5.0.117(zod@4.3.4)
|
||||
@ -15112,7 +15268,7 @@ snapshots:
|
||||
'@opeoginni/github-copilot-openai-compatible@0.1.22(zod@4.3.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai': 2.0.85(patch_hash=f2077f4759520d1de69b164dfd8adca1a9ace9de667e35cb0e55e812ce2ac13b)(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=66f6605ef3f852d8f2b638a1d64b138eb8b2ad34ca6f331a0496c1d1379379c1)(zod@4.3.4)
|
||||
'@ai-sdk/openai-compatible': 1.0.28(patch_hash=5ea49b4f07636a8e4630097e67e2787779ba7e933bd0459f81b1803cb125edda)(zod@4.3.4)
|
||||
'@ai-sdk/provider': 2.1.0-beta.5
|
||||
'@ai-sdk/provider-utils': 3.0.20(zod@4.3.4)
|
||||
transitivePeerDependencies:
|
||||
@ -15124,6 +15280,8 @@ snapshots:
|
||||
|
||||
'@oxc-project/types@0.106.0': {}
|
||||
|
||||
'@oxc-project/types@0.107.0': {}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.2.1':
|
||||
optional: true
|
||||
|
||||
@ -15573,60 +15731,90 @@ snapshots:
|
||||
'@rolldown/binding-android-arm64@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.53':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
@ -15637,24 +15825,37 @@ snapshots:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.59':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.53':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.58':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.59':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.58': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.59': {}
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.45.1':
|
||||
optional: true
|
||||
|
||||
@ -16054,6 +16255,9 @@ snapshots:
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@strongtz/win32-arm64-msvc@0.4.7':
|
||||
optional: true
|
||||
|
||||
'@svta/common-media-library@0.17.4': {}
|
||||
|
||||
'@swc/core-darwin-arm64@1.15.8':
|
||||
@ -18459,6 +18663,8 @@ snapshots:
|
||||
|
||||
commander@13.1.0: {}
|
||||
|
||||
commander@14.0.2: {}
|
||||
|
||||
commander@2.20.3: {}
|
||||
|
||||
commander@5.1.0: {}
|
||||
@ -23676,7 +23882,7 @@ snapshots:
|
||||
- oxc-resolver
|
||||
- supports-color
|
||||
|
||||
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.8.3):
|
||||
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.5
|
||||
'@babel/parser': 7.28.5
|
||||
@ -23686,7 +23892,7 @@ snapshots:
|
||||
debug: 4.4.3
|
||||
dts-resolver: 2.1.3
|
||||
get-tsconfig: 4.13.0
|
||||
rolldown: 1.0.0-beta.58
|
||||
rolldown: 1.0.0-beta.59
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview': 7.0.0-dev.20260104.1
|
||||
typescript: 5.8.3
|
||||
@ -23694,7 +23900,7 @@ snapshots:
|
||||
- oxc-resolver
|
||||
- supports-color
|
||||
|
||||
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.9.2):
|
||||
rolldown-plugin-dts@0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.5
|
||||
'@babel/parser': 7.28.5
|
||||
@ -23704,7 +23910,7 @@ snapshots:
|
||||
debug: 4.4.3
|
||||
dts-resolver: 2.1.3
|
||||
get-tsconfig: 4.13.0
|
||||
rolldown: 1.0.0-beta.58
|
||||
rolldown: 1.0.0-beta.59
|
||||
optionalDependencies:
|
||||
'@typescript/native-preview': 7.0.0-dev.20260104.1
|
||||
typescript: 5.9.2
|
||||
@ -23784,6 +23990,25 @@ snapshots:
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58
|
||||
|
||||
rolldown@1.0.0-beta.59:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.107.0
|
||||
'@rolldown/pluginutils': 1.0.0-beta.59
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm64': 1.0.0-beta.59
|
||||
'@rolldown/binding-darwin-arm64': 1.0.0-beta.59
|
||||
'@rolldown/binding-darwin-x64': 1.0.0-beta.59
|
||||
'@rolldown/binding-freebsd-x64': 1.0.0-beta.59
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59
|
||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59
|
||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59
|
||||
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.59
|
||||
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.59
|
||||
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.59
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59
|
||||
|
||||
rollup-plugin-visualizer@5.14.0:
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
@ -24600,8 +24825,8 @@ snapshots:
|
||||
diff: 8.0.2
|
||||
empathic: 2.0.0
|
||||
hookable: 5.5.3
|
||||
rolldown: 1.0.0-beta.58
|
||||
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.8.3)
|
||||
rolldown: 1.0.0-beta.59
|
||||
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.8.3)
|
||||
semver: 7.7.3
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
@ -24624,8 +24849,8 @@ snapshots:
|
||||
diff: 8.0.2
|
||||
empathic: 2.0.0
|
||||
hookable: 5.5.3
|
||||
rolldown: 1.0.0-beta.58
|
||||
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.58)(typescript@5.9.2)
|
||||
rolldown: 1.0.0-beta.59
|
||||
rolldown-plugin-dts: 0.15.10(@typescript/native-preview@7.0.0-dev.20260104.1)(rolldown@1.0.0-beta.59)(typescript@5.9.2)
|
||||
semver: 7.7.3
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
|
||||
@ -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 = []
|
||||
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
|
||||
}
|
||||
|
||||
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`
|
||||
)
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(downloadPromises)
|
||||
}
|
||||
await downloadPackages()
|
||||
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,15 @@ if (isLinux && process.env.XDG_SESSION_TYPE === 'wayland') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set window class and name for X11
|
||||
* This ensures the system tray and window manager identify the app correctly
|
||||
*/
|
||||
if (isLinux) {
|
||||
app.commandLine.appendSwitch('class', 'cherry-studio')
|
||||
app.commandLine.appendSwitch('name', 'cherry-studio')
|
||||
}
|
||||
|
||||
// DocumentPolicyIncludeJSCallStacksInCrashReports: Enable features for unresponsive renderer js call stacks
|
||||
// EarlyEstablishGpuChannel,EstablishGpuChannelAsync: Enable features for early establish gpu channel
|
||||
// speed up the startup time
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -1083,18 +1083,33 @@ export class SelectionService {
|
||||
this.lastCtrlkeyDownTime = -1
|
||||
}
|
||||
|
||||
//check if the key is ctrl key
|
||||
// Check if the key is ctrl key
|
||||
// Windows: VK_LCONTROL(162), VK_RCONTROL(163)
|
||||
// macOS: kVK_Control(59), kVK_RightControl(62)
|
||||
private isCtrlkey(vkCode: number) {
|
||||
if (isMac) {
|
||||
return vkCode === 59 || vkCode === 62
|
||||
}
|
||||
return vkCode === 162 || vkCode === 163
|
||||
}
|
||||
|
||||
//check if the key is shift key
|
||||
// Check if the key is shift key
|
||||
// Windows: VK_LSHIFT(160), VK_RSHIFT(161)
|
||||
// macOS: kVK_Shift(56), kVK_RightShift(60)
|
||||
private isShiftkey(vkCode: number) {
|
||||
if (isMac) {
|
||||
return vkCode === 56 || vkCode === 60
|
||||
}
|
||||
return vkCode === 160 || vkCode === 161
|
||||
}
|
||||
|
||||
//check if the key is alt key
|
||||
// Check if the key is alt/option key
|
||||
// Windows: VK_LMENU(164), VK_RMENU(165)
|
||||
// macOS: kVK_Option(58), kVK_RightOption(61)
|
||||
private isAltkey(vkCode: number) {
|
||||
if (isMac) {
|
||||
return vkCode === 58 || vkCode === 61
|
||||
}
|
||||
return vkCode === 164 || vkCode === 165
|
||||
}
|
||||
|
||||
|
||||
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,85 @@
|
||||
/**
|
||||
* Anthropic Prompt Caching Middleware
|
||||
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
|
||||
*/
|
||||
import type { LanguageModelV2Message } from '@ai-sdk/provider'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import type { Provider } from '@renderer/types'
|
||||
import type { LanguageModelMiddleware } from 'ai'
|
||||
|
||||
const cacheProviderOptions = {
|
||||
anthropic: { cacheControl: { type: 'ephemeral' } }
|
||||
}
|
||||
|
||||
function estimateContentTokens(content: LanguageModelV2Message['content']): number {
|
||||
if (typeof content === 'string') return estimateTextTokens(content)
|
||||
if (Array.isArray(content)) {
|
||||
return content.reduce((acc, part) => {
|
||||
if (part.type === 'text') {
|
||||
return acc + estimateTextTokens(part.text as string)
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export function anthropicCacheMiddleware(provider: Provider): LanguageModelMiddleware {
|
||||
return {
|
||||
middlewareVersion: 'v2',
|
||||
transformParams: async ({ params }) => {
|
||||
const settings = provider.anthropicCacheControl
|
||||
if (!settings?.tokenThreshold || !Array.isArray(params.prompt) || params.prompt.length === 0) {
|
||||
return params
|
||||
}
|
||||
|
||||
const { tokenThreshold, cacheSystemMessage, cacheLastNMessages } = settings
|
||||
const messages = [...params.prompt]
|
||||
let cachedCount = 0
|
||||
|
||||
// Cache system message (providerOptions on message object)
|
||||
if (cacheSystemMessage) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i] as LanguageModelV2Message
|
||||
if (msg.role === 'system' && estimateContentTokens(msg.content) >= tokenThreshold) {
|
||||
messages[i] = { ...msg, providerOptions: cacheProviderOptions }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache last N non-system messages (providerOptions on content parts)
|
||||
if (cacheLastNMessages > 0) {
|
||||
const cumsumTokens = [] as Array<number>
|
||||
let tokenSum = 0 as number
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i] as LanguageModelV2Message
|
||||
tokenSum += estimateContentTokens(msg.content)
|
||||
cumsumTokens.push(tokenSum)
|
||||
}
|
||||
|
||||
for (let i = messages.length - 1; i >= 0 && cachedCount < cacheLastNMessages; i--) {
|
||||
const msg = messages[i] as LanguageModelV2Message
|
||||
if (msg.role === 'system' || cumsumTokens[i] < tokenThreshold || msg.content.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const newContent = [...msg.content]
|
||||
const lastIndex = newContent.length - 1
|
||||
newContent[lastIndex] = {
|
||||
...newContent[lastIndex],
|
||||
providerOptions: cacheProviderOptions
|
||||
}
|
||||
|
||||
messages[i] = {
|
||||
...msg,
|
||||
content: newContent
|
||||
} as LanguageModelV2Message
|
||||
cachedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return { ...params, prompt: messages }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -325,9 +325,9 @@ function createDeveloperToSystemFetch(originalFetch?: typeof fetch): typeof fetc
|
||||
if (options?.body && typeof options.body === 'string') {
|
||||
try {
|
||||
const body = JSON.parse(options.body)
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
if (body.input && Array.isArray(body.input)) {
|
||||
let hasChanges = false
|
||||
body.messages = body.messages.map((msg: { role: string }) => {
|
||||
body.input = body.input.map((msg: { role: string }) => {
|
||||
if (msg.role === 'developer') {
|
||||
hasChanges = true
|
||||
return { ...msg, role: 'system' }
|
||||
|
||||
@ -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,12 +616,17 @@ function buildGenericProviderOptions(
|
||||
}
|
||||
if (enableReasoning) {
|
||||
if (isInterleavedThinkingModel(model)) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enableWebSearch) {
|
||||
const webSearchParams = getWebSearchParams(model)
|
||||
@ -648,6 +653,10 @@ function buildGenericProviderOptions(
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpenAIModel(model)) {
|
||||
providerOptions.strictJsonSchema = false
|
||||
}
|
||||
|
||||
return {
|
||||
[providerId]: providerOptions
|
||||
}
|
||||
|
||||
@ -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: [
|
||||
@ -753,7 +777,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
{ id: 'qwen-flash', name: 'qwen-flash', provider: 'dashscope', group: 'qwen-flash', owned_by: 'system' },
|
||||
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
|
||||
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
|
||||
{ id: 'qwen3-max', name: 'qwen3-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' }
|
||||
{ id: 'qwen3-max', name: 'qwen3-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
|
||||
{ id: 'text-embedding-v4', name: 'text-embedding-v4', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'text-embedding-v3', name: 'text-embedding-v3', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'text-embedding-v2', name: 'text-embedding-v2', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'text-embedding-v1', name: 'text-embedding-v1', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'qwen3-rerank', name: 'qwen3-rerank', provider: 'dashscope', group: 'qwen-rerank' }
|
||||
],
|
||||
stepfun: [
|
||||
{
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -378,7 +378,7 @@
|
||||
"about": "Despre",
|
||||
"close": "Închide fereastra",
|
||||
"copy": "Copiază",
|
||||
"cut": "Taie",
|
||||
"cut": "Decupează",
|
||||
"delete": "Șterge",
|
||||
"documentation": "Documentație",
|
||||
"edit": "Editare",
|
||||
@ -1191,7 +1191,7 @@
|
||||
"agent_one": "Agent",
|
||||
"agent_other": "Agenți",
|
||||
"and": "și",
|
||||
"assistant": "Agent",
|
||||
"assistant": "Asistent",
|
||||
"assistant_one": "Asistent",
|
||||
"assistant_other": "Asistenți",
|
||||
"avatar": "Avatar",
|
||||
@ -1208,7 +1208,7 @@
|
||||
"copy": "Copiază",
|
||||
"copy_failed": "Copiere eșuată",
|
||||
"current": "Curent",
|
||||
"cut": "Taie",
|
||||
"cut": "Decupează",
|
||||
"default": "Implicit",
|
||||
"delete": "Șterge",
|
||||
"delete_confirm": "Ești sigur că vrei să ștergi?",
|
||||
@ -1222,10 +1222,10 @@
|
||||
"duplicate": "Duplică",
|
||||
"edit": "Editează",
|
||||
"enabled": "Activat",
|
||||
"error": "eroare",
|
||||
"error": "Eroare",
|
||||
"errors": {
|
||||
"create_message": "Nu s-a putut crea mesajul",
|
||||
"validation": "Verificarea a eșuat"
|
||||
"validation": "Validarea a eșuat"
|
||||
},
|
||||
"expand": "Extinde",
|
||||
"file": {
|
||||
@ -1611,7 +1611,7 @@
|
||||
"title": "Setări bază de cunoștințe"
|
||||
},
|
||||
"sitemap_added": "Adăugat cu succes",
|
||||
"sitemap_placeholder": "Introdu URL-ul hărții site-ului",
|
||||
"sitemap_placeholder": "Introdu URL-ul sitemap-ului",
|
||||
"sitemaps": "Site-uri web",
|
||||
"source": "Sursă",
|
||||
"status": "Stare",
|
||||
@ -1623,7 +1623,7 @@
|
||||
"status_pending": "În așteptare",
|
||||
"status_preprocess_completed": "Preprocesare finalizată",
|
||||
"status_preprocess_failed": "Preprocesare eșuată",
|
||||
"status_processing": "Se procesează",
|
||||
"status_processing": "În procesare",
|
||||
"subtitle_file": "fișier subtitrare",
|
||||
"threshold": "Prag de potrivire",
|
||||
"threshold_placeholder": "Nesetat",
|
||||
@ -1633,9 +1633,9 @@
|
||||
"topN": "Număr rezultate returnate",
|
||||
"topN_placeholder": "Nesetat",
|
||||
"topN_too_large_or_small": "Numărul de rezultate returnate nu poate fi mai mare de 30 sau mai mic de 1.",
|
||||
"topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și mai mulți tokeni consumați.",
|
||||
"topN_tooltip": "Numărul de rezultate potrivite returnate; cu cât valoarea este mai mare, cu atât mai multe rezultate, dar și un consum mai mare de tokeni.",
|
||||
"url_added": "URL adăugat",
|
||||
"url_placeholder": "Introdu URL, separă URL-urile multiple prin Enter",
|
||||
"url_placeholder": "Introdu URL-ul; separă mai multe URL-uri prin Enter",
|
||||
"urls": "URL-uri",
|
||||
"videos": "video",
|
||||
"videos_file": "fișier video"
|
||||
@ -1760,7 +1760,7 @@
|
||||
"switch_user_confirm": "Schimbi contextul de utilizator la {{user}}?",
|
||||
"time": "Timp",
|
||||
"title": "Amintiri",
|
||||
"total_memories": "total amintiri",
|
||||
"total_memories": "Total amintiri",
|
||||
"try_different_filters": "Încearcă să ajustezi criteriile de căutare",
|
||||
"update_failed": "Nu s-a putut actualiza amintirea",
|
||||
"update_success": "Amintire actualizată cu succes",
|
||||
@ -1779,7 +1779,7 @@
|
||||
"user_memories_reset": "Toate amintirile pentru {{user}} au fost resetate",
|
||||
"user_switch_failed": "Nu s-a putut schimba utilizatorul",
|
||||
"user_switched": "Contextul de utilizator a fost schimbat la {{user}}",
|
||||
"users": "utilizatori"
|
||||
"users": "Utilizatori"
|
||||
},
|
||||
"message": {
|
||||
"agents": {
|
||||
@ -2196,7 +2196,7 @@
|
||||
"navbar": {
|
||||
"expand": "Extinde dialogul",
|
||||
"hide_sidebar": "Ascunde bara laterală",
|
||||
"show_sidebar": "Arată bara laterală",
|
||||
"show_sidebar": "Afișează bara laterală",
|
||||
"window": {
|
||||
"close": "Închide",
|
||||
"maximize": "Maximizează",
|
||||
@ -2216,27 +2216,27 @@
|
||||
},
|
||||
"characters": "Caractere",
|
||||
"collapse": "Restrânge",
|
||||
"content_placeholder": "Te rugăm să introduci conținutul notiței...",
|
||||
"content_placeholder": "Introdu conținutul notiței...",
|
||||
"copyContent": "Copiază conținutul",
|
||||
"crossPlatformRestoreWarning": "Configurația multi-platformă a fost restaurată, dar directorul de notițe este gol. Te rugăm să copiezi fișierele notițelor în: {{path}}",
|
||||
"delete": "șterge",
|
||||
"delete": "Șterge",
|
||||
"delete_confirm": "Ești sigur că vrei să ștergi acest {{type}}?",
|
||||
"delete_folder_confirm": "Ești sigur că vrei să ștergi dosarul \"{{name}}\" și tot conținutul său?",
|
||||
"delete_note_confirm": "Ești sigur că vrei să ștergi notița \"{{name}}\"?",
|
||||
"drop_markdown_hint": "Trage fișiere sau dosare .md aici pentru a importa",
|
||||
"empty": "Încă nu există notițe disponibile",
|
||||
"expand": "desfășoară",
|
||||
"expand": "Extinde",
|
||||
"export_failed": "Exportul în baza de cunoștințe a eșuat",
|
||||
"export_knowledge": "Exportă notițele în baza de cunoștințe",
|
||||
"export_success": "Exportat cu succes în baza de cunoștințe",
|
||||
"folder": "dosar",
|
||||
"folder": "Dosar",
|
||||
"new_folder": "Dosar nou",
|
||||
"new_note": "Creează o notiță nouă",
|
||||
"no_content_to_copy": "Niciun conținut de copiat",
|
||||
"no_file_selected": "Te rugăm să selectezi fișierul de încărcat",
|
||||
"no_valid_files": "Nu a fost încărcat niciun fișier valid",
|
||||
"open_folder": "Deschide un dosar extern",
|
||||
"open_outside": "Deschide din exterior",
|
||||
"open_outside": "Deschide extern",
|
||||
"rename": "Redenumește",
|
||||
"rename_changed": "Din cauza politicilor de securitate, numele fișierului a fost schimbat din {{original}} în {{final}}",
|
||||
"save": "Salvează în Notițe",
|
||||
@ -2275,7 +2275,7 @@
|
||||
"font_size_small": "Mic",
|
||||
"font_title": "Setări font",
|
||||
"serif_font": "Font cu serife",
|
||||
"show_table_of_contents": "Arată cuprinsul",
|
||||
"show_table_of_contents": "Afișează cuprinsul",
|
||||
"show_table_of_contents_description": "Afișează o bară laterală cu cuprinsul pentru o navigare ușoară în documente",
|
||||
"title": "Setări afișare"
|
||||
},
|
||||
@ -3115,6 +3115,10 @@
|
||||
},
|
||||
"settings": {
|
||||
"about": {
|
||||
"careers": {
|
||||
"button": "Vedere",
|
||||
"title": "Carieră"
|
||||
},
|
||||
"checkUpdate": {
|
||||
"available": "Actualizare",
|
||||
"label": "Verifică actualizări"
|
||||
@ -4258,7 +4262,7 @@
|
||||
"display_title": "Setări afișare mini-aplicații",
|
||||
"empty": "Trage mini-aplicațiile din stânga pentru a le ascunde",
|
||||
"open_link_external": {
|
||||
"title": "Deschide linkurile de fereastră nouă în browser"
|
||||
"title": "Deschide în browser linkurile care deschid ferestre noi"
|
||||
},
|
||||
"reset_tooltip": "Resetează la implicit",
|
||||
"sidebar_description": "Arată mini-aplicațiile active în bara laterală",
|
||||
@ -4358,7 +4362,7 @@
|
||||
"description": "Model folosit pentru sarcini simple, cum ar fi numirea subiectelor și extragerea cuvintelor cheie",
|
||||
"label": "Model rapid",
|
||||
"setting_title": "Configurare model rapid",
|
||||
"tooltip": "Se recomandă alegerea unui model ușor și nu se recomandă alegerea unui model de gândire."
|
||||
"tooltip": "Se recomandă alegerea unui model ușor, nu a unui model de raționament complex."
|
||||
},
|
||||
"topic_naming": {
|
||||
"auto": "Numire automată subiect",
|
||||
@ -4475,6 +4479,14 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"anthropic_cache": {
|
||||
"cache_last_n": "Cache Ultimelor N Mesaje",
|
||||
"cache_last_n_help": "Stochează ultimele N mesaje din conversație (excluzând mesajele de sistem)",
|
||||
"cache_system": "Mesaj de sistem Cache",
|
||||
"cache_system_help": "Dacă să se memoreze în cache promptul de sistem",
|
||||
"token_threshold": "Prag de Token Cache",
|
||||
"token_threshold_help": "Mesajele care depășesc acest număr de tokeni vor fi memorate în cache. Setați la 0 pentru a dezactiva memorarea în cache."
|
||||
},
|
||||
"array_content": {
|
||||
"help": "Furnizorul acceptă ca câmpul content al mesajului să fie de tip array?",
|
||||
"label": "Acceptă conținut mesaj în format array"
|
||||
@ -4690,7 +4702,7 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "Acțiune",
|
||||
"actions": "operațiune",
|
||||
"actions": "Comandă",
|
||||
"clear_shortcut": "Șterge comanda rapidă",
|
||||
"clear_topic": "Șterge mesajele",
|
||||
"copy_last_message": "Copiază ultimul mesaj",
|
||||
@ -4721,8 +4733,8 @@
|
||||
},
|
||||
"theme": {
|
||||
"color_primary": "Culoare primară",
|
||||
"dark": "Întunecat",
|
||||
"light": "Luminos",
|
||||
"dark": "Întunecată",
|
||||
"light": "Luminoasă",
|
||||
"system": "Sistem",
|
||||
"title": "Temă",
|
||||
"window": {
|
||||
@ -4884,21 +4896,21 @@
|
||||
"translate": {
|
||||
"custom": {
|
||||
"delete": {
|
||||
"description": "Ești sigur că vrei să ștergi?",
|
||||
"description": "Ești sigur că vrei să ștergi această limbă?",
|
||||
"title": "Șterge limba personalizată"
|
||||
},
|
||||
"error": {
|
||||
"add": "Adăugarea a eșuat",
|
||||
"delete": "Ștergerea a eșuat",
|
||||
"langCode": {
|
||||
"builtin": "Limba are suport integrat",
|
||||
"empty": "Codul limbii este gol",
|
||||
"builtin": "Limbă deja integrată",
|
||||
"empty": "Codul limbii lipsește",
|
||||
"exists": "Limba există deja",
|
||||
"invalid": "Cod limbă invalid"
|
||||
},
|
||||
"update": "Actualizarea a eșuat",
|
||||
"value": {
|
||||
"empty": "Numele limbii nu poate fi gol",
|
||||
"empty": "Numele limbii este obligatoriu",
|
||||
"too_long": "Numele limbii este prea lung"
|
||||
}
|
||||
},
|
||||
@ -4908,13 +4920,13 @@
|
||||
"placeholder": "en-us"
|
||||
},
|
||||
"success": {
|
||||
"add": "Adăugat cu succes",
|
||||
"delete": "Șters cu succes",
|
||||
"update": "Actualizare reușită"
|
||||
"add": "Adăugată cu succes",
|
||||
"delete": "Ștearsă cu succes",
|
||||
"update": "Actualizată cu succes"
|
||||
},
|
||||
"table": {
|
||||
"action": {
|
||||
"title": "Operațiune"
|
||||
"title": "Acțiuni"
|
||||
}
|
||||
},
|
||||
"value": {
|
||||
@ -4946,7 +4958,7 @@
|
||||
"mcp-servers": "Servere MCP",
|
||||
"memories": "Amintiri",
|
||||
"notes": "Notițe",
|
||||
"paintings": "Picturi",
|
||||
"paintings": "Imagini",
|
||||
"settings": "Setări",
|
||||
"store": "Bibliotecă asistenți",
|
||||
"translate": "Traducere"
|
||||
@ -4990,7 +5002,7 @@
|
||||
"detect": {
|
||||
"method": {
|
||||
"algo": {
|
||||
"label": "algoritm",
|
||||
"label": "Algoritm",
|
||||
"tip": "Folosește biblioteca franc pentru detectarea limbii"
|
||||
},
|
||||
"auto": {
|
||||
@ -5097,7 +5109,7 @@
|
||||
"tray": {
|
||||
"quit": "Ieșire",
|
||||
"show_mini_window": "Asistent rapid",
|
||||
"show_window": "Arată fereastra"
|
||||
"show_window": "Afișează fereastra"
|
||||
},
|
||||
"update": {
|
||||
"install": "Instalează",
|
||||
@ -5113,7 +5125,7 @@
|
||||
"words": {
|
||||
"knowledgeGraph": "Grafic de cunoștințe",
|
||||
"quit": "Ieșire",
|
||||
"show_window": "Arată fereastra",
|
||||
"show_window": "Afișează fereastra",
|
||||
"visualization": "Vizualizare"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": "поддержка формата массива для содержимого сообщения"
|
||||
|
||||
231
src/renderer/src/pages/code/__tests__/index.test.ts
Normal file
231
src/renderer/src/pages/code/__tests__/index.test.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Mock CodeToolsPage which is the default export
|
||||
vi.mock('../CodeToolsPage', () => ({ default: () => null }))
|
||||
|
||||
// Mock dependencies needed by CodeToolsPage
|
||||
vi.mock('@renderer/hooks/useCodeTools', () => ({
|
||||
useCodeTools: () => ({
|
||||
selectedCliTool: codeTools.qwenCode,
|
||||
selectedModel: null,
|
||||
selectedTerminal: 'systemDefault',
|
||||
environmentVariables: '',
|
||||
directories: [],
|
||||
currentDirectory: '',
|
||||
canLaunch: true,
|
||||
setCliTool: vi.fn(),
|
||||
setModel: vi.fn(),
|
||||
setTerminal: vi.fn(),
|
||||
setEnvVars: vi.fn(),
|
||||
setCurrentDir: vi.fn(),
|
||||
removeDir: vi.fn(),
|
||||
selectFolder: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useProvider', () => ({
|
||||
useProviders: () => ({ providers: [] }),
|
||||
useAllProviders: () => []
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/services/AssistantService', () => ({
|
||||
getProviderByModel: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/services/LoggerService', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/store', () => ({
|
||||
useAppDispatch: () => vi.fn(),
|
||||
useAppSelector: () => false
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/aiCore', () => ({
|
||||
default: class {
|
||||
getBaseURL() {
|
||||
return ''
|
||||
}
|
||||
getApiKey() {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/api', () => ({
|
||||
formatApiHost: vi.fn((host) => {
|
||||
if (!host) return ''
|
||||
const normalized = host.replace(/\/$/, '').trim()
|
||||
if (normalized.endsWith('#')) {
|
||||
return normalized.replace(/#$/, '')
|
||||
}
|
||||
if (/\/v\d+(?:alpha|beta)?(?=\/|$)/i.test(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
return `${normalized}/v1`
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
describe('generateToolEnvironment', () => {
|
||||
const createMockModel = (id: string, provider: string): Model => ({
|
||||
id,
|
||||
name: id,
|
||||
provider,
|
||||
group: provider
|
||||
})
|
||||
|
||||
const createMockProvider = (id: string, apiHost: string): Provider => ({
|
||||
id,
|
||||
type: 'openai',
|
||||
name: id,
|
||||
apiKey: 'test-key',
|
||||
apiHost,
|
||||
models: [],
|
||||
isSystem: true
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should format baseUrl with /v1 for qwenCode when missing', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-turbo', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
|
||||
})
|
||||
|
||||
it('should not duplicate /v1 when already present for qwenCode', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-turbo', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/v1')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
|
||||
})
|
||||
|
||||
it('should handle empty baseUrl gracefully', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-turbo', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', '')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: ''
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('')
|
||||
})
|
||||
|
||||
it('should preserve other API versions when present', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-plus', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/v2'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2')
|
||||
})
|
||||
|
||||
it('should format baseUrl with /v1 for openaiCodex when missing', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('gpt-4', 'openai')
|
||||
const provider = createMockProvider('openai', 'https://api.openai.com')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.openaiCodex,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://api.openai.com'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('should format baseUrl with /v1 for iFlowCli when missing', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('gpt-4', 'iflow')
|
||||
const provider = createMockProvider('iflow', 'https://api.iflow.cn')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.iFlowCli,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://api.iflow.cn'
|
||||
})
|
||||
|
||||
expect(env.IFLOW_BASE_URL).toBe('https://api.iflow.cn/v1')
|
||||
})
|
||||
|
||||
it('should handle trailing slash correctly', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-turbo', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/compatible-mode/')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1')
|
||||
})
|
||||
|
||||
it('should handle v2beta version correctly', async () => {
|
||||
const { generateToolEnvironment } = await import('../index')
|
||||
const model = createMockModel('qwen-plus', 'dashscope')
|
||||
const provider = createMockProvider('dashscope', 'https://dashscope.aliyuncs.com/v2beta')
|
||||
|
||||
const env = generateToolEnvironment({
|
||||
tool: codeTools.qwenCode,
|
||||
model,
|
||||
modelProvider: provider,
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://dashscope.aliyuncs.com/v2beta'
|
||||
})
|
||||
|
||||
expect(env.OPENAI_BASE_URL).toBe('https://dashscope.aliyuncs.com/v2beta')
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,5 @@
|
||||
import { type EndpointType, type Model, type Provider, SystemProviderIds } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { codeTools } from '@shared/config/constant'
|
||||
|
||||
export interface LaunchValidationResult {
|
||||
@ -145,6 +146,7 @@ export const generateToolEnvironment = ({
|
||||
baseUrl: string
|
||||
}): Record<string, string> => {
|
||||
const env: Record<string, string> = {}
|
||||
const formattedBaseUrl = formatApiHost(baseUrl)
|
||||
|
||||
switch (tool) {
|
||||
case codeTools.claudeCode:
|
||||
@ -169,19 +171,19 @@ export const generateToolEnvironment = ({
|
||||
|
||||
case codeTools.qwenCode:
|
||||
env.OPENAI_API_KEY = apiKey
|
||||
env.OPENAI_BASE_URL = baseUrl
|
||||
env.OPENAI_BASE_URL = formattedBaseUrl
|
||||
env.OPENAI_MODEL = model.id
|
||||
break
|
||||
case codeTools.openaiCodex:
|
||||
env.OPENAI_API_KEY = apiKey
|
||||
env.OPENAI_BASE_URL = baseUrl
|
||||
env.OPENAI_BASE_URL = formattedBaseUrl
|
||||
env.OPENAI_MODEL = model.id
|
||||
env.OPENAI_MODEL_PROVIDER = modelProvider.id
|
||||
break
|
||||
|
||||
case codeTools.iFlowCli:
|
||||
env.IFLOW_API_KEY = apiKey
|
||||
env.IFLOW_BASE_URL = baseUrl
|
||||
env.IFLOW_BASE_URL = formattedBaseUrl
|
||||
env.IFLOW_MODEL_NAME = model.id
|
||||
break
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import AssistantAvatar from '@renderer/components/Avatar/AssistantAvatar'
|
||||
import type { DraggableVirtualListRef } from '@renderer/components/DraggableList'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
@ -85,6 +86,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||
const listRef = useRef<DraggableVirtualListRef>(null)
|
||||
|
||||
// 管理模式状态
|
||||
const manageState = useTopicManageMode()
|
||||
@ -168,10 +170,46 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
|
||||
const onPinTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
let newIndex = 0
|
||||
|
||||
if (topic.pinned) {
|
||||
// 取消固定:将话题移到未固定话题的顶部
|
||||
const pinnedTopics = assistant.topics.filter((t) => t.pinned)
|
||||
const unpinnedTopics = assistant.topics.filter((t) => !t.pinned)
|
||||
|
||||
// 构建新顺序:其他固定话题 + 取消固定的话题(移到顶部) + 其他未固定话题
|
||||
const reorderedTopics = [
|
||||
...pinnedTopics.filter((t) => t.id !== topic.id), // 其他固定话题
|
||||
topic, // 取消固定的话题移到顶部
|
||||
...unpinnedTopics // 其他未固定话题
|
||||
]
|
||||
|
||||
newIndex = pinnedTopics.length - 1 // 最后一个固定话题的索引 + 1 = 第一个未固定的索引
|
||||
updateTopics(reorderedTopics)
|
||||
} else {
|
||||
// 固定话题:移到固定区域顶部
|
||||
const pinnedTopics = assistant.topics.filter((t) => t.pinned)
|
||||
const unpinnedTopics = assistant.topics.filter((t) => !t.pinned)
|
||||
|
||||
const reorderedTopics = [
|
||||
topic, // 新固定的话题移到顶部
|
||||
...pinnedTopics, // 其他固定话题
|
||||
...unpinnedTopics.filter((t) => t.id !== topic.id) // 其他未固定话题(排除 topic)
|
||||
]
|
||||
|
||||
newIndex = 0
|
||||
updateTopics(reorderedTopics)
|
||||
}
|
||||
|
||||
const updatedTopic = { ...topic, pinned: !topic.pinned }
|
||||
updateTopic(updatedTopic)
|
||||
|
||||
// 延迟滚动到话题位置(等待渲染完成)
|
||||
setTimeout(() => {
|
||||
listRef.current?.scrollToIndex(newIndex, { align: 'auto' })
|
||||
}, 50)
|
||||
},
|
||||
[updateTopic]
|
||||
[assistant.topics, updateTopic, updateTopics]
|
||||
)
|
||||
|
||||
const onDeleteTopic = useCallback(
|
||||
@ -529,6 +567,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
return (
|
||||
<>
|
||||
<DraggableVirtualList
|
||||
ref={listRef}
|
||||
className="topics-tab"
|
||||
list={filteredTopics}
|
||||
onUpdate={updateTopics}
|
||||
@ -663,7 +702,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
||||
</TopicPromptText>
|
||||
)}
|
||||
{showTopicTime && (
|
||||
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
|
||||
<TopicTime className="time">{dayjs(topic.createdAt).format('YYYY/MM/DD HH:mm')}</TopicTime>
|
||||
)}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
|
||||
@ -15,7 +15,7 @@ import { runAsyncFunction } from '@renderer/utils'
|
||||
import { UpgradeChannel } from '@shared/config/constant'
|
||||
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { Briefcase, Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||
import { BadgeQuestionMark } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -327,6 +327,16 @@ const AboutSettings: FC = () => {
|
||||
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Briefcase size={18} />
|
||||
{t('settings.about.careers.title')}
|
||||
</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://www.cherry-ai.com/careers')}>
|
||||
{t('settings.about.careers.button')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Bug size={18} />
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { InfoTooltip } from '@renderer/components/TooltipIcons'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import type { Provider } from '@renderer/types'
|
||||
import { Flex, Switch } from 'antd'
|
||||
import { type AnthropicCacheControlSettings, type Provider } from '@renderer/types'
|
||||
import { isSupportAnthropicPromptCacheProvider } from '@renderer/utils/provider'
|
||||
import { Divider, Flex, InputNumber, Switch } from 'antd'
|
||||
import { startTransition, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -114,6 +115,27 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
|
||||
return items
|
||||
}, [openAIOptions, provider.apiOptions, provider.type, t, updateProviderTransition])
|
||||
|
||||
const isSupportAnthropicPromptCache = isSupportAnthropicPromptCacheProvider(provider)
|
||||
|
||||
const cacheSettings = useMemo(
|
||||
() =>
|
||||
provider.anthropicCacheControl ?? {
|
||||
tokenThreshold: 0,
|
||||
cacheSystemMessage: true,
|
||||
cacheLastNMessages: 0
|
||||
},
|
||||
[provider.anthropicCacheControl]
|
||||
)
|
||||
|
||||
const updateCacheSettings = useCallback(
|
||||
(updates: Partial<AnthropicCacheControlSettings>) => {
|
||||
updateProviderTransition({
|
||||
anthropicCacheControl: { ...cacheSettings, ...updates }
|
||||
})
|
||||
},
|
||||
[cacheSettings, updateProviderTransition]
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex vertical gap="middle">
|
||||
{options.map((item) => (
|
||||
@ -127,6 +149,52 @@ const ApiOptionsSettings = ({ providerId }: Props) => {
|
||||
<Switch id={item.key} checked={item.checked} onChange={item.onChange} />
|
||||
</HStack>
|
||||
))}
|
||||
|
||||
{isSupportAnthropicPromptCache && (
|
||||
<>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<HStack justifyContent="space-between">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<span>{t('settings.provider.api.options.anthropic_cache.token_threshold')}</span>
|
||||
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.token_threshold_help')} />
|
||||
</HStack>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100000}
|
||||
value={cacheSettings.tokenThreshold}
|
||||
onChange={(v) => updateCacheSettings({ tokenThreshold: v ?? 0 })}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</HStack>
|
||||
{cacheSettings.tokenThreshold > 0 && (
|
||||
<>
|
||||
<HStack justifyContent="space-between">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<span>{t('settings.provider.api.options.anthropic_cache.cache_system')}</span>
|
||||
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.cache_system_help')} />
|
||||
</HStack>
|
||||
<Switch
|
||||
checked={cacheSettings.cacheSystemMessage}
|
||||
onChange={(v) => updateCacheSettings({ cacheSystemMessage: v })}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justifyContent="space-between">
|
||||
<HStack alignItems="center" gap={6}>
|
||||
<span>{t('settings.provider.api.options.anthropic_cache.cache_last_n')}</span>
|
||||
<InfoTooltip title={t('settings.provider.api.options.anthropic_cache.cache_last_n_help')} />
|
||||
</HStack>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={10}
|
||||
value={cacheSettings.cacheLastNMessages}
|
||||
onChange={(v) => updateCacheSettings({ cacheLastNMessages: v ?? 0 })}
|
||||
style={{ width: 100 }}
|
||||
/>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
isOllamaProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
isOpenAIProvider,
|
||||
isSupportAnthropicPromptCacheProvider,
|
||||
isVertexProvider
|
||||
} from '@renderer/utils/provider'
|
||||
import { Button, Divider, Flex, Input, Select, Space, Switch, Tooltip } from 'antd'
|
||||
@ -400,7 +401,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
|
||||
<Button type="text" size="small" icon={<SquareArrowOutUpRight size={14} />} />
|
||||
</Link>
|
||||
)}
|
||||
{!isSystemProvider(provider) && (
|
||||
{(!isSystemProvider(provider) || isSupportAnthropicPromptCacheProvider(provider)) && (
|
||||
<Tooltip title={t('settings.provider.api.options.label')}>
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
@ -113,9 +113,10 @@ class LoggerService {
|
||||
* @param data - Additional data to log
|
||||
*/
|
||||
private processLog(level: LogLevel, message: string, data: any[]): void {
|
||||
let windowSource = this.window
|
||||
if (!this.window) {
|
||||
console.error('[LoggerService] window source not initialized, please initialize window source first')
|
||||
return
|
||||
windowSource = 'UNKNOWN'
|
||||
}
|
||||
|
||||
const currentLevel = LEVEL_MAP[level]
|
||||
@ -164,7 +165,7 @@ class LoggerService {
|
||||
if (currentLevel >= LEVEL_MAP[this.logToMainLevel] || forceLogToMain) {
|
||||
const source: LogSourceWithContext = {
|
||||
process: 'renderer',
|
||||
window: this.window,
|
||||
window: windowSource,
|
||||
module: this.module
|
||||
}
|
||||
|
||||
|
||||
@ -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