mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +08:00
Compare commits
265 Commits
v1.7.0-rc.
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
90cd06d23d | ||
|
|
8d56bf80dd | ||
|
|
7766438853 | ||
|
|
3ec6e1167f | ||
|
|
b83fbc0ace | ||
|
|
040f4daa98 | ||
|
|
d0a1512f23 | ||
|
|
2777af77d8 | ||
|
|
6d15b0dfd1 | ||
|
|
334b9bbe04 | ||
|
|
ed3401a016 | ||
|
|
91b6ed81cc | ||
|
|
c940b5613f | ||
|
|
6b0bb64795 | ||
|
|
116ee6f94b | ||
|
|
af7896b900 | ||
|
|
bb9b73557b | ||
|
|
a5038ac844 | ||
|
|
9e45f801a8 | ||
|
|
313dac0f64 | ||
|
|
76ee67d4d7 | ||
|
|
2a31fa2ad5 | ||
|
|
c4f372feba | ||
|
|
ad164f2c1b | ||
|
|
ca3ddff00e | ||
|
|
b4aeced1f9 | ||
|
|
d27d750bc5 | ||
|
|
a2639053ef | ||
|
|
68a75dc4e3 | ||
|
|
4c67e5b43a | ||
|
|
2383fd06db | ||
|
|
f8519f0bf0 | ||
|
|
2012378341 | ||
|
|
86adb2e11c | ||
|
|
680bda3993 | ||
|
|
acd1ecc09c | ||
|
|
e3d1996254 | ||
|
|
56cf347909 | ||
|
|
2a3955919e | ||
|
|
ca2b0ac28d | ||
|
|
078cf39313 | ||
|
|
48a582820f | ||
|
|
77e024027c | ||
|
|
d391e55a8a | ||
|
|
f878c8ab3b | ||
|
|
33cdcaa558 | ||
|
|
bc9eeb9f30 | ||
|
|
068cf1083c | ||
|
|
ed4353b054 | ||
|
|
528d6d37f2 | ||
|
|
efbe64e5da | ||
|
|
cccf9bb7be | ||
|
|
c242860abc | ||
|
|
cb93eee29d | ||
|
|
5ff173fcc7 | ||
|
|
b78df05f28 | ||
|
|
c13dc6eab5 | ||
|
|
2008d70707 | ||
|
|
723fa11647 | ||
|
|
9586f38157 | ||
|
|
401d66f3dd | ||
|
|
99b431ec92 | ||
|
|
ab3bce33b8 | ||
|
|
0f0e18231d | ||
|
|
4ae9bf8ff4 | ||
|
|
05dfb459a6 | ||
|
|
0669253abb | ||
|
|
4ba0f2d25c | ||
|
|
f7312697e7 | ||
|
|
d9171e0596 | ||
|
|
89a6d817f1 | ||
|
|
09e58d3756 | ||
|
|
e093a18deb | ||
|
|
265934be5a | ||
|
|
5f0006dced | ||
|
|
6815ab65d1 | ||
|
|
6bdaba8a15 | ||
|
|
d1c93e4eae | ||
|
|
7a862974c2 | ||
|
|
26a3bd0259 | ||
|
|
e16413de76 | ||
|
|
fc3e92e2f7 | ||
|
|
9a435b8abb | ||
|
|
c4f94848e8 | ||
|
|
c747b8e2a4 | ||
|
|
a35bf4afa1 | ||
|
|
9f948e1ce7 | ||
|
|
4508fe2877 | ||
|
|
3045f924ce | ||
|
|
a6ba5d34e0 | ||
|
|
8ab375161d | ||
|
|
42260710d8 | ||
|
|
5e8646c6a5 | ||
|
|
7e93e8b9b2 | ||
|
|
eb7a2cc85a | ||
|
|
fd6986076a | ||
|
|
6309cc179d | ||
|
|
c04529a23c | ||
|
|
0f1b3afa72 | ||
|
|
0cf0072b51 | ||
|
|
150bb3e3a0 | ||
|
|
739096deca | ||
|
|
1d5dafa325 | ||
|
|
bdfda7afb1 | ||
|
|
ef25eef0eb | ||
|
|
c676a93595 | ||
|
|
e85009fcd6 | ||
|
|
99d7223a0a | ||
|
|
bdd272b7cd | ||
|
|
782f8496e0 | ||
|
|
bfeef7ef91 | ||
|
|
784fdd4fed | ||
|
|
432b31c7b1 | ||
|
|
f2b4a2382b | ||
|
|
b66787280a | ||
|
|
d41229c69b | ||
|
|
aeebd343d7 | ||
|
|
71df9d61fd | ||
|
|
4d3d5ae4ce | ||
|
|
a1f0addafb | ||
|
|
e78f25ff91 | ||
|
|
68f70e3b16 | ||
|
|
fd921103dd | ||
|
|
a1e44a6827 | ||
|
|
ee7eee24da | ||
|
|
f0ec2354dc | ||
|
|
5bd550bfb4 | ||
|
|
dc0c47c64d | ||
|
|
66feee714b | ||
|
|
96aba33077 | ||
|
|
97f6275104 | ||
|
|
b906849c17 | ||
|
|
f742ebed1f | ||
|
|
d7b9a6e09a | ||
|
|
be9a8b8699 | ||
|
|
512d872ac3 | ||
|
|
95f5853d7d | ||
|
|
c1bf6cfbb7 | ||
|
|
595a0f194a | ||
|
|
a91c69982c | ||
|
|
6b25fbb901 | ||
|
|
c52a2dbc48 | ||
|
|
367c4fe6b6 | ||
|
|
5f3af646f4 | ||
|
|
ed695a8620 | ||
|
|
8cd4b1b747 | ||
|
|
9ac7e2c78d | ||
|
|
c4fd48376d | ||
|
|
600a045ff7 | ||
|
|
880673c4eb | ||
|
|
03db02d5f7 | ||
|
|
fda2287475 | ||
|
|
76524d68c6 | ||
|
|
96085707ce | ||
|
|
711f805a5b | ||
|
|
6df60a69c3 | ||
|
|
058a2c763b | ||
|
|
7507443d8b | ||
|
|
8ede7b197f | ||
|
|
086190228a | ||
|
|
adbadf5da6 | ||
|
|
73fc74d875 | ||
|
|
bc00c11a00 | ||
|
|
f8c33db450 | ||
|
|
61c171dafc | ||
|
|
e1e6702425 | ||
|
|
e6003463ac | ||
|
|
0cc4c96bc0 | ||
|
|
d35434b6d6 | ||
|
|
ef5b97813c | ||
|
|
4c4f832bc7 | ||
|
|
9f7e47304d | ||
|
|
1a737f5137 | ||
|
|
82ec18c0fb | ||
|
|
0cabdefb9a | ||
|
|
224ab6a69b | ||
|
|
bba7ecae6e | ||
|
|
516b8479d6 | ||
|
|
b58a2fce03 | ||
|
|
ebfc60b039 | ||
|
|
8f39ecf762 | ||
|
|
8d1d09b1ec | ||
|
|
3cedb95db3 | ||
|
|
9d6d827f88 | ||
|
|
968210faa7 | ||
|
|
ea36b918f1 | ||
|
|
92bb05950d | ||
|
|
a566cd65f4 | ||
|
|
cd699825ed | ||
|
|
86a16f5762 | ||
|
|
6343628739 | ||
|
|
a2a6c62f48 | ||
|
|
981bb9f451 | ||
|
|
9637fb8a43 | ||
|
|
fb20173194 | ||
|
|
387e8f77f5 | ||
|
|
4f701d3e45 | ||
|
|
aeabc28451 | ||
|
|
f571dd7af0 | ||
|
|
33457686ac | ||
|
|
6696bcacb8 | ||
|
|
a1e95b55f8 | ||
|
|
600199dfcf | ||
|
|
77fd90ef7d | ||
|
|
fb45d94efb | ||
|
|
3aedf6f138 | ||
|
|
3e6dc56196 | ||
|
|
b3a58ec321 | ||
|
|
0097ca80e2 | ||
|
|
d968df4612 | ||
|
|
2bd680361a | ||
|
|
cc676d4bef | ||
|
|
3b1155b538 | ||
|
|
03ff6e1ca6 | ||
|
|
706fac898a | ||
|
|
f5c144404d | ||
|
|
50a217a638 | ||
|
|
444c13e1e3 | ||
|
|
255b19d6ee | ||
|
|
f1f4831157 | ||
|
|
876f59d650 | ||
|
|
c23e88ecd1 | ||
|
|
284d0f99e1 | ||
|
|
13ac5d564a | ||
|
|
4620b71aee | ||
|
|
1b926178f1 | ||
|
|
5167c927be | ||
|
|
b18c64b725 | ||
|
|
7ce1590eaf | ||
|
|
77a9504f74 | ||
|
|
bf35902696 | ||
|
|
0d12b5fbc2 | ||
|
|
1746e8b21f | ||
|
|
0836eef1a6 | ||
|
|
d0bd10190d | ||
|
|
d8191bd4fb | ||
|
|
d15571c727 | ||
|
|
a2f67dddb6 | ||
|
|
8f00321a60 | ||
|
|
eb4670c22c |
60
.github/workflows/auto-i18n.yml
vendored
60
.github/workflows/auto-i18n.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 🐈⬛ Checkout
|
- name: 🐈⬛ Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -32,38 +32,37 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: 📦 Install corepack
|
- name: 📦 Install pnpm
|
||||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: 📂 Get yarn cache directory path
|
- name: 📂 Get pnpm store directory
|
||||||
id: yarn-cache-dir-path
|
id: pnpm-cache
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 💾 Cache yarn dependencies
|
- name: 💾 Cache pnpm dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-pnpm-
|
||||||
|
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: |
|
run: |
|
||||||
yarn install
|
pnpm install
|
||||||
|
|
||||||
- name: 🏃♀️ Translate
|
- name: 🏃♀️ Translate
|
||||||
run: yarn sync:i18n && yarn auto:i18n
|
run: pnpm i18n:sync && pnpm i18n:translate
|
||||||
|
|
||||||
- name: 🔍 Format
|
- name: 🔍 Format
|
||||||
run: yarn format
|
run: pnpm format
|
||||||
|
|
||||||
- name: 🔍 Check for changes
|
- name: 🔍 Check for changes
|
||||||
id: git_status
|
id: git_status
|
||||||
run: |
|
run: |
|
||||||
# Check if there are any uncommitted changes
|
# Check if there are any uncommitted changes
|
||||||
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
|
git reset -- package.json pnpm-lock.yaml # 不提交 package.json 和 pnpm-lock.yaml 的更改
|
||||||
git diff --exit-code --quiet || echo "::set-output name=has_changes::true"
|
git diff --exit-code --quiet || echo "::set-output name=has_changes::true"
|
||||||
git status --porcelain
|
git status --porcelain
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 🚀 Create Pull Request if changes exist
|
- name: 🚀 Create Pull Request if changes exist
|
||||||
if: steps.git_status.outputs.has_changes == 'true'
|
if: steps.git_status.outputs.has_changes == 'true'
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions
|
token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions
|
||||||
commit-message: "feat(bot): Weekly automated script run"
|
commit-message: "feat(bot): Weekly automated script run"
|
||||||
@ -91,3 +90,30 @@ jobs:
|
|||||||
- name: 📢 Notify if no changes
|
- name: 📢 Notify if no changes
|
||||||
if: steps.git_status.outputs.has_changes != 'true'
|
if: steps.git_status.outputs.has_changes != 'true'
|
||||||
run: echo "Bot script ran, but no changes were detected. No PR created."
|
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}"
|
||||||
|
|||||||
2
.github/workflows/claude-code-review.yml
vendored
2
.github/workflows/claude-code-review.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/claude-translator.yml
vendored
2
.github/workflows/claude-translator.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
actions: read # Required for Claude to read CI results on PRs
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/dispatch-docs-update.yml
vendored
2
.github/workflows/dispatch-docs-update.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
|
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Dispatch update-download-version workflow to cherry-studio-docs
|
- name: Dispatch update-download-version workflow to cherry-studio-docs
|
||||||
uses: peter-evans/repository-dispatch@v3
|
uses: peter-evans/repository-dispatch@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: CherryHQ/cherry-studio-docs
|
repository: CherryHQ/cherry-studio-docs
|
||||||
|
|||||||
94
.github/workflows/github-issue-tracker.yml
vendored
94
.github/workflows/github-issue-tracker.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Check Beijing Time
|
- name: Check Beijing Time
|
||||||
id: check_time
|
id: check_time
|
||||||
@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add pending label if in quiet hours
|
- name: Add pending label if in quiet hours
|
||||||
if: steps.check_time.outputs.should_delay == 'true'
|
if: steps.check_time.outputs.should_delay == 'true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@ -58,14 +58,34 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
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
|
- name: Process issue with Claude
|
||||||
if: steps.check_time.outputs.should_delay == 'false'
|
if: steps.check_time.outputs.should_delay == 'false'
|
||||||
uses: anthropics/claude-code-action@main
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
allowed_non_write_users: "*"
|
allowed_non_write_users: "*"
|
||||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
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: |
|
prompt: |
|
||||||
你是一个GitHub Issue自动化处理助手。请完成以下任务:
|
你是一个GitHub Issue自动化处理助手。请完成以下任务:
|
||||||
|
|
||||||
@ -74,9 +94,14 @@ jobs:
|
|||||||
- 标题:${{ github.event.issue.title }}
|
- 标题:${{ github.event.issue.title }}
|
||||||
- 作者:${{ github.event.issue.user.login }}
|
- 作者:${{ github.event.issue.user.login }}
|
||||||
- URL:${{ github.event.issue.html_url }}
|
- URL:${{ github.event.issue.html_url }}
|
||||||
- 内容:${{ github.event.issue.body }}
|
|
||||||
- 标签:${{ join(github.event.issue.labels.*.name, ', ') }}
|
- 标签:${{ join(github.event.issue.labels.*.name, ', ') }}
|
||||||
|
|
||||||
|
### Issue body
|
||||||
|
|
||||||
|
`````md
|
||||||
|
${{ github.event.issue.body }}
|
||||||
|
`````
|
||||||
|
|
||||||
## 任务步骤
|
## 任务步骤
|
||||||
|
|
||||||
1. **分析并总结issue**
|
1. **分析并总结issue**
|
||||||
@ -86,20 +111,20 @@ jobs:
|
|||||||
- 重要的技术细节
|
- 重要的技术细节
|
||||||
|
|
||||||
2. **发送飞书通知**
|
2. **发送飞书通知**
|
||||||
使用以下命令发送飞书通知(注意:ISSUE_SUMMARY需要用引号包裹):
|
使用CLI工具发送飞书通知,参考以下示例:
|
||||||
```bash
|
```bash
|
||||||
ISSUE_URL="${{ github.event.issue.html_url }}" \
|
pnpm tsx scripts/feishu-notify.ts issue \
|
||||||
ISSUE_NUMBER="${{ github.event.issue.number }}" \
|
-u "${{ github.event.issue.html_url }}" \
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}" \
|
-n "${{ github.event.issue.number }}" \
|
||||||
ISSUE_AUTHOR="${{ github.event.issue.user.login }}" \
|
-t "${{ github.event.issue.title }}" \
|
||||||
ISSUE_LABELS="${{ join(github.event.issue.labels.*.name, ',') }}" \
|
-a "${{ github.event.issue.user.login }}" \
|
||||||
ISSUE_SUMMARY="<你生成的中文总结>" \
|
-l "${{ join(github.event.issue.labels.*.name, ',') }}" \
|
||||||
node scripts/feishu-notify.js
|
-m "<你生成的中文总结>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
- 总结必须使用简体中文
|
- 总结必须使用简体中文
|
||||||
- ISSUE_SUMMARY 在传递给 node 命令时需要正确转义特殊字符
|
- 命令行参数需要正确转义特殊字符
|
||||||
- 如果issue内容为空,也要提供一个简短的说明
|
- 如果issue内容为空,也要提供一个简短的说明
|
||||||
|
|
||||||
请开始执行任务!
|
请开始执行任务!
|
||||||
@ -118,20 +143,39 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
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
|
- name: Process pending issues with Claude
|
||||||
uses: anthropics/claude-code-action@main
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }}
|
||||||
allowed_non_write_users: "*"
|
allowed_non_write_users: "*"
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
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: |
|
prompt: |
|
||||||
你是一个GitHub Issue自动化处理助手。请完成以下任务:
|
你是一个GitHub Issue自动化处理助手。请完成以下任务:
|
||||||
|
|
||||||
@ -153,15 +197,15 @@ jobs:
|
|||||||
- 重要的技术细节
|
- 重要的技术细节
|
||||||
|
|
||||||
3. **发送飞书通知**
|
3. **发送飞书通知**
|
||||||
对于每个issue,使用以下命令发送飞书通知:
|
使用CLI工具发送飞书通知,参考以下示例:
|
||||||
```bash
|
```bash
|
||||||
ISSUE_URL="<issue的html_url>" \
|
pnpm tsx scripts/feishu-notify.ts issue \
|
||||||
ISSUE_NUMBER="<issue编号>" \
|
-u "<issue的html_url>" \
|
||||||
ISSUE_TITLE="<issue标题>" \
|
-n "<issue编号>" \
|
||||||
ISSUE_AUTHOR="<issue作者>" \
|
-t "<issue标题>" \
|
||||||
ISSUE_LABELS="<逗号分隔的标签列表,排除pending-feishu-notification>" \
|
-a "<issue作者>" \
|
||||||
ISSUE_SUMMARY="<你生成的中文总结>" \
|
-l "<逗号分隔的标签列表,排除pending-feishu-notification>" \
|
||||||
node scripts/feishu-notify.js
|
-m "<你生成的中文总结>"
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **移除标签**
|
4. **移除标签**
|
||||||
|
|||||||
31
.github/workflows/nightly-build.yml
vendored
31
.github/workflows/nightly-build.yml
vendored
@ -51,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
@ -65,25 +65,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
brew install python-setuptools
|
brew install python-setuptools
|
||||||
|
|
||||||
- name: Install corepack
|
- name: Install pnpm
|
||||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get pnpm store directory
|
||||||
id: yarn-cache-dir-path
|
id: pnpm-cache
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache yarn dependencies
|
- name: Cache pnpm dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-pnpm-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Generate date tag
|
- name: Generate date tag
|
||||||
id: date
|
id: date
|
||||||
@ -94,7 +93,7 @@ jobs:
|
|||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y rpm
|
sudo apt-get install -y rpm
|
||||||
yarn build:linux
|
pnpm build:linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
@ -106,7 +105,7 @@ jobs:
|
|||||||
- name: Build Mac
|
- name: Build Mac
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:mac
|
pnpm build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
@ -123,7 +122,7 @@ jobs:
|
|||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:win
|
pnpm build:win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
|
|||||||
35
.github/workflows/pr-ci.yml
vendored
35
.github/workflows/pr-ci.yml
vendored
@ -21,44 +21,43 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Install corepack
|
- name: Install pnpm
|
||||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get pnpm store directory
|
||||||
id: yarn-cache-dir-path
|
id: pnpm-cache
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache yarn dependencies
|
- name: Cache pnpm dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-pnpm-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Lint Check
|
- name: Lint Check
|
||||||
run: yarn test:lint
|
run: pnpm test:lint
|
||||||
|
|
||||||
- name: Format Check
|
- name: Format Check
|
||||||
run: yarn format:check
|
run: pnpm format:check
|
||||||
|
|
||||||
- name: Type Check
|
- name: Type Check
|
||||||
run: yarn typecheck
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: i18n Check
|
- name: i18n Check
|
||||||
run: yarn check:i18n
|
run: pnpm i18n:check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: pnpm test
|
||||||
|
|||||||
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out Git repository
|
- name: Check out Git repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@ -56,31 +56,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
brew install python-setuptools
|
brew install python-setuptools
|
||||||
|
|
||||||
- name: Install corepack
|
- name: Install pnpm
|
||||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get pnpm store directory
|
||||||
id: yarn-cache-dir-path
|
id: pnpm-cache
|
||||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache yarn dependencies
|
- name: Cache pnpm dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
node_modules
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-pnpm-
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build Linux
|
- name: Build Linux
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y rpm
|
sudo apt-get install -y rpm
|
||||||
yarn build:linux
|
pnpm build:linux
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -94,7 +93,7 @@ jobs:
|
|||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: |
|
run: |
|
||||||
sudo -H pip install setuptools
|
sudo -H pip install setuptools
|
||||||
yarn build:mac
|
pnpm build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
@ -111,7 +110,7 @@ jobs:
|
|||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:win
|
pnpm build:win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
|
|||||||
330
.github/workflows/sync-to-gitcode.yml
vendored
Normal file
330
.github/workflows/sync-to-gitcode.yml
vendored
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
name: Sync Release to GitCode
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag (e.g. v1.0.0)'
|
||||||
|
required: true
|
||||||
|
clean:
|
||||||
|
description: 'Clean node_modules before build'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-sync-to-gitcode:
|
||||||
|
runs-on: [self-hosted, windows-signing]
|
||||||
|
steps:
|
||||||
|
- name: Get tag name
|
||||||
|
id: get-tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tag=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ steps.get-tag.outputs.tag }}
|
||||||
|
|
||||||
|
- name: Set package.json version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.get-tag.outputs.tag }}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Clean node_modules
|
||||||
|
if: ${{ github.event.inputs.clean == 'true' }}
|
||||||
|
shell: bash
|
||||||
|
run: rm -rf node_modules
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
shell: bash
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build Windows with code signing
|
||||||
|
shell: bash
|
||||||
|
run: pnpm build:win
|
||||||
|
env:
|
||||||
|
WIN_SIGN: true
|
||||||
|
CHERRY_CERT_PATH: ${{ secrets.CHERRY_CERT_PATH }}
|
||||||
|
CHERRY_CERT_KEY: ${{ secrets.CHERRY_CERT_KEY }}
|
||||||
|
CHERRY_CERT_CSP: ${{ secrets.CHERRY_CERT_CSP }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NODE_OPTIONS: --max-old-space-size=8192
|
||||||
|
MAIN_VITE_CHERRYAI_CLIENT_SECRET: ${{ secrets.MAIN_VITE_CHERRYAI_CLIENT_SECRET }}
|
||||||
|
MAIN_VITE_MINERU_API_KEY: ${{ secrets.MAIN_VITE_MINERU_API_KEY }}
|
||||||
|
RENDERER_VITE_AIHUBMIX_SECRET: ${{ secrets.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||||
|
RENDERER_VITE_PPIO_APP_SECRET: ${{ secrets.RENDERER_VITE_PPIO_APP_SECRET }}
|
||||||
|
|
||||||
|
- name: List built Windows artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Built Windows artifacts:"
|
||||||
|
ls -la dist/*.exe dist/latest*.yml
|
||||||
|
|
||||||
|
- name: Download GitHub release assets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
||||||
|
run: |
|
||||||
|
echo "Downloading release assets for $TAG_NAME..."
|
||||||
|
mkdir -p release-assets
|
||||||
|
cd release-assets
|
||||||
|
|
||||||
|
# Download all assets from the release
|
||||||
|
gh release download "$TAG_NAME" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--pattern "*" \
|
||||||
|
--skip-existing
|
||||||
|
|
||||||
|
echo "Downloaded GitHub release assets:"
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
- name: Replace Windows files with signed versions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Replacing Windows files with signed versions..."
|
||||||
|
|
||||||
|
# Verify signed files exist first
|
||||||
|
if ! ls dist/*.exe 1>/dev/null 2>&1; then
|
||||||
|
echo "ERROR: No signed .exe files found in dist/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove unsigned Windows files from downloaded assets
|
||||||
|
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/latest.yml release-assets/ || { echo "ERROR: Failed to copy latest.yml"; exit 1; }
|
||||||
|
|
||||||
|
echo "Final release assets:"
|
||||||
|
ls -la release-assets/
|
||||||
|
|
||||||
|
- name: Get release info
|
||||||
|
id: release-info
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
||||||
|
LANG: C.UTF-8
|
||||||
|
LC_ALL: C.UTF-8
|
||||||
|
run: |
|
||||||
|
# Always use gh cli to avoid special character issues
|
||||||
|
RELEASE_NAME=$(gh release view "$TAG_NAME" --repo "${{ github.repository }}" --json name -q '.name')
|
||||||
|
# Use delimiter to safely handle special characters in release name
|
||||||
|
{
|
||||||
|
echo 'name<<EOF'
|
||||||
|
echo "$RELEASE_NAME"
|
||||||
|
echo 'EOF'
|
||||||
|
} >> $GITHUB_OUTPUT
|
||||||
|
# Extract releaseNotes from electron-builder.yml (from releaseNotes: | to end of file, remove 4-space indent)
|
||||||
|
sed -n '/releaseNotes: |/,$ { /releaseNotes: |/d; s/^ //; p }' electron-builder.yml > release_body.txt
|
||||||
|
|
||||||
|
- name: Create GitCode release and upload files
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITCODE_TOKEN: ${{ secrets.GITCODE_TOKEN }}
|
||||||
|
GITCODE_OWNER: ${{ vars.GITCODE_OWNER }}
|
||||||
|
GITCODE_REPO: ${{ vars.GITCODE_REPO }}
|
||||||
|
GITCODE_API_URL: ${{ vars.GITCODE_API_URL }}
|
||||||
|
TAG_NAME: ${{ steps.get-tag.outputs.tag }}
|
||||||
|
RELEASE_NAME: ${{ steps.release-info.outputs.name }}
|
||||||
|
LANG: C.UTF-8
|
||||||
|
LC_ALL: C.UTF-8
|
||||||
|
run: |
|
||||||
|
# Validate required environment variables
|
||||||
|
if [ -z "$GITCODE_TOKEN" ]; then
|
||||||
|
echo "ERROR: GITCODE_TOKEN is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$GITCODE_OWNER" ]; then
|
||||||
|
echo "ERROR: GITCODE_OWNER is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$GITCODE_REPO" ]; then
|
||||||
|
echo "ERROR: GITCODE_REPO is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
API_URL="${GITCODE_API_URL:-https://api.gitcode.com/api/v5}"
|
||||||
|
|
||||||
|
echo "Creating GitCode release..."
|
||||||
|
echo "Tag: $TAG_NAME"
|
||||||
|
echo "Repo: $GITCODE_OWNER/$GITCODE_REPO"
|
||||||
|
|
||||||
|
# Step 1: Create release
|
||||||
|
# Use --rawfile to read body directly from file, avoiding shell variable encoding issues
|
||||||
|
jq -n \
|
||||||
|
--arg tag "$TAG_NAME" \
|
||||||
|
--arg name "$RELEASE_NAME" \
|
||||||
|
--rawfile body release_body.txt \
|
||||||
|
'{
|
||||||
|
tag_name: $tag,
|
||||||
|
name: $name,
|
||||||
|
body: $body,
|
||||||
|
target_commitish: "main"
|
||||||
|
}' > /tmp/release_payload.json
|
||||||
|
|
||||||
|
RELEASE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
|
||||||
|
--connect-timeout 30 --max-time 60 \
|
||||||
|
"${API_URL}/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases" \
|
||||||
|
-H "Content-Type: application/json; charset=utf-8" \
|
||||||
|
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
|
||||||
|
--data-binary "@/tmp/release_payload.json")
|
||||||
|
|
||||||
|
HTTP_CODE=$(echo "$RELEASE_RESPONSE" | tail -n1)
|
||||||
|
RESPONSE_BODY=$(echo "$RELEASE_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||||
|
echo "Release created successfully"
|
||||||
|
else
|
||||||
|
echo "Warning: Release creation returned HTTP $HTTP_CODE"
|
||||||
|
echo "$RESPONSE_BODY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 2: Upload files to release
|
||||||
|
echo "Uploading files to GitCode release..."
|
||||||
|
|
||||||
|
# Function to upload a single file with retry
|
||||||
|
upload_file() {
|
||||||
|
local file="$1"
|
||||||
|
local filename=$(basename "$file")
|
||||||
|
local max_retries=3
|
||||||
|
local retry=0
|
||||||
|
local curl_status=0
|
||||||
|
|
||||||
|
echo "Uploading: $filename"
|
||||||
|
|
||||||
|
# URL encode the filename
|
||||||
|
encoded_filename=$(printf '%s' "$filename" | jq -sRr @uri)
|
||||||
|
|
||||||
|
while [ $retry -lt $max_retries ]; do
|
||||||
|
# Get upload URL
|
||||||
|
curl_status=0
|
||||||
|
UPLOAD_INFO=$(curl -s --connect-timeout 30 --max-time 60 \
|
||||||
|
-H "Authorization: Bearer ${GITCODE_TOKEN}" \
|
||||||
|
"${API_URL}/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases/${TAG_NAME}/upload_url?file_name=${encoded_filename}") || curl_status=$?
|
||||||
|
|
||||||
|
if [ $curl_status -eq 0 ]; then
|
||||||
|
UPLOAD_URL=$(echo "$UPLOAD_INFO" | jq -r '.url // empty')
|
||||||
|
|
||||||
|
if [ -n "$UPLOAD_URL" ]; then
|
||||||
|
# Write headers to temp file to avoid shell escaping issues
|
||||||
|
echo "$UPLOAD_INFO" | jq -r '.headers | to_entries[] | "header = \"" + .key + ": " + .value + "\""' > /tmp/upload_headers.txt
|
||||||
|
|
||||||
|
# Upload file using PUT with headers from file
|
||||||
|
curl_status=0
|
||||||
|
UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||||
|
-K /tmp/upload_headers.txt \
|
||||||
|
--data-binary "@${file}" \
|
||||||
|
"$UPLOAD_URL") || curl_status=$?
|
||||||
|
|
||||||
|
if [ $curl_status -eq 0 ]; then
|
||||||
|
HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | tail -n1)
|
||||||
|
RESPONSE_BODY=$(echo "$UPLOAD_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||||
|
echo " Uploaded: $filename"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo " Failed (HTTP $HTTP_CODE), retry $((retry + 1))/$max_retries"
|
||||||
|
echo " Response: $RESPONSE_BODY"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Upload request failed (curl exit $curl_status), retry $((retry + 1))/$max_retries"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Failed to get upload URL, retry $((retry + 1))/$max_retries"
|
||||||
|
echo " Response: $UPLOAD_INFO"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Failed to get upload URL (curl exit $curl_status), retry $((retry + 1))/$max_retries"
|
||||||
|
echo " Response: $UPLOAD_INFO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
retry=$((retry + 1))
|
||||||
|
[ $retry -lt $max_retries ] && sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " Failed: $filename after $max_retries retries"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload non-yml/json files first
|
||||||
|
for file in release-assets/*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
if [[ ! "$filename" =~ \.(yml|yaml|json)$ ]]; then
|
||||||
|
upload_file "$file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Upload yml/json files last
|
||||||
|
for file in release-assets/*; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
filename=$(basename "$file")
|
||||||
|
if [[ "$filename" =~ \.(yml|yaml|json)$ ]]; then
|
||||||
|
upload_file "$file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "GitCode release sync completed!"
|
||||||
|
|
||||||
|
- name: Cleanup temp files
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
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}"
|
||||||
45
.github/workflows/update-app-upgrade-config.yml
vendored
45
.github/workflows/update-app-upgrade-config.yml
vendored
@ -19,10 +19,9 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
propose-update:
|
update-config:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.draft == false)
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && github.event.release.draft == false)
|
||||||
|
|
||||||
@ -135,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout default branch
|
- name: Checkout default branch
|
||||||
if: steps.check.outputs.should_run == 'true'
|
if: steps.check.outputs.should_run == 'true'
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.repository.default_branch }}
|
ref: ${{ github.event.repository.default_branch }}
|
||||||
path: main
|
path: main
|
||||||
@ -143,7 +142,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout x-files/app-upgrade-config branch
|
- name: Checkout x-files/app-upgrade-config branch
|
||||||
if: steps.check.outputs.should_run == 'true'
|
if: steps.check.outputs.should_run == 'true'
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: x-files/app-upgrade-config
|
ref: x-files/app-upgrade-config
|
||||||
path: cs
|
path: cs
|
||||||
@ -155,14 +154,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
- name: Enable Corepack
|
- name: Enable corepack
|
||||||
if: steps.check.outputs.should_run == 'true'
|
if: steps.check.outputs.should_run == 'true'
|
||||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
working-directory: main
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.check.outputs.should_run == 'true'
|
if: steps.check.outputs.should_run == 'true'
|
||||||
working-directory: main
|
working-directory: main
|
||||||
run: yarn install --immutable
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Update upgrade config
|
- name: Update upgrade config
|
||||||
if: steps.check.outputs.should_run == 'true'
|
if: steps.check.outputs.should_run == 'true'
|
||||||
@ -171,7 +171,7 @@ jobs:
|
|||||||
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
|
RELEASE_TAG: ${{ steps.meta.outputs.tag }}
|
||||||
IS_PRERELEASE: ${{ steps.check.outputs.is_prerelease }}
|
IS_PRERELEASE: ${{ steps.check.outputs.is_prerelease }}
|
||||||
run: |
|
run: |
|
||||||
yarn tsx scripts/update-app-upgrade-config.ts \
|
pnpm tsx scripts/update-app-upgrade-config.ts \
|
||||||
--tag "$RELEASE_TAG" \
|
--tag "$RELEASE_TAG" \
|
||||||
--config ../cs/app-upgrade-config.json \
|
--config ../cs/app-upgrade-config.json \
|
||||||
--is-prerelease "$IS_PRERELEASE"
|
--is-prerelease "$IS_PRERELEASE"
|
||||||
@ -187,25 +187,20 @@ jobs:
|
|||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create pull request
|
- name: Commit and push changes
|
||||||
if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed == 'true'
|
if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed == 'true'
|
||||||
uses: peter-evans/create-pull-request@v7
|
working-directory: cs
|
||||||
with:
|
run: |
|
||||||
path: cs
|
git config user.name "github-actions[bot]"
|
||||||
base: x-files/app-upgrade-config
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
branch: chore/update-app-upgrade-config/${{ steps.meta.outputs.safe_tag }}
|
git add app-upgrade-config.json
|
||||||
commit-message: "🤖 chore: sync app-upgrade-config for ${{ steps.meta.outputs.tag }}"
|
git commit -m "chore: sync app-upgrade-config for ${{ steps.meta.outputs.tag }}" -m "Automated update triggered by \`${{ steps.meta.outputs.trigger }}\`.
|
||||||
title: "chore: update app-upgrade-config for ${{ steps.meta.outputs.tag }}"
|
|
||||||
body: |
|
|
||||||
Automated update triggered by `${{ steps.meta.outputs.trigger }}`.
|
|
||||||
|
|
||||||
- Source tag: `${{ steps.meta.outputs.tag }}`
|
- Source tag: \`${{ steps.meta.outputs.tag }}\`
|
||||||
- Pre-release: `${{ steps.meta.outputs.prerelease }}`
|
- Pre-release: \`${{ steps.meta.outputs.prerelease }}\`
|
||||||
- Latest: `${{ steps.meta.outputs.latest }}`
|
- Latest: \`${{ steps.meta.outputs.latest }}\`
|
||||||
- Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
- Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
labels: |
|
git push origin x-files/app-upgrade-config
|
||||||
automation
|
|
||||||
app-upgrade
|
|
||||||
|
|
||||||
- name: No changes detected
|
- name: No changes detected
|
||||||
if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed != 'true'
|
if: steps.check.outputs.should_run == 'true' && steps.diff.outputs.changed != 'true'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
yarn lint-staged
|
pnpm lint-staged
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@ -1 +1 @@
|
|||||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"dist/**",
|
"dist/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"local/**",
|
"local/**",
|
||||||
|
"tests/**",
|
||||||
".yarn/**",
|
".yarn/**",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
"scripts/cloudflare-worker.js",
|
"scripts/cloudflare-worker.js",
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
diff --git a/dist/index.js b/dist/index.js
|
|
||||||
index 73045a7d38faafdc7f7d2cd79d7ff0e2b031056b..8d948c9ac4ea4b474db9ef3c5491961e7fcf9a07 100644
|
|
||||||
--- a/dist/index.js
|
|
||||||
+++ b/dist/index.js
|
|
||||||
@@ -421,6 +421,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
|
||||||
text: reasoning
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+ if (choice.message.images) {
|
|
||||||
+ for (const image of choice.message.images) {
|
|
||||||
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
|
||||||
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
|
||||||
+ content.push({
|
|
||||||
+ type: 'file',
|
|
||||||
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
|
||||||
+ data: match2 ? match2[1] : image.image_url.url,
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
if (choice.message.tool_calls != null) {
|
|
||||||
for (const toolCall of choice.message.tool_calls) {
|
|
||||||
content.push({
|
|
||||||
@@ -598,6 +609,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
|
||||||
delta: delta.content
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+ if (delta.images) {
|
|
||||||
+ for (const image of delta.images) {
|
|
||||||
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
|
||||||
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
|
||||||
+ controller.enqueue({
|
|
||||||
+ type: 'file',
|
|
||||||
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
|
||||||
+ data: match2 ? match2[1] : image.image_url.url,
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
if (delta.tool_calls != null) {
|
|
||||||
for (const toolCallDelta of delta.tool_calls) {
|
|
||||||
const index = toolCallDelta.index;
|
|
||||||
@@ -765,6 +787,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
|
|
||||||
arguments: import_v43.z.string()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
+ ).nullish(),
|
|
||||||
+ images: import_v43.z.array(
|
|
||||||
+ import_v43.z.object({
|
|
||||||
+ type: import_v43.z.literal('image_url'),
|
|
||||||
+ image_url: import_v43.z.object({
|
|
||||||
+ url: import_v43.z.string(),
|
|
||||||
+ })
|
|
||||||
+ })
|
|
||||||
).nullish()
|
|
||||||
}),
|
|
||||||
finish_reason: import_v43.z.string().nullish()
|
|
||||||
@@ -795,6 +825,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
|
|
||||||
arguments: import_v43.z.string().nullish()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
+ ).nullish(),
|
|
||||||
+ images: import_v43.z.array(
|
|
||||||
+ import_v43.z.object({
|
|
||||||
+ type: import_v43.z.literal('image_url'),
|
|
||||||
+ image_url: import_v43.z.object({
|
|
||||||
+ url: import_v43.z.string(),
|
|
||||||
+ })
|
|
||||||
+ })
|
|
||||||
).nullish()
|
|
||||||
}).nullish(),
|
|
||||||
finish_reason: import_v43.z.string().nullish()
|
|
||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
|
||||||
index 1c2b9560bbfbfe10cb01af080aeeed4ff59db29c..2c8ddc4fc9bfc5e7e06cfca105d197a08864c427 100644
|
|
||||||
--- a/dist/index.mjs
|
|
||||||
+++ b/dist/index.mjs
|
|
||||||
@@ -405,6 +405,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
|
||||||
text: reasoning
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+ if (choice.message.images) {
|
|
||||||
+ for (const image of choice.message.images) {
|
|
||||||
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
|
||||||
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
|
||||||
+ content.push({
|
|
||||||
+ type: 'file',
|
|
||||||
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
|
||||||
+ data: match2 ? match2[1] : image.image_url.url,
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
if (choice.message.tool_calls != null) {
|
|
||||||
for (const toolCall of choice.message.tool_calls) {
|
|
||||||
content.push({
|
|
||||||
@@ -582,6 +593,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
|
||||||
delta: delta.content
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+ if (delta.images) {
|
|
||||||
+ for (const image of delta.images) {
|
|
||||||
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
|
||||||
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
|
||||||
+ controller.enqueue({
|
|
||||||
+ type: 'file',
|
|
||||||
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
|
||||||
+ data: match2 ? match2[1] : image.image_url.url,
|
|
||||||
+ });
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
if (delta.tool_calls != null) {
|
|
||||||
for (const toolCallDelta of delta.tool_calls) {
|
|
||||||
const index = toolCallDelta.index;
|
|
||||||
@@ -749,6 +771,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
|
|
||||||
arguments: z3.string()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
+ ).nullish(),
|
|
||||||
+ images: z3.array(
|
|
||||||
+ z3.object({
|
|
||||||
+ type: z3.literal('image_url'),
|
|
||||||
+ image_url: z3.object({
|
|
||||||
+ url: z3.string(),
|
|
||||||
+ })
|
|
||||||
+ })
|
|
||||||
).nullish()
|
|
||||||
}),
|
|
||||||
finish_reason: z3.string().nullish()
|
|
||||||
@@ -779,6 +809,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
|
|
||||||
arguments: z3.string().nullish()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
+ ).nullish(),
|
|
||||||
+ images: z3.array(
|
|
||||||
+ z3.object({
|
|
||||||
+ type: z3.literal('image_url'),
|
|
||||||
+ image_url: z3.object({
|
|
||||||
+ url: z3.string(),
|
|
||||||
+ })
|
|
||||||
+ })
|
|
||||||
).nullish()
|
|
||||||
}).nullish(),
|
|
||||||
finish_reason: z3.string().nullish()
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
diff --git a/sdk.mjs b/sdk.mjs
|
|
||||||
index bf429a344b7d59f70aead16b639f949b07688a81..f77d50cc5d3fb04292cb3ac7fa7085d02dcc628f 100755
|
|
||||||
--- a/sdk.mjs
|
|
||||||
+++ b/sdk.mjs
|
|
||||||
@@ -6250,7 +6250,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
|
|
||||||
@@ -6619,18 +6619,11 @@ class ProcessTransport {
|
|
||||||
const errorMessage = isNativeBinary(pathToClaudeCodeExecutable) ? `Claude Code native binary not found at ${pathToClaudeCodeExecutable}. Please ensure Claude Code is installed via native installer or specify a valid path with options.pathToClaudeCodeExecutable.` : `Claude Code executable not found at ${pathToClaudeCodeExecutable}. Is options.pathToClaudeCodeExecutable set?`;
|
|
||||||
throw new ReferenceError(errorMessage);
|
|
||||||
}
|
|
||||||
- const isNative = isNativeBinary(pathToClaudeCodeExecutable);
|
|
||||||
- const spawnCommand = isNative ? pathToClaudeCodeExecutable : executable;
|
|
||||||
- const spawnArgs = isNative ? [...executableArgs, ...args] : [...executableArgs, pathToClaudeCodeExecutable, ...args];
|
|
||||||
- const spawnMessage = isNative ? `Spawning Claude Code native binary: ${spawnCommand} ${spawnArgs.join(" ")}` : `Spawning Claude Code process: ${spawnCommand} ${spawnArgs.join(" ")}`;
|
|
||||||
- logForSdkDebugging(spawnMessage);
|
|
||||||
- if (stderr) {
|
|
||||||
- stderr(spawnMessage);
|
|
||||||
- }
|
|
||||||
+ logForSdkDebugging(`Forking Claude Code Node.js process: ${pathToClaudeCodeExecutable} ${args.join(" ")}`);
|
|
||||||
const stderrMode = env.DEBUG_CLAUDE_AGENT_SDK || stderr ? "pipe" : "ignore";
|
|
||||||
- this.child = spawn(spawnCommand, spawnArgs, {
|
|
||||||
+ this.child = fork(pathToClaudeCodeExecutable, args, {
|
|
||||||
cwd,
|
|
||||||
- stdio: ["pipe", "pipe", stderrMode],
|
|
||||||
+ stdio: stderrMode === "pipe" ? ["pipe", "pipe", "pipe", "ipc"] : ["pipe", "pipe", "ignore", "ipc"],
|
|
||||||
signal: this.abortController.signal,
|
|
||||||
env
|
|
||||||
});
|
|
||||||
BIN
.yarn/releases/yarn-4.9.1.cjs
vendored
BIN
.yarn/releases/yarn-4.9.1.cjs
vendored
Binary file not shown.
@ -1,9 +0,0 @@
|
|||||||
enableImmutableInstalls: false
|
|
||||||
|
|
||||||
httpTimeout: 300000
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
|
||||||
npmRegistryServer: https://registry.npmjs.org
|
|
||||||
npmPublishRegistry: https://registry.npmjs.org
|
|
||||||
45
CLAUDE.md
45
CLAUDE.md
@ -10,42 +10,53 @@ This file provides guidance to AI coding assistants when working with code in th
|
|||||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||||
- **Lint, test, and format before completion**: Coding tasks are only complete after running `yarn lint`, `yarn test`, and `yarn format` successfully.
|
- **Lint, test, and format before completion**: Coding tasks are only complete after running `pnpm lint`, `pnpm test`, and `pnpm format` successfully.
|
||||||
- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`).
|
- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`).
|
||||||
- **Follow PR template**: When submitting pull requests, follow the template in `.github/pull_request_template.md` to ensure complete context and documentation.
|
|
||||||
|
## Pull Request Workflow (CRITICAL)
|
||||||
|
|
||||||
|
When creating a Pull Request, you MUST:
|
||||||
|
|
||||||
|
1. **Read the PR template first**: Always read `.github/pull_request_template.md` before creating the PR
|
||||||
|
2. **Follow ALL template sections**: Structure the `--body` parameter to include every section from the template
|
||||||
|
3. **Never skip sections**: Include all sections even if marking them as N/A or "None"
|
||||||
|
4. **Use proper formatting**: Match the template's markdown structure exactly (headings, checkboxes, code blocks)
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
- **Install**: `yarn install` - Install all project dependencies
|
- **Install**: `pnpm install` - Install all project dependencies
|
||||||
- **Development**: `yarn dev` - Runs Electron app in development mode with hot reload
|
- **Development**: `pnpm dev` - Runs Electron app in development mode with hot reload
|
||||||
- **Debug**: `yarn debug` - Starts with debugging enabled, use `chrome://inspect` to attach debugger
|
- **Debug**: `pnpm debug` - Starts with debugging enabled, use `chrome://inspect` to attach debugger
|
||||||
- **Build Check**: `yarn build:check` - **REQUIRED** before commits (lint + test + typecheck)
|
- **Build Check**: `pnpm build:check` - **REQUIRED** before commits (lint + test + typecheck)
|
||||||
- If having i18n sort issues, run `yarn sync:i18n` first to sync template
|
- If having i18n sort issues, run `pnpm i18n:sync` first to sync template
|
||||||
- If having formatting issues, run `yarn format` first
|
- If having formatting issues, run `pnpm format` first
|
||||||
- **Test**: `yarn test` - Run all tests (Vitest) across main and renderer processes
|
- **Test**: `pnpm test` - Run all tests (Vitest) across main and renderer processes
|
||||||
- **Single Test**:
|
- **Single Test**:
|
||||||
- `yarn test:main` - Run tests for main process only
|
- `pnpm test:main` - Run tests for main process only
|
||||||
- `yarn test:renderer` - Run tests for renderer process only
|
- `pnpm test:renderer` - Run tests for renderer process only
|
||||||
- **Lint**: `yarn lint` - Fix linting issues and run TypeScript type checking
|
- **Lint**: `pnpm lint` - Fix linting issues and run TypeScript type checking
|
||||||
- **Format**: `yarn format` - Auto-format code using Biome
|
- **Format**: `pnpm format` - Auto-format code using Biome
|
||||||
|
|
||||||
## Project Architecture
|
## Project Architecture
|
||||||
|
|
||||||
### Electron Structure
|
### Electron Structure
|
||||||
|
|
||||||
- **Main Process** (`src/main/`): Node.js backend with services (MCP, Knowledge, Storage, etc.)
|
- **Main Process** (`src/main/`): Node.js backend with services (MCP, Knowledge, Storage, etc.)
|
||||||
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
||||||
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
|
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
|
||||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
- **Build System**: Electron-Vite with experimental rolldown-vite, pnpm workspaces.
|
||||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from "@logger";
|
||||||
const logger = loggerService.withContext('moduleName')
|
const logger = loggerService.withContext("moduleName");
|
||||||
// Renderer: loggerService.initWindowSource('windowName') first
|
// Renderer: loggerService.initWindowSource('windowName') first
|
||||||
logger.info('message', CONTEXT)
|
logger.info("message", CONTEXT);
|
||||||
```
|
```
|
||||||
|
|||||||
14
README.md
14
README.md
@ -34,7 +34,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
|
<p align="center">English | <a href="./docs/zh/README.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/docs/en-us">Documents</a> | <a href="./docs/en/guides/development.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@ -242,12 +242,12 @@ The Enterprise Edition addresses core challenges in team collaboration by centra
|
|||||||
|
|
||||||
## Version Comparison
|
## Version Comparison
|
||||||
|
|
||||||
| Feature | Community Edition | Enterprise Edition |
|
| Feature | Community Edition | Enterprise Edition |
|
||||||
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
||||||
| **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee |
|
| **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee |
|
||||||
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
|
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
|
||||||
| **Server** | — | ✅ Dedicated Private Deployment |
|
| **Server** | — | ✅ Dedicated Private Deployment |
|
||||||
|
|
||||||
## Get the Enterprise Edition
|
## Get the Enterprise Edition
|
||||||
|
|
||||||
@ -275,7 +275,7 @@ We believe the Enterprise Edition will become your team's AI productivity engine
|
|||||||
|
|
||||||
# 📊 GitHub Stats
|
# 📊 GitHub Stats
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# ⭐️ Star History
|
# ⭐️ Star History
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false,
|
"ignoreUnknown": false,
|
||||||
"includes": ["**", "!**/.claude/**", "!**/.vscode/**"],
|
"includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**"],
|
||||||
"maxSize": 2097152
|
"maxSize": 2097152
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
@ -50,7 +50,8 @@
|
|||||||
"!*.json",
|
"!*.json",
|
||||||
"!src/main/integration/**",
|
"!src/main/integration/**",
|
||||||
"!**/tailwind.css",
|
"!**/tailwind.css",
|
||||||
"!**/package.json"
|
"!**/package.json",
|
||||||
|
"!.zed/**"
|
||||||
],
|
],
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 2,
|
"indentWidth": 2,
|
||||||
|
|||||||
@ -12,8 +12,13 @@
|
|||||||
|
|
||||||
; https://github.com/electron-userland/electron-builder/issues/1122
|
; https://github.com/electron-userland/electron-builder/issues/1122
|
||||||
!ifndef BUILD_UNINSTALLER
|
!ifndef BUILD_UNINSTALLER
|
||||||
|
; Check VC++ Redistributable based on architecture stored in $1
|
||||||
Function checkVCRedist
|
Function checkVCRedist
|
||||||
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
${If} $1 == "arm64"
|
||||||
|
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\ARM64" "Installed"
|
||||||
|
${Else}
|
||||||
|
ReadRegDWORD $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed"
|
||||||
|
${EndIf}
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
Function checkArchitectureCompatibility
|
Function checkArchitectureCompatibility
|
||||||
@ -97,29 +102,47 @@
|
|||||||
|
|
||||||
Call checkVCRedist
|
Call checkVCRedist
|
||||||
${If} $0 != "1"
|
${If} $0 != "1"
|
||||||
MessageBox MB_YESNO "\
|
; VC++ is required - install automatically since declining would abort anyway
|
||||||
NOTE: ${PRODUCT_NAME} requires $\r$\n\
|
; Select download URL based on system architecture (stored in $1)
|
||||||
'Microsoft Visual C++ Redistributable'$\r$\n\
|
${If} $1 == "arm64"
|
||||||
to function properly.$\r$\n$\r$\n\
|
StrCpy $2 "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
|
||||||
Download and install now?" /SD IDYES IDYES InstallVCRedist IDNO DontInstall
|
StrCpy $3 "$TEMP\vc_redist.arm64.exe"
|
||||||
InstallVCRedist:
|
${Else}
|
||||||
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe"
|
StrCpy $2 "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
||||||
ExecWait "$TEMP\vc_redist.x64.exe /install /norestart"
|
StrCpy $3 "$TEMP\vc_redist.x64.exe"
|
||||||
;IfErrors InstallError ContinueInstall ; vc_redist exit code is unreliable :(
|
${EndIf}
|
||||||
Call checkVCRedist
|
|
||||||
${If} $0 == "1"
|
|
||||||
Goto ContinueInstall
|
|
||||||
${EndIf}
|
|
||||||
|
|
||||||
;InstallError:
|
inetc::get /CAPTION " " /BANNER "Downloading Microsoft Visual C++ Redistributable..." \
|
||||||
MessageBox MB_ICONSTOP "\
|
$2 $3 /END
|
||||||
There was an unexpected error installing$\r$\n\
|
Pop $0 ; Get download status from inetc::get
|
||||||
Microsoft Visual C++ Redistributable.$\r$\n\
|
${If} $0 != "OK"
|
||||||
The installation of ${PRODUCT_NAME} cannot continue."
|
MessageBox MB_ICONSTOP|MB_YESNO "\
|
||||||
DontInstall:
|
Failed to download Microsoft Visual C++ Redistributable.$\r$\n$\r$\n\
|
||||||
|
Error: $0$\r$\n$\r$\n\
|
||||||
|
Would you like to open the download page in your browser?$\r$\n\
|
||||||
|
$2" IDYES openDownloadUrl IDNO skipDownloadUrl
|
||||||
|
openDownloadUrl:
|
||||||
|
ExecShell "open" $2
|
||||||
|
skipDownloadUrl:
|
||||||
Abort
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ExecWait "$3 /install /quiet /norestart"
|
||||||
|
; Note: vc_redist exit code is unreliable, verify via registry check instead
|
||||||
|
|
||||||
|
Call checkVCRedist
|
||||||
|
${If} $0 != "1"
|
||||||
|
MessageBox MB_ICONSTOP|MB_YESNO "\
|
||||||
|
Microsoft Visual C++ Redistributable installation failed.$\r$\n$\r$\n\
|
||||||
|
Would you like to open the download page in your browser?$\r$\n\
|
||||||
|
$2$\r$\n$\r$\n\
|
||||||
|
The installation of ${PRODUCT_NAME} cannot continue." IDYES openInstallUrl IDNO skipInstallUrl
|
||||||
|
openInstallUrl:
|
||||||
|
ExecShell "open" $2
|
||||||
|
skipInstallUrl:
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
${EndIf}
|
${EndIf}
|
||||||
ContinueInstall:
|
|
||||||
Pop $4
|
Pop $4
|
||||||
Pop $3
|
Pop $3
|
||||||
Pop $2
|
Pop $2
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
@ -20,35 +20,35 @@ yarn
|
|||||||
|
|
||||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||||
|
|
||||||
### Setup Yarn
|
### Setup pnpm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.9.1 --activate
|
corepack prepare pnpm@10.27.0 --activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Dependencies
|
### Install Dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### ENV
|
### ENV
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
copy .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start
|
### Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug
|
### Debug
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn debug
|
pnpm debug
|
||||||
```
|
```
|
||||||
|
|
||||||
Then input chrome://inspect in browser
|
Then input chrome://inspect in browser
|
||||||
@ -56,18 +56,18 @@ Then input chrome://inspect in browser
|
|||||||
### Test
|
### Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For windows
|
# For windows
|
||||||
$ yarn build:win
|
$ pnpm build:win
|
||||||
|
|
||||||
# For macOS
|
# For macOS
|
||||||
$ yarn build:mac
|
$ pnpm build:mac
|
||||||
|
|
||||||
# For Linux
|
# For Linux
|
||||||
$ yarn build:linux
|
$ pnpm build:linux
|
||||||
```
|
```
|
||||||
|
|||||||
@ -71,7 +71,7 @@ Tools like i18n Ally cannot parse dynamic content within template strings, resul
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Not recommended - Plugin cannot resolve
|
// Not recommended - Plugin cannot resolve
|
||||||
const message = t(`fruits.${fruit}`)
|
const message = t(`fruits.${fruit}`);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. **No Real-time Rendering in Editor**
|
#### 2. **No Real-time Rendering in Editor**
|
||||||
@ -91,14 +91,14 @@ For example:
|
|||||||
```ts
|
```ts
|
||||||
// src/renderer/src/i18n/label.ts
|
// src/renderer/src/i18n/label.ts
|
||||||
const themeModeKeyMap = {
|
const themeModeKeyMap = {
|
||||||
dark: 'settings.theme.dark',
|
dark: "settings.theme.dark",
|
||||||
light: 'settings.theme.light',
|
light: "settings.theme.light",
|
||||||
system: 'settings.theme.system'
|
system: "settings.theme.system",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export const getThemeModeLabel = (key: string): string => {
|
export const getThemeModeLabel = (key: string): string => {
|
||||||
return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key
|
return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
By avoiding template strings, you gain better developer experience, more reliable translation checks, and a more maintainable codebase.
|
By avoiding template strings, you gain better developer experience, more reliable translation checks, and a more maintainable codebase.
|
||||||
@ -107,7 +107,7 @@ By avoiding template strings, you gain better developer experience, more reliabl
|
|||||||
|
|
||||||
The project includes several scripts to automate i18n-related tasks:
|
The project includes several scripts to automate i18n-related tasks:
|
||||||
|
|
||||||
### `check:i18n` - Validate i18n Structure
|
### `i18n:check` - Validate i18n Structure
|
||||||
|
|
||||||
This script checks:
|
This script checks:
|
||||||
|
|
||||||
@ -116,10 +116,10 @@ This script checks:
|
|||||||
- Whether keys are properly sorted
|
- Whether keys are properly sorted
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn check:i18n
|
pnpm i18n:check
|
||||||
```
|
```
|
||||||
|
|
||||||
### `sync:i18n` - Synchronize JSON Structure and Sort Order
|
### `i18n:sync` - Synchronize JSON Structure and Sort Order
|
||||||
|
|
||||||
This script uses `zh-cn.json` as the source of truth to sync structure across all language files, including:
|
This script uses `zh-cn.json` as the source of truth to sync structure across all language files, including:
|
||||||
|
|
||||||
@ -128,14 +128,14 @@ This script uses `zh-cn.json` as the source of truth to sync structure across al
|
|||||||
3. Sorting keys automatically
|
3. Sorting keys automatically
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn sync:i18n
|
pnpm i18n:sync
|
||||||
```
|
```
|
||||||
|
|
||||||
### `auto:i18n` - Automatically Translate Pending Texts
|
### `i18n:translate` - Automatically Translate Pending Texts
|
||||||
|
|
||||||
This script fills in texts marked as `[to be translated]` using machine translation.
|
This script fills in texts marked as `[to be translated]` using machine translation.
|
||||||
|
|
||||||
Typically, after adding new texts in `zh-cn.json`, run `sync:i18n`, then `auto:i18n` to complete translations.
|
Typically, after adding new texts in `zh-cn.json`, run `i18n:sync`, then `i18n:translate` to complete translations.
|
||||||
|
|
||||||
Before using this script, set the required environment variables:
|
Before using this script, set the required environment variables:
|
||||||
|
|
||||||
@ -148,30 +148,20 @@ MODEL="qwen-plus-latest"
|
|||||||
Alternatively, add these variables directly to your `.env` file.
|
Alternatively, add these variables directly to your `.env` file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn auto:i18n
|
pnpm i18n:translate
|
||||||
```
|
|
||||||
|
|
||||||
### `update:i18n` - Object-level Translation Update
|
|
||||||
|
|
||||||
Updates translations in language files under `src/renderer/src/i18n/translate` at the object level, preserving existing translations and only updating new content.
|
|
||||||
|
|
||||||
**Not recommended** — prefer `auto:i18n` for translation tasks.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn update:i18n
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Workflow
|
### Workflow
|
||||||
|
|
||||||
1. During development, first add the required text in `zh-cn.json`
|
1. During development, first add the required text in `zh-cn.json`
|
||||||
2. Confirm it displays correctly in the Chinese environment
|
2. Confirm it displays correctly in the Chinese environment
|
||||||
3. Run `yarn sync:i18n` to propagate the keys to other language files
|
3. Run `pnpm i18n:sync` to propagate the keys to other language files
|
||||||
4. Run `yarn auto:i18n` to perform machine translation
|
4. Run `pnpm i18n:translate` to perform machine translation
|
||||||
5. Grab a coffee and let the magic happen!
|
5. Grab a coffee and let the magic happen!
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Use Chinese as Source Language**: All development starts in Chinese, then translates to other languages.
|
1. **Use Chinese as Source Language**: All development starts in Chinese, then translates to other languages.
|
||||||
2. **Run Check Script Before Commit**: Use `yarn check:i18n` to catch i18n issues early.
|
2. **Run Check Script Before Commit**: Use `pnpm i18n:check` to catch i18n issues early.
|
||||||
3. **Translate in Small Increments**: Avoid accumulating a large backlog of untranslated content.
|
3. **Translate in Small Increments**: Avoid accumulating a large backlog of untranslated content.
|
||||||
4. **Keep Keys Semantically Clear**: Keys should clearly express their purpose, e.g., `user.profile.avatar.upload.error`
|
4. **Keep Keys Semantically Clear**: Keys should clearly express their purpose, e.g., `user.profile.avatar.upload.error`
|
||||||
|
|||||||
@ -37,8 +37,8 @@ The `x-files/app-upgrade-config/app-upgrade-config.json` file is synchronized by
|
|||||||
|
|
||||||
1. **Guard + metadata preparation** – the `Check if should proceed` and `Prepare metadata` steps compute the target tag, prerelease flag, whether the tag is the newest release, and a `safe_tag` slug used for branch names. When any rule fails, the workflow stops without touching the config.
|
1. **Guard + metadata preparation** – the `Check if should proceed` and `Prepare metadata` steps compute the target tag, prerelease flag, whether the tag is the newest release, and a `safe_tag` slug used for branch names. When any rule fails, the workflow stops without touching the config.
|
||||||
2. **Checkout source branches** – the default branch is checked out into `main/`, while the long-lived `x-files/app-upgrade-config` branch lives in `cs/`. All modifications happen in the latter directory.
|
2. **Checkout source branches** – the default branch is checked out into `main/`, while the long-lived `x-files/app-upgrade-config` branch lives in `cs/`. All modifications happen in the latter directory.
|
||||||
3. **Install toolchain** – Node.js 22, Corepack, and frozen Yarn dependencies are installed inside `main/`.
|
3. **Install toolchain** – Node.js 22, Corepack, and frozen pnpm dependencies are installed inside `main/`.
|
||||||
4. **Run the update script** – `yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag>` updates the JSON in-place.
|
4. **Run the update script** – `pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag>` updates the JSON in-place.
|
||||||
- The script normalizes the tag (e.g., strips `v` prefix), detects the release channel (`latest`, `rc`, `beta`), and loads segment rules from `config/app-upgrade-segments.json`.
|
- The script normalizes the tag (e.g., strips `v` prefix), detects the release channel (`latest`, `rc`, `beta`), and loads segment rules from `config/app-upgrade-segments.json`.
|
||||||
- It validates that prerelease flags and semantic suffixes agree, enforces locked segments, builds mirror feed URLs, and performs release-availability checks (GitHub HEAD request for every channel; GitCode GET for latest channels, falling back to `https://releases.cherry-ai.com` when gitcode is delayed).
|
- It validates that prerelease flags and semantic suffixes agree, enforces locked segments, builds mirror feed URLs, and performs release-availability checks (GitHub HEAD request for every channel; GitCode GET for latest channels, falling back to `https://releases.cherry-ai.com` when gitcode is delayed).
|
||||||
- After updating the relevant channel entry, the script rewrites the config with semver-sort order and a new `lastUpdated` timestamp.
|
- After updating the relevant channel entry, the script rewrites the config with semver-sort order and a new `lastUpdated` timestamp.
|
||||||
@ -223,10 +223,10 @@ interface ChannelConfig {
|
|||||||
Starting from this change, `.github/workflows/update-app-upgrade-config.yml` listens to GitHub release events (published + prerelease). The workflow:
|
Starting from this change, `.github/workflows/update-app-upgrade-config.yml` listens to GitHub release events (published + prerelease). The workflow:
|
||||||
|
|
||||||
1. Checks out the default branch (for scripts) and the `x-files/app-upgrade-config` branch (where the config is hosted).
|
1. Checks out the default branch (for scripts) and the `x-files/app-upgrade-config` branch (where the config is hosted).
|
||||||
2. Runs `yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json` to regenerate the config directly inside the `x-files/app-upgrade-config` working tree.
|
2. Runs `pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json` to regenerate the config directly inside the `x-files/app-upgrade-config` working tree.
|
||||||
3. If the file changed, it opens a PR against `x-files/app-upgrade-config` via `peter-evans/create-pull-request`, with the generated diff limited to `app-upgrade-config.json`.
|
3. If the file changed, it opens a PR against `x-files/app-upgrade-config` via `peter-evans/create-pull-request`, with the generated diff limited to `app-upgrade-config.json`.
|
||||||
|
|
||||||
You can run the same script locally via `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json` (add `--dry-run` to preview) to reproduce or debug whatever the workflow does. Passing `--skip-release-checks` along with `--dry-run` lets you bypass the release-page existence check (useful when the GitHub/GitCode pages aren’t published yet). Running without `--config` continues to update the copy in your current working directory (main branch) for documentation purposes.
|
You can run the same script locally via `pnpm update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json` (add `--dry-run` to preview) to reproduce or debug whatever the workflow does. Passing `--skip-release-checks` along with `--dry-run` lets you bypass the release-page existence check (useful when the GitHub/GitCode pages aren't published yet). Running without `--config` continues to update the copy in your current working directory (main branch) for documentation purposes.
|
||||||
|
|
||||||
## Version Matching Logic
|
## Version Matching Logic
|
||||||
|
|
||||||
|
|||||||
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()`
|
||||||
129
docs/en/references/fuzzy-search.md
Normal file
129
docs/en/references/fuzzy-search.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Fuzzy Search for File List
|
||||||
|
|
||||||
|
This document describes the fuzzy search implementation for file listing in Cherry Studio.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The fuzzy search feature allows users to find files by typing partial or approximate file names/paths. It uses a two-tier file filtering strategy (ripgrep glob pre-filtering with greedy substring fallback) combined with subsequence-based scoring for optimal performance and flexibility.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Ripgrep Glob Pre-filtering**: Primary filtering using glob patterns for fast native-level filtering
|
||||||
|
- **Greedy Substring Matching**: Fallback file filtering strategy when ripgrep glob pre-filtering returns no results
|
||||||
|
- **Subsequence-based Segment Scoring**: During scoring, path segments gain additional weight when query characters appear in order
|
||||||
|
- **Relevance Scoring**: Results are sorted by a relevance score derived from multiple factors
|
||||||
|
|
||||||
|
## Matching Strategies
|
||||||
|
|
||||||
|
### 1. Ripgrep Glob Pre-filtering (Primary)
|
||||||
|
|
||||||
|
The query is converted to a glob pattern for ripgrep to do initial filtering:
|
||||||
|
|
||||||
|
```
|
||||||
|
Query: "updater"
|
||||||
|
Glob: "*u*p*d*a*t*e*r*"
|
||||||
|
```
|
||||||
|
|
||||||
|
This leverages ripgrep's native performance for the initial file filtering.
|
||||||
|
|
||||||
|
### 2. Greedy Substring Matching (Fallback)
|
||||||
|
|
||||||
|
When the glob pre-filter returns no results, the system falls back to greedy substring matching. This allows more flexible matching:
|
||||||
|
|
||||||
|
```
|
||||||
|
Query: "updatercontroller"
|
||||||
|
File: "packages/update/src/node/updateController.ts"
|
||||||
|
|
||||||
|
Matching process:
|
||||||
|
1. Find "update" (longest match from start)
|
||||||
|
2. Remaining "rcontroller" → find "r" then "controller"
|
||||||
|
3. All parts matched → Success
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scoring Algorithm
|
||||||
|
|
||||||
|
Results are ranked by a relevance score based on named constants defined in `FileStorage.ts`:
|
||||||
|
|
||||||
|
| Constant | Value | Description |
|
||||||
|
|----------|-------|-------------|
|
||||||
|
| `SCORE_FILENAME_STARTS` | 100 | Filename starts with query (highest priority) |
|
||||||
|
| `SCORE_FILENAME_CONTAINS` | 80 | Filename contains exact query substring |
|
||||||
|
| `SCORE_SEGMENT_MATCH` | 60 | Per path segment that matches query |
|
||||||
|
| `SCORE_WORD_BOUNDARY` | 20 | Query matches start of a word |
|
||||||
|
| `SCORE_CONSECUTIVE_CHAR` | 15 | Per consecutive character match |
|
||||||
|
| `PATH_LENGTH_PENALTY_FACTOR` | 4 | Logarithmic penalty for longer paths |
|
||||||
|
|
||||||
|
### Scoring Strategy
|
||||||
|
|
||||||
|
The scoring prioritizes:
|
||||||
|
1. **Filename matches** (highest): Files where the query appears in the filename are most relevant
|
||||||
|
2. **Path segment matches**: Multiple matching segments indicate stronger relevance
|
||||||
|
3. **Word boundaries**: Matching at word starts (e.g., "upd" matching "update") is preferred
|
||||||
|
4. **Consecutive matches**: Longer consecutive character sequences score higher
|
||||||
|
5. **Path length**: Shorter paths are preferred (logarithmic penalty prevents long paths from dominating)
|
||||||
|
|
||||||
|
### Example Scoring
|
||||||
|
|
||||||
|
For query `updater`:
|
||||||
|
|
||||||
|
| File | Score Factors |
|
||||||
|
|------|---------------|
|
||||||
|
| `RCUpdater.js` | Short path + filename contains "updater" |
|
||||||
|
| `updateController.ts` | Multiple segment matches |
|
||||||
|
| `UpdaterHelper.plist` | Long path penalty |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### DirectoryListOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DirectoryListOptions {
|
||||||
|
recursive?: boolean // Default: true
|
||||||
|
maxDepth?: number // Default: 10
|
||||||
|
includeHidden?: boolean // Default: false
|
||||||
|
includeFiles?: boolean // Default: true
|
||||||
|
includeDirectories?: boolean // Default: true
|
||||||
|
maxEntries?: number // Default: 20
|
||||||
|
searchPattern?: string // Default: '.'
|
||||||
|
fuzzy?: boolean // Default: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic fuzzy search
|
||||||
|
const files = await window.api.file.listDirectory(dirPath, {
|
||||||
|
searchPattern: 'updater',
|
||||||
|
fuzzy: true,
|
||||||
|
maxEntries: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// Disable fuzzy search (exact glob matching)
|
||||||
|
const files = await window.api.file.listDirectory(dirPath, {
|
||||||
|
searchPattern: 'update',
|
||||||
|
fuzzy: false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
1. **Ripgrep Pre-filtering**: Most queries are handled by ripgrep's native glob matching, which is extremely fast
|
||||||
|
2. **Fallback Only When Needed**: Greedy substring matching (which loads all files) only runs when glob matching returns empty results
|
||||||
|
3. **Result Limiting**: Only top 20 results are returned by default
|
||||||
|
4. **Excluded Directories**: Common large directories are automatically excluded:
|
||||||
|
- `node_modules`
|
||||||
|
- `.git`
|
||||||
|
- `dist`, `build`
|
||||||
|
- `.next`, `.nuxt`
|
||||||
|
- `coverage`, `.cache`
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The implementation is located in `src/main/services/FileStorage.ts`:
|
||||||
|
|
||||||
|
- `queryToGlobPattern()`: Converts query to ripgrep glob pattern
|
||||||
|
- `isFuzzyMatch()`: Subsequence matching algorithm
|
||||||
|
- `isGreedySubstringMatch()`: Greedy substring matching fallback
|
||||||
|
- `getFuzzyMatchScore()`: Calculates relevance score
|
||||||
|
- `listDirectoryWithRipgrep()`: Main search orchestration
|
||||||
@ -34,7 +34,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/zh-cn">文档</a> | <a href="./dev.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
|
<a href="https://github.com/CherryHQ/cherry-studio">English</a> | 中文 | <a href="https://cherry-ai.com">官方网站</a> | <a href="https://docs.cherry-ai.com">文档</a> | <a href="./guides/development.md">开发</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">反馈</a><br>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- 题头徽章组合 -->
|
<!-- 题头徽章组合 -->
|
||||||
@ -281,7 +281,7 @@ https://docs.cherry-ai.com
|
|||||||
|
|
||||||
# 📊 GitHub 统计
|
# 📊 GitHub 统计
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
# ⭐️ Star 记录
|
# ⭐️ Star 记录
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
@ -20,35 +20,35 @@ yarn
|
|||||||
|
|
||||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||||
|
|
||||||
### Setup Yarn
|
### Setup pnpm
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack prepare yarn@4.9.1 --activate
|
corepack prepare pnpm@10.27.0 --activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Dependencies
|
### Install Dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### ENV
|
### ENV
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
copy .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start
|
### Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug
|
### Debug
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn debug
|
pnpm debug
|
||||||
```
|
```
|
||||||
|
|
||||||
Then input chrome://inspect in browser
|
Then input chrome://inspect in browser
|
||||||
@ -56,18 +56,18 @@ Then input chrome://inspect in browser
|
|||||||
### Test
|
### Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For windows
|
# For windows
|
||||||
$ yarn build:win
|
$ pnpm build:win
|
||||||
|
|
||||||
# For macOS
|
# For macOS
|
||||||
$ yarn build:mac
|
$ pnpm build:mac
|
||||||
|
|
||||||
# For Linux
|
# For Linux
|
||||||
$ yarn build:linux
|
$ pnpm build:linux
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
# 如何优雅地做好 i18n
|
# 如何优雅地做好 i18n
|
||||||
|
|
||||||
## 使用i18n ally插件提升开发体验
|
## 使用 i18n ally 插件提升开发体验
|
||||||
|
|
||||||
i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反馈,帮助开发者更早发现文案缺失和错译问题。
|
i18n ally 是一个强大的 VSCode 插件,它能在开发阶段提供实时反馈,帮助开发者更早发现文案缺失和错译问题。
|
||||||
|
|
||||||
项目中已经配置好了插件设置,直接安装即可。
|
项目中已经配置好了插件设置,直接安装即可。
|
||||||
|
|
||||||
### 开发时优势
|
### 开发时优势
|
||||||
|
|
||||||
- **实时预览**:翻译文案会直接显示在编辑器中
|
- **实时预览**:翻译文案会直接显示在编辑器中
|
||||||
- **错误检测**:自动追踪标记出缺失的翻译或未使用的key
|
- **错误检测**:自动追踪标记出缺失的翻译或未使用的 key
|
||||||
- **快速跳转**:可通过key直接跳转到定义处(Ctrl/Cmd + click)
|
- **快速跳转**:可通过 key 直接跳转到定义处(Ctrl/Cmd + click)
|
||||||
- **自动补全**:输入i18n key时提供自动补全建议
|
- **自动补全**:输入 i18n key 时提供自动补全建议
|
||||||
|
|
||||||
### 效果展示
|
### 效果展示
|
||||||
|
|
||||||
@ -23,9 +23,9 @@ i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反
|
|||||||
|
|
||||||
## i18n 约定
|
## i18n 约定
|
||||||
|
|
||||||
### **绝对避免使用flat格式**
|
### **绝对避免使用 flat 格式**
|
||||||
|
|
||||||
绝对避免使用flat格式,如`"add.button.tip": "添加"`。应采用清晰的嵌套结构:
|
绝对避免使用 flat 格式,如`"add.button.tip": "添加"`。应采用清晰的嵌套结构:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// 错误示例 - flat结构
|
// 错误示例 - flat结构
|
||||||
@ -52,14 +52,14 @@ i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反
|
|||||||
#### 为什么要使用嵌套结构
|
#### 为什么要使用嵌套结构
|
||||||
|
|
||||||
1. **自然分组**:通过对象结构天然能将相关上下文的文案分到一个组别中
|
1. **自然分组**:通过对象结构天然能将相关上下文的文案分到一个组别中
|
||||||
2. **插件要求**:i18n ally 插件需要嵌套或flat格式其一的文件才能正常分析
|
2. **插件要求**:i18n ally 插件需要嵌套或 flat 格式其一的文件才能正常分析
|
||||||
|
|
||||||
### **避免在`t()`中使用模板字符串**
|
### **避免在`t()`中使用模板字符串**
|
||||||
|
|
||||||
**强烈建议避免使用模板字符串**进行动态插值。虽然模板字符串在JavaScript开发中非常方便,但在国际化场景下会带来一系列问题。
|
**强烈建议避免使用模板字符串**进行动态插值。虽然模板字符串在 JavaScript 开发中非常方便,但在国际化场景下会带来一系列问题。
|
||||||
|
|
||||||
1. **插件无法跟踪**
|
1. **插件无法跟踪**
|
||||||
i18n ally等工具无法解析模板字符串中的动态内容,导致:
|
i18n ally 等工具无法解析模板字符串中的动态内容,导致:
|
||||||
|
|
||||||
- 无法正确显示实时预览
|
- 无法正确显示实时预览
|
||||||
- 无法检测翻译缺失
|
- 无法检测翻译缺失
|
||||||
@ -67,11 +67,11 @@ i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 不推荐 - 插件无法解析
|
// 不推荐 - 插件无法解析
|
||||||
const message = t(`fruits.${fruit}`)
|
const message = t(`fruits.${fruit}`);
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **编辑器无法实时渲染**
|
2. **编辑器无法实时渲染**
|
||||||
在IDE中,模板字符串会显示为原始代码而非最终翻译结果,降低了开发体验。
|
在 IDE 中,模板字符串会显示为原始代码而非最终翻译结果,降低了开发体验。
|
||||||
|
|
||||||
3. **更难以维护**
|
3. **更难以维护**
|
||||||
由于插件无法跟踪这样的文案,编辑器中也无法渲染,开发者必须人工确认语言文件中是否存在相应的文案。
|
由于插件无法跟踪这样的文案,编辑器中也无法渲染,开发者必须人工确认语言文件中是否存在相应的文案。
|
||||||
@ -85,36 +85,36 @@ i18n ally是一个强大的VSCode插件,它能在开发阶段提供实时反
|
|||||||
```ts
|
```ts
|
||||||
// src/renderer/src/i18n/label.ts
|
// src/renderer/src/i18n/label.ts
|
||||||
const themeModeKeyMap = {
|
const themeModeKeyMap = {
|
||||||
dark: 'settings.theme.dark',
|
dark: "settings.theme.dark",
|
||||||
light: 'settings.theme.light',
|
light: "settings.theme.light",
|
||||||
system: 'settings.theme.system'
|
system: "settings.theme.system",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export const getThemeModeLabel = (key: string): string => {
|
export const getThemeModeLabel = (key: string): string => {
|
||||||
return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key
|
return themeModeKeyMap[key] ? t(themeModeKeyMap[key]) : key;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
通过避免模板字符串,可以获得更好的开发体验、更可靠的翻译检查以及更易维护的代码库。
|
通过避免模板字符串,可以获得更好的开发体验、更可靠的翻译检查以及更易维护的代码库。
|
||||||
|
|
||||||
## 自动化脚本
|
## 自动化脚本
|
||||||
|
|
||||||
项目中有一系列脚本来自动化i18n相关任务:
|
项目中有一系列脚本来自动化 i18n 相关任务:
|
||||||
|
|
||||||
### `check:i18n` - 检查i18n结构
|
### `i18n:check` - 检查 i18n 结构
|
||||||
|
|
||||||
此脚本会检查:
|
此脚本会检查:
|
||||||
|
|
||||||
- 所有语言文件是否为嵌套结构
|
- 所有语言文件是否为嵌套结构
|
||||||
- 是否存在缺失的key
|
- 是否存在缺失的 key
|
||||||
- 是否存在多余的key
|
- 是否存在多余的 key
|
||||||
- 是否已经有序
|
- 是否已经有序
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn check:i18n
|
pnpm i18n:check
|
||||||
```
|
```
|
||||||
|
|
||||||
### `sync:i18n` - 同步json结构与排序
|
### `i18n:sync` - 同步 json 结构与排序
|
||||||
|
|
||||||
此脚本以`zh-cn.json`文件为基准,将结构同步到其他语言文件,包括:
|
此脚本以`zh-cn.json`文件为基准,将结构同步到其他语言文件,包括:
|
||||||
|
|
||||||
@ -123,14 +123,14 @@ yarn check:i18n
|
|||||||
3. 自动排序
|
3. 自动排序
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn sync:i18n
|
pnpm i18n:sync
|
||||||
```
|
```
|
||||||
|
|
||||||
### `auto:i18n` - 自动翻译待翻译文本
|
### `i18n:translate` - 自动翻译待翻译文本
|
||||||
|
|
||||||
次脚本自动将标记为待翻译的文本通过机器翻译填充。
|
次脚本自动将标记为待翻译的文本通过机器翻译填充。
|
||||||
|
|
||||||
通常,在`zh-cn.json`中添加所需文案后,执行`sync:i18n`即可自动完成翻译。
|
通常,在`zh-cn.json`中添加所需文案后,执行`i18n:sync`即可自动完成翻译。
|
||||||
|
|
||||||
使用该脚本前,需要配置环境变量,例如:
|
使用该脚本前,需要配置环境变量,例如:
|
||||||
|
|
||||||
@ -143,29 +143,19 @@ MODEL="qwen-plus-latest"
|
|||||||
你也可以通过直接编辑`.env`文件来添加环境变量。
|
你也可以通过直接编辑`.env`文件来添加环境变量。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn auto:i18n
|
pnpm i18n:translate
|
||||||
```
|
|
||||||
|
|
||||||
### `update:i18n` - 对象级别翻译更新
|
|
||||||
|
|
||||||
对`src/renderer/src/i18n/translate`中的语言文件进行对象级别的翻译更新,保留已有翻译,只更新新增内容。
|
|
||||||
|
|
||||||
**不建议**使用该脚本,更推荐使用`auto:i18n`进行翻译。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn update:i18n
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 工作流
|
### 工作流
|
||||||
|
|
||||||
1. 开发阶段,先在`zh-cn.json`中添加所需文案
|
1. 开发阶段,先在`zh-cn.json`中添加所需文案
|
||||||
2. 确认在中文环境下显示无误后,使用`yarn sync:i18n`将文案同步到其他语言文件
|
2. 确认在中文环境下显示无误后,使用`pnpm i18n:sync`将文案同步到其他语言文件
|
||||||
3. 使用`yarn auto:i18n`进行自动翻译
|
3. 使用`pnpm i18n:translate`进行自动翻译
|
||||||
4. 喝杯咖啡,等翻译完成吧!
|
4. 喝杯咖啡,等翻译完成吧!
|
||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
1. **以中文为源语言**:所有开发首先使用中文,再翻译为其他语言
|
1. **以中文为源语言**:所有开发首先使用中文,再翻译为其他语言
|
||||||
2. **提交前运行检查脚本**:使用`yarn check:i18n`检查i18n是否有问题
|
2. **提交前运行检查脚本**:使用`pnpm i18n:check`检查 i18n 是否有问题
|
||||||
3. **小步提交翻译**:避免积累大量未翻译文本
|
3. **小步提交翻译**:避免积累大量未翻译文本
|
||||||
4. **保持key语义明确**:key应能清晰表达其用途,如`user.profile.avatar.upload.error`
|
4. **保持 key 语义明确**:key 应能清晰表达其用途,如`user.profile.avatar.upload.error`
|
||||||
|
|||||||
@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
1. **检查与元数据准备**:`Check if should proceed` 和 `Prepare metadata` 步骤会计算 tag、prerelease 标志、是否最新版本以及用于分支名的 `safe_tag`。若任意校验失败,工作流立即退出。
|
1. **检查与元数据准备**:`Check if should proceed` 和 `Prepare metadata` 步骤会计算 tag、prerelease 标志、是否最新版本以及用于分支名的 `safe_tag`。若任意校验失败,工作流立即退出。
|
||||||
2. **检出分支**:默认分支被检出到 `main/`,长期维护的 `x-files/app-upgrade-config` 分支则在 `cs/` 中,所有改动都发生在 `cs/`。
|
2. **检出分支**:默认分支被检出到 `main/`,长期维护的 `x-files/app-upgrade-config` 分支则在 `cs/` 中,所有改动都发生在 `cs/`。
|
||||||
3. **安装工具链**:安装 Node.js 22、启用 Corepack,并在 `main/` 目录执行 `yarn install --immutable`。
|
3. **安装工具链**:安装 Node.js 22、启用 Corepack,并在 `main/` 目录执行 `pnpm install --frozen-lockfile`。
|
||||||
4. **运行更新脚本**:执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag>`。
|
4. **运行更新脚本**:执行 `pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json --is-prerelease <flag>`。
|
||||||
- 脚本会标准化 tag(去掉 `v` 前缀等)、识别渠道、加载 `config/app-upgrade-segments.json` 中的分段规则。
|
- 脚本会标准化 tag(去掉 `v` 前缀等)、识别渠道、加载 `config/app-upgrade-segments.json` 中的分段规则。
|
||||||
- 校验 prerelease 标志与语义后缀是否匹配、强制锁定的 segment 是否满足、生成镜像的下载地址,并检查 release 是否已经在 GitHub/GitCode 可用(latest 渠道在 GitCode 不可用时会回退到 `https://releases.cherry-ai.com`)。
|
- 校验 prerelease 标志与语义后缀是否匹配、强制锁定的 segment 是否满足、生成镜像的下载地址,并检查 release 是否已经在 GitHub/GitCode 可用(latest 渠道在 GitCode 不可用时会回退到 `https://releases.cherry-ai.com`)。
|
||||||
- 更新对应的渠道配置后,脚本会按 semver 排序写回 JSON,并刷新 `lastUpdated`。
|
- 更新对应的渠道配置后,脚本会按 semver 排序写回 JSON,并刷新 `lastUpdated`。
|
||||||
@ -223,10 +223,10 @@ interface ChannelConfig {
|
|||||||
`.github/workflows/update-app-upgrade-config.yml` 会在 GitHub Release(包含正常发布与 Pre Release)触发:
|
`.github/workflows/update-app-upgrade-config.yml` 会在 GitHub Release(包含正常发布与 Pre Release)触发:
|
||||||
|
|
||||||
1. 同时 Checkout 仓库默认分支(用于脚本)和 `x-files/app-upgrade-config` 分支(真实托管配置的分支)。
|
1. 同时 Checkout 仓库默认分支(用于脚本)和 `x-files/app-upgrade-config` 分支(真实托管配置的分支)。
|
||||||
2. 在默认分支目录执行 `yarn tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json`,直接重写 `x-files/app-upgrade-config` 分支里的配置文件。
|
2. 在默认分支目录执行 `pnpm tsx scripts/update-app-upgrade-config.ts --tag <tag> --config ../cs/app-upgrade-config.json`,直接重写 `x-files/app-upgrade-config` 分支里的配置文件。
|
||||||
3. 如果 `app-upgrade-config.json` 有变化,则通过 `peter-evans/create-pull-request` 自动创建一个指向 `x-files/app-upgrade-config` 的 PR,Diff 仅包含该文件。
|
3. 如果 `app-upgrade-config.json` 有变化,则通过 `peter-evans/create-pull-request` 自动创建一个指向 `x-files/app-upgrade-config` 的 PR,Diff 仅包含该文件。
|
||||||
|
|
||||||
如需本地调试,可执行 `yarn update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json`(加 `--dry-run` 仅打印结果)来复现 CI 行为。若需要暂时跳过 GitHub/GitCode Release 页面是否就绪的校验,可在 `--dry-run` 的同时附加 `--skip-release-checks`。不加 `--config` 时默认更新当前工作目录(通常是 main 分支)下的副本,方便文档/审查。
|
如需本地调试,可执行 `pnpm update:upgrade-config --tag v2.1.6 --config ../cs/app-upgrade-config.json`(加 `--dry-run` 仅打印结果)来复现 CI 行为。若需要暂时跳过 GitHub/GitCode Release 页面是否就绪的校验,可在 `--dry-run` 的同时附加 `--skip-release-checks`。不加 `--config` 时默认更新当前工作目录(通常是 main 分支)下的副本,方便文档/审查。
|
||||||
|
|
||||||
## 版本匹配逻辑
|
## 版本匹配逻辑
|
||||||
|
|
||||||
|
|||||||
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()` 注册命令
|
||||||
129
docs/zh/references/fuzzy-search.md
Normal file
129
docs/zh/references/fuzzy-search.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 文件列表模糊搜索
|
||||||
|
|
||||||
|
本文档描述了 Cherry Studio 中文件列表的模糊搜索实现。
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
模糊搜索功能允许用户通过输入部分或近似的文件名/路径来查找文件。它使用两层文件过滤策略(ripgrep glob 预过滤 + 贪婪子串匹配回退),结合基于子序列的评分,以获得最佳性能和灵活性。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **Ripgrep Glob 预过滤**:使用 glob 模式进行快速原生级过滤的主要过滤策略
|
||||||
|
- **贪婪子串匹配**:当 ripgrep glob 预过滤无结果时的回退文件过滤策略
|
||||||
|
- **基于子序列的段评分**:评分时,当查询字符按顺序出现时,路径段获得额外权重
|
||||||
|
- **相关性评分**:结果按多因素相关性分数排序
|
||||||
|
|
||||||
|
## 匹配策略
|
||||||
|
|
||||||
|
### 1. Ripgrep Glob 预过滤(主要)
|
||||||
|
|
||||||
|
查询被转换为 glob 模式供 ripgrep 进行初始过滤:
|
||||||
|
|
||||||
|
```
|
||||||
|
查询: "updater"
|
||||||
|
Glob: "*u*p*d*a*t*e*r*"
|
||||||
|
```
|
||||||
|
|
||||||
|
这利用了 ripgrep 的原生性能进行初始文件过滤。
|
||||||
|
|
||||||
|
### 2. 贪婪子串匹配(回退)
|
||||||
|
|
||||||
|
当 glob 预过滤无结果时,系统回退到贪婪子串匹配。这允许更灵活的匹配:
|
||||||
|
|
||||||
|
```
|
||||||
|
查询: "updatercontroller"
|
||||||
|
文件: "packages/update/src/node/updateController.ts"
|
||||||
|
|
||||||
|
匹配过程:
|
||||||
|
1. 找到 "update"(从开头的最长匹配)
|
||||||
|
2. 剩余 "rcontroller" → 找到 "r" 然后 "controller"
|
||||||
|
3. 所有部分都匹配 → 成功
|
||||||
|
```
|
||||||
|
|
||||||
|
## 评分算法
|
||||||
|
|
||||||
|
结果根据 `FileStorage.ts` 中定义的命名常量进行相关性分数排名:
|
||||||
|
|
||||||
|
| 常量 | 值 | 描述 |
|
||||||
|
|------|-----|------|
|
||||||
|
| `SCORE_FILENAME_STARTS` | 100 | 文件名以查询开头(最高优先级)|
|
||||||
|
| `SCORE_FILENAME_CONTAINS` | 80 | 文件名包含精确查询子串 |
|
||||||
|
| `SCORE_SEGMENT_MATCH` | 60 | 每个匹配查询的路径段 |
|
||||||
|
| `SCORE_WORD_BOUNDARY` | 20 | 查询匹配单词开头 |
|
||||||
|
| `SCORE_CONSECUTIVE_CHAR` | 15 | 每个连续字符匹配 |
|
||||||
|
| `PATH_LENGTH_PENALTY_FACTOR` | 4 | 较长路径的对数惩罚 |
|
||||||
|
|
||||||
|
### 评分策略
|
||||||
|
|
||||||
|
评分优先级:
|
||||||
|
1. **文件名匹配**(最高):查询出现在文件名中的文件最相关
|
||||||
|
2. **路径段匹配**:多个匹配段表示更强的相关性
|
||||||
|
3. **词边界**:在单词开头匹配(如 "upd" 匹配 "update")更优先
|
||||||
|
4. **连续匹配**:更长的连续字符序列得分更高
|
||||||
|
5. **路径长度**:较短路径更优先(对数惩罚防止长路径主导评分)
|
||||||
|
|
||||||
|
### 评分示例
|
||||||
|
|
||||||
|
对于查询 `updater`:
|
||||||
|
|
||||||
|
| 文件 | 评分因素 |
|
||||||
|
|------|----------|
|
||||||
|
| `RCUpdater.js` | 短路径 + 文件名包含 "updater" |
|
||||||
|
| `updateController.ts` | 多个路径段匹配 |
|
||||||
|
| `UpdaterHelper.plist` | 长路径惩罚 |
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
### DirectoryListOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DirectoryListOptions {
|
||||||
|
recursive?: boolean // 默认: true
|
||||||
|
maxDepth?: number // 默认: 10
|
||||||
|
includeHidden?: boolean // 默认: false
|
||||||
|
includeFiles?: boolean // 默认: true
|
||||||
|
includeDirectories?: boolean // 默认: true
|
||||||
|
maxEntries?: number // 默认: 20
|
||||||
|
searchPattern?: string // 默认: '.'
|
||||||
|
fuzzy?: boolean // 默认: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 基本模糊搜索
|
||||||
|
const files = await window.api.file.listDirectory(dirPath, {
|
||||||
|
searchPattern: 'updater',
|
||||||
|
fuzzy: true,
|
||||||
|
maxEntries: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// 禁用模糊搜索(精确 glob 匹配)
|
||||||
|
const files = await window.api.file.listDirectory(dirPath, {
|
||||||
|
searchPattern: 'update',
|
||||||
|
fuzzy: false
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
1. **Ripgrep 预过滤**:大多数查询由 ripgrep 的原生 glob 匹配处理,速度极快
|
||||||
|
2. **仅在需要时回退**:贪婪子串匹配(加载所有文件)仅在 glob 匹配返回空结果时运行
|
||||||
|
3. **结果限制**:默认只返回前 20 个结果
|
||||||
|
4. **排除目录**:自动排除常见的大型目录:
|
||||||
|
- `node_modules`
|
||||||
|
- `.git`
|
||||||
|
- `dist`、`build`
|
||||||
|
- `.next`、`.nuxt`
|
||||||
|
- `coverage`、`.cache`
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
实现位于 `src/main/services/FileStorage.ts`:
|
||||||
|
|
||||||
|
- `queryToGlobPattern()`:将查询转换为 ripgrep glob 模式
|
||||||
|
- `isFuzzyMatch()`:子序列匹配算法
|
||||||
|
- `isGreedySubstringMatch()`:贪婪子串匹配回退
|
||||||
|
- `getFuzzyMatchScore()`:计算相关性分数
|
||||||
|
- `listDirectoryWithRipgrep()`:主搜索协调
|
||||||
850
docs/zh/references/lan-transfer-protocol.md
Normal file
850
docs/zh/references/lan-transfer-protocol.md
Normal file
@ -0,0 +1,850 @@
|
|||||||
|
# Cherry Studio 局域网传输协议规范
|
||||||
|
|
||||||
|
> 版本: 1.0
|
||||||
|
> 最后更新: 2025-12
|
||||||
|
|
||||||
|
本文档定义了 Cherry Studio 桌面客户端(Electron)与移动端(Expo)之间的局域网文件传输协议。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [协议概述](#1-协议概述)
|
||||||
|
2. [服务发现(Bonjour/mDNS)](#2-服务发现bonjourmdns)
|
||||||
|
3. [TCP 连接与握手](#3-tcp-连接与握手)
|
||||||
|
4. [消息格式规范](#4-消息格式规范)
|
||||||
|
5. [文件传输协议](#5-文件传输协议)
|
||||||
|
6. [心跳与连接保活](#6-心跳与连接保活)
|
||||||
|
7. [错误处理](#7-错误处理)
|
||||||
|
8. [常量与配置](#8-常量与配置)
|
||||||
|
9. [完整时序图](#9-完整时序图)
|
||||||
|
10. [移动端实现指南](#10-移动端实现指南)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 协议概述
|
||||||
|
|
||||||
|
### 1.1 架构角色
|
||||||
|
|
||||||
|
| 角色 | 平台 | 职责 |
|
||||||
|
| -------------------- | --------------- | ---------------------------- |
|
||||||
|
| **Client(客户端)** | Electron 桌面端 | 扫描服务、发起连接、发送文件 |
|
||||||
|
| **Server(服务端)** | Expo 移动端 | 发布服务、接受连接、接收文件 |
|
||||||
|
|
||||||
|
### 1.2 协议栈(v1)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 应用层(文件传输) │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 消息层(控制: JSON \n) │
|
||||||
|
│ (数据: 二进制帧) │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 传输层(TCP) │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 发现层(Bonjour/mDNS) │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 通信流程概览
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 服务发现 → 移动端发布 mDNS 服务,桌面端扫描发现
|
||||||
|
2. TCP 握手 → 建立连接,交换设备信息(`version=1`)
|
||||||
|
3. 文件传输 → 控制消息使用 JSON,`file_chunk` 使用二进制帧分块传输
|
||||||
|
4. 连接保活 → ping/pong 心跳
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 服务发现(Bonjour/mDNS)
|
||||||
|
|
||||||
|
### 2.1 服务类型
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
| ------------ | -------------------- |
|
||||||
|
| 服务类型 | `cherrystudio` |
|
||||||
|
| 协议 | `tcp` |
|
||||||
|
| 完整服务标识 | `_cherrystudio._tcp` |
|
||||||
|
|
||||||
|
### 2.2 服务发布(移动端)
|
||||||
|
|
||||||
|
移动端需要通过 mDNS/Bonjour 发布服务:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 服务发布参数
|
||||||
|
{
|
||||||
|
name: "Cherry Studio Mobile", // 设备名称
|
||||||
|
type: "cherrystudio", // 服务类型
|
||||||
|
protocol: "tcp", // 协议
|
||||||
|
port: 53317, // TCP 监听端口
|
||||||
|
txt: { // TXT 记录(可选)
|
||||||
|
version: "1",
|
||||||
|
platform: "ios" // 或 "android"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 服务发现(桌面端)
|
||||||
|
|
||||||
|
桌面端扫描并解析服务信息:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 发现的服务信息结构
|
||||||
|
type LocalTransferPeer = {
|
||||||
|
id: string; // 唯一标识符
|
||||||
|
name: string; // 设备名称
|
||||||
|
host?: string; // 主机名
|
||||||
|
fqdn?: string; // 完全限定域名
|
||||||
|
port?: number; // TCP 端口
|
||||||
|
type?: string; // 服务类型
|
||||||
|
protocol?: "tcp" | "udp"; // 协议
|
||||||
|
addresses: string[]; // IP 地址列表
|
||||||
|
txt?: Record<string, string>; // TXT 记录
|
||||||
|
updatedAt: number; // 发现时间戳
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 IP 地址选择策略
|
||||||
|
|
||||||
|
当服务有多个 IP 地址时,优先选择 IPv4:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 优先选择 IPv4 地址
|
||||||
|
const preferredAddress = addresses.find((addr) => isIPv4(addr)) || addresses[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. TCP 连接与握手
|
||||||
|
|
||||||
|
### 3.1 连接建立
|
||||||
|
|
||||||
|
1. 客户端使用发现的 `host:port` 建立 TCP 连接
|
||||||
|
2. 连接成功后立即发送握手消息
|
||||||
|
3. 等待服务端响应握手确认
|
||||||
|
|
||||||
|
### 3.2 握手消息(协议版本 v1)
|
||||||
|
|
||||||
|
#### Client → Server: `handshake`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferHandshakeMessage = {
|
||||||
|
type: "handshake";
|
||||||
|
deviceName: string; // 设备名称
|
||||||
|
version: string; // 协议版本,当前为 "1"
|
||||||
|
platform?: string; // 平台:'darwin' | 'win32' | 'linux'
|
||||||
|
appVersion?: string; // 应用版本
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "handshake",
|
||||||
|
"deviceName": "Cherry Studio 1.7.2",
|
||||||
|
"version": "1",
|
||||||
|
"platform": "darwin",
|
||||||
|
"appVersion": "1.7.2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 消息格式规范(混合协议)
|
||||||
|
|
||||||
|
v1 使用"控制 JSON + 二进制数据帧"的混合协议(流式传输模式,无 per-chunk ACK):
|
||||||
|
|
||||||
|
- **控制消息**(握手、心跳、file_start/ack、file_end、file_complete):UTF-8 JSON,`\n` 分隔
|
||||||
|
- **数据消息**(`file_chunk`):二进制帧,使用 Magic + 总长度做分帧,不经 Base64
|
||||||
|
|
||||||
|
### 4.1 控制消息编码(JSON + `\n`)
|
||||||
|
|
||||||
|
| 属性 | 规范 |
|
||||||
|
| ---------- | ------------ |
|
||||||
|
| 编码格式 | UTF-8 |
|
||||||
|
| 序列化格式 | JSON |
|
||||||
|
| 消息分隔符 | `\n`(0x0A) |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function sendControlMessage(socket: Socket, message: object): void {
|
||||||
|
socket.write(`${JSON.stringify(message)}\n`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 `file_chunk` 二进制帧格式
|
||||||
|
|
||||||
|
为解决 TCP 分包/粘包并消除 Base64 开销,`file_chunk` 采用带总长度的二进制帧:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┬────────┬───────────────┬──────────────┬────────────┬───────────┐
|
||||||
|
│ Magic │ TotalLen │ Type │ TransferId Len│ TransferId │ ChunkIdx │ Data │
|
||||||
|
│ 0x43 0x53│ (4B BE) │ 0x01 │ (2B BE) │ (UTF-8) │ (4B BE) │ (raw) │
|
||||||
|
└──────────┴──────────┴────────┴───────────────┴──────────────┴────────────┴───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 大小 | 说明 |
|
||||||
|
| -------------- | ---- | ------------------------------------------- |
|
||||||
|
| Magic | 2B | 常量 `0x43 0x53` ("CS"), 用于区分 JSON 消息 |
|
||||||
|
| TotalLen | 4B | Big-endian,帧总长度(不含 Magic/TotalLen) |
|
||||||
|
| Type | 1B | `0x01` 代表 `file_chunk` |
|
||||||
|
| TransferId Len | 2B | Big-endian,transferId 字符串长度 |
|
||||||
|
| TransferId | nB | UTF-8 transferId(长度由上一字段给出) |
|
||||||
|
| ChunkIdx | 4B | Big-endian,块索引,从 0 开始 |
|
||||||
|
| Data | mB | 原始文件二进制数据(未编码) |
|
||||||
|
|
||||||
|
> 计算帧总长度:`TotalLen = 1 + 2 + transferIdLen + 4 + dataLen`(即 Type~Data 的长度和)。
|
||||||
|
|
||||||
|
### 4.3 消息解析策略
|
||||||
|
|
||||||
|
1. 读取 socket 数据到缓冲区;
|
||||||
|
2. 若前两字节为 `0x43 0x53` → 按二进制帧解析:
|
||||||
|
- 至少需要 6 字节头(Magic + TotalLen),不足则等待更多数据
|
||||||
|
- 读取 `TotalLen` 判断帧整体长度,缓冲区不足则继续等待
|
||||||
|
- 解析 Type/TransferId/ChunkIdx/Data,并传入文件接收逻辑
|
||||||
|
3. 否则若首字节为 `{` → 按 JSON + `\n` 解析控制消息
|
||||||
|
4. 其它数据丢弃 1 字节并继续循环,避免阻塞。
|
||||||
|
|
||||||
|
### 4.4 消息类型汇总(v1)
|
||||||
|
|
||||||
|
| 类型 | 方向 | 编码 | 用途 |
|
||||||
|
| ---------------- | --------------- | -------- | ----------------------- |
|
||||||
|
| `handshake` | Client → Server | JSON+\n | 握手请求(version=1) |
|
||||||
|
| `handshake_ack` | Server → Client | JSON+\n | 握手响应 |
|
||||||
|
| `ping` | Client → Server | JSON+\n | 心跳请求 |
|
||||||
|
| `pong` | Server → Client | JSON+\n | 心跳响应 |
|
||||||
|
| `file_start` | Client → Server | JSON+\n | 开始文件传输 |
|
||||||
|
| `file_start_ack` | Server → Client | JSON+\n | 文件传输确认 |
|
||||||
|
| `file_chunk` | Client → Server | 二进制帧 | 文件数据块(无 Base64,流式无 per-chunk ACK) |
|
||||||
|
| `file_end` | Client → Server | JSON+\n | 文件传输结束 |
|
||||||
|
| `file_complete` | Server → Client | JSON+\n | 传输完成结果 |
|
||||||
|
|
||||||
|
```
|
||||||
|
{"type":"message_type",...其他字段...}\n
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 文件传输协议
|
||||||
|
|
||||||
|
### 5.1 传输流程
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (Sender) Server (Receiver)
|
||||||
|
| |
|
||||||
|
|──── 1. file_start ────────────────>|
|
||||||
|
| (文件元数据) |
|
||||||
|
| |
|
||||||
|
|<─── 2. file_start_ack ─────────────|
|
||||||
|
| (接受/拒绝) |
|
||||||
|
| |
|
||||||
|
|══════ 循环发送数据块(流式,无 ACK) ═════|
|
||||||
|
| |
|
||||||
|
|──── 3. file_chunk [0] ────────────>|
|
||||||
|
| |
|
||||||
|
|──── 3. file_chunk [1] ────────────>|
|
||||||
|
| |
|
||||||
|
| ... 重复直到所有块发送完成 ... |
|
||||||
|
| |
|
||||||
|
|══════════════════════════════════════
|
||||||
|
| |
|
||||||
|
|──── 5. file_end ──────────────────>|
|
||||||
|
| (所有块已发送) |
|
||||||
|
| |
|
||||||
|
|<─── 6. file_complete ──────────────|
|
||||||
|
| (最终结果) |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 消息定义
|
||||||
|
|
||||||
|
#### 5.2.1 `file_start` - 开始传输
|
||||||
|
|
||||||
|
**方向:** Client → Server
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferFileStartMessage = {
|
||||||
|
type: "file_start";
|
||||||
|
transferId: string; // UUID,唯一传输标识
|
||||||
|
fileName: string; // 文件名(含扩展名)
|
||||||
|
fileSize: number; // 文件总字节数
|
||||||
|
mimeType: string; // MIME 类型
|
||||||
|
checksum: string; // 整个文件的 SHA-256 哈希(hex)
|
||||||
|
totalChunks: number; // 总数据块数
|
||||||
|
chunkSize: number; // 每块大小(字节)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_start",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"fileName": "backup.zip",
|
||||||
|
"fileSize": 524288000,
|
||||||
|
"mimeType": "application/zip",
|
||||||
|
"checksum": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||||
|
"totalChunks": 8192,
|
||||||
|
"chunkSize": 65536
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.2 `file_start_ack` - 传输确认
|
||||||
|
|
||||||
|
**方向:** Server → Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferFileStartAckMessage = {
|
||||||
|
type: "file_start_ack";
|
||||||
|
transferId: string; // 对应的传输 ID
|
||||||
|
accepted: boolean; // 是否接受传输
|
||||||
|
message?: string; // 拒绝原因
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**接受示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_start_ack",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"accepted": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**拒绝示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_start_ack",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"accepted": false,
|
||||||
|
"message": "Insufficient storage space"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.3 `file_chunk` - 数据块
|
||||||
|
|
||||||
|
**方向:** Client → Server(**二进制帧**,见 4.2)
|
||||||
|
|
||||||
|
- 不再使用 JSON/`\n`,也不再使用 Base64
|
||||||
|
- 帧结构:`Magic` + `TotalLen` + `Type` + `TransferId` + `ChunkIdx` + `Data`
|
||||||
|
- `Type` 固定 `0x01`,`Data` 为原始文件二进制数据
|
||||||
|
- 传输完整性依赖 `file_start.checksum`(全文件 SHA-256);分块校验和可选,不在帧中发送
|
||||||
|
|
||||||
|
#### 5.2.4 `file_chunk_ack` - 数据块确认(v1 流式不使用)
|
||||||
|
|
||||||
|
v1 采用流式传输,不发送 per-chunk ACK。本节类型仅保留作为向后兼容参考,实际不会发送。
|
||||||
|
|
||||||
|
#### 5.2.5 `file_end` - 传输结束
|
||||||
|
|
||||||
|
**方向:** Client → Server
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferFileEndMessage = {
|
||||||
|
type: "file_end";
|
||||||
|
transferId: string; // 传输 ID
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_end",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.6 `file_complete` - 传输完成
|
||||||
|
|
||||||
|
**方向:** Server → Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferFileCompleteMessage = {
|
||||||
|
type: "file_complete";
|
||||||
|
transferId: string; // 传输 ID
|
||||||
|
success: boolean; // 是否成功
|
||||||
|
filePath?: string; // 保存路径(成功时)
|
||||||
|
error?: string; // 错误信息(失败时)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_complete",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"success": true,
|
||||||
|
"filePath": "/storage/emulated/0/Documents/backup.zip"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "file_complete",
|
||||||
|
"transferId": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"success": false,
|
||||||
|
"error": "File checksum verification failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 校验和算法
|
||||||
|
|
||||||
|
#### 整个文件校验和(保持不变)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function calculateFileChecksum(filePath: string): Promise<string> {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
hash.update(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.digest("hex");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据块校验和
|
||||||
|
|
||||||
|
v1 默认 **不传输分块校验和**,依赖最终文件 checksum。若需要,可在应用层自定义(非协议字段)。
|
||||||
|
|
||||||
|
### 5.4 校验流程
|
||||||
|
|
||||||
|
**发送端(Client):**
|
||||||
|
|
||||||
|
1. 发送前计算整个文件的 SHA-256 → `file_start.checksum`
|
||||||
|
2. 分块直接发送原始二进制(无 Base64)
|
||||||
|
|
||||||
|
**接收端(Server):**
|
||||||
|
|
||||||
|
1. 收到 `file_chunk` 后直接使用二进制数据
|
||||||
|
2. 边收边落盘并增量计算 SHA-256(推荐)
|
||||||
|
3. 所有块接收完成后,计算/完成增量哈希,得到最终 SHA-256
|
||||||
|
4. 与 `file_start.checksum` 比对,结果写入 `file_complete`
|
||||||
|
|
||||||
|
### 5.5 数据块大小计算
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const CHUNK_SIZE = 512 * 1024; // 512KB
|
||||||
|
|
||||||
|
const totalChunks = Math.ceil(fileSize / CHUNK_SIZE);
|
||||||
|
|
||||||
|
// 最后一个块可能小于 CHUNK_SIZE
|
||||||
|
const lastChunkSize = fileSize % CHUNK_SIZE || CHUNK_SIZE;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 心跳与连接保活
|
||||||
|
|
||||||
|
### 6.1 心跳消息
|
||||||
|
|
||||||
|
#### `ping`
|
||||||
|
|
||||||
|
**方向:** Client → Server
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferPingMessage = {
|
||||||
|
type: "ping";
|
||||||
|
payload?: string; // 可选载荷
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping",
|
||||||
|
"payload": "heartbeat"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `pong`
|
||||||
|
|
||||||
|
**方向:** Server → Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LanTransferPongMessage = {
|
||||||
|
type: "pong";
|
||||||
|
received: boolean; // 确认收到
|
||||||
|
payload?: string; // 回传 ping 的载荷
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "pong",
|
||||||
|
"received": true,
|
||||||
|
"payload": "heartbeat"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 心跳策略
|
||||||
|
|
||||||
|
- 握手成功后立即发送一次 `ping` 验证连接
|
||||||
|
- 可选:定期发送心跳保持连接活跃
|
||||||
|
- `pong` 应返回 `ping` 中的 `payload`(可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 错误处理
|
||||||
|
|
||||||
|
### 7.1 超时配置
|
||||||
|
|
||||||
|
| 操作 | 超时时间 | 说明 |
|
||||||
|
| ---------- | -------- | --------------------- |
|
||||||
|
| TCP 连接 | 10 秒 | 连接建立超时 |
|
||||||
|
| 握手等待 | 10 秒 | 等待 `handshake_ack` |
|
||||||
|
| 传输完成 | 60 秒 | 等待 `file_complete` |
|
||||||
|
|
||||||
|
### 7.2 错误场景处理
|
||||||
|
|
||||||
|
| 场景 | Client 处理 | Server 处理 |
|
||||||
|
| --------------- | ------------------ | ---------------------- |
|
||||||
|
| TCP 连接失败 | 通知 UI,允许重试 | - |
|
||||||
|
| 握手超时 | 断开连接,通知 UI | 关闭 socket |
|
||||||
|
| 握手被拒绝 | 显示拒绝原因 | - |
|
||||||
|
| 数据块处理失败 | 中止传输,清理状态 | 清理临时文件 |
|
||||||
|
| 连接意外断开 | 清理状态,通知 UI | 清理临时文件 |
|
||||||
|
| 存储空间不足 | - | 发送 `accepted: false` |
|
||||||
|
|
||||||
|
### 7.3 资源清理
|
||||||
|
|
||||||
|
**Client 端:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function cleanup(): void {
|
||||||
|
// 1. 销毁文件读取流
|
||||||
|
if (readStream) {
|
||||||
|
readStream.destroy();
|
||||||
|
}
|
||||||
|
// 2. 清理传输状态
|
||||||
|
activeTransfer = undefined;
|
||||||
|
// 3. 关闭 socket(如需要)
|
||||||
|
socket?.destroy();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server 端:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function cleanup(): void {
|
||||||
|
// 1. 关闭文件写入流
|
||||||
|
if (writeStream) {
|
||||||
|
writeStream.end();
|
||||||
|
}
|
||||||
|
// 2. 删除未完成的临时文件
|
||||||
|
if (tempFilePath) {
|
||||||
|
fs.unlinkSync(tempFilePath);
|
||||||
|
}
|
||||||
|
// 3. 清理传输状态
|
||||||
|
activeTransfer = undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 常量与配置
|
||||||
|
|
||||||
|
### 8.1 协议常量
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 协议版本(v1 = 控制 JSON + 二进制 chunk + 流式传输)
|
||||||
|
export const LAN_TRANSFER_PROTOCOL_VERSION = "1";
|
||||||
|
|
||||||
|
// 服务发现
|
||||||
|
export const LAN_TRANSFER_SERVICE_TYPE = "cherrystudio";
|
||||||
|
export const LAN_TRANSFER_SERVICE_FULL_NAME = "_cherrystudio._tcp";
|
||||||
|
|
||||||
|
// TCP 端口
|
||||||
|
export const LAN_TRANSFER_TCP_PORT = 53317;
|
||||||
|
|
||||||
|
// 文件传输(与二进制帧一致)
|
||||||
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024; // 512KB
|
||||||
|
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
|
||||||
|
|
||||||
|
// 超时设置
|
||||||
|
export const LAN_TRANSFER_HANDSHAKE_TIMEOUT_MS = 10_000; // 10秒
|
||||||
|
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000; // 30秒
|
||||||
|
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000; // 60秒
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 支持的文件类型
|
||||||
|
|
||||||
|
当前仅支持 ZIP 文件:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const LAN_TRANSFER_ALLOWED_EXTENSIONS = [".zip"];
|
||||||
|
export const LAN_TRANSFER_ALLOWED_MIME_TYPES = [
|
||||||
|
"application/zip",
|
||||||
|
"application/x-zip-compressed",
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 完整时序图
|
||||||
|
|
||||||
|
### 9.1 完整传输流程(v1,流式传输)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||||
|
│ Renderer│ │ Main │ │ Mobile │
|
||||||
|
│ (UI) │ │ Process │ │ Server │
|
||||||
|
└────┬────┘ └────┬────┘ └────┬────┘
|
||||||
|
│ │ │
|
||||||
|
│ ════════════ 服务发现阶段 ════════════ │
|
||||||
|
│ │ │
|
||||||
|
│ startScan() │ │
|
||||||
|
│────────────────────────────────────>│ │
|
||||||
|
│ │ mDNS browse │
|
||||||
|
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│
|
||||||
|
│ │ │
|
||||||
|
│ │<─ ─ ─ service discovered ─ ─ ─ ─ ─ ─│
|
||||||
|
│ │ │
|
||||||
|
│<────── onServicesUpdated ───────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ ════════════ 握手连接阶段 ════════════ │
|
||||||
|
│ │ │
|
||||||
|
│ connect(peer) │ │
|
||||||
|
│────────────────────────────────────>│ │
|
||||||
|
│ │──────── TCP Connect ───────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │──────── handshake ─────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │<─────── handshake_ack ──────────────│
|
||||||
|
│ │ │
|
||||||
|
│ │──────── ping ──────────────────────>│
|
||||||
|
│ │<─────── pong ───────────────────────│
|
||||||
|
│ │ │
|
||||||
|
│<────── connect result ──────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ ════════════ 文件传输阶段 ════════════ │
|
||||||
|
│ │ │
|
||||||
|
│ sendFile(path) │ │
|
||||||
|
│────────────────────────────────────>│ │
|
||||||
|
│ │──────── file_start ────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │<─────── file_start_ack ─────────────│
|
||||||
|
│ │ │
|
||||||
|
│ │ │
|
||||||
|
│ │══════ 循环发送数据块 ═══════════════│
|
||||||
|
│ │ │
|
||||||
|
│ │──────── file_chunk[0] (binary) ────>│
|
||||||
|
│<────── progress event ──────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ │──────── file_chunk[1] (binary) ────>│
|
||||||
|
│<────── progress event ──────────────│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ ... 重复 ... │
|
||||||
|
│ │ │
|
||||||
|
│ │══════════════════════════════════════│
|
||||||
|
│ │ │
|
||||||
|
│ │──────── file_end ──────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │<─────── file_complete ──────────────│
|
||||||
|
│ │ │
|
||||||
|
│<────── complete event ──────────────│ │
|
||||||
|
│<────── sendFile result ─────────────│ │
|
||||||
|
│ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 移动端实现指南(v1 要点)
|
||||||
|
|
||||||
|
### 10.1 必须实现的功能
|
||||||
|
|
||||||
|
1. **mDNS 服务发布**
|
||||||
|
|
||||||
|
- 发布 `_cherrystudio._tcp` 服务
|
||||||
|
- 提供 TCP 端口号 `53317`
|
||||||
|
- 可选:TXT 记录(版本、平台信息)
|
||||||
|
|
||||||
|
2. **TCP 服务端**
|
||||||
|
|
||||||
|
- 监听指定端口
|
||||||
|
- 支持单连接或多连接
|
||||||
|
|
||||||
|
3. **消息解析**
|
||||||
|
|
||||||
|
- 控制消息:UTF-8 + `\n` JSON
|
||||||
|
- 数据消息:二进制帧(Magic+TotalLen 分帧)
|
||||||
|
|
||||||
|
4. **握手处理**
|
||||||
|
|
||||||
|
- 验证 `handshake` 消息
|
||||||
|
- 发送 `handshake_ack` 响应
|
||||||
|
- 响应 `ping` 消息
|
||||||
|
|
||||||
|
5. **文件接收(流式模式)**
|
||||||
|
- 解析 `file_start`,准备接收
|
||||||
|
- 接收 `file_chunk` 二进制帧,直接写入文件/缓冲并增量哈希
|
||||||
|
- v1 不发送 per-chunk ACK(流式传输)
|
||||||
|
- 处理 `file_end`,完成增量哈希并校验 checksum
|
||||||
|
- 发送 `file_complete` 结果
|
||||||
|
|
||||||
|
### 10.2 推荐的库
|
||||||
|
|
||||||
|
**React Native / Expo:**
|
||||||
|
|
||||||
|
- mDNS: `react-native-zeroconf` 或 `@homielab/react-native-bonjour`
|
||||||
|
- TCP: `react-native-tcp-socket`
|
||||||
|
- Crypto: `expo-crypto` 或 `react-native-quick-crypto`
|
||||||
|
|
||||||
|
### 10.3 接收端伪代码
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FileReceiver {
|
||||||
|
private transfer?: {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: number;
|
||||||
|
checksum: string;
|
||||||
|
totalChunks: number;
|
||||||
|
receivedChunks: number;
|
||||||
|
tempPath: string;
|
||||||
|
// v1: 边收边写文件,避免大文件 OOM
|
||||||
|
// stream: FileSystem writable stream (平台相关封装)
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMessage(message: any) {
|
||||||
|
switch (message.type) {
|
||||||
|
case "handshake":
|
||||||
|
this.handleHandshake(message);
|
||||||
|
break;
|
||||||
|
case "ping":
|
||||||
|
this.sendPong(message);
|
||||||
|
break;
|
||||||
|
case "file_start":
|
||||||
|
this.handleFileStart(message);
|
||||||
|
break;
|
||||||
|
// v1: file_chunk 为二进制帧,不再走 JSON 分支
|
||||||
|
case "file_end":
|
||||||
|
this.handleFileEnd(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileStart(msg: LanTransferFileStartMessage) {
|
||||||
|
// 1. 检查存储空间
|
||||||
|
// 2. 创建临时文件
|
||||||
|
// 3. 初始化传输状态
|
||||||
|
// 4. 发送 file_start_ack
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1: 二进制帧处理在 socket data 流中解析,随后调用 handleBinaryFileChunk
|
||||||
|
handleBinaryFileChunk(transferId: string, chunkIndex: number, data: Buffer) {
|
||||||
|
// 直接使用二进制数据,按 chunkSize/lastChunk 计算长度
|
||||||
|
// 写入文件流并更新增量 SHA-256
|
||||||
|
this.transfer.receivedChunks++;
|
||||||
|
// v1: 流式传输,不发送 per-chunk ACK
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileEnd(msg: LanTransferFileEndMessage) {
|
||||||
|
// 1. 合并所有数据块
|
||||||
|
// 2. 验证完整文件 checksum
|
||||||
|
// 3. 写入最终位置
|
||||||
|
// 4. 发送 file_complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 A:TypeScript 类型定义
|
||||||
|
|
||||||
|
完整的类型定义位于 `packages/shared/config/types.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 握手消息
|
||||||
|
export interface LanTransferHandshakeMessage {
|
||||||
|
type: "handshake";
|
||||||
|
deviceName: string;
|
||||||
|
version: string;
|
||||||
|
platform?: string;
|
||||||
|
appVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanTransferHandshakeAckMessage {
|
||||||
|
type: "handshake_ack";
|
||||||
|
accepted: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 心跳消息
|
||||||
|
export interface LanTransferPingMessage {
|
||||||
|
type: "ping";
|
||||||
|
payload?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanTransferPongMessage {
|
||||||
|
type: "pong";
|
||||||
|
received: boolean;
|
||||||
|
payload?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件传输消息 (Client -> Server)
|
||||||
|
export interface LanTransferFileStartMessage {
|
||||||
|
type: "file_start";
|
||||||
|
transferId: string;
|
||||||
|
fileName: string;
|
||||||
|
fileSize: number;
|
||||||
|
mimeType: string;
|
||||||
|
checksum: string;
|
||||||
|
totalChunks: number;
|
||||||
|
chunkSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanTransferFileChunkMessage {
|
||||||
|
type: "file_chunk";
|
||||||
|
transferId: string;
|
||||||
|
chunkIndex: number;
|
||||||
|
data: string; // Base64 encoded (v1: 二进制帧模式下不使用)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanTransferFileEndMessage {
|
||||||
|
type: "file_end";
|
||||||
|
transferId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件传输响应消息 (Server -> Client)
|
||||||
|
export interface LanTransferFileStartAckMessage {
|
||||||
|
type: "file_start_ack";
|
||||||
|
transferId: string;
|
||||||
|
accepted: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1 流式不发送 per-chunk ACK,以下类型仅用于向后兼容参考
|
||||||
|
export interface LanTransferFileChunkAckMessage {
|
||||||
|
type: "file_chunk_ack";
|
||||||
|
transferId: string;
|
||||||
|
chunkIndex: number;
|
||||||
|
received: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LanTransferFileCompleteMessage {
|
||||||
|
type: "file_complete";
|
||||||
|
transferId: string;
|
||||||
|
success: boolean;
|
||||||
|
filePath?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 常量
|
||||||
|
export const LAN_TRANSFER_TCP_PORT = 53317;
|
||||||
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024;
|
||||||
|
export const LAN_TRANSFER_CHUNK_TIMEOUT_MS = 30_000;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录 B:版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 变更 |
|
||||||
|
| ---- | ------- | ---------------------------------------- |
|
||||||
|
| 1.0 | 2025-12 | 初始发布版本,支持二进制帧格式与流式传输 |
|
||||||
@ -28,6 +28,12 @@ files:
|
|||||||
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
|
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
|
||||||
- "!**/{.editorconfig,.jekyll-metadata}"
|
- "!**/{.editorconfig,.jekyll-metadata}"
|
||||||
- "!src"
|
- "!src"
|
||||||
|
- "!config"
|
||||||
|
- "!patches"
|
||||||
|
- "!app-upgrade-config.json"
|
||||||
|
- "!**/node_modules/**/*.cpp"
|
||||||
|
- "!**/node_modules/node-addon-api/**"
|
||||||
|
- "!**/node_modules/prebuild-install/**"
|
||||||
- "!scripts"
|
- "!scripts"
|
||||||
- "!local"
|
- "!local"
|
||||||
- "!docs"
|
- "!docs"
|
||||||
@ -90,6 +96,7 @@ nsis:
|
|||||||
oneClick: false
|
oneClick: false
|
||||||
include: build/nsis-installer.nsh
|
include: build/nsis-installer.nsh
|
||||||
buildUniversalInstaller: false
|
buildUniversalInstaller: false
|
||||||
|
differentialPackage: false
|
||||||
portable:
|
portable:
|
||||||
artifactName: ${productName}-${version}-${arch}-portable.${ext}
|
artifactName: ${productName}-${version}-${arch}-portable.${ext}
|
||||||
buildUniversalInstaller: false
|
buildUniversalInstaller: false
|
||||||
@ -105,6 +112,8 @@ mac:
|
|||||||
target:
|
target:
|
||||||
- target: dmg
|
- target: dmg
|
||||||
- target: zip
|
- target: zip
|
||||||
|
dmg:
|
||||||
|
writeUpdateInfo: false
|
||||||
linux:
|
linux:
|
||||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
target:
|
target:
|
||||||
@ -134,56 +143,30 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
|||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
<!--LANG:en-->
|
<!--LANG:en-->
|
||||||
What's New in v1.7.0-rc.3
|
Cherry Studio 1.7.13 - Security & Bug Fixes
|
||||||
|
|
||||||
✨ New Features:
|
🔒 Security
|
||||||
- Provider: Added Silicon provider support for Anthropic API compatibility
|
- [Plugin] Fix security vulnerability in DXT plugin system on Windows
|
||||||
- Provider: AIHubMix support for nano banana
|
|
||||||
|
|
||||||
🐛 Bug Fixes:
|
🐛 Bug Fixes
|
||||||
- i18n: Clean up translation tags and untranslated strings
|
- [Agent] Fix Agent not working when Node.js is not installed on system
|
||||||
- Provider: Fixed Silicon provider code list
|
- [Chat] Fix app crash when opening certain agents
|
||||||
- Provider: Fixed Poe API reasoning parameters for GPT-5 and reasoning models
|
- [Chat] Fix reasoning process not displaying correctly for some providers
|
||||||
- Provider: Fixed duplicate /v1 in Anthropic API endpoints
|
- [Chat] Fix memory leak issue during streaming conversations
|
||||||
- Provider: Fixed Azure provider handling in AI SDK integration
|
- [MCP] Fix timeout field not accepting string format in MCP configuration
|
||||||
- Models: Added Claude Opus 4.5 pattern to THINKING_TOKEN_MAP
|
- [Settings] Add careers section in About page
|
||||||
- Models: Improved Gemini reasoning and message handling
|
|
||||||
- Models: Fixed custom parameters for Gemini models
|
|
||||||
- Models: Fixed qwen-mt-flash text delta support
|
|
||||||
- Models: Fixed Groq verbosity setting
|
|
||||||
- UI: Fixed quota display and quota tips
|
|
||||||
- UI: Fixed web search button condition
|
|
||||||
- Settings: Fixed updateAssistantPreset reducer to properly update preset
|
|
||||||
- Settings: Respect enableMaxTokens setting when maxTokens is not configured
|
|
||||||
- SDK: Fixed header merging logic in AI SDK
|
|
||||||
|
|
||||||
⚡ Improvements:
|
|
||||||
- SDK: Upgraded @anthropic-ai/claude-agent-sdk to 0.1.53
|
|
||||||
|
|
||||||
<!--LANG:zh-CN-->
|
<!--LANG:zh-CN-->
|
||||||
v1.7.0-rc.3 更新内容
|
Cherry Studio 1.7.13 - 安全与问题修复
|
||||||
|
|
||||||
✨ 新功能:
|
🔒 安全修复
|
||||||
- 提供商:新增 Silicon 提供商对 Anthropic API 的兼容性支持
|
- [插件] 修复 Windows 系统 DXT 插件的安全漏洞
|
||||||
- 提供商:AIHubMix 支持 nano banana
|
|
||||||
|
|
||||||
🐛 问题修复:
|
🐛 问题修复
|
||||||
- 国际化:清理翻译标签和未翻译字符串
|
- [Agent] 修复系统未安装 Node.js 时 Agent 功能无法使用的问题
|
||||||
- 提供商:修复 Silicon 提供商代码列表
|
- [对话] 修复打开某些智能体时应用崩溃的问题
|
||||||
- 提供商:修复 Poe API 对 GPT-5 和推理模型的推理参数
|
- [对话] 修复部分服务商推理过程无法正确显示的问题
|
||||||
- 提供商:修复 Anthropic API 端点重复 /v1 问题
|
- [对话] 修复流式对话时的内存泄漏问题
|
||||||
- 提供商:修复 Azure 提供商在 AI SDK 集成中的处理
|
- [MCP] 修复 MCP 配置的 timeout 字段不支持字符串格式的问题
|
||||||
- 模型:Claude Opus 4.5 添加到 THINKING_TOKEN_MAP
|
- [设置] 关于页面新增招聘入口
|
||||||
- 模型:改进 Gemini 推理和消息处理
|
|
||||||
- 模型:修复 Gemini 模型自定义参数
|
|
||||||
- 模型:修复 qwen-mt-flash text delta 支持
|
|
||||||
- 模型:修复 Groq verbosity 设置
|
|
||||||
- 界面:修复配额显示和配额提示
|
|
||||||
- 界面:修复 Web 搜索按钮条件
|
|
||||||
- 设置:修复 updateAssistantPreset reducer 正确更新 preset
|
|
||||||
- 设置:尊重 enableMaxTokens 设置
|
|
||||||
- SDK:修复 AI SDK 中 header 合并逻辑
|
|
||||||
|
|
||||||
⚡ 改进:
|
|
||||||
- SDK:升级 @anthropic-ai/claude-agent-sdk 到 0.1.53
|
|
||||||
<!--LANG:END-->
|
<!--LANG:END-->
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import { CodeInspectorPlugin } from 'code-inspector-plugin'
|
import { CodeInspectorPlugin } from 'code-inspector-plugin'
|
||||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
import { defineConfig } from 'electron-vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const isProd = process.env.NODE_ENV === 'production'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')],
|
plugins: [...visualizerPlugin('main')],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@main': resolve('src/main'),
|
'@main': resolve('src/main'),
|
||||||
@ -51,8 +51,7 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
react({
|
react({
|
||||||
tsDecorators: true
|
tsDecorators: true
|
||||||
}),
|
})
|
||||||
externalizeDepsPlugin()
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@ -68,18 +67,7 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||||
react({
|
react({
|
||||||
tsDecorators: true,
|
tsDecorators: true
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
'@swc/plugin-styled-components',
|
|
||||||
{
|
|
||||||
displayName: true, // 开发环境下启用组件名称
|
|
||||||
fileName: false, // 不在类名中包含文件名
|
|
||||||
pure: true, // 优化性能
|
|
||||||
ssr: false // 不需要服务端渲染
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}),
|
}),
|
||||||
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
|
...(isDev ? [CodeInspectorPlugin({ bundler: 'vite' })] : []), // 只在开发环境下启用 CodeInspectorPlugin
|
||||||
...visualizerPlugin('renderer')
|
...visualizerPlugin('renderer')
|
||||||
|
|||||||
@ -58,8 +58,10 @@ export default defineConfig([
|
|||||||
'dist/**',
|
'dist/**',
|
||||||
'out/**',
|
'out/**',
|
||||||
'local/**',
|
'local/**',
|
||||||
|
'tests/**',
|
||||||
'.yarn/**',
|
'.yarn/**',
|
||||||
'.gitignore',
|
'.gitignore',
|
||||||
|
'.conductor/**',
|
||||||
'scripts/cloudflare-worker.js',
|
'scripts/cloudflare-worker.js',
|
||||||
'src/main/integration/nutstore/sso/lib/**',
|
'src/main/integration/nutstore/sso/lib/**',
|
||||||
'src/main/integration/cherryai/index.js',
|
'src/main/integration/cherryai/index.js',
|
||||||
@ -82,7 +84,7 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
selector: 'CallExpression[callee.object.name="console"]',
|
selector: 'CallExpression[callee.object.name="console"]',
|
||||||
message:
|
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'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
349
package.json
349
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.7.0-rc.3",
|
"version": "1.7.13",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@ -9,27 +9,13 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0"
|
"node": ">=22.0.0"
|
||||||
},
|
},
|
||||||
"workspaces": {
|
|
||||||
"packages": [
|
|
||||||
"local",
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"installConfig": {
|
|
||||||
"hoistingLimits": [
|
|
||||||
"packages/database",
|
|
||||||
"packages/mcp-trace/trace-core",
|
|
||||||
"packages/mcp-trace/trace-node",
|
|
||||||
"packages/mcp-trace/trace-web",
|
|
||||||
"packages/extension-table-plus"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "dotenv electron-vite dev",
|
"dev": "dotenv electron-vite dev",
|
||||||
|
"dev:watch": "dotenv electron-vite dev -- -w",
|
||||||
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
|
||||||
"build": "npm run typecheck && electron-vite build",
|
"build": "npm run typecheck && electron-vite build",
|
||||||
"build:check": "yarn lint && yarn test",
|
"build:check": "pnpm lint && pnpm test",
|
||||||
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
"build:unpack": "dotenv npm run build && electron-builder --dir",
|
||||||
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
"build:win": "dotenv npm run build && electron-builder --win --x64 --arm64",
|
||||||
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
"build:win:x64": "dotenv npm run build && electron-builder --win --x64",
|
||||||
@ -41,68 +27,63 @@
|
|||||||
"build:linux:arm64": "dotenv npm run build && electron-builder --linux --arm64",
|
"build:linux:arm64": "dotenv npm run build && electron-builder --linux --arm64",
|
||||||
"build:linux:x64": "dotenv npm run build && electron-builder --linux --x64",
|
"build:linux:x64": "dotenv npm run build && electron-builder --linux --x64",
|
||||||
"release": "node scripts/version.js",
|
"release": "node scripts/version.js",
|
||||||
"publish": "yarn build:check && yarn release patch push",
|
"publish": "pnpm build:check && pnpm release patch push",
|
||||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||||
"agents:generate": "NODE_ENV='development' drizzle-kit generate --config src/main/services/agents/drizzle.config.ts",
|
"agents:generate": "NODE_ENV='development' drizzle-kit generate --config src/main/services/agents/drizzle.config.ts",
|
||||||
"agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts",
|
"agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts",
|
||||||
"agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts",
|
"agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts",
|
||||||
"agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts",
|
"agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts",
|
||||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
"analyze:renderer": "VISUALIZER_RENDERER=true pnpm build",
|
||||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
"analyze:main": "VISUALIZER_MAIN=true pnpm build",
|
||||||
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
|
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
|
||||||
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
||||||
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
"i18n:check": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
||||||
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
"i18n:sync": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
||||||
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
"i18n:translate": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
||||||
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
"i18n:all": "pnpm i18n:sync && pnpm i18n:translate",
|
||||||
"update:languages": "tsx scripts/update-languages.ts",
|
"update:languages": "tsx scripts/update-languages.ts",
|
||||||
"update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts",
|
"update:upgrade-config": "tsx scripts/update-app-upgrade-config.ts",
|
||||||
"test": "vitest run --silent",
|
"test": "vitest run --silent",
|
||||||
"test:main": "vitest run --project main",
|
"test:main": "vitest run --project main",
|
||||||
"test:renderer": "vitest run --project renderer",
|
"test:renderer": "vitest run --project renderer",
|
||||||
"test:update": "yarn test:renderer --update",
|
"test:aicore": "vitest run --project aiCore",
|
||||||
|
"test:update": "pnpm test:renderer --update",
|
||||||
"test:coverage": "vitest run --coverage --silent",
|
"test:coverage": "vitest run --coverage --silent",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:e2e": "yarn playwright test",
|
"test:e2e": "pnpm playwright test",
|
||||||
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
||||||
"test:scripts": "vitest scripts",
|
"test:scripts": "vitest scripts",
|
||||||
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n && yarn format:check",
|
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && pnpm typecheck && pnpm i18n:check && pnpm format:check",
|
||||||
"format": "biome format --write && biome lint --write",
|
"format": "biome format --write && biome lint --write",
|
||||||
"format:check": "biome format && biome lint",
|
"format:check": "biome format && biome lint",
|
||||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
||||||
"claude": "dotenv -e .env -- claude",
|
"claude": "dotenv -e .env -- claude",
|
||||||
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --preid alpha --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
"release:aicore:alpha": "pnpm --filter @cherrystudio/ai-core version prerelease --preid alpha && pnpm --filter @cherrystudio/ai-core build && pnpm --filter @cherrystudio/ai-core publish --tag alpha --access public",
|
||||||
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --preid beta --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
"release:aicore:beta": "pnpm --filter @cherrystudio/ai-core version prerelease --preid beta && pnpm --filter @cherrystudio/ai-core build && pnpm --filter @cherrystudio/ai-core publish --tag beta --access public",
|
||||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --access public",
|
"release:aicore": "pnpm --filter @cherrystudio/ai-core version patch && pnpm --filter @cherrystudio/ai-core build && pnpm --filter @cherrystudio/ai-core publish --access public",
|
||||||
"release:ai-sdk-provider": "yarn workspace @cherrystudio/ai-sdk-provider version patch --immediate && yarn workspace @cherrystudio/ai-sdk-provider build && yarn workspace @cherrystudio/ai-sdk-provider npm publish --access public"
|
"release:ai-sdk-provider": "pnpm --filter @cherrystudio/ai-sdk-provider version patch && pnpm --filter @cherrystudio/ai-sdk-provider build && pnpm --filter @cherrystudio/ai-sdk-provider publish --access public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.53#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.53-4b77f4cf29.patch",
|
"@anthropic-ai/claude-agent-sdk": "0.1.76",
|
||||||
"@libsql/client": "0.14.0",
|
"@libsql/client": "0.14.0",
|
||||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
"@napi-rs/system-ocr": "1.0.2",
|
||||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
"@paymoapp/electron-shutdown-handler": "1.1.2",
|
||||||
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
"express": "5.1.0",
|
||||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
"font-list": "2.0.0",
|
||||||
"emoji-picker-element-data": "^1",
|
"graceful-fs": "4.2.11",
|
||||||
"express": "^5.1.0",
|
"gray-matter": "4.0.3",
|
||||||
"font-list": "^2.0.0",
|
|
||||||
"graceful-fs": "^4.2.11",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "1.15.0",
|
||||||
"officeparser": "^4.2.0",
|
"officeparser": "4.2.0",
|
||||||
"os-proxy-config": "^1.1.2",
|
"os-proxy-config": "1.1.2",
|
||||||
"qrcode.react": "^4.2.0",
|
"selection-hook": "1.0.12",
|
||||||
"selection-hook": "^1.0.12",
|
"sharp": "0.34.3",
|
||||||
"sharp": "^0.34.3",
|
"swagger-jsdoc": "6.2.8",
|
||||||
"socket.io": "^4.8.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"tesseract.js": "6.0.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
|
||||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
|
||||||
"turndown": "7.2.0"
|
"turndown": "7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -111,37 +92,48 @@
|
|||||||
"@agentic/tavily": "^7.3.3",
|
"@agentic/tavily": "^7.3.3",
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.61",
|
"@ai-sdk/amazon-bedrock": "^3.0.61",
|
||||||
"@ai-sdk/anthropic": "^2.0.49",
|
"@ai-sdk/anthropic": "^2.0.49",
|
||||||
|
"@ai-sdk/azure": "2.0.87",
|
||||||
"@ai-sdk/cerebras": "^1.0.31",
|
"@ai-sdk/cerebras": "^1.0.31",
|
||||||
"@ai-sdk/gateway": "^2.0.15",
|
"@ai-sdk/gateway": "^2.0.15",
|
||||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.43#~/.yarn/patches/@ai-sdk-google-npm-2.0.43-689ed559b3.patch",
|
"@ai-sdk/google": "2.0.49",
|
||||||
"@ai-sdk/google-vertex": "^3.0.79",
|
"@ai-sdk/google-vertex": "^3.0.94",
|
||||||
"@ai-sdk/huggingface": "^0.0.10",
|
"@ai-sdk/huggingface": "^0.0.10",
|
||||||
"@ai-sdk/mistral": "^2.0.24",
|
"@ai-sdk/mistral": "^2.0.24",
|
||||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.72#~/.yarn/patches/@ai-sdk-openai-npm-2.0.72-234e68da87.patch",
|
"@ai-sdk/openai": "2.0.85",
|
||||||
"@ai-sdk/perplexity": "^2.0.20",
|
"@ai-sdk/perplexity": "^2.0.20",
|
||||||
|
"@ai-sdk/provider": "2.0.0",
|
||||||
|
"@ai-sdk/provider-utils": "3.0.17",
|
||||||
"@ai-sdk/test-server": "^0.0.1",
|
"@ai-sdk/test-server": "^0.0.1",
|
||||||
|
"@ai-sdk/xai": "2.0.36",
|
||||||
|
"@ant-design/cssinjs": "1.23.0",
|
||||||
|
"@ant-design/icons": "5.6.1",
|
||||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@anthropic-ai/sdk": "^0.41.0",
|
"@anthropic-ai/sdk": "^0.41.0",
|
||||||
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
"@anthropic-ai/vertex-sdk": "0.11.4",
|
||||||
"@aws-sdk/client-bedrock": "^3.910.0",
|
"@aws-sdk/client-bedrock": "^3.910.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
||||||
"@aws-sdk/client-s3": "^3.910.0",
|
"@aws-sdk/client-s3": "^3.910.0",
|
||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@cherrystudio/ai-core": "workspace:^1.0.9",
|
"@cherrystudio/ai-core": "workspace:^1.0.9",
|
||||||
"@cherrystudio/embedjs": "^0.1.31",
|
"@cherrystudio/embedjs": "0.1.31",
|
||||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
"@cherrystudio/embedjs-interfaces": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
"@cherrystudio/embedjs-libsql": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-image": "^0.1.31",
|
"@cherrystudio/embedjs-loader-csv": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
|
"@cherrystudio/embedjs-loader-image": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-msoffice": "^0.1.31",
|
"@cherrystudio/embedjs-loader-markdown": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-pdf": "^0.1.31",
|
"@cherrystudio/embedjs-loader-msoffice": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
|
"@cherrystudio/embedjs-loader-pdf": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-web": "^0.1.31",
|
"@cherrystudio/embedjs-loader-sitemap": "0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
|
"@cherrystudio/embedjs-loader-web": "0.1.31",
|
||||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
"@cherrystudio/embedjs-loader-xml": "0.1.31",
|
||||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
"@cherrystudio/embedjs-ollama": "0.1.31",
|
||||||
|
"@cherrystudio/embedjs-openai": "0.1.31",
|
||||||
|
"@cherrystudio/embedjs-utils": "0.1.31",
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^",
|
"@cherrystudio/extension-table-plus": "workspace:^",
|
||||||
"@cherrystudio/openai": "^6.9.0",
|
"@cherrystudio/openai": "6.15.0",
|
||||||
|
"@codemirror/lang-json": "6.0.1",
|
||||||
|
"@codemirror/lint": "6.8.5",
|
||||||
|
"@codemirror/view": "6.38.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@ -154,28 +146,32 @@
|
|||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@eslint-react/eslint-plugin": "^1.36.1",
|
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
"@floating-ui/dom": "1.7.3",
|
||||||
|
"@google/genai": "1.0.1",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.3",
|
||||||
"@langchain/community": "^1.0.0",
|
"@langchain/community": "^1.0.0",
|
||||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
"@langchain/core": "1.0.2",
|
||||||
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
"@langchain/openai": "1.0.0",
|
||||||
|
"@langchain/textsplitters": "0.1.0",
|
||||||
"@mistralai/mistralai": "^1.7.5",
|
"@mistralai/mistralai": "^1.7.5",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
"@modelcontextprotocol/sdk": "1.23.0",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@notionhq/client": "^2.2.15",
|
"@notionhq/client": "^2.2.15",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.5",
|
"@openrouter/ai-sdk-provider": "^1.2.8",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/context-async-hooks": "2.0.1",
|
||||||
"@opentelemetry/core": "2.0.0",
|
"@opentelemetry/core": "2.0.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
||||||
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
"@opentelemetry/sdk-trace-web": "^2.0.0",
|
||||||
"@opeoginni/github-copilot-openai-compatible": "^0.1.21",
|
"@opeoginni/github-copilot-openai-compatible": "^0.1.21",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.55.1",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
"@shikijs/markdown-it": "^3.12.0",
|
"@shikijs/markdown-it": "^3.12.0",
|
||||||
|
"@swc/core": "^1.15.8",
|
||||||
"@swc/plugin-styled-components": "^8.0.4",
|
"@swc/plugin-styled-components": "^8.0.4",
|
||||||
"@tailwindcss/vite": "^4.1.13",
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
@ -184,21 +180,25 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@tiptap/extension-collaboration": "^3.2.0",
|
"@tiptap/core": "3.2.0",
|
||||||
"@tiptap/extension-drag-handle": "patch:@tiptap/extension-drag-handle@npm%3A3.2.0#~/.yarn/patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch",
|
"@tiptap/extension-code-block": "3.2.0",
|
||||||
"@tiptap/extension-drag-handle-react": "^3.2.0",
|
"@tiptap/extension-collaboration": "3.2.0",
|
||||||
"@tiptap/extension-image": "^3.2.0",
|
"@tiptap/extension-drag-handle": "3.2.0",
|
||||||
"@tiptap/extension-list": "^3.2.0",
|
"@tiptap/extension-drag-handle-react": "3.2.0",
|
||||||
"@tiptap/extension-mathematics": "^3.2.0",
|
"@tiptap/extension-heading": "3.2.0",
|
||||||
"@tiptap/extension-mention": "^3.2.0",
|
"@tiptap/extension-image": "3.2.0",
|
||||||
"@tiptap/extension-node-range": "^3.2.0",
|
"@tiptap/extension-link": "3.2.0",
|
||||||
"@tiptap/extension-table-of-contents": "^3.2.0",
|
"@tiptap/extension-list": "3.2.0",
|
||||||
"@tiptap/extension-typography": "^3.2.0",
|
"@tiptap/extension-mathematics": "3.2.0",
|
||||||
"@tiptap/extension-underline": "^3.2.0",
|
"@tiptap/extension-mention": "3.2.0",
|
||||||
"@tiptap/pm": "^3.2.0",
|
"@tiptap/extension-node-range": "3.2.0",
|
||||||
"@tiptap/react": "^3.2.0",
|
"@tiptap/extension-table-of-contents": "3.2.0",
|
||||||
"@tiptap/starter-kit": "^3.2.0",
|
"@tiptap/extension-typography": "3.2.0",
|
||||||
"@tiptap/suggestion": "^3.2.0",
|
"@tiptap/extension-underline": "3.2.0",
|
||||||
|
"@tiptap/pm": "3.2.0",
|
||||||
|
"@tiptap/react": "3.2.0",
|
||||||
|
"@tiptap/starter-kit": "3.2.0",
|
||||||
|
"@tiptap/suggestion": "3.2.0",
|
||||||
"@tiptap/y-tiptap": "^3.0.0",
|
"@tiptap/y-tiptap": "^3.0.0",
|
||||||
"@truto/turndown-plugin-gfm": "^1.0.2",
|
"@truto/turndown-plugin-gfm": "^1.0.2",
|
||||||
"@tryfabric/martian": "^1.2.4",
|
"@tryfabric/martian": "^1.2.4",
|
||||||
@ -206,16 +206,20 @@
|
|||||||
"@types/content-type": "^1.1.9",
|
"@types/content-type": "^1.1.9",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/diff": "^7",
|
"@types/diff": "^7",
|
||||||
|
"@types/dotenv": "^8.2.3",
|
||||||
"@types/express": "^5",
|
"@types/express": "^5",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
|
"@types/hast": "^3.0.4",
|
||||||
"@types/he": "^1",
|
"@types/he": "^1",
|
||||||
"@types/html-to-text": "^9",
|
"@types/html-to-text": "^9",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/json-schema": "7.0.15",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
|
"@types/mdast": "4.0.4",
|
||||||
"@types/mime-types": "^3",
|
"@types/mime-types": "^3",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "22.17.2",
|
||||||
"@types/pako": "^1.0.2",
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@ -226,9 +230,10 @@
|
|||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
|
"@types/unist": "3.0.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/word-extractor": "^1",
|
"@types/word-extractor": "^1",
|
||||||
"@typescript/native-preview": "latest",
|
"@typescript/native-preview": "7.0.0-dev.20250915.1",
|
||||||
"@uiw/codemirror-extensions-langs": "^4.25.1",
|
"@uiw/codemirror-extensions-langs": "^4.25.1",
|
||||||
"@uiw/codemirror-themes-all": "^4.25.1",
|
"@uiw/codemirror-themes-all": "^4.25.1",
|
||||||
"@uiw/react-codemirror": "^4.25.1",
|
"@uiw/react-codemirror": "^4.25.1",
|
||||||
@ -240,12 +245,16 @@
|
|||||||
"@viz-js/lang-dot": "^1.0.5",
|
"@viz-js/lang-dot": "^1.0.5",
|
||||||
"@viz-js/viz": "^3.14.0",
|
"@viz-js/viz": "^3.14.0",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
|
"adm-zip": "0.4.16",
|
||||||
"ai": "^5.0.98",
|
"ai": "^5.0.98",
|
||||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
"antd": "5.27.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
|
"bonjour-service": "1.3.0",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
|
"builder-util-runtime": "9.5.0",
|
||||||
|
"chalk": "4.1.2",
|
||||||
"chardet": "^2.1.0",
|
"chardet": "^2.1.0",
|
||||||
"check-disk-space": "3.4.0",
|
"check-disk-space": "3.4.0",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
@ -254,8 +263,11 @@
|
|||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"code-inspector-plugin": "^0.20.14",
|
"code-inspector-plugin": "^0.20.14",
|
||||||
|
"codemirror-lang-mermaid": "0.5.0",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
|
"commander": "^14.0.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
|
"cors": "2.8.5",
|
||||||
"country-flag-emoji-polyfill": "0.1.8",
|
"country-flag-emoji-polyfill": "0.1.8",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
@ -263,6 +275,7 @@
|
|||||||
"diff": "^8.0.2",
|
"diff": "^8.0.2",
|
||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
|
"dotenv": "16.6.1",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"drizzle-kit": "^0.31.4",
|
"drizzle-kit": "^0.31.4",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
@ -271,12 +284,13 @@
|
|||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-reload": "^2.0.0-alpha.1",
|
"electron-reload": "^2.0.0-alpha.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "patch:electron-updater@npm%3A6.7.0#~/.yarn/patches/electron-updater-npm-6.7.0-47b11bb0d4.patch",
|
"electron-updater": "6.7.0",
|
||||||
"electron-vite": "4.0.1",
|
"electron-vite": "5.0.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
"emoji-picker-element-data": "1",
|
||||||
|
"epub": "1.3.0",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-import-zod": "^1.2.0",
|
"eslint-plugin-import-zod": "^1.2.0",
|
||||||
"eslint-plugin-oxlint": "^1.15.0",
|
"eslint-plugin-oxlint": "^1.15.0",
|
||||||
@ -287,6 +301,7 @@
|
|||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
"fetch-socks": "1.3.2",
|
"fetch-socks": "1.3.2",
|
||||||
|
"form-data": "4.0.4",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"franc-min": "^6.2.0",
|
"franc-min": "^6.2.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
@ -303,6 +318,11 @@
|
|||||||
"isbinaryfile": "5.0.4",
|
"isbinaryfile": "5.0.4",
|
||||||
"jaison": "^2.0.2",
|
"jaison": "^2.0.2",
|
||||||
"jest-styled-components": "^7.2.0",
|
"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",
|
||||||
"linguist-languages": "^8.1.0",
|
"linguist-languages": "^8.1.0",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@ -310,19 +330,27 @@
|
|||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"macos-release": "^3.4.0",
|
"macos-release": "^3.4.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
"md5": "2.3.0",
|
||||||
"mermaid": "^11.10.1",
|
"mermaid": "^11.10.1",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
|
"nanoid": "3.3.11",
|
||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
|
"ollama-ai-provider-v2": "1.5.5",
|
||||||
|
"open": "^8.4.2",
|
||||||
"oxlint": "^1.22.0",
|
"oxlint": "^1.22.0",
|
||||||
"oxlint-tsgolint": "^0.2.0",
|
"oxlint-tsgolint": "^0.2.0",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
|
"pako": "1.0.11",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"playwright": "^1.55.1",
|
"prosemirror-model": "1.25.2",
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
|
"rc-input": "1.8.0",
|
||||||
|
"rc-select": "14.16.6",
|
||||||
|
"rc-virtual-list": "3.18.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
@ -349,8 +377,11 @@
|
|||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-github-blockquote-alert": "^2.0.0",
|
"remark-github-blockquote-alert": "^2.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"remark-parse": "11.0.0",
|
||||||
|
"remark-stringify": "11.0.0",
|
||||||
"remove-markdown": "^0.6.2",
|
"remove-markdown": "^0.6.2",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
|
"semver": "7.7.1",
|
||||||
"shiki": "^3.12.0",
|
"shiki": "^3.12.0",
|
||||||
"strict-url-sanitise": "^0.0.1",
|
"strict-url-sanitise": "^0.0.1",
|
||||||
"string-width": "^7.2.0",
|
"string-width": "^7.2.0",
|
||||||
@ -365,11 +396,12 @@
|
|||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.3",
|
||||||
"undici": "6.21.2",
|
"undici": "6.21.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
|
"unist-util-visit": "5.0.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "npm:rolldown-vite@7.1.5",
|
"vite": "npm:rolldown-vite@7.3.0",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"webdav": "^5.8.0",
|
"webdav": "^5.8.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
@ -382,41 +414,68 @@
|
|||||||
"zipread": "^1.3.3",
|
"zipread": "^1.3.3",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"pnpm": {
|
||||||
"@smithy/types": "4.7.1",
|
"overrides": {
|
||||||
"@codemirror/language": "6.11.3",
|
"@smithy/types": "4.7.1",
|
||||||
"@codemirror/lint": "6.8.5",
|
"@codemirror/language": "6.11.3",
|
||||||
"@codemirror/view": "6.38.1",
|
"@codemirror/lint": "6.8.5",
|
||||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
"@codemirror/view": "6.38.1",
|
||||||
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
"esbuild": "^0.25.0",
|
||||||
"esbuild": "^0.25.0",
|
"node-abi": "4.24.0",
|
||||||
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
"openai": "npm:@cherrystudio/openai@6.15.0",
|
||||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
"tar-fs": "^2.1.4",
|
||||||
"node-abi": "4.24.0",
|
"undici": "6.21.2",
|
||||||
"openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0",
|
"vite": "npm:rolldown-vite@7.3.0",
|
||||||
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0",
|
"@img/sharp-darwin-arm64": "0.34.3",
|
||||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
"@img/sharp-darwin-x64": "0.34.3",
|
||||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
"@img/sharp-linux-arm": "0.34.3",
|
||||||
"tar-fs": "^2.1.4",
|
"@img/sharp-linux-arm64": "0.34.3",
|
||||||
"undici": "6.21.2",
|
"@img/sharp-linux-x64": "0.34.3",
|
||||||
"vite": "npm:rolldown-vite@7.1.5",
|
"@img/sharp-win32-x64": "0.34.3",
|
||||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
"@langchain/core": "1.0.2",
|
||||||
"@ai-sdk/openai@npm:^2.0.52": "patch:@ai-sdk/openai@npm%3A2.0.52#~/.yarn/patches/@ai-sdk-openai-npm-2.0.52-b36d949c76.patch",
|
"@ai-sdk/openai-compatible@1.0.27": "1.0.28",
|
||||||
"@img/sharp-darwin-arm64": "0.34.3",
|
"@ai-sdk/openai-compatible@1.0.30": "1.0.28"
|
||||||
"@img/sharp-darwin-x64": "0.34.3",
|
},
|
||||||
"@img/sharp-linux-arm": "0.34.3",
|
"patchedDependencies": {
|
||||||
"@img/sharp-linux-arm64": "0.34.3",
|
"@napi-rs/system-ocr@1.0.2": "patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||||
"@img/sharp-linux-x64": "0.34.3",
|
"tesseract.js@6.0.1": "patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||||
"@img/sharp-win32-x64": "0.34.3",
|
"@ai-sdk/google@2.0.49": "patches/@ai-sdk-google-npm-2.0.49-84720f41bd.patch",
|
||||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0",
|
"@ai-sdk/openai@2.0.85": "patches/@ai-sdk-openai-npm-2.0.85-27483d1d6a.patch",
|
||||||
"@langchain/openai@npm:>=0.1.0 <0.6.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
"@anthropic-ai/vertex-sdk@0.11.4": "patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
"@google/genai@1.0.1": "patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||||
"@langchain/openai@npm:>=0.2.0 <0.7.0": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
"@langchain/core@1.0.2": "patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||||
"@ai-sdk/openai@npm:^2.0.42": "patch:@ai-sdk/openai@npm%3A2.0.72#~/.yarn/patches/@ai-sdk-openai-npm-2.0.72-234e68da87.patch",
|
"@langchain/openai@1.0.0": "patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",
|
||||||
"@ai-sdk/google@npm:^2.0.40": "patch:@ai-sdk/google@npm%3A2.0.40#~/.yarn/patches/@ai-sdk-google-npm-2.0.40-47e0eeee83.patch",
|
"@tiptap/extension-drag-handle@3.2.0": "patches/@tiptap-extension-drag-handle-npm-3.2.0-5a9ebff7c9.patch",
|
||||||
"@ai-sdk/openai-compatible@npm:^1.0.27": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch"
|
"antd@5.27.0": "patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||||
|
"electron-updater@6.7.0": "patches/electron-updater-npm-6.7.0-47b11bb0d4.patch",
|
||||||
|
"epub@1.3.0": "patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||||
|
"ollama-ai-provider-v2@1.5.5": "patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch",
|
||||||
|
"atomically@1.7.0": "patches/atomically-npm-1.7.0-e742e5293b.patch",
|
||||||
|
"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@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",
|
||||||
|
"@paymoapp/electron-shutdown-handler",
|
||||||
|
"@scarf/scarf",
|
||||||
|
"@swc/core",
|
||||||
|
"electron",
|
||||||
|
"electron-winstaller",
|
||||||
|
"esbuild",
|
||||||
|
"msw",
|
||||||
|
"protobufjs",
|
||||||
|
"registry-js",
|
||||||
|
"selection-hook",
|
||||||
|
"sharp",
|
||||||
|
"tesseract.js",
|
||||||
|
"zipfile"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1",
|
"packageManager": "pnpm@10.27.0",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
||||||
"biome format --write --no-errors-on-unmatched",
|
"biome format --write --no-errors-on-unmatched",
|
||||||
@ -425,5 +484,27 @@
|
|||||||
"*.{json,yml,yaml,css,html}": [
|
"*.{json,yml,yaml,css,html}": [
|
||||||
"biome format --write --no-errors-on-unmatched"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ It exposes the CherryIN OpenAI-compatible entrypoints and dynamically routes Ant
|
|||||||
```bash
|
```bash
|
||||||
npm install ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai
|
npm install ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai
|
||||||
# or
|
# or
|
||||||
yarn add ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai
|
pnpm add ai @cherrystudio/ai-sdk-provider @ai-sdk/anthropic @ai-sdk/google @ai-sdk/openai
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note**: This package requires peer dependencies `ai`, `@ai-sdk/anthropic`, `@ai-sdk/google`, and `@ai-sdk/openai` to be installed.
|
> **Note**: This package requires peer dependencies `ai`, `@ai-sdk/anthropic`, `@ai-sdk/google`, and `@ai-sdk/openai` to be installed.
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
"ai": "^5.0.26"
|
"ai": "^5.0.26"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ai-sdk/openai-compatible": "1.0.28",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
"@ai-sdk/provider-utils": "^3.0.17"
|
"@ai-sdk/provider-utils": "^3.0.17"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { AnthropicMessagesLanguageModel } from '@ai-sdk/anthropic/internal'
|
|||||||
import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal'
|
import { GoogleGenerativeAILanguageModel } from '@ai-sdk/google/internal'
|
||||||
import type { OpenAIProviderSettings } from '@ai-sdk/openai'
|
import type { OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
import {
|
import {
|
||||||
OpenAIChatLanguageModel,
|
|
||||||
OpenAICompletionLanguageModel,
|
OpenAICompletionLanguageModel,
|
||||||
OpenAIEmbeddingModel,
|
OpenAIEmbeddingModel,
|
||||||
OpenAIImageModel,
|
OpenAIImageModel,
|
||||||
@ -10,6 +9,7 @@ import {
|
|||||||
OpenAISpeechModel,
|
OpenAISpeechModel,
|
||||||
OpenAITranscriptionModel
|
OpenAITranscriptionModel
|
||||||
} from '@ai-sdk/openai/internal'
|
} from '@ai-sdk/openai/internal'
|
||||||
|
import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible'
|
||||||
import {
|
import {
|
||||||
type EmbeddingModelV2,
|
type EmbeddingModelV2,
|
||||||
type ImageModelV2,
|
type ImageModelV2,
|
||||||
@ -69,6 +69,7 @@ export interface CherryInProviderSettings {
|
|||||||
headers?: HeadersInput
|
headers?: HeadersInput
|
||||||
/**
|
/**
|
||||||
* Optional endpoint type to distinguish different endpoint behaviors.
|
* Optional endpoint type to distinguish different endpoint behaviors.
|
||||||
|
* "image-generation" is also openai endpoint, but specifically for image generation.
|
||||||
*/
|
*/
|
||||||
endpointType?: 'openai' | 'openai-response' | 'anthropic' | 'gemini' | 'image-generation' | 'jina-rerank'
|
endpointType?: 'openai' | 'openai-response' | 'anthropic' | 'gemini' | 'image-generation' | 'jina-rerank'
|
||||||
}
|
}
|
||||||
@ -117,7 +118,7 @@ const createCustomFetch = (originalFetch?: any) => {
|
|||||||
return originalFetch ? originalFetch(url, options) : fetch(url, options)
|
return originalFetch ? originalFetch(url, options) : fetch(url, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class CherryInOpenAIChatLanguageModel extends OpenAIChatLanguageModel {
|
class CherryInOpenAIChatLanguageModel extends OpenAICompatibleChatLanguageModel {
|
||||||
constructor(modelId: string, settings: any) {
|
constructor(modelId: string, settings: any) {
|
||||||
super(modelId, {
|
super(modelId, {
|
||||||
...settings,
|
...settings,
|
||||||
|
|||||||
@ -40,9 +40,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.49",
|
"@ai-sdk/anthropic": "^2.0.49",
|
||||||
"@ai-sdk/azure": "^2.0.74",
|
"@ai-sdk/azure": "^2.0.87",
|
||||||
"@ai-sdk/deepseek": "^1.0.29",
|
"@ai-sdk/deepseek": "^1.0.31",
|
||||||
"@ai-sdk/openai-compatible": "patch:@ai-sdk/openai-compatible@npm%3A1.0.27#~/.yarn/patches/@ai-sdk-openai-compatible-npm-1.0.27-06f74278cf.patch",
|
"@ai-sdk/openai-compatible": "1.0.28",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
"@ai-sdk/provider-utils": "^3.0.17",
|
"@ai-sdk/provider-utils": "^3.0.17",
|
||||||
"@ai-sdk/xai": "^2.0.36",
|
"@ai-sdk/xai": "^2.0.36",
|
||||||
|
|||||||
@ -3,12 +3,13 @@
|
|||||||
* Provides realistic mock responses for all provider types
|
* Provides realistic mock responses for all provider types
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { jsonSchema, type ModelMessage, type Tool } from 'ai'
|
import type { ModelMessage, Tool } from 'ai'
|
||||||
|
import { jsonSchema } from 'ai'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard test messages for all scenarios
|
* Standard test messages for all scenarios
|
||||||
*/
|
*/
|
||||||
export const testMessages = {
|
export const testMessages: Record<string, ModelMessage[]> = {
|
||||||
simple: [{ role: 'user' as const, content: 'Hello, how are you?' }],
|
simple: [{ role: 'user' as const, content: 'Hello, how are you?' }],
|
||||||
|
|
||||||
conversation: [
|
conversation: [
|
||||||
@ -45,7 +46,7 @@ export const testMessages = {
|
|||||||
{ role: 'assistant' as const, content: '15 * 23 = 345' },
|
{ role: 'assistant' as const, content: '15 * 23 = 345' },
|
||||||
{ role: 'user' as const, content: 'Now divide that by 5' }
|
{ role: 'user' as const, content: 'Now divide that by 5' }
|
||||||
]
|
]
|
||||||
} satisfies Record<string, ModelMessage[]>
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard test tools for tool calling scenarios
|
* Standard test tools for tool calling scenarios
|
||||||
@ -138,68 +139,17 @@ export const testTools: Record<string, Tool> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock streaming chunks for different providers
|
|
||||||
*/
|
|
||||||
export const mockStreamingChunks = {
|
|
||||||
text: [
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'Hello' },
|
|
||||||
{ type: 'text-delta' as const, textDelta: ', ' },
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'this ' },
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'is ' },
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'a ' },
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'test.' }
|
|
||||||
],
|
|
||||||
|
|
||||||
withToolCall: [
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'Let me check the weather for you.' },
|
|
||||||
{
|
|
||||||
type: 'tool-call-delta' as const,
|
|
||||||
toolCallType: 'function' as const,
|
|
||||||
toolCallId: 'call_123',
|
|
||||||
toolName: 'getWeather',
|
|
||||||
argsTextDelta: '{"location":'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'tool-call-delta' as const,
|
|
||||||
toolCallType: 'function' as const,
|
|
||||||
toolCallId: 'call_123',
|
|
||||||
toolName: 'getWeather',
|
|
||||||
argsTextDelta: ' "San Francisco, CA"}'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'tool-call' as const,
|
|
||||||
toolCallType: 'function' as const,
|
|
||||||
toolCallId: 'call_123',
|
|
||||||
toolName: 'getWeather',
|
|
||||||
args: { location: 'San Francisco, CA' }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
withFinish: [
|
|
||||||
{ type: 'text-delta' as const, textDelta: 'Complete response.' },
|
|
||||||
{
|
|
||||||
type: 'finish' as const,
|
|
||||||
finishReason: 'stop' as const,
|
|
||||||
usage: {
|
|
||||||
promptTokens: 10,
|
|
||||||
completionTokens: 5,
|
|
||||||
totalTokens: 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock complete responses for non-streaming scenarios
|
* Mock complete responses for non-streaming scenarios
|
||||||
|
* Note: AI SDK v5 uses inputTokens/outputTokens instead of promptTokens/completionTokens
|
||||||
*/
|
*/
|
||||||
export const mockCompleteResponses = {
|
export const mockCompleteResponses = {
|
||||||
simple: {
|
simple: {
|
||||||
text: 'This is a simple response.',
|
text: 'This is a simple response.',
|
||||||
finishReason: 'stop' as const,
|
finishReason: 'stop' as const,
|
||||||
usage: {
|
usage: {
|
||||||
promptTokens: 15,
|
inputTokens: 15,
|
||||||
completionTokens: 8,
|
outputTokens: 8,
|
||||||
totalTokens: 23
|
totalTokens: 23
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -215,8 +165,8 @@ export const mockCompleteResponses = {
|
|||||||
],
|
],
|
||||||
finishReason: 'tool-calls' as const,
|
finishReason: 'tool-calls' as const,
|
||||||
usage: {
|
usage: {
|
||||||
promptTokens: 25,
|
inputTokens: 25,
|
||||||
completionTokens: 12,
|
outputTokens: 12,
|
||||||
totalTokens: 37
|
totalTokens: 37
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -225,14 +175,15 @@ export const mockCompleteResponses = {
|
|||||||
text: 'Response with warnings.',
|
text: 'Response with warnings.',
|
||||||
finishReason: 'stop' as const,
|
finishReason: 'stop' as const,
|
||||||
usage: {
|
usage: {
|
||||||
promptTokens: 10,
|
inputTokens: 10,
|
||||||
completionTokens: 5,
|
outputTokens: 5,
|
||||||
totalTokens: 15
|
totalTokens: 15
|
||||||
},
|
},
|
||||||
warnings: [
|
warnings: [
|
||||||
{
|
{
|
||||||
type: 'unsupported-setting' as const,
|
type: 'unsupported-setting' as const,
|
||||||
message: 'Temperature parameter not supported for this model'
|
setting: 'temperature',
|
||||||
|
details: 'Temperature parameter not supported for this model'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -285,47 +236,3 @@ export const mockImageResponses = {
|
|||||||
warnings: []
|
warnings: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mock error responses
|
|
||||||
*/
|
|
||||||
export const mockErrors = {
|
|
||||||
invalidApiKey: {
|
|
||||||
name: 'APIError',
|
|
||||||
message: 'Invalid API key provided',
|
|
||||||
statusCode: 401
|
|
||||||
},
|
|
||||||
|
|
||||||
rateLimitExceeded: {
|
|
||||||
name: 'RateLimitError',
|
|
||||||
message: 'Rate limit exceeded. Please try again later.',
|
|
||||||
statusCode: 429,
|
|
||||||
headers: {
|
|
||||||
'retry-after': '60'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
modelNotFound: {
|
|
||||||
name: 'ModelNotFoundError',
|
|
||||||
message: 'The requested model was not found',
|
|
||||||
statusCode: 404
|
|
||||||
},
|
|
||||||
|
|
||||||
contextLengthExceeded: {
|
|
||||||
name: 'ContextLengthError',
|
|
||||||
message: "This model's maximum context length is 4096 tokens",
|
|
||||||
statusCode: 400
|
|
||||||
},
|
|
||||||
|
|
||||||
timeout: {
|
|
||||||
name: 'TimeoutError',
|
|
||||||
message: 'Request timed out after 30000ms',
|
|
||||||
code: 'ETIMEDOUT'
|
|
||||||
},
|
|
||||||
|
|
||||||
networkError: {
|
|
||||||
name: 'NetworkError',
|
|
||||||
message: 'Network connection failed',
|
|
||||||
code: 'ECONNREFUSED'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
packages/aiCore/src/__tests__/mocks/ai-sdk-provider.ts
Normal file
35
packages/aiCore/src/__tests__/mocks/ai-sdk-provider.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Mock for @cherrystudio/ai-sdk-provider
|
||||||
|
* This mock is used in tests to avoid importing the actual package
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CherryInProviderSettings = {
|
||||||
|
apiKey?: string
|
||||||
|
baseURL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// oxlint-disable-next-line no-unused-vars
|
||||||
|
export const createCherryIn = (_options?: CherryInProviderSettings) => ({
|
||||||
|
// oxlint-disable-next-line no-unused-vars
|
||||||
|
languageModel: (_modelId: string) => ({
|
||||||
|
specificationVersion: 'v1',
|
||||||
|
provider: 'cherryin',
|
||||||
|
modelId: 'mock-model',
|
||||||
|
doGenerate: async () => ({ text: 'mock response' }),
|
||||||
|
doStream: async () => ({ stream: (async function* () {})() })
|
||||||
|
}),
|
||||||
|
// oxlint-disable-next-line no-unused-vars
|
||||||
|
chat: (_modelId: string) => ({
|
||||||
|
specificationVersion: 'v1',
|
||||||
|
provider: 'cherryin-chat',
|
||||||
|
modelId: 'mock-model',
|
||||||
|
doGenerate: async () => ({ text: 'mock response' }),
|
||||||
|
doStream: async () => ({ stream: (async function* () {})() })
|
||||||
|
}),
|
||||||
|
// oxlint-disable-next-line no-unused-vars
|
||||||
|
textEmbeddingModel: (_modelId: string) => ({
|
||||||
|
specificationVersion: 'v1',
|
||||||
|
provider: 'cherryin',
|
||||||
|
modelId: 'mock-embedding-model'
|
||||||
|
})
|
||||||
|
})
|
||||||
9
packages/aiCore/src/__tests__/setup.ts
Normal file
9
packages/aiCore/src/__tests__/setup.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Vitest Setup File
|
||||||
|
* Global test configuration and mocks for @cherrystudio/ai-core package
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock Vite SSR helper to avoid Node environment errors
|
||||||
|
;(globalThis as any).__vite_ssr_exportName__ = (_name: string, value: any) => value
|
||||||
|
|
||||||
|
// Note: @cherrystudio/ai-sdk-provider is mocked via alias in vitest.config.ts
|
||||||
109
packages/aiCore/src/core/options/__tests__/factory.test.ts
Normal file
109
packages/aiCore/src/core/options/__tests__/factory.test.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { createOpenAIOptions, createOpenRouterOptions, mergeProviderOptions } from '../factory'
|
||||||
|
|
||||||
|
describe('mergeProviderOptions', () => {
|
||||||
|
it('deep merges provider options for the same provider', () => {
|
||||||
|
const reasoningOptions = createOpenRouterOptions({
|
||||||
|
reasoning: {
|
||||||
|
enabled: true,
|
||||||
|
effort: 'medium'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const webSearchOptions = createOpenRouterOptions({
|
||||||
|
plugins: [{ id: 'web', max_results: 5 }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = mergeProviderOptions(reasoningOptions, webSearchOptions)
|
||||||
|
|
||||||
|
expect(merged.openrouter).toEqual({
|
||||||
|
reasoning: {
|
||||||
|
enabled: true,
|
||||||
|
effort: 'medium'
|
||||||
|
},
|
||||||
|
plugins: [{ id: 'web', max_results: 5 }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves options from other providers while merging', () => {
|
||||||
|
const openRouter = createOpenRouterOptions({
|
||||||
|
reasoning: { enabled: true }
|
||||||
|
})
|
||||||
|
const openAI = createOpenAIOptions({
|
||||||
|
reasoningEffort: 'low'
|
||||||
|
})
|
||||||
|
const merged = mergeProviderOptions(openRouter, openAI)
|
||||||
|
|
||||||
|
expect(merged.openrouter).toEqual({ reasoning: { enabled: true } })
|
||||||
|
expect(merged.openai).toEqual({ reasoningEffort: 'low' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overwrites primitive values with later values', () => {
|
||||||
|
const first = createOpenAIOptions({
|
||||||
|
reasoningEffort: 'low',
|
||||||
|
user: 'user-123'
|
||||||
|
})
|
||||||
|
const second = createOpenAIOptions({
|
||||||
|
reasoningEffort: 'high',
|
||||||
|
maxToolCalls: 5
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = mergeProviderOptions(first, second)
|
||||||
|
|
||||||
|
expect(merged.openai).toEqual({
|
||||||
|
reasoningEffort: 'high', // overwritten by second
|
||||||
|
user: 'user-123', // preserved from first
|
||||||
|
maxToolCalls: 5 // added from second
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overwrites arrays with later values instead of merging', () => {
|
||||||
|
const first = createOpenRouterOptions({
|
||||||
|
models: ['gpt-4', 'gpt-3.5-turbo']
|
||||||
|
})
|
||||||
|
const second = createOpenRouterOptions({
|
||||||
|
models: ['claude-3-opus', 'claude-3-sonnet']
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = mergeProviderOptions(first, second)
|
||||||
|
|
||||||
|
// Array is completely replaced, not merged
|
||||||
|
expect(merged.openrouter?.models).toEqual(['claude-3-opus', 'claude-3-sonnet'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deeply merges nested objects while overwriting primitives', () => {
|
||||||
|
const first = createOpenRouterOptions({
|
||||||
|
reasoning: {
|
||||||
|
enabled: true,
|
||||||
|
effort: 'low'
|
||||||
|
},
|
||||||
|
user: 'user-123'
|
||||||
|
})
|
||||||
|
const second = createOpenRouterOptions({
|
||||||
|
reasoning: {
|
||||||
|
effort: 'high',
|
||||||
|
max_tokens: 500
|
||||||
|
},
|
||||||
|
user: 'user-456'
|
||||||
|
})
|
||||||
|
|
||||||
|
const merged = mergeProviderOptions(first, second)
|
||||||
|
|
||||||
|
expect(merged.openrouter).toEqual({
|
||||||
|
reasoning: {
|
||||||
|
enabled: true, // preserved from first
|
||||||
|
effort: 'high', // overwritten by second
|
||||||
|
max_tokens: 500 // added from second
|
||||||
|
},
|
||||||
|
user: 'user-456' // overwritten by second
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces arrays instead of merging them', () => {
|
||||||
|
const first = createOpenRouterOptions({ plugins: [{ id: 'old' }] })
|
||||||
|
const second = createOpenRouterOptions({ plugins: [{ id: 'new' }] })
|
||||||
|
const merged = mergeProviderOptions(first, second)
|
||||||
|
// @ts-expect-error type-check for openrouter options is skipped. see function signature of createOpenRouterOptions
|
||||||
|
expect(merged.openrouter?.plugins).toEqual([{ id: 'new' }])
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -26,13 +26,65 @@ export function createGenericProviderOptions<T extends string>(
|
|||||||
return { [provider]: options } as Record<T, Record<string, any>>
|
return { [provider]: options } as Record<T, Record<string, any>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PlainObject = Record<string, any>
|
||||||
|
|
||||||
|
const isPlainObject = (value: unknown): value is PlainObject => {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMergeObjects<T extends PlainObject>(target: T, source: PlainObject): T {
|
||||||
|
const result: PlainObject = { ...target }
|
||||||
|
Object.entries(source).forEach(([key, value]) => {
|
||||||
|
if (isPlainObject(value) && isPlainObject(result[key])) {
|
||||||
|
result[key] = deepMergeObjects(result[key], value)
|
||||||
|
} else {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合并多个供应商的options
|
* Deep-merge multiple provider-specific options.
|
||||||
* @param optionsMap 包含多个供应商选项的对象
|
* Nested objects are recursively merged; primitive values are overwritten.
|
||||||
* @returns 合并后的TypedProviderOptions
|
*
|
||||||
|
* When the same key appears in multiple options:
|
||||||
|
* - If both values are plain objects: they are deeply merged (recursive merge)
|
||||||
|
* - If values are primitives/arrays: the later value overwrites the earlier one
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* mergeProviderOptions(
|
||||||
|
* { openrouter: { reasoning: { enabled: true, effort: 'low' }, user: 'user-123' } },
|
||||||
|
* { openrouter: { reasoning: { effort: 'high', max_tokens: 500 }, models: ['gpt-4'] } }
|
||||||
|
* )
|
||||||
|
* // Result: {
|
||||||
|
* // openrouter: {
|
||||||
|
* // reasoning: { enabled: true, effort: 'high', max_tokens: 500 },
|
||||||
|
* // user: 'user-123',
|
||||||
|
* // models: ['gpt-4']
|
||||||
|
* // }
|
||||||
|
* // }
|
||||||
|
*
|
||||||
|
* @param optionsMap Objects containing options for multiple providers
|
||||||
|
* @returns Fully merged TypedProviderOptions
|
||||||
*/
|
*/
|
||||||
export function mergeProviderOptions(...optionsMap: Partial<TypedProviderOptions>[]): TypedProviderOptions {
|
export function mergeProviderOptions(...optionsMap: Partial<TypedProviderOptions>[]): TypedProviderOptions {
|
||||||
return Object.assign({}, ...optionsMap)
|
return optionsMap.reduce<TypedProviderOptions>((acc, options) => {
|
||||||
|
if (!options) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
Object.entries(options).forEach(([providerId, providerOptions]) => {
|
||||||
|
if (!providerOptions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (acc[providerId]) {
|
||||||
|
acc[providerId] = deepMergeObjects(acc[providerId] as PlainObject, providerOptions as PlainObject)
|
||||||
|
} else {
|
||||||
|
acc[providerId] = providerOptions as any
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return acc
|
||||||
|
}, {} as TypedProviderOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export class StreamEventManager {
|
|||||||
const recursiveResult = await context.recursiveCall(recursiveParams)
|
const recursiveResult = await context.recursiveCall(recursiveParams)
|
||||||
|
|
||||||
if (recursiveResult && recursiveResult.fullStream) {
|
if (recursiveResult && recursiveResult.fullStream) {
|
||||||
await this.pipeRecursiveStream(controller, recursiveResult.fullStream, context)
|
await this.pipeRecursiveStream(controller, recursiveResult.fullStream)
|
||||||
} else {
|
} else {
|
||||||
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
|
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
|
||||||
}
|
}
|
||||||
@ -74,11 +74,7 @@ export class StreamEventManager {
|
|||||||
/**
|
/**
|
||||||
* 将递归流的数据传递到当前流
|
* 将递归流的数据传递到当前流
|
||||||
*/
|
*/
|
||||||
private async pipeRecursiveStream(
|
private async pipeRecursiveStream(controller: StreamController, recursiveStream: ReadableStream): Promise<void> {
|
||||||
controller: StreamController,
|
|
||||||
recursiveStream: ReadableStream,
|
|
||||||
context?: AiRequestContext
|
|
||||||
): Promise<void> {
|
|
||||||
const reader = recursiveStream.getReader()
|
const reader = recursiveStream.getReader()
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -86,18 +82,14 @@ export class StreamEventManager {
|
|||||||
if (done) {
|
if (done) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if (value.type === 'start') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (value.type === 'finish') {
|
if (value.type === 'finish') {
|
||||||
// 迭代的流不发finish,但需要累加其 usage
|
|
||||||
if (value.usage && context?.accumulatedUsage) {
|
|
||||||
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// 对于 finish-step 类型,累加其 usage
|
|
||||||
if (value.type === 'finish-step' && value.usage && context?.accumulatedUsage) {
|
|
||||||
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
|
||||||
}
|
|
||||||
// 将递归流的数据传递到当前流
|
|
||||||
controller.enqueue(value)
|
controller.enqueue(value)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -135,10 +127,8 @@ export class StreamEventManager {
|
|||||||
// 构建新的对话消息
|
// 构建新的对话消息
|
||||||
const newMessages: ModelMessage[] = [
|
const newMessages: ModelMessage[] = [
|
||||||
...(context.originalParams.messages || []),
|
...(context.originalParams.messages || []),
|
||||||
{
|
// 只有当 textBuffer 有内容时才添加 assistant 消息,避免空消息导致 API 错误
|
||||||
role: 'assistant',
|
...(textBuffer ? [{ role: 'assistant' as const, content: textBuffer }] : []),
|
||||||
content: textBuffer
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: toolResultsText
|
content: toolResultsText
|
||||||
@ -161,7 +151,7 @@ export class StreamEventManager {
|
|||||||
/**
|
/**
|
||||||
* 累加 usage 数据
|
* 累加 usage 数据
|
||||||
*/
|
*/
|
||||||
private accumulateUsage(target: any, source: any): void {
|
accumulateUsage(target: any, source: any): void {
|
||||||
if (!target || !source) return
|
if (!target || !source) return
|
||||||
|
|
||||||
// 累加各种 token 类型
|
// 累加各种 token 类型
|
||||||
|
|||||||
@ -21,11 +21,8 @@ const TOOL_USE_TAG_CONFIG: TagConfig = {
|
|||||||
separator: '\n'
|
separator: '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const DEFAULT_SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \
|
||||||
* 默认系统提示符模板(提取自 Cherry Studio)
|
You can use one or more tools per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
||||||
*/
|
|
||||||
const DEFAULT_SYSTEM_PROMPT = `In this environment you have access to a set of tools you can use to answer the user's question. \\
|
|
||||||
You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
|
||||||
|
|
||||||
## Tool Use Formatting
|
## Tool Use Formatting
|
||||||
|
|
||||||
@ -38,10 +35,16 @@ Tool use is formatted using XML-style tags. The tool name is enclosed in opening
|
|||||||
|
|
||||||
The tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:
|
The tool name should be the exact name of the tool you are using, and the arguments should be a JSON object containing the parameters required by that tool. For example:
|
||||||
<tool_use>
|
<tool_use>
|
||||||
<name>python_interpreter</name>
|
<name>search</name>
|
||||||
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
|
<arguments>{ "query": "browser,fetch" }</arguments>
|
||||||
</tool_use>
|
</tool_use>
|
||||||
|
|
||||||
|
<tool_use>
|
||||||
|
<name>exec</name>
|
||||||
|
<arguments>{ "code": "const page = await CherryBrowser_fetch({ url: "https://example.com" })\nreturn page" }</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
|
||||||
The user will respond with the result of the tool use, which should be formatted as follows:
|
The user will respond with the result of the tool use, which should be formatted as follows:
|
||||||
|
|
||||||
<tool_use_result>
|
<tool_use_result>
|
||||||
@ -59,13 +62,6 @@ For example, if the result of the tool use is an image file, you can use it in t
|
|||||||
|
|
||||||
Always adhere to this format for the tool use to ensure proper parsing and execution.
|
Always adhere to this format for the tool use to ensure proper parsing and execution.
|
||||||
|
|
||||||
## Tool Use Examples
|
|
||||||
{{ TOOL_USE_EXAMPLES }}
|
|
||||||
|
|
||||||
## Tool Use Available Tools
|
|
||||||
Above example were using notional tools that might not exist for you. You only have access to these tools:
|
|
||||||
{{ AVAILABLE_TOOLS }}
|
|
||||||
|
|
||||||
## Tool Use Rules
|
## Tool Use Rules
|
||||||
Here are the rules you should always follow to solve your task:
|
Here are the rules you should always follow to solve your task:
|
||||||
1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.
|
1. Always use the right arguments for the tools. Never use variable names as the action arguments, use the value instead.
|
||||||
@ -74,10 +70,15 @@ Here are the rules you should always follow to solve your task:
|
|||||||
4. Never re-do a tool call that you previously did with the exact same parameters.
|
4. Never re-do a tool call that you previously did with the exact same parameters.
|
||||||
5. For tool use, MAKE SURE use XML tag format as shown in the examples above. Do not use any other format.
|
5. For tool use, MAKE SURE use XML tag format as shown in the examples above. Do not use any other format.
|
||||||
|
|
||||||
|
{{ TOOLS_INFO }}
|
||||||
|
|
||||||
|
## Response rules
|
||||||
|
|
||||||
|
Respond in the language of the user's query, unless the user instructions specify additional requirements for the language to be used.
|
||||||
|
|
||||||
# User Instructions
|
# User Instructions
|
||||||
{{ USER_SYSTEM_PROMPT }}
|
{{ USER_SYSTEM_PROMPT }}
|
||||||
|
`
|
||||||
Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.`
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认工具使用示例(提取自 Cherry Studio)
|
* 默认工具使用示例(提取自 Cherry Studio)
|
||||||
@ -151,7 +152,8 @@ User: <tool_use_result>
|
|||||||
<name>search</name>
|
<name>search</name>
|
||||||
<result>26 million (2019)</result>
|
<result>26 million (2019)</result>
|
||||||
</tool_use_result>
|
</tool_use_result>
|
||||||
Assistant: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.`
|
|
||||||
|
A: The population of Shanghai is 26 million, while Guangzhou has a population of 15 million. Therefore, Shanghai has the highest population.`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建可用工具部分(提取自 Cherry Studio)
|
* 构建可用工具部分(提取自 Cherry Studio)
|
||||||
@ -181,13 +183,30 @@ ${result}
|
|||||||
/**
|
/**
|
||||||
* 默认的系统提示符构建函数(提取自 Cherry Studio)
|
* 默认的系统提示符构建函数(提取自 Cherry Studio)
|
||||||
*/
|
*/
|
||||||
function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet): string {
|
function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet, mcpMode?: string): string {
|
||||||
const availableTools = buildAvailableTools(tools)
|
const availableTools = buildAvailableTools(tools)
|
||||||
if (availableTools === null) return userSystemPrompt
|
if (availableTools === null) return userSystemPrompt
|
||||||
|
|
||||||
const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES)
|
if (mcpMode == 'auto') {
|
||||||
|
return DEFAULT_SYSTEM_PROMPT.replace('{{ TOOLS_INFO }}', '').replace(
|
||||||
|
'{{ USER_SYSTEM_PROMPT }}',
|
||||||
|
userSystemPrompt || ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const toolsInfo = `
|
||||||
|
## Tool Use Examples
|
||||||
|
{{ TOOL_USE_EXAMPLES }}
|
||||||
|
|
||||||
|
## Tool Use Available Tools
|
||||||
|
Above example were using notional tools that might not exist for you. You only have access to these tools:
|
||||||
|
{{ AVAILABLE_TOOLS }}`
|
||||||
|
.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES)
|
||||||
.replace('{{ AVAILABLE_TOOLS }}', availableTools)
|
.replace('{{ AVAILABLE_TOOLS }}', availableTools)
|
||||||
.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '')
|
|
||||||
|
const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace('{{ TOOLS_INFO }}', toolsInfo).replace(
|
||||||
|
'{{ USER_SYSTEM_PROMPT }}',
|
||||||
|
userSystemPrompt || ''
|
||||||
|
)
|
||||||
|
|
||||||
return fullPrompt
|
return fullPrompt
|
||||||
}
|
}
|
||||||
@ -220,7 +239,17 @@ function defaultParseToolUse(content: string, tools: ToolSet): { results: ToolUs
|
|||||||
// Find all tool use blocks
|
// Find all tool use blocks
|
||||||
while ((match = toolUsePattern.exec(contentToProcess)) !== null) {
|
while ((match = toolUsePattern.exec(contentToProcess)) !== null) {
|
||||||
const fullMatch = match[0]
|
const fullMatch = match[0]
|
||||||
const toolName = match[2].trim()
|
let toolName = match[2].trim()
|
||||||
|
switch (toolName.toLowerCase()) {
|
||||||
|
case 'search':
|
||||||
|
toolName = 'mcp__CherryHub__search'
|
||||||
|
break
|
||||||
|
case 'exec':
|
||||||
|
toolName = 'mcp__CherryHub__exec'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
const toolArgs = match[4].trim()
|
const toolArgs = match[4].trim()
|
||||||
|
|
||||||
// Try to parse the arguments as JSON
|
// Try to parse the arguments as JSON
|
||||||
@ -252,7 +281,12 @@ function defaultParseToolUse(content: string, tools: ToolSet): { results: ToolUs
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
||||||
const { enabled = true, buildSystemPrompt = defaultBuildSystemPrompt, parseToolUse = defaultParseToolUse } = config
|
const {
|
||||||
|
enabled = true,
|
||||||
|
buildSystemPrompt = defaultBuildSystemPrompt,
|
||||||
|
parseToolUse = defaultParseToolUse,
|
||||||
|
mcpMode
|
||||||
|
} = config
|
||||||
|
|
||||||
return definePlugin({
|
return definePlugin({
|
||||||
name: 'built-in:prompt-tool-use',
|
name: 'built-in:prompt-tool-use',
|
||||||
@ -282,7 +316,7 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
|||||||
|
|
||||||
// 构建系统提示符(只包含非 provider-defined 工具)
|
// 构建系统提示符(只包含非 provider-defined 工具)
|
||||||
const userSystemPrompt = typeof params.system === 'string' ? params.system : ''
|
const userSystemPrompt = typeof params.system === 'string' ? params.system : ''
|
||||||
const systemPrompt = buildSystemPrompt(userSystemPrompt, promptTools)
|
const systemPrompt = buildSystemPrompt(userSystemPrompt, promptTools, mcpMode)
|
||||||
let systemMessage: string | null = systemPrompt
|
let systemMessage: string | null = systemPrompt
|
||||||
if (config.createSystemMessage) {
|
if (config.createSystemMessage) {
|
||||||
// 🎯 如果用户提供了自定义处理函数,使用它
|
// 🎯 如果用户提供了自定义处理函数,使用它
|
||||||
@ -411,7 +445,10 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有执行工具调用,直接传递原始finish-step事件
|
// 如果没有执行工具调用,累加 usage 后透传 finish-step 事件
|
||||||
|
if (chunk.usage && context.accumulatedUsage) {
|
||||||
|
streamEventManager.accumulateUsage(context.accumulatedUsage, chunk.usage)
|
||||||
|
}
|
||||||
controller.enqueue(chunk)
|
controller.enqueue(chunk)
|
||||||
|
|
||||||
// 清理状态
|
// 清理状态
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export interface PromptToolUseConfig extends BaseToolUsePluginConfig {
|
|||||||
// 自定义工具解析函数(可选,有默认实现)
|
// 自定义工具解析函数(可选,有默认实现)
|
||||||
parseToolUse?: (content: string, tools: ToolSet) => { results: ToolUseResult[]; content: string }
|
parseToolUse?: (content: string, tools: ToolSet) => { results: ToolUseResult[]; content: string }
|
||||||
createSystemMessage?: (systemPrompt: string, originalParams: any, context: AiRequestContext) => string | null
|
createSystemMessage?: (systemPrompt: string, originalParams: any, context: AiRequestContext) => string | null
|
||||||
|
mcpMode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { type Tool } from 'ai'
|
|||||||
|
|
||||||
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
||||||
import type { ProviderOptionsMap } from '../../../options/types'
|
import type { ProviderOptionsMap } from '../../../options/types'
|
||||||
|
import type { AiRequestContext } from '../../'
|
||||||
import type { OpenRouterSearchConfig } from './openrouter'
|
import type { OpenRouterSearchConfig } from './openrouter'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +36,6 @@ export interface WebSearchPluginConfig {
|
|||||||
anthropic?: AnthropicSearchConfig
|
anthropic?: AnthropicSearchConfig
|
||||||
xai?: ProviderOptionsMap['xai']['searchParameters']
|
xai?: ProviderOptionsMap['xai']['searchParameters']
|
||||||
google?: GoogleSearchConfig
|
google?: GoogleSearchConfig
|
||||||
'google-vertex'?: GoogleSearchConfig
|
|
||||||
openrouter?: OpenRouterSearchConfig
|
openrouter?: OpenRouterSearchConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +44,6 @@ export interface WebSearchPluginConfig {
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
||||||
google: {},
|
google: {},
|
||||||
'google-vertex': {},
|
|
||||||
openai: {},
|
openai: {},
|
||||||
'openai-chat': {},
|
'openai-chat': {},
|
||||||
xai: {
|
xai: {
|
||||||
@ -97,55 +96,84 @@ export type WebSearchToolInputSchema = {
|
|||||||
'openai-chat': InferToolInput<OpenAIChatWebSearchTool>
|
'openai-chat': InferToolInput<OpenAIChatWebSearchTool>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const switchWebSearchTool = (providerId: string, config: WebSearchPluginConfig, params: any) => {
|
/**
|
||||||
switch (providerId) {
|
* Helper function to ensure params.tools object exists
|
||||||
case 'openai': {
|
*/
|
||||||
if (config.openai) {
|
const ensureToolsObject = (params: any) => {
|
||||||
if (!params.tools) params.tools = {}
|
if (!params.tools) params.tools = {}
|
||||||
params.tools.web_search = openai.tools.webSearch(config.openai)
|
}
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'openai-chat': {
|
|
||||||
if (config['openai-chat']) {
|
|
||||||
if (!params.tools) params.tools = {}
|
|
||||||
params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat'])
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'anthropic': {
|
/**
|
||||||
if (config.anthropic) {
|
* Helper function to apply tool-based web search configuration
|
||||||
if (!params.tools) params.tools = {}
|
*/
|
||||||
params.tools.web_search = anthropic.tools.webSearch_20250305(config.anthropic)
|
const applyToolBasedSearch = (params: any, toolName: string, toolInstance: any) => {
|
||||||
}
|
ensureToolsObject(params)
|
||||||
break
|
params.tools[toolName] = toolInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'google': {
|
/**
|
||||||
// case 'google-vertex':
|
* Helper function to apply provider options-based web search configuration
|
||||||
if (!params.tools) params.tools = {}
|
*/
|
||||||
params.tools.web_search = google.tools.googleSearch(config.google || {})
|
const applyProviderOptionsSearch = (params: any, searchOptions: any) => {
|
||||||
break
|
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'xai': {
|
export const switchWebSearchTool = (config: WebSearchPluginConfig, params: any, context?: AiRequestContext) => {
|
||||||
if (config.xai) {
|
const providerId = context?.providerId
|
||||||
const searchOptions = createXaiOptions({
|
|
||||||
searchParameters: { ...config.xai, mode: 'on' }
|
|
||||||
})
|
|
||||||
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'openrouter': {
|
// Provider-specific configuration map
|
||||||
if (config.openrouter) {
|
const providerHandlers: Record<string, () => void> = {
|
||||||
const searchOptions = createOpenRouterOptions(config.openrouter)
|
openai: () => {
|
||||||
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
const cfg = config.openai ?? DEFAULT_WEB_SEARCH_CONFIG.openai
|
||||||
}
|
applyToolBasedSearch(params, 'web_search', openai.tools.webSearch(cfg))
|
||||||
|
},
|
||||||
|
'openai-chat': () => {
|
||||||
|
const cfg = (config['openai-chat'] ?? DEFAULT_WEB_SEARCH_CONFIG['openai-chat']) as OpenAISearchPreviewConfig
|
||||||
|
applyToolBasedSearch(params, 'web_search_preview', openai.tools.webSearchPreview(cfg))
|
||||||
|
},
|
||||||
|
anthropic: () => {
|
||||||
|
const cfg = config.anthropic ?? DEFAULT_WEB_SEARCH_CONFIG.anthropic
|
||||||
|
applyToolBasedSearch(params, 'web_search', anthropic.tools.webSearch_20250305(cfg))
|
||||||
|
},
|
||||||
|
google: () => {
|
||||||
|
const cfg = (config.google ?? DEFAULT_WEB_SEARCH_CONFIG.google) as GoogleSearchConfig
|
||||||
|
applyToolBasedSearch(params, 'web_search', google.tools.googleSearch(cfg))
|
||||||
|
},
|
||||||
|
xai: () => {
|
||||||
|
const cfg = config.xai ?? DEFAULT_WEB_SEARCH_CONFIG.xai
|
||||||
|
const searchOptions = createXaiOptions({ searchParameters: { ...cfg, mode: 'on' } })
|
||||||
|
applyProviderOptionsSearch(params, searchOptions)
|
||||||
|
},
|
||||||
|
openrouter: () => {
|
||||||
|
const cfg = (config.openrouter ?? DEFAULT_WEB_SEARCH_CONFIG.openrouter) as OpenRouterSearchConfig
|
||||||
|
const searchOptions = createOpenRouterOptions(cfg)
|
||||||
|
applyProviderOptionsSearch(params, searchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try provider-specific handler first
|
||||||
|
const handler = providerId && providerHandlers[providerId]
|
||||||
|
if (handler) {
|
||||||
|
handler()
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: apply based on available config keys (prioritized order)
|
||||||
|
const fallbackOrder: Array<keyof WebSearchPluginConfig> = [
|
||||||
|
'openai',
|
||||||
|
'openai-chat',
|
||||||
|
'anthropic',
|
||||||
|
'google',
|
||||||
|
'xai',
|
||||||
|
'openrouter'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const key of fallbackOrder) {
|
||||||
|
if (config[key]) {
|
||||||
|
providerHandlers[key]()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { definePlugin } from '../../'
|
import { definePlugin } from '../../'
|
||||||
import type { AiRequestContext } from '../../types'
|
|
||||||
import type { WebSearchPluginConfig } from './helper'
|
import type { WebSearchPluginConfig } from './helper'
|
||||||
import { DEFAULT_WEB_SEARCH_CONFIG, switchWebSearchTool } from './helper'
|
import { DEFAULT_WEB_SEARCH_CONFIG, switchWebSearchTool } from './helper'
|
||||||
|
|
||||||
@ -18,15 +17,22 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
|
|||||||
name: 'webSearch',
|
name: 'webSearch',
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
|
|
||||||
transformParams: async (params: any, context: AiRequestContext) => {
|
transformParams: async (params: any, context) => {
|
||||||
const { providerId } = context
|
let { providerId } = context
|
||||||
switchWebSearchTool(providerId, config, params)
|
|
||||||
|
|
||||||
|
// For cherryin providers, extract the actual provider from the model's provider string
|
||||||
|
// Expected format: "cherryin.{actualProvider}" (e.g., "cherryin.gemini")
|
||||||
if (providerId === 'cherryin' || providerId === 'cherryin-chat') {
|
if (providerId === 'cherryin' || providerId === 'cherryin-chat') {
|
||||||
// cherryin.gemini
|
const provider = params.model?.provider
|
||||||
const _providerId = params.model.provider.split('.')[1]
|
if (provider && typeof provider === 'string' && provider.includes('.')) {
|
||||||
switchWebSearchTool(_providerId, config, params)
|
const extractedProviderId = provider.split('.')[1]
|
||||||
|
if (extractedProviderId) {
|
||||||
|
providerId = extractedProviderId
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchWebSearchTool(config, params, { ...context, providerId })
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -19,15 +19,20 @@ describe('Provider Schemas', () => {
|
|||||||
expect(Array.isArray(baseProviders)).toBe(true)
|
expect(Array.isArray(baseProviders)).toBe(true)
|
||||||
expect(baseProviders.length).toBeGreaterThan(0)
|
expect(baseProviders.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// These are the actual base providers defined in schemas.ts
|
||||||
const expectedIds = [
|
const expectedIds = [
|
||||||
'openai',
|
'openai',
|
||||||
'openai-responses',
|
'openai-chat',
|
||||||
'openai-compatible',
|
'openai-compatible',
|
||||||
'anthropic',
|
'anthropic',
|
||||||
'google',
|
'google',
|
||||||
'xai',
|
'xai',
|
||||||
'azure',
|
'azure',
|
||||||
'deepseek'
|
'azure-responses',
|
||||||
|
'deepseek',
|
||||||
|
'openrouter',
|
||||||
|
'cherryin',
|
||||||
|
'cherryin-chat'
|
||||||
]
|
]
|
||||||
const actualIds = baseProviders.map((p) => p.id)
|
const actualIds = baseProviders.map((p) => p.id)
|
||||||
expectedIds.forEach((id) => {
|
expectedIds.forEach((id) => {
|
||||||
|
|||||||
@ -232,11 +232,13 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
|
|
||||||
expect(pluginCallOrder).toEqual(['onRequestStart', 'transformParams', 'transformResult', 'onRequestEnd'])
|
expect(pluginCallOrder).toEqual(['onRequestStart', 'transformParams', 'transformResult', 'onRequestEnd'])
|
||||||
|
|
||||||
|
// transformParams receives params without model (model is handled separately)
|
||||||
|
// and context with core fields + dynamic fields (requestId, startTime, etc.)
|
||||||
expect(testPlugin.transformParams).toHaveBeenCalledWith(
|
expect(testPlugin.transformParams).toHaveBeenCalledWith(
|
||||||
{ prompt: 'A test image' },
|
expect.objectContaining({ prompt: 'A test image' }),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
providerId: 'openai',
|
providerId: 'openai',
|
||||||
modelId: 'dall-e-3'
|
model: 'dall-e-3'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -273,11 +275,12 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
|
|
||||||
await executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
await executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
||||||
|
|
||||||
|
// resolveModel receives model id and context with core fields
|
||||||
expect(modelResolutionPlugin.resolveModel).toHaveBeenCalledWith(
|
expect(modelResolutionPlugin.resolveModel).toHaveBeenCalledWith(
|
||||||
'dall-e-3',
|
'dall-e-3',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
providerId: 'openai',
|
providerId: 'openai',
|
||||||
modelId: 'dall-e-3'
|
model: 'dall-e-3'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -339,12 +342,11 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
.generateImage({ model: 'invalid-model', prompt: 'A test image' })
|
.generateImage({ model: 'invalid-model', prompt: 'A test image' })
|
||||||
.catch((error) => error)
|
.catch((error) => error)
|
||||||
|
|
||||||
expect(thrownError).toBeInstanceOf(ImageGenerationError)
|
// Error is thrown from pluginEngine directly as ImageModelResolutionError
|
||||||
expect(thrownError.message).toContain('Failed to generate image:')
|
expect(thrownError).toBeInstanceOf(ImageModelResolutionError)
|
||||||
|
expect(thrownError.message).toContain('Failed to resolve image model: invalid-model')
|
||||||
expect(thrownError.providerId).toBe('openai')
|
expect(thrownError.providerId).toBe('openai')
|
||||||
expect(thrownError.modelId).toBe('invalid-model')
|
expect(thrownError.modelId).toBe('invalid-model')
|
||||||
expect(thrownError.cause).toBeInstanceOf(ImageModelResolutionError)
|
|
||||||
expect(thrownError.cause.message).toContain('Failed to resolve image model: invalid-model')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle ImageModelResolutionError without provider', async () => {
|
it('should handle ImageModelResolutionError without provider', async () => {
|
||||||
@ -362,8 +364,9 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
const apiError = new Error('API request failed')
|
const apiError = new Error('API request failed')
|
||||||
vi.mocked(aiGenerateImage).mockRejectedValue(apiError)
|
vi.mocked(aiGenerateImage).mockRejectedValue(apiError)
|
||||||
|
|
||||||
|
// Error propagates directly from pluginEngine without wrapping
|
||||||
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
'Failed to generate image:'
|
'API request failed'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -376,8 +379,9 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
vi.mocked(aiGenerateImage).mockRejectedValue(noImageError)
|
vi.mocked(aiGenerateImage).mockRejectedValue(noImageError)
|
||||||
vi.mocked(NoImageGeneratedError.isInstance).mockReturnValue(true)
|
vi.mocked(NoImageGeneratedError.isInstance).mockReturnValue(true)
|
||||||
|
|
||||||
|
// Error propagates directly from pluginEngine
|
||||||
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
'Failed to generate image:'
|
'No image generated'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -398,15 +402,17 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
[errorPlugin]
|
[errorPlugin]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Error propagates directly from pluginEngine
|
||||||
await expect(executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
await expect(executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
'Failed to generate image:'
|
'Generation failed'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// onError receives the original error and context with core fields
|
||||||
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
||||||
error,
|
error,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
providerId: 'openai',
|
providerId: 'openai',
|
||||||
modelId: 'dall-e-3'
|
model: 'dall-e-3'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -419,9 +425,10 @@ describe('RuntimeExecutor.generateImage', () => {
|
|||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
setTimeout(() => abortController.abort(), 10)
|
setTimeout(() => abortController.abort(), 10)
|
||||||
|
|
||||||
|
// Error propagates directly from pluginEngine
|
||||||
await expect(
|
await expect(
|
||||||
executor.generateImage({ model: 'dall-e-3', prompt: 'A test image', abortSignal: abortController.signal })
|
executor.generateImage({ model: 'dall-e-3', prompt: 'A test image', abortSignal: abortController.signal })
|
||||||
).rejects.toThrow('Failed to generate image:')
|
).rejects.toThrow('Operation was aborted')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,14 @@ import type { AiPlugin } from '../../plugins'
|
|||||||
import { globalRegistryManagement } from '../../providers/RegistryManagement'
|
import { globalRegistryManagement } from '../../providers/RegistryManagement'
|
||||||
import { RuntimeExecutor } from '../executor'
|
import { RuntimeExecutor } from '../executor'
|
||||||
|
|
||||||
// Mock AI SDK
|
// Mock AI SDK - use importOriginal to keep jsonSchema and other non-mocked exports
|
||||||
vi.mock('ai', () => ({
|
vi.mock('ai', async (importOriginal) => {
|
||||||
generateText: vi.fn()
|
const actual = (await importOriginal()) as Record<string, unknown>
|
||||||
}))
|
return {
|
||||||
|
...actual,
|
||||||
|
generateText: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../../providers/RegistryManagement', () => ({
|
vi.mock('../../providers/RegistryManagement', () => ({
|
||||||
globalRegistryManagement: {
|
globalRegistryManagement: {
|
||||||
@ -409,11 +413,12 @@ describe('RuntimeExecutor.generateText', () => {
|
|||||||
})
|
})
|
||||||
).rejects.toThrow('Generation failed')
|
).rejects.toThrow('Generation failed')
|
||||||
|
|
||||||
|
// onError receives the original error and context with core fields
|
||||||
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
||||||
error,
|
error,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
providerId: 'openai',
|
providerId: 'openai',
|
||||||
modelId: 'gpt-4'
|
model: 'gpt-4'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -11,10 +11,14 @@ import type { AiPlugin } from '../../plugins'
|
|||||||
import { globalRegistryManagement } from '../../providers/RegistryManagement'
|
import { globalRegistryManagement } from '../../providers/RegistryManagement'
|
||||||
import { RuntimeExecutor } from '../executor'
|
import { RuntimeExecutor } from '../executor'
|
||||||
|
|
||||||
// Mock AI SDK
|
// Mock AI SDK - use importOriginal to keep jsonSchema and other non-mocked exports
|
||||||
vi.mock('ai', () => ({
|
vi.mock('ai', async (importOriginal) => {
|
||||||
streamText: vi.fn()
|
const actual = (await importOriginal()) as Record<string, unknown>
|
||||||
}))
|
return {
|
||||||
|
...actual,
|
||||||
|
streamText: vi.fn()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('../../providers/RegistryManagement', () => ({
|
vi.mock('../../providers/RegistryManagement', () => ({
|
||||||
globalRegistryManagement: {
|
globalRegistryManagement: {
|
||||||
@ -153,7 +157,7 @@ describe('RuntimeExecutor.streamText', () => {
|
|||||||
describe('Max Tokens Parameter', () => {
|
describe('Max Tokens Parameter', () => {
|
||||||
const maxTokensValues = [10, 50, 100, 500, 1000, 2000, 4000]
|
const maxTokensValues = [10, 50, 100, 500, 1000, 2000, 4000]
|
||||||
|
|
||||||
it.each(maxTokensValues)('should support maxTokens=%s', async (maxTokens) => {
|
it.each(maxTokensValues)('should support maxOutputTokens=%s', async (maxOutputTokens) => {
|
||||||
const mockStream = {
|
const mockStream = {
|
||||||
textStream: (async function* () {
|
textStream: (async function* () {
|
||||||
yield 'Response'
|
yield 'Response'
|
||||||
@ -168,12 +172,13 @@ describe('RuntimeExecutor.streamText', () => {
|
|||||||
await executor.streamText({
|
await executor.streamText({
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
messages: testMessages.simple,
|
messages: testMessages.simple,
|
||||||
maxOutputTokens: maxTokens
|
maxOutputTokens
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Parameters are passed through without transformation
|
||||||
expect(streamText).toHaveBeenCalledWith(
|
expect(streamText).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
maxTokens
|
maxOutputTokens
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -513,11 +518,12 @@ describe('RuntimeExecutor.streamText', () => {
|
|||||||
})
|
})
|
||||||
).rejects.toThrow('Stream error')
|
).rejects.toThrow('Stream error')
|
||||||
|
|
||||||
|
// onError receives the original error and context with core fields
|
||||||
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
||||||
error,
|
error,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
providerId: 'openai',
|
providerId: 'openai',
|
||||||
modelId: 'gpt-4'
|
model: 'gpt-4'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,20 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true
|
globals: true,
|
||||||
|
setupFiles: [path.resolve(__dirname, './src/__tests__/setup.ts')]
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': './src'
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
// Mock external packages that may not be available in test environment
|
||||||
|
'@cherrystudio/ai-sdk-provider': path.resolve(__dirname, './src/__tests__/mocks/ai-sdk-provider.ts')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
|
|||||||
@ -68,8 +68,8 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@tiptap/core": "^3.2.0",
|
"@tiptap/core": "3.2.0",
|
||||||
"@tiptap/pm": "^3.2.0",
|
"@tiptap/pm": "3.2.0",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
@ -89,5 +89,5 @@
|
|||||||
"build": "tsdown",
|
"build": "tsdown",
|
||||||
"lint": "biome format ./src/ --write && eslint --fix ./src/"
|
"lint": "biome format ./src/ --write && eslint --fix ./src/"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1"
|
"packageManager": "pnpm@10.27.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,8 @@ export enum IpcChannel {
|
|||||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||||
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
||||||
Webview_SearchHotkey = 'webview:search-hotkey',
|
Webview_SearchHotkey = 'webview:search-hotkey',
|
||||||
|
Webview_PrintToPDF = 'webview:print-to-pdf',
|
||||||
|
Webview_SaveAsHTML = 'webview:save-as-html',
|
||||||
|
|
||||||
// Open
|
// Open
|
||||||
Open_Path = 'open:path',
|
Open_Path = 'open:path',
|
||||||
@ -90,6 +92,8 @@ export enum IpcChannel {
|
|||||||
Mcp_AbortTool = 'mcp:abort-tool',
|
Mcp_AbortTool = 'mcp:abort-tool',
|
||||||
Mcp_GetServerVersion = 'mcp:get-server-version',
|
Mcp_GetServerVersion = 'mcp:get-server-version',
|
||||||
Mcp_Progress = 'mcp:progress',
|
Mcp_Progress = 'mcp:progress',
|
||||||
|
Mcp_GetServerLogs = 'mcp:get-server-logs',
|
||||||
|
Mcp_ServerLog = 'mcp:server-log',
|
||||||
// Python
|
// Python
|
||||||
Python_Execute = 'python:execute',
|
Python_Execute = 'python:execute',
|
||||||
|
|
||||||
@ -196,6 +200,9 @@ export enum IpcChannel {
|
|||||||
File_ValidateNotesDirectory = 'file:validateNotesDirectory',
|
File_ValidateNotesDirectory = 'file:validateNotesDirectory',
|
||||||
File_StartWatcher = 'file:startWatcher',
|
File_StartWatcher = 'file:startWatcher',
|
||||||
File_StopWatcher = 'file:stopWatcher',
|
File_StopWatcher = 'file:stopWatcher',
|
||||||
|
File_PauseWatcher = 'file:pauseWatcher',
|
||||||
|
File_ResumeWatcher = 'file:resumeWatcher',
|
||||||
|
File_BatchUploadMarkdown = 'file:batchUploadMarkdown',
|
||||||
File_ShowInFolder = 'file:showInFolder',
|
File_ShowInFolder = 'file:showInFolder',
|
||||||
|
|
||||||
// file service
|
// file service
|
||||||
@ -226,6 +233,8 @@ export enum IpcChannel {
|
|||||||
Backup_ListS3Files = 'backup:listS3Files',
|
Backup_ListS3Files = 'backup:listS3Files',
|
||||||
Backup_DeleteS3File = 'backup:deleteS3File',
|
Backup_DeleteS3File = 'backup:deleteS3File',
|
||||||
Backup_CheckS3Connection = 'backup:checkS3Connection',
|
Backup_CheckS3Connection = 'backup:checkS3Connection',
|
||||||
|
Backup_CreateLanTransferBackup = 'backup:createLanTransferBackup',
|
||||||
|
Backup_DeleteTempBackup = 'backup:deleteTempBackup',
|
||||||
|
|
||||||
// zip
|
// zip
|
||||||
Zip_Compress = 'zip:compress',
|
Zip_Compress = 'zip:compress',
|
||||||
@ -236,6 +245,9 @@ export enum IpcChannel {
|
|||||||
System_GetHostname = 'system:getHostname',
|
System_GetHostname = 'system:getHostname',
|
||||||
System_GetCpuName = 'system:getCpuName',
|
System_GetCpuName = 'system:getCpuName',
|
||||||
System_CheckGitBash = 'system:checkGitBash',
|
System_CheckGitBash = 'system:checkGitBash',
|
||||||
|
System_GetGitBashPath = 'system:getGitBashPath',
|
||||||
|
System_GetGitBashPathInfo = 'system:getGitBashPathInfo',
|
||||||
|
System_SetGitBashPath = 'system:setGitBashPath',
|
||||||
|
|
||||||
// DevTools
|
// DevTools
|
||||||
System_ToggleDevTools = 'system:toggleDevTools',
|
System_ToggleDevTools = 'system:toggleDevTools',
|
||||||
@ -290,6 +302,8 @@ export enum IpcChannel {
|
|||||||
Selection_ActionWindowClose = 'selection:action-window-close',
|
Selection_ActionWindowClose = 'selection:action-window-close',
|
||||||
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
||||||
Selection_ActionWindowPin = 'selection:action-window-pin',
|
Selection_ActionWindowPin = 'selection:action-window-pin',
|
||||||
|
// [Windows only] Electron bug workaround - can be removed once https://github.com/electron/electron/issues/48554 is fixed
|
||||||
|
Selection_ActionWindowResize = 'selection:action-window-resize',
|
||||||
Selection_ProcessAction = 'selection:process-action',
|
Selection_ProcessAction = 'selection:process-action',
|
||||||
Selection_UpdateActionData = 'selection:update-action-data',
|
Selection_UpdateActionData = 'selection:update-action-data',
|
||||||
|
|
||||||
@ -304,6 +318,7 @@ export enum IpcChannel {
|
|||||||
Memory_DeleteUser = 'memory:delete-user',
|
Memory_DeleteUser = 'memory:delete-user',
|
||||||
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
|
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
|
||||||
Memory_GetUsersList = 'memory:get-users-list',
|
Memory_GetUsersList = 'memory:get-users-list',
|
||||||
|
Memory_MigrateMemoryDb = 'memory:migrate-memory-db',
|
||||||
|
|
||||||
// TRACE
|
// TRACE
|
||||||
TRACE_SAVE_DATA = 'trace:saveData',
|
TRACE_SAVE_DATA = 'trace:saveData',
|
||||||
@ -349,6 +364,7 @@ export enum IpcChannel {
|
|||||||
OCR_ListProviders = 'ocr:list-providers',
|
OCR_ListProviders = 'ocr:list-providers',
|
||||||
|
|
||||||
// OVMS
|
// OVMS
|
||||||
|
Ovms_IsSupported = 'ovms:is-supported',
|
||||||
Ovms_AddModel = 'ovms:add-model',
|
Ovms_AddModel = 'ovms:add-model',
|
||||||
Ovms_StopAddModel = 'ovms:stop-addmodel',
|
Ovms_StopAddModel = 'ovms:stop-addmodel',
|
||||||
Ovms_GetModels = 'ovms:get-models',
|
Ovms_GetModels = 'ovms:get-models',
|
||||||
@ -369,10 +385,14 @@ export enum IpcChannel {
|
|||||||
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
ClaudeCodePlugin_ReadContent = 'claudeCodePlugin:read-content',
|
||||||
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content',
|
ClaudeCodePlugin_WriteContent = 'claudeCodePlugin:write-content',
|
||||||
|
|
||||||
// WebSocket
|
// Local Transfer
|
||||||
WebSocket_Start = 'webSocket:start',
|
LocalTransfer_ListServices = 'local-transfer:list',
|
||||||
WebSocket_Stop = 'webSocket:stop',
|
LocalTransfer_StartScan = 'local-transfer:start-scan',
|
||||||
WebSocket_Status = 'webSocket:status',
|
LocalTransfer_StopScan = 'local-transfer:stop-scan',
|
||||||
WebSocket_SendFile = 'webSocket:send-file',
|
LocalTransfer_ServicesUpdated = 'local-transfer:services-updated',
|
||||||
WebSocket_GetAllCandidates = 'webSocket:get-all-candidates'
|
LocalTransfer_Connect = 'local-transfer:connect',
|
||||||
|
LocalTransfer_Disconnect = 'local-transfer:disconnect',
|
||||||
|
LocalTransfer_ClientEvent = 'local-transfer:client-event',
|
||||||
|
LocalTransfer_SendFile = 'local-transfer:send-file',
|
||||||
|
LocalTransfer_CancelTransfer = 'local-transfer:cancel-transfer'
|
||||||
}
|
}
|
||||||
|
|||||||
138
packages/shared/__tests__/utils.test.ts
Normal file
138
packages/shared/__tests__/utils.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { isBase64ImageDataUrl, isDataUrl, parseDataUrl } from '../utils'
|
||||||
|
|
||||||
|
describe('parseDataUrl', () => {
|
||||||
|
it('parses a standard base64 image data URL', () => {
|
||||||
|
const result = parseDataUrl('data:image/png;base64,iVBORw0KGgo=')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: 'image/png',
|
||||||
|
isBase64: true,
|
||||||
|
data: 'iVBORw0KGgo='
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses a base64 data URL with additional parameters', () => {
|
||||||
|
const result = parseDataUrl('data:image/jpeg;name=foo;base64,/9j/4AAQ')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: 'image/jpeg',
|
||||||
|
isBase64: true,
|
||||||
|
data: '/9j/4AAQ'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses a plain text data URL (non-base64)', () => {
|
||||||
|
const result = parseDataUrl('data:text/plain,Hello%20World')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: 'text/plain',
|
||||||
|
isBase64: false,
|
||||||
|
data: 'Hello%20World'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses a data URL with empty media type', () => {
|
||||||
|
const result = parseDataUrl('data:;base64,SGVsbG8=')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: undefined,
|
||||||
|
isBase64: true,
|
||||||
|
data: 'SGVsbG8='
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null for non-data URLs', () => {
|
||||||
|
const result = parseDataUrl('https://example.com/image.png')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null for malformed data URL without comma', () => {
|
||||||
|
const result = parseDataUrl('data:image/png;base64')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
const result = parseDataUrl('')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles large base64 data without performance issues', () => {
|
||||||
|
// Simulate a 4K image base64 string (about 1MB)
|
||||||
|
const largeData = 'A'.repeat(1024 * 1024)
|
||||||
|
const dataUrl = `data:image/png;base64,${largeData}`
|
||||||
|
|
||||||
|
const start = performance.now()
|
||||||
|
const result = parseDataUrl(dataUrl)
|
||||||
|
const duration = performance.now() - start
|
||||||
|
|
||||||
|
expect(result).not.toBeNull()
|
||||||
|
expect(result?.mediaType).toBe('image/png')
|
||||||
|
expect(result?.isBase64).toBe(true)
|
||||||
|
expect(result?.data).toBe(largeData)
|
||||||
|
// Should complete in under 10ms (string operations are fast)
|
||||||
|
expect(duration).toBeLessThan(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses SVG data URL', () => {
|
||||||
|
const result = parseDataUrl('data:image/svg+xml;base64,PHN2Zz4=')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: 'image/svg+xml',
|
||||||
|
isBase64: true,
|
||||||
|
data: 'PHN2Zz4='
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses JSON data URL', () => {
|
||||||
|
const result = parseDataUrl('data:application/json,{"key":"value"}')
|
||||||
|
expect(result).toEqual({
|
||||||
|
mediaType: 'application/json',
|
||||||
|
isBase64: false,
|
||||||
|
data: '{"key":"value"}'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isDataUrl', () => {
|
||||||
|
it('returns true for valid data URLs', () => {
|
||||||
|
expect(isDataUrl('data:image/png;base64,ABC')).toBe(true)
|
||||||
|
expect(isDataUrl('data:text/plain,hello')).toBe(true)
|
||||||
|
expect(isDataUrl('data:,simple')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for non-data URLs', () => {
|
||||||
|
expect(isDataUrl('https://example.com')).toBe(false)
|
||||||
|
expect(isDataUrl('file:///path/to/file')).toBe(false)
|
||||||
|
expect(isDataUrl('')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for malformed data URLs', () => {
|
||||||
|
expect(isDataUrl('data:')).toBe(false)
|
||||||
|
expect(isDataUrl('data:image/png')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isBase64ImageDataUrl', () => {
|
||||||
|
it('returns true for base64 image data URLs', () => {
|
||||||
|
expect(isBase64ImageDataUrl('data:image/png;base64,ABC')).toBe(true)
|
||||||
|
expect(isBase64ImageDataUrl('data:image/jpeg;base64,/9j/')).toBe(true)
|
||||||
|
expect(isBase64ImageDataUrl('data:image/gif;base64,R0lG')).toBe(true)
|
||||||
|
expect(isBase64ImageDataUrl('data:image/webp;base64,UklG')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for non-base64 image data URLs', () => {
|
||||||
|
expect(isBase64ImageDataUrl('data:image/svg+xml,<svg></svg>')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for non-image data URLs', () => {
|
||||||
|
expect(isBase64ImageDataUrl('data:text/plain;base64,SGVsbG8=')).toBe(false)
|
||||||
|
expect(isBase64ImageDataUrl('data:application/json,{}')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for regular URLs', () => {
|
||||||
|
expect(isBase64ImageDataUrl('https://example.com/image.png')).toBe(false)
|
||||||
|
expect(isBase64ImageDataUrl('file:///image.png')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false for malformed data URLs', () => {
|
||||||
|
expect(isBase64ImageDataUrl('data:image/png')).toBe(false)
|
||||||
|
expect(isBase64ImageDataUrl('')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -88,16 +88,11 @@ export function getSdkClient(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let baseURL =
|
const baseURL =
|
||||||
provider.type === 'anthropic'
|
provider.type === 'anthropic'
|
||||||
? provider.apiHost
|
? provider.apiHost
|
||||||
: (provider.anthropicApiHost && provider.anthropicApiHost.trim()) || provider.apiHost
|
: (provider.anthropicApiHost && provider.anthropicApiHost.trim()) || provider.apiHost
|
||||||
|
|
||||||
// Anthropic SDK automatically appends /v1 to all endpoints (like /v1/messages, /v1/models)
|
|
||||||
// We need to strip api version from baseURL to avoid duplication (e.g., /v3/v1/models)
|
|
||||||
// formatProviderApiHost adds /v1 for AI SDK compatibility, but Anthropic SDK needs it removed
|
|
||||||
baseURL = baseURL.replace(/\/v\d+(?:alpha|beta)?(?=\/|$)/i, '')
|
|
||||||
|
|
||||||
logger.debug('Anthropic API baseURL', { baseURL, providerId: provider.id })
|
logger.debug('Anthropic API baseURL', { baseURL, providerId: provider.id })
|
||||||
|
|
||||||
if (provider.id === 'aihubmix') {
|
if (provider.id === 'aihubmix') {
|
||||||
|
|||||||
@ -7,6 +7,11 @@ export const documentExts = ['.pdf', '.doc', '.docx', '.pptx', '.xlsx', '.odt',
|
|||||||
export const thirdPartyApplicationExts = ['.draftsExport']
|
export const thirdPartyApplicationExts = ['.draftsExport']
|
||||||
export const bookExts = ['.epub']
|
export const bookExts = ['.epub']
|
||||||
|
|
||||||
|
export const API_SERVER_DEFAULTS = {
|
||||||
|
HOST: '127.0.0.1',
|
||||||
|
PORT: 23333
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flat array of all file extensions known by the linguist database.
|
* A flat array of all file extensions known by the linguist database.
|
||||||
* This is the primary source for identifying code files.
|
* This is the primary source for identifying code files.
|
||||||
@ -483,3 +488,11 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
|||||||
|
|
||||||
// resources/scripts should be maintained manually
|
// resources/scripts should be maintained manually
|
||||||
export const HOME_CHERRY_DIR = '.cherrystudio'
|
export const HOME_CHERRY_DIR = '.cherrystudio'
|
||||||
|
|
||||||
|
// Git Bash path configuration types
|
||||||
|
export type GitBashPathSource = 'manual' | 'auto'
|
||||||
|
|
||||||
|
export interface GitBashPathInfo {
|
||||||
|
path: string | null
|
||||||
|
source: GitBashPathSource | null
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*
|
*
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
||||||
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
|
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
|
||||||
* Run `yarn update:languages` to update this file.
|
* Run `pnpm update:languages` to update this file.
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export type LoaderReturn = {
|
|||||||
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir' | 'refresh'
|
||||||
|
|
||||||
export type FileChangeEvent = {
|
export type FileChangeEvent = {
|
||||||
eventType: FileChangeEventType
|
eventType: FileChangeEventType
|
||||||
@ -23,6 +23,14 @@ export type MCPProgressEvent = {
|
|||||||
progress: number // 0-1 range
|
progress: number // 0-1 range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MCPServerLogEntry = {
|
||||||
|
timestamp: number
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error' | 'stderr' | 'stdout'
|
||||||
|
message: string
|
||||||
|
data?: any
|
||||||
|
source?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type WebviewKeyEvent = {
|
export type WebviewKeyEvent = {
|
||||||
webviewId: number
|
webviewId: number
|
||||||
key: string
|
key: string
|
||||||
@ -44,3 +52,196 @@ export interface WebSocketCandidatesResponse {
|
|||||||
interface: string
|
interface: string
|
||||||
priority: number
|
priority: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LocalTransferPeer = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
host?: string
|
||||||
|
fqdn?: string
|
||||||
|
port?: number
|
||||||
|
type?: string
|
||||||
|
protocol?: 'tcp' | 'udp'
|
||||||
|
addresses: string[]
|
||||||
|
txt?: Record<string, string>
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocalTransferState = {
|
||||||
|
services: LocalTransferPeer[]
|
||||||
|
isScanning: boolean
|
||||||
|
lastScanStartedAt?: number
|
||||||
|
lastUpdatedAt: number
|
||||||
|
lastError?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanHandshakeRequestMessage = {
|
||||||
|
type: 'handshake'
|
||||||
|
deviceName: string
|
||||||
|
version: string
|
||||||
|
platform?: string
|
||||||
|
appVersion?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanHandshakeAckMessage = {
|
||||||
|
type: 'handshake_ack'
|
||||||
|
accepted: boolean
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LocalTransferConnectPayload = {
|
||||||
|
peerId: string
|
||||||
|
metadata?: Record<string, string>
|
||||||
|
timeoutMs?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LanClientEvent =
|
||||||
|
| {
|
||||||
|
type: 'ping_sent'
|
||||||
|
payload: string
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'pong'
|
||||||
|
payload?: string
|
||||||
|
received?: boolean
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'socket_closed'
|
||||||
|
reason?: string
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'error'
|
||||||
|
message: string
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'file_transfer_progress'
|
||||||
|
transferId: string
|
||||||
|
fileName: string
|
||||||
|
bytesSent: number
|
||||||
|
totalBytes: number
|
||||||
|
chunkIndex: number
|
||||||
|
totalChunks: number
|
||||||
|
progress: number // 0-100
|
||||||
|
speed: number // bytes/sec
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'file_transfer_complete'
|
||||||
|
transferId: string
|
||||||
|
fileName: string
|
||||||
|
success: boolean
|
||||||
|
filePath?: string
|
||||||
|
error?: string
|
||||||
|
timestamp: number
|
||||||
|
peerId?: string
|
||||||
|
peerName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LAN File Transfer Protocol Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Constants for file transfer
|
||||||
|
export const LAN_TRANSFER_TCP_PORT = 53317
|
||||||
|
export const LAN_TRANSFER_CHUNK_SIZE = 512 * 1024 // 512KB
|
||||||
|
export const LAN_TRANSFER_MAX_FILE_SIZE = 500 * 1024 * 1024 // 500MB
|
||||||
|
export const LAN_TRANSFER_COMPLETE_TIMEOUT_MS = 60_000 // 60s - wait for file_complete after file_end
|
||||||
|
export const LAN_TRANSFER_GLOBAL_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes - global transfer timeout
|
||||||
|
|
||||||
|
// Binary protocol constants (v1)
|
||||||
|
export const LAN_TRANSFER_PROTOCOL_VERSION = '1'
|
||||||
|
export const LAN_BINARY_FRAME_MAGIC = 0x4353 // "CS" as uint16
|
||||||
|
export const LAN_BINARY_TYPE_FILE_CHUNK = 0x01
|
||||||
|
|
||||||
|
// Messages from Electron (Client/Sender) to Mobile (Server/Receiver)
|
||||||
|
|
||||||
|
/** Request to start file transfer */
|
||||||
|
export type LanFileStartMessage = {
|
||||||
|
type: 'file_start'
|
||||||
|
transferId: string
|
||||||
|
fileName: string
|
||||||
|
fileSize: number
|
||||||
|
mimeType: string // 'application/zip'
|
||||||
|
checksum: string // SHA-256 of entire file
|
||||||
|
totalChunks: number
|
||||||
|
chunkSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File chunk data (JSON format)
|
||||||
|
* @deprecated Use binary frame format in protocol v1. This type is kept for reference only.
|
||||||
|
*/
|
||||||
|
export type LanFileChunkMessage = {
|
||||||
|
type: 'file_chunk'
|
||||||
|
transferId: string
|
||||||
|
chunkIndex: number
|
||||||
|
data: string // Base64 encoded
|
||||||
|
chunkChecksum: string // SHA-256 of this chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notification that all chunks have been sent */
|
||||||
|
export type LanFileEndMessage = {
|
||||||
|
type: 'file_end'
|
||||||
|
transferId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Request to cancel file transfer */
|
||||||
|
export type LanFileCancelMessage = {
|
||||||
|
type: 'file_cancel'
|
||||||
|
transferId: string
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages from Mobile (Server/Receiver) to Electron (Client/Sender)
|
||||||
|
|
||||||
|
/** Acknowledgment of file transfer request */
|
||||||
|
export type LanFileStartAckMessage = {
|
||||||
|
type: 'file_start_ack'
|
||||||
|
transferId: string
|
||||||
|
accepted: boolean
|
||||||
|
message?: string // Rejection reason
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acknowledgment of file chunk received
|
||||||
|
* @deprecated Protocol v1 uses streaming mode without per-chunk acknowledgment.
|
||||||
|
* This type is kept for backward compatibility reference only.
|
||||||
|
*/
|
||||||
|
export type LanFileChunkAckMessage = {
|
||||||
|
type: 'file_chunk_ack'
|
||||||
|
transferId: string
|
||||||
|
chunkIndex: number
|
||||||
|
received: boolean
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Final result of file transfer */
|
||||||
|
export type LanFileCompleteMessage = {
|
||||||
|
type: 'file_complete'
|
||||||
|
transferId: string
|
||||||
|
success: boolean
|
||||||
|
filePath?: string // Path where file was saved on mobile
|
||||||
|
error?: string
|
||||||
|
// Enhanced error diagnostics
|
||||||
|
errorCode?: 'CHECKSUM_MISMATCH' | 'INCOMPLETE_TRANSFER' | 'DISK_ERROR' | 'CANCELLED'
|
||||||
|
receivedChunks?: number
|
||||||
|
receivedBytes?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Payload for sending a file via IPC */
|
||||||
|
export type LanFileSendPayload = {
|
||||||
|
filePath: string
|
||||||
|
}
|
||||||
|
|||||||
116
packages/shared/mcp.ts
Normal file
116
packages/shared/mcp.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Convert a string to camelCase, ensuring it's a valid JavaScript identifier.
|
||||||
|
*
|
||||||
|
* - Normalizes to lowercase first, then capitalizes word boundaries
|
||||||
|
* - Non-alphanumeric characters are treated as word separators
|
||||||
|
* - Non-ASCII characters are dropped (ASCII-only output)
|
||||||
|
* - If result starts with a digit, prefixes with underscore
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toCamelCase('my-server') // 'myServer'
|
||||||
|
* toCamelCase('MY_SERVER') // 'myServer'
|
||||||
|
* toCamelCase('123tool') // '_123tool'
|
||||||
|
*/
|
||||||
|
export function toCamelCase(str: string): string {
|
||||||
|
let result = str
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+(.)/g, (_, char) => char.toUpperCase())
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '')
|
||||||
|
|
||||||
|
if (result && !/^[a-zA-Z_]/.test(result)) {
|
||||||
|
result = '_' + result
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export type McpToolNameOptions = {
|
||||||
|
/** Prefix added before the name (e.g., 'mcp__'). Must be JS-identifier-safe. */
|
||||||
|
prefix?: string
|
||||||
|
/** Delimiter between server and tool parts (e.g., '_' or '__'). Must be JS-identifier-safe. */
|
||||||
|
delimiter?: string
|
||||||
|
/** Maximum length of the final name. Suffix numbers for uniqueness are included in this limit. */
|
||||||
|
maxLength?: number
|
||||||
|
/** Mutable Set for collision detection. The final name will be added to this Set. */
|
||||||
|
existingNames?: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a valid JavaScript function name from server and tool names.
|
||||||
|
* Uses camelCase for both parts.
|
||||||
|
*
|
||||||
|
* @param serverName - The MCP server name (optional)
|
||||||
|
* @param toolName - The tool name
|
||||||
|
* @param options - Configuration options
|
||||||
|
* @returns A valid JS identifier
|
||||||
|
*/
|
||||||
|
export function buildMcpToolName(
|
||||||
|
serverName: string | undefined,
|
||||||
|
toolName: string,
|
||||||
|
options: McpToolNameOptions = {}
|
||||||
|
): string {
|
||||||
|
const { prefix = '', delimiter = '_', maxLength, existingNames } = options
|
||||||
|
|
||||||
|
const serverPart = serverName ? toCamelCase(serverName) : ''
|
||||||
|
const toolPart = toCamelCase(toolName)
|
||||||
|
const baseName = serverPart ? `${prefix}${serverPart}${delimiter}${toolPart}` : `${prefix}${toolPart}`
|
||||||
|
|
||||||
|
if (!existingNames) {
|
||||||
|
return maxLength ? truncateToLength(baseName, maxLength) : baseName
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = maxLength ? truncateToLength(baseName, maxLength) : baseName
|
||||||
|
let counter = 1
|
||||||
|
|
||||||
|
while (existingNames.has(name)) {
|
||||||
|
const suffix = String(counter)
|
||||||
|
const truncatedBase = maxLength ? truncateToLength(baseName, maxLength - suffix.length) : baseName
|
||||||
|
name = `${truncatedBase}${suffix}`
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
existingNames.add(name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateToLength(str: string, maxLength: number): string {
|
||||||
|
if (str.length <= maxLength) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return str.slice(0, maxLength).replace(/_+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique function name from server name and tool name.
|
||||||
|
* Format: serverName_toolName (camelCase)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* generateMcpToolFunctionName('github', 'search_issues') // 'github_searchIssues'
|
||||||
|
*/
|
||||||
|
export function generateMcpToolFunctionName(
|
||||||
|
serverName: string | undefined,
|
||||||
|
toolName: string,
|
||||||
|
existingNames?: Set<string>
|
||||||
|
): string {
|
||||||
|
return buildMcpToolName(serverName, toolName, { existingNames })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a valid JavaScript function name for MCP tool calls.
|
||||||
|
* Format: mcp__{serverName}__{toolName}
|
||||||
|
*
|
||||||
|
* @param serverName - The MCP server name
|
||||||
|
* @param toolName - The tool name from the server
|
||||||
|
* @returns A valid JS identifier in format mcp__{server}__{tool}, max 63 chars
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* buildFunctionCallToolName('github', 'search_issues') // 'mcp__github__searchIssues'
|
||||||
|
*/
|
||||||
|
export function buildFunctionCallToolName(serverName: string, toolName: string): string {
|
||||||
|
return buildMcpToolName(serverName, toolName, {
|
||||||
|
prefix: 'mcp__',
|
||||||
|
delimiter: '__',
|
||||||
|
maxLength: 63
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -35,3 +35,134 @@ export const defaultAppHeaders = () => {
|
|||||||
// return value
|
// return value
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the trailing API version segment from a URL path.
|
||||||
|
*
|
||||||
|
* This function extracts API version patterns (e.g., `v1`, `v2beta`) from the end of a URL.
|
||||||
|
* Only versions at the end of the path are extracted, not versions in the middle.
|
||||||
|
* The returned version string does not include leading or trailing slashes.
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL string to parse.
|
||||||
|
* @returns {string | undefined} The trailing API version found (e.g., 'v1', 'v2beta'), or undefined if none found.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* getTrailingApiVersion('https://api.example.com/v1') // 'v1'
|
||||||
|
* getTrailingApiVersion('https://api.example.com/v2beta/') // 'v2beta'
|
||||||
|
* getTrailingApiVersion('https://api.example.com/v1/chat') // undefined (version not at end)
|
||||||
|
* getTrailingApiVersion('https://gateway.ai.cloudflare.com/v1/xxx/v1beta') // 'v1beta'
|
||||||
|
* getTrailingApiVersion('https://api.example.com') // undefined
|
||||||
|
*/
|
||||||
|
export function getTrailingApiVersion(url: string): string | undefined {
|
||||||
|
const match = url.match(TRAILING_VERSION_REGEX)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// Extract version without leading slash and trailing slash
|
||||||
|
return match[0].replace(/^\//, '').replace(/\/$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches an API version at the end of a URL (with optional trailing slash).
|
||||||
|
* Used to detect and extract versions only from the trailing position.
|
||||||
|
*/
|
||||||
|
const TRAILING_VERSION_REGEX = /\/v\d+(?:alpha|beta)?\/?$/i
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the trailing API version segment from a URL path.
|
||||||
|
*
|
||||||
|
* This function removes API version patterns (e.g., `/v1`, `/v2beta`) from the end of a URL.
|
||||||
|
* Only versions at the end of the path are removed, not versions in the middle.
|
||||||
|
*
|
||||||
|
* @param {string} url - The URL string to process.
|
||||||
|
* @returns {string} The URL with the trailing API version removed, or the original URL if no trailing version found.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* withoutTrailingApiVersion('https://api.example.com/v1') // 'https://api.example.com'
|
||||||
|
* withoutTrailingApiVersion('https://api.example.com/v2beta/') // 'https://api.example.com'
|
||||||
|
* withoutTrailingApiVersion('https://api.example.com/v1/chat') // 'https://api.example.com/v1/chat' (no change)
|
||||||
|
* withoutTrailingApiVersion('https://api.example.com') // 'https://api.example.com'
|
||||||
|
*/
|
||||||
|
export function withoutTrailingApiVersion(url: string): string {
|
||||||
|
return url.replace(TRAILING_VERSION_REGEX, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataUrlParts {
|
||||||
|
/** The media type (e.g., 'image/png', 'text/plain') */
|
||||||
|
mediaType?: string
|
||||||
|
/** Whether the data is base64 encoded */
|
||||||
|
isBase64: boolean
|
||||||
|
/** The data portion (everything after the comma). This is the raw string, not decoded. */
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a data URL into its component parts without using regex on the data portion.
|
||||||
|
* This is memory-safe for large data URLs (e.g., 4K images) as it uses indexOf instead of regex.
|
||||||
|
*
|
||||||
|
* Data URL format: data:[<mediatype>][;base64],<data>
|
||||||
|
*
|
||||||
|
* @param url - The data URL string to parse
|
||||||
|
* @returns DataUrlParts if valid, null if invalid
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseDataUrl('data:image/png;base64,iVBORw0KGgo...')
|
||||||
|
* // { mediaType: 'image/png', isBase64: true, data: 'iVBORw0KGgo...' }
|
||||||
|
*
|
||||||
|
* parseDataUrl('data:text/plain,Hello')
|
||||||
|
* // { mediaType: 'text/plain', isBase64: false, data: 'Hello' }
|
||||||
|
*
|
||||||
|
* parseDataUrl('invalid-url')
|
||||||
|
* // null
|
||||||
|
*/
|
||||||
|
export function parseDataUrl(url: string): DataUrlParts | null {
|
||||||
|
if (!url.startsWith('data:')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const commaIndex = url.indexOf(',')
|
||||||
|
if (commaIndex === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = url.slice(5, commaIndex)
|
||||||
|
|
||||||
|
const isBase64 = header.includes(';base64')
|
||||||
|
|
||||||
|
const semicolonIndex = header.indexOf(';')
|
||||||
|
const mediaType = (semicolonIndex === -1 ? header : header.slice(0, semicolonIndex)).trim() || undefined
|
||||||
|
|
||||||
|
const data = url.slice(commaIndex + 1)
|
||||||
|
|
||||||
|
return { mediaType, isBase64, data }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a data URL.
|
||||||
|
*
|
||||||
|
* @param url - The string to check
|
||||||
|
* @returns true if the string is a valid data URL
|
||||||
|
*/
|
||||||
|
export function isDataUrl(url: string): boolean {
|
||||||
|
return url.startsWith('data:') && url.includes(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a data URL contains base64-encoded image data.
|
||||||
|
*
|
||||||
|
* @param url - The data URL to check
|
||||||
|
* @returns true if the URL is a base64-encoded image data URL
|
||||||
|
*/
|
||||||
|
export function isBase64ImageDataUrl(url: string): boolean {
|
||||||
|
if (!url.startsWith('data:image/')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const commaIndex = url.indexOf(',')
|
||||||
|
if (commaIndex === -1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const header = url.slice(5, commaIndex)
|
||||||
|
return header.includes(';base64')
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
diff --git a/dist/index.js b/dist/index.js
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
index 51ce7e423934fb717cb90245cdfcdb3dae6780e6..0f7f7009e2f41a79a8669d38c8a44867bbff5e1f 100644
|
index d004b415c5841a1969705823614f395265ea5a8a..6b1e0dad4610b0424393ecc12e9114723bbe316b 100644
|
||||||
--- a/dist/index.js
|
--- a/dist/index.js
|
||||||
+++ b/dist/index.js
|
+++ b/dist/index.js
|
||||||
@@ -474,7 +474,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
@@ -474,7 +474,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||||
@ -12,7 +12,7 @@ index 51ce7e423934fb717cb90245cdfcdb3dae6780e6..0f7f7009e2f41a79a8669d38c8a44867
|
|||||||
|
|
||||||
// src/google-generative-ai-options.ts
|
// src/google-generative-ai-options.ts
|
||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
index f4b77e35c0cbfece85a3ef0d4f4e67aa6dde6271..8d2fecf8155a226006a0bde72b00b6036d4014b6 100644
|
index 1780dd2391b7f42224a0b8048c723d2f81222c44..1f12ed14399d6902107ce9b435d7d8e6cc61e06b 100644
|
||||||
--- a/dist/index.mjs
|
--- a/dist/index.mjs
|
||||||
+++ b/dist/index.mjs
|
+++ b/dist/index.mjs
|
||||||
@@ -480,7 +480,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
@@ -480,7 +480,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
|
||||||
@ -24,3 +24,14 @@ index f4b77e35c0cbfece85a3ef0d4f4e67aa6dde6271..8d2fecf8155a226006a0bde72b00b603
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/google-generative-ai-options.ts
|
// src/google-generative-ai-options.ts
|
||||||
|
@@ -1909,8 +1909,7 @@ function createGoogleGenerativeAI(options = {}) {
|
||||||
|
}
|
||||||
|
var google = createGoogleGenerativeAI();
|
||||||
|
export {
|
||||||
|
- VERSION,
|
||||||
|
createGoogleGenerativeAI,
|
||||||
|
- google
|
||||||
|
+ google, VERSION
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=index.mjs.map
|
||||||
|
\ No newline at end of file
|
||||||
@ -1,8 +1,8 @@
|
|||||||
diff --git a/dist/index.js b/dist/index.js
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a70ea2b5a2 100644
|
index 130094d194ea1e8e7d3027d07d82465741192124..4d13dcee8c962ca9ee8f1c3d748f8ffe6a3cfb47 100644
|
||||||
--- a/dist/index.js
|
--- a/dist/index.js
|
||||||
+++ b/dist/index.js
|
+++ b/dist/index.js
|
||||||
@@ -274,6 +274,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
|
@@ -290,6 +290,7 @@ var openaiChatResponseSchema = (0, import_provider_utils3.lazyValidator)(
|
||||||
message: import_v42.z.object({
|
message: import_v42.z.object({
|
||||||
role: import_v42.z.literal("assistant").nullish(),
|
role: import_v42.z.literal("assistant").nullish(),
|
||||||
content: import_v42.z.string().nullish(),
|
content: import_v42.z.string().nullish(),
|
||||||
@ -10,7 +10,7 @@ index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a7
|
|||||||
tool_calls: import_v42.z.array(
|
tool_calls: import_v42.z.array(
|
||||||
import_v42.z.object({
|
import_v42.z.object({
|
||||||
id: import_v42.z.string().nullish(),
|
id: import_v42.z.string().nullish(),
|
||||||
@@ -340,6 +341,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
|
@@ -356,6 +357,7 @@ var openaiChatChunkSchema = (0, import_provider_utils3.lazyValidator)(
|
||||||
delta: import_v42.z.object({
|
delta: import_v42.z.object({
|
||||||
role: import_v42.z.enum(["assistant"]).nullish(),
|
role: import_v42.z.enum(["assistant"]).nullish(),
|
||||||
content: import_v42.z.string().nullish(),
|
content: import_v42.z.string().nullish(),
|
||||||
@ -18,7 +18,7 @@ index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a7
|
|||||||
tool_calls: import_v42.z.array(
|
tool_calls: import_v42.z.array(
|
||||||
import_v42.z.object({
|
import_v42.z.object({
|
||||||
index: import_v42.z.number(),
|
index: import_v42.z.number(),
|
||||||
@@ -795,6 +797,13 @@ var OpenAIChatLanguageModel = class {
|
@@ -814,6 +816,13 @@ var OpenAIChatLanguageModel = class {
|
||||||
if (text != null && text.length > 0) {
|
if (text != null && text.length > 0) {
|
||||||
content.push({ type: "text", text });
|
content.push({ type: "text", text });
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a7
|
|||||||
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
|
for (const toolCall of (_a = choice.message.tool_calls) != null ? _a : []) {
|
||||||
content.push({
|
content.push({
|
||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
@@ -876,6 +885,7 @@ var OpenAIChatLanguageModel = class {
|
@@ -895,6 +904,7 @@ var OpenAIChatLanguageModel = class {
|
||||||
};
|
};
|
||||||
let metadataExtracted = false;
|
let metadataExtracted = false;
|
||||||
let isActiveText = false;
|
let isActiveText = false;
|
||||||
@ -40,7 +40,7 @@ index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a7
|
|||||||
const providerMetadata = { openai: {} };
|
const providerMetadata = { openai: {} };
|
||||||
return {
|
return {
|
||||||
stream: response.pipeThrough(
|
stream: response.pipeThrough(
|
||||||
@@ -933,6 +943,21 @@ var OpenAIChatLanguageModel = class {
|
@@ -952,6 +962,21 @@ var OpenAIChatLanguageModel = class {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const delta = choice.delta;
|
const delta = choice.delta;
|
||||||
@ -62,7 +62,7 @@ index bf900591bf2847a3253fe441aad24c06da19c6c1..c1d9bb6fefa2df1383339324073db0a7
|
|||||||
if (delta.content != null) {
|
if (delta.content != null) {
|
||||||
if (!isActiveText) {
|
if (!isActiveText) {
|
||||||
controller.enqueue({ type: "text-start", id: "0" });
|
controller.enqueue({ type: "text-start", id: "0" });
|
||||||
@@ -1045,6 +1070,9 @@ var OpenAIChatLanguageModel = class {
|
@@ -1064,6 +1089,9 @@ var OpenAIChatLanguageModel = class {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flush(controller) {
|
flush(controller) {
|
||||||
308
patches/@ai-sdk__openai-compatible@1.0.28.patch
Normal file
308
patches/@ai-sdk__openai-compatible@1.0.28.patch
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
diff --git a/dist/index.d.ts b/dist/index.d.ts
|
||||||
|
index 48e2f6263c6ee4c75d7e5c28733e64f6ebe92200..00d0729c4a3cbf9a48e8e1e962c7e2b256b75eba 100644
|
||||||
|
--- a/dist/index.d.ts
|
||||||
|
+++ b/dist/index.d.ts
|
||||||
|
@@ -7,6 +7,7 @@ declare const openaiCompatibleProviderOptions: z.ZodObject<{
|
||||||
|
user: z.ZodOptional<z.ZodString>;
|
||||||
|
reasoningEffort: z.ZodOptional<z.ZodString>;
|
||||||
|
textVerbosity: z.ZodOptional<z.ZodString>;
|
||||||
|
+ sendReasoning: z.ZodOptional<z.ZodBoolean>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
type OpenAICompatibleProviderOptions = z.infer<typeof openaiCompatibleProviderOptions>;
|
||||||
|
|
||||||
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
|
index da237bb35b7fa8e24b37cd861ee73dfc51cdfc72..88349c614a69a268a2e4f3b157cb5e328ca1d347 100644
|
||||||
|
--- a/dist/index.js
|
||||||
|
+++ b/dist/index.js
|
||||||
|
@@ -41,7 +41,7 @@ function getOpenAIMetadata(message) {
|
||||||
|
var _a, _b;
|
||||||
|
return (_b = (_a = message == null ? void 0 : message.providerOptions) == null ? void 0 : _a.openaiCompatible) != null ? _b : {};
|
||||||
|
}
|
||||||
|
-function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
+function convertToOpenAICompatibleChatMessages({prompt, options}) {
|
||||||
|
const messages = [];
|
||||||
|
for (const { role, content, ...message } of prompt) {
|
||||||
|
const metadata = getOpenAIMetadata({ ...message });
|
||||||
|
@@ -91,6 +91,7 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
}
|
||||||
|
case "assistant": {
|
||||||
|
let text = "";
|
||||||
|
+ let reasoning_text = "";
|
||||||
|
const toolCalls = [];
|
||||||
|
for (const part of content) {
|
||||||
|
const partMetadata = getOpenAIMetadata(part);
|
||||||
|
@@ -99,6 +100,12 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
text += part.text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
+ case "reasoning": {
|
||||||
|
+ if (options.sendReasoning) {
|
||||||
|
+ reasoning_text += part.text;
|
||||||
|
+ }
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
case "tool-call": {
|
||||||
|
toolCalls.push({
|
||||||
|
id: part.toolCallId,
|
||||||
|
@@ -116,6 +123,7 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
messages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: text,
|
||||||
|
+ reasoning_content: reasoning_text || undefined,
|
||||||
|
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
|
||||||
|
...metadata
|
||||||
|
});
|
||||||
|
@@ -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(),
|
||||||
|
+ strictJsonSchema: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// src/openai-compatible-error.ts
|
||||||
|
@@ -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:
|
||||||
|
- messages: convertToOpenAICompatibleChatMessages(prompt),
|
||||||
|
+ messages: convertToOpenAICompatibleChatMessages({prompt, options: compatibleOptions}),
|
||||||
|
// tools:
|
||||||
|
tools: openaiTools,
|
||||||
|
tool_choice: openaiToolChoice
|
||||||
|
@@ -421,6 +433,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||||
|
text: reasoning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
+ if (choice.message.images) {
|
||||||
|
+ for (const image of choice.message.images) {
|
||||||
|
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
||||||
|
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
||||||
|
+ content.push({
|
||||||
|
+ type: 'file',
|
||||||
|
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
||||||
|
+ data: match2 ? match2[1] : image.image_url.url,
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
if (choice.message.tool_calls != null) {
|
||||||
|
for (const toolCall of choice.message.tool_calls) {
|
||||||
|
content.push({
|
||||||
|
@@ -598,6 +621,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||||
|
delta: delta.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
+ if (delta.images) {
|
||||||
|
+ for (const image of delta.images) {
|
||||||
|
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
||||||
|
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
||||||
|
+ controller.enqueue({
|
||||||
|
+ type: 'file',
|
||||||
|
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
||||||
|
+ data: match2 ? match2[1] : image.image_url.url,
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
if (delta.tool_calls != null) {
|
||||||
|
for (const toolCallDelta of delta.tool_calls) {
|
||||||
|
const index = toolCallDelta.index;
|
||||||
|
@@ -765,6 +799,14 @@ var OpenAICompatibleChatResponseSchema = import_v43.z.object({
|
||||||
|
arguments: import_v43.z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
+ ).nullish(),
|
||||||
|
+ images: import_v43.z.array(
|
||||||
|
+ import_v43.z.object({
|
||||||
|
+ type: import_v43.z.literal('image_url'),
|
||||||
|
+ image_url: import_v43.z.object({
|
||||||
|
+ url: import_v43.z.string(),
|
||||||
|
+ })
|
||||||
|
+ })
|
||||||
|
).nullish()
|
||||||
|
}),
|
||||||
|
finish_reason: import_v43.z.string().nullish()
|
||||||
|
@@ -795,6 +837,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => import_v43.z.union(
|
||||||
|
arguments: import_v43.z.string().nullish()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
+ ).nullish(),
|
||||||
|
+ images: import_v43.z.array(
|
||||||
|
+ import_v43.z.object({
|
||||||
|
+ type: import_v43.z.literal('image_url'),
|
||||||
|
+ image_url: import_v43.z.object({
|
||||||
|
+ url: import_v43.z.string(),
|
||||||
|
+ })
|
||||||
|
+ })
|
||||||
|
).nullish()
|
||||||
|
}).nullish(),
|
||||||
|
finish_reason: import_v43.z.string().nullish()
|
||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index a809a7aa0e148bfd43e01dd7b018568b151c8ad5..fca65c04000ce4c01fb90e93326ac179c2378055 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -23,7 +23,7 @@ function getOpenAIMetadata(message) {
|
||||||
|
var _a, _b;
|
||||||
|
return (_b = (_a = message == null ? void 0 : message.providerOptions) == null ? void 0 : _a.openaiCompatible) != null ? _b : {};
|
||||||
|
}
|
||||||
|
-function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
+function convertToOpenAICompatibleChatMessages({prompt, options}) {
|
||||||
|
const messages = [];
|
||||||
|
for (const { role, content, ...message } of prompt) {
|
||||||
|
const metadata = getOpenAIMetadata({ ...message });
|
||||||
|
@@ -73,6 +73,7 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
}
|
||||||
|
case "assistant": {
|
||||||
|
let text = "";
|
||||||
|
+ let reasoning_text = "";
|
||||||
|
const toolCalls = [];
|
||||||
|
for (const part of content) {
|
||||||
|
const partMetadata = getOpenAIMetadata(part);
|
||||||
|
@@ -81,6 +82,12 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
text += part.text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
+ case "reasoning": {
|
||||||
|
+ if (options.sendReasoning) {
|
||||||
|
+ reasoning_text += part.text;
|
||||||
|
+ }
|
||||||
|
+ break;
|
||||||
|
+ }
|
||||||
|
case "tool-call": {
|
||||||
|
toolCalls.push({
|
||||||
|
id: part.toolCallId,
|
||||||
|
@@ -98,6 +105,7 @@ function convertToOpenAICompatibleChatMessages(prompt) {
|
||||||
|
messages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: text,
|
||||||
|
+ reasoning_content: reasoning_text || undefined,
|
||||||
|
tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
|
||||||
|
...metadata
|
||||||
|
});
|
||||||
|
@@ -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(),
|
||||||
|
+ strictJsonSchema: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// src/openai-compatible-error.ts
|
||||||
|
@@ -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:
|
||||||
|
- messages: convertToOpenAICompatibleChatMessages(prompt),
|
||||||
|
+ messages: convertToOpenAICompatibleChatMessages({prompt, options: compatibleOptions}),
|
||||||
|
// tools:
|
||||||
|
tools: openaiTools,
|
||||||
|
tool_choice: openaiToolChoice
|
||||||
|
@@ -405,6 +417,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||||
|
text: reasoning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
+ if (choice.message.images) {
|
||||||
|
+ for (const image of choice.message.images) {
|
||||||
|
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
||||||
|
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
||||||
|
+ content.push({
|
||||||
|
+ type: 'file',
|
||||||
|
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
||||||
|
+ data: match2 ? match2[1] : image.image_url.url,
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
if (choice.message.tool_calls != null) {
|
||||||
|
for (const toolCall of choice.message.tool_calls) {
|
||||||
|
content.push({
|
||||||
|
@@ -582,6 +605,17 @@ var OpenAICompatibleChatLanguageModel = class {
|
||||||
|
delta: delta.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
+ if (delta.images) {
|
||||||
|
+ for (const image of delta.images) {
|
||||||
|
+ const match1 = image.image_url.url.match(/^data:([^;]+)/)
|
||||||
|
+ const match2 = image.image_url.url.match(/^data:[^;]*;base64,(.+)$/);
|
||||||
|
+ controller.enqueue({
|
||||||
|
+ type: 'file',
|
||||||
|
+ mediaType: match1 ? (match1[1] ?? 'image/jpeg') : 'image/jpeg',
|
||||||
|
+ data: match2 ? match2[1] : image.image_url.url,
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
if (delta.tool_calls != null) {
|
||||||
|
for (const toolCallDelta of delta.tool_calls) {
|
||||||
|
const index = toolCallDelta.index;
|
||||||
|
@@ -749,6 +783,14 @@ var OpenAICompatibleChatResponseSchema = z3.object({
|
||||||
|
arguments: z3.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
+ ).nullish(),
|
||||||
|
+ images: z3.array(
|
||||||
|
+ z3.object({
|
||||||
|
+ type: z3.literal('image_url'),
|
||||||
|
+ image_url: z3.object({
|
||||||
|
+ url: z3.string(),
|
||||||
|
+ })
|
||||||
|
+ })
|
||||||
|
).nullish()
|
||||||
|
}),
|
||||||
|
finish_reason: z3.string().nullish()
|
||||||
|
@@ -779,6 +821,14 @@ var createOpenAICompatibleChatChunkSchema = (errorSchema) => z3.union([
|
||||||
|
arguments: z3.string().nullish()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
+ ).nullish(),
|
||||||
|
+ images: z3.array(
|
||||||
|
+ z3.object({
|
||||||
|
+ type: z3.literal('image_url'),
|
||||||
|
+ image_url: z3.object({
|
||||||
|
+ url: z3.string(),
|
||||||
|
+ })
|
||||||
|
+ })
|
||||||
|
).nullish()
|
||||||
|
}).nullish(),
|
||||||
|
finish_reason: 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({
|
||||||
145
patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch
Normal file
145
patches/ollama-ai-provider-v2-npm-1.5.5-8bef249af9.patch
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
diff --git a/dist/index.d.ts b/dist/index.d.ts
|
||||||
|
index 8dd9b498050dbecd8dd6b901acf1aa8ca38a49af..ed644349c9d38fe2a66b2fb44214f7c18eb97f89 100644
|
||||||
|
--- a/dist/index.d.ts
|
||||||
|
+++ b/dist/index.d.ts
|
||||||
|
@@ -4,7 +4,7 @@ import { z } from 'zod/v4';
|
||||||
|
|
||||||
|
type OllamaChatModelId = "athene-v2" | "athene-v2:72b" | "aya-expanse" | "aya-expanse:8b" | "aya-expanse:32b" | "codegemma" | "codegemma:2b" | "codegemma:7b" | "codellama" | "codellama:7b" | "codellama:13b" | "codellama:34b" | "codellama:70b" | "codellama:code" | "codellama:python" | "command-r" | "command-r:35b" | "command-r-plus" | "command-r-plus:104b" | "command-r7b" | "command-r7b:7b" | "deepseek-r1" | "deepseek-r1:1.5b" | "deepseek-r1:7b" | "deepseek-r1:8b" | "deepseek-r1:14b" | "deepseek-r1:32b" | "deepseek-r1:70b" | "deepseek-r1:671b" | "deepseek-coder-v2" | "deepseek-coder-v2:16b" | "deepseek-coder-v2:236b" | "deepseek-v3" | "deepseek-v3:671b" | "devstral" | "devstral:24b" | "dolphin3" | "dolphin3:8b" | "exaone3.5" | "exaone3.5:2.4b" | "exaone3.5:7.8b" | "exaone3.5:32b" | "falcon2" | "falcon2:11b" | "falcon3" | "falcon3:1b" | "falcon3:3b" | "falcon3:7b" | "falcon3:10b" | "firefunction-v2" | "firefunction-v2:70b" | "gemma" | "gemma:2b" | "gemma:7b" | "gemma2" | "gemma2:2b" | "gemma2:9b" | "gemma2:27b" | "gemma3" | "gemma3:1b" | "gemma3:4b" | "gemma3:12b" | "gemma3:27b" | "granite3-dense" | "granite3-dense:2b" | "granite3-dense:8b" | "granite3-guardian" | "granite3-guardian:2b" | "granite3-guardian:8b" | "granite3-moe" | "granite3-moe:1b" | "granite3-moe:3b" | "granite3.1-dense" | "granite3.1-dense:2b" | "granite3.1-dense:8b" | "granite3.1-moe" | "granite3.1-moe:1b" | "granite3.1-moe:3b" | "llama2" | "llama2:7b" | "llama2:13b" | "llama2:70b" | "llama3" | "llama3:8b" | "llama3:70b" | "llama3-chatqa" | "llama3-chatqa:8b" | "llama3-chatqa:70b" | "llama3-gradient" | "llama3-gradient:8b" | "llama3-gradient:70b" | "llama3.1" | "llama3.1:8b" | "llama3.1:70b" | "llama3.1:405b" | "llama3.2" | "llama3.2:1b" | "llama3.2:3b" | "llama3.2-vision" | "llama3.2-vision:11b" | "llama3.2-vision:90b" | "llama3.3" | "llama3.3:70b" | "llama4" | "llama4:16x17b" | "llama4:128x17b" | "llama-guard3" | "llama-guard3:1b" | "llama-guard3:8b" | "llava" | "llava:7b" | "llava:13b" | "llava:34b" | "llava-llama3" | "llava-llama3:8b" | "llava-phi3" | "llava-phi3:3.8b" | "marco-o1" | "marco-o1:7b" | "mistral" | "mistral:7b" | "mistral-large" | "mistral-large:123b" | "mistral-nemo" | "mistral-nemo:12b" | "mistral-small" | "mistral-small:22b" | "mixtral" | "mixtral:8x7b" | "mixtral:8x22b" | "moondream" | "moondream:1.8b" | "openhermes" | "openhermes:v2.5" | "nemotron" | "nemotron:70b" | "nemotron-mini" | "nemotron-mini:4b" | "olmo" | "olmo:7b" | "olmo:13b" | "opencoder" | "opencoder:1.5b" | "opencoder:8b" | "phi3" | "phi3:3.8b" | "phi3:14b" | "phi3.5" | "phi3.5:3.8b" | "phi4" | "phi4:14b" | "qwen" | "qwen:7b" | "qwen:14b" | "qwen:32b" | "qwen:72b" | "qwen:110b" | "qwen2" | "qwen2:0.5b" | "qwen2:1.5b" | "qwen2:7b" | "qwen2:72b" | "qwen2.5" | "qwen2.5:0.5b" | "qwen2.5:1.5b" | "qwen2.5:3b" | "qwen2.5:7b" | "qwen2.5:14b" | "qwen2.5:32b" | "qwen2.5:72b" | "qwen2.5-coder" | "qwen2.5-coder:0.5b" | "qwen2.5-coder:1.5b" | "qwen2.5-coder:3b" | "qwen2.5-coder:7b" | "qwen2.5-coder:14b" | "qwen2.5-coder:32b" | "qwen3" | "qwen3:0.6b" | "qwen3:1.7b" | "qwen3:4b" | "qwen3:8b" | "qwen3:14b" | "qwen3:30b" | "qwen3:32b" | "qwen3:235b" | "qwq" | "qwq:32b" | "sailor2" | "sailor2:1b" | "sailor2:8b" | "sailor2:20b" | "shieldgemma" | "shieldgemma:2b" | "shieldgemma:9b" | "shieldgemma:27b" | "smallthinker" | "smallthinker:3b" | "smollm" | "smollm:135m" | "smollm:360m" | "smollm:1.7b" | "tinyllama" | "tinyllama:1.1b" | "tulu3" | "tulu3:8b" | "tulu3:70b" | (string & {});
|
||||||
|
declare const ollamaProviderOptions: z.ZodObject<{
|
||||||
|
- think: z.ZodOptional<z.ZodBoolean>;
|
||||||
|
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodLiteral<"low">, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>;
|
||||||
|
options: z.ZodOptional<z.ZodObject<{
|
||||||
|
num_ctx: z.ZodOptional<z.ZodNumber>;
|
||||||
|
repeat_last_n: z.ZodOptional<z.ZodNumber>;
|
||||||
|
@@ -27,9 +27,11 @@ interface OllamaCompletionSettings {
|
||||||
|
* the model's thinking from the model's output. When disabled, the model will not think
|
||||||
|
* and directly output the content.
|
||||||
|
*
|
||||||
|
+ * For gpt-oss models, you can also use 'low', 'medium', or 'high' to control the depth of thinking.
|
||||||
|
+ *
|
||||||
|
* Only supported by certain models like DeepSeek R1 and Qwen 3.
|
||||||
|
*/
|
||||||
|
- think?: boolean;
|
||||||
|
+ think?: boolean | 'low' | 'medium' | 'high';
|
||||||
|
/**
|
||||||
|
* Echo back the prompt in addition to the completion.
|
||||||
|
*/
|
||||||
|
@@ -146,7 +148,7 @@ declare const ollamaEmbeddingProviderOptions: z.ZodObject<{
|
||||||
|
type OllamaEmbeddingProviderOptions = z.infer<typeof ollamaEmbeddingProviderOptions>;
|
||||||
|
|
||||||
|
declare const ollamaCompletionProviderOptions: z.ZodObject<{
|
||||||
|
- think: z.ZodOptional<z.ZodBoolean>;
|
||||||
|
+ think: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodLiteral<"low">, z.ZodLiteral<"medium">, z.ZodLiteral<"high">]>>;
|
||||||
|
user: z.ZodOptional<z.ZodString>;
|
||||||
|
suffix: z.ZodOptional<z.ZodString>;
|
||||||
|
echo: z.ZodOptional<z.ZodBoolean>;
|
||||||
|
diff --git a/dist/index.js b/dist/index.js
|
||||||
|
index 35b5142ce8476ce2549ed7c2ec48e7d8c46c90d9..2ef64dc9a4c2be043e6af608241a6a8309a5a69f 100644
|
||||||
|
--- a/dist/index.js
|
||||||
|
+++ b/dist/index.js
|
||||||
|
@@ -158,7 +158,7 @@ function getResponseMetadata({
|
||||||
|
|
||||||
|
// src/completion/ollama-completion-language-model.ts
|
||||||
|
var ollamaCompletionProviderOptions = import_v42.z.object({
|
||||||
|
- think: import_v42.z.boolean().optional(),
|
||||||
|
+ think: import_v42.z.union([import_v42.z.boolean(), import_v42.z.literal('low'), import_v42.z.literal('medium'), import_v42.z.literal('high')]).optional(),
|
||||||
|
user: import_v42.z.string().optional(),
|
||||||
|
suffix: import_v42.z.string().optional(),
|
||||||
|
echo: import_v42.z.boolean().optional()
|
||||||
|
@@ -662,7 +662,7 @@ function convertToOllamaChatMessages({
|
||||||
|
const images = content.filter((part) => part.type === "file" && part.mediaType.startsWith("image/")).map((part) => part.data);
|
||||||
|
messages.push({
|
||||||
|
role: "user",
|
||||||
|
- content: userText.length > 0 ? userText : [],
|
||||||
|
+ content: userText.length > 0 ? userText : '',
|
||||||
|
images: images.length > 0 ? images : void 0
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
@@ -813,9 +813,11 @@ var ollamaProviderOptions = import_v44.z.object({
|
||||||
|
* the model's thinking from the model's output. When disabled, the model will not think
|
||||||
|
* and directly output the content.
|
||||||
|
*
|
||||||
|
+ * For gpt-oss models, you can also use 'low', 'medium', or 'high' to control the depth of thinking.
|
||||||
|
+ *
|
||||||
|
* Only supported by certain models like DeepSeek R1 and Qwen 3.
|
||||||
|
*/
|
||||||
|
- think: import_v44.z.boolean().optional(),
|
||||||
|
+ think: import_v44.z.union([import_v44.z.boolean(), import_v44.z.literal('low'), import_v44.z.literal('medium'), import_v44.z.literal('high')]).optional(),
|
||||||
|
options: import_v44.z.object({
|
||||||
|
num_ctx: import_v44.z.number().optional(),
|
||||||
|
repeat_last_n: import_v44.z.number().optional(),
|
||||||
|
@@ -929,14 +931,16 @@ var OllamaRequestBuilder = class {
|
||||||
|
prompt,
|
||||||
|
systemMessageMode: "system"
|
||||||
|
}),
|
||||||
|
- temperature,
|
||||||
|
- top_p: topP,
|
||||||
|
max_output_tokens: maxOutputTokens,
|
||||||
|
...(responseFormat == null ? void 0 : responseFormat.type) === "json" && {
|
||||||
|
format: responseFormat.schema != null ? responseFormat.schema : "json"
|
||||||
|
},
|
||||||
|
think: (_a = ollamaOptions == null ? void 0 : ollamaOptions.think) != null ? _a : false,
|
||||||
|
- options: (_b = ollamaOptions == null ? void 0 : ollamaOptions.options) != null ? _b : void 0
|
||||||
|
+ options: {
|
||||||
|
+ ...temperature !== void 0 && { temperature },
|
||||||
|
+ ...topP !== void 0 && { top_p: topP },
|
||||||
|
+ ...((_b = ollamaOptions == null ? void 0 : ollamaOptions.options) != null ? _b : {})
|
||||||
|
+ }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
|
index e2a634a78d80ac9542f2cc4f96cf2291094b10cf..67b23efce3c1cf4f026693d3ff9246988a3ef26e 100644
|
||||||
|
--- a/dist/index.mjs
|
||||||
|
+++ b/dist/index.mjs
|
||||||
|
@@ -144,7 +144,7 @@ function getResponseMetadata({
|
||||||
|
|
||||||
|
// src/completion/ollama-completion-language-model.ts
|
||||||
|
var ollamaCompletionProviderOptions = z2.object({
|
||||||
|
- think: z2.boolean().optional(),
|
||||||
|
+ think: z2.union([z2.boolean(), z2.literal('low'), z2.literal('medium'), z2.literal('high')]).optional(),
|
||||||
|
user: z2.string().optional(),
|
||||||
|
suffix: z2.string().optional(),
|
||||||
|
echo: z2.boolean().optional()
|
||||||
|
@@ -662,7 +662,7 @@ function convertToOllamaChatMessages({
|
||||||
|
const images = content.filter((part) => part.type === "file" && part.mediaType.startsWith("image/")).map((part) => part.data);
|
||||||
|
messages.push({
|
||||||
|
role: "user",
|
||||||
|
- content: userText.length > 0 ? userText : [],
|
||||||
|
+ content: userText.length > 0 ? userText : '',
|
||||||
|
images: images.length > 0 ? images : void 0
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
@@ -815,9 +815,11 @@ var ollamaProviderOptions = z4.object({
|
||||||
|
* the model's thinking from the model's output. When disabled, the model will not think
|
||||||
|
* and directly output the content.
|
||||||
|
*
|
||||||
|
+ * For gpt-oss models, you can also use 'low', 'medium', or 'high' to control the depth of thinking.
|
||||||
|
+ *
|
||||||
|
* Only supported by certain models like DeepSeek R1 and Qwen 3.
|
||||||
|
*/
|
||||||
|
- think: z4.boolean().optional(),
|
||||||
|
+ think: z4.union([z4.boolean(), z4.literal('low'), z4.literal('medium'), z4.literal('high')]).optional(),
|
||||||
|
options: z4.object({
|
||||||
|
num_ctx: z4.number().optional(),
|
||||||
|
repeat_last_n: z4.number().optional(),
|
||||||
|
@@ -931,14 +933,16 @@ var OllamaRequestBuilder = class {
|
||||||
|
prompt,
|
||||||
|
systemMessageMode: "system"
|
||||||
|
}),
|
||||||
|
- temperature,
|
||||||
|
- top_p: topP,
|
||||||
|
max_output_tokens: maxOutputTokens,
|
||||||
|
...(responseFormat == null ? void 0 : responseFormat.type) === "json" && {
|
||||||
|
format: responseFormat.schema != null ? responseFormat.schema : "json"
|
||||||
|
},
|
||||||
|
think: (_a = ollamaOptions == null ? void 0 : ollamaOptions.think) != null ? _a : false,
|
||||||
|
- options: (_b = ollamaOptions == null ? void 0 : ollamaOptions.options) != null ? _b : void 0
|
||||||
|
+ options: {
|
||||||
|
+ ...temperature !== void 0 && { temperature },
|
||||||
|
+ ...topP !== void 0 && { top_p: topP },
|
||||||
|
+ ...((_b = ollamaOptions == null ? void 0 : ollamaOptions.options) != null ? _b : {})
|
||||||
|
+ }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,42 +1,64 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test'
|
import { defineConfig } from '@playwright/test'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* Playwright configuration for Electron e2e testing.
|
||||||
|
* See https://playwright.dev/docs/test-configuration
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// Look for test files, relative to this configuration file.
|
// Look for test files in the specs directory
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e/specs',
|
||||||
/* Run tests in files in parallel */
|
|
||||||
fullyParallel: true,
|
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
/* Retry on CI only */
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
/* Opt out of parallel tests on CI. */
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
||||||
reporter: 'html',
|
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
||||||
use: {
|
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
// baseURL: 'http://localhost:3000',
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
// Global timeout for each test
|
||||||
trace: 'on-first-retry'
|
timeout: 60000,
|
||||||
|
|
||||||
|
// Assertion timeout
|
||||||
|
expect: {
|
||||||
|
timeout: 10000
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
// Electron apps should run tests sequentially to avoid conflicts
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
|
||||||
|
// Fail the build on CI if you accidentally left test.only in the source code
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
|
||||||
|
// Retry on CI only
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
|
||||||
|
// Reporter configuration
|
||||||
|
reporter: [['html', { outputFolder: 'playwright-report' }], ['list']],
|
||||||
|
|
||||||
|
// Global setup and teardown
|
||||||
|
globalSetup: './tests/e2e/global-setup.ts',
|
||||||
|
globalTeardown: './tests/e2e/global-teardown.ts',
|
||||||
|
|
||||||
|
// Output directory for test artifacts
|
||||||
|
outputDir: './test-results',
|
||||||
|
|
||||||
|
// Shared settings for all tests
|
||||||
|
use: {
|
||||||
|
// Collect trace when retrying the failed test
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
|
||||||
|
// Take screenshot only on failure
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
|
||||||
|
// Record video only on failure
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
|
||||||
|
// Action timeout
|
||||||
|
actionTimeout: 15000,
|
||||||
|
|
||||||
|
// Navigation timeout
|
||||||
|
navigationTimeout: 30000
|
||||||
|
},
|
||||||
|
|
||||||
|
// Single project for Electron testing
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'electron',
|
||||||
use: { ...devices['Desktop Chrome'] }
|
testMatch: '**/*.spec.ts'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
|
||||||
// webServer: {
|
|
||||||
// command: 'npm run start',
|
|
||||||
// url: 'http://localhost:3000',
|
|
||||||
// reuseExistingServer: !process.env.CI,
|
|
||||||
// },
|
|
||||||
})
|
})
|
||||||
|
|||||||
25639
pnpm-lock.yaml
Normal file
25639
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
8
pnpm-workspace.yaml
Normal file
8
pnpm-workspace.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
|
|
||||||
|
supportedArchitectures:
|
||||||
|
os:
|
||||||
|
- current
|
||||||
|
cpu:
|
||||||
|
- current
|
||||||
@ -6,12 +6,12 @@ const { downloadWithPowerShell } = require('./download')
|
|||||||
|
|
||||||
// Base URL for downloading OVMS binaries
|
// Base URL for downloading OVMS binaries
|
||||||
const OVMS_RELEASE_BASE_URL =
|
const OVMS_RELEASE_BASE_URL =
|
||||||
'https://storage.openvinotoolkit.org/repositories/openvino_model_server/packages/2025.3.0/ovms_windows_python_on.zip'
|
'https://storage.openvinotoolkit.org/repositories/openvino_model_server/packages/2025.4.1/ovms_windows_python_on.zip'
|
||||||
const OVMS_EX_URL = 'https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/ovms_25.3_ex.zip'
|
const OVMS_EX_URL = 'https://gitcode.com/gcw_ggDjjkY3/kjfile/releases/download/download/ovms_25.4_ex.zip'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* error code:
|
* error code:
|
||||||
* 101: Unsupported CPU (not Intel Ultra)
|
* 101: Unsupported CPU (not Intel)
|
||||||
* 102: Unsupported platform (not Windows)
|
* 102: Unsupported platform (not Windows)
|
||||||
* 103: Download failed
|
* 103: Download failed
|
||||||
* 104: Installation failed
|
* 104: Installation failed
|
||||||
@ -213,8 +213,8 @@ async function installOvms() {
|
|||||||
console.log(`CPU Name: ${cpuName}`)
|
console.log(`CPU Name: ${cpuName}`)
|
||||||
|
|
||||||
// Check if CPU name contains "Ultra"
|
// Check if CPU name contains "Ultra"
|
||||||
if (!cpuName.toLowerCase().includes('intel') || !cpuName.toLowerCase().includes('ultra')) {
|
if (!cpuName.toLowerCase().includes('intel')) {
|
||||||
console.error('OVMS installation requires an Intel(R) Core(TM) Ultra CPU.')
|
console.error('OVMS installation requires an Intel CPU.')
|
||||||
return 101
|
return 101
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ Usage Instructions:
|
|||||||
- pt-pt (Portuguese)
|
- pt-pt (Portuguese)
|
||||||
|
|
||||||
Run Command:
|
Run Command:
|
||||||
yarn auto:i18n
|
pnpm i18n:translate
|
||||||
|
|
||||||
Performance Optimization Recommendations:
|
Performance Optimization Recommendations:
|
||||||
- For stable API services: MAX_CONCURRENT_TRANSLATIONS=8, TRANSLATION_DELAY_MS=50
|
- For stable API services: MAX_CONCURRENT_TRANSLATIONS=8, TRANSLATION_DELAY_MS=50
|
||||||
@ -152,7 +152,8 @@ const languageMap = {
|
|||||||
'es-es': 'Spanish',
|
'es-es': 'Spanish',
|
||||||
'fr-fr': 'French',
|
'fr-fr': 'French',
|
||||||
'pt-pt': 'Portuguese',
|
'pt-pt': 'Portuguese',
|
||||||
'de-de': 'German'
|
'de-de': 'German',
|
||||||
|
'ro-ro': 'Romanian'
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROMPT = `
|
const PROMPT = `
|
||||||
|
|||||||
@ -1,42 +1,35 @@
|
|||||||
const { Arch } = require('electron-builder')
|
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
|
// if you want to add new prebuild binaries packages with different architectures, you can add them here
|
||||||
// please add to allX64 and allArm64 from yarn.lock
|
// please add to allX64 and allArm64 from pnpm-lock.yaml
|
||||||
const allArm64 = {
|
const packages = [
|
||||||
'@img/sharp-darwin-arm64': '0.34.3',
|
'@img/sharp-darwin-arm64',
|
||||||
'@img/sharp-win32-arm64': '0.34.3',
|
'@img/sharp-darwin-x64',
|
||||||
'@img/sharp-linux-arm64': '0.34.3',
|
'@img/sharp-linux-arm64',
|
||||||
|
'@img/sharp-linux-x64',
|
||||||
'@img/sharp-libvips-darwin-arm64': '1.2.0',
|
'@img/sharp-win32-arm64',
|
||||||
'@img/sharp-libvips-linux-arm64': '1.2.0',
|
'@img/sharp-win32-x64',
|
||||||
|
'@img/sharp-libvips-darwin-arm64',
|
||||||
'@libsql/darwin-arm64': '0.4.7',
|
'@img/sharp-libvips-darwin-x64',
|
||||||
'@libsql/linux-arm64-gnu': '0.4.7',
|
'@img/sharp-libvips-linux-arm64',
|
||||||
'@strongtz/win32-arm64-msvc': '0.4.7',
|
'@img/sharp-libvips-linux-x64',
|
||||||
|
'@libsql/darwin-arm64',
|
||||||
'@napi-rs/system-ocr-darwin-arm64': '1.0.2',
|
'@libsql/darwin-x64',
|
||||||
'@napi-rs/system-ocr-win32-arm64-msvc': '1.0.2'
|
'@libsql/linux-arm64-gnu',
|
||||||
}
|
'@libsql/linux-x64-gnu',
|
||||||
|
'@libsql/win32-x64-msvc',
|
||||||
const allX64 = {
|
'@napi-rs/system-ocr-darwin-arm64',
|
||||||
'@img/sharp-darwin-x64': '0.34.3',
|
'@napi-rs/system-ocr-darwin-x64',
|
||||||
'@img/sharp-linux-x64': '0.34.3',
|
'@napi-rs/system-ocr-win32-arm64-msvc',
|
||||||
'@img/sharp-win32-x64': '0.34.3',
|
'@napi-rs/system-ocr-win32-x64-msvc',
|
||||||
|
'@strongtz/win32-arm64-msvc'
|
||||||
'@img/sharp-libvips-darwin-x64': '1.2.0',
|
]
|
||||||
'@img/sharp-libvips-linux-x64': '1.2.0',
|
|
||||||
|
|
||||||
'@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 platformToArch = {
|
const platformToArch = {
|
||||||
mac: 'darwin',
|
mac: 'darwin',
|
||||||
@ -45,61 +38,82 @@ const platformToArch = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.default = async function (context) {
|
exports.default = async function (context) {
|
||||||
const arch = context.arch
|
const arch = context.arch === Arch.arm64 ? 'arm64' : 'x64'
|
||||||
const archType = arch === Arch.arm64 ? 'arm64' : 'x64'
|
const platformName = context.packager.platform.name
|
||||||
const platform = context.packager.platform.name
|
const platform = platformToArch[platformName]
|
||||||
|
|
||||||
const downloadPackages = async (packages) => {
|
const downloadPackages = async () => {
|
||||||
console.log('downloading packages ......')
|
// Skip if target platform and architecture match current system
|
||||||
const downloadPromises = []
|
if (platform === process.platform && arch === process.arch) {
|
||||||
|
console.log(`Skipping install: target (${platform}/${arch}) matches current system`)
|
||||||
for (const name of Object.keys(packages)) {
|
return
|
||||||
if (name.includes(`${platformToArch[platform]}`) && name.includes(`-${archType}`)) {
|
|
||||||
downloadPromises.push(
|
|
||||||
downloadNpmPackage(
|
|
||||||
name,
|
|
||||||
`https://registry.npmjs.org/${name}/-/${name.split('/').pop()}-${packages[name]}.tgz`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(downloadPromises)
|
console.log(`Installing packages for target platform=${platform} arch=${arch}...`)
|
||||||
|
|
||||||
|
// Backup and modify pnpm-workspace.yaml to add target platform support
|
||||||
|
const originalWorkspaceConfig = fs.readFileSync(workspaceConfigPath, 'utf-8')
|
||||||
|
const workspaceConfig = yaml.load(originalWorkspaceConfig)
|
||||||
|
|
||||||
|
// Add target platform to supportedArchitectures.os
|
||||||
|
if (!workspaceConfig.supportedArchitectures.os.includes(platform)) {
|
||||||
|
workspaceConfig.supportedArchitectures.os.push(platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add target architecture to supportedArchitectures.cpu
|
||||||
|
if (!workspaceConfig.supportedArchitectures.cpu.includes(arch)) {
|
||||||
|
workspaceConfig.supportedArchitectures.cpu.push(arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedWorkspaceConfig = yaml.dump(workspaceConfig)
|
||||||
|
console.log('Modified workspace config:', modifiedWorkspaceConfig)
|
||||||
|
fs.writeFileSync(workspaceConfigPath, modifiedWorkspaceConfig)
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`pnpm install`, { stdio: 'inherit' })
|
||||||
|
} finally {
|
||||||
|
// Restore original pnpm-workspace.yaml
|
||||||
|
fs.writeFileSync(workspaceConfigPath, originalWorkspaceConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeFilters = async (filtersToExclude, filtersToInclude) => {
|
await downloadPackages()
|
||||||
// remove filters for the target architecture (allow inclusion)
|
|
||||||
let filters = context.packager.config.files[0].filter
|
const excludePackages = async (packagesToExclude) => {
|
||||||
filters = filters.filter((filter) => !filtersToInclude.includes(filter))
|
// 从项目根目录的 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)
|
// add filters for other architectures (exclude them)
|
||||||
filters.push(...filtersToExclude)
|
filters.push(...packagesToExclude)
|
||||||
|
|
||||||
context.packager.config.files[0].filter = filters
|
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 x64KeepPackages = packages.filter((p) => p.includes('x64') && p.includes(platform))
|
||||||
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
|
const x64ExcludePackages = packages
|
||||||
const excludeClaudeCodeRipgrepFilters = claudeCodeVenders
|
.filter((p) => !x64KeepPackages.includes(p))
|
||||||
.filter((f) => f !== `${archType}-${platformToArch[platform]}`)
|
.map((p) => '!node_modules/' + p + '/**')
|
||||||
.map((f) => '!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + f + '/**')
|
|
||||||
const excludeClaudeCodeJBPlutins = ['!node_modules/' + claudeCodeVenderPath + '/' + 'claude-code-jetbrains-plugin']
|
|
||||||
|
|
||||||
const includeClaudeCodeFilters = [
|
const excludeRipgrepFilters = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
|
||||||
'!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + `${archType}-${platformToArch[platform]}/**`
|
.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) {
|
if (context.arch === Arch.arm64) {
|
||||||
await changeFilters(
|
await excludePackages([...arm64ExcludePackages, ...excludeRipgrepFilters])
|
||||||
[...x64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
|
|
||||||
[...arm64Filters, ...includeClaudeCodeFilters]
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
await changeFilters(
|
await excludePackages([...x64ExcludePackages, ...excludeRipgrepFilters])
|
||||||
[...arm64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
|
|
||||||
[...x64Filters, ...includeClaudeCodeFilters]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,7 +145,7 @@ export function main() {
|
|||||||
console.log('i18n 检查已通过')
|
console.log('i18n 检查已通过')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
throw new Error(`检查未通过。尝试运行 yarn sync:i18n 以解决问题。`)
|
throw new Error(`检查未通过。尝试运行 pnpm i18n:sync 以解决问题。`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,228 +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: `**🐛 New GitHub Issue #${issueNumber}**`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'hr'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag: 'div',
|
|
||||||
text: {
|
|
||||||
tag: 'lark_md',
|
|
||||||
content: `**📝 Title:** ${issueTitle}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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: '🆕 Cherry Studio - New Issue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
||||||
@ -57,7 +57,7 @@ function generateLanguagesFileContent(languages: Record<string, LanguageData>):
|
|||||||
*
|
*
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
||||||
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
|
* THIS FILE IS AUTOMATICALLY GENERATED BY A SCRIPT. DO NOT EDIT IT MANUALLY!
|
||||||
* Run \`yarn update:languages\` to update this file.
|
* Run \`pnpm update:languages\` to update this file.
|
||||||
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
* ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -81,7 +81,7 @@ export const languages: Record<string, LanguageData> = ${languagesObjectString};
|
|||||||
async function format(filePath: string): Promise<void> {
|
async function format(filePath: string): Promise<void> {
|
||||||
console.log('🎨 Formatting file with Biome...')
|
console.log('🎨 Formatting file with Biome...')
|
||||||
try {
|
try {
|
||||||
await execAsync(`yarn biome format --write ${filePath}`)
|
await execAsync(`pnpm biome format --write ${filePath}`)
|
||||||
console.log('✅ Biome formatting complete.')
|
console.log('✅ Biome formatting complete.')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('❌ Biome formatting failed:', e.stdout || e.stderr)
|
console.error('❌ Biome formatting failed:', e.stdout || e.stderr)
|
||||||
@ -96,7 +96,7 @@ async function format(filePath: string): Promise<void> {
|
|||||||
async function checkTypeScript(filePath: string): Promise<void> {
|
async function checkTypeScript(filePath: string): Promise<void> {
|
||||||
console.log('🧐 Checking file with TypeScript compiler...')
|
console.log('🧐 Checking file with TypeScript compiler...')
|
||||||
try {
|
try {
|
||||||
await execAsync(`yarn tsc --noEmit --skipLibCheck ${filePath}`)
|
await execAsync(`pnpm tsc --noEmit --skipLibCheck ${filePath}`)
|
||||||
console.log('✅ TypeScript check passed.')
|
console.log('✅ TypeScript check passed.')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('❌ TypeScript check failed:', e.stdout || e.stderr)
|
console.error('❌ TypeScript check failed:', e.stdout || e.stderr)
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -18,7 +18,7 @@ if (!['patch', 'minor', 'major'].includes(versionType)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新版本
|
// 更新版本
|
||||||
exec(`yarn version ${versionType} --immediate`)
|
exec(`pnpm version ${versionType}`)
|
||||||
|
|
||||||
// 读取更新后的 package.json 获取新版本号
|
// 读取更新后的 package.json 获取新版本号
|
||||||
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user