Compare commits
416 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 86a2780e2c | |||
| d960a42d6e | |||
| 3ae1b3d4cb | |||
| 1c045231c8 | |||
| 282aa6e81a | |||
| 117e390cf1 | |||
| 34b05a138b | |||
| 6c233fef9f | |||
| 1c813aa6c3 | |||
| dd5592ddbb | |||
| 6e9d8a1747 | |||
| cee78c6610 | |||
| 0b2dfbb88f | |||
| 1fd44a68b0 | |||
| fcacc50fdc | |||
| 009b58c9c3 | |||
| 77c64cf868 | |||
| f5acddbfeb | |||
| ae35d689ec | |||
| 825b5e1be4 | |||
| 17df1db120 | |||
| d56521260c | |||
| 8efafc6ba9 | |||
| f35987a9a9 | |||
| c7ec55c69a | |||
| c77d7dff78 | |||
| b282e4d729 | |||
| c426876d0d | |||
| 027ef17a2e | |||
| f0ac74dccf | |||
| d6468f33c5 | |||
| 1515f511a1 | |||
| 1c2211aefb | |||
| 49f9dff9da | |||
| 92ba1e4fc3 | |||
| 7060aab33d | |||
| 0cce8220ce | |||
| 1722d9f435 | |||
| 078fd57eb5 | |||
| 4bd6087dc0 | |||
| e45231376c | |||
| 01c7e509fd | |||
| 5ddf9683b4 | |||
| d91df12dbc | |||
| 64e3de9ada | |||
| 2cf2f04a70 | |||
| 73380d76df | |||
| 38076babcf | |||
| 00cc410dcc | |||
| 7fc676bbc3 | |||
| 798126d39c | |||
| 874d74cf6e | |||
| d73834e7f6 | |||
| cb3afaceab | |||
| fc0ba5d0d5 | |||
| 7ce4fc50ea | |||
| b5ef8a93ca | |||
| 8ead4e9c0f | |||
| 432d84cda5 | |||
| d3378dcf78 | |||
| f0724af2aa | |||
| f127150ea1 | |||
| b3ef6d4534 | |||
| 1ce791d517 | |||
| 3d561ad8e3 | |||
| 14509d1077 | |||
| a424e3a039 | |||
| eaa5ec5545 | |||
| 5850e5da66 | |||
| eb3ff6f570 | |||
| ae9c78e643 | |||
| 445528aff7 | |||
| d13c25444c | |||
| 5386716ebe | |||
| da3cd62486 | |||
| d8b47e30c4 | |||
| 1c19e529ac | |||
| 514b60f704 | |||
| df1d4cd62b | |||
| 4839b91cef | |||
| 5048d6987d | |||
| 809736dd33 | |||
| 369cc37071 | |||
| d0b64dabc2 | |||
| 02d2838424 | |||
| 4c4039283f | |||
| 77df6fd58e | |||
| 100801821f | |||
| 2201ebbb88 | |||
| 9810f01330 | |||
| 7b428be93d | |||
| a4c2ed5328 | |||
| 934cc0dd33 | |||
| da61500e34 | |||
| db2042800b | |||
| 08772741e6 | |||
| f5f542911f | |||
| 3b5b1986e6 | |||
| 3b0995c8ef | |||
| 34c95ca787 | |||
| a4c2a1d435 | |||
| e4f0743e2f | |||
| 7632efda88 | |||
| ab90eb2aab | |||
| 4c5bed0b1f | |||
| 302331043a | |||
| 09f5e7af8c | |||
| 664304241a | |||
| 27f98b02a6 | |||
| af6a3c87d6 | |||
| d1819274bb | |||
| 8058ed21b3 | |||
| eaf302bb40 | |||
| 3405b7e429 | |||
| 2fc1df8793 | |||
| ec82eb2881 | |||
| 1c978e0684 | |||
| e938e1572c | |||
| 9a7681c5c8 | |||
| 259f2157f6 | |||
| 21ce139df0 | |||
| 71536d6ef5 | |||
| ef1a035701 | |||
| 2b76c326ee | |||
| 64ee5c528b | |||
| 136d343c18 | |||
| 0b1b9a913f | |||
| cb0833a915 | |||
| 984c28d4be | |||
| 49add96dc0 | |||
| db58762a13 | |||
| 6e89d0037f | |||
| e1ab17387c | |||
| 54de2341bd | |||
| b131f0c48c | |||
| 9e4b792fc3 | |||
| 7abd5da57d | |||
| be7399b3c4 | |||
| 8d92b515ab | |||
| 524098d6d3 | |||
| 42fa2d94be | |||
| a65b30f3a1 | |||
| f1991b356b | |||
| 352c23180a | |||
| 825c376c5c | |||
| 231a923c9d | |||
| dbf01652f8 | |||
| 842a6cb178 | |||
| d56c526709 | |||
| 70a68bef27 | |||
| f9b49ffde6 | |||
| a264fd42e4 | |||
| 219844cb74 | |||
| 230205d210 | |||
| f9fb0f9125 | |||
| 0f777e357d | |||
| 5c578c191b | |||
| 7a4952f773 | |||
| 71a1daddef | |||
| 0a82955e91 | |||
| 62d2da3815 | |||
| 84aab66aa6 | |||
| 2d0d599ac8 | |||
| dca6be45b0 | |||
| 5a71807cc9 | |||
| 0d0ab4dcf5 | |||
| ac3da51890 | |||
| 9ea361f7e8 | |||
| cc6160892a | |||
| 49eed449c3 | |||
| 8ada7ffaf6 | |||
| e7c37231e0 | |||
| ca597b9b9b | |||
| c76df7fb16 | |||
| 89d5bd817b | |||
| 6c9fc598d4 | |||
| 273475881e | |||
| 6afaf6244c | |||
| 77535b002a | |||
| dec68ee297 | |||
| a8bf55abc2 | |||
| c196a02c95 | |||
| 1481149e51 | |||
| d1ff8591a6 | |||
| 4f91a321a0 | |||
| 219d162e1a | |||
| 669f60273c | |||
| 7319fc5ef4 | |||
| cc860e48b1 | |||
| a9093b1dea | |||
| 697f7d1946 | |||
| ee95fad7e5 | |||
| e4d04f8346 | |||
| c37af25525 | |||
| ea90c6c9cb | |||
| 58dbb514e0 | |||
| 1a5138c5b1 | |||
| 5451e2f34a | |||
| c641b116ba | |||
| d44654f003 | |||
| 85b8724c73 | |||
| a8e2df6bed | |||
| 2f74becb31 | |||
| b31ac74f96 | |||
| 4d1d3e316f | |||
| e5ccf68476 | |||
| e3d2bb2ec6 | |||
| 7f9f5514a4 | |||
| d5487ba6ac | |||
| 54b4e6a80b | |||
| 079d2c3cb3 | |||
| 911f9d8bc9 | |||
| 95ed17aa69 | |||
| 4f64afd8f4 | |||
| f90bda861f | |||
| 4f8b5c1250 | |||
| 71ed94de31 | |||
| 6eccb92817 | |||
| 734ddd7489 | |||
| dc16cf2aa7 | |||
| be12898b7b | |||
| 3fc92e093b | |||
| b6187ad637 | |||
| ca8ac9911e | |||
| 95a1e210b6 | |||
| ea4db1c864 | |||
| f9171f3df8 | |||
| b55f419a95 | |||
| 8953961a51 | |||
| 9b1e9552d6 | |||
| 8836663c35 | |||
| aaba77c360 | |||
| 532bad8eb7 | |||
| e5b43c8176 | |||
| 5f9c2d7f6a | |||
| 568257e7b6 | |||
| df31629c5f | |||
| e6dc8619d9 | |||
| 9a71a01b66 | |||
| 941a6666e6 | |||
| 1bf63865a8 | |||
| ba41d8021f | |||
| 1e919a908f | |||
| 702612e3f9 | |||
| fa380619ce | |||
| a30b2e2cb2 | |||
| ae0cee9ef4 | |||
| dc9fb381f7 | |||
| 943fccf655 | |||
| e0d2d44f35 | |||
| 5a6413f356 | |||
| f3ef4c77f5 | |||
| 751e391db6 | |||
| 3e04c9493f | |||
| 6b0a1a42ad | |||
| ee82b23886 | |||
| c9bb3ff6f6 | |||
| 993d497aad | |||
| 0d2dc2c257 | |||
| c785be82dd | |||
| 80afb3a86e | |||
| f5d8974d04 | |||
| a4bb82a02d | |||
| e8c94f3584 | |||
| d123eec476 | |||
| 002a443281 | |||
| 64f3d08d4e | |||
| 9c956a30ea | |||
| 276269e583 | |||
| 5eaa90a7a2 | |||
| 8f36c4e793 | |||
| 5f999d3c84 | |||
| d68aafea15 | |||
| e093ae72da | |||
| d17362d8c4 | |||
| 5e19e7ac6c | |||
| 871565c687 | |||
| 6104b7803b | |||
| 95a332f38a | |||
| 4c4cc52c07 | |||
| 4714442d6e | |||
| 66115ca306 | |||
| 7fec4c0dac | |||
| e3f5033bc4 | |||
| 2ec3b20b23 | |||
| d26d02babc | |||
| 675671688b | |||
| bcdd48615d | |||
| 1f974558f8 | |||
| 0f1ad59e58 | |||
| 2e31a5bbcb | |||
| d6a320490a | |||
| 125353c5a3 | |||
| cf7584bb63 | |||
| e10042a433 | |||
| 3eee8faad4 | |||
| 42eb61434d | |||
| 2962ef79dc | |||
| 87bdfbeeeb | |||
| 493b0d4a11 | |||
| 0c589a6f79 | |||
| 9b1aa3cd36 | |||
| de860ac316 | |||
| 2495871c48 | |||
| 56c1851848 | |||
| 86f9e93e97 | |||
| 11502edad2 | |||
| f6ffd574bf | |||
| 7df1060370 | |||
| ce26828e41 | |||
| 2bae30bd11 | |||
| e16db26d18 | |||
| 9f81a77943 | |||
| f9c60423a8 | |||
| 0a554661ad | |||
| 8a67a87461 | |||
| 1cd89561ab | |||
| 79592e2c27 | |||
| 4a72d40394 | |||
| 95f6e4dd3b | |||
| 778b8718c9 | |||
| b81c3b8f1b | |||
| 1d8760742e | |||
| eb00ceb1c7 | |||
| 33f8ea5acb | |||
| 4b65dfa6ea | |||
| 0187f1780e | |||
| 4a5f374b7c | |||
| c4e22a23ea | |||
| 8b7776b545 | |||
| f3787beade | |||
| 3607341413 | |||
| b33b14b4b7 | |||
| d09743d254 | |||
| 2361c1b211 | |||
| ff74e9035d | |||
| cb4b26c8a4 | |||
| 1640ba2e9e | |||
| ca003943a9 | |||
| 71783aea21 | |||
| b5632b0097 | |||
| c506ff6872 | |||
| fd83834fca | |||
| 973cab57ab | |||
| d1535e1789 | |||
| 60e1f15e42 | |||
| 6531d40386 | |||
| c940e0bc92 | |||
| 86e3776fff | |||
| 469b29c941 | |||
| 1b57b272f9 | |||
| c906307a33 | |||
| b914613e80 | |||
| ba99d83d17 | |||
| a58b58cd95 | |||
| 96d41ae8f6 | |||
| e3afab765d | |||
| 507a653d81 | |||
| b1a39e9b38 | |||
| 19846c7e01 | |||
| e3e8783eb8 | |||
| e9db53c973 | |||
| fd0645fbb4 | |||
| 2849cbf257 | |||
| b2459d8f48 | |||
| 935189f3f7 | |||
| b647017c43 | |||
| 3a8a603527 | |||
| b6d10656f9 | |||
| 7de31d8cb6 | |||
| cac84a8795 | |||
| a227f6dcb9 | |||
| 9ff4acf092 | |||
| 128b1fe9bc | |||
| 9a92372c3e | |||
| 0a36869b3c | |||
| a9a38f88bb | |||
| aca1fcad18 | |||
| 24bc878c27 | |||
| b1a9fbc6fd | |||
| 8a4c635c97 | |||
| 16d5f5c299 | |||
| 69a5a0434a | |||
| 6d1f3a5729 | |||
| b725400428 | |||
| 9f7d2be463 | |||
| fdee510c8c | |||
| 76ac1bd8f7 | |||
| 362658339a | |||
| 925d7e2a25 | |||
| 089477eb1e | |||
| f153f77a7e | |||
| a34141c912 | |||
| 94374e7de2 | |||
| bdf6748956 | |||
| d6dcb471f9 | |||
| 2c0391da81 | |||
| 77c2255da4 | |||
| 5ce7261678 | |||
| 001253b32d | |||
| 2480822690 | |||
| 16b9f49cc8 | |||
| 1295d37ff6 | |||
| 7119c8155a | |||
| cef8791c82 | |||
| e417b57123 | |||
| c827aeaab2 | |||
| f271cf737c | |||
| 1f9a8276b1 | |||
| 06b17128fd | |||
| 6fb878d3b6 | |||
| 80f49aecd7 | |||
| 5ab98c9d05 | |||
| df7fd26907 | |||
| 86d8e10dcb | |||
| d258d9474a |
@@ -0,0 +1,4 @@
|
|||||||
|
/src/renderer/src/store/ @0xfullex
|
||||||
|
/src/main/services/ConfigManager.ts @0xfullex
|
||||||
|
/packages/shared/IpcChannel.ts @0xfullex
|
||||||
|
/src/main/ipc.ts @0xfullex
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
name: 🐛 错误报告 (中文)
|
|
||||||
description: 创建一个报告以帮助我们改进
|
|
||||||
title: '[错误]: '
|
|
||||||
labels: ['BUG']
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢您花时间填写此错误报告!
|
|
||||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: checklist
|
|
||||||
attributes:
|
|
||||||
label: 提交前检查
|
|
||||||
description: |
|
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
|
||||||
options:
|
|
||||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
|
||||||
required: true
|
|
||||||
- label: 我的问题不是 [常见问题](https://github.com/CherryHQ/cherry-studio/issues/3881) 中的内容。
|
|
||||||
required: true
|
|
||||||
- label: 我已经查看了 **置顶 Issue** 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
|
||||||
required: true
|
|
||||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
|
||||||
required: true
|
|
||||||
- label: 我确认我正在使用最新版本的 Cherry Studio。
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: 平台
|
|
||||||
description: 您正在使用哪个平台?
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- macOS
|
|
||||||
- Linux
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: 版本
|
|
||||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
|
||||||
placeholder: 例如 v1.0.0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: 错误描述
|
|
||||||
description: 描述问题时请尽可能详细。请尽可能提供截图或屏幕录制,以帮助我们更好地理解问题。
|
|
||||||
placeholder: 告诉我们发生了什么...(记得附上截图/录屏,如果适用)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: reproduction
|
|
||||||
attributes:
|
|
||||||
label: 重现步骤
|
|
||||||
description: 提供详细的重现步骤,以便于我们的开发人员可以准确地重现问题。请尽可能为每个步骤提供截图或屏幕录制。
|
|
||||||
placeholder: |
|
|
||||||
1. 转到 '...'
|
|
||||||
2. 点击 '....'
|
|
||||||
3. 向下滚动到 '....'
|
|
||||||
4. 看到错误
|
|
||||||
|
|
||||||
记得尽可能为每个步骤附上截图/录屏!
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: 预期行为
|
|
||||||
description: 清晰简洁地描述您期望发生的事情
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: logs
|
|
||||||
attributes:
|
|
||||||
label: 相关日志输出
|
|
||||||
description: 请复制并粘贴任何相关的日志输出
|
|
||||||
render: shell
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: 附加信息
|
|
||||||
description: 任何能让我们对你所遇到的问题有更多了解的东西
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
name: 💡 功能建议 (中文)
|
|
||||||
description: 为项目提出新的想法
|
|
||||||
title: '[功能]: '
|
|
||||||
labels: ['feature']
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢您花时间提出新的功能建议!
|
|
||||||
在提交此问题之前,请确保您已经了解了[项目规划](https://docs.cherry-ai.com/cherrystudio/planning)和[功能介绍](https://docs.cherry-ai.com/cherrystudio/preview)
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: checklist
|
|
||||||
attributes:
|
|
||||||
label: 提交前检查
|
|
||||||
description: |
|
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
|
||||||
options:
|
|
||||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
|
||||||
required: true
|
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的建议。
|
|
||||||
required: true
|
|
||||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是“一个建议”、“卡住了”等。
|
|
||||||
required: true
|
|
||||||
- label: 最新的 Cherry Studio 版本没有实现我所提出的功能。
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: 平台
|
|
||||||
description: 您正在使用哪个平台?
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- macOS
|
|
||||||
- Linux
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: 版本
|
|
||||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
|
||||||
placeholder: 例如 v1.0.0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: problem
|
|
||||||
attributes:
|
|
||||||
label: 您的功能建议是否与某个问题/issue相关?
|
|
||||||
description: 请简明扼要地描述您遇到的问题
|
|
||||||
placeholder: 我总是感到沮丧,因为...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: solution
|
|
||||||
attributes:
|
|
||||||
label: 请描述您希望实现的解决方案
|
|
||||||
description: 请简明扼要地描述您希望发生的情况
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: alternatives
|
|
||||||
attributes:
|
|
||||||
label: 请描述您考虑过的其他方案
|
|
||||||
description: 请简明扼要地描述您考虑过的任何其他解决方案或功能
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: 其他补充信息
|
|
||||||
description: 在此添加任何其他与功能建议相关的上下文或截图
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
name: ❓ 提问 & 讨论 (中文)
|
|
||||||
description: 寻求帮助、讨论问题、提出疑问等...
|
|
||||||
title: '[讨论]: '
|
|
||||||
labels: ['discussion', 'help wanted']
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢您的提问!请尽可能详细地描述您的问题,这样我们才能更好地帮助您。
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: checklist
|
|
||||||
attributes:
|
|
||||||
label: Issue 检查清单
|
|
||||||
description: |
|
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
|
||||||
options:
|
|
||||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
|
||||||
required: true
|
|
||||||
- label: 我确认自己需要的是提出问题并且讨论问题,而不是 Bug 反馈或需求建议。
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: 平台
|
|
||||||
description: 您正在使用哪个平台?
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- macOS
|
|
||||||
- Linux
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: 版本
|
|
||||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
|
||||||
placeholder: 例如 v1.0.0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: question
|
|
||||||
attributes:
|
|
||||||
label: 您的问题
|
|
||||||
description: 请详细描述您的问题
|
|
||||||
placeholder: 请尽可能清楚地说明您的问题...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: 相关背景
|
|
||||||
description: 请提供一些背景信息,帮助我们更好地理解您的问题
|
|
||||||
placeholder: 例如:使用场景、已尝试的解决方案等
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: 补充信息
|
|
||||||
description: 任何其他相关的信息、截图或代码示例
|
|
||||||
render: shell
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: priority
|
|
||||||
attributes:
|
|
||||||
label: 优先级
|
|
||||||
description: 这个问题对您来说有多紧急?
|
|
||||||
options:
|
|
||||||
- 低 (有空再看)
|
|
||||||
- 中 (希望尽快得到答复)
|
|
||||||
- 高 (阻碍工作进行)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
name: 🤔 其他问题 (中文)
|
|
||||||
description: 提交不属于错误报告或功能需求的问题
|
|
||||||
title: '[其他]: '
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
感谢您花时间提出问题!
|
|
||||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: checklist
|
|
||||||
attributes:
|
|
||||||
label: 提交前检查
|
|
||||||
description: |
|
|
||||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
|
||||||
options:
|
|
||||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
|
||||||
required: true
|
|
||||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
|
||||||
required: true
|
|
||||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是"一个问题"、"求助"等。
|
|
||||||
required: true
|
|
||||||
- label: 我的问题不属于错误报告或功能需求类别。
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: 平台
|
|
||||||
description: 您正在使用哪个平台?
|
|
||||||
options:
|
|
||||||
- Windows
|
|
||||||
- macOS
|
|
||||||
- Linux
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: 版本
|
|
||||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
|
||||||
placeholder: 例如 v1.0.0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: question
|
|
||||||
attributes:
|
|
||||||
label: 问题描述
|
|
||||||
description: 请详细描述您的问题或疑问
|
|
||||||
placeholder: 我想了解有关...的更多信息
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: 相关背景
|
|
||||||
description: 请提供与您的问题相关的任何背景信息或上下文
|
|
||||||
placeholder: 我尝试实现...时遇到了疑问
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: attempts
|
|
||||||
attributes:
|
|
||||||
label: 您已尝试的方法
|
|
||||||
description: 请描述您为解决问题已经尝试过的方法(如果有)
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: additional
|
|
||||||
attributes:
|
|
||||||
label: 附加信息
|
|
||||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Auto I18N
|
||||||
|
|
||||||
|
env:
|
||||||
|
API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||||
|
MODEL: ${{ vars.MODEL || 'deepseek/deepseek-v3.1'}}
|
||||||
|
BASE_URL: ${{ vars.BASE_URL || 'https://api.ppinfra.com/openai'}}
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto-i18n:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.head.repo.full_name == 'CherryHQ/cherry-studio'
|
||||||
|
name: Auto I18N
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 🐈⬛ Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
|
- name: 📦 Setting Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: 📦 Install dependencies in isolated directory
|
||||||
|
run: |
|
||||||
|
# 在临时目录安装依赖
|
||||||
|
mkdir -p /tmp/translation-deps
|
||||||
|
cd /tmp/translation-deps
|
||||||
|
echo '{"dependencies": {"openai": "^5.12.2", "cli-progress": "^3.12.0", "tsx": "^4.20.3", "@biomejs/biome": "2.2.4"}}' > package.json
|
||||||
|
npm install --no-package-lock
|
||||||
|
|
||||||
|
# 设置 NODE_PATH 让项目能找到这些依赖
|
||||||
|
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: 🏃♀️ Translate
|
||||||
|
run: npx tsx scripts/auto-translate-i18n.ts
|
||||||
|
|
||||||
|
- name: 🔍 Format
|
||||||
|
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/
|
||||||
|
|
||||||
|
- name: 🔄 Commit changes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add .
|
||||||
|
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
else
|
||||||
|
git commit -m "fix(i18n): Auto update translations for PR #${{ github.event.pull_request.number }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🚀 Push changes
|
||||||
|
uses: ad-m/github-push-action@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: ${{ github.event.pull_request.head.ref }}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Claude Code Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
# Optional: Only run on specific file changes
|
||||||
|
# paths:
|
||||||
|
# - "src/**/*.ts"
|
||||||
|
# - "src/**/*.tsx"
|
||||||
|
# - "src/**/*.js"
|
||||||
|
# - "src/**/*.jsx"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-review:
|
||||||
|
# Only trigger code review for PRs from the main repository due to upstream OIDC issues
|
||||||
|
# https://github.com/anthropics/claude-code-action/issues/542
|
||||||
|
if: |
|
||||||
|
(github.event.pull_request.head.repo.full_name == github.repository) &&
|
||||||
|
(github.event.pull_request.draft == false)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run Claude Code Review
|
||||||
|
id: claude-review
|
||||||
|
uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
prompt: |
|
||||||
|
Please review this pull request and provide feedback on:
|
||||||
|
- Code quality and best practices
|
||||||
|
- Potential bugs or issues
|
||||||
|
- Performance considerations
|
||||||
|
- Security concerns
|
||||||
|
- Test coverage
|
||||||
|
|
||||||
|
PR number: ${{ github.event.number }}
|
||||||
|
Repo: ${{ github.repository }}
|
||||||
|
|
||||||
|
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
||||||
|
|
||||||
|
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
||||||
|
|
||||||
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
|
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||||
|
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Claude Translator
|
||||||
|
concurrency:
|
||||||
|
group: translator-${{ github.event.comment.id || github.event.issue.number || github.event.review.id }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted, edited]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
translate:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issues') ||
|
||||||
|
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
||||||
|
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
|
||||||
|
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write # 编辑issues/comments
|
||||||
|
pull-requests: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run Claude for translation
|
||||||
|
uses: anthropics/claude-code-action@main
|
||||||
|
id: claude
|
||||||
|
with:
|
||||||
|
# Warning: Permissions should have been controlled by workflow permission.
|
||||||
|
# Now `contents: read` is safe for files, but we could make a fine-grained token to control it.
|
||||||
|
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/security.md
|
||||||
|
github_token: ${{ secrets.TOKEN_GITHUB_WRITE }}
|
||||||
|
allowed_non_write_users: "*"
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:repos/*/issues:*),Bash(gh api:repos/*/pulls/*/reviews/*),Bash(gh api:repos/*/pulls/comments/*)"
|
||||||
|
prompt: |
|
||||||
|
你是一个多语言翻译助手。你需要响应 GitHub Webhooks 中的以下四种事件:
|
||||||
|
|
||||||
|
- issues
|
||||||
|
- issue_comment
|
||||||
|
- pull_request_review
|
||||||
|
- pull_request_review_comment
|
||||||
|
|
||||||
|
请完成以下任务:
|
||||||
|
|
||||||
|
1. 获取当前事件的完整信息。
|
||||||
|
|
||||||
|
- 如果当前事件是 issues,就获取该 issues 的信息。
|
||||||
|
- 如果当前事件是 issue_comment,就获取该 comment 的信息。
|
||||||
|
- 如果当前事件是 pull_request_review,就获取该 review 的信息。
|
||||||
|
- 如果当前事件是 pull_request_review_comment,就获取该 comment 的信息。
|
||||||
|
|
||||||
|
2. 智能检测内容。
|
||||||
|
|
||||||
|
- 如果获取到的信息是已经遵循格式要求翻译过的内容,则检查翻译内容和原始内容是否匹配。若不匹配,则重新翻译一次令其匹配,并遵循格式要求;
|
||||||
|
- 如果获取到的信息是未翻译过的内容,检查其内容语言。若不是英文,则翻译成英文;
|
||||||
|
- 如果获取到的信息是部分翻译为英文的内容,则将其翻译为英文;
|
||||||
|
- 如果获取到的信息包含了对已翻译内容的引用,则将引用内容清理为仅含英文的内容。引用的内容不能够包含"This xxx was translated by Claude"和"Original Content`等内容。
|
||||||
|
- 如果获取到的信息包含了其他类型的引用,即对非 Claude 翻译的内容的引用,则直接照原样引用,不进行翻译。
|
||||||
|
- 如果获取到的信息是通过邮件回复的内容,则在翻译时应当将邮件内容的引用放到最后。在原始内容和翻译内容中只需要回复的内容本身,不要包含对邮件内容的引用。
|
||||||
|
- 如果获取到的信息本身不需要任何处理,则跳过任务。
|
||||||
|
|
||||||
|
3. 格式要求:
|
||||||
|
|
||||||
|
- 标题:英文翻译(如果非英文)
|
||||||
|
- 内容格式:
|
||||||
|
> [!NOTE]
|
||||||
|
> This issue/comment/review was translated by Claude.
|
||||||
|
|
||||||
|
[翻译内容]
|
||||||
|
|
||||||
|
---
|
||||||
|
<details>
|
||||||
|
<summary>Original Content</summary>
|
||||||
|
[原始内容]
|
||||||
|
</details>
|
||||||
|
|
||||||
|
4. 使用gh工具更新:
|
||||||
|
|
||||||
|
- 根据环境信息中的Event类型选择正确的命令:
|
||||||
|
- 如果 Event 是 'issues': gh issue edit [ISSUE_NUMBER] --title "[英文标题]" --body "[翻译内容 + 原始内容]"
|
||||||
|
- 如果 Event 是 'issue_comment': gh api -X PATCH /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]"
|
||||||
|
- 如果 Event 是 'pull_request_review': gh api -X PUT /repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${{ github.event.review.id }} -f body="[翻译内容]"
|
||||||
|
- 如果 Event 是 'pull_request_review_comment': gh api -X PATCH /repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }} -f body="[翻译内容 + 原始内容]"
|
||||||
|
|
||||||
|
环境信息:
|
||||||
|
- Event: ${{ github.event_name }}
|
||||||
|
- Issue Number: ${{ github.event.issue.number }}
|
||||||
|
- Repository: ${{ github.repository }}
|
||||||
|
- (Review) Comment ID: ${{ github.event.comment.id || 'N/A' }}
|
||||||
|
- Pull Request Number: ${{ github.event.pull_request.number || 'N/A' }}
|
||||||
|
- Review ID: ${{ github.event.review.id || 'N/A' }}
|
||||||
|
|
||||||
|
|
||||||
|
使用以下命令获取完整信息:
|
||||||
|
gh issue view ${{ github.event.issue.number }} --json title,body,comments
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Claude Code
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment'
|
||||||
|
&& contains(github.event.comment.body, '@claude')
|
||||||
|
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
|
||||||
|
||
|
||||||
|
(github.event_name == 'pull_request_review_comment'
|
||||||
|
&& contains(github.event.comment.body, '@claude')
|
||||||
|
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.comment.author_association))
|
||||||
|
||
|
||||||
|
(github.event_name == 'pull_request_review'
|
||||||
|
&& contains(github.event.review.body, '@claude')
|
||||||
|
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.review.author_association))
|
||||||
|
||
|
||||||
|
(github.event_name == 'issues'
|
||||||
|
&& (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))
|
||||||
|
&& contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.issue.author_association))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Run Claude Code
|
||||||
|
id: claude
|
||||||
|
uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
|
||||||
|
# This is an optional setting that allows Claude to read CI results on PRs
|
||||||
|
additional_permissions: |
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||||
|
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||||
|
|
||||||
|
# Optional: Add claude_args to customize behavior and configuration
|
||||||
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
|
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
||||||
|
# claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
|
||||||
@@ -94,7 +94,6 @@ 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:npm linux
|
|
||||||
yarn build:linux
|
yarn build:linux
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -107,7 +106,6 @@ jobs:
|
|||||||
- name: Build Mac
|
- name: Build Mac
|
||||||
if: matrix.os == 'macos-latest'
|
if: matrix.os == 'macos-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:npm mac
|
|
||||||
yarn build:mac
|
yarn build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
@@ -125,7 +123,6 @@ jobs:
|
|||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:npm windows
|
|
||||||
yarn build:win
|
yarn build:win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
|
- v2
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -48,6 +49,9 @@ jobs:
|
|||||||
- name: Lint Check
|
- name: Lint Check
|
||||||
run: yarn test:lint
|
run: yarn test:lint
|
||||||
|
|
||||||
|
- name: Format Check
|
||||||
|
run: yarn format:check
|
||||||
|
|
||||||
- name: Type Check
|
- name: Type Check
|
||||||
run: yarn typecheck
|
run: yarn typecheck
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ 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:npm linux
|
|
||||||
yarn build:linux
|
yarn build:linux
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -95,7 +94,6 @@ 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:npm mac
|
|
||||||
yarn build:mac
|
yarn build:mac
|
||||||
env:
|
env:
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
@@ -113,7 +111,6 @@ jobs:
|
|||||||
- name: Build Windows
|
- name: Build Windows
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
yarn build:npm windows
|
|
||||||
yarn build:win
|
yarn build:win
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dist
|
|||||||
out
|
out
|
||||||
mcp_server
|
mcp_server
|
||||||
stats.html
|
stats.html
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
# ENV
|
# ENV
|
||||||
.env
|
.env
|
||||||
@@ -60,8 +61,13 @@ coverage
|
|||||||
.vitest-cache
|
.vitest-cache
|
||||||
vitest.config.*.timestamp-*
|
vitest.config.*.timestamp-*
|
||||||
|
|
||||||
|
# TypeScript incremental build
|
||||||
|
.tsbuildinfo
|
||||||
|
|
||||||
# playwright
|
# playwright
|
||||||
playwright-report
|
playwright-report
|
||||||
test-results
|
test-results
|
||||||
|
|
||||||
YOUR_MEMORY_FILE_PATH
|
YOUR_MEMORY_FILE_PATH
|
||||||
|
|
||||||
|
.sessions/
|
||||||
|
|||||||
+215
@@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"categories": {},
|
||||||
|
"env": {
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"globals": {},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules/**",
|
||||||
|
"build/**",
|
||||||
|
"dist/**",
|
||||||
|
"out/**",
|
||||||
|
"local/**",
|
||||||
|
".yarn/**",
|
||||||
|
".gitignore",
|
||||||
|
"scripts/cloudflare-worker.js",
|
||||||
|
"src/main/integration/nutstore/sso/lib/**",
|
||||||
|
"src/main/integration/cherryin/index.js",
|
||||||
|
"src/main/integration/nutstore/sso/lib/**",
|
||||||
|
"src/renderer/src/ui/**",
|
||||||
|
"packages/**/dist",
|
||||||
|
"eslint.config.mjs"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
// set different env
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/renderer/**/*.{ts,tsx}",
|
||||||
|
"packages/aiCore/**",
|
||||||
|
"packages/extension-table-plus/**",
|
||||||
|
"resources/js/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"vitest": true
|
||||||
|
},
|
||||||
|
"files": ["**/__tests__/*.test.{ts,tsx}", "tests/**"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"files": ["src/preload/**"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// We don't use the React plugin here because its behavior differs slightly from that of ESLint's React plugin.
|
||||||
|
"plugins": ["unicorn", "typescript", "oxc", "import"],
|
||||||
|
"rules": {
|
||||||
|
"constructor-super": "error",
|
||||||
|
"for-direction": "error",
|
||||||
|
"getter-return": "error",
|
||||||
|
"no-array-constructor": "off",
|
||||||
|
// "import/no-cycle": "error", // tons of error, bro
|
||||||
|
"no-async-promise-executor": "error",
|
||||||
|
"no-caller": "warn",
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-class-assign": "error",
|
||||||
|
"no-compare-neg-zero": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-const-assign": "error",
|
||||||
|
"no-constant-binary-expression": "error",
|
||||||
|
"no-constant-condition": "error",
|
||||||
|
"no-control-regex": "error",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-delete-var": "error",
|
||||||
|
"no-dupe-args": "error",
|
||||||
|
"no-dupe-class-members": "error",
|
||||||
|
"no-dupe-else-if": "error",
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
"no-empty-pattern": "error",
|
||||||
|
"no-empty-static-block": "error",
|
||||||
|
"no-eval": "warn",
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
"no-extra-boolean-cast": "error",
|
||||||
|
"no-fallthrough": "warn",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-global-assign": "error",
|
||||||
|
"no-import-assign": "error",
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-loss-of-precision": "error",
|
||||||
|
"no-misleading-character-class": "error",
|
||||||
|
"no-new-native-nonconstructor": "error",
|
||||||
|
"no-nonoctal-decimal-escape": "error",
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
"no-octal": "error",
|
||||||
|
"no-prototype-builtins": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-regex-spaces": "error",
|
||||||
|
"no-self-assign": "error",
|
||||||
|
"no-setter-return": "error",
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
"no-this-before-super": "error",
|
||||||
|
"no-unassigned-vars": "warn",
|
||||||
|
"no-undef": "error",
|
||||||
|
"no-unexpected-multiline": "error",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unsafe-finally": "error",
|
||||||
|
"no-unsafe-negation": "error",
|
||||||
|
"no-unsafe-optional-chaining": "error",
|
||||||
|
"no-unused-expressions": "off", // this rule disallow us to use expression to call function, like `condition && fn()`
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-unused-private-class-members": "error",
|
||||||
|
"no-unused-vars": ["error", { "caughtErrors": "none" }],
|
||||||
|
"no-useless-backreference": "error",
|
||||||
|
"no-useless-catch": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-useless-rename": "warn",
|
||||||
|
"no-with": "error",
|
||||||
|
"oxc/bad-array-method-on-arguments": "warn",
|
||||||
|
"oxc/bad-char-at-comparison": "warn",
|
||||||
|
"oxc/bad-comparison-sequence": "warn",
|
||||||
|
"oxc/bad-min-max-func": "warn",
|
||||||
|
"oxc/bad-object-literal-comparison": "warn",
|
||||||
|
"oxc/bad-replace-all-arg": "warn",
|
||||||
|
"oxc/const-comparisons": "warn",
|
||||||
|
"oxc/double-comparisons": "warn",
|
||||||
|
"oxc/erasing-op": "warn",
|
||||||
|
"oxc/missing-throw": "warn",
|
||||||
|
"oxc/number-arg-out-of-range": "warn",
|
||||||
|
"oxc/only-used-in-recursion": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||||
|
"oxc/uninvoked-array-callback": "warn",
|
||||||
|
"require-yield": "error",
|
||||||
|
"typescript/await-thenable": "warn",
|
||||||
|
// "typescript/ban-ts-comment": "error",
|
||||||
|
"typescript/no-array-constructor": "error",
|
||||||
|
// "typescript/consistent-type-imports": "error",
|
||||||
|
"typescript/no-array-delete": "warn",
|
||||||
|
"typescript/no-base-to-string": "warn",
|
||||||
|
"typescript/no-duplicate-enum-values": "error",
|
||||||
|
"typescript/no-duplicate-type-constituents": "warn",
|
||||||
|
"typescript/no-empty-object-type": "off",
|
||||||
|
"typescript/no-explicit-any": "off", // not safe but too many errors
|
||||||
|
"typescript/no-extra-non-null-assertion": "error",
|
||||||
|
"typescript/no-floating-promises": "warn",
|
||||||
|
"typescript/no-for-in-array": "warn",
|
||||||
|
"typescript/no-implied-eval": "warn",
|
||||||
|
"typescript/no-meaningless-void-operator": "warn",
|
||||||
|
"typescript/no-misused-new": "error",
|
||||||
|
"typescript/no-misused-spread": "warn",
|
||||||
|
"typescript/no-namespace": "error",
|
||||||
|
"typescript/no-non-null-asserted-optional-chain": "off", // it's off now. but may turn it on.
|
||||||
|
"typescript/no-redundant-type-constituents": "warn",
|
||||||
|
"typescript/no-require-imports": "off",
|
||||||
|
"typescript/no-this-alias": "error",
|
||||||
|
"typescript/no-unnecessary-parameter-property-assignment": "warn",
|
||||||
|
"typescript/no-unnecessary-type-constraint": "error",
|
||||||
|
"typescript/no-unsafe-declaration-merging": "error",
|
||||||
|
"typescript/no-unsafe-function-type": "error",
|
||||||
|
"typescript/no-unsafe-unary-minus": "warn",
|
||||||
|
"typescript/no-useless-empty-export": "warn",
|
||||||
|
"typescript/no-wrapper-object-types": "error",
|
||||||
|
"typescript/prefer-as-const": "error",
|
||||||
|
"typescript/prefer-namespace-keyword": "error",
|
||||||
|
"typescript/require-array-sort-compare": "warn",
|
||||||
|
"typescript/restrict-template-expressions": "warn",
|
||||||
|
"typescript/triple-slash-reference": "error",
|
||||||
|
"typescript/unbound-method": "warn",
|
||||||
|
"unicorn/no-await-in-promise-methods": "warn",
|
||||||
|
"unicorn/no-empty-file": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||||
|
"unicorn/no-invalid-fetch-options": "warn",
|
||||||
|
"unicorn/no-invalid-remove-event-listener": "warn",
|
||||||
|
"unicorn/no-new-array": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||||
|
"unicorn/no-single-promise-in-promise-methods": "warn",
|
||||||
|
"unicorn/no-thenable": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||||
|
"unicorn/no-unnecessary-await": "warn",
|
||||||
|
"unicorn/no-useless-fallback-in-spread": "warn",
|
||||||
|
"unicorn/no-useless-length-check": "warn",
|
||||||
|
"unicorn/no-useless-spread": "off", // manually off bacause of existing warning. may turn it on in the future
|
||||||
|
"unicorn/prefer-set-size": "warn",
|
||||||
|
"unicorn/prefer-string-starts-ends-with": "warn",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-typeof": "error"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"jsdoc": {
|
||||||
|
"augmentsExtendsReplacesDocs": false,
|
||||||
|
"exemptDestructuredRootsFromChecks": false,
|
||||||
|
"ignoreInternal": false,
|
||||||
|
"ignorePrivate": false,
|
||||||
|
"ignoreReplacesDocs": true,
|
||||||
|
"implementsReplacesDocs": false,
|
||||||
|
"overrideReplacesDocs": true,
|
||||||
|
"tagNamePreference": {}
|
||||||
|
},
|
||||||
|
"jsx-a11y": {
|
||||||
|
"attributes": {},
|
||||||
|
"components": {},
|
||||||
|
"polymorphicPropName": null
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"rootDir": []
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"formComponents": [],
|
||||||
|
"linkComponents": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
out
|
|
||||||
dist
|
|
||||||
pnpm-lock.yaml
|
|
||||||
LICENSE.md
|
|
||||||
tsconfig.json
|
|
||||||
tsconfig.*.json
|
|
||||||
CHANGELOG*.md
|
|
||||||
agents.json
|
|
||||||
src/renderer/src/integration/nutstore/sso/lib
|
|
||||||
src/main/integration/cherryin/index.js
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"bracketSameLine": true,
|
|
||||||
"endOfLine": "lf",
|
|
||||||
"jsonRecursiveSort": true,
|
|
||||||
"jsonSortOrder": "{\"*\": \"lexical\"}",
|
|
||||||
"plugins": ["prettier-plugin-sort-json"],
|
|
||||||
"printWidth": 120,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none"
|
|
||||||
}
|
|
||||||
Vendored
+5
-2
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"lokalise.i18n-ally"
|
"lokalise.i18n-ally",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"vitest.explorer",
|
||||||
|
"oxc.oxc-vscode",
|
||||||
|
"biomejs.biome"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+14
-9
@@ -1,38 +1,43 @@
|
|||||||
{
|
{
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[markdown]": {
|
"[markdown]": {
|
||||||
"files.trimTrailingWhitespace": false
|
"files.trimTrailingWhitespace": false
|
||||||
},
|
},
|
||||||
"[scss]": {
|
"[scss]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.oxc": "explicit",
|
||||||
"source.organizeImports": "never"
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
},
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"i18n-ally.displayLanguage": "zh-cn",
|
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
|
||||||
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
|
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
|
||||||
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
|
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
|
||||||
"i18n-ally.fullReloadOnChanged": true, // 界面显示语言
|
"i18n-ally.fullReloadOnChanged": true,
|
||||||
"i18n-ally.keystyle": "nested", // 翻译路径格式
|
"i18n-ally.keystyle": "nested", // 翻译路径格式
|
||||||
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
|
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
|
||||||
// "i18n-ally.namespace": true, // 开启命名空间
|
// "i18n-ally.namespace": true, // 开启命名空间
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
diff --git a/sdk.mjs b/sdk.mjs
|
||||||
|
index e2dbafb4e2faa1bf2b6b02f0009a2b9bbf57c757..3f07a1d5c2949a246fe5414e69ab45942fa605a2 100644
|
||||||
|
--- a/sdk.mjs
|
||||||
|
+++ b/sdk.mjs
|
||||||
|
@@ -6355,11 +6355,11 @@ class ProcessTransport {
|
||||||
|
prompt,
|
||||||
|
additionalDirectories = [],
|
||||||
|
cwd,
|
||||||
|
- executable = isRunningWithBun() ? "bun" : "node",
|
||||||
|
+ executable = process.execPath,
|
||||||
|
executableArgs = [],
|
||||||
|
extraArgs = {},
|
||||||
|
pathToClaudeCodeExecutable,
|
||||||
|
- env = { ...process.env },
|
||||||
|
+ env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||||
|
stderr,
|
||||||
|
customSystemPrompt,
|
||||||
|
appendSystemPrompt,
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
diff --git a/index.js b/index.js
|
||||||
|
index dc071739e79876dff88e1be06a9168e294222d13..b9df7525c62bdf777e89e732e1b0c81f84d872f2 100644
|
||||||
|
--- a/index.js
|
||||||
|
+++ b/index.js
|
||||||
|
@@ -380,7 +380,7 @@ if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-if (!nativeBinding) {
|
||||||
|
+if (!nativeBinding && process.platform !== 'linux') {
|
||||||
|
if (loadErrors.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find native binding. ` +
|
||||||
|
@@ -392,6 +392,13 @@ if (!nativeBinding) {
|
||||||
|
throw new Error(`Failed to load native binding`)
|
||||||
|
}
|
||||||
|
|
||||||
|
-module.exports = nativeBinding
|
||||||
|
-module.exports.OcrAccuracy = nativeBinding.OcrAccuracy
|
||||||
|
-module.exports.recognize = nativeBinding.recognize
|
||||||
|
+if (process.platform === 'linux') {
|
||||||
|
+ module.exports = {OcrAccuracy: {
|
||||||
|
+ Fast: 0,
|
||||||
|
+ Accurate: 1
|
||||||
|
+ }, recognize: () => Promise.resolve({text: '', confidence: 1.0})}
|
||||||
|
+}else{
|
||||||
|
+ module.exports = nativeBinding
|
||||||
|
+ module.exports.OcrAccuracy = nativeBinding.OcrAccuracy
|
||||||
|
+ module.exports.recognize = nativeBinding.recognize
|
||||||
|
+}
|
||||||
@@ -5,3 +5,5 @@ httpTimeout: 300000
|
|||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||||
|
npmRegistryServer: https://registry.npmjs.org
|
||||||
|
npmPublishRegistry: https://registry.npmjs.org
|
||||||
|
|||||||
@@ -1,120 +1,52 @@
|
|||||||
# CLAUDE.md
|
# AI Assistant Guide
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to AI coding assistants when working with code in this repository. Adherence to these guidelines is crucial for maintaining code quality and consistency.
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
|
||||||
|
- **Clarity and Simplicity**: Write code that is easy to understand and maintain.
|
||||||
|
- **Consistency**: Follow existing patterns and conventions in the codebase.
|
||||||
|
- **Correctness**: Ensure code is correct, well-tested, and robust.
|
||||||
|
- **Efficiency**: Write performant code and use resources judiciously.
|
||||||
|
|
||||||
|
## MUST Follow Rules
|
||||||
|
|
||||||
|
1. **Code Search**: Use `ast-grep` for semantic code pattern searches when available. Fallback to `rg` (ripgrep) or `grep` for text-based searches.
|
||||||
|
2. **UI Framework**: Exclusively use **HeroUI** for all new UI components. The use of `antd` or `styled-components` is strictly **PROHIBITED**.
|
||||||
|
3. **Quality Assurance**: **Always** run `yarn build:check` before finalizing your work or making any commits. This ensures code quality (linting, testing, and type checking).
|
||||||
|
4. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`.
|
||||||
|
5. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand.
|
||||||
|
6. **Code Reviews**: Always seek a code review from a human developer before merging significant changes. This ensures adherence to project standards and catches potential issues.
|
||||||
|
7. **Documentation**: Update or create documentation for any new features, modules, or significant changes to existing functionality.
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Environment Setup
|
- **Install**: `yarn install`
|
||||||
|
- **Development**: `yarn dev` - Runs Electron app in development mode
|
||||||
|
- **Debug**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
||||||
|
- **Build Check**: `yarn build:check` - REQUIRED before commits (lint + test + typecheck)
|
||||||
|
- **Test**: `yarn test` - Run all tests (Vitest)
|
||||||
|
- **Single Test**: `yarn test:main` or `yarn test:renderer`
|
||||||
|
- **Lint**: `yarn lint` - Fix linting issues and run typecheck
|
||||||
|
|
||||||
- **Prerequisites**: Node.js v22.x.x or higher, Yarn 4.9.1
|
## Project Architecture
|
||||||
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.9.1 --activate`
|
|
||||||
- **Install Dependencies**: `yarn install`
|
|
||||||
|
|
||||||
### Development
|
### Electron Structure
|
||||||
|
- **Main Process** (`src/main/`): Node.js backend with services (MCP, Knowledge, Storage, etc.)
|
||||||
|
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
|
||||||
|
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
|
||||||
|
|
||||||
- **Start Development**: `yarn dev` - Runs Electron app in development mode
|
### Key Components
|
||||||
- **Debug Mode**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
|
||||||
|
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||||
### Testing & Quality
|
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
||||||
|
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||||
- **Run Tests**: `yarn test` - Runs all tests (Vitest)
|
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
||||||
- **Run E2E Tests**: `yarn test:e2e` - Playwright end-to-end tests
|
|
||||||
- **Type Check**: `yarn typecheck` - Checks TypeScript for both node and web
|
|
||||||
- **Lint**: `yarn lint` - ESLint with auto-fix
|
|
||||||
- **Format**: `yarn format` - Prettier formatting
|
|
||||||
|
|
||||||
### Build & Release
|
|
||||||
|
|
||||||
- **Build**: `yarn build` - Builds for production (includes typecheck)
|
|
||||||
- **Platform-specific builds**:
|
|
||||||
- Windows: `yarn build:win`
|
|
||||||
- macOS: `yarn build:mac`
|
|
||||||
- Linux: `yarn build:linux`
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
### Electron Multi-Process Architecture
|
|
||||||
|
|
||||||
- **Main Process** (`src/main/`): Node.js backend handling system integration, file operations, and services
|
|
||||||
- **Renderer Process** (`src/renderer/`): React-based UI running in Chromium
|
|
||||||
- **Preload Scripts** (`src/preload/`): Secure bridge between main and renderer processes
|
|
||||||
|
|
||||||
### Key Architectural Components
|
|
||||||
|
|
||||||
#### Main Process Services (`src/main/services/`)
|
|
||||||
|
|
||||||
- **MCPService**: Model Context Protocol server management
|
|
||||||
- **KnowledgeService**: Document processing and knowledge base management
|
|
||||||
- **FileStorage/S3Storage/WebDav**: Multiple storage backends
|
|
||||||
- **WindowService**: Multi-window management (main, mini, selection windows)
|
|
||||||
- **ProxyManager**: Network proxy handling
|
|
||||||
- **SearchService**: Full-text search capabilities
|
|
||||||
|
|
||||||
#### AI Core (`src/renderer/src/aiCore/`)
|
|
||||||
|
|
||||||
- **Middleware System**: Composable pipeline for AI request processing
|
|
||||||
- **Client Factory**: Supports multiple AI providers (OpenAI, Anthropic, Gemini, etc.)
|
|
||||||
- **Stream Processing**: Real-time response handling
|
|
||||||
|
|
||||||
#### State Management (`src/renderer/src/store/`)
|
|
||||||
|
|
||||||
- **Redux Toolkit**: Centralized state management
|
|
||||||
- **Persistent Storage**: Redux-persist for data persistence
|
|
||||||
- **Thunks**: Async actions for complex operations
|
|
||||||
|
|
||||||
#### Knowledge Management
|
|
||||||
|
|
||||||
- **Embeddings**: Vector search with multiple providers (OpenAI, Voyage, etc.)
|
|
||||||
- **OCR**: Document text extraction (system OCR, Doc2x, Mineru)
|
|
||||||
- **Preprocessing**: Document preparation pipeline
|
|
||||||
- **Loaders**: Support for various file formats (PDF, DOCX, EPUB, etc.)
|
|
||||||
|
|
||||||
### Build System
|
|
||||||
|
|
||||||
- **Electron-Vite**: Development and build tooling (v4.0.0)
|
|
||||||
- **Rolldown-Vite**: Using experimental rolldown-vite instead of standard vite
|
|
||||||
- **Workspaces**: Monorepo structure with `packages/` directory
|
|
||||||
- **Multiple Entry Points**: Main app, mini window, selection toolbar
|
|
||||||
- **Styled Components**: CSS-in-JS styling with SWC optimization
|
|
||||||
|
|
||||||
### Testing Strategy
|
|
||||||
|
|
||||||
- **Vitest**: Unit and integration testing
|
|
||||||
- **Playwright**: End-to-end testing
|
|
||||||
- **Component Testing**: React Testing Library
|
|
||||||
- **Coverage**: Available via `yarn test:coverage`
|
|
||||||
|
|
||||||
### Key Patterns
|
|
||||||
|
|
||||||
- **IPC Communication**: Secure main-renderer communication via preload scripts
|
|
||||||
- **Service Layer**: Clear separation between UI and business logic
|
|
||||||
- **Plugin Architecture**: Extensible via MCP servers and middleware
|
|
||||||
- **Multi-language Support**: i18n with dynamic loading
|
|
||||||
- **Theme System**: Light/dark themes with custom CSS variables
|
|
||||||
|
|
||||||
## Logging Standards
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
|
### Logging
|
||||||
```typescript
|
```typescript
|
||||||
// Main process
|
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
const logger = loggerService.withContext('moduleName')
|
const logger = loggerService.withContext('moduleName')
|
||||||
|
// Renderer: loggerService.initWindowSource('windowName') first
|
||||||
// Renderer process (set window source first)
|
|
||||||
loggerService.initWindowSource('windowName')
|
|
||||||
const logger = loggerService.withContext('moduleName')
|
|
||||||
|
|
||||||
// Logging
|
|
||||||
logger.info('message', CONTEXT)
|
logger.info('message', CONTEXT)
|
||||||
logger.error('message', new Error('error'), CONTEXT)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Log Levels (highest to lowest)
|
|
||||||
|
|
||||||
- `error` - Critical errors causing crash/unusable functionality
|
|
||||||
- `warn` - Potential issues that don't affect core functionality
|
|
||||||
- `info` - Application lifecycle and key user actions
|
|
||||||
- `verbose` - Detailed flow information for feature tracing
|
|
||||||
- `debug` - Development diagnostic info (not for production)
|
|
||||||
- `silly` - Extreme debugging, low-level information
|
|
||||||
|
|||||||
@@ -1,48 +1,3 @@
|
|||||||
**许可协议 (Licensing)**
|
|
||||||
|
|
||||||
本项目采用**区分用户的双重许可 (User-Segmented Dual Licensing)** 模式。
|
|
||||||
|
|
||||||
**核心原则:**
|
|
||||||
|
|
||||||
* **个人用户 和 10人及以下企业/组织:** 默认适用 **GNU Affero 通用公共许可证 v3.0 (AGPLv3)**。
|
|
||||||
* **超过10人的企业/组织:** **必须** 获取 **商业许可证 (Commercial License)**。
|
|
||||||
|
|
||||||
定义:“10人及以下”
|
|
||||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织**
|
|
||||||
|
|
||||||
* 如果您是个人用户,或者您的组织满足上述“10人及以下”的定义,您可以在 **AGPLv3** 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问 [https://www.gnu.org/licenses/agpl-3.0.html](https://www.gnu.org/licenses/agpl-3.0.html) 获取。
|
|
||||||
* **核心义务:** AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3 许可证向接收者提供相应的**完整源代码**。即使您符合“10人及以下”的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。
|
|
||||||
* 使用前请务必仔细阅读并理解 AGPLv3 的所有条款。
|
|
||||||
|
|
||||||
**2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3 义务的用户**
|
|
||||||
|
|
||||||
* **强制要求:** 如果您的组织**不**满足上述“10人及以下”的定义(即有11人或更多人可以访问、使用或受益于本软件),您**必须**联系我们获取并签署一份商业许可证才能使用 Cherry Studio。
|
|
||||||
* **自愿选择:** 即使您的组织满足“10人及以下”的条件,但如果您的使用场景**无法满足 AGPLv3 的条款要求**(特别是关于**源代码公开**的义务),或者您需要 AGPLv3 **未提供**的特定商业条款(如保证、赔偿、无 Copyleft 限制等),您也**必须**联系我们获取并签署一份商业许可证。
|
|
||||||
* **需要商业许可证的常见情况包括(但不限于):**
|
|
||||||
* 您的组织规模超过10人。
|
|
||||||
* (无论组织规模)您希望分发修改过的 Cherry Studio 版本,但**不希望**根据 AGPLv3 公开您修改部分的源代码。
|
|
||||||
* (无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但**不希望**根据 AGPLv3 向服务使用者提供修改后的源代码。
|
|
||||||
* (无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
|
|
||||||
* 商业许可证将为您提供豁免 AGPLv3 义务(如源代码公开)的权利,并可能包含额外的商业保障条款。
|
|
||||||
* **获取商业许可:** 请通过邮箱 **bd@cherry-ai.com** 联系 Cherry Studio 开发团队洽谈商业授权事宜。
|
|
||||||
|
|
||||||
**3. 贡献 (Contributions)**
|
|
||||||
|
|
||||||
* 我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 **AGPLv3** 许可证下提供。
|
|
||||||
* 通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
|
||||||
* 您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。
|
|
||||||
|
|
||||||
**4. 其他条款 (Other Terms)**
|
|
||||||
|
|
||||||
* 关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。
|
|
||||||
* 项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Licensing**
|
**Licensing**
|
||||||
|
|
||||||
This project employs a **User-Segmented Dual Licensing** model.
|
This project employs a **User-Segmented Dual Licensing** model.
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl
|
|||||||
1. **Diverse LLM Provider Support**:
|
1. **Diverse LLM Provider Support**:
|
||||||
|
|
||||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||||
- 🔗 AI Web Service Integration: Claude, Peplexity, Poe, and others
|
- 🔗 AI Web Service Integration: Claude, Perplexity, Poe, and others
|
||||||
- 💻 Local Model Support with Ollama, LM Studio
|
- 💻 Local Model Support with Ollama, LM Studio
|
||||||
|
|
||||||
2. **AI Assistants & Conversations**:
|
2. **AI Assistants & Conversations**:
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
|
||||||
|
"assist": {
|
||||||
|
// to sort json
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on",
|
||||||
|
"useSortedKeys": {
|
||||||
|
"level": "on",
|
||||||
|
"options": {
|
||||||
|
"sortOrder": "lexicographic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"includes": ["**/*.json", "!*.json", "!**/package.json"]
|
||||||
|
},
|
||||||
|
"css": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": { "ignoreUnknown": false },
|
||||||
|
"formatter": {
|
||||||
|
"attributePosition": "auto",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"enabled": true,
|
||||||
|
"expand": "auto",
|
||||||
|
"formatWithErrors": true,
|
||||||
|
"includes": [
|
||||||
|
"**",
|
||||||
|
"!out/**",
|
||||||
|
"!**/dist/**",
|
||||||
|
"!build/**",
|
||||||
|
"!.yarn/**",
|
||||||
|
"!.github/**",
|
||||||
|
"!.husky/**",
|
||||||
|
"!.vscode/**",
|
||||||
|
"!*.yaml",
|
||||||
|
"!*.yml",
|
||||||
|
"!*.mjs",
|
||||||
|
"!*.cjs",
|
||||||
|
"!*.md",
|
||||||
|
"!*.json",
|
||||||
|
"!src/main/integration/**",
|
||||||
|
"!**/tailwind.css",
|
||||||
|
"!**/package.json"
|
||||||
|
],
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"lineWidth": 120,
|
||||||
|
"useEditorconfig": true
|
||||||
|
},
|
||||||
|
"html": { "formatter": { "selfCloseVoidElements": "always" } },
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"arrowParentheses": "always",
|
||||||
|
"attributePosition": "auto",
|
||||||
|
// To minimize changes in this PR as much as possible, it's set to true. However, setting it to false would make it more convenient to add attributes at the end.
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxQuoteStyle": "double",
|
||||||
|
"quoteProperties": "asNeeded",
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"semicolons": "asNeeded",
|
||||||
|
"trailingCommas": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"parser": {
|
||||||
|
"allowComments": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"includes": ["!**/tailwind.css", "src/renderer/**/*.{tsx,ts}"],
|
||||||
|
// only enable sorted tailwind css rule. used as formatter instead of linter
|
||||||
|
"rules": {
|
||||||
|
"nursery": {
|
||||||
|
// to sort tailwind css classes
|
||||||
|
"useSortedClasses": {
|
||||||
|
"fix": "safe",
|
||||||
|
"level": "warn",
|
||||||
|
"options": {
|
||||||
|
"functions": ["cn"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recommended": false,
|
||||||
|
"suspicious": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vcs": { "clientKind": "git", "enabled": false, "useIgnoreFile": false }
|
||||||
|
}
|
||||||
@@ -8,5 +8,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@renderer/ui/third-party",
|
||||||
|
"hooks": "@renderer/hooks",
|
||||||
|
"lib": "@renderer/lib",
|
||||||
|
"ui": "@renderer/ui",
|
||||||
|
"utils": "@renderer/utils"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rsc": false,
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"config": "",
|
||||||
|
"css": "src/renderer/src/assets/styles/tailwind.css",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"tsx": true
|
||||||
|
}
|
||||||
+2
-2
@@ -13,7 +13,7 @@
|
|||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=fr">Français</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=fr">Français</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=de">Deutsch</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=de">Deutsch</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=es">Español</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=es">Español</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=it">Itapano</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=it">Italiano</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ru">Русский</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=ru">Русский</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=pt">Português</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=pt">Português</a></p>
|
||||||
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=nl">Nederlands</a></p>
|
<p><a href="https://openaitx.github.io/view.html?user=CherryHQ&project=cherry-studio&lang=nl">Nederlands</a></p>
|
||||||
@@ -89,7 +89,7 @@ https://docs.cherry-ai.com
|
|||||||
1. **多样化 LLM 服务支持**:
|
1. **多样化 LLM 服务支持**:
|
||||||
|
|
||||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||||
- 🔗 集成流行 AI Web 服务:Claude、Peplexity、Poe、腾讯元宝、知乎直答等
|
- 🔗 集成流行 AI Web 服务:Claude、Perplexity、Poe、腾讯元宝、知乎直答等
|
||||||
- 💻 支持 Ollama、LM Studio 本地模型部署
|
- 💻 支持 Ollama、LM Studio 本地模型部署
|
||||||
|
|
||||||
2. **智能助手与对话**:
|
2. **智能助手与对话**:
|
||||||
|
|||||||
+3
-1
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
## IDE Setup
|
## IDE Setup
|
||||||
|
|
||||||
[Cursor](https://www.cursor.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
- Editor: [Cursor](https://www.cursor.com/), etc. Any VS Code compatible editor.
|
||||||
|
- Linter: [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||||
|
- Formatter: [Biome](https://marketplace.visualstudio.com/items?itemName=biomejs.biome)
|
||||||
|
|
||||||
## Project Setup
|
## Project Setup
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
| 字段名 | 类型 | 是否主键 | 索引 | 说明 |
|
| 字段名 | 类型 | 是否主键 | 索引 | 说明 |
|
||||||
| ---------- | ------ | -------- | ---- | ------------------------------------------------------------------------ |
|
| ---------- | ------ | -------- | ---- | ------------------------------------------------------------------------ |
|
||||||
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
|
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
|
||||||
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
|
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
|
||||||
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
|
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
|
||||||
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji,用户输入 |
|
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji,用户输入 |
|
||||||
|
|
||||||
> `langCode` 虽非主键,但在业务层应当避免重复插入相同语言代码。
|
> `langCode` 虽非主键,但在业务层应当避免重复插入相同语言代码。
|
||||||
|
|||||||
+61
-63
@@ -17,51 +17,52 @@ protocols:
|
|||||||
schemes:
|
schemes:
|
||||||
- cherrystudio
|
- cherrystudio
|
||||||
files:
|
files:
|
||||||
- '**/*'
|
- "**/*"
|
||||||
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
|
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
|
||||||
- '!electron.vite.config.{js,ts,mjs,cjs}}'
|
- "!electron.vite.config.{js,ts,mjs,cjs}}"
|
||||||
- '!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
|
||||||
- '!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||||
- '!**/{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"
|
||||||
- '!scripts'
|
- "!scripts"
|
||||||
- '!local'
|
- "!local"
|
||||||
- '!docs'
|
- "!docs"
|
||||||
- '!packages'
|
- "!packages"
|
||||||
- '!.swc'
|
- "!.swc"
|
||||||
- '!.bin'
|
- "!.bin"
|
||||||
- '!._*'
|
- "!._*"
|
||||||
- '!*.log'
|
- "!*.log"
|
||||||
- '!stats.html'
|
- "!stats.html"
|
||||||
- '!*.md'
|
- "!*.md"
|
||||||
- '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}'
|
- "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}"
|
||||||
- '!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}'
|
- "!**/*.{map,ts,tsx,jsx,less,scss,sass,css.d.ts,d.cts,d.mts,md,markdown,yaml,yml}"
|
||||||
- '!**/{test,tests,__tests__,powered-test,coverage}/**'
|
- "!**/{test,tests,__tests__,powered-test,coverage}/**"
|
||||||
- '!**/{example,examples}/**'
|
- "!**/{example,examples}/**"
|
||||||
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
|
- "!**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||||
- '!**/*.min.*.map'
|
- "!**/*.min.*.map"
|
||||||
- '!**/*.d.ts'
|
- "!**/*.d.ts"
|
||||||
- '!**/dist/es6/**'
|
- "!**/dist/es6/**"
|
||||||
- '!**/dist/demo/**'
|
- "!**/dist/demo/**"
|
||||||
- '!**/amd/**'
|
- "!**/amd/**"
|
||||||
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
|
- "!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}"
|
||||||
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
|
- "!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}"
|
||||||
- '!node_modules/rollup-plugin-visualizer'
|
- "!node_modules/rollup-plugin-visualizer"
|
||||||
- '!node_modules/js-tiktoken'
|
- "!node_modules/js-tiktoken"
|
||||||
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
|
- "!node_modules/@tavily/core/node_modules/js-tiktoken"
|
||||||
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
|
- "!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}"
|
||||||
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
- "!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}"
|
||||||
- '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds
|
- "!node_modules/selection-hook/prebuilds/**/*" # we rebuild .node, don't use prebuilds
|
||||||
- '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir
|
- "!node_modules/selection-hook/node_modules" # we don't need what in the node_modules dir
|
||||||
- '!node_modules/selection-hook/src' # we don't need source files
|
- "!node_modules/selection-hook/src" # we don't need source files
|
||||||
- '!node_modules/tesseract.js-core/{tesseract-core.js,tesseract-core.wasm,tesseract-core.wasm.js}' # we don't need source files
|
- "!node_modules/tesseract.js-core/{tesseract-core.js,tesseract-core.wasm,tesseract-core.wasm.js}" # we don't need source files
|
||||||
- '!node_modules/tesseract.js-core/{tesseract-core-lstm.js,tesseract-core-lstm.wasm,tesseract-core-lstm.wasm.js}' # we don't need source files
|
- "!node_modules/tesseract.js-core/{tesseract-core-lstm.js,tesseract-core-lstm.wasm,tesseract-core-lstm.wasm.js}" # we don't need source files
|
||||||
- '!node_modules/tesseract.js-core/{tesseract-core-simd-lstm.js,tesseract-core-simd-lstm.wasm,tesseract-core-simd-lstm.wasm.js}' # we don't need source files
|
- "!node_modules/tesseract.js-core/{tesseract-core-simd-lstm.js,tesseract-core-simd-lstm.wasm,tesseract-core-simd-lstm.wasm.js}" # we don't need source files
|
||||||
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
|
- "!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}" # filter .node build files
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
- '**/*.{metal,exp,lib}'
|
- "**/*.{metal,exp,lib}"
|
||||||
|
- "node_modules/@img/sharp-libvips-*/**"
|
||||||
win:
|
win:
|
||||||
executableName: Cherry Studio
|
executableName: Cherry Studio
|
||||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||||
@@ -87,7 +88,7 @@ mac:
|
|||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
notarize: false
|
notarize: false
|
||||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||||
minimumSystemVersion: '20.1.0' # 最低支持 macOS 11.0
|
minimumSystemVersion: "20.1.0" # 最低支持 macOS 11.0
|
||||||
extendInfo:
|
extendInfo:
|
||||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
@@ -109,34 +110,31 @@ linux:
|
|||||||
StartupWMClass: CherryStudio
|
StartupWMClass: CherryStudio
|
||||||
mimeTypes:
|
mimeTypes:
|
||||||
- x-scheme-handler/cherrystudio
|
- x-scheme-handler/cherrystudio
|
||||||
|
rpm:
|
||||||
|
# Workaround for electron build issue on rpm package:
|
||||||
|
# https://github.com/electron/forge/issues/3594
|
||||||
|
fpm: ["--rpm-rpmbuild-define=_build_id_links none"]
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: generic
|
||||||
url: https://releases.cherry-ai.com
|
url: https://releases.cherry-ai.com
|
||||||
electronDownload:
|
electronDownload:
|
||||||
mirror: https://npmmirror.com/mirrors/electron/
|
mirror: https://npmmirror.com/mirrors/electron/
|
||||||
|
beforePack: scripts/before-pack.js
|
||||||
afterPack: scripts/after-pack.js
|
afterPack: scripts/after-pack.js
|
||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
✨ 重要更新:
|
🐛 问题修复:
|
||||||
- 新增笔记模块,支持富文本编辑和管理
|
- 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理
|
||||||
- 内置 GLM-4.5-Flash 免费模型(由智谱开放平台提供)
|
- 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题
|
||||||
- 内置 Qwen3-8B 免费模型(由硅基流动提供)
|
- 修复 MiniWindow 高度问题
|
||||||
- 新增 Nano Banana(Gemini 2.5 Flash Image)模型支持
|
|
||||||
- 新增系统 OCR 功能 (macOS & Windows)
|
|
||||||
- 新增图片 OCR 识别和翻译功能
|
|
||||||
- 模型切换支持通过标签筛选
|
|
||||||
- 翻译功能增强:历史搜索和收藏
|
|
||||||
|
|
||||||
🔧 性能优化:
|
🚀 性能优化:
|
||||||
- 优化历史页面搜索性能
|
- 优化输入栏提及模型状态缓存,在渲染间保持状态
|
||||||
- 优化拖拽列表组件交互
|
- 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持
|
||||||
- 升级 Electron 到 37.4.0
|
|
||||||
|
|
||||||
🐛 修复问题:
|
🔧 重构改进:
|
||||||
- 修复知识库加密 PDF 文档处理
|
- 更新 HeroUIProvider 导入路径,改善上下文管理
|
||||||
- 修复导航栏在左侧时笔记侧边栏按钮缺失
|
- 更新依赖项和 VSCode 开发环境配置
|
||||||
- 修复多个模型兼容性问题
|
- 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17
|
||||||
- 修复 MCP 相关问题
|
|
||||||
- 其他稳定性改进
|
|
||||||
|
|||||||
+10
-1
@@ -4,6 +4,10 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
|||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
import { visualizer } from 'rollup-plugin-visualizer'
|
import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
|
||||||
|
// assert not supported by biome
|
||||||
|
// import pkg from './package.json' assert { type: 'json' }
|
||||||
|
import pkg from './package.json'
|
||||||
|
|
||||||
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||||
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
||||||
}
|
}
|
||||||
@@ -26,7 +30,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['@libsql/client', 'bufferutil', 'utf-8-validate'],
|
external: ['bufferutil', 'utf-8-validate', 'electron', ...Object.keys(pkg.dependencies)],
|
||||||
output: {
|
output: {
|
||||||
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
|
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
|
||||||
inlineDynamicImports: true // 内联所有动态导入,这是关键配置
|
inlineDynamicImports: true // 内联所有动态导入,这是关键配置
|
||||||
@@ -58,6 +62,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
renderer: {
|
renderer: {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||||
react({
|
react({
|
||||||
tsDecorators: true,
|
tsDecorators: true,
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -79,9 +84,13 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@renderer': resolve('src/renderer/src'),
|
'@renderer': resolve('src/renderer/src'),
|
||||||
'@shared': resolve('packages/shared'),
|
'@shared': resolve('packages/shared'),
|
||||||
|
'@types': resolve('src/renderer/src/types'),
|
||||||
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
'@logger': resolve('src/renderer/src/services/LoggerService'),
|
||||||
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
|
||||||
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
|
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
|
||||||
|
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
|
||||||
|
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
|
||||||
|
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
|
||||||
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src')
|
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-6
@@ -1,8 +1,8 @@
|
|||||||
import electronConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
|
||||||
import tseslint from '@electron-toolkit/eslint-config-ts'
|
import tseslint from '@electron-toolkit/eslint-config-ts'
|
||||||
import eslint from '@eslint/js'
|
import eslint from '@eslint/js'
|
||||||
import eslintReact from '@eslint-react/eslint-plugin'
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
import { defineConfig } from 'eslint/config'
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import oxlint from 'eslint-plugin-oxlint'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||||
import unusedImports from 'eslint-plugin-unused-imports'
|
import unusedImports from 'eslint-plugin-unused-imports'
|
||||||
@@ -10,7 +10,6 @@ import unusedImports from 'eslint-plugin-unused-imports'
|
|||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
electronConfigPrettier,
|
|
||||||
eslintReact.configs['recommended-typescript'],
|
eslintReact.configs['recommended-typescript'],
|
||||||
reactHooks.configs['recommended-latest'],
|
reactHooks.configs['recommended-latest'],
|
||||||
{
|
{
|
||||||
@@ -26,7 +25,6 @@ export default defineConfig([
|
|||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'unused-imports/no-unused-imports': 'error',
|
'unused-imports/no-unused-imports': 'error',
|
||||||
'@eslint-react/no-prop-types': 'error',
|
'@eslint-react/no-prop-types': 'error',
|
||||||
'prettier/prettier': ['error']
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||||
@@ -53,7 +51,7 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
// LoggerService Custom Rules - only apply to src directory
|
// LoggerService Custom Rules - only apply to src directory
|
||||||
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
||||||
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*'],
|
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-syntax': [
|
'no-restricted-syntax': [
|
||||||
process.env.PRCI ? 'error' : 'warn',
|
process.env.PRCI ? 'error' : 'warn',
|
||||||
@@ -123,7 +121,14 @@ export default defineConfig([
|
|||||||
'.gitignore',
|
'.gitignore',
|
||||||
'scripts/cloudflare-worker.js',
|
'scripts/cloudflare-worker.js',
|
||||||
'src/main/integration/nutstore/sso/lib/**',
|
'src/main/integration/nutstore/sso/lib/**',
|
||||||
'src/main/integration/cherryin/index.js'
|
'src/main/integration/cherryin/index.js',
|
||||||
|
'src/main/integration/nutstore/sso/lib/**',
|
||||||
|
'src/renderer/src/ui/**',
|
||||||
|
'packages/**/dist'
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
// turn off oxlint supported rules.
|
||||||
|
...oxlint.configs['flat/eslint'],
|
||||||
|
...oxlint.configs['flat/typescript'],
|
||||||
|
...oxlint.configs['flat/unicorn']
|
||||||
])
|
])
|
||||||
|
|||||||
+79
-28
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.5.8",
|
"version": "1.6.0-rc.2",
|
||||||
"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",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "dotenv electron-vite dev",
|
"dev": "dotenv electron-vite dev",
|
||||||
|
"dev:main": "dotenv electron-vite dev --watch",
|
||||||
"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": "yarn lint && yarn test",
|
||||||
@@ -40,19 +41,21 @@
|
|||||||
"build:linux": "dotenv npm run build && electron-builder --linux --x64 --arm64",
|
"build:linux": "dotenv npm run build && electron-builder --linux --x64 --arm64",
|
||||||
"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",
|
||||||
"build:npm": "node scripts/build-npm.js",
|
|
||||||
"release": "node scripts/version.js",
|
"release": "node scripts/version.js",
|
||||||
"publish": "yarn build:check && yarn release patch push",
|
"publish": "yarn build:check && yarn release patch push",
|
||||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
"agents:generate": "NODE_ENV='development' drizzle-kit generate --config src/main/services/agents/drizzle.config.ts",
|
||||||
|
"agents:push": "NODE_ENV='development' drizzle-kit push --config src/main/services/agents/drizzle.config.ts",
|
||||||
|
"agents:studio": "NODE_ENV='development' drizzle-kit studio --config src/main/services/agents/drizzle.config.ts",
|
||||||
|
"agents:drop": "NODE_ENV='development' drizzle-kit drop --config src/main/services/agents/drizzle.config.ts",
|
||||||
"generate: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 yarn build",
|
||||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||||
"typecheck": "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": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
|
||||||
"check:i18n": "tsx scripts/check-i18n.ts",
|
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
|
||||||
"sync:i18n": "tsx scripts/sync-i18n.ts",
|
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
|
||||||
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
|
||||||
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
|
||||||
"update:languages": "tsx scripts/update-languages.ts",
|
"update:languages": "tsx scripts/update-languages.ts",
|
||||||
@@ -64,24 +67,37 @@
|
|||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:e2e": "yarn playwright test",
|
"test:e2e": "yarn playwright test",
|
||||||
"test:lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts",
|
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
|
||||||
"test:scripts": "vitest scripts",
|
"test:scripts": "vitest scripts",
|
||||||
"format": "prettier --write .",
|
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n",
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix && yarn typecheck && yarn check:i18n",
|
"format": "biome format --write && biome lint --write",
|
||||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
|
"format:check": "biome format && biome lint",
|
||||||
|
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
||||||
|
"claude": "dotenv -e .env -- claude",
|
||||||
|
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
||||||
|
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
||||||
|
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/claude-code": "patch:@anthropic-ai/claude-code@npm%3A1.0.118#~/.yarn/patches/@anthropic-ai-claude-code-npm-1.0.118-bbf4e9e59f.patch",
|
||||||
"@libsql/client": "0.14.0",
|
"@libsql/client": "0.14.0",
|
||||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
"@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",
|
||||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"drizzle-orm": "^0.44.5",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-validator": "^7.2.1",
|
||||||
|
"font-list": "^2.0.0",
|
||||||
"graceful-fs": "^4.2.11",
|
"graceful-fs": "^4.2.11",
|
||||||
"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",
|
||||||
"selection-hook": "^1.0.11",
|
"selection-hook": "^1.0.12",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
|
"swagger-jsdoc": "^6.2.8",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
"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"
|
||||||
},
|
},
|
||||||
@@ -89,12 +105,18 @@
|
|||||||
"@agentic/exa": "^7.3.3",
|
"@agentic/exa": "^7.3.3",
|
||||||
"@agentic/searxng": "^7.3.3",
|
"@agentic/searxng": "^7.3.3",
|
||||||
"@agentic/tavily": "^7.3.3",
|
"@agentic/tavily": "^7.3.3",
|
||||||
|
"@ai-sdk/amazon-bedrock": "^3.0.21",
|
||||||
|
"@ai-sdk/google-vertex": "^3.0.27",
|
||||||
|
"@ai-sdk/mistral": "^2.0.14",
|
||||||
|
"@ai-sdk/perplexity": "^2.0.9",
|
||||||
"@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": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
|
||||||
"@aws-sdk/client-bedrock": "^3.840.0",
|
"@aws-sdk/client-bedrock": "^3.840.0",
|
||||||
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
|
||||||
"@aws-sdk/client-s3": "^3.840.0",
|
"@aws-sdk/client-s3": "^3.840.0",
|
||||||
|
"@biomejs/biome": "2.2.4",
|
||||||
|
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
|
||||||
"@cherrystudio/embedjs": "^0.1.31",
|
"@cherrystudio/embedjs": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
||||||
@@ -112,7 +134,6 @@
|
|||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
@@ -123,13 +144,14 @@
|
|||||||
"@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",
|
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
|
"@heroui/react": "^2.8.3",
|
||||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
"@langchain/community": "^0.3.36",
|
"@langchain/community": "^0.3.50",
|
||||||
"@langchain/ollama": "^0.2.1",
|
|
||||||
"@mistralai/mistralai": "^1.7.5",
|
"@mistralai/mistralai": "^1.7.5",
|
||||||
"@modelcontextprotocol/sdk": "^1.17.0",
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||||
"@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.1.2",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@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",
|
||||||
@@ -137,9 +159,11 @@
|
|||||||
"@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",
|
||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
|
"@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/plugin-styled-components": "^7.1.5",
|
"@swc/plugin-styled-components": "^8.0.4",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
"@tanstack/react-query": "^5.85.5",
|
"@tanstack/react-query": "^5.85.5",
|
||||||
"@tanstack/react-virtual": "^3.13.12",
|
"@tanstack/react-virtual": "^3.13.12",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
@@ -165,20 +189,30 @@
|
|||||||
"@truto/turndown-plugin-gfm": "^1.0.2",
|
"@truto/turndown-plugin-gfm": "^1.0.2",
|
||||||
"@tryfabric/martian": "^1.2.4",
|
"@tryfabric/martian": "^1.2.4",
|
||||||
"@types/cli-progress": "^3",
|
"@types/cli-progress": "^3",
|
||||||
|
"@types/content-type": "^1.1.9",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/diff": "^7",
|
||||||
|
"@types/express": "^5",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/he": "^1",
|
"@types/he": "^1",
|
||||||
|
"@types/html-to-text": "^9",
|
||||||
"@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/mime-types": "^3",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.1",
|
||||||
"@types/pako": "^1.0.2",
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^19.0.12",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
|
"@types/react-window": "^1",
|
||||||
|
"@types/swagger-jsdoc": "^6",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@types/turndown": "^5.0.5",
|
"@types/turndown": "^5.0.5",
|
||||||
"@types/word-extractor": "^1",
|
"@types/word-extractor": "^1",
|
||||||
|
"@typescript/native-preview": "latest",
|
||||||
"@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",
|
||||||
@@ -190,16 +224,21 @@
|
|||||||
"@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",
|
||||||
|
"ai": "^5.0.44",
|
||||||
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
|
||||||
"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",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"chardet": "^2.1.0",
|
"chardet": "^2.1.0",
|
||||||
|
"check-disk-space": "3.4.0",
|
||||||
|
"cheerio": "^1.1.2",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"code-inspector-plugin": "^0.20.14",
|
"code-inspector-plugin": "^0.20.14",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
"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",
|
||||||
@@ -208,9 +247,11 @@
|
|||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
|
"drizzle-kit": "^0.31.4",
|
||||||
"electron": "37.4.0",
|
"electron": "37.4.0",
|
||||||
"electron-builder": "26.0.15",
|
"electron-builder": "26.0.15",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-reload": "^2.0.0-alpha.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "6.6.4",
|
"electron-updater": "6.6.4",
|
||||||
"electron-vite": "4.0.0",
|
"electron-vite": "4.0.0",
|
||||||
@@ -219,18 +260,21 @@
|
|||||||
"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",
|
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
|
"eslint-plugin-oxlint": "^1.15.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",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"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",
|
||||||
|
"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",
|
||||||
"google-auth-library": "^9.15.1",
|
"google-auth-library": "^9.15.1",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"html-tags": "^5.1.0",
|
"html-tags": "^5.1.0",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"htmlparser2": "^10.0.0",
|
"htmlparser2": "^10.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
@@ -247,15 +291,17 @@
|
|||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"mermaid": "^11.10.1",
|
"mermaid": "^11.10.1",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
"openai": "patch:openai@npm%3A5.12.2#~/.yarn/patches/openai-npm-5.12.2-30b075401c.patch",
|
||||||
|
"oxlint": "^1.15.0",
|
||||||
|
"oxlint-tsgolint": "^0.2.0",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdf-parse": "^1.1.1",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"prettier-plugin-sort-json": "^4.1.1",
|
|
||||||
"proxy-agent": "^6.5.0",
|
"proxy-agent": "^6.5.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -265,6 +311,7 @@
|
|||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-json-view": "^1.21.3",
|
"react-json-view": "^1.21.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-player": "^3.3.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "6",
|
"react-router-dom": "6",
|
||||||
@@ -284,21 +331,23 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remove-markdown": "^0.6.2",
|
"remove-markdown": "^0.6.2",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.88.0",
|
|
||||||
"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",
|
||||||
"striptags": "^3.2.0",
|
"striptags": "^3.2.0",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
|
"swr": "^2.3.6",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"tiny-pinyin": "^1.3.2",
|
"tiny-pinyin": "^1.3.2",
|
||||||
"tokenx": "^1.1.0",
|
"tokenx": "^1.1.0",
|
||||||
"tsx": "^4.20.3",
|
"tsx": "^4.20.3",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
"typescript": "^5.6.2",
|
"tw-animate-css": "^1.3.8",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
"undici": "6.21.2",
|
"undici": "6.21.2",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"webdav": "^5.8.0",
|
"webdav": "^5.8.0",
|
||||||
@@ -306,9 +355,11 @@
|
|||||||
"winston-daily-rotate-file": "^5.0.0",
|
"winston-daily-rotate-file": "^5.0.0",
|
||||||
"word-extractor": "^1.0.4",
|
"word-extractor": "^1.0.4",
|
||||||
"y-protocols": "^1.0.6",
|
"y-protocols": "^1.0.6",
|
||||||
|
"yaml": "^2.8.1",
|
||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
|
"youtubei.js": "^15.0.1",
|
||||||
"zipread": "^1.3.3",
|
"zipread": "^1.3.3",
|
||||||
"zod": "^3.25.74"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@codemirror/language": "6.11.3",
|
"@codemirror/language": "6.11.3",
|
||||||
@@ -334,11 +385,11 @@
|
|||||||
"packageManager": "yarn@4.9.1",
|
"packageManager": "yarn@4.9.1",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
|
||||||
"prettier --write",
|
"biome format --write --no-errors-on-unmatched",
|
||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
"*.{json,md,yml,yaml,css,scss,html}": [
|
"*.{json,yml,yaml,css,html}": [
|
||||||
"prettier --write"
|
"biome format --write --no-errors-on-unmatched"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,514 @@
|
|||||||
|
# AI Core 基于 Vercel AI SDK 的技术架构
|
||||||
|
|
||||||
|
## 1. 架构设计理念
|
||||||
|
|
||||||
|
### 1.1 设计目标
|
||||||
|
|
||||||
|
- **简化分层**:`models`(模型层)→ `runtime`(运行时层),清晰的职责分离
|
||||||
|
- **统一接口**:使用 Vercel AI SDK 统一不同 AI Provider 的接口差异
|
||||||
|
- **动态导入**:通过动态导入实现按需加载,减少打包体积
|
||||||
|
- **最小包装**:直接使用 AI SDK 的类型和接口,避免重复定义
|
||||||
|
- **插件系统**:基于钩子的通用插件架构,支持请求全生命周期扩展
|
||||||
|
- **类型安全**:利用 TypeScript 和 AI SDK 的类型系统确保类型安全
|
||||||
|
- **轻量级**:专注核心功能,保持包的轻量和高效
|
||||||
|
- **包级独立**:作为独立包管理,便于复用和维护
|
||||||
|
- **Agent就绪**:为将来集成 OpenAI Agents SDK 预留扩展空间
|
||||||
|
|
||||||
|
### 1.2 核心优势
|
||||||
|
|
||||||
|
- **标准化**:AI SDK 提供统一的模型接口,减少适配工作
|
||||||
|
- **简化设计**:函数式API,避免过度抽象
|
||||||
|
- **更好的开发体验**:完整的 TypeScript 支持和丰富的生态系统
|
||||||
|
- **性能优化**:AI SDK 内置优化和最佳实践
|
||||||
|
- **模块化设计**:独立包结构,支持跨项目复用
|
||||||
|
- **可扩展插件**:通用的流转换和参数处理插件系统
|
||||||
|
- **面向未来**:为 OpenAI Agents SDK 集成做好准备
|
||||||
|
|
||||||
|
## 2. 整体架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "用户应用 (如 Cherry Studio)"
|
||||||
|
UI["用户界面"]
|
||||||
|
Components["应用组件"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "packages/aiCore (AI Core 包)"
|
||||||
|
subgraph "Runtime Layer (运行时层)"
|
||||||
|
RuntimeExecutor["RuntimeExecutor (运行时执行器)"]
|
||||||
|
PluginEngine["PluginEngine (插件引擎)"]
|
||||||
|
RuntimeAPI["Runtime API (便捷函数)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Models Layer (模型层)"
|
||||||
|
ModelFactory["createModel() (模型工厂)"]
|
||||||
|
ProviderCreator["ProviderCreator (提供商创建器)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Core Systems (核心系统)"
|
||||||
|
subgraph "Plugins (插件)"
|
||||||
|
PluginManager["PluginManager (插件管理)"]
|
||||||
|
BuiltInPlugins["Built-in Plugins (内置插件)"]
|
||||||
|
StreamTransforms["Stream Transforms (流转换)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Middleware (中间件)"
|
||||||
|
MiddlewareWrapper["wrapModelWithMiddlewares() (中间件包装)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Providers (提供商)"
|
||||||
|
Registry["Provider Registry (注册表)"]
|
||||||
|
Factory["Provider Factory (工厂)"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Vercel AI SDK"
|
||||||
|
AICore["ai (核心库)"]
|
||||||
|
OpenAI["@ai-sdk/openai"]
|
||||||
|
Anthropic["@ai-sdk/anthropic"]
|
||||||
|
Google["@ai-sdk/google"]
|
||||||
|
XAI["@ai-sdk/xai"]
|
||||||
|
Others["其他 19+ Providers"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Future: OpenAI Agents SDK"
|
||||||
|
AgentSDK["@openai/agents (未来集成)"]
|
||||||
|
AgentExtensions["Agent Extensions (预留)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
UI --> RuntimeAPI
|
||||||
|
Components --> RuntimeExecutor
|
||||||
|
RuntimeAPI --> RuntimeExecutor
|
||||||
|
RuntimeExecutor --> PluginEngine
|
||||||
|
RuntimeExecutor --> ModelFactory
|
||||||
|
PluginEngine --> PluginManager
|
||||||
|
ModelFactory --> ProviderCreator
|
||||||
|
ModelFactory --> MiddlewareWrapper
|
||||||
|
ProviderCreator --> Registry
|
||||||
|
Registry --> Factory
|
||||||
|
Factory --> OpenAI
|
||||||
|
Factory --> Anthropic
|
||||||
|
Factory --> Google
|
||||||
|
Factory --> XAI
|
||||||
|
Factory --> Others
|
||||||
|
|
||||||
|
RuntimeExecutor --> AICore
|
||||||
|
AICore --> streamText
|
||||||
|
AICore --> generateText
|
||||||
|
AICore --> streamObject
|
||||||
|
AICore --> generateObject
|
||||||
|
|
||||||
|
PluginManager --> StreamTransforms
|
||||||
|
PluginManager --> BuiltInPlugins
|
||||||
|
|
||||||
|
%% 未来集成路径
|
||||||
|
RuntimeExecutor -.-> AgentSDK
|
||||||
|
AgentSDK -.-> AgentExtensions
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 包结构设计
|
||||||
|
|
||||||
|
### 3.1 新架构文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/aiCore/
|
||||||
|
├── src/
|
||||||
|
│ ├── core/ # 核心层 - 内部实现
|
||||||
|
│ │ ├── models/ # 模型层 - 模型创建和配置
|
||||||
|
│ │ │ ├── factory.ts # 模型工厂函数 ✅
|
||||||
|
│ │ │ ├── ModelCreator.ts # 模型创建器 ✅
|
||||||
|
│ │ │ ├── ConfigManager.ts # 配置管理器 ✅
|
||||||
|
│ │ │ ├── types.ts # 模型类型定义 ✅
|
||||||
|
│ │ │ └── index.ts # 模型层导出 ✅
|
||||||
|
│ │ ├── runtime/ # 运行时层 - 执行和用户API
|
||||||
|
│ │ │ ├── executor.ts # 运行时执行器 ✅
|
||||||
|
│ │ │ ├── pluginEngine.ts # 插件引擎 ✅
|
||||||
|
│ │ │ ├── types.ts # 运行时类型定义 ✅
|
||||||
|
│ │ │ └── index.ts # 运行时导出 ✅
|
||||||
|
│ │ ├── middleware/ # 中间件系统
|
||||||
|
│ │ │ ├── wrapper.ts # 模型包装器 ✅
|
||||||
|
│ │ │ ├── manager.ts # 中间件管理器 ✅
|
||||||
|
│ │ │ ├── types.ts # 中间件类型 ✅
|
||||||
|
│ │ │ └── index.ts # 中间件导出 ✅
|
||||||
|
│ │ ├── plugins/ # 插件系统
|
||||||
|
│ │ │ ├── types.ts # 插件类型定义 ✅
|
||||||
|
│ │ │ ├── manager.ts # 插件管理器 ✅
|
||||||
|
│ │ │ ├── built-in/ # 内置插件 ✅
|
||||||
|
│ │ │ │ ├── logging.ts # 日志插件 ✅
|
||||||
|
│ │ │ │ ├── webSearchPlugin/ # 网络搜索插件 ✅
|
||||||
|
│ │ │ │ ├── toolUsePlugin/ # 工具使用插件 ✅
|
||||||
|
│ │ │ │ └── index.ts # 内置插件导出 ✅
|
||||||
|
│ │ │ ├── README.md # 插件文档 ✅
|
||||||
|
│ │ │ └── index.ts # 插件导出 ✅
|
||||||
|
│ │ ├── providers/ # 提供商管理
|
||||||
|
│ │ │ ├── registry.ts # 提供商注册表 ✅
|
||||||
|
│ │ │ ├── factory.ts # 提供商工厂 ✅
|
||||||
|
│ │ │ ├── creator.ts # 提供商创建器 ✅
|
||||||
|
│ │ │ ├── types.ts # 提供商类型 ✅
|
||||||
|
│ │ │ ├── utils.ts # 工具函数 ✅
|
||||||
|
│ │ │ └── index.ts # 提供商导出 ✅
|
||||||
|
│ │ ├── options/ # 配置选项
|
||||||
|
│ │ │ ├── factory.ts # 选项工厂 ✅
|
||||||
|
│ │ │ ├── types.ts # 选项类型 ✅
|
||||||
|
│ │ │ ├── xai.ts # xAI 选项 ✅
|
||||||
|
│ │ │ ├── openrouter.ts # OpenRouter 选项 ✅
|
||||||
|
│ │ │ ├── examples.ts # 示例配置 ✅
|
||||||
|
│ │ │ └── index.ts # 选项导出 ✅
|
||||||
|
│ │ └── index.ts # 核心层导出 ✅
|
||||||
|
│ ├── types.ts # 全局类型定义 ✅
|
||||||
|
│ └── index.ts # 包主入口文件 ✅
|
||||||
|
├── package.json # 包配置文件 ✅
|
||||||
|
├── tsconfig.json # TypeScript 配置 ✅
|
||||||
|
├── README.md # 包说明文档 ✅
|
||||||
|
└── AI_SDK_ARCHITECTURE.md # 本文档 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 架构分层详解
|
||||||
|
|
||||||
|
### 4.1 Models Layer (模型层)
|
||||||
|
|
||||||
|
**职责**:统一的模型创建和配置管理
|
||||||
|
|
||||||
|
**核心文件**:
|
||||||
|
|
||||||
|
- `factory.ts`: 模型工厂函数 (`createModel`, `createModels`)
|
||||||
|
- `ProviderCreator.ts`: 底层提供商创建和模型实例化
|
||||||
|
- `types.ts`: 模型配置类型定义
|
||||||
|
|
||||||
|
**设计特点**:
|
||||||
|
|
||||||
|
- 函数式设计,避免不必要的类抽象
|
||||||
|
- 统一的模型配置接口
|
||||||
|
- 自动处理中间件应用
|
||||||
|
- 支持批量模型创建
|
||||||
|
|
||||||
|
**核心API**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 模型配置接口
|
||||||
|
export interface ModelConfig {
|
||||||
|
providerId: ProviderId
|
||||||
|
modelId: string
|
||||||
|
options: ProviderSettingsMap[ProviderId]
|
||||||
|
middlewares?: LanguageModelV1Middleware[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心模型创建函数
|
||||||
|
export async function createModel(config: ModelConfig): Promise<LanguageModel>
|
||||||
|
export async function createModels(configs: ModelConfig[]): Promise<LanguageModel[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Runtime Layer (运行时层)
|
||||||
|
|
||||||
|
**职责**:运行时执行器和用户面向的API接口
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
|
||||||
|
- `executor.ts`: 运行时执行器类
|
||||||
|
- `plugin-engine.ts`: 插件引擎(原PluginEnabledAiClient)
|
||||||
|
- `index.ts`: 便捷函数和工厂方法
|
||||||
|
|
||||||
|
**设计特点**:
|
||||||
|
|
||||||
|
- 提供三种使用方式:类实例、静态工厂、函数式调用
|
||||||
|
- 自动集成模型创建和插件处理
|
||||||
|
- 完整的类型安全支持
|
||||||
|
- 为 OpenAI Agents SDK 预留扩展接口
|
||||||
|
|
||||||
|
**核心API**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 运行时执行器
|
||||||
|
export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||||
|
static create<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T],
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
): RuntimeExecutor<T>
|
||||||
|
|
||||||
|
async streamText(modelId: string, params: StreamTextParams): Promise<StreamTextResult>
|
||||||
|
async generateText(modelId: string, params: GenerateTextParams): Promise<GenerateTextResult>
|
||||||
|
async streamObject(modelId: string, params: StreamObjectParams): Promise<StreamObjectResult>
|
||||||
|
async generateObject(modelId: string, params: GenerateObjectParams): Promise<GenerateObjectResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷函数式API
|
||||||
|
export async function streamText<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T],
|
||||||
|
modelId: string,
|
||||||
|
params: StreamTextParams,
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
): Promise<StreamTextResult>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Plugin System (插件系统)
|
||||||
|
|
||||||
|
**职责**:可扩展的插件架构
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
|
||||||
|
- `PluginManager`: 插件生命周期管理
|
||||||
|
- `built-in/`: 内置插件集合
|
||||||
|
- 流转换收集和应用
|
||||||
|
|
||||||
|
**设计特点**:
|
||||||
|
|
||||||
|
- 借鉴 Rollup 的钩子分类设计
|
||||||
|
- 支持流转换 (`experimental_transform`)
|
||||||
|
- 内置常用插件(日志、计数等)
|
||||||
|
- 完整的生命周期钩子
|
||||||
|
|
||||||
|
**插件接口**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface AiPlugin {
|
||||||
|
name: string
|
||||||
|
enforce?: 'pre' | 'post'
|
||||||
|
|
||||||
|
// 【First】首个钩子 - 只执行第一个返回值的插件
|
||||||
|
resolveModel?: (modelId: string, context: AiRequestContext) => string | null | Promise<string | null>
|
||||||
|
loadTemplate?: (templateName: string, context: AiRequestContext) => any | null | Promise<any | null>
|
||||||
|
|
||||||
|
// 【Sequential】串行钩子 - 链式执行,支持数据转换
|
||||||
|
transformParams?: (params: any, context: AiRequestContext) => any | Promise<any>
|
||||||
|
transformResult?: (result: any, context: AiRequestContext) => any | Promise<any>
|
||||||
|
|
||||||
|
// 【Parallel】并行钩子 - 不依赖顺序,用于副作用
|
||||||
|
onRequestStart?: (context: AiRequestContext) => void | Promise<void>
|
||||||
|
onRequestEnd?: (context: AiRequestContext, result: any) => void | Promise<void>
|
||||||
|
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
||||||
|
|
||||||
|
// 【Stream】流处理
|
||||||
|
transformStream?: () => TransformStream
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Middleware System (中间件系统)
|
||||||
|
|
||||||
|
**职责**:AI SDK原生中间件支持
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
|
||||||
|
- `ModelWrapper.ts`: 模型包装函数
|
||||||
|
|
||||||
|
**设计哲学**:
|
||||||
|
|
||||||
|
- 直接使用AI SDK的 `wrapLanguageModel`
|
||||||
|
- 与插件系统分离,职责明确
|
||||||
|
- 函数式设计,简化使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function wrapModelWithMiddlewares(model: LanguageModel, middlewares: LanguageModelV1Middleware[]): LanguageModel
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Provider System (提供商系统)
|
||||||
|
|
||||||
|
**职责**:AI Provider注册表和动态导入
|
||||||
|
|
||||||
|
**核心组件**:
|
||||||
|
|
||||||
|
- `registry.ts`: 19+ Provider配置和类型
|
||||||
|
- `factory.ts`: Provider配置工厂
|
||||||
|
|
||||||
|
**支持的Providers**:
|
||||||
|
|
||||||
|
- OpenAI, Anthropic, Google, XAI
|
||||||
|
- Azure OpenAI, Amazon Bedrock, Google Vertex
|
||||||
|
- Groq, Together.ai, Fireworks, DeepSeek
|
||||||
|
- 等19+ AI SDK官方支持的providers
|
||||||
|
|
||||||
|
## 5. 使用方式
|
||||||
|
|
||||||
|
### 5.1 函数式调用 (推荐 - 简单场景)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { streamText, generateText } from '@cherrystudio/ai-core/runtime'
|
||||||
|
|
||||||
|
// 直接函数调用
|
||||||
|
const stream = await streamText(
|
||||||
|
'anthropic',
|
||||||
|
{ apiKey: 'your-api-key' },
|
||||||
|
'claude-3',
|
||||||
|
{ messages: [{ role: 'user', content: 'Hello!' }] },
|
||||||
|
[loggingPlugin]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 执行器实例 (推荐 - 复杂场景)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createExecutor } from '@cherrystudio/ai-core/runtime'
|
||||||
|
|
||||||
|
// 创建可复用的执行器
|
||||||
|
const executor = createExecutor('openai', { apiKey: 'your-api-key' }, [plugin1, plugin2])
|
||||||
|
|
||||||
|
// 多次使用
|
||||||
|
const stream = await executor.streamText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await executor.generateText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'How are you?' }]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 静态工厂方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RuntimeExecutor } from '@cherrystudio/ai-core/runtime'
|
||||||
|
|
||||||
|
// 静态创建
|
||||||
|
const executor = RuntimeExecutor.create('anthropic', { apiKey: 'your-api-key' })
|
||||||
|
await executor.streamText('claude-3', { messages: [...] })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 直接模型创建 (高级用法)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createModel } from '@cherrystudio/ai-core/models'
|
||||||
|
import { streamText } from 'ai'
|
||||||
|
|
||||||
|
// 直接创建模型使用
|
||||||
|
const model = await createModel({
|
||||||
|
providerId: 'openai',
|
||||||
|
modelId: 'gpt-4',
|
||||||
|
options: { apiKey: 'your-api-key' },
|
||||||
|
middlewares: [middleware1, middleware2]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 直接使用 AI SDK
|
||||||
|
const result = await streamText({ model, messages: [...] })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 为 OpenAI Agents SDK 预留的设计
|
||||||
|
|
||||||
|
### 6.1 架构兼容性
|
||||||
|
|
||||||
|
当前架构完全兼容 OpenAI Agents SDK 的集成需求:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 当前的模型创建
|
||||||
|
const model = await createModel({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelId: 'claude-3',
|
||||||
|
options: { apiKey: 'xxx' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将来可以直接用于 OpenAI Agents SDK
|
||||||
|
import { Agent, run } from '@openai/agents'
|
||||||
|
|
||||||
|
const agent = new Agent({
|
||||||
|
model, // ✅ 直接兼容 LanguageModel 接口
|
||||||
|
name: 'Assistant',
|
||||||
|
instructions: '...',
|
||||||
|
tools: [tool1, tool2]
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await run(agent, 'user input')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 预留的扩展点
|
||||||
|
|
||||||
|
1. **runtime/agents/** 目录预留
|
||||||
|
2. **AgentExecutor** 类预留
|
||||||
|
3. **Agent工具转换插件** 预留
|
||||||
|
4. **多Agent编排** 预留
|
||||||
|
|
||||||
|
### 6.3 未来架构扩展
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/aiCore/src/core/
|
||||||
|
├── runtime/
|
||||||
|
│ ├── agents/ # 🚀 未来添加
|
||||||
|
│ │ ├── AgentExecutor.ts
|
||||||
|
│ │ ├── WorkflowManager.ts
|
||||||
|
│ │ └── ConversationManager.ts
|
||||||
|
│ ├── executor.ts
|
||||||
|
│ └── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 架构优势
|
||||||
|
|
||||||
|
### 7.1 简化设计
|
||||||
|
|
||||||
|
- **移除过度抽象**:删除了orchestration层和creation层的复杂包装
|
||||||
|
- **函数式优先**:models层使用函数而非类
|
||||||
|
- **直接明了**:runtime层直接提供用户API
|
||||||
|
|
||||||
|
### 7.2 职责清晰
|
||||||
|
|
||||||
|
- **Models**: 专注模型创建和配置
|
||||||
|
- **Runtime**: 专注执行和用户API
|
||||||
|
- **Plugins**: 专注扩展功能
|
||||||
|
- **Providers**: 专注AI Provider管理
|
||||||
|
|
||||||
|
### 7.3 类型安全
|
||||||
|
|
||||||
|
- 完整的 TypeScript 支持
|
||||||
|
- AI SDK 类型的直接复用
|
||||||
|
- 避免类型重复定义
|
||||||
|
|
||||||
|
### 7.4 灵活使用
|
||||||
|
|
||||||
|
- 三种使用模式满足不同需求
|
||||||
|
- 从简单函数调用到复杂执行器
|
||||||
|
- 支持直接AI SDK使用
|
||||||
|
|
||||||
|
### 7.5 面向未来
|
||||||
|
|
||||||
|
- 为 OpenAI Agents SDK 集成做好准备
|
||||||
|
- 清晰的扩展点和架构边界
|
||||||
|
- 模块化设计便于功能添加
|
||||||
|
|
||||||
|
## 8. 技术决策记录
|
||||||
|
|
||||||
|
### 8.1 为什么选择简化的两层架构?
|
||||||
|
|
||||||
|
- **职责分离**:models专注创建,runtime专注执行
|
||||||
|
- **模块化**:每层都有清晰的边界和职责
|
||||||
|
- **扩展性**:为Agent功能预留了清晰的扩展空间
|
||||||
|
|
||||||
|
### 8.2 为什么选择函数式设计?
|
||||||
|
|
||||||
|
- **简洁性**:避免不必要的类设计
|
||||||
|
- **性能**:减少对象创建开销
|
||||||
|
- **易用性**:函数调用更直观
|
||||||
|
|
||||||
|
### 8.3 为什么分离插件和中间件?
|
||||||
|
|
||||||
|
- **职责明确**: 插件处理应用特定需求
|
||||||
|
- **原生支持**: 中间件使用AI SDK原生功能
|
||||||
|
- **灵活性**: 两套系统可以独立演进
|
||||||
|
|
||||||
|
## 9. 总结
|
||||||
|
|
||||||
|
AI Core架构实现了:
|
||||||
|
|
||||||
|
### 9.1 核心特点
|
||||||
|
|
||||||
|
- ✅ **简化架构**: 2层核心架构,职责清晰
|
||||||
|
- ✅ **函数式设计**: models层完全函数化
|
||||||
|
- ✅ **类型安全**: 统一的类型定义和AI SDK类型复用
|
||||||
|
- ✅ **插件扩展**: 强大的插件系统
|
||||||
|
- ✅ **多种使用方式**: 满足不同复杂度需求
|
||||||
|
- ✅ **Agent就绪**: 为OpenAI Agents SDK集成做好准备
|
||||||
|
|
||||||
|
### 9.2 核心价值
|
||||||
|
|
||||||
|
- **统一接口**: 一套API支持19+ AI providers
|
||||||
|
- **灵活使用**: 函数式、实例式、静态工厂式
|
||||||
|
- **强类型**: 完整的TypeScript支持
|
||||||
|
- **可扩展**: 插件和中间件双重扩展能力
|
||||||
|
- **高性能**: 最小化包装,直接使用AI SDK
|
||||||
|
- **面向未来**: Agent SDK集成架构就绪
|
||||||
|
|
||||||
|
### 9.3 未来发展
|
||||||
|
|
||||||
|
这个架构提供了:
|
||||||
|
|
||||||
|
- **优秀的开发体验**: 简洁的API和清晰的使用模式
|
||||||
|
- **强大的扩展能力**: 为Agent功能预留了完整的架构空间
|
||||||
|
- **良好的维护性**: 职责分离明确,代码易于维护
|
||||||
|
- **广泛的适用性**: 既适合简单调用也适合复杂应用
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
# @cherrystudio/ai-core
|
||||||
|
|
||||||
|
Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口包,为 AI 应用提供强大的抽象层和插件化架构。
|
||||||
|
|
||||||
|
## ✨ 核心亮点
|
||||||
|
|
||||||
|
### 🏗️ 优雅的架构设计
|
||||||
|
|
||||||
|
- **简化分层**:`models`(模型层)→ `runtime`(运行时层),清晰的职责分离
|
||||||
|
- **函数式优先**:避免过度抽象,提供简洁直观的 API
|
||||||
|
- **类型安全**:完整的 TypeScript 支持,直接复用 AI SDK 类型系统
|
||||||
|
- **最小包装**:直接使用 AI SDK 的接口,避免重复定义和性能损耗
|
||||||
|
|
||||||
|
### 🔌 强大的插件系统
|
||||||
|
|
||||||
|
- **生命周期钩子**:支持请求全生命周期的扩展点
|
||||||
|
- **流转换支持**:基于 AI SDK 的 `experimental_transform` 实现流处理
|
||||||
|
- **插件分类**:First、Sequential、Parallel 三种钩子类型,满足不同场景
|
||||||
|
- **内置插件**:webSearch、logging、toolUse 等开箱即用的功能
|
||||||
|
|
||||||
|
### 🌐 统一多 Provider 接口
|
||||||
|
|
||||||
|
- **扩展注册**:支持自定义 Provider 注册,无限扩展能力
|
||||||
|
- **配置统一**:统一的配置接口,简化多 Provider 管理
|
||||||
|
|
||||||
|
### 🚀 多种使用方式
|
||||||
|
|
||||||
|
- **函数式调用**:适合简单场景的直接函数调用
|
||||||
|
- **执行器实例**:适合复杂场景的可复用执行器
|
||||||
|
- **静态工厂**:便捷的静态创建方法
|
||||||
|
- **原生兼容**:完全兼容 AI SDK 原生 Provider Registry
|
||||||
|
|
||||||
|
### 🔮 面向未来
|
||||||
|
|
||||||
|
- **Agent 就绪**:为 OpenAI Agents SDK 集成预留架构空间
|
||||||
|
- **模块化设计**:独立包结构,支持跨项目复用
|
||||||
|
- **渐进式迁移**:可以逐步从现有 AI SDK 代码迁移
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🚀 统一的 AI Provider 接口
|
||||||
|
- 🔄 动态导入支持
|
||||||
|
- 🛠️ TypeScript 支持
|
||||||
|
- 📦 强大的插件系统
|
||||||
|
- 🌍 内置webSearch(Openai,Google,Anthropic,xAI)
|
||||||
|
- 🎯 多种使用模式(函数式/实例式/静态工厂)
|
||||||
|
- 🔌 可扩展的 Provider 注册系统
|
||||||
|
- 🧩 完整的中间件支持
|
||||||
|
- 📊 插件统计和调试功能
|
||||||
|
|
||||||
|
## 支持的 Providers
|
||||||
|
|
||||||
|
基于 [AI SDK 官方支持的 providers](https://ai-sdk.dev/providers/ai-sdk-providers):
|
||||||
|
|
||||||
|
**核心 Providers(内置支持):**
|
||||||
|
|
||||||
|
- OpenAI
|
||||||
|
- Anthropic
|
||||||
|
- Google Generative AI
|
||||||
|
- OpenAI-Compatible
|
||||||
|
- xAI (Grok)
|
||||||
|
- Azure OpenAI
|
||||||
|
- DeepSeek
|
||||||
|
|
||||||
|
**扩展 Providers(通过注册API支持):**
|
||||||
|
|
||||||
|
- Google Vertex AI
|
||||||
|
- ...
|
||||||
|
- 自定义 Provider
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @cherrystudio/ai-core ai
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Native
|
||||||
|
|
||||||
|
如果你在 React Native 项目中使用此包,需要在 `metro.config.js` 中添加以下配置:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// metro.config.js
|
||||||
|
const { getDefaultConfig } = require('expo/metro-config')
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname)
|
||||||
|
|
||||||
|
// 添加对 @cherrystudio/ai-core 的支持
|
||||||
|
config.resolver.resolverMainFields = ['react-native', 'browser', 'main']
|
||||||
|
config.resolver.platforms = ['ios', 'android', 'native', 'web']
|
||||||
|
|
||||||
|
module.exports = config
|
||||||
|
```
|
||||||
|
|
||||||
|
还需要安装你要使用的 AI SDK provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiCore } from '@cherrystudio/ai-core'
|
||||||
|
|
||||||
|
// 创建 OpenAI executor
|
||||||
|
const executor = AiCore.create('openai', {
|
||||||
|
apiKey: 'your-api-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 流式生成
|
||||||
|
const result = await executor.streamText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 非流式生成
|
||||||
|
const response = await executor.generateText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 便捷函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createOpenAIExecutor } from '@cherrystudio/ai-core'
|
||||||
|
|
||||||
|
// 快速创建 OpenAI executor
|
||||||
|
const executor = createOpenAIExecutor({
|
||||||
|
apiKey: 'your-api-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用 executor
|
||||||
|
const result = await executor.streamText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello!' }]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多 Provider 支持
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AiCore } from '@cherrystudio/ai-core'
|
||||||
|
|
||||||
|
// 支持多种 AI providers
|
||||||
|
const openaiExecutor = AiCore.create('openai', { apiKey: 'openai-key' })
|
||||||
|
const anthropicExecutor = AiCore.create('anthropic', { apiKey: 'anthropic-key' })
|
||||||
|
const googleExecutor = AiCore.create('google', { apiKey: 'google-key' })
|
||||||
|
const xaiExecutor = AiCore.create('xai', { apiKey: 'xai-key' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展 Provider 注册
|
||||||
|
|
||||||
|
对于非内置的 providers,可以通过注册 API 扩展支持:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { registerProvider, AiCore } from '@cherrystudio/ai-core'
|
||||||
|
|
||||||
|
// 方式一:导入并注册第三方 provider
|
||||||
|
import { createGroq } from '@ai-sdk/groq'
|
||||||
|
|
||||||
|
registerProvider({
|
||||||
|
id: 'groq',
|
||||||
|
name: 'Groq',
|
||||||
|
creator: createGroq,
|
||||||
|
supportsImageGeneration: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 现在可以使用 Groq
|
||||||
|
const groqExecutor = AiCore.create('groq', { apiKey: 'groq-key' })
|
||||||
|
|
||||||
|
// 方式二:动态导入方式注册
|
||||||
|
registerProvider({
|
||||||
|
id: 'mistral',
|
||||||
|
name: 'Mistral AI',
|
||||||
|
import: () => import('@ai-sdk/mistral'),
|
||||||
|
creatorFunctionName: 'createMistral'
|
||||||
|
})
|
||||||
|
|
||||||
|
const mistralExecutor = AiCore.create('mistral', { apiKey: 'mistral-key' })
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 插件系统
|
||||||
|
|
||||||
|
AI Core 提供了强大的插件系统,支持请求全生命周期的扩展。
|
||||||
|
|
||||||
|
### 内置插件
|
||||||
|
|
||||||
|
#### webSearchPlugin - 网络搜索插件
|
||||||
|
|
||||||
|
为不同 AI Provider 提供统一的网络搜索能力:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { webSearchPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||||
|
|
||||||
|
const executor = AiCore.create('openai', { apiKey: 'your-key' }, [
|
||||||
|
webSearchPlugin({
|
||||||
|
openai: {
|
||||||
|
/* OpenAI 搜索配置 */
|
||||||
|
},
|
||||||
|
anthropic: { maxUses: 5 },
|
||||||
|
google: {
|
||||||
|
/* Google 搜索配置 */
|
||||||
|
},
|
||||||
|
xai: {
|
||||||
|
mode: 'on',
|
||||||
|
returnCitations: true,
|
||||||
|
maxSearchResults: 5,
|
||||||
|
sources: [{ type: 'web' }, { type: 'x' }, { type: 'news' }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### loggingPlugin - 日志插件
|
||||||
|
|
||||||
|
提供详细的请求日志记录:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createLoggingPlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||||
|
|
||||||
|
const executor = AiCore.create('openai', { apiKey: 'your-key' }, [
|
||||||
|
createLoggingPlugin({
|
||||||
|
logLevel: 'info',
|
||||||
|
includeParams: true,
|
||||||
|
includeResult: false
|
||||||
|
})
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### promptToolUsePlugin - 提示工具使用插件
|
||||||
|
|
||||||
|
为不支持原生 Function Call 的模型提供 prompt 方式的工具调用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createPromptToolUsePlugin } from '@cherrystudio/ai-core/built-in/plugins'
|
||||||
|
|
||||||
|
// 对于不支持 function call 的模型
|
||||||
|
const executor = AiCore.create(
|
||||||
|
'providerId',
|
||||||
|
{
|
||||||
|
apiKey: 'your-key',
|
||||||
|
baseURL: 'https://your-model-endpoint'
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createPromptToolUsePlugin({
|
||||||
|
enabled: true,
|
||||||
|
// 可选:自定义系统提示符构建
|
||||||
|
buildSystemPrompt: (userPrompt, tools) => {
|
||||||
|
return `${userPrompt}\n\nAvailable tools: ${Object.keys(tools).join(', ')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义插件
|
||||||
|
|
||||||
|
创建自定义插件非常简单:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { definePlugin } from '@cherrystudio/ai-core'
|
||||||
|
|
||||||
|
const customPlugin = definePlugin({
|
||||||
|
name: 'custom-plugin',
|
||||||
|
enforce: 'pre', // 'pre' | 'post' | undefined
|
||||||
|
|
||||||
|
// 在请求开始时记录日志
|
||||||
|
onRequestStart: async (context) => {
|
||||||
|
console.log(`Starting request for model: ${context.modelId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 转换请求参数
|
||||||
|
transformParams: async (params, context) => {
|
||||||
|
// 添加自定义系统消息
|
||||||
|
if (params.messages) {
|
||||||
|
params.messages.unshift({
|
||||||
|
role: 'system',
|
||||||
|
content: 'You are a helpful assistant.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理响应结果
|
||||||
|
transformResult: async (result, context) => {
|
||||||
|
// 添加元数据
|
||||||
|
if (result.text) {
|
||||||
|
result.metadata = {
|
||||||
|
processedAt: new Date().toISOString(),
|
||||||
|
modelId: context.modelId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用自定义插件
|
||||||
|
const executor = AiCore.create('openai', { apiKey: 'your-key' }, [customPlugin])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 AI SDK 原生 Provider 注册表
|
||||||
|
|
||||||
|
> https://ai-sdk.dev/docs/reference/ai-sdk-core/provider-registry
|
||||||
|
|
||||||
|
除了使用内建的 provider 管理,你还可以使用 AI SDK 原生的 `createProviderRegistry` 来构建自己的 provider 注册表。
|
||||||
|
|
||||||
|
#### 基本用法示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createClient } from '@cherrystudio/ai-core'
|
||||||
|
import { createProviderRegistry } from 'ai'
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
|
import { anthropic } from '@ai-sdk/anthropic'
|
||||||
|
|
||||||
|
// 1. 创建 AI SDK 原生注册表
|
||||||
|
export const registry = createProviderRegistry({
|
||||||
|
// register provider with prefix and default setup:
|
||||||
|
anthropic,
|
||||||
|
|
||||||
|
// register provider with prefix and custom setup:
|
||||||
|
openai: createOpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 创建client,'openai'可以传空或者传providerId(内建的provider)
|
||||||
|
const client = PluginEnabledAiClient.create('openai', {
|
||||||
|
apiKey: process.env.OPENAI_API_KEY
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 方式1:使用内建逻辑(传统方式)
|
||||||
|
const result1 = await client.streamText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello with built-in logic!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 方式2:使用自定义注册表(灵活方式)
|
||||||
|
const result2 = await client.streamText({
|
||||||
|
model: registry.languageModel('openai:gpt-4'),
|
||||||
|
messages: [{ role: 'user', content: 'Hello with custom registry!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. 支持的重载方法
|
||||||
|
await client.generateObject({
|
||||||
|
model: registry.languageModel('openai:gpt-4'),
|
||||||
|
schema: z.object({ name: z.string() }),
|
||||||
|
messages: [{ role: 'user', content: 'Generate a user' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.streamObject({
|
||||||
|
model: registry.languageModel('anthropic:claude-3-opus-20240229'),
|
||||||
|
schema: z.object({ items: z.array(z.string()) }),
|
||||||
|
messages: [{ role: 'user', content: 'Generate a list' }]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 与插件系统配合使用
|
||||||
|
|
||||||
|
更强大的是,你还可以将自定义注册表与 Cherry Studio 的插件系统结合使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PluginEnabledAiClient } from '@cherrystudio/ai-core'
|
||||||
|
import { createProviderRegistry } from 'ai'
|
||||||
|
import { createOpenAI } from '@ai-sdk/openai'
|
||||||
|
import { anthropic } from '@ai-sdk/anthropic'
|
||||||
|
|
||||||
|
// 1. 创建带插件的客户端
|
||||||
|
const client = PluginEnabledAiClient.create(
|
||||||
|
'openai',
|
||||||
|
{
|
||||||
|
apiKey: process.env.OPENAI_API_KEY
|
||||||
|
},
|
||||||
|
[LoggingPlugin, RetryPlugin]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. 创建自定义注册表
|
||||||
|
const registry = createProviderRegistry({
|
||||||
|
openai: createOpenAI({ apiKey: process.env.OPENAI_API_KEY }),
|
||||||
|
anthropic: anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. 方式1:使用内建逻辑 + 完整插件系统
|
||||||
|
await client.streamText('gpt-4', {
|
||||||
|
messages: [{ role: 'user', content: 'Hello with plugins!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 方式2:使用自定义注册表 + 有限插件支持
|
||||||
|
await client.streamText({
|
||||||
|
model: registry.languageModel('anthropic:claude-3-opus-20240229'),
|
||||||
|
messages: [{ role: 'user', content: 'Hello from Claude!' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. 支持的方法
|
||||||
|
await client.generateObject({
|
||||||
|
model: registry.languageModel('openai:gpt-4'),
|
||||||
|
schema: z.object({ name: z.string() }),
|
||||||
|
messages: [{ role: 'user', content: 'Generate a user' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
await client.streamObject({
|
||||||
|
model: registry.languageModel('openai:gpt-4'),
|
||||||
|
schema: z.object({ items: z.array(z.string()) }),
|
||||||
|
messages: [{ role: 'user', content: 'Generate a list' }]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 混合使用的优势
|
||||||
|
|
||||||
|
- **灵活性**:可以根据需要选择使用内建逻辑或自定义注册表
|
||||||
|
- **兼容性**:完全兼容 AI SDK 的 `createProviderRegistry` API
|
||||||
|
- **渐进式**:可以逐步迁移现有代码,无需一次性重构
|
||||||
|
- **插件支持**:自定义注册表仍可享受插件系统的部分功能
|
||||||
|
- **最佳实践**:结合两种方式的优点,既有动态加载的性能优势,又有统一注册表的便利性
|
||||||
|
|
||||||
|
## 📚 相关资源
|
||||||
|
|
||||||
|
- [Vercel AI SDK 文档](https://ai-sdk.dev/)
|
||||||
|
- [Cherry Studio 项目](https://github.com/CherryHQ/cherry-studio)
|
||||||
|
- [AI SDK Providers](https://ai-sdk.dev/providers/ai-sdk-providers)
|
||||||
|
|
||||||
|
## 未来版本
|
||||||
|
|
||||||
|
- 🔮 多 Agent 编排
|
||||||
|
- 🔮 可视化插件配置
|
||||||
|
- 🔮 实时监控和分析
|
||||||
|
- 🔮 云端插件同步
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - 详见 [LICENSE](https://github.com/CherryHQ/cherry-studio/blob/main/LICENSE) 文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Cherry Studio AI Core** - 让 AI 开发更简单、更强大、更灵活 🚀
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"name": "@cherrystudio/ai-core",
|
||||||
|
"version": "1.0.0-alpha.18",
|
||||||
|
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"react-native": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsdown",
|
||||||
|
"dev": "tsc -w",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"sdk",
|
||||||
|
"openai",
|
||||||
|
"anthropic",
|
||||||
|
"google",
|
||||||
|
"cherry-studio",
|
||||||
|
"vercel-ai-sdk"
|
||||||
|
],
|
||||||
|
"author": "Cherry Studio",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/CherryHQ/cherry-studio.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/CherryHQ/cherry-studio/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/CherryHQ/cherry-studio#readme",
|
||||||
|
"peerDependencies": {
|
||||||
|
"ai": "^5.0.26"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-sdk/anthropic": "^2.0.17",
|
||||||
|
"@ai-sdk/azure": "^2.0.30",
|
||||||
|
"@ai-sdk/deepseek": "^1.0.17",
|
||||||
|
"@ai-sdk/google": "^2.0.14",
|
||||||
|
"@ai-sdk/openai": "^2.0.30",
|
||||||
|
"@ai-sdk/openai-compatible": "^1.0.17",
|
||||||
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
|
"@ai-sdk/provider-utils": "^3.0.9",
|
||||||
|
"@ai-sdk/xai": "^2.0.18",
|
||||||
|
"zod": "^4.1.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsdown": "^0.12.9",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"react-native": "./dist/index.js",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./built-in/plugins": {
|
||||||
|
"types": "./dist/built-in/plugins/index.d.ts",
|
||||||
|
"react-native": "./dist/built-in/plugins/index.js",
|
||||||
|
"import": "./dist/built-in/plugins/index.mjs",
|
||||||
|
"require": "./dist/built-in/plugins/index.js",
|
||||||
|
"default": "./dist/built-in/plugins/index.js"
|
||||||
|
},
|
||||||
|
"./provider": {
|
||||||
|
"types": "./dist/provider/index.d.ts",
|
||||||
|
"react-native": "./dist/provider/index.js",
|
||||||
|
"import": "./dist/provider/index.mjs",
|
||||||
|
"require": "./dist/provider/index.js",
|
||||||
|
"default": "./dist/provider/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// 模拟 Vite SSR helper,避免 Node 环境找不到时报错
|
||||||
|
;(globalThis as any).__vite_ssr_exportName__ = (name: string, value: any) => value
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# @cherryStudio-aiCore
|
||||||
|
|
||||||
|
Core
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Core 模块导出
|
||||||
|
* 内部核心功能,供其他模块使用,不直接面向最终调用者
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 中间件系统
|
||||||
|
export type { NamedMiddleware } from './middleware'
|
||||||
|
export { createMiddlewares, wrapModelWithMiddlewares } from './middleware'
|
||||||
|
|
||||||
|
// 创建管理
|
||||||
|
export { globalModelResolver, ModelResolver } from './models'
|
||||||
|
export type { ModelConfig as ModelConfigType } from './models/types'
|
||||||
|
|
||||||
|
// 执行管理
|
||||||
|
export type { ToolUseRequestContext } from './plugins/built-in/toolUsePlugin/type'
|
||||||
|
export { createExecutor, createOpenAICompatibleExecutor } from './runtime'
|
||||||
|
export type { RuntimeConfig } from './runtime/types'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Middleware 模块导出
|
||||||
|
* 提供通用的中间件管理能力
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { createMiddlewares } from './manager'
|
||||||
|
export type { NamedMiddleware } from './types'
|
||||||
|
export { wrapModelWithMiddlewares } from './wrapper'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 中间件管理器
|
||||||
|
* 专注于 AI SDK 中间件的管理,与插件系统分离
|
||||||
|
*/
|
||||||
|
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建中间件列表
|
||||||
|
* 合并用户提供的中间件
|
||||||
|
*/
|
||||||
|
export function createMiddlewares(userMiddlewares: LanguageModelV2Middleware[] = []): LanguageModelV2Middleware[] {
|
||||||
|
// 未来可以在这里添加默认的中间件
|
||||||
|
const defaultMiddlewares: LanguageModelV2Middleware[] = []
|
||||||
|
|
||||||
|
return [...defaultMiddlewares, ...userMiddlewares]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 中间件系统类型定义
|
||||||
|
*/
|
||||||
|
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 具名中间件接口
|
||||||
|
*/
|
||||||
|
export interface NamedMiddleware {
|
||||||
|
name: string
|
||||||
|
middleware: LanguageModelV2Middleware
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 模型包装工具函数
|
||||||
|
* 用于将中间件应用到LanguageModel上
|
||||||
|
*/
|
||||||
|
import { LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
import { wrapLanguageModel } from 'ai'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用中间件包装模型
|
||||||
|
*/
|
||||||
|
export function wrapModelWithMiddlewares(
|
||||||
|
model: LanguageModelV2,
|
||||||
|
middlewares: LanguageModelV2Middleware[]
|
||||||
|
): LanguageModelV2 {
|
||||||
|
if (middlewares.length === 0) {
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapLanguageModel({
|
||||||
|
model,
|
||||||
|
middleware: middlewares
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* 模型解析器 - models模块的核心
|
||||||
|
* 负责将modelId解析为AI SDK的LanguageModel实例
|
||||||
|
* 支持传统格式和命名空间格式
|
||||||
|
* 集成了来自 ModelCreator 的特殊处理逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
|
import { wrapModelWithMiddlewares } from '../middleware/wrapper'
|
||||||
|
import { DEFAULT_SEPARATOR, globalRegistryManagement } from '../providers/RegistryManagement'
|
||||||
|
|
||||||
|
export class ModelResolver {
|
||||||
|
/**
|
||||||
|
* 核心方法:解析任意格式的modelId为语言模型
|
||||||
|
*
|
||||||
|
* @param modelId 模型ID,支持 'gpt-4' 和 'anthropic>claude-3' 两种格式
|
||||||
|
* @param fallbackProviderId 当modelId为传统格式时使用的providerId
|
||||||
|
* @param providerOptions provider配置选项(用于OpenAI模式选择等)
|
||||||
|
* @param middlewares 中间件数组,会应用到最终模型上
|
||||||
|
*/
|
||||||
|
async resolveLanguageModel(
|
||||||
|
modelId: string,
|
||||||
|
fallbackProviderId: string,
|
||||||
|
providerOptions?: any,
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<LanguageModelV2> {
|
||||||
|
let finalProviderId = fallbackProviderId
|
||||||
|
let model: LanguageModelV2
|
||||||
|
// 🎯 处理 OpenAI 模式选择逻辑 (从 ModelCreator 迁移)
|
||||||
|
if ((fallbackProviderId === 'openai' || fallbackProviderId === 'azure') && providerOptions?.mode === 'chat') {
|
||||||
|
finalProviderId = `${fallbackProviderId}-chat`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是命名空间格式
|
||||||
|
if (modelId.includes(DEFAULT_SEPARATOR)) {
|
||||||
|
model = this.resolveNamespacedModel(modelId)
|
||||||
|
} else {
|
||||||
|
// 传统格式:使用处理后的 providerId + modelId
|
||||||
|
model = this.resolveTraditionalModel(finalProviderId, modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 应用中间件(如果有)
|
||||||
|
if (middlewares && middlewares.length > 0) {
|
||||||
|
model = wrapModelWithMiddlewares(model, middlewares)
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析文本嵌入模型
|
||||||
|
*/
|
||||||
|
async resolveTextEmbeddingModel(modelId: string, fallbackProviderId: string): Promise<EmbeddingModelV2<string>> {
|
||||||
|
if (modelId.includes(DEFAULT_SEPARATOR)) {
|
||||||
|
return this.resolveNamespacedEmbeddingModel(modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolveTraditionalEmbeddingModel(fallbackProviderId, modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析图像模型
|
||||||
|
*/
|
||||||
|
async resolveImageModel(modelId: string, fallbackProviderId: string): Promise<ImageModelV2> {
|
||||||
|
if (modelId.includes(DEFAULT_SEPARATOR)) {
|
||||||
|
return this.resolveNamespacedImageModel(modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.resolveTraditionalImageModel(fallbackProviderId, modelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命名空间格式的语言模型
|
||||||
|
* aihubmix:anthropic:claude-3 -> globalRegistryManagement.languageModel('aihubmix:anthropic:claude-3')
|
||||||
|
*/
|
||||||
|
private resolveNamespacedModel(modelId: string): LanguageModelV2 {
|
||||||
|
return globalRegistryManagement.languageModel(modelId as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析传统格式的语言模型
|
||||||
|
* providerId: 'openai', modelId: 'gpt-4' -> globalRegistryManagement.languageModel('openai:gpt-4')
|
||||||
|
*/
|
||||||
|
private resolveTraditionalModel(providerId: string, modelId: string): LanguageModelV2 {
|
||||||
|
const fullModelId = `${providerId}${DEFAULT_SEPARATOR}${modelId}`
|
||||||
|
return globalRegistryManagement.languageModel(fullModelId as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命名空间格式的嵌入模型
|
||||||
|
*/
|
||||||
|
private resolveNamespacedEmbeddingModel(modelId: string): EmbeddingModelV2<string> {
|
||||||
|
return globalRegistryManagement.textEmbeddingModel(modelId as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析传统格式的嵌入模型
|
||||||
|
*/
|
||||||
|
private resolveTraditionalEmbeddingModel(providerId: string, modelId: string): EmbeddingModelV2<string> {
|
||||||
|
const fullModelId = `${providerId}${DEFAULT_SEPARATOR}${modelId}`
|
||||||
|
return globalRegistryManagement.textEmbeddingModel(fullModelId as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命名空间格式的图像模型
|
||||||
|
*/
|
||||||
|
private resolveNamespacedImageModel(modelId: string): ImageModelV2 {
|
||||||
|
return globalRegistryManagement.imageModel(modelId as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析传统格式的图像模型
|
||||||
|
*/
|
||||||
|
private resolveTraditionalImageModel(providerId: string, modelId: string): ImageModelV2 {
|
||||||
|
const fullModelId = `${providerId}${DEFAULT_SEPARATOR}${modelId}`
|
||||||
|
return globalRegistryManagement.imageModel(fullModelId as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局模型解析器实例
|
||||||
|
*/
|
||||||
|
export const globalModelResolver = new ModelResolver()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Models 模块统一导出 - 简化版
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 核心模型解析器
|
||||||
|
export { globalModelResolver, ModelResolver } from './ModelResolver'
|
||||||
|
|
||||||
|
// 保留的类型定义(可能被其他地方使用)
|
||||||
|
export type { ModelConfig as ModelConfigType } from './types'
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Creation 模块类型定义
|
||||||
|
*/
|
||||||
|
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
|
import type { ProviderId, ProviderSettingsMap } from '../providers/types'
|
||||||
|
|
||||||
|
export interface ModelConfig<T extends ProviderId = ProviderId> {
|
||||||
|
providerId: T
|
||||||
|
modelId: string
|
||||||
|
providerSettings: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' }
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
// 额外模型参数
|
||||||
|
extraModelConfig?: Record<string, any>
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { streamText } from 'ai'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAnthropicOptions,
|
||||||
|
createGenericProviderOptions,
|
||||||
|
createGoogleOptions,
|
||||||
|
createOpenAIOptions,
|
||||||
|
mergeProviderOptions
|
||||||
|
} from './factory'
|
||||||
|
|
||||||
|
// 示例1: 使用已知供应商的严格类型约束
|
||||||
|
export function exampleOpenAIWithOptions() {
|
||||||
|
const openaiOptions = createOpenAIOptions({
|
||||||
|
reasoningEffort: 'medium'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 这里会有类型检查,确保选项符合OpenAI的设置
|
||||||
|
return streamText({
|
||||||
|
model: {} as any, // 实际使用时替换为真实模型
|
||||||
|
prompt: 'Hello',
|
||||||
|
providerOptions: openaiOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例2: 使用Anthropic供应商选项
|
||||||
|
export function exampleAnthropicWithOptions() {
|
||||||
|
const anthropicOptions = createAnthropicOptions({
|
||||||
|
thinking: {
|
||||||
|
type: 'enabled',
|
||||||
|
budgetTokens: 1000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return streamText({
|
||||||
|
model: {} as any,
|
||||||
|
prompt: 'Hello',
|
||||||
|
providerOptions: anthropicOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例3: 使用Google供应商选项
|
||||||
|
export function exampleGoogleWithOptions() {
|
||||||
|
const googleOptions = createGoogleOptions({
|
||||||
|
thinkingConfig: {
|
||||||
|
includeThoughts: true,
|
||||||
|
thinkingBudget: 1000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return streamText({
|
||||||
|
model: {} as any,
|
||||||
|
prompt: 'Hello',
|
||||||
|
providerOptions: googleOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例4: 使用未知供应商(通用类型)
|
||||||
|
export function exampleUnknownProviderWithOptions() {
|
||||||
|
const customProviderOptions = createGenericProviderOptions('custom-provider', {
|
||||||
|
temperature: 0.7,
|
||||||
|
customSetting: 'value',
|
||||||
|
anotherOption: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return streamText({
|
||||||
|
model: {} as any,
|
||||||
|
prompt: 'Hello',
|
||||||
|
providerOptions: customProviderOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例5: 合并多个供应商选项
|
||||||
|
export function exampleMergedOptions() {
|
||||||
|
const openaiOptions = createOpenAIOptions({})
|
||||||
|
|
||||||
|
const customOptions = createGenericProviderOptions('custom', {
|
||||||
|
customParam: 'value'
|
||||||
|
})
|
||||||
|
|
||||||
|
const mergedOptions = mergeProviderOptions(openaiOptions, customOptions)
|
||||||
|
|
||||||
|
return streamText({
|
||||||
|
model: {} as any,
|
||||||
|
prompt: 'Hello',
|
||||||
|
providerOptions: mergedOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { ExtractProviderOptions, ProviderOptionsMap, TypedProviderOptions } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建特定供应商的选项
|
||||||
|
* @param provider 供应商名称
|
||||||
|
* @param options 供应商特定的选项
|
||||||
|
* @returns 格式化的provider options
|
||||||
|
*/
|
||||||
|
export function createProviderOptions<T extends keyof ProviderOptionsMap>(
|
||||||
|
provider: T,
|
||||||
|
options: ExtractProviderOptions<T>
|
||||||
|
): Record<T, ExtractProviderOptions<T>> {
|
||||||
|
return { [provider]: options } as Record<T, ExtractProviderOptions<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建任意供应商的选项(包括未知供应商)
|
||||||
|
* @param provider 供应商名称
|
||||||
|
* @param options 供应商选项
|
||||||
|
* @returns 格式化的provider options
|
||||||
|
*/
|
||||||
|
export function createGenericProviderOptions<T extends string>(
|
||||||
|
provider: T,
|
||||||
|
options: Record<string, any>
|
||||||
|
): Record<T, Record<string, any>> {
|
||||||
|
return { [provider]: options } as Record<T, Record<string, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并多个供应商的options
|
||||||
|
* @param optionsMap 包含多个供应商选项的对象
|
||||||
|
* @returns 合并后的TypedProviderOptions
|
||||||
|
*/
|
||||||
|
export function mergeProviderOptions(...optionsMap: Partial<TypedProviderOptions>[]): TypedProviderOptions {
|
||||||
|
return Object.assign({}, ...optionsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建OpenAI供应商选项的便捷函数
|
||||||
|
*/
|
||||||
|
export function createOpenAIOptions(options: ExtractProviderOptions<'openai'>) {
|
||||||
|
return createProviderOptions('openai', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Anthropic供应商选项的便捷函数
|
||||||
|
*/
|
||||||
|
export function createAnthropicOptions(options: ExtractProviderOptions<'anthropic'>) {
|
||||||
|
return createProviderOptions('anthropic', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Google供应商选项的便捷函数
|
||||||
|
*/
|
||||||
|
export function createGoogleOptions(options: ExtractProviderOptions<'google'>) {
|
||||||
|
return createProviderOptions('google', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建OpenRouter供应商选项的便捷函数
|
||||||
|
*/
|
||||||
|
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'> | Record<string, any>) {
|
||||||
|
return createProviderOptions('openrouter', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建XAI供应商选项的便捷函数
|
||||||
|
*/
|
||||||
|
export function createXaiOptions(options: ExtractProviderOptions<'xai'>) {
|
||||||
|
return createProviderOptions('xai', options)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './factory'
|
||||||
|
export * from './types'
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { type AnthropicProviderOptions } from '@ai-sdk/anthropic'
|
||||||
|
import { type GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
|
||||||
|
import { type OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
|
||||||
|
import { type SharedV2ProviderMetadata } from '@ai-sdk/provider'
|
||||||
|
import { type XaiProviderOptions } from '@ai-sdk/xai'
|
||||||
|
import { type OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider'
|
||||||
|
|
||||||
|
export type ProviderOptions<T extends keyof SharedV2ProviderMetadata> = SharedV2ProviderMetadata[T]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商选项类型,如果map中没有,说明没有约束
|
||||||
|
*/
|
||||||
|
export type ProviderOptionsMap = {
|
||||||
|
openai: OpenAIResponsesProviderOptions
|
||||||
|
anthropic: AnthropicProviderOptions
|
||||||
|
google: GoogleGenerativeAIProviderOptions
|
||||||
|
openrouter: OpenRouterProviderOptions
|
||||||
|
xai: XaiProviderOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具类型,用于从ProviderOptionsMap中提取特定供应商的选项类型
|
||||||
|
export type ExtractProviderOptions<T extends keyof ProviderOptionsMap> = ProviderOptionsMap[T]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型安全的ProviderOptions
|
||||||
|
* 对于已知供应商使用严格类型,对于未知供应商允许任意Record<string, JSONValue>
|
||||||
|
*/
|
||||||
|
export type TypedProviderOptions = {
|
||||||
|
[K in keyof ProviderOptionsMap]?: ProviderOptionsMap[K]
|
||||||
|
} & {
|
||||||
|
[K in string]?: Record<string, any>
|
||||||
|
} & SharedV2ProviderMetadata
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# AI Core 插件系统
|
||||||
|
|
||||||
|
支持四种钩子类型:**First**、**Sequential**、**Parallel** 和 **Stream**。
|
||||||
|
|
||||||
|
## 🎯 设计理念
|
||||||
|
|
||||||
|
- **语义清晰**:不同钩子有不同的执行语义
|
||||||
|
- **类型安全**:TypeScript 完整支持
|
||||||
|
- **性能优化**:First 短路、Parallel 并发、Sequential 链式
|
||||||
|
- **易于扩展**:`enforce` 排序 + 功能分类
|
||||||
|
|
||||||
|
## 📋 钩子类型
|
||||||
|
|
||||||
|
### 🥇 First 钩子 - 首个有效结果
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 只执行第一个返回值的插件,用于解析和查找
|
||||||
|
resolveModel?: (modelId: string, context: AiRequestContext) => string | null
|
||||||
|
loadTemplate?: (templateName: string, context: AiRequestContext) => any | null
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Sequential 钩子 - 链式数据转换
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 按顺序链式执行,每个插件可以修改数据
|
||||||
|
transformParams?: (params: any, context: AiRequestContext) => any
|
||||||
|
transformResult?: (result: any, context: AiRequestContext) => any
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚡ Parallel 钩子 - 并行副作用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 并发执行,用于日志、监控等副作用
|
||||||
|
onRequestStart?: (context: AiRequestContext) => void
|
||||||
|
onRequestEnd?: (context: AiRequestContext, result: any) => void
|
||||||
|
onError?: (error: Error, context: AiRequestContext) => void
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌊 Stream 钩子 - 流处理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 直接使用 AI SDK 的 TransformStream
|
||||||
|
transformStream?: () => (options) => TransformStream<TextStreamPart, TextStreamPart>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 基础用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { PluginManager, createContext, definePlugin } from '@cherrystudio/ai-core/middleware'
|
||||||
|
|
||||||
|
// 创建插件管理器
|
||||||
|
const pluginManager = new PluginManager()
|
||||||
|
|
||||||
|
// 添加插件
|
||||||
|
pluginManager.use({
|
||||||
|
name: 'my-plugin',
|
||||||
|
async transformParams(params, context) {
|
||||||
|
return { ...params, temperature: 0.7 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用插件
|
||||||
|
const context = createContext('openai', 'gpt-4', { messages: [] })
|
||||||
|
const transformedParams = await pluginManager.executeSequential(
|
||||||
|
'transformParams',
|
||||||
|
{ messages: [{ role: 'user', content: 'Hello' }] },
|
||||||
|
context
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
PluginManager,
|
||||||
|
ModelAliasPlugin,
|
||||||
|
LoggingPlugin,
|
||||||
|
ParamsValidationPlugin,
|
||||||
|
createContext
|
||||||
|
} from '@cherrystudio/ai-core/middleware'
|
||||||
|
|
||||||
|
// 创建插件管理器
|
||||||
|
const manager = new PluginManager([
|
||||||
|
ModelAliasPlugin, // 模型别名解析
|
||||||
|
ParamsValidationPlugin, // 参数验证
|
||||||
|
LoggingPlugin // 日志记录
|
||||||
|
])
|
||||||
|
|
||||||
|
// AI 请求流程
|
||||||
|
async function aiRequest(providerId: string, modelId: string, params: any) {
|
||||||
|
const context = createContext(providerId, modelId, params)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 【并行】触发请求开始事件
|
||||||
|
await manager.executeParallel('onRequestStart', context)
|
||||||
|
|
||||||
|
// 2. 【首个】解析模型别名
|
||||||
|
const resolvedModel = await manager.executeFirst('resolveModel', modelId, context)
|
||||||
|
context.modelId = resolvedModel || modelId
|
||||||
|
|
||||||
|
// 3. 【串行】转换请求参数
|
||||||
|
const transformedParams = await manager.executeSequential('transformParams', params, context)
|
||||||
|
|
||||||
|
// 4. 【流处理】收集流转换器(AI SDK 原生支持数组)
|
||||||
|
const streamTransforms = manager.collectStreamTransforms()
|
||||||
|
|
||||||
|
// 5. 调用 AI SDK(这里省略具体实现)
|
||||||
|
const result = await callAiSdk(transformedParams, streamTransforms)
|
||||||
|
|
||||||
|
// 6. 【串行】转换响应结果
|
||||||
|
const transformedResult = await manager.executeSequential('transformResult', result, context)
|
||||||
|
|
||||||
|
// 7. 【并行】触发请求完成事件
|
||||||
|
await manager.executeParallel('onRequestEnd', context, transformedResult)
|
||||||
|
|
||||||
|
return transformedResult
|
||||||
|
} catch (error) {
|
||||||
|
// 8. 【并行】触发错误事件
|
||||||
|
await manager.executeParallel('onError', context, undefined, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 自定义插件
|
||||||
|
|
||||||
|
### 模型别名插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ModelAliasPlugin = definePlugin({
|
||||||
|
name: 'model-alias',
|
||||||
|
enforce: 'pre', // 最先执行
|
||||||
|
|
||||||
|
async resolveModel(modelId) {
|
||||||
|
const aliases = {
|
||||||
|
gpt4: 'gpt-4-turbo-preview',
|
||||||
|
claude: 'claude-3-sonnet-20240229'
|
||||||
|
}
|
||||||
|
return aliases[modelId] || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数验证插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ValidationPlugin = definePlugin({
|
||||||
|
name: 'validation',
|
||||||
|
|
||||||
|
async transformParams(params) {
|
||||||
|
if (!params.messages) {
|
||||||
|
throw new Error('messages is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...params,
|
||||||
|
temperature: params.temperature ?? 0.7,
|
||||||
|
max_tokens: params.max_tokens ?? 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监控插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MonitoringPlugin = definePlugin({
|
||||||
|
name: 'monitoring',
|
||||||
|
enforce: 'post', // 最后执行
|
||||||
|
|
||||||
|
async onRequestEnd(context, result) {
|
||||||
|
const duration = Date.now() - context.startTime
|
||||||
|
console.log(`请求耗时: ${duration}ms`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内容过滤插件
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const FilterPlugin = definePlugin({
|
||||||
|
name: 'content-filter',
|
||||||
|
|
||||||
|
transformStream() {
|
||||||
|
return () =>
|
||||||
|
new TransformStream({
|
||||||
|
transform(chunk, controller) {
|
||||||
|
if (chunk.type === 'text-delta') {
|
||||||
|
const filtered = chunk.textDelta.replace(/敏感词/g, '***')
|
||||||
|
controller.enqueue({ ...chunk, textDelta: filtered })
|
||||||
|
} else {
|
||||||
|
controller.enqueue(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 执行顺序
|
||||||
|
|
||||||
|
### 插件排序
|
||||||
|
|
||||||
|
```
|
||||||
|
enforce: 'pre' → normal → enforce: 'post'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 钩子执行流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[请求开始] --> B[onRequestStart 并行执行]
|
||||||
|
B --> C[resolveModel 首个有效]
|
||||||
|
C --> D[loadTemplate 首个有效]
|
||||||
|
D --> E[transformParams 串行执行]
|
||||||
|
E --> F[collectStreamTransforms]
|
||||||
|
F --> G[AI SDK 调用]
|
||||||
|
G --> H[transformResult 串行执行]
|
||||||
|
H --> I[onRequestEnd 并行执行]
|
||||||
|
|
||||||
|
G --> J[异常处理]
|
||||||
|
J --> K[onError 并行执行]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 最佳实践
|
||||||
|
|
||||||
|
1. **功能单一**:每个插件专注一个功能
|
||||||
|
2. **幂等性**:插件应该是幂等的,重复执行不会产生副作用
|
||||||
|
3. **错误处理**:插件内部处理异常,不要让异常向上传播
|
||||||
|
4. **性能优化**:使用合适的钩子类型(First vs Sequential vs Parallel)
|
||||||
|
5. **命名规范**:使用语义化的插件名称
|
||||||
|
|
||||||
|
## 🔍 调试工具
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 查看插件统计信息
|
||||||
|
const stats = manager.getStats()
|
||||||
|
console.log('插件统计:', stats)
|
||||||
|
|
||||||
|
// 查看所有插件
|
||||||
|
const plugins = manager.getPlugins()
|
||||||
|
console.log(
|
||||||
|
'已注册插件:',
|
||||||
|
plugins.map((p) => p.name)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ 性能优势
|
||||||
|
|
||||||
|
- **First 钩子**:一旦找到结果立即停止,避免无效计算
|
||||||
|
- **Parallel 钩子**:真正并发执行,不阻塞主流程
|
||||||
|
- **Sequential 钩子**:保证数据转换的顺序性
|
||||||
|
- **Stream 钩子**:直接集成 AI SDK,零开销
|
||||||
|
|
||||||
|
这个设计兼顾了简洁性和强大功能,为 AI Core 提供了灵活而高效的扩展机制。
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { google } from '@ai-sdk/google'
|
||||||
|
|
||||||
|
import { definePlugin } from '../../'
|
||||||
|
import type { AiRequestContext } from '../../types'
|
||||||
|
|
||||||
|
const toolNameMap = {
|
||||||
|
googleSearch: 'google_search',
|
||||||
|
urlContext: 'url_context',
|
||||||
|
codeExecution: 'code_execution'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type ToolConfigKey = keyof typeof toolNameMap
|
||||||
|
type ToolConfig = { googleSearch?: boolean; urlContext?: boolean; codeExecution?: boolean }
|
||||||
|
|
||||||
|
export const googleToolsPlugin = (config?: ToolConfig) =>
|
||||||
|
definePlugin({
|
||||||
|
name: 'googleToolsPlugin',
|
||||||
|
transformParams: <T>(params: T, context: AiRequestContext): T => {
|
||||||
|
const { providerId } = context
|
||||||
|
if (providerId === 'google' && config) {
|
||||||
|
if (typeof params === 'object' && params !== null) {
|
||||||
|
const typedParams = params as T & { tools?: Record<string, unknown> }
|
||||||
|
|
||||||
|
if (!typedParams.tools) {
|
||||||
|
typedParams.tools = {}
|
||||||
|
}
|
||||||
|
// 使用类型安全的方式遍历配置
|
||||||
|
;(Object.keys(config) as ToolConfigKey[]).forEach((key) => {
|
||||||
|
if (config[key] && key in toolNameMap && key in google.tools) {
|
||||||
|
const toolName = toolNameMap[key]
|
||||||
|
typedParams.tools![toolName] = google.tools[key]({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 内置插件命名空间
|
||||||
|
* 所有内置插件都以 'built-in:' 为前缀
|
||||||
|
*/
|
||||||
|
export const BUILT_IN_PLUGIN_PREFIX = 'built-in:'
|
||||||
|
|
||||||
|
export { googleToolsPlugin } from './googleToolsPlugin'
|
||||||
|
export { createLoggingPlugin } from './logging'
|
||||||
|
export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin'
|
||||||
|
export type {
|
||||||
|
PromptToolUseConfig,
|
||||||
|
ToolUseRequestContext,
|
||||||
|
ToolUseResult
|
||||||
|
} from './toolUsePlugin/type'
|
||||||
|
export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin'
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 内置插件:日志记录
|
||||||
|
* 记录AI调用的关键信息,支持性能监控和调试
|
||||||
|
*/
|
||||||
|
import { definePlugin } from '../index'
|
||||||
|
import type { AiRequestContext } from '../types'
|
||||||
|
|
||||||
|
export interface LoggingConfig {
|
||||||
|
// 日志级别
|
||||||
|
level?: 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
// 是否记录参数
|
||||||
|
logParams?: boolean
|
||||||
|
// 是否记录结果
|
||||||
|
logResult?: boolean
|
||||||
|
// 是否记录性能数据
|
||||||
|
logPerformance?: boolean
|
||||||
|
// 自定义日志函数
|
||||||
|
logger?: (level: string, message: string, data?: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建日志插件
|
||||||
|
*/
|
||||||
|
export function createLoggingPlugin(config: LoggingConfig = {}) {
|
||||||
|
const { level = 'info', logParams = true, logResult = false, logPerformance = true, logger = console.log } = config
|
||||||
|
|
||||||
|
const startTimes = new Map<string, number>()
|
||||||
|
|
||||||
|
return definePlugin({
|
||||||
|
name: 'built-in:logging',
|
||||||
|
|
||||||
|
onRequestStart: (context: AiRequestContext) => {
|
||||||
|
const requestId = context.requestId
|
||||||
|
startTimes.set(requestId, Date.now())
|
||||||
|
|
||||||
|
logger(level, `🚀 AI Request Started`, {
|
||||||
|
requestId,
|
||||||
|
providerId: context.providerId,
|
||||||
|
modelId: context.modelId,
|
||||||
|
originalParams: logParams ? context.originalParams : '[hidden]'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequestEnd: (context: AiRequestContext, result: any) => {
|
||||||
|
const requestId = context.requestId
|
||||||
|
const startTime = startTimes.get(requestId)
|
||||||
|
const duration = startTime ? Date.now() - startTime : undefined
|
||||||
|
startTimes.delete(requestId)
|
||||||
|
|
||||||
|
const logData: any = {
|
||||||
|
requestId,
|
||||||
|
providerId: context.providerId,
|
||||||
|
modelId: context.modelId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logPerformance && duration) {
|
||||||
|
logData.duration = `${duration}ms`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logResult) {
|
||||||
|
logData.result = result
|
||||||
|
}
|
||||||
|
|
||||||
|
logger(level, `✅ AI Request Completed`, logData)
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error: Error, context: AiRequestContext) => {
|
||||||
|
const requestId = context.requestId
|
||||||
|
const startTime = startTimes.get(requestId)
|
||||||
|
const duration = startTime ? Date.now() - startTime : undefined
|
||||||
|
startTimes.delete(requestId)
|
||||||
|
|
||||||
|
logger('error', `❌ AI Request Failed`, {
|
||||||
|
requestId,
|
||||||
|
providerId: context.providerId,
|
||||||
|
modelId: context.modelId,
|
||||||
|
duration: duration ? `${duration}ms` : undefined,
|
||||||
|
error: {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* 流事件管理器
|
||||||
|
*
|
||||||
|
* 负责处理 AI SDK 流事件的发送和管理
|
||||||
|
* 从 promptToolUsePlugin.ts 中提取出来以降低复杂度
|
||||||
|
*/
|
||||||
|
import type { ModelMessage } from 'ai'
|
||||||
|
|
||||||
|
import type { AiRequestContext } from '../../types'
|
||||||
|
import type { StreamController } from './ToolExecutor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流事件管理器类
|
||||||
|
*/
|
||||||
|
export class StreamEventManager {
|
||||||
|
/**
|
||||||
|
* 发送工具调用步骤开始事件
|
||||||
|
*/
|
||||||
|
sendStepStartEvent(controller: StreamController): void {
|
||||||
|
controller.enqueue({
|
||||||
|
type: 'start-step',
|
||||||
|
request: {},
|
||||||
|
warnings: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送步骤完成事件
|
||||||
|
*/
|
||||||
|
sendStepFinishEvent(
|
||||||
|
controller: StreamController,
|
||||||
|
chunk: any,
|
||||||
|
context: AiRequestContext,
|
||||||
|
finishReason: string = 'stop'
|
||||||
|
): void {
|
||||||
|
// 累加当前步骤的 usage
|
||||||
|
if (chunk.usage && context.accumulatedUsage) {
|
||||||
|
this.accumulateUsage(context.accumulatedUsage, chunk.usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue({
|
||||||
|
type: 'finish-step',
|
||||||
|
finishReason,
|
||||||
|
response: chunk.response,
|
||||||
|
usage: chunk.usage,
|
||||||
|
providerMetadata: chunk.providerMetadata
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理递归调用并将结果流接入当前流
|
||||||
|
*/
|
||||||
|
async handleRecursiveCall(
|
||||||
|
controller: StreamController,
|
||||||
|
recursiveParams: any,
|
||||||
|
context: AiRequestContext
|
||||||
|
): Promise<void> {
|
||||||
|
// try {
|
||||||
|
// 重置工具执行状态,准备处理新的步骤
|
||||||
|
context.hasExecutedToolsInCurrentStep = false
|
||||||
|
|
||||||
|
const recursiveResult = await context.recursiveCall(recursiveParams)
|
||||||
|
|
||||||
|
if (recursiveResult && recursiveResult.fullStream) {
|
||||||
|
await this.pipeRecursiveStream(controller, recursiveResult.fullStream, context)
|
||||||
|
} else {
|
||||||
|
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
|
||||||
|
}
|
||||||
|
// } catch (error) {
|
||||||
|
// this.handleRecursiveCallError(controller, error, stepId)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将递归流的数据传递到当前流
|
||||||
|
*/
|
||||||
|
private async pipeRecursiveStream(
|
||||||
|
controller: StreamController,
|
||||||
|
recursiveStream: ReadableStream,
|
||||||
|
context?: AiRequestContext
|
||||||
|
): Promise<void> {
|
||||||
|
const reader = recursiveStream.getReader()
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (value.type === 'finish') {
|
||||||
|
// 迭代的流不发finish,但需要累加其 usage
|
||||||
|
if (value.usage && context?.accumulatedUsage) {
|
||||||
|
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 对于 finish-step 类型,累加其 usage
|
||||||
|
if (value.type === 'finish-step' && value.usage && context?.accumulatedUsage) {
|
||||||
|
this.accumulateUsage(context.accumulatedUsage, value.usage)
|
||||||
|
}
|
||||||
|
// 将递归流的数据传递到当前流
|
||||||
|
controller.enqueue(value)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理递归调用错误
|
||||||
|
*/
|
||||||
|
// private handleRecursiveCallError(controller: StreamController, error: unknown): void {
|
||||||
|
// console.error('[MCP Prompt] Recursive call failed:', error)
|
||||||
|
|
||||||
|
// // 使用 AI SDK 标准错误格式,但不中断流
|
||||||
|
// controller.enqueue({
|
||||||
|
// type: 'error',
|
||||||
|
// error: {
|
||||||
|
// message: error instanceof Error ? error.message : String(error),
|
||||||
|
// name: error instanceof Error ? error.name : 'RecursiveCallError'
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // // 继续发送文本增量,保持流的连续性
|
||||||
|
// // controller.enqueue({
|
||||||
|
// // type: 'text-delta',
|
||||||
|
// // id: stepId,
|
||||||
|
// // text: '\n\n[工具执行后递归调用失败,继续对话...]'
|
||||||
|
// // })
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建递归调用的参数
|
||||||
|
*/
|
||||||
|
buildRecursiveParams(context: AiRequestContext, textBuffer: string, toolResultsText: string, tools: any): any {
|
||||||
|
// 构建新的对话消息
|
||||||
|
const newMessages: ModelMessage[] = [
|
||||||
|
...(context.originalParams.messages || []),
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: textBuffer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: toolResultsText
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 递归调用,继续对话,重新传递 tools
|
||||||
|
const recursiveParams = {
|
||||||
|
...context.originalParams,
|
||||||
|
messages: newMessages,
|
||||||
|
tools: tools
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新上下文中的消息
|
||||||
|
context.originalParams.messages = newMessages
|
||||||
|
|
||||||
|
return recursiveParams
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 累加 usage 数据
|
||||||
|
*/
|
||||||
|
private accumulateUsage(target: any, source: any): void {
|
||||||
|
if (!target || !source) return
|
||||||
|
|
||||||
|
// 累加各种 token 类型
|
||||||
|
target.inputTokens = (target.inputTokens || 0) + (source.inputTokens || 0)
|
||||||
|
target.outputTokens = (target.outputTokens || 0) + (source.outputTokens || 0)
|
||||||
|
target.totalTokens = (target.totalTokens || 0) + (source.totalTokens || 0)
|
||||||
|
target.reasoningTokens = (target.reasoningTokens || 0) + (source.reasoningTokens || 0)
|
||||||
|
target.cachedInputTokens = (target.cachedInputTokens || 0) + (source.cachedInputTokens || 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* 工具执行器
|
||||||
|
*
|
||||||
|
* 负责工具的执行、结果格式化和相关事件发送
|
||||||
|
* 从 promptToolUsePlugin.ts 中提取出来以降低复杂度
|
||||||
|
*/
|
||||||
|
import type { ToolSet, TypedToolError } from 'ai'
|
||||||
|
|
||||||
|
import type { ToolUseResult } from './type'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具执行结果
|
||||||
|
*/
|
||||||
|
export interface ExecutedResult {
|
||||||
|
toolCallId: string
|
||||||
|
toolName: string
|
||||||
|
result: any
|
||||||
|
isError?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流控制器类型(从 AI SDK 提取)
|
||||||
|
*/
|
||||||
|
export interface StreamController {
|
||||||
|
enqueue(chunk: any): void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具执行器类
|
||||||
|
*/
|
||||||
|
export class ToolExecutor {
|
||||||
|
/**
|
||||||
|
* 执行多个工具调用
|
||||||
|
*/
|
||||||
|
async executeTools(
|
||||||
|
toolUses: ToolUseResult[],
|
||||||
|
tools: ToolSet,
|
||||||
|
controller: StreamController
|
||||||
|
): Promise<ExecutedResult[]> {
|
||||||
|
const executedResults: ExecutedResult[] = []
|
||||||
|
for (const toolUse of toolUses) {
|
||||||
|
try {
|
||||||
|
const tool = tools[toolUse.toolName]
|
||||||
|
if (!tool || typeof tool.execute !== 'function') {
|
||||||
|
throw new Error(`Tool "${toolUse.toolName}" has no execute method`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 tool-call 事件
|
||||||
|
controller.enqueue({
|
||||||
|
type: 'tool-call',
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
toolName: toolUse.toolName,
|
||||||
|
input: toolUse.arguments
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await tool.execute(toolUse.arguments, {
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
messages: [],
|
||||||
|
abortSignal: new AbortController().signal
|
||||||
|
})
|
||||||
|
|
||||||
|
// 发送 tool-result 事件
|
||||||
|
controller.enqueue({
|
||||||
|
type: 'tool-result',
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
toolName: toolUse.toolName,
|
||||||
|
input: toolUse.arguments,
|
||||||
|
output: result
|
||||||
|
})
|
||||||
|
|
||||||
|
executedResults.push({
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
toolName: toolUse.toolName,
|
||||||
|
result,
|
||||||
|
isError: false
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MCP Prompt Stream] Tool execution failed: ${toolUse.toolName}`, error)
|
||||||
|
|
||||||
|
// 处理错误情况
|
||||||
|
const errorResult = this.handleToolError(toolUse, error, controller)
|
||||||
|
executedResults.push(errorResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return executedResults
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化工具结果为 Cherry Studio 标准格式
|
||||||
|
*/
|
||||||
|
formatToolResults(executedResults: ExecutedResult[]): string {
|
||||||
|
return executedResults
|
||||||
|
.map((tr) => {
|
||||||
|
if (!tr.isError) {
|
||||||
|
return `<tool_use_result>\n <name>${tr.toolName}</name>\n <result>${JSON.stringify(tr.result)}</result>\n</tool_use_result>`
|
||||||
|
} else {
|
||||||
|
const error = tr.result || 'Unknown error'
|
||||||
|
return `<tool_use_result>\n <name>${tr.toolName}</name>\n <error>${error}</error>\n</tool_use_result>`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('\n\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送工具调用开始相关事件
|
||||||
|
*/
|
||||||
|
// private sendToolStartEvents(controller: StreamController, toolUse: ToolUseResult): void {
|
||||||
|
// // 发送 tool-input-start 事件
|
||||||
|
// controller.enqueue({
|
||||||
|
// type: 'tool-input-start',
|
||||||
|
// id: toolUse.id,
|
||||||
|
// toolName: toolUse.toolName
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理工具执行错误
|
||||||
|
*/
|
||||||
|
private handleToolError<T extends ToolSet>(
|
||||||
|
toolUse: ToolUseResult,
|
||||||
|
error: unknown,
|
||||||
|
controller: StreamController
|
||||||
|
): ExecutedResult {
|
||||||
|
// 使用 AI SDK 标准错误格式
|
||||||
|
const toolError: TypedToolError<T> = {
|
||||||
|
type: 'tool-error',
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
toolName: toolUse.toolName,
|
||||||
|
input: toolUse.arguments,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(toolError)
|
||||||
|
|
||||||
|
// 发送标准错误事件
|
||||||
|
// controller.enqueue({
|
||||||
|
// type: 'tool-error',
|
||||||
|
// toolCallId: toolUse.id,
|
||||||
|
// error: error instanceof Error ? error.message : String(error),
|
||||||
|
// input: toolUse.arguments
|
||||||
|
// })
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolCallId: toolUse.id,
|
||||||
|
toolName: toolUse.toolName,
|
||||||
|
result: error,
|
||||||
|
isError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,427 @@
|
|||||||
|
/**
|
||||||
|
* 内置插件:MCP Prompt 模式
|
||||||
|
* 为不支持原生 Function Call 的模型提供 prompt 方式的工具调用
|
||||||
|
* 内置默认逻辑,支持自定义覆盖
|
||||||
|
*/
|
||||||
|
import type { TextStreamPart, ToolSet } from 'ai'
|
||||||
|
|
||||||
|
import { definePlugin } from '../../index'
|
||||||
|
import type { AiRequestContext } from '../../types'
|
||||||
|
import { StreamEventManager } from './StreamEventManager'
|
||||||
|
import { type TagConfig, TagExtractor } from './tagExtraction'
|
||||||
|
import { ToolExecutor } from './ToolExecutor'
|
||||||
|
import { PromptToolUseConfig, ToolUseResult } from './type'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具使用标签配置
|
||||||
|
*/
|
||||||
|
const TOOL_USE_TAG_CONFIG: TagConfig = {
|
||||||
|
openingTag: '<tool_use>',
|
||||||
|
closingTag: '</tool_use>',
|
||||||
|
separator: '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认系统提示符模板(提取自 Cherry Studio)
|
||||||
|
*/
|
||||||
|
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 is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:
|
||||||
|
|
||||||
|
<tool_use>
|
||||||
|
<name>{tool_name}</name>
|
||||||
|
<arguments>{json_arguments}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
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>
|
||||||
|
<name>python_interpreter</name>
|
||||||
|
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
The user will respond with the result of the tool use, which should be formatted as follows:
|
||||||
|
|
||||||
|
<tool_use_result>
|
||||||
|
<name>{tool_name}</name>
|
||||||
|
<result>{result}</result>
|
||||||
|
</tool_use_result>
|
||||||
|
|
||||||
|
The result should be a string, which can represent a file or any other output type. You can use this result as input for the next action.
|
||||||
|
For example, if the result of the tool use is an image file, you can use it in the next action like this:
|
||||||
|
|
||||||
|
<tool_use>
|
||||||
|
<name>image_transformer</name>
|
||||||
|
<arguments>{"image": "image_1.jpg"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
2. Call a tool only when needed: do not call the search agent if you do not need information, try to solve the task yourself.
|
||||||
|
3. If no tool call is needed, just answer the question directly.
|
||||||
|
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.
|
||||||
|
|
||||||
|
# User Instructions
|
||||||
|
{{ USER_SYSTEM_PROMPT }}
|
||||||
|
|
||||||
|
Now Begin! If you solve the task correctly, you will receive a reward of $1,000,000.`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认工具使用示例(提取自 Cherry Studio)
|
||||||
|
*/
|
||||||
|
const DEFAULT_TOOL_USE_EXAMPLES = `
|
||||||
|
Here are a few examples using notional tools:
|
||||||
|
---
|
||||||
|
User: Generate an image of the oldest person in this document.
|
||||||
|
|
||||||
|
A: I can use the document_qa tool to find out who the oldest person is in the document.
|
||||||
|
<tool_use>
|
||||||
|
<name>document_qa</name>
|
||||||
|
<arguments>{"document": "document.pdf", "question": "Who is the oldest person mentioned?"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
User: <tool_use_result>
|
||||||
|
<name>document_qa</name>
|
||||||
|
<result>John Doe, a 55 year old lumberjack living in Newfoundland.</result>
|
||||||
|
</tool_use_result>
|
||||||
|
|
||||||
|
A: I can use the image_generator tool to create a portrait of John Doe.
|
||||||
|
<tool_use>
|
||||||
|
<name>image_generator</name>
|
||||||
|
<arguments>{"prompt": "A portrait of John Doe, a 55-year-old man living in Canada."}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
User: <tool_use_result>
|
||||||
|
<name>image_generator</name>
|
||||||
|
<result>image.png</result>
|
||||||
|
</tool_use_result>
|
||||||
|
|
||||||
|
A: the image is generated as image.png
|
||||||
|
|
||||||
|
---
|
||||||
|
User: "What is the result of the following operation: 5 + 3 + 1294.678?"
|
||||||
|
|
||||||
|
A: I can use the python_interpreter tool to calculate the result of the operation.
|
||||||
|
<tool_use>
|
||||||
|
<name>python_interpreter</name>
|
||||||
|
<arguments>{"code": "5 + 3 + 1294.678"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
User: <tool_use_result>
|
||||||
|
<name>python_interpreter</name>
|
||||||
|
<result>1302.678</result>
|
||||||
|
</tool_use_result>
|
||||||
|
|
||||||
|
A: The result of the operation is 1302.678.
|
||||||
|
|
||||||
|
---
|
||||||
|
User: "Which city has the highest population , Guangzhou or Shanghai?"
|
||||||
|
|
||||||
|
A: I can use the search tool to find the population of Guangzhou.
|
||||||
|
<tool_use>
|
||||||
|
<name>search</name>
|
||||||
|
<arguments>{"query": "Population Guangzhou"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
User: <tool_use_result>
|
||||||
|
<name>search</name>
|
||||||
|
<result>Guangzhou has a population of 15 million inhabitants as of 2021.</result>
|
||||||
|
</tool_use_result>
|
||||||
|
|
||||||
|
A: I can use the search tool to find the population of Shanghai.
|
||||||
|
<tool_use>
|
||||||
|
<name>search</name>
|
||||||
|
<arguments>{"query": "Population Shanghai"}</arguments>
|
||||||
|
</tool_use>
|
||||||
|
|
||||||
|
User: <tool_use_result>
|
||||||
|
<name>search</name>
|
||||||
|
<result>26 million (2019)</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.`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建可用工具部分(提取自 Cherry Studio)
|
||||||
|
*/
|
||||||
|
function buildAvailableTools(tools: ToolSet): string | null {
|
||||||
|
const availableTools = Object.keys(tools)
|
||||||
|
if (availableTools.length === 0) return null
|
||||||
|
const result = availableTools
|
||||||
|
.map((toolName: string) => {
|
||||||
|
const tool = tools[toolName]
|
||||||
|
return `
|
||||||
|
<tool>
|
||||||
|
<name>${toolName}</name>
|
||||||
|
<description>${tool.description || ''}</description>
|
||||||
|
<arguments>
|
||||||
|
${tool.inputSchema ? JSON.stringify(tool.inputSchema) : ''}
|
||||||
|
</arguments>
|
||||||
|
</tool>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
return `<tools>
|
||||||
|
${result}
|
||||||
|
</tools>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认的系统提示符构建函数(提取自 Cherry Studio)
|
||||||
|
*/
|
||||||
|
function defaultBuildSystemPrompt(userSystemPrompt: string, tools: ToolSet): string {
|
||||||
|
const availableTools = buildAvailableTools(tools)
|
||||||
|
if (availableTools === null) return userSystemPrompt
|
||||||
|
|
||||||
|
const fullPrompt = DEFAULT_SYSTEM_PROMPT.replace('{{ TOOL_USE_EXAMPLES }}', DEFAULT_TOOL_USE_EXAMPLES)
|
||||||
|
.replace('{{ AVAILABLE_TOOLS }}', availableTools)
|
||||||
|
.replace('{{ USER_SYSTEM_PROMPT }}', userSystemPrompt || '')
|
||||||
|
|
||||||
|
return fullPrompt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认工具解析函数(提取自 Cherry Studio)
|
||||||
|
* 解析 XML 格式的工具调用
|
||||||
|
*/
|
||||||
|
function defaultParseToolUse(content: string, tools: ToolSet): { results: ToolUseResult[]; content: string } {
|
||||||
|
if (!content || !tools || Object.keys(tools).length === 0) {
|
||||||
|
return { results: [], content: content }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持两种格式:
|
||||||
|
// 1. 完整的 <tool_use></tool_use> 标签包围的内容
|
||||||
|
// 2. 只有内部内容(从 TagExtractor 提取出来的)
|
||||||
|
|
||||||
|
let contentToProcess = content
|
||||||
|
// 如果内容不包含 <tool_use> 标签,说明是从 TagExtractor 提取的内部内容,需要包装
|
||||||
|
if (!content.includes('<tool_use>')) {
|
||||||
|
contentToProcess = `<tool_use>\n${content}\n</tool_use>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolUsePattern =
|
||||||
|
/<tool_use>([\s\S]*?)<name>([\s\S]*?)<\/name>([\s\S]*?)<arguments>([\s\S]*?)<\/arguments>([\s\S]*?)<\/tool_use>/g
|
||||||
|
const results: ToolUseResult[] = []
|
||||||
|
let match
|
||||||
|
let idx = 0
|
||||||
|
|
||||||
|
// Find all tool use blocks
|
||||||
|
while ((match = toolUsePattern.exec(contentToProcess)) !== null) {
|
||||||
|
const fullMatch = match[0]
|
||||||
|
const toolName = match[2].trim()
|
||||||
|
const toolArgs = match[4].trim()
|
||||||
|
|
||||||
|
// Try to parse the arguments as JSON
|
||||||
|
let parsedArgs
|
||||||
|
try {
|
||||||
|
parsedArgs = JSON.parse(toolArgs)
|
||||||
|
} catch (error) {
|
||||||
|
// If parsing fails, use the string as is
|
||||||
|
parsedArgs = toolArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the corresponding tool
|
||||||
|
const tool = tools[toolName]
|
||||||
|
if (!tool) {
|
||||||
|
console.warn(`Tool "${toolName}" not found in available tools`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to results array
|
||||||
|
results.push({
|
||||||
|
id: `${toolName}-${idx++}`, // Unique ID for each tool use
|
||||||
|
toolName: toolName,
|
||||||
|
arguments: parsedArgs,
|
||||||
|
status: 'pending'
|
||||||
|
})
|
||||||
|
contentToProcess = contentToProcess.replace(fullMatch, '')
|
||||||
|
}
|
||||||
|
return { results, content: contentToProcess }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
|
||||||
|
const { enabled = true, buildSystemPrompt = defaultBuildSystemPrompt, parseToolUse = defaultParseToolUse } = config
|
||||||
|
|
||||||
|
return definePlugin({
|
||||||
|
name: 'built-in:prompt-tool-use',
|
||||||
|
transformParams: (params: any, context: AiRequestContext) => {
|
||||||
|
if (!enabled || !params.tools || typeof params.tools !== 'object') {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mcpTools = params.tools
|
||||||
|
|
||||||
|
// 构建系统提示符
|
||||||
|
const userSystemPrompt = typeof params.system === 'string' ? params.system : ''
|
||||||
|
const systemPrompt = buildSystemPrompt(userSystemPrompt, params.tools)
|
||||||
|
let systemMessage: string | null = systemPrompt
|
||||||
|
if (config.createSystemMessage) {
|
||||||
|
// 🎯 如果用户提供了自定义处理函数,使用它
|
||||||
|
systemMessage = config.createSystemMessage(systemPrompt, params, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 tools,改为 prompt 模式
|
||||||
|
const transformedParams = {
|
||||||
|
...params,
|
||||||
|
...(systemMessage ? { system: systemMessage } : {}),
|
||||||
|
tools: undefined
|
||||||
|
}
|
||||||
|
context.originalParams = transformedParams
|
||||||
|
return transformedParams
|
||||||
|
},
|
||||||
|
transformStream: (_: any, context: AiRequestContext) => () => {
|
||||||
|
let textBuffer = ''
|
||||||
|
// let stepId = ''
|
||||||
|
|
||||||
|
if (!context.mcpTools) {
|
||||||
|
throw new Error('No tools available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 context 中获取或初始化 usage 累加器
|
||||||
|
if (!context.accumulatedUsage) {
|
||||||
|
context.accumulatedUsage = {
|
||||||
|
inputTokens: 0,
|
||||||
|
outputTokens: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
cachedInputTokens: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建工具执行器、流事件管理器和标签提取器
|
||||||
|
const toolExecutor = new ToolExecutor()
|
||||||
|
const streamEventManager = new StreamEventManager()
|
||||||
|
const tagExtractor = new TagExtractor(TOOL_USE_TAG_CONFIG)
|
||||||
|
|
||||||
|
// 在context中初始化工具执行状态,避免递归调用时状态丢失
|
||||||
|
if (!context.hasExecutedToolsInCurrentStep) {
|
||||||
|
context.hasExecutedToolsInCurrentStep = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于hold text-start事件,直到确认有非工具标签内容
|
||||||
|
let pendingTextStart: TextStreamPart<TOOLS> | null = null
|
||||||
|
let hasStartedText = false
|
||||||
|
|
||||||
|
type TOOLS = NonNullable<typeof context.mcpTools>
|
||||||
|
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
|
||||||
|
async transform(
|
||||||
|
chunk: TextStreamPart<TOOLS>,
|
||||||
|
controller: TransformStreamDefaultController<TextStreamPart<TOOLS>>
|
||||||
|
) {
|
||||||
|
// Hold住text-start事件,直到确认有非工具标签内容
|
||||||
|
if ((chunk as any).type === 'text-start') {
|
||||||
|
pendingTextStart = chunk
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// text-delta阶段:收集文本内容并过滤工具标签
|
||||||
|
if (chunk.type === 'text-delta') {
|
||||||
|
textBuffer += chunk.text || ''
|
||||||
|
// stepId = chunk.id || ''
|
||||||
|
|
||||||
|
// 使用TagExtractor过滤工具标签,只传递非标签内容到UI层
|
||||||
|
const extractionResults = tagExtractor.processText(chunk.text || '')
|
||||||
|
|
||||||
|
for (const result of extractionResults) {
|
||||||
|
// 只传递非标签内容到UI层
|
||||||
|
if (!result.isTagContent && result.content) {
|
||||||
|
// 如果还没有发送text-start且有pending的text-start,先发送它
|
||||||
|
if (!hasStartedText && pendingTextStart) {
|
||||||
|
controller.enqueue(pendingTextStart)
|
||||||
|
hasStartedText = true
|
||||||
|
pendingTextStart = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredChunk = {
|
||||||
|
...chunk,
|
||||||
|
text: result.content
|
||||||
|
}
|
||||||
|
controller.enqueue(filteredChunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type === 'text-end') {
|
||||||
|
// 只有当已经发送了text-start时才发送text-end
|
||||||
|
if (hasStartedText) {
|
||||||
|
controller.enqueue(chunk)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.type === 'finish-step') {
|
||||||
|
// 统一在finish-step阶段检查并执行工具调用
|
||||||
|
const tools = context.mcpTools
|
||||||
|
if (tools && Object.keys(tools).length > 0 && !context.hasExecutedToolsInCurrentStep) {
|
||||||
|
// 解析完整的textBuffer来检测工具调用
|
||||||
|
const { results: parsedTools } = parseToolUse(textBuffer, tools)
|
||||||
|
const validToolUses = parsedTools.filter((t) => t.status === 'pending')
|
||||||
|
|
||||||
|
if (validToolUses.length > 0) {
|
||||||
|
context.hasExecutedToolsInCurrentStep = true
|
||||||
|
|
||||||
|
// 执行工具调用(不需要手动发送 start-step,外部流已经处理)
|
||||||
|
const executedResults = await toolExecutor.executeTools(validToolUses, tools, controller)
|
||||||
|
|
||||||
|
// 发送步骤完成事件,使用 tool-calls 作为 finishReason
|
||||||
|
streamEventManager.sendStepFinishEvent(controller, chunk, context, 'tool-calls')
|
||||||
|
|
||||||
|
// 处理递归调用
|
||||||
|
const toolResultsText = toolExecutor.formatToolResults(executedResults)
|
||||||
|
const recursiveParams = streamEventManager.buildRecursiveParams(
|
||||||
|
context,
|
||||||
|
textBuffer,
|
||||||
|
toolResultsText,
|
||||||
|
tools
|
||||||
|
)
|
||||||
|
|
||||||
|
await streamEventManager.handleRecursiveCall(controller, recursiveParams, context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有执行工具调用,直接传递原始finish-step事件
|
||||||
|
controller.enqueue(chunk)
|
||||||
|
|
||||||
|
// 清理状态
|
||||||
|
textBuffer = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 finish 类型,使用累加后的 totalUsage
|
||||||
|
if (chunk.type === 'finish') {
|
||||||
|
controller.enqueue({
|
||||||
|
...chunk,
|
||||||
|
totalUsage: context.accumulatedUsage
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于其他类型的事件,直接传递(不包括text-start,已在上面处理)
|
||||||
|
if ((chunk as any).type !== 'text-start') {
|
||||||
|
controller.enqueue(chunk)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
// 清理pending状态
|
||||||
|
pendingTextStart = null
|
||||||
|
hasStartedText = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// Copied from https://github.com/vercel/ai/blob/main/packages/ai/core/util/get-potential-start-index.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the start of the searchedText in the text, or null if it
|
||||||
|
* is not found.
|
||||||
|
*/
|
||||||
|
export function getPotentialStartIndex(text: string, searchedText: string): number | null {
|
||||||
|
// Return null immediately if searchedText is empty.
|
||||||
|
if (searchedText.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the searchedText exists as a direct substring of text.
|
||||||
|
const directIndex = text.indexOf(searchedText)
|
||||||
|
if (directIndex !== -1) {
|
||||||
|
return directIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, look for the largest suffix of "text" that matches
|
||||||
|
// a prefix of "searchedText". We go from the end of text inward.
|
||||||
|
for (let i = text.length - 1; i >= 0; i--) {
|
||||||
|
const suffix = text.substring(i)
|
||||||
|
if (searchedText.startsWith(suffix)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagConfig {
|
||||||
|
openingTag: string
|
||||||
|
closingTag: string
|
||||||
|
separator?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagExtractionState {
|
||||||
|
textBuffer: string
|
||||||
|
isInsideTag: boolean
|
||||||
|
isFirstTag: boolean
|
||||||
|
isFirstText: boolean
|
||||||
|
afterSwitch: boolean
|
||||||
|
accumulatedTagContent: string
|
||||||
|
hasTagContent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagExtractionResult {
|
||||||
|
content: string
|
||||||
|
isTagContent: boolean
|
||||||
|
complete: boolean
|
||||||
|
tagContentExtracted?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用标签提取处理器
|
||||||
|
* 可以处理各种形式的标签对,如 <think>...</think>, <tool_use>...</tool_use> 等
|
||||||
|
*/
|
||||||
|
export class TagExtractor {
|
||||||
|
private config: TagConfig
|
||||||
|
private state: TagExtractionState
|
||||||
|
|
||||||
|
constructor(config: TagConfig) {
|
||||||
|
this.config = config
|
||||||
|
this.state = {
|
||||||
|
textBuffer: '',
|
||||||
|
isInsideTag: false,
|
||||||
|
isFirstTag: true,
|
||||||
|
isFirstText: true,
|
||||||
|
afterSwitch: false,
|
||||||
|
accumulatedTagContent: '',
|
||||||
|
hasTagContent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理文本块,返回处理结果
|
||||||
|
*/
|
||||||
|
processText(newText: string): TagExtractionResult[] {
|
||||||
|
this.state.textBuffer += newText
|
||||||
|
const results: TagExtractionResult[] = []
|
||||||
|
|
||||||
|
// 处理标签提取逻辑
|
||||||
|
while (true) {
|
||||||
|
const nextTag = this.state.isInsideTag ? this.config.closingTag : this.config.openingTag
|
||||||
|
const startIndex = getPotentialStartIndex(this.state.textBuffer, nextTag)
|
||||||
|
|
||||||
|
if (startIndex == null) {
|
||||||
|
const content = this.state.textBuffer
|
||||||
|
if (content.length > 0) {
|
||||||
|
results.push({
|
||||||
|
content: this.addPrefix(content),
|
||||||
|
isTagContent: this.state.isInsideTag,
|
||||||
|
complete: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.state.isInsideTag) {
|
||||||
|
this.state.accumulatedTagContent += this.addPrefix(content)
|
||||||
|
this.state.hasTagContent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.state.textBuffer = ''
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签前的内容
|
||||||
|
const contentBeforeTag = this.state.textBuffer.slice(0, startIndex)
|
||||||
|
if (contentBeforeTag.length > 0) {
|
||||||
|
results.push({
|
||||||
|
content: this.addPrefix(contentBeforeTag),
|
||||||
|
isTagContent: this.state.isInsideTag,
|
||||||
|
complete: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.state.isInsideTag) {
|
||||||
|
this.state.accumulatedTagContent += this.addPrefix(contentBeforeTag)
|
||||||
|
this.state.hasTagContent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundFullMatch = startIndex + nextTag.length <= this.state.textBuffer.length
|
||||||
|
|
||||||
|
if (foundFullMatch) {
|
||||||
|
// 如果找到完整的标签
|
||||||
|
this.state.textBuffer = this.state.textBuffer.slice(startIndex + nextTag.length)
|
||||||
|
|
||||||
|
// 如果刚刚结束一个标签内容,生成完整的标签内容结果
|
||||||
|
if (this.state.isInsideTag && this.state.hasTagContent) {
|
||||||
|
results.push({
|
||||||
|
content: '',
|
||||||
|
isTagContent: false,
|
||||||
|
complete: true,
|
||||||
|
tagContentExtracted: this.state.accumulatedTagContent
|
||||||
|
})
|
||||||
|
this.state.accumulatedTagContent = ''
|
||||||
|
this.state.hasTagContent = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.isInsideTag = !this.state.isInsideTag
|
||||||
|
this.state.afterSwitch = true
|
||||||
|
|
||||||
|
if (this.state.isInsideTag) {
|
||||||
|
this.state.isFirstTag = false
|
||||||
|
} else {
|
||||||
|
this.state.isFirstText = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state.textBuffer = this.state.textBuffer.slice(startIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成处理,返回任何剩余的标签内容
|
||||||
|
*/
|
||||||
|
finalize(): TagExtractionResult | null {
|
||||||
|
if (this.state.hasTagContent && this.state.accumulatedTagContent) {
|
||||||
|
const result = {
|
||||||
|
content: '',
|
||||||
|
isTagContent: false,
|
||||||
|
complete: true,
|
||||||
|
tagContentExtracted: this.state.accumulatedTagContent
|
||||||
|
}
|
||||||
|
this.state.accumulatedTagContent = ''
|
||||||
|
this.state.hasTagContent = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private addPrefix(text: string): string {
|
||||||
|
const needsPrefix =
|
||||||
|
this.state.afterSwitch && (this.state.isInsideTag ? !this.state.isFirstTag : !this.state.isFirstText)
|
||||||
|
|
||||||
|
const prefix = needsPrefix && this.config.separator ? this.config.separator : ''
|
||||||
|
this.state.afterSwitch = false
|
||||||
|
return prefix + text
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置状态
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.state = {
|
||||||
|
textBuffer: '',
|
||||||
|
isInsideTag: false,
|
||||||
|
isFirstTag: true,
|
||||||
|
isFirstText: true,
|
||||||
|
afterSwitch: false,
|
||||||
|
accumulatedTagContent: '',
|
||||||
|
hasTagContent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ToolSet } from 'ai'
|
||||||
|
|
||||||
|
import { AiRequestContext } from '../..'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析结果类型
|
||||||
|
* 表示从AI响应中解析出的工具使用意图
|
||||||
|
*/
|
||||||
|
export interface ToolUseResult {
|
||||||
|
id: string
|
||||||
|
toolName: string
|
||||||
|
arguments: any
|
||||||
|
status: 'pending' | 'invoking' | 'done' | 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseToolUsePluginConfig {
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptToolUseConfig extends BaseToolUsePluginConfig {
|
||||||
|
// 自定义系统提示符构建函数(可选,有默认实现)
|
||||||
|
buildSystemPrompt?: (userSystemPrompt: string, tools: ToolSet) => string
|
||||||
|
// 自定义工具解析函数(可选,有默认实现)
|
||||||
|
parseToolUse?: (content: string, tools: ToolSet) => { results: ToolUseResult[]; content: string }
|
||||||
|
createSystemMessage?: (systemPrompt: string, originalParams: any, context: AiRequestContext) => string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展的 AI 请求上下文,支持 MCP 工具存储
|
||||||
|
*/
|
||||||
|
export interface ToolUseRequestContext extends AiRequestContext {
|
||||||
|
mcpTools: ToolSet
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { anthropic } from '@ai-sdk/anthropic'
|
||||||
|
import { google } from '@ai-sdk/google'
|
||||||
|
import { openai } from '@ai-sdk/openai'
|
||||||
|
|
||||||
|
import { ProviderOptionsMap } from '../../../options/types'
|
||||||
|
import { OpenRouterSearchConfig } from './openrouter'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。
|
||||||
|
*/
|
||||||
|
export type OpenAISearchConfig = NonNullable<Parameters<typeof openai.tools.webSearch>[0]>
|
||||||
|
export type OpenAISearchPreviewConfig = NonNullable<Parameters<typeof openai.tools.webSearchPreview>[0]>
|
||||||
|
export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tools.webSearch_20250305>[0]>
|
||||||
|
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
|
||||||
|
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件初始化时接收的完整配置对象
|
||||||
|
*
|
||||||
|
* 其结构与 ProviderOptions 保持一致,方便上游统一管理配置
|
||||||
|
*/
|
||||||
|
export interface WebSearchPluginConfig {
|
||||||
|
openai?: OpenAISearchConfig
|
||||||
|
'openai-chat'?: OpenAISearchPreviewConfig
|
||||||
|
anthropic?: AnthropicSearchConfig
|
||||||
|
xai?: ProviderOptionsMap['xai']['searchParameters']
|
||||||
|
google?: GoogleSearchConfig
|
||||||
|
'google-vertex'?: GoogleSearchConfig
|
||||||
|
openrouter?: OpenRouterSearchConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件的默认配置
|
||||||
|
*/
|
||||||
|
export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
|
||||||
|
google: {},
|
||||||
|
'google-vertex': {},
|
||||||
|
openai: {},
|
||||||
|
'openai-chat': {},
|
||||||
|
xai: {
|
||||||
|
mode: 'on',
|
||||||
|
returnCitations: true,
|
||||||
|
maxSearchResults: 5,
|
||||||
|
sources: [{ type: 'web' }, { type: 'x' }, { type: 'news' }]
|
||||||
|
},
|
||||||
|
anthropic: {
|
||||||
|
maxUses: 5
|
||||||
|
},
|
||||||
|
openrouter: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
id: 'web',
|
||||||
|
max_results: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebSearchToolOutputSchema = {
|
||||||
|
// Anthropic 工具 - 手动定义
|
||||||
|
anthropicWebSearch: Array<{
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
pageAge: string | null
|
||||||
|
encryptedContent: string
|
||||||
|
type: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
// OpenAI 工具 - 基于实际输出
|
||||||
|
openaiWebSearch: {
|
||||||
|
status: 'completed' | 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google 工具
|
||||||
|
googleSearch: {
|
||||||
|
webSearchQueries?: string[]
|
||||||
|
groundingChunks?: Array<{
|
||||||
|
web?: { uri: string; title: string }
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Web Search Plugin
|
||||||
|
* 提供统一的网络搜索能力,支持多个 AI Provider
|
||||||
|
*/
|
||||||
|
import { anthropic } from '@ai-sdk/anthropic'
|
||||||
|
import { google } from '@ai-sdk/google'
|
||||||
|
import { openai } from '@ai-sdk/openai'
|
||||||
|
|
||||||
|
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
|
||||||
|
import { definePlugin } from '../../'
|
||||||
|
import type { AiRequestContext } from '../../types'
|
||||||
|
import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络搜索插件
|
||||||
|
*
|
||||||
|
* @param config - 在插件初始化时传入的静态配置
|
||||||
|
*/
|
||||||
|
export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEARCH_CONFIG) =>
|
||||||
|
definePlugin({
|
||||||
|
name: 'webSearch',
|
||||||
|
enforce: 'pre',
|
||||||
|
|
||||||
|
transformParams: async (params: any, context: AiRequestContext) => {
|
||||||
|
const { providerId } = context
|
||||||
|
switch (providerId) {
|
||||||
|
case 'openai': {
|
||||||
|
if (config.openai) {
|
||||||
|
if (!params.tools) params.tools = {}
|
||||||
|
params.tools.web_search = openai.tools.webSearch(config.openai)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'openai-chat': {
|
||||||
|
if (config['openai-chat']) {
|
||||||
|
if (!params.tools) params.tools = {}
|
||||||
|
params.tools.web_search_preview = openai.tools.webSearchPreview(config['openai-chat'])
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'anthropic': {
|
||||||
|
if (config.anthropic) {
|
||||||
|
if (!params.tools) params.tools = {}
|
||||||
|
params.tools.web_search = anthropic.tools.webSearch_20250305(config.anthropic)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'google': {
|
||||||
|
// case 'google-vertex':
|
||||||
|
if (!params.tools) params.tools = {}
|
||||||
|
params.tools.web_search = google.tools.googleSearch(config.google || {})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'xai': {
|
||||||
|
if (config.xai) {
|
||||||
|
const searchOptions = createXaiOptions({
|
||||||
|
searchParameters: { ...config.xai, mode: 'on' }
|
||||||
|
})
|
||||||
|
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'openrouter': {
|
||||||
|
if (config.openrouter) {
|
||||||
|
const searchOptions = createOpenRouterOptions(config.openrouter)
|
||||||
|
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导出类型定义供开发者使用
|
||||||
|
export type { WebSearchPluginConfig, WebSearchToolOutputSchema } from './helper'
|
||||||
|
|
||||||
|
// 默认导出
|
||||||
|
export default webSearchPlugin
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export type OpenRouterSearchConfig = {
|
||||||
|
plugins?: Array<{
|
||||||
|
id: 'web'
|
||||||
|
/**
|
||||||
|
* Maximum number of search results to include (default: 5)
|
||||||
|
*/
|
||||||
|
max_results?: number
|
||||||
|
/**
|
||||||
|
* Custom search prompt to guide the search query
|
||||||
|
*/
|
||||||
|
search_prompt?: string
|
||||||
|
}>
|
||||||
|
/**
|
||||||
|
* Built-in web search options for models that support native web search
|
||||||
|
*/
|
||||||
|
web_search_options?: {
|
||||||
|
/**
|
||||||
|
* Maximum number of search results to include
|
||||||
|
*/
|
||||||
|
max_results?: number
|
||||||
|
/**
|
||||||
|
* Custom search prompt to guide the search query
|
||||||
|
*/
|
||||||
|
search_prompt?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 核心类型和接口
|
||||||
|
export type { AiPlugin, AiRequestContext, HookResult, PluginManagerConfig } from './types'
|
||||||
|
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import type { LanguageModel } from 'ai'
|
||||||
|
|
||||||
|
import type { ProviderId } from '../providers'
|
||||||
|
import type { AiPlugin, AiRequestContext } from './types'
|
||||||
|
|
||||||
|
// 插件管理器
|
||||||
|
export { PluginManager } from './manager'
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export function createContext<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
model: LanguageModel | ImageModelV2,
|
||||||
|
originalParams: any
|
||||||
|
): AiRequestContext {
|
||||||
|
return {
|
||||||
|
providerId,
|
||||||
|
model,
|
||||||
|
originalParams,
|
||||||
|
metadata: {},
|
||||||
|
startTime: Date.now(),
|
||||||
|
requestId: `${providerId}-${typeof model === 'string' ? model : model?.modelId}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
// 占位
|
||||||
|
recursiveCall: () => Promise.resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插件构建器 - 便于创建插件
|
||||||
|
export function definePlugin(plugin: AiPlugin): AiPlugin
|
||||||
|
export function definePlugin<T extends (...args: any[]) => AiPlugin>(pluginFactory: T): T
|
||||||
|
export function definePlugin(plugin: AiPlugin | ((...args: any[]) => AiPlugin)) {
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { AiPlugin, AiRequestContext } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理器
|
||||||
|
*/
|
||||||
|
export class PluginManager {
|
||||||
|
private plugins: AiPlugin[] = []
|
||||||
|
|
||||||
|
constructor(plugins: AiPlugin[] = []) {
|
||||||
|
this.plugins = this.sortPlugins(plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加插件
|
||||||
|
*/
|
||||||
|
use(plugin: AiPlugin): this {
|
||||||
|
this.plugins = this.sortPlugins([...this.plugins, plugin])
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除插件
|
||||||
|
*/
|
||||||
|
remove(pluginName: string): this {
|
||||||
|
this.plugins = this.plugins.filter((p) => p.name !== pluginName)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件排序:pre -> normal -> post
|
||||||
|
*/
|
||||||
|
private sortPlugins(plugins: AiPlugin[]): AiPlugin[] {
|
||||||
|
const pre: AiPlugin[] = []
|
||||||
|
const normal: AiPlugin[] = []
|
||||||
|
const post: AiPlugin[] = []
|
||||||
|
|
||||||
|
plugins.forEach((plugin) => {
|
||||||
|
if (plugin.enforce === 'pre') {
|
||||||
|
pre.push(plugin)
|
||||||
|
} else if (plugin.enforce === 'post') {
|
||||||
|
post.push(plugin)
|
||||||
|
} else {
|
||||||
|
normal.push(plugin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...pre, ...normal, ...post]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 First 钩子 - 返回第一个有效结果
|
||||||
|
*/
|
||||||
|
async executeFirst<T>(
|
||||||
|
hookName: 'resolveModel' | 'loadTemplate',
|
||||||
|
arg: any,
|
||||||
|
context: AiRequestContext
|
||||||
|
): Promise<T | null> {
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
const hook = plugin[hookName]
|
||||||
|
if (hook) {
|
||||||
|
const result = await hook(arg, context)
|
||||||
|
if (result !== null && result !== undefined) {
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 Sequential 钩子 - 链式数据转换
|
||||||
|
*/
|
||||||
|
async executeSequential<T>(
|
||||||
|
hookName: 'transformParams' | 'transformResult',
|
||||||
|
initialValue: T,
|
||||||
|
context: AiRequestContext
|
||||||
|
): Promise<T> {
|
||||||
|
let result = initialValue
|
||||||
|
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
const hook = plugin[hookName]
|
||||||
|
if (hook) {
|
||||||
|
result = await hook<T>(result, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 ConfigureContext 钩子 - 串行配置上下文
|
||||||
|
*/
|
||||||
|
async executeConfigureContext(context: AiRequestContext): Promise<void> {
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
const hook = plugin.configureContext
|
||||||
|
if (hook) {
|
||||||
|
await hook(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 Parallel 钩子 - 并行副作用
|
||||||
|
*/
|
||||||
|
async executeParallel(
|
||||||
|
hookName: 'onRequestStart' | 'onRequestEnd' | 'onError',
|
||||||
|
context: AiRequestContext,
|
||||||
|
result?: any,
|
||||||
|
error?: Error
|
||||||
|
): Promise<void> {
|
||||||
|
const promises = this.plugins
|
||||||
|
.map((plugin) => {
|
||||||
|
const hook = plugin[hookName]
|
||||||
|
if (!hook) return null
|
||||||
|
|
||||||
|
if (hookName === 'onError' && error) {
|
||||||
|
return (hook as any)(error, context)
|
||||||
|
} else if (hookName === 'onRequestEnd' && result !== undefined) {
|
||||||
|
return (hook as any)(context, result)
|
||||||
|
} else if (hookName === 'onRequestStart') {
|
||||||
|
return (hook as any)(context)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
// 使用 Promise.all 而不是 allSettled,让插件错误能够抛出
|
||||||
|
await Promise.all(promises)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集所有流转换器(返回数组,AI SDK 原生支持)
|
||||||
|
*/
|
||||||
|
collectStreamTransforms(params: any, context: AiRequestContext) {
|
||||||
|
return this.plugins
|
||||||
|
.filter((plugin) => plugin.transformStream)
|
||||||
|
.map((plugin) => plugin.transformStream?.(params, context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有插件信息
|
||||||
|
*/
|
||||||
|
getPlugins(): AiPlugin[] {
|
||||||
|
return [...this.plugins]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件统计信息
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.plugins.length,
|
||||||
|
pre: 0,
|
||||||
|
normal: 0,
|
||||||
|
post: 0,
|
||||||
|
hooks: {
|
||||||
|
resolveModel: 0,
|
||||||
|
loadTemplate: 0,
|
||||||
|
transformParams: 0,
|
||||||
|
transformResult: 0,
|
||||||
|
onRequestStart: 0,
|
||||||
|
onRequestEnd: 0,
|
||||||
|
onError: 0,
|
||||||
|
transformStream: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plugins.forEach((plugin) => {
|
||||||
|
// 统计 enforce 类型
|
||||||
|
if (plugin.enforce === 'pre') stats.pre++
|
||||||
|
else if (plugin.enforce === 'post') stats.post++
|
||||||
|
else stats.normal++
|
||||||
|
|
||||||
|
// 统计钩子数量
|
||||||
|
Object.keys(stats.hooks).forEach((hookName) => {
|
||||||
|
if (plugin[hookName as keyof AiPlugin]) {
|
||||||
|
stats.hooks[hookName as keyof typeof stats.hooks]++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import type { LanguageModel, TextStreamPart, ToolSet } from 'ai'
|
||||||
|
|
||||||
|
import { type ProviderId } from '../providers/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归调用函数类型
|
||||||
|
* 使用 any 是因为递归调用时参数和返回类型可能完全不同
|
||||||
|
*/
|
||||||
|
export type RecursiveCallFn = (newParams: any) => Promise<any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 请求上下文
|
||||||
|
*/
|
||||||
|
export interface AiRequestContext {
|
||||||
|
providerId: ProviderId
|
||||||
|
model: LanguageModel | ImageModelV2
|
||||||
|
originalParams: any
|
||||||
|
metadata: Record<string, any>
|
||||||
|
startTime: number
|
||||||
|
requestId: string
|
||||||
|
recursiveCall: RecursiveCallFn
|
||||||
|
isRecursiveCall?: boolean
|
||||||
|
mcpTools?: ToolSet
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钩子分类
|
||||||
|
*/
|
||||||
|
export interface AiPlugin {
|
||||||
|
name: string
|
||||||
|
enforce?: 'pre' | 'post'
|
||||||
|
|
||||||
|
// 【First】首个钩子 - 只执行第一个返回值的插件
|
||||||
|
resolveModel?: (
|
||||||
|
modelId: string,
|
||||||
|
context: AiRequestContext
|
||||||
|
) => Promise<LanguageModel | ImageModelV2 | null> | LanguageModel | ImageModelV2 | null
|
||||||
|
loadTemplate?: (templateName: string, context: AiRequestContext) => any | null | Promise<any | null>
|
||||||
|
|
||||||
|
// 【Sequential】串行钩子 - 链式执行,支持数据转换
|
||||||
|
configureContext?: (context: AiRequestContext) => void | Promise<void>
|
||||||
|
transformParams?: <T>(params: T, context: AiRequestContext) => T | Promise<T>
|
||||||
|
transformResult?: <T>(result: T, context: AiRequestContext) => T | Promise<T>
|
||||||
|
|
||||||
|
// 【Parallel】并行钩子 - 不依赖顺序,用于副作用
|
||||||
|
onRequestStart?: (context: AiRequestContext) => void | Promise<void>
|
||||||
|
onRequestEnd?: (context: AiRequestContext, result: any) => void | Promise<void>
|
||||||
|
onError?: (error: Error, context: AiRequestContext) => void | Promise<void>
|
||||||
|
|
||||||
|
// 【Stream】流处理 - 直接使用 AI SDK
|
||||||
|
transformStream?: (
|
||||||
|
params: any,
|
||||||
|
context: AiRequestContext
|
||||||
|
) => <TOOLS extends ToolSet>(options?: {
|
||||||
|
tools: TOOLS
|
||||||
|
stopStream: () => void
|
||||||
|
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>
|
||||||
|
|
||||||
|
// AI SDK 原生中间件
|
||||||
|
// aiSdkMiddlewares?: LanguageModelV1Middleware[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理器配置
|
||||||
|
*/
|
||||||
|
export interface PluginManagerConfig {
|
||||||
|
plugins: AiPlugin[]
|
||||||
|
context: Partial<AiRequestContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钩子执行结果
|
||||||
|
*/
|
||||||
|
export interface HookResult<T = any> {
|
||||||
|
value: T
|
||||||
|
stop?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Hub Provider - 支持路由到多个底层provider
|
||||||
|
*
|
||||||
|
* 支持格式: hubId:providerId:modelId
|
||||||
|
* 例如: aihubmix:anthropic:claude-3.5-sonnet
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ProviderV2 } from '@ai-sdk/provider'
|
||||||
|
import { customProvider } from 'ai'
|
||||||
|
|
||||||
|
import { globalRegistryManagement } from './RegistryManagement'
|
||||||
|
import type { AiSdkMethodName, AiSdkModelReturn, AiSdkModelType } from './types'
|
||||||
|
|
||||||
|
export interface HubProviderConfig {
|
||||||
|
/** Hub的唯一标识符 */
|
||||||
|
hubId: string
|
||||||
|
/** 是否启用调试日志 */
|
||||||
|
debug?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HubProviderError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly hubId: string,
|
||||||
|
public readonly providerId?: string,
|
||||||
|
public readonly originalError?: Error
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'HubProviderError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析Hub模型ID
|
||||||
|
*/
|
||||||
|
function parseHubModelId(modelId: string): { provider: string; actualModelId: string } {
|
||||||
|
const parts = modelId.split(':')
|
||||||
|
if (parts.length !== 2) {
|
||||||
|
throw new HubProviderError(`Invalid hub model ID format. Expected "provider:modelId", got: ${modelId}`, 'unknown')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
provider: parts[0],
|
||||||
|
actualModelId: parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Hub Provider
|
||||||
|
*/
|
||||||
|
export function createHubProvider(config: HubProviderConfig): ProviderV2 {
|
||||||
|
const { hubId } = config
|
||||||
|
|
||||||
|
function getTargetProvider(providerId: string): ProviderV2 {
|
||||||
|
// 从全局注册表获取provider实例
|
||||||
|
try {
|
||||||
|
const provider = globalRegistryManagement.getProvider(providerId)
|
||||||
|
if (!provider) {
|
||||||
|
throw new HubProviderError(
|
||||||
|
`Provider "${providerId}" is not initialized. Please call initializeProvider("${providerId}", options) first.`,
|
||||||
|
hubId,
|
||||||
|
providerId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return provider
|
||||||
|
} catch (error) {
|
||||||
|
throw new HubProviderError(
|
||||||
|
`Failed to get provider "${providerId}": ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
hubId,
|
||||||
|
providerId,
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveModel<T extends AiSdkModelType>(
|
||||||
|
modelId: string,
|
||||||
|
modelType: T,
|
||||||
|
methodName: AiSdkMethodName<T>
|
||||||
|
): AiSdkModelReturn<T> {
|
||||||
|
const { provider, actualModelId } = parseHubModelId(modelId)
|
||||||
|
const targetProvider = getTargetProvider(provider)
|
||||||
|
|
||||||
|
const fn = targetProvider[methodName] as (id: string) => AiSdkModelReturn<T>
|
||||||
|
|
||||||
|
if (!fn) {
|
||||||
|
throw new HubProviderError(`Provider "${provider}" does not support ${modelType}`, hubId, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(actualModelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return customProvider({
|
||||||
|
fallbackProvider: {
|
||||||
|
languageModel: (modelId: string) => resolveModel(modelId, 'text', 'languageModel'),
|
||||||
|
textEmbeddingModel: (modelId: string) => resolveModel(modelId, 'embedding', 'textEmbeddingModel'),
|
||||||
|
imageModel: (modelId: string) => resolveModel(modelId, 'image', 'imageModel'),
|
||||||
|
transcriptionModel: (modelId: string) => resolveModel(modelId, 'transcription', 'transcriptionModel'),
|
||||||
|
speechModel: (modelId: string) => resolveModel(modelId, 'speech', 'speechModel')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Provider 注册表管理器
|
||||||
|
* 纯粹的管理功能:存储、检索已配置好的 provider 实例
|
||||||
|
* 基于 AI SDK 原生的 createProviderRegistry
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EmbeddingModelV2, ImageModelV2, LanguageModelV2, ProviderV2 } from '@ai-sdk/provider'
|
||||||
|
import { createProviderRegistry, type ProviderRegistryProvider } from 'ai'
|
||||||
|
|
||||||
|
type PROVIDERS = Record<string, ProviderV2>
|
||||||
|
|
||||||
|
export const DEFAULT_SEPARATOR = '|'
|
||||||
|
|
||||||
|
// export type MODEL_ID = `${string}${typeof DEFAULT_SEPARATOR}${string}`
|
||||||
|
|
||||||
|
export class RegistryManagement<SEPARATOR extends string = typeof DEFAULT_SEPARATOR> {
|
||||||
|
private providers: PROVIDERS = {}
|
||||||
|
private aliases: Set<string> = new Set() // 记录哪些key是别名
|
||||||
|
private separator: SEPARATOR
|
||||||
|
private registry: ProviderRegistryProvider<PROVIDERS, SEPARATOR> | null = null
|
||||||
|
|
||||||
|
constructor(options: { separator: SEPARATOR } = { separator: DEFAULT_SEPARATOR as SEPARATOR }) {
|
||||||
|
this.separator = options.separator
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册已配置好的 provider 实例
|
||||||
|
*/
|
||||||
|
registerProvider(id: string, provider: ProviderV2, aliases?: string[]): this {
|
||||||
|
// 注册主provider
|
||||||
|
this.providers[id] = provider
|
||||||
|
|
||||||
|
// 注册别名(都指向同一个provider实例)
|
||||||
|
if (aliases) {
|
||||||
|
aliases.forEach((alias) => {
|
||||||
|
this.providers[alias] = provider // 直接存储引用
|
||||||
|
this.aliases.add(alias) // 标记为别名
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rebuildRegistry()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已注册的provider实例
|
||||||
|
*/
|
||||||
|
getProvider(id: string): ProviderV2 | undefined {
|
||||||
|
return this.providers[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量注册 providers
|
||||||
|
*/
|
||||||
|
registerProviders(providers: Record<string, ProviderV2>): this {
|
||||||
|
Object.assign(this.providers, providers)
|
||||||
|
this.rebuildRegistry()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 provider(同时清理相关别名)
|
||||||
|
*/
|
||||||
|
unregisterProvider(id: string): this {
|
||||||
|
const provider = this.providers[id]
|
||||||
|
if (!provider) return this
|
||||||
|
|
||||||
|
// 如果移除的是真实ID,需要清理所有指向它的别名
|
||||||
|
if (!this.aliases.has(id)) {
|
||||||
|
// 找到所有指向此provider的别名并删除
|
||||||
|
const aliasesToRemove: string[] = []
|
||||||
|
this.aliases.forEach((alias) => {
|
||||||
|
if (this.providers[alias] === provider) {
|
||||||
|
aliasesToRemove.push(alias)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
aliasesToRemove.forEach((alias) => {
|
||||||
|
delete this.providers[alias]
|
||||||
|
this.aliases.delete(alias)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果移除的是别名,只删除别名记录
|
||||||
|
this.aliases.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.providers[id]
|
||||||
|
this.rebuildRegistry()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即重建 registry - 每次变更都重建
|
||||||
|
*/
|
||||||
|
private rebuildRegistry(): void {
|
||||||
|
if (Object.keys(this.providers).length === 0) {
|
||||||
|
this.registry = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registry = createProviderRegistry<PROVIDERS, SEPARATOR>(this.providers, {
|
||||||
|
separator: this.separator
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言模型 - AI SDK 原生方法
|
||||||
|
*/
|
||||||
|
languageModel(id: `${string}${SEPARATOR}${string}`): LanguageModelV2 {
|
||||||
|
if (!this.registry) {
|
||||||
|
throw new Error('No providers registered')
|
||||||
|
}
|
||||||
|
return this.registry.languageModel(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本嵌入模型 - AI SDK 原生方法
|
||||||
|
*/
|
||||||
|
textEmbeddingModel(id: `${string}${SEPARATOR}${string}`): EmbeddingModelV2<string> {
|
||||||
|
if (!this.registry) {
|
||||||
|
throw new Error('No providers registered')
|
||||||
|
}
|
||||||
|
return this.registry.textEmbeddingModel(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图像模型 - AI SDK 原生方法
|
||||||
|
*/
|
||||||
|
imageModel(id: `${string}${SEPARATOR}${string}`): ImageModelV2 {
|
||||||
|
if (!this.registry) {
|
||||||
|
throw new Error('No providers registered')
|
||||||
|
}
|
||||||
|
return this.registry.imageModel(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取转录模型 - AI SDK 原生方法
|
||||||
|
*/
|
||||||
|
transcriptionModel(id: `${string}${SEPARATOR}${string}`): any {
|
||||||
|
if (!this.registry) {
|
||||||
|
throw new Error('No providers registered')
|
||||||
|
}
|
||||||
|
return this.registry.transcriptionModel(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语音模型 - AI SDK 原生方法
|
||||||
|
*/
|
||||||
|
speechModel(id: `${string}${SEPARATOR}${string}`): any {
|
||||||
|
if (!this.registry) {
|
||||||
|
throw new Error('No providers registered')
|
||||||
|
}
|
||||||
|
return this.registry.speechModel(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已注册的 provider 列表
|
||||||
|
*/
|
||||||
|
getRegisteredProviders(): string[] {
|
||||||
|
return Object.keys(this.providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有已注册的 providers
|
||||||
|
*/
|
||||||
|
hasProviders(): boolean {
|
||||||
|
return Object.keys(this.providers).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有 providers
|
||||||
|
*/
|
||||||
|
clear(): this {
|
||||||
|
this.providers = {}
|
||||||
|
this.aliases.clear()
|
||||||
|
this.registry = null
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析真实的Provider ID(供getAiSdkProviderId使用)
|
||||||
|
* 如果传入的是别名,返回真实的Provider ID
|
||||||
|
* 如果传入的是真实ID,直接返回
|
||||||
|
*/
|
||||||
|
resolveProviderId(id: string): string {
|
||||||
|
if (!this.aliases.has(id)) return id // 不是别名,直接返回
|
||||||
|
|
||||||
|
// 是别名,找到真实ID
|
||||||
|
const targetProvider = this.providers[id]
|
||||||
|
for (const [realId, provider] of Object.entries(this.providers)) {
|
||||||
|
if (provider === targetProvider && !this.aliases.has(realId)) {
|
||||||
|
return realId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为别名
|
||||||
|
*/
|
||||||
|
isAlias(id: string): boolean {
|
||||||
|
return this.aliases.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有别名映射关系
|
||||||
|
*/
|
||||||
|
getAllAliases(): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
this.aliases.forEach((alias) => {
|
||||||
|
result[alias] = this.resolveProviderId(alias)
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局注册表管理器实例
|
||||||
|
* 使用 | 作为分隔符,因为 : 会和 :free 等suffix冲突
|
||||||
|
*/
|
||||||
|
export const globalRegistryManagement = new RegistryManagement()
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
/**
|
||||||
|
* 测试真正的 AiProviderRegistry 功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
// 模拟 AI SDK
|
||||||
|
vi.mock('@ai-sdk/openai', () => ({
|
||||||
|
createOpenAI: vi.fn(() => ({ name: 'openai-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/anthropic', () => ({
|
||||||
|
createAnthropic: vi.fn(() => ({ name: 'anthropic-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/azure', () => ({
|
||||||
|
createAzure: vi.fn(() => ({ name: 'azure-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/deepseek', () => ({
|
||||||
|
createDeepSeek: vi.fn(() => ({ name: 'deepseek-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/google', () => ({
|
||||||
|
createGoogleGenerativeAI: vi.fn(() => ({ name: 'google-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/openai-compatible', () => ({
|
||||||
|
createOpenAICompatible: vi.fn(() => ({ name: 'openai-compatible-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@ai-sdk/xai', () => ({
|
||||||
|
createXai: vi.fn(() => ({ name: 'xai-mock' }))
|
||||||
|
}))
|
||||||
|
|
||||||
|
import {
|
||||||
|
cleanup,
|
||||||
|
clearAllProviders,
|
||||||
|
createAndRegisterProvider,
|
||||||
|
createProvider,
|
||||||
|
getAllProviderConfigAliases,
|
||||||
|
getAllProviderConfigs,
|
||||||
|
getInitializedProviders,
|
||||||
|
getLanguageModel,
|
||||||
|
getProviderConfig,
|
||||||
|
getProviderConfigByAlias,
|
||||||
|
getSupportedProviders,
|
||||||
|
hasInitializedProviders,
|
||||||
|
hasProviderConfig,
|
||||||
|
hasProviderConfigByAlias,
|
||||||
|
isProviderConfigAlias,
|
||||||
|
ProviderInitializationError,
|
||||||
|
providerRegistry,
|
||||||
|
registerMultipleProviderConfigs,
|
||||||
|
registerProvider,
|
||||||
|
registerProviderConfig,
|
||||||
|
resolveProviderConfigId
|
||||||
|
} from '../registry'
|
||||||
|
import type { ProviderConfig } from '../schemas'
|
||||||
|
|
||||||
|
describe('Provider Registry 功能测试', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// 清理状态
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('基础功能', () => {
|
||||||
|
it('能够获取支持的 providers 列表', () => {
|
||||||
|
const providers = getSupportedProviders()
|
||||||
|
expect(Array.isArray(providers)).toBe(true)
|
||||||
|
expect(providers.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// 检查返回的数据结构
|
||||||
|
providers.forEach((provider) => {
|
||||||
|
expect(provider).toHaveProperty('id')
|
||||||
|
expect(provider).toHaveProperty('name')
|
||||||
|
expect(typeof provider.id).toBe('string')
|
||||||
|
expect(typeof provider.name).toBe('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 包含基础 providers
|
||||||
|
const providerIds = providers.map((p) => p.id)
|
||||||
|
expect(providerIds).toContain('openai')
|
||||||
|
expect(providerIds).toContain('anthropic')
|
||||||
|
expect(providerIds).toContain('google')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够获取已初始化的 providers', () => {
|
||||||
|
// 初始状态下没有已初始化的 providers
|
||||||
|
expect(getInitializedProviders()).toEqual([])
|
||||||
|
expect(hasInitializedProviders()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够访问全局注册管理器', () => {
|
||||||
|
expect(providerRegistry).toBeDefined()
|
||||||
|
expect(typeof providerRegistry.clear).toBe('function')
|
||||||
|
expect(typeof providerRegistry.getRegisteredProviders).toBe('function')
|
||||||
|
expect(typeof providerRegistry.hasProviders).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够获取语言模型', () => {
|
||||||
|
// 在没有注册 provider 的情况下,这个函数应该会抛出错误
|
||||||
|
expect(() => getLanguageModel('non-existent')).toThrow('No providers registered')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Provider 配置注册', () => {
|
||||||
|
it('能够注册自定义 provider 配置', () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'custom-provider',
|
||||||
|
name: 'Custom Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'custom' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(config)
|
||||||
|
expect(success).toBe(true)
|
||||||
|
|
||||||
|
expect(hasProviderConfig('custom-provider')).toBe(true)
|
||||||
|
expect(getProviderConfig('custom-provider')).toEqual(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够注册带别名的 provider 配置', () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'custom-provider-with-aliases',
|
||||||
|
name: 'Custom Provider with Aliases',
|
||||||
|
creator: vi.fn(() => ({ name: 'custom-aliased' })),
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: ['alias-1', 'alias-2']
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(config)
|
||||||
|
expect(success).toBe(true)
|
||||||
|
|
||||||
|
expect(hasProviderConfigByAlias('alias-1')).toBe(true)
|
||||||
|
expect(hasProviderConfigByAlias('alias-2')).toBe(true)
|
||||||
|
expect(getProviderConfigByAlias('alias-1')).toEqual(config)
|
||||||
|
expect(resolveProviderConfigId('alias-1')).toBe('custom-provider-with-aliases')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝无效的配置', () => {
|
||||||
|
// 缺少必要字段
|
||||||
|
const invalidConfig = {
|
||||||
|
id: 'invalid-provider'
|
||||||
|
// 缺少 name, creator 等
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(invalidConfig as any)
|
||||||
|
expect(success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够批量注册 provider 配置', () => {
|
||||||
|
const configs: ProviderConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'provider-1',
|
||||||
|
name: 'Provider 1',
|
||||||
|
creator: vi.fn(() => ({ name: 'provider-1' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider-2',
|
||||||
|
name: 'Provider 2',
|
||||||
|
creator: vi.fn(() => ({ name: 'provider-2' })),
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '', // 无效配置
|
||||||
|
name: 'Invalid Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'invalid' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
} as any
|
||||||
|
]
|
||||||
|
|
||||||
|
const successCount = registerMultipleProviderConfigs(configs)
|
||||||
|
expect(successCount).toBe(2) // 只有前两个成功
|
||||||
|
|
||||||
|
expect(hasProviderConfig('provider-1')).toBe(true)
|
||||||
|
expect(hasProviderConfig('provider-2')).toBe(true)
|
||||||
|
expect(hasProviderConfig('')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够获取所有配置和别名信息', () => {
|
||||||
|
// 注册一些配置
|
||||||
|
registerProviderConfig({
|
||||||
|
id: 'test-provider',
|
||||||
|
name: 'Test Provider',
|
||||||
|
creator: vi.fn(),
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: ['test-alias']
|
||||||
|
})
|
||||||
|
|
||||||
|
const allConfigs = getAllProviderConfigs()
|
||||||
|
expect(Array.isArray(allConfigs)).toBe(true)
|
||||||
|
expect(allConfigs.some((config) => config.id === 'test-provider')).toBe(true)
|
||||||
|
|
||||||
|
const aliases = getAllProviderConfigAliases()
|
||||||
|
expect(aliases['test-alias']).toBe('test-provider')
|
||||||
|
expect(isProviderConfigAlias('test-alias')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Provider 创建和注册', () => {
|
||||||
|
it('能够创建 provider 实例', async () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'test-create-provider',
|
||||||
|
name: 'Test Create Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'test-created' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先注册配置
|
||||||
|
registerProviderConfig(config)
|
||||||
|
|
||||||
|
// 创建 provider 实例
|
||||||
|
const provider = await createProvider('test-create-provider', { apiKey: 'test' })
|
||||||
|
expect(provider).toBeDefined()
|
||||||
|
expect(config.creator).toHaveBeenCalledWith({ apiKey: 'test' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够注册 provider 到全局管理器', () => {
|
||||||
|
const mockProvider = { name: 'mock-provider' }
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'test-register-provider',
|
||||||
|
name: 'Test Register Provider',
|
||||||
|
creator: vi.fn(() => mockProvider),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先注册配置
|
||||||
|
registerProviderConfig(config)
|
||||||
|
|
||||||
|
// 注册 provider 到全局管理器
|
||||||
|
const success = registerProvider('test-register-provider', mockProvider)
|
||||||
|
expect(success).toBe(true)
|
||||||
|
|
||||||
|
// 验证注册成功
|
||||||
|
const registeredProviders = getInitializedProviders()
|
||||||
|
expect(registeredProviders).toContain('test-register-provider')
|
||||||
|
expect(hasInitializedProviders()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够一步完成创建和注册', async () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'test-create-and-register',
|
||||||
|
name: 'Test Create and Register',
|
||||||
|
creator: vi.fn(() => ({ name: 'test-both' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先注册配置
|
||||||
|
registerProviderConfig(config)
|
||||||
|
|
||||||
|
// 一步完成创建和注册
|
||||||
|
const success = await createAndRegisterProvider('test-create-and-register', { apiKey: 'test' })
|
||||||
|
expect(success).toBe(true)
|
||||||
|
|
||||||
|
// 验证注册成功
|
||||||
|
const registeredProviders = getInitializedProviders()
|
||||||
|
expect(registeredProviders).toContain('test-create-and-register')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Registry 管理', () => {
|
||||||
|
it('能够清理所有配置和注册的 providers', () => {
|
||||||
|
// 注册一些配置
|
||||||
|
registerProviderConfig({
|
||||||
|
id: 'temp-provider',
|
||||||
|
name: 'Temp Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'temp' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(hasProviderConfig('temp-provider')).toBe(true)
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
expect(hasProviderConfig('temp-provider')).toBe(false)
|
||||||
|
// 但基础配置应该重新加载
|
||||||
|
expect(hasProviderConfig('openai')).toBe(true) // 基础 providers 会重新初始化
|
||||||
|
})
|
||||||
|
|
||||||
|
it('能够单独清理已注册的 providers', () => {
|
||||||
|
// 清理所有 providers
|
||||||
|
clearAllProviders()
|
||||||
|
|
||||||
|
expect(getInitializedProviders()).toEqual([])
|
||||||
|
expect(hasInitializedProviders()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ProviderInitializationError 错误类工作正常', () => {
|
||||||
|
const error = new ProviderInitializationError('Test error', 'test-provider')
|
||||||
|
expect(error.message).toBe('Test error')
|
||||||
|
expect(error.providerId).toBe('test-provider')
|
||||||
|
expect(error.name).toBe('ProviderInitializationError')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('错误处理', () => {
|
||||||
|
it('优雅处理空配置', () => {
|
||||||
|
const success = registerProviderConfig(null as any)
|
||||||
|
expect(success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('优雅处理未定义配置', () => {
|
||||||
|
const success = registerProviderConfig(undefined as any)
|
||||||
|
expect(success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理空字符串 ID', () => {
|
||||||
|
const config = {
|
||||||
|
id: '',
|
||||||
|
name: 'Empty ID Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'empty' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(config)
|
||||||
|
expect(success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理创建不存在配置的 provider', async () => {
|
||||||
|
await expect(createProvider('non-existent-provider', {})).rejects.toThrow(
|
||||||
|
'ProviderConfig not found for id: non-existent-provider'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理注册不存在配置的 provider', () => {
|
||||||
|
const mockProvider = { name: 'mock' }
|
||||||
|
const success = registerProvider('non-existent-provider', mockProvider)
|
||||||
|
expect(success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理获取不存在配置的情况', () => {
|
||||||
|
expect(getProviderConfig('non-existent')).toBeUndefined()
|
||||||
|
expect(getProviderConfigByAlias('non-existent-alias')).toBeUndefined()
|
||||||
|
expect(hasProviderConfig('non-existent')).toBe(false)
|
||||||
|
expect(hasProviderConfigByAlias('non-existent-alias')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理批量注册时的部分失败', () => {
|
||||||
|
const mixedConfigs: ProviderConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'valid-provider-1',
|
||||||
|
name: 'Valid Provider 1',
|
||||||
|
creator: vi.fn(() => ({ name: 'valid-1' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '', // 无效配置
|
||||||
|
name: 'Invalid Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'invalid' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
id: 'valid-provider-2',
|
||||||
|
name: 'Valid Provider 2',
|
||||||
|
creator: vi.fn(() => ({ name: 'valid-2' })),
|
||||||
|
supportsImageGeneration: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const successCount = registerMultipleProviderConfigs(mixedConfigs)
|
||||||
|
expect(successCount).toBe(2) // 只有两个有效配置成功
|
||||||
|
|
||||||
|
expect(hasProviderConfig('valid-provider-1')).toBe(true)
|
||||||
|
expect(hasProviderConfig('valid-provider-2')).toBe(true)
|
||||||
|
expect(hasProviderConfig('')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理动态导入失败的情况', async () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'import-test-provider',
|
||||||
|
name: 'Import Test Provider',
|
||||||
|
import: vi.fn().mockRejectedValue(new Error('Import failed')),
|
||||||
|
creatorFunctionName: 'createTest',
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProviderConfig(config)
|
||||||
|
|
||||||
|
await expect(createProvider('import-test-provider', {})).rejects.toThrow('Import failed')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('集成测试', () => {
|
||||||
|
it('正确处理复杂的配置、创建、注册和清理场景', async () => {
|
||||||
|
// 初始状态验证
|
||||||
|
const initialConfigs = getAllProviderConfigs()
|
||||||
|
expect(initialConfigs.length).toBeGreaterThan(0) // 有基础配置
|
||||||
|
expect(getInitializedProviders()).toEqual([])
|
||||||
|
|
||||||
|
// 注册多个带别名的 provider 配置
|
||||||
|
const configs: ProviderConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'integration-provider-1',
|
||||||
|
name: 'Integration Provider 1',
|
||||||
|
creator: vi.fn(() => ({ name: 'integration-1' })),
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: ['alias-1', 'short-name-1']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'integration-provider-2',
|
||||||
|
name: 'Integration Provider 2',
|
||||||
|
creator: vi.fn(() => ({ name: 'integration-2' })),
|
||||||
|
supportsImageGeneration: true,
|
||||||
|
aliases: ['alias-2', 'short-name-2']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const successCount = registerMultipleProviderConfigs(configs)
|
||||||
|
expect(successCount).toBe(2)
|
||||||
|
|
||||||
|
// 验证配置注册成功
|
||||||
|
expect(hasProviderConfig('integration-provider-1')).toBe(true)
|
||||||
|
expect(hasProviderConfig('integration-provider-2')).toBe(true)
|
||||||
|
expect(hasProviderConfigByAlias('alias-1')).toBe(true)
|
||||||
|
expect(hasProviderConfigByAlias('alias-2')).toBe(true)
|
||||||
|
|
||||||
|
// 验证别名映射
|
||||||
|
const aliases = getAllProviderConfigAliases()
|
||||||
|
expect(aliases['alias-1']).toBe('integration-provider-1')
|
||||||
|
expect(aliases['alias-2']).toBe('integration-provider-2')
|
||||||
|
|
||||||
|
// 创建和注册 providers
|
||||||
|
const success1 = await createAndRegisterProvider('integration-provider-1', { apiKey: 'test1' })
|
||||||
|
const success2 = await createAndRegisterProvider('integration-provider-2', { apiKey: 'test2' })
|
||||||
|
expect(success1).toBe(true)
|
||||||
|
expect(success2).toBe(true)
|
||||||
|
|
||||||
|
// 验证注册成功
|
||||||
|
const registeredProviders = getInitializedProviders()
|
||||||
|
expect(registeredProviders).toContain('integration-provider-1')
|
||||||
|
expect(registeredProviders).toContain('integration-provider-2')
|
||||||
|
expect(hasInitializedProviders()).toBe(true)
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
// 验证清理后的状态
|
||||||
|
expect(getInitializedProviders()).toEqual([])
|
||||||
|
expect(hasProviderConfig('integration-provider-1')).toBe(false)
|
||||||
|
expect(hasProviderConfig('integration-provider-2')).toBe(false)
|
||||||
|
expect(getAllProviderConfigAliases()).toEqual({})
|
||||||
|
|
||||||
|
// 基础配置应该重新加载
|
||||||
|
expect(hasProviderConfig('openai')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('正确处理动态导入配置的 provider', async () => {
|
||||||
|
const mockModule = { createCustomProvider: vi.fn(() => ({ name: 'custom-dynamic' })) }
|
||||||
|
const dynamicImportConfig: ProviderConfig = {
|
||||||
|
id: 'dynamic-import-provider',
|
||||||
|
name: 'Dynamic Import Provider',
|
||||||
|
import: vi.fn().mockResolvedValue(mockModule),
|
||||||
|
creatorFunctionName: 'createCustomProvider',
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册配置
|
||||||
|
const configSuccess = registerProviderConfig(dynamicImportConfig)
|
||||||
|
expect(configSuccess).toBe(true)
|
||||||
|
|
||||||
|
// 创建和注册 provider
|
||||||
|
const registerSuccess = await createAndRegisterProvider('dynamic-import-provider', { apiKey: 'test' })
|
||||||
|
expect(registerSuccess).toBe(true)
|
||||||
|
|
||||||
|
// 验证导入函数被调用
|
||||||
|
expect(dynamicImportConfig.import).toHaveBeenCalled()
|
||||||
|
expect(mockModule.createCustomProvider).toHaveBeenCalledWith({ apiKey: 'test' })
|
||||||
|
|
||||||
|
// 验证注册成功
|
||||||
|
expect(getInitializedProviders()).toContain('dynamic-import-provider')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('正确处理大量配置的注册和管理', () => {
|
||||||
|
const largeConfigList: ProviderConfig[] = []
|
||||||
|
|
||||||
|
// 生成50个配置
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
largeConfigList.push({
|
||||||
|
id: `bulk-provider-${i}`,
|
||||||
|
name: `Bulk Provider ${i}`,
|
||||||
|
creator: vi.fn(() => ({ name: `bulk-${i}` })),
|
||||||
|
supportsImageGeneration: i % 2 === 0, // 偶数支持图像生成
|
||||||
|
aliases: [`alias-${i}`, `short-${i}`]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = registerMultipleProviderConfigs(largeConfigList)
|
||||||
|
expect(successCount).toBe(50)
|
||||||
|
|
||||||
|
// 验证所有配置都被正确注册
|
||||||
|
const allConfigs = getAllProviderConfigs()
|
||||||
|
expect(allConfigs.filter((config) => config.id.startsWith('bulk-provider-')).length).toBe(50)
|
||||||
|
|
||||||
|
// 验证别名数量
|
||||||
|
const aliases = getAllProviderConfigAliases()
|
||||||
|
const bulkAliases = Object.keys(aliases).filter(
|
||||||
|
(alias) => alias.startsWith('alias-') || alias.startsWith('short-')
|
||||||
|
)
|
||||||
|
expect(bulkAliases.length).toBe(100) // 每个 provider 有2个别名
|
||||||
|
|
||||||
|
// 随机验证几个配置
|
||||||
|
expect(hasProviderConfig('bulk-provider-0')).toBe(true)
|
||||||
|
expect(hasProviderConfig('bulk-provider-25')).toBe(true)
|
||||||
|
expect(hasProviderConfig('bulk-provider-49')).toBe(true)
|
||||||
|
|
||||||
|
// 验证别名工作正常
|
||||||
|
expect(resolveProviderConfigId('alias-25')).toBe('bulk-provider-25')
|
||||||
|
expect(isProviderConfigAlias('short-30')).toBe(true)
|
||||||
|
|
||||||
|
// 清理能正确处理大量数据
|
||||||
|
cleanup()
|
||||||
|
const cleanupAliases = getAllProviderConfigAliases()
|
||||||
|
expect(
|
||||||
|
Object.keys(cleanupAliases).filter((alias) => alias.startsWith('alias-') || alias.startsWith('short-'))
|
||||||
|
).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('边界测试', () => {
|
||||||
|
it('处理包含特殊字符的 provider IDs', () => {
|
||||||
|
const specialCharsConfigs: ProviderConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'provider-with-dashes',
|
||||||
|
name: 'Provider With Dashes',
|
||||||
|
creator: vi.fn(() => ({ name: 'dashes' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider_with_underscores',
|
||||||
|
name: 'Provider With Underscores',
|
||||||
|
creator: vi.fn(() => ({ name: 'underscores' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'provider.with.dots',
|
||||||
|
name: 'Provider With Dots',
|
||||||
|
creator: vi.fn(() => ({ name: 'dots' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const successCount = registerMultipleProviderConfigs(specialCharsConfigs)
|
||||||
|
expect(successCount).toBeGreaterThan(0) // 至少有一些成功
|
||||||
|
|
||||||
|
// 验证支持的特殊字符格式
|
||||||
|
if (hasProviderConfig('provider-with-dashes')) {
|
||||||
|
expect(getProviderConfig('provider-with-dashes')).toBeDefined()
|
||||||
|
}
|
||||||
|
if (hasProviderConfig('provider_with_underscores')) {
|
||||||
|
expect(getProviderConfig('provider_with_underscores')).toBeDefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理空的批量注册', () => {
|
||||||
|
const successCount = registerMultipleProviderConfigs([])
|
||||||
|
expect(successCount).toBe(0)
|
||||||
|
|
||||||
|
// 确保没有额外的配置被添加
|
||||||
|
const configsBefore = getAllProviderConfigs().length
|
||||||
|
expect(configsBefore).toBeGreaterThan(0) // 应该有基础配置
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理重复的配置注册', () => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'duplicate-test-provider',
|
||||||
|
name: 'Duplicate Test Provider',
|
||||||
|
creator: vi.fn(() => ({ name: 'duplicate' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一次注册成功
|
||||||
|
expect(registerProviderConfig(config)).toBe(true)
|
||||||
|
expect(hasProviderConfig('duplicate-test-provider')).toBe(true)
|
||||||
|
|
||||||
|
// 重复注册相同的配置(允许覆盖)
|
||||||
|
const updatedConfig: ProviderConfig = {
|
||||||
|
...config,
|
||||||
|
name: 'Updated Duplicate Test Provider'
|
||||||
|
}
|
||||||
|
expect(registerProviderConfig(updatedConfig)).toBe(true)
|
||||||
|
expect(hasProviderConfig('duplicate-test-provider')).toBe(true)
|
||||||
|
|
||||||
|
// 验证配置被更新
|
||||||
|
const retrievedConfig = getProviderConfig('duplicate-test-provider')
|
||||||
|
expect(retrievedConfig?.name).toBe('Updated Duplicate Test Provider')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理极长的 ID 和名称', () => {
|
||||||
|
const longId = 'very-long-provider-id-' + 'x'.repeat(100)
|
||||||
|
const longName = 'Very Long Provider Name ' + 'Y'.repeat(100)
|
||||||
|
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: longId,
|
||||||
|
name: longName,
|
||||||
|
creator: vi.fn(() => ({ name: 'long-test' })),
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(config)
|
||||||
|
expect(success).toBe(true)
|
||||||
|
expect(hasProviderConfig(longId)).toBe(true)
|
||||||
|
|
||||||
|
const retrievedConfig = getProviderConfig(longId)
|
||||||
|
expect(retrievedConfig?.name).toBe(longName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('处理大量别名的配置', () => {
|
||||||
|
const manyAliases = Array.from({ length: 50 }, (_, i) => `alias-${i}`)
|
||||||
|
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: 'provider-with-many-aliases',
|
||||||
|
name: 'Provider With Many Aliases',
|
||||||
|
creator: vi.fn(() => ({ name: 'many-aliases' })),
|
||||||
|
supportsImageGeneration: false,
|
||||||
|
aliases: manyAliases
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = registerProviderConfig(config)
|
||||||
|
expect(success).toBe(true)
|
||||||
|
|
||||||
|
// 验证所有别名都能正确解析
|
||||||
|
manyAliases.forEach((alias) => {
|
||||||
|
expect(hasProviderConfigByAlias(alias)).toBe(true)
|
||||||
|
expect(resolveProviderConfigId(alias)).toBe('provider-with-many-aliases')
|
||||||
|
expect(isProviderConfigAlias(alias)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type BaseProviderId,
|
||||||
|
baseProviderIds,
|
||||||
|
baseProviderIdSchema,
|
||||||
|
baseProviders,
|
||||||
|
type CustomProviderId,
|
||||||
|
customProviderIdSchema,
|
||||||
|
providerConfigSchema,
|
||||||
|
type ProviderId,
|
||||||
|
providerIdSchema
|
||||||
|
} from '../schemas'
|
||||||
|
|
||||||
|
describe('Provider Schemas', () => {
|
||||||
|
describe('baseProviders', () => {
|
||||||
|
it('包含所有预期的基础 providers', () => {
|
||||||
|
expect(baseProviders).toBeDefined()
|
||||||
|
expect(Array.isArray(baseProviders)).toBe(true)
|
||||||
|
expect(baseProviders.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const expectedIds = [
|
||||||
|
'openai',
|
||||||
|
'openai-responses',
|
||||||
|
'openai-compatible',
|
||||||
|
'anthropic',
|
||||||
|
'google',
|
||||||
|
'xai',
|
||||||
|
'azure',
|
||||||
|
'deepseek'
|
||||||
|
]
|
||||||
|
const actualIds = baseProviders.map((p) => p.id)
|
||||||
|
expectedIds.forEach((id) => {
|
||||||
|
expect(actualIds).toContain(id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('每个基础 provider 有必要的属性', () => {
|
||||||
|
baseProviders.forEach((provider) => {
|
||||||
|
expect(provider).toHaveProperty('id')
|
||||||
|
expect(provider).toHaveProperty('name')
|
||||||
|
expect(provider).toHaveProperty('creator')
|
||||||
|
expect(provider).toHaveProperty('supportsImageGeneration')
|
||||||
|
|
||||||
|
expect(typeof provider.id).toBe('string')
|
||||||
|
expect(typeof provider.name).toBe('string')
|
||||||
|
expect(typeof provider.creator).toBe('function')
|
||||||
|
expect(typeof provider.supportsImageGeneration).toBe('boolean')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('provider ID 是唯一的', () => {
|
||||||
|
const ids = baseProviders.map((p) => p.id)
|
||||||
|
const uniqueIds = [...new Set(ids)]
|
||||||
|
expect(ids).toEqual(uniqueIds)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('baseProviderIds', () => {
|
||||||
|
it('正确提取所有基础 provider IDs', () => {
|
||||||
|
expect(baseProviderIds).toBeDefined()
|
||||||
|
expect(Array.isArray(baseProviderIds)).toBe(true)
|
||||||
|
expect(baseProviderIds.length).toBe(baseProviders.length)
|
||||||
|
|
||||||
|
baseProviders.forEach((provider) => {
|
||||||
|
expect(baseProviderIds).toContain(provider.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('baseProviderIdSchema', () => {
|
||||||
|
it('验证有效的基础 provider IDs', () => {
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(baseProviderIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝无效的基础 provider IDs', () => {
|
||||||
|
const invalidIds = ['invalid', 'not-exists', '']
|
||||||
|
invalidIds.forEach((id) => {
|
||||||
|
expect(baseProviderIdSchema.safeParse(id).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('customProviderIdSchema', () => {
|
||||||
|
it('接受有效的自定义 provider IDs', () => {
|
||||||
|
const validIds = ['custom-provider', 'my-ai-service', 'company-llm-v2']
|
||||||
|
validIds.forEach((id) => {
|
||||||
|
expect(customProviderIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝与基础 provider IDs 冲突的 IDs', () => {
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(customProviderIdSchema.safeParse(id).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝空字符串', () => {
|
||||||
|
expect(customProviderIdSchema.safeParse('').success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('providerIdSchema', () => {
|
||||||
|
it('接受基础 provider IDs', () => {
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(providerIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('接受有效的自定义 provider IDs', () => {
|
||||||
|
const validCustomIds = ['custom-provider', 'my-ai-service']
|
||||||
|
validCustomIds.forEach((id) => {
|
||||||
|
expect(providerIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝无效的 IDs', () => {
|
||||||
|
const invalidIds = ['', undefined, null, 123]
|
||||||
|
invalidIds.forEach((id) => {
|
||||||
|
expect(providerIdSchema.safeParse(id).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('providerConfigSchema', () => {
|
||||||
|
it('验证带有 creator 的有效配置', () => {
|
||||||
|
const validConfig = {
|
||||||
|
id: 'custom-provider',
|
||||||
|
name: 'Custom Provider',
|
||||||
|
creator: vi.fn(),
|
||||||
|
supportsImageGeneration: true
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(validConfig).success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('验证带有 import 配置的有效配置', () => {
|
||||||
|
const validConfig = {
|
||||||
|
id: 'custom-provider',
|
||||||
|
name: 'Custom Provider',
|
||||||
|
import: vi.fn(),
|
||||||
|
creatorFunctionName: 'createCustom',
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(validConfig).success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝既没有 creator 也没有 import 配置的配置', () => {
|
||||||
|
const invalidConfig = {
|
||||||
|
id: 'invalid',
|
||||||
|
name: 'Invalid Provider',
|
||||||
|
supportsImageGeneration: false
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(invalidConfig).success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('为 supportsImageGeneration 设置默认值', () => {
|
||||||
|
const config = {
|
||||||
|
id: 'test',
|
||||||
|
name: 'Test',
|
||||||
|
creator: vi.fn()
|
||||||
|
}
|
||||||
|
const result = providerConfigSchema.safeParse(config)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.supportsImageGeneration).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝使用基础 provider ID 的配置', () => {
|
||||||
|
const invalidConfig = {
|
||||||
|
id: 'openai', // 基础 provider ID
|
||||||
|
name: 'Should Fail',
|
||||||
|
creator: vi.fn()
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(invalidConfig).success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('拒绝缺少必需字段的配置', () => {
|
||||||
|
const invalidConfigs = [
|
||||||
|
{ name: 'Missing ID', creator: vi.fn() },
|
||||||
|
{ id: 'missing-name', creator: vi.fn() },
|
||||||
|
{ id: '', name: 'Empty ID', creator: vi.fn() },
|
||||||
|
{ id: 'valid-custom', name: '', creator: vi.fn() }
|
||||||
|
]
|
||||||
|
|
||||||
|
invalidConfigs.forEach((config) => {
|
||||||
|
expect(providerConfigSchema.safeParse(config).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Schema 验证功能', () => {
|
||||||
|
it('baseProviderIdSchema 正确验证基础 provider IDs', () => {
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(baseProviderIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(baseProviderIdSchema.safeParse('invalid-id').success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('customProviderIdSchema 正确验证自定义 provider IDs', () => {
|
||||||
|
const customIds = ['custom-provider', 'my-service', 'company-llm']
|
||||||
|
customIds.forEach((id) => {
|
||||||
|
expect(customProviderIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拒绝基础 provider IDs
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(customProviderIdSchema.safeParse(id).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('providerIdSchema 接受基础和自定义 provider IDs', () => {
|
||||||
|
// 基础 IDs
|
||||||
|
baseProviderIds.forEach((id) => {
|
||||||
|
expect(providerIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 自定义 IDs
|
||||||
|
const customIds = ['custom-provider', 'my-service']
|
||||||
|
customIds.forEach((id) => {
|
||||||
|
expect(providerIdSchema.safeParse(id).success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('providerConfigSchema 验证完整的 provider 配置', () => {
|
||||||
|
const validConfig = {
|
||||||
|
id: 'custom-provider',
|
||||||
|
name: 'Custom Provider',
|
||||||
|
creator: vi.fn(),
|
||||||
|
supportsImageGeneration: true
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(validConfig).success).toBe(true)
|
||||||
|
|
||||||
|
const invalidConfig = {
|
||||||
|
id: 'openai', // 不允许基础 provider ID
|
||||||
|
name: 'OpenAI',
|
||||||
|
creator: vi.fn()
|
||||||
|
}
|
||||||
|
expect(providerConfigSchema.safeParse(invalidConfig).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('类型推导', () => {
|
||||||
|
it('BaseProviderId 类型正确', () => {
|
||||||
|
const id: BaseProviderId = 'openai'
|
||||||
|
expect(baseProviderIds).toContain(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('CustomProviderId 类型是字符串', () => {
|
||||||
|
const id: CustomProviderId = 'custom-provider'
|
||||||
|
expect(typeof id).toBe('string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ProviderId 类型支持基础和自定义 IDs', () => {
|
||||||
|
const baseId: ProviderId = 'openai'
|
||||||
|
const customId: ProviderId = 'custom-provider'
|
||||||
|
expect(typeof baseId).toBe('string')
|
||||||
|
expect(typeof customId).toBe('string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* AI Provider 配置工厂
|
||||||
|
* 提供类型安全的 Provider 配置构建器
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ProviderId, ProviderSettingsMap } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用配置基础类型,包含所有 Provider 共有的属性
|
||||||
|
*/
|
||||||
|
export interface BaseProviderConfig {
|
||||||
|
apiKey?: string
|
||||||
|
baseURL?: string
|
||||||
|
timeout?: number
|
||||||
|
headers?: Record<string, string>
|
||||||
|
fetch?: typeof globalThis.fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整的配置类型,结合基础配置、AI SDK 配置和特定 Provider 配置
|
||||||
|
*/
|
||||||
|
type CompleteProviderConfig<T extends ProviderId> = BaseProviderConfig & Partial<ProviderSettingsMap[T]>
|
||||||
|
|
||||||
|
type ConfigHandler<T extends ProviderId> = (
|
||||||
|
builder: ProviderConfigBuilder<T>,
|
||||||
|
provider: CompleteProviderConfig<T>
|
||||||
|
) => void
|
||||||
|
|
||||||
|
const configHandlers: {
|
||||||
|
[K in ProviderId]?: ConfigHandler<K>
|
||||||
|
} = {
|
||||||
|
azure: (builder, provider) => {
|
||||||
|
const azureBuilder = builder as ProviderConfigBuilder<'azure'>
|
||||||
|
const azureProvider = provider as CompleteProviderConfig<'azure'>
|
||||||
|
azureBuilder.withAzureConfig({
|
||||||
|
apiVersion: azureProvider.apiVersion,
|
||||||
|
resourceName: azureProvider.resourceName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProviderConfigBuilder<T extends ProviderId = ProviderId> {
|
||||||
|
private config: CompleteProviderConfig<T> = {} as CompleteProviderConfig<T>
|
||||||
|
|
||||||
|
constructor(private providerId: T) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 API Key
|
||||||
|
*/
|
||||||
|
withApiKey(apiKey: string): this
|
||||||
|
withApiKey(apiKey: string, options: T extends 'openai' ? { organization?: string; project?: string } : never): this
|
||||||
|
withApiKey(apiKey: string, options?: any): this {
|
||||||
|
this.config.apiKey = apiKey
|
||||||
|
|
||||||
|
// 类型安全的 OpenAI 特定配置
|
||||||
|
if (this.providerId === 'openai' && options) {
|
||||||
|
const openaiConfig = this.config as CompleteProviderConfig<'openai'>
|
||||||
|
if (options.organization) {
|
||||||
|
openaiConfig.organization = options.organization
|
||||||
|
}
|
||||||
|
if (options.project) {
|
||||||
|
openaiConfig.project = options.project
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置基础 URL
|
||||||
|
*/
|
||||||
|
withBaseURL(baseURL: string) {
|
||||||
|
this.config.baseURL = baseURL
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置请求配置
|
||||||
|
*/
|
||||||
|
withRequestConfig(options: { headers?: Record<string, string>; fetch?: typeof fetch }): this {
|
||||||
|
if (options.headers) {
|
||||||
|
this.config.headers = { ...this.config.headers, ...options.headers }
|
||||||
|
}
|
||||||
|
if (options.fetch) {
|
||||||
|
this.config.fetch = options.fetch
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure OpenAI 特定配置
|
||||||
|
*/
|
||||||
|
withAzureConfig(options: { apiVersion?: string; resourceName?: string }): T extends 'azure' ? this : never
|
||||||
|
withAzureConfig(options: any): any {
|
||||||
|
if (this.providerId === 'azure') {
|
||||||
|
const azureConfig = this.config as CompleteProviderConfig<'azure'>
|
||||||
|
if (options.apiVersion) {
|
||||||
|
azureConfig.apiVersion = options.apiVersion
|
||||||
|
}
|
||||||
|
if (options.resourceName) {
|
||||||
|
azureConfig.resourceName = options.resourceName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自定义参数
|
||||||
|
*/
|
||||||
|
withCustomParams(params: Record<string, any>) {
|
||||||
|
Object.assign(this.config, params)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建最终配置
|
||||||
|
*/
|
||||||
|
build(): ProviderSettingsMap[T] {
|
||||||
|
return this.config as ProviderSettingsMap[T]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 配置工厂
|
||||||
|
* 提供便捷的配置创建方法
|
||||||
|
*/
|
||||||
|
export class ProviderConfigFactory {
|
||||||
|
/**
|
||||||
|
* 创建配置构建器
|
||||||
|
*/
|
||||||
|
static builder<T extends ProviderId>(providerId: T): ProviderConfigBuilder<T> {
|
||||||
|
return new ProviderConfigBuilder(providerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从通用Provider对象创建配置 - 使用更优雅的处理器模式
|
||||||
|
*/
|
||||||
|
static fromProvider<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
provider: CompleteProviderConfig<T>,
|
||||||
|
options?: {
|
||||||
|
headers?: Record<string, string>
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
): ProviderSettingsMap[T] {
|
||||||
|
const builder = new ProviderConfigBuilder<T>(providerId)
|
||||||
|
|
||||||
|
// 设置基本配置
|
||||||
|
if (provider.apiKey) {
|
||||||
|
builder.withApiKey(provider.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.baseURL) {
|
||||||
|
builder.withBaseURL(provider.baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求配置
|
||||||
|
if (options?.headers) {
|
||||||
|
builder.withRequestConfig({
|
||||||
|
headers: options.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用配置处理器模式 - 更加优雅和可扩展
|
||||||
|
const handler = configHandlers[providerId]
|
||||||
|
if (handler) {
|
||||||
|
handler(builder, provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加其他自定义参数
|
||||||
|
if (options) {
|
||||||
|
const customOptions = { ...options }
|
||||||
|
delete customOptions.headers // 已经处理过了
|
||||||
|
if (Object.keys(customOptions).length > 0) {
|
||||||
|
builder.withCustomParams(customOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建 OpenAI 配置
|
||||||
|
*/
|
||||||
|
static createOpenAI(
|
||||||
|
apiKey: string,
|
||||||
|
options?: {
|
||||||
|
baseURL?: string
|
||||||
|
organization?: string
|
||||||
|
project?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const builder = this.builder('openai')
|
||||||
|
|
||||||
|
// 使用类型安全的重载
|
||||||
|
if (options?.organization || options?.project) {
|
||||||
|
builder.withApiKey(apiKey, {
|
||||||
|
organization: options.organization,
|
||||||
|
project: options.project
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
builder.withApiKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.withBaseURL(options?.baseURL || 'https://api.openai.com').build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建 Anthropic 配置
|
||||||
|
*/
|
||||||
|
static createAnthropic(
|
||||||
|
apiKey: string,
|
||||||
|
options?: {
|
||||||
|
baseURL?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.builder('anthropic')
|
||||||
|
.withApiKey(apiKey)
|
||||||
|
.withBaseURL(options?.baseURL || 'https://api.anthropic.com')
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建 Azure OpenAI 配置
|
||||||
|
*/
|
||||||
|
static createAzureOpenAI(
|
||||||
|
apiKey: string,
|
||||||
|
options: {
|
||||||
|
baseURL: string
|
||||||
|
apiVersion?: string
|
||||||
|
resourceName?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.builder('azure')
|
||||||
|
.withApiKey(apiKey)
|
||||||
|
.withBaseURL(options.baseURL)
|
||||||
|
.withAzureConfig({
|
||||||
|
apiVersion: options.apiVersion,
|
||||||
|
resourceName: options.resourceName
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建 Google 配置
|
||||||
|
*/
|
||||||
|
static createGoogle(
|
||||||
|
apiKey: string,
|
||||||
|
options?: {
|
||||||
|
baseURL?: string
|
||||||
|
projectId?: string
|
||||||
|
location?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.builder('google')
|
||||||
|
.withApiKey(apiKey)
|
||||||
|
.withBaseURL(options?.baseURL || 'https://generativelanguage.googleapis.com')
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建 Vertex AI 配置
|
||||||
|
*/
|
||||||
|
static createVertexAI() {
|
||||||
|
// credentials: {
|
||||||
|
// clientEmail: string
|
||||||
|
// privateKey: string
|
||||||
|
// },
|
||||||
|
// options?: {
|
||||||
|
// project?: string
|
||||||
|
// location?: string
|
||||||
|
// }
|
||||||
|
// return this.builder('google-vertex')
|
||||||
|
// .withGoogleCredentials(credentials)
|
||||||
|
// .withGoogleVertexConfig({
|
||||||
|
// project: options?.project,
|
||||||
|
// location: options?.location
|
||||||
|
// })
|
||||||
|
// .build()
|
||||||
|
}
|
||||||
|
|
||||||
|
static createOpenAICompatible(baseURL: string, apiKey: string) {
|
||||||
|
return this.builder('openai-compatible').withBaseURL(baseURL).withApiKey(apiKey).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷的配置创建函数
|
||||||
|
*/
|
||||||
|
export const createProviderConfig = ProviderConfigFactory.fromProvider
|
||||||
|
export const providerConfigBuilder = ProviderConfigFactory.builder
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Providers 模块统一导出 - 独立Provider包
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 核心管理器 ====================
|
||||||
|
|
||||||
|
// Provider 注册表管理器
|
||||||
|
export { globalRegistryManagement, RegistryManagement } from './RegistryManagement'
|
||||||
|
|
||||||
|
// Provider 核心功能
|
||||||
|
export {
|
||||||
|
// 状态管理
|
||||||
|
cleanup,
|
||||||
|
clearAllProviders,
|
||||||
|
createAndRegisterProvider,
|
||||||
|
createProvider,
|
||||||
|
getAllProviderConfigAliases,
|
||||||
|
getAllProviderConfigs,
|
||||||
|
getImageModel,
|
||||||
|
// 工具函数
|
||||||
|
getInitializedProviders,
|
||||||
|
getLanguageModel,
|
||||||
|
getProviderConfig,
|
||||||
|
getProviderConfigByAlias,
|
||||||
|
getSupportedProviders,
|
||||||
|
getTextEmbeddingModel,
|
||||||
|
hasInitializedProviders,
|
||||||
|
// 工具函数
|
||||||
|
hasProviderConfig,
|
||||||
|
// 别名支持
|
||||||
|
hasProviderConfigByAlias,
|
||||||
|
isProviderConfigAlias,
|
||||||
|
// 错误类型
|
||||||
|
ProviderInitializationError,
|
||||||
|
// 全局访问
|
||||||
|
providerRegistry,
|
||||||
|
registerMultipleProviderConfigs,
|
||||||
|
registerProvider,
|
||||||
|
// 统一Provider系统
|
||||||
|
registerProviderConfig,
|
||||||
|
resolveProviderConfigId
|
||||||
|
} from './registry'
|
||||||
|
|
||||||
|
// ==================== 基础数据和类型 ====================
|
||||||
|
|
||||||
|
// 基础Provider数据源
|
||||||
|
export { baseProviderIds, baseProviders } from './schemas'
|
||||||
|
|
||||||
|
// 类型定义和Schema
|
||||||
|
export type {
|
||||||
|
BaseProviderId,
|
||||||
|
CustomProviderId,
|
||||||
|
DynamicProviderRegistration,
|
||||||
|
ProviderConfig,
|
||||||
|
ProviderId
|
||||||
|
} from './schemas' // 从 schemas 导出的类型
|
||||||
|
export { baseProviderIdSchema, customProviderIdSchema, providerConfigSchema, providerIdSchema } from './schemas' // Schema 导出
|
||||||
|
export type {
|
||||||
|
DynamicProviderRegistry,
|
||||||
|
ExtensibleProviderSettingsMap,
|
||||||
|
ProviderError,
|
||||||
|
ProviderSettingsMap,
|
||||||
|
ProviderTypeRegistrar
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
// Provider配置工厂
|
||||||
|
export {
|
||||||
|
type BaseProviderConfig,
|
||||||
|
createProviderConfig,
|
||||||
|
ProviderConfigBuilder,
|
||||||
|
providerConfigBuilder,
|
||||||
|
ProviderConfigFactory
|
||||||
|
} from './factory'
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export { formatPrivateKey } from './utils'
|
||||||
|
|
||||||
|
// ==================== 扩展功能 ====================
|
||||||
|
|
||||||
|
// Hub Provider 功能
|
||||||
|
export { createHubProvider, type HubProviderConfig, HubProviderError } from './HubProvider'
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
/**
|
||||||
|
* Provider 初始化器
|
||||||
|
* 负责根据配置创建 providers 并注册到全局管理器
|
||||||
|
* 集成了来自 ModelCreator 的特殊处理逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { customProvider } from 'ai'
|
||||||
|
|
||||||
|
import { globalRegistryManagement } from './RegistryManagement'
|
||||||
|
import { baseProviders, type ProviderConfig } from './schemas'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 初始化错误类型
|
||||||
|
*/
|
||||||
|
class ProviderInitializationError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public providerId?: string,
|
||||||
|
public cause?: Error
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ProviderInitializationError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 全局管理器导出 ====================
|
||||||
|
|
||||||
|
export { globalRegistryManagement as providerRegistry }
|
||||||
|
|
||||||
|
// ==================== 便捷访问方法 ====================
|
||||||
|
|
||||||
|
export const getLanguageModel = (id: string) => globalRegistryManagement.languageModel(id as any)
|
||||||
|
export const getTextEmbeddingModel = (id: string) => globalRegistryManagement.textEmbeddingModel(id as any)
|
||||||
|
export const getImageModel = (id: string) => globalRegistryManagement.imageModel(id as any)
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支持的 Providers 列表
|
||||||
|
*/
|
||||||
|
export function getSupportedProviders(): Array<{
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}> {
|
||||||
|
return baseProviders.map((provider) => ({
|
||||||
|
id: provider.id,
|
||||||
|
name: provider.name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已初始化的 providers
|
||||||
|
*/
|
||||||
|
export function getInitializedProviders(): string[] {
|
||||||
|
return globalRegistryManagement.getRegisteredProviders()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有任何已初始化的 providers
|
||||||
|
*/
|
||||||
|
export function hasInitializedProviders(): boolean {
|
||||||
|
return globalRegistryManagement.hasProviders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 统一Provider配置系统 ====================
|
||||||
|
|
||||||
|
// 全局Provider配置存储
|
||||||
|
const providerConfigs = new Map<string, ProviderConfig>()
|
||||||
|
// 全局ProviderConfig别名映射 - 借鉴RegistryManagement模式
|
||||||
|
const providerConfigAliases = new Map<string, string>() // alias -> realId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化内置配置 - 将baseProviders转换为统一格式
|
||||||
|
*/
|
||||||
|
function initializeBuiltInConfigs(): void {
|
||||||
|
baseProviders.forEach((provider) => {
|
||||||
|
const config: ProviderConfig = {
|
||||||
|
id: provider.id,
|
||||||
|
name: provider.name,
|
||||||
|
creator: provider.creator as any, // 类型转换以兼容多种creator签名
|
||||||
|
supportsImageGeneration: provider.supportsImageGeneration || false
|
||||||
|
}
|
||||||
|
providerConfigs.set(provider.id, config)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动时自动注册内置配置
|
||||||
|
initializeBuiltInConfigs()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤1: 注册Provider配置 - 仅存储配置,不执行创建
|
||||||
|
*/
|
||||||
|
export function registerProviderConfig(config: ProviderConfig): boolean {
|
||||||
|
try {
|
||||||
|
// 验证配置
|
||||||
|
if (!config || !config.id || !config.name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否与已有配置冲突(包括内置配置)
|
||||||
|
if (providerConfigs.has(config.id)) {
|
||||||
|
console.warn(`ProviderConfig "${config.id}" already exists, will override`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储配置(内置和用户配置统一处理)
|
||||||
|
providerConfigs.set(config.id, config)
|
||||||
|
|
||||||
|
// 处理别名
|
||||||
|
if (config.aliases && config.aliases.length > 0) {
|
||||||
|
config.aliases.forEach((alias) => {
|
||||||
|
if (providerConfigAliases.has(alias)) {
|
||||||
|
console.warn(`ProviderConfig alias "${alias}" already exists, will override`)
|
||||||
|
}
|
||||||
|
providerConfigAliases.set(alias, config.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to register ProviderConfig:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤2: 创建Provider - 根据配置执行实际创建
|
||||||
|
*/
|
||||||
|
export async function createProvider(providerId: string, options: any): Promise<any> {
|
||||||
|
// 支持通过别名查找配置
|
||||||
|
const config = getProviderConfigByAlias(providerId)
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`ProviderConfig not found for id: ${providerId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let creator: (options: any) => any
|
||||||
|
|
||||||
|
if (config.creator) {
|
||||||
|
// 方式1: 直接执行 creator
|
||||||
|
creator = config.creator
|
||||||
|
} else if (config.import && config.creatorFunctionName) {
|
||||||
|
// 方式2: 动态导入并执行
|
||||||
|
const module = await config.import()
|
||||||
|
creator = (module as any)[config.creatorFunctionName]
|
||||||
|
|
||||||
|
if (!creator || typeof creator !== 'function') {
|
||||||
|
throw new Error(`Creator function "${config.creatorFunctionName}" not found in imported module`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No valid creator method provided in ProviderConfig')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用真实配置创建provider实例
|
||||||
|
return creator(options)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create provider "${providerId}":`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 步骤3: 注册Provider到全局管理器
|
||||||
|
*/
|
||||||
|
export function registerProvider(providerId: string, provider: any): boolean {
|
||||||
|
try {
|
||||||
|
const config = providerConfigs.get(providerId)
|
||||||
|
if (!config) {
|
||||||
|
console.error(`ProviderConfig not found for id: ${providerId}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取aliases配置
|
||||||
|
const aliases = config.aliases
|
||||||
|
|
||||||
|
// 处理特殊provider逻辑
|
||||||
|
if (providerId === 'openai') {
|
||||||
|
// 注册默认 openai
|
||||||
|
globalRegistryManagement.registerProvider(providerId, provider, aliases)
|
||||||
|
|
||||||
|
// 创建并注册 openai-chat 变体
|
||||||
|
const openaiChatProvider = customProvider({
|
||||||
|
fallbackProvider: {
|
||||||
|
...provider,
|
||||||
|
languageModel: (modelId: string) => provider.chat(modelId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
globalRegistryManagement.registerProvider(`${providerId}-chat`, openaiChatProvider)
|
||||||
|
} else if (providerId === 'azure') {
|
||||||
|
globalRegistryManagement.registerProvider(`${providerId}-chat`, provider, aliases)
|
||||||
|
// 跟上面相反,creator产出的默认会调用chat
|
||||||
|
const azureResponsesProvider = customProvider({
|
||||||
|
fallbackProvider: {
|
||||||
|
...provider,
|
||||||
|
languageModel: (modelId: string) => provider.responses(modelId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
globalRegistryManagement.registerProvider(providerId, azureResponsesProvider)
|
||||||
|
} else {
|
||||||
|
// 其他provider直接注册
|
||||||
|
globalRegistryManagement.registerProvider(providerId, provider, aliases)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to register provider "${providerId}" to global registry:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数: 一次性完成创建+注册
|
||||||
|
*/
|
||||||
|
export async function createAndRegisterProvider(providerId: string, options: any): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 步骤2: 创建provider
|
||||||
|
const provider = await createProvider(providerId, options)
|
||||||
|
|
||||||
|
// 步骤3: 注册到全局管理器
|
||||||
|
return registerProvider(providerId, provider)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to create and register provider "${providerId}":`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量注册Provider配置
|
||||||
|
*/
|
||||||
|
export function registerMultipleProviderConfigs(configs: ProviderConfig[]): number {
|
||||||
|
let successCount = 0
|
||||||
|
configs.forEach((config) => {
|
||||||
|
if (registerProviderConfig(config)) {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return successCount
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有对应的Provider配置
|
||||||
|
*/
|
||||||
|
export function hasProviderConfig(providerId: string): boolean {
|
||||||
|
return providerConfigs.has(providerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过别名或ID检查是否有对应的Provider配置
|
||||||
|
*/
|
||||||
|
export function hasProviderConfigByAlias(aliasOrId: string): boolean {
|
||||||
|
const realId = resolveProviderConfigId(aliasOrId)
|
||||||
|
return providerConfigs.has(realId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有Provider配置
|
||||||
|
*/
|
||||||
|
export function getAllProviderConfigs(): ProviderConfig[] {
|
||||||
|
return Array.from(providerConfigs.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取Provider配置
|
||||||
|
*/
|
||||||
|
export function getProviderConfig(providerId: string): ProviderConfig | undefined {
|
||||||
|
return providerConfigs.get(providerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过别名或ID获取Provider配置
|
||||||
|
*/
|
||||||
|
export function getProviderConfigByAlias(aliasOrId: string): ProviderConfig | undefined {
|
||||||
|
// 先检查是否为别名,如果是则解析为真实ID
|
||||||
|
const realId = providerConfigAliases.get(aliasOrId) || aliasOrId
|
||||||
|
return providerConfigs.get(realId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析真实的ProviderConfig ID(去别名化)
|
||||||
|
*/
|
||||||
|
export function resolveProviderConfigId(aliasOrId: string): string {
|
||||||
|
return providerConfigAliases.get(aliasOrId) || aliasOrId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为ProviderConfig别名
|
||||||
|
*/
|
||||||
|
export function isProviderConfigAlias(id: string): boolean {
|
||||||
|
return providerConfigAliases.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有ProviderConfig别名映射关系
|
||||||
|
*/
|
||||||
|
export function getAllProviderConfigAliases(): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
providerConfigAliases.forEach((realId, alias) => {
|
||||||
|
result[alias] = realId
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理所有Provider配置和已注册的providers
|
||||||
|
*/
|
||||||
|
export function cleanup(): void {
|
||||||
|
providerConfigs.clear()
|
||||||
|
providerConfigAliases.clear() // 清理别名映射
|
||||||
|
globalRegistryManagement.clear()
|
||||||
|
// 重新初始化内置配置
|
||||||
|
initializeBuiltInConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllProviders(): void {
|
||||||
|
globalRegistryManagement.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 导出错误类型 ====================
|
||||||
|
|
||||||
|
export { ProviderInitializationError }
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Provider Config 定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createAnthropic } from '@ai-sdk/anthropic'
|
||||||
|
import { createAzure } from '@ai-sdk/azure'
|
||||||
|
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
||||||
|
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||||
|
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||||
|
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||||
|
import { LanguageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import { createXai } from '@ai-sdk/xai'
|
||||||
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||||
|
import { customProvider, Provider } from 'ai'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础 Provider IDs
|
||||||
|
*/
|
||||||
|
export const baseProviderIds = [
|
||||||
|
'openai',
|
||||||
|
'openai-chat',
|
||||||
|
'openai-compatible',
|
||||||
|
'anthropic',
|
||||||
|
'google',
|
||||||
|
'xai',
|
||||||
|
'azure',
|
||||||
|
'azure-responses',
|
||||||
|
'deepseek',
|
||||||
|
'openrouter'
|
||||||
|
] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础 Provider ID Schema
|
||||||
|
*/
|
||||||
|
export const baseProviderIdSchema = z.enum(baseProviderIds)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础 Provider ID
|
||||||
|
*/
|
||||||
|
export type BaseProviderId = z.infer<typeof baseProviderIdSchema>
|
||||||
|
|
||||||
|
export const isBaseProvider = (id: ProviderId): id is BaseProviderId => {
|
||||||
|
return baseProviderIdSchema.safeParse(id).success
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseProvider = {
|
||||||
|
id: BaseProviderId
|
||||||
|
name: string
|
||||||
|
creator: (options: any) => Provider | LanguageModelV2
|
||||||
|
supportsImageGeneration: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础 Providers 定义
|
||||||
|
* 作为唯一数据源,避免重复维护
|
||||||
|
*/
|
||||||
|
export const baseProviders = [
|
||||||
|
{
|
||||||
|
id: 'openai',
|
||||||
|
name: 'OpenAI',
|
||||||
|
creator: createOpenAI,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openai-chat',
|
||||||
|
name: 'OpenAI Chat',
|
||||||
|
creator: (options: OpenAIProviderSettings) => {
|
||||||
|
const provider = createOpenAI(options)
|
||||||
|
return customProvider({
|
||||||
|
fallbackProvider: {
|
||||||
|
...provider,
|
||||||
|
languageModel: (modelId: string) => provider.chat(modelId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openai-compatible',
|
||||||
|
name: 'OpenAI Compatible',
|
||||||
|
creator: createOpenAICompatible,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'anthropic',
|
||||||
|
name: 'Anthropic',
|
||||||
|
creator: createAnthropic,
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'google',
|
||||||
|
name: 'Google Generative AI',
|
||||||
|
creator: createGoogleGenerativeAI,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xai',
|
||||||
|
name: 'xAI (Grok)',
|
||||||
|
creator: createXai,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'azure',
|
||||||
|
name: 'Azure OpenAI',
|
||||||
|
creator: createAzure,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'azure-responses',
|
||||||
|
name: 'Azure OpenAI Responses',
|
||||||
|
creator: (options: AzureOpenAIProviderSettings) => {
|
||||||
|
const provider = createAzure(options)
|
||||||
|
return customProvider({
|
||||||
|
fallbackProvider: {
|
||||||
|
...provider,
|
||||||
|
languageModel: (modelId: string) => provider.responses(modelId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
supportsImageGeneration: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek',
|
||||||
|
name: 'DeepSeek',
|
||||||
|
creator: createDeepSeek,
|
||||||
|
supportsImageGeneration: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openrouter',
|
||||||
|
name: 'OpenRouter',
|
||||||
|
creator: createOpenRouter,
|
||||||
|
supportsImageGeneration: true
|
||||||
|
}
|
||||||
|
] as const satisfies BaseProvider[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户自定义 Provider ID Schema
|
||||||
|
* 允许任意字符串,但排除基础 provider IDs 以避免冲突
|
||||||
|
*/
|
||||||
|
export const customProviderIdSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((id) => !baseProviderIds.includes(id as any), {
|
||||||
|
message: 'Custom provider ID cannot conflict with base provider IDs'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider ID Schema - 支持基础和自定义
|
||||||
|
*/
|
||||||
|
export const providerIdSchema = z.union([baseProviderIdSchema, customProviderIdSchema])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 配置 Schema
|
||||||
|
* 用于Provider的配置验证
|
||||||
|
*/
|
||||||
|
export const providerConfigSchema = z
|
||||||
|
.object({
|
||||||
|
id: customProviderIdSchema, // 只允许自定义ID
|
||||||
|
name: z.string().min(1),
|
||||||
|
creator: z
|
||||||
|
.function({
|
||||||
|
input: z.any(),
|
||||||
|
output: z.any()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
import: z.function().optional(),
|
||||||
|
creatorFunctionName: z.string().optional(),
|
||||||
|
supportsImageGeneration: z.boolean().default(false),
|
||||||
|
imageCreator: z.function().optional(),
|
||||||
|
validateOptions: z.function().optional(),
|
||||||
|
aliases: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
.refine((data) => data.creator || (data.import && data.creatorFunctionName), {
|
||||||
|
message: 'Must provide either creator function or import configuration'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider ID 类型 - 基于 zod schema 推导
|
||||||
|
*/
|
||||||
|
export type ProviderId = z.infer<typeof providerIdSchema>
|
||||||
|
export type CustomProviderId = z.infer<typeof customProviderIdSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider 配置类型
|
||||||
|
*/
|
||||||
|
export type ProviderConfig = z.infer<typeof providerConfigSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容性类型别名
|
||||||
|
* @deprecated 使用 ProviderConfig 替代
|
||||||
|
*/
|
||||||
|
export type DynamicProviderRegistration = ProviderConfig
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { type AnthropicProviderSettings } from '@ai-sdk/anthropic'
|
||||||
|
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
||||||
|
import { type DeepSeekProviderSettings } from '@ai-sdk/deepseek'
|
||||||
|
import { type GoogleGenerativeAIProviderSettings } from '@ai-sdk/google'
|
||||||
|
import { type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
|
import { type OpenAICompatibleProviderSettings } from '@ai-sdk/openai-compatible'
|
||||||
|
import {
|
||||||
|
EmbeddingModelV2 as EmbeddingModel,
|
||||||
|
ImageModelV2 as ImageModel,
|
||||||
|
LanguageModelV2 as LanguageModel,
|
||||||
|
ProviderV2,
|
||||||
|
SpeechModelV2 as SpeechModel,
|
||||||
|
TranscriptionModelV2 as TranscriptionModel
|
||||||
|
} from '@ai-sdk/provider'
|
||||||
|
import { type XaiProviderSettings } from '@ai-sdk/xai'
|
||||||
|
|
||||||
|
// 导入基于 Zod 的 ProviderId 类型
|
||||||
|
import { type ProviderId as ZodProviderId } from './schemas'
|
||||||
|
|
||||||
|
export interface ExtensibleProviderSettingsMap {
|
||||||
|
// 基础的静态providers
|
||||||
|
openai: OpenAIProviderSettings
|
||||||
|
'openai-responses': OpenAIProviderSettings
|
||||||
|
'openai-compatible': OpenAICompatibleProviderSettings
|
||||||
|
anthropic: AnthropicProviderSettings
|
||||||
|
google: GoogleGenerativeAIProviderSettings
|
||||||
|
xai: XaiProviderSettings
|
||||||
|
azure: AzureOpenAIProviderSettings
|
||||||
|
deepseek: DeepSeekProviderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态扩展的provider类型注册表
|
||||||
|
export interface DynamicProviderRegistry {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并基础和动态provider类型
|
||||||
|
export type ProviderSettingsMap = ExtensibleProviderSettingsMap & DynamicProviderRegistry
|
||||||
|
|
||||||
|
// 错误类型
|
||||||
|
export class ProviderError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public providerId: string,
|
||||||
|
public code?: string,
|
||||||
|
public cause?: Error
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ProviderError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态ProviderId类型 - 基于 Zod Schema,支持运行时扩展和验证
|
||||||
|
export type ProviderId = ZodProviderId
|
||||||
|
|
||||||
|
export interface ProviderTypeRegistrar {
|
||||||
|
registerProviderType<T extends string, S>(providerId: T, settingsType: S): void
|
||||||
|
getProviderSettings<T extends string>(providerId: T): any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新导出所有类型供外部使用
|
||||||
|
export type {
|
||||||
|
AnthropicProviderSettings,
|
||||||
|
AzureOpenAIProviderSettings,
|
||||||
|
DeepSeekProviderSettings,
|
||||||
|
GoogleGenerativeAIProviderSettings,
|
||||||
|
OpenAICompatibleProviderSettings,
|
||||||
|
OpenAIProviderSettings,
|
||||||
|
XaiProviderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiSdkModel = LanguageModel | ImageModel | EmbeddingModel<string> | TranscriptionModel | SpeechModel
|
||||||
|
|
||||||
|
export type AiSdkModelType = 'text' | 'image' | 'embedding' | 'transcription' | 'speech'
|
||||||
|
|
||||||
|
export const METHOD_MAP = {
|
||||||
|
text: 'languageModel',
|
||||||
|
image: 'imageModel',
|
||||||
|
embedding: 'textEmbeddingModel',
|
||||||
|
transcription: 'transcriptionModel',
|
||||||
|
speech: 'speechModel'
|
||||||
|
} as const satisfies Record<AiSdkModelType, keyof ProviderV2>
|
||||||
|
|
||||||
|
export type AiSdkModelMethodMap = Record<AiSdkModelType, keyof ProviderV2>
|
||||||
|
|
||||||
|
export type AiSdkModelReturnMap = {
|
||||||
|
text: LanguageModel
|
||||||
|
image: ImageModel
|
||||||
|
embedding: EmbeddingModel<string>
|
||||||
|
transcription: TranscriptionModel
|
||||||
|
speech: SpeechModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiSdkMethodName<T extends AiSdkModelType> = (typeof METHOD_MAP)[T]
|
||||||
|
|
||||||
|
export type AiSdkModelReturn<T extends AiSdkModelType> = AiSdkModelReturnMap[T]
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 格式化私钥,确保它包含正确的PEM头部和尾部
|
||||||
|
*/
|
||||||
|
export function formatPrivateKey(privateKey: string): string {
|
||||||
|
if (!privateKey || typeof privateKey !== 'string') {
|
||||||
|
throw new Error('Private key must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先处理 JSON 字符串中的转义换行符
|
||||||
|
const key = privateKey.replace(/\\n/g, '\n')
|
||||||
|
|
||||||
|
// 检查是否已经是正确格式的 PEM 私钥
|
||||||
|
const hasBeginMarker = key.includes('-----BEGIN PRIVATE KEY-----')
|
||||||
|
const hasEndMarker = key.includes('-----END PRIVATE KEY-----')
|
||||||
|
|
||||||
|
if (hasBeginMarker && hasEndMarker) {
|
||||||
|
// 已经是 PEM 格式,但可能格式不规范,重新格式化
|
||||||
|
return normalizePemFormat(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有完整的 PEM 头尾,尝试重新构建
|
||||||
|
return reconstructPemKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准化 PEM 格式
|
||||||
|
*/
|
||||||
|
function normalizePemFormat(pemKey: string): string {
|
||||||
|
// 分离头部、内容和尾部
|
||||||
|
const lines = pemKey
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
|
||||||
|
let keyContent = ''
|
||||||
|
let foundBegin = false
|
||||||
|
let foundEnd = false
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line === '-----BEGIN PRIVATE KEY-----') {
|
||||||
|
foundBegin = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (line === '-----END PRIVATE KEY-----') {
|
||||||
|
foundEnd = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (foundBegin && !foundEnd) {
|
||||||
|
keyContent += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundBegin || !foundEnd || !keyContent) {
|
||||||
|
throw new Error('Invalid PEM format: missing BEGIN/END markers or key content')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新格式化为 64 字符一行
|
||||||
|
const formattedContent = keyContent.match(/.{1,64}/g)?.join('\n') || keyContent
|
||||||
|
|
||||||
|
return `-----BEGIN PRIVATE KEY-----\n${formattedContent}\n-----END PRIVATE KEY-----`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新构建 PEM 私钥
|
||||||
|
*/
|
||||||
|
function reconstructPemKey(key: string): string {
|
||||||
|
// 移除所有空白字符和可能存在的不完整头尾
|
||||||
|
let cleanKey = key.replace(/\s+/g, '')
|
||||||
|
cleanKey = cleanKey.replace(/-----BEGIN[^-]*-----/g, '')
|
||||||
|
cleanKey = cleanKey.replace(/-----END[^-]*-----/g, '')
|
||||||
|
|
||||||
|
// 确保私钥内容不为空
|
||||||
|
if (!cleanKey) {
|
||||||
|
throw new Error('Private key content is empty after cleaning')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否是有效的 Base64 字符
|
||||||
|
if (!/^[A-Za-z0-9+/=]+$/.test(cleanKey)) {
|
||||||
|
throw new Error('Private key contains invalid characters (not valid Base64)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化为 64 字符一行
|
||||||
|
const formattedKey = cleanKey.match(/.{1,64}/g)?.join('\n') || cleanKey
|
||||||
|
|
||||||
|
return `-----BEGIN PRIVATE KEY-----\n${formattedKey}\n-----END PRIVATE KEY-----`
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
import { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import { experimental_generateImage as aiGenerateImage, NoImageGeneratedError } from 'ai'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { type AiPlugin } from '../../plugins'
|
||||||
|
import { globalRegistryManagement } from '../../providers/RegistryManagement'
|
||||||
|
import { ImageGenerationError, ImageModelResolutionError } from '../errors'
|
||||||
|
import { RuntimeExecutor } from '../executor'
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('ai', () => ({
|
||||||
|
experimental_generateImage: vi.fn(),
|
||||||
|
NoImageGeneratedError: class NoImageGeneratedError extends Error {
|
||||||
|
static isInstance = vi.fn()
|
||||||
|
constructor() {
|
||||||
|
super('No image generated')
|
||||||
|
this.name = 'NoImageGeneratedError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../providers/RegistryManagement', () => ({
|
||||||
|
globalRegistryManagement: {
|
||||||
|
imageModel: vi.fn()
|
||||||
|
},
|
||||||
|
DEFAULT_SEPARATOR: '|'
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('RuntimeExecutor.generateImage', () => {
|
||||||
|
let executor: RuntimeExecutor<'openai'>
|
||||||
|
let mockImageModel: ImageModelV2
|
||||||
|
let mockGenerateImageResult: any
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset all mocks
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
// Create executor instance
|
||||||
|
executor = RuntimeExecutor.create('openai', {
|
||||||
|
apiKey: 'test-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock image model
|
||||||
|
mockImageModel = {
|
||||||
|
modelId: 'dall-e-3',
|
||||||
|
provider: 'openai'
|
||||||
|
} as ImageModelV2
|
||||||
|
|
||||||
|
// Mock generateImage result
|
||||||
|
mockGenerateImageResult = {
|
||||||
|
image: {
|
||||||
|
base64: 'base64-encoded-image-data',
|
||||||
|
uint8Array: new Uint8Array([1, 2, 3]),
|
||||||
|
mediaType: 'image/png'
|
||||||
|
},
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
base64: 'base64-encoded-image-data',
|
||||||
|
uint8Array: new Uint8Array([1, 2, 3]),
|
||||||
|
mediaType: 'image/png'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
warnings: [],
|
||||||
|
providerMetadata: {
|
||||||
|
openai: {
|
||||||
|
images: [{ revisedPrompt: 'A detailed prompt' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responses: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup mocks to avoid "No providers registered" error
|
||||||
|
vi.mocked(globalRegistryManagement.imageModel).mockReturnValue(mockImageModel)
|
||||||
|
vi.mocked(aiGenerateImage).mockResolvedValue(mockGenerateImageResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Basic functionality', () => {
|
||||||
|
it('should generate a single image with minimal parameters', async () => {
|
||||||
|
const result = await executor.generateImage({ model: 'dall-e-3', prompt: 'A futuristic cityscape at sunset' })
|
||||||
|
|
||||||
|
expect(globalRegistryManagement.imageModel).toHaveBeenCalledWith('openai|dall-e-3')
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A futuristic cityscape at sunset'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockGenerateImageResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should generate image with pre-created model', async () => {
|
||||||
|
const result = await executor.generateImage({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A beautiful landscape'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Note: globalRegistryManagement.imageModel may still be called due to resolveImageModel logic
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A beautiful landscape'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockGenerateImageResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support multiple images generation', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A futuristic cityscape', n: 3 })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A futuristic cityscape',
|
||||||
|
n: 3
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support size specification', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A beautiful sunset', size: '1024x1024' })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A beautiful sunset',
|
||||||
|
size: '1024x1024'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support aspect ratio specification', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A mountain landscape', aspectRatio: '16:9' })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A mountain landscape',
|
||||||
|
aspectRatio: '16:9'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support seed for consistent output', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A cat in space', seed: 1234567890 })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A cat in space',
|
||||||
|
seed: 1234567890
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support abort signal', async () => {
|
||||||
|
const abortController = new AbortController()
|
||||||
|
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A cityscape', abortSignal: abortController.signal })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A cityscape',
|
||||||
|
abortSignal: abortController.signal
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support provider-specific options', async () => {
|
||||||
|
await executor.generateImage({
|
||||||
|
model: 'dall-e-3',
|
||||||
|
prompt: 'A space station',
|
||||||
|
providerOptions: {
|
||||||
|
openai: {
|
||||||
|
quality: 'hd',
|
||||||
|
style: 'vivid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A space station',
|
||||||
|
providerOptions: {
|
||||||
|
openai: {
|
||||||
|
quality: 'hd',
|
||||||
|
style: 'vivid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support custom headers', async () => {
|
||||||
|
await executor.generateImage({
|
||||||
|
model: 'dall-e-3',
|
||||||
|
prompt: 'A robot',
|
||||||
|
headers: {
|
||||||
|
'X-Custom-Header': 'test-value'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A robot',
|
||||||
|
headers: {
|
||||||
|
'X-Custom-Header': 'test-value'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Plugin integration', () => {
|
||||||
|
it('should execute plugins in correct order', async () => {
|
||||||
|
const pluginCallOrder: string[] = []
|
||||||
|
|
||||||
|
const testPlugin: AiPlugin = {
|
||||||
|
name: 'test-plugin',
|
||||||
|
onRequestStart: vi.fn(async () => {
|
||||||
|
pluginCallOrder.push('onRequestStart')
|
||||||
|
}),
|
||||||
|
transformParams: vi.fn(async (params) => {
|
||||||
|
pluginCallOrder.push('transformParams')
|
||||||
|
return { ...params, size: '512x512' }
|
||||||
|
}),
|
||||||
|
transformResult: vi.fn(async (result) => {
|
||||||
|
pluginCallOrder.push('transformResult')
|
||||||
|
return { ...result, processed: true }
|
||||||
|
}),
|
||||||
|
onRequestEnd: vi.fn(async () => {
|
||||||
|
pluginCallOrder.push('onRequestEnd')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const executorWithPlugin = RuntimeExecutor.create(
|
||||||
|
'openai',
|
||||||
|
{
|
||||||
|
apiKey: 'test-key'
|
||||||
|
},
|
||||||
|
[testPlugin]
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
||||||
|
|
||||||
|
expect(pluginCallOrder).toEqual(['onRequestStart', 'transformParams', 'transformResult', 'onRequestEnd'])
|
||||||
|
|
||||||
|
expect(testPlugin.transformParams).toHaveBeenCalledWith(
|
||||||
|
{ prompt: 'A test image' },
|
||||||
|
expect.objectContaining({
|
||||||
|
providerId: 'openai',
|
||||||
|
modelId: 'dall-e-3'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A test image',
|
||||||
|
size: '512x512' // Should be transformed by plugin
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
...mockGenerateImageResult,
|
||||||
|
processed: true // Should be transformed by plugin
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle model resolution through plugins', async () => {
|
||||||
|
const customImageModel = {
|
||||||
|
modelId: 'custom-model',
|
||||||
|
provider: 'openai'
|
||||||
|
} as ImageModelV2
|
||||||
|
|
||||||
|
const modelResolutionPlugin: AiPlugin = {
|
||||||
|
name: 'model-resolver',
|
||||||
|
resolveModel: vi.fn(async () => customImageModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
const executorWithPlugin = RuntimeExecutor.create(
|
||||||
|
'openai',
|
||||||
|
{
|
||||||
|
apiKey: 'test-key'
|
||||||
|
},
|
||||||
|
[modelResolutionPlugin]
|
||||||
|
)
|
||||||
|
|
||||||
|
await executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
||||||
|
|
||||||
|
expect(modelResolutionPlugin.resolveModel).toHaveBeenCalledWith(
|
||||||
|
'dall-e-3',
|
||||||
|
expect.objectContaining({
|
||||||
|
providerId: 'openai',
|
||||||
|
modelId: 'dall-e-3'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: customImageModel,
|
||||||
|
prompt: 'A test image'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support recursive calls from plugins', async () => {
|
||||||
|
const recursivePlugin: AiPlugin = {
|
||||||
|
name: 'recursive-plugin',
|
||||||
|
transformParams: vi.fn(async (params, context) => {
|
||||||
|
if (!context.isRecursiveCall && params.prompt === 'original') {
|
||||||
|
// Make a recursive call with modified prompt
|
||||||
|
await context.recursiveCall({
|
||||||
|
model: 'dall-e-3',
|
||||||
|
prompt: 'modified'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const executorWithPlugin = RuntimeExecutor.create(
|
||||||
|
'openai',
|
||||||
|
{
|
||||||
|
apiKey: 'test-key'
|
||||||
|
},
|
||||||
|
[recursivePlugin]
|
||||||
|
)
|
||||||
|
|
||||||
|
await executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'original' })
|
||||||
|
|
||||||
|
expect(recursivePlugin.transformParams).toHaveBeenCalledTimes(2)
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Error handling', () => {
|
||||||
|
it('should handle model creation errors', async () => {
|
||||||
|
const modelError = new Error('Failed to get image model')
|
||||||
|
vi.mocked(globalRegistryManagement.imageModel).mockImplementation(() => {
|
||||||
|
throw modelError
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(executor.generateImage({ model: 'invalid-model', prompt: 'A test image' })).rejects.toThrow(
|
||||||
|
ImageGenerationError
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle ImageModelResolutionError correctly', async () => {
|
||||||
|
const resolutionError = new ImageModelResolutionError('invalid-model', 'openai', new Error('Model not found'))
|
||||||
|
vi.mocked(globalRegistryManagement.imageModel).mockImplementation(() => {
|
||||||
|
throw resolutionError
|
||||||
|
})
|
||||||
|
|
||||||
|
const thrownError = await executor
|
||||||
|
.generateImage({ model: 'invalid-model', prompt: 'A test image' })
|
||||||
|
.catch((error) => error)
|
||||||
|
|
||||||
|
expect(thrownError).toBeInstanceOf(ImageGenerationError)
|
||||||
|
expect(thrownError.message).toContain('Failed to generate image:')
|
||||||
|
expect(thrownError.providerId).toBe('openai')
|
||||||
|
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 () => {
|
||||||
|
const resolutionError = new ImageModelResolutionError('unknown-model')
|
||||||
|
vi.mocked(globalRegistryManagement.imageModel).mockImplementation(() => {
|
||||||
|
throw resolutionError
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(executor.generateImage({ model: 'unknown-model', prompt: 'A test image' })).rejects.toThrow(
|
||||||
|
ImageGenerationError
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle image generation API errors', async () => {
|
||||||
|
const apiError = new Error('API request failed')
|
||||||
|
vi.mocked(aiGenerateImage).mockRejectedValue(apiError)
|
||||||
|
|
||||||
|
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
|
'Failed to generate image:'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle NoImageGeneratedError', async () => {
|
||||||
|
const noImageError = new NoImageGeneratedError({
|
||||||
|
cause: new Error('No image generated'),
|
||||||
|
responses: []
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mocked(aiGenerateImage).mockRejectedValue(noImageError)
|
||||||
|
vi.mocked(NoImageGeneratedError.isInstance).mockReturnValue(true)
|
||||||
|
|
||||||
|
await expect(executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
|
'Failed to generate image:'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute onError plugin hook on failure', async () => {
|
||||||
|
const error = new Error('Generation failed')
|
||||||
|
vi.mocked(aiGenerateImage).mockRejectedValue(error)
|
||||||
|
|
||||||
|
const errorPlugin: AiPlugin = {
|
||||||
|
name: 'error-handler',
|
||||||
|
onError: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
const executorWithPlugin = RuntimeExecutor.create(
|
||||||
|
'openai',
|
||||||
|
{
|
||||||
|
apiKey: 'test-key'
|
||||||
|
},
|
||||||
|
[errorPlugin]
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(executorWithPlugin.generateImage({ model: 'dall-e-3', prompt: 'A test image' })).rejects.toThrow(
|
||||||
|
'Failed to generate image:'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(errorPlugin.onError).toHaveBeenCalledWith(
|
||||||
|
error,
|
||||||
|
expect.objectContaining({
|
||||||
|
providerId: 'openai',
|
||||||
|
modelId: 'dall-e-3'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle abort signal timeout', async () => {
|
||||||
|
const abortError = new Error('Operation was aborted')
|
||||||
|
abortError.name = 'AbortError'
|
||||||
|
vi.mocked(aiGenerateImage).mockRejectedValue(abortError)
|
||||||
|
|
||||||
|
const abortController = new AbortController()
|
||||||
|
setTimeout(() => abortController.abort(), 10)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
executor.generateImage({ model: 'dall-e-3', prompt: 'A test image', abortSignal: abortController.signal })
|
||||||
|
).rejects.toThrow('Failed to generate image:')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Multiple providers support', () => {
|
||||||
|
it('should work with different providers', async () => {
|
||||||
|
const googleExecutor = RuntimeExecutor.create('google', {
|
||||||
|
apiKey: 'google-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
await googleExecutor.generateImage({ model: 'imagen-3.0-generate-002', prompt: 'A landscape' })
|
||||||
|
|
||||||
|
expect(globalRegistryManagement.imageModel).toHaveBeenCalledWith('google|imagen-3.0-generate-002')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support xAI Grok image models', async () => {
|
||||||
|
const xaiExecutor = RuntimeExecutor.create('xai', {
|
||||||
|
apiKey: 'xai-key'
|
||||||
|
})
|
||||||
|
|
||||||
|
await xaiExecutor.generateImage({ model: 'grok-2-image', prompt: 'A futuristic robot' })
|
||||||
|
|
||||||
|
expect(globalRegistryManagement.imageModel).toHaveBeenCalledWith('xai|grok-2-image')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Advanced features', () => {
|
||||||
|
it('should support batch image generation with maxImagesPerCall', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A test image', n: 10, maxImagesPerCall: 5 })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A test image',
|
||||||
|
n: 10,
|
||||||
|
maxImagesPerCall: 5
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support retries with maxRetries', async () => {
|
||||||
|
await executor.generateImage({ model: 'dall-e-3', prompt: 'A test image', maxRetries: 3 })
|
||||||
|
|
||||||
|
expect(aiGenerateImage).toHaveBeenCalledWith({
|
||||||
|
model: mockImageModel,
|
||||||
|
prompt: 'A test image',
|
||||||
|
maxRetries: 3
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle warnings from the model', async () => {
|
||||||
|
const resultWithWarnings = {
|
||||||
|
...mockGenerateImageResult,
|
||||||
|
warnings: [
|
||||||
|
{
|
||||||
|
type: 'unsupported-setting',
|
||||||
|
message: 'Size parameter not supported for this model'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(aiGenerateImage).mockResolvedValue(resultWithWarnings)
|
||||||
|
|
||||||
|
const result = await executor.generateImage({
|
||||||
|
model: 'dall-e-3',
|
||||||
|
prompt: 'A test image',
|
||||||
|
size: '2048x2048' // Unsupported size
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.warnings).toHaveLength(1)
|
||||||
|
expect(result.warnings[0].type).toBe('unsupported-setting')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should provide access to provider metadata', async () => {
|
||||||
|
const result = await executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
||||||
|
|
||||||
|
expect(result.providerMetadata).toBeDefined()
|
||||||
|
expect(result.providerMetadata.openai).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should provide response metadata', async () => {
|
||||||
|
const resultWithMetadata = {
|
||||||
|
...mockGenerateImageResult,
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
timestamp: new Date(),
|
||||||
|
modelId: 'dall-e-3',
|
||||||
|
headers: { 'x-request-id': 'test-123' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(aiGenerateImage).mockResolvedValue(resultWithMetadata)
|
||||||
|
|
||||||
|
const result = await executor.generateImage({ model: 'dall-e-3', prompt: 'A test image' })
|
||||||
|
|
||||||
|
expect(result.responses).toHaveLength(1)
|
||||||
|
expect(result.responses[0].modelId).toBe('dall-e-3')
|
||||||
|
expect(result.responses[0].headers).toEqual({ 'x-request-id': 'test-123' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Error classes for runtime operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when image generation fails
|
||||||
|
*/
|
||||||
|
export class ImageGenerationError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public providerId?: string,
|
||||||
|
public modelId?: string,
|
||||||
|
public cause?: Error
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ImageGenerationError'
|
||||||
|
|
||||||
|
// Maintain proper stack trace (for V8 engines)
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, ImageGenerationError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when model resolution fails during image generation
|
||||||
|
*/
|
||||||
|
export class ImageModelResolutionError extends ImageGenerationError {
|
||||||
|
constructor(modelId: string, providerId?: string, cause?: Error) {
|
||||||
|
super(
|
||||||
|
`Failed to resolve image model: ${modelId}${providerId ? ` for provider: ${providerId}` : ''}`,
|
||||||
|
providerId,
|
||||||
|
modelId,
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
this.name = 'ImageModelResolutionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
/**
|
||||||
|
* 运行时执行器
|
||||||
|
* 专注于插件化的AI调用处理
|
||||||
|
*/
|
||||||
|
import { ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
import {
|
||||||
|
experimental_generateImage as _generateImage,
|
||||||
|
generateObject as _generateObject,
|
||||||
|
generateText as _generateText,
|
||||||
|
LanguageModel,
|
||||||
|
streamObject as _streamObject,
|
||||||
|
streamText as _streamText
|
||||||
|
} from 'ai'
|
||||||
|
|
||||||
|
import { globalModelResolver } from '../models'
|
||||||
|
import { type ModelConfig } from '../models/types'
|
||||||
|
import { type AiPlugin, type AiRequestContext, definePlugin } from '../plugins'
|
||||||
|
import { type ProviderId } from '../providers'
|
||||||
|
import { ImageGenerationError, ImageModelResolutionError } from './errors'
|
||||||
|
import { PluginEngine } from './pluginEngine'
|
||||||
|
import type {
|
||||||
|
generateImageParams,
|
||||||
|
generateObjectParams,
|
||||||
|
generateTextParams,
|
||||||
|
RuntimeConfig,
|
||||||
|
streamObjectParams,
|
||||||
|
streamTextParams
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export class RuntimeExecutor<T extends ProviderId = ProviderId> {
|
||||||
|
public pluginEngine: PluginEngine<T>
|
||||||
|
// private options: ProviderSettingsMap[T]
|
||||||
|
private config: RuntimeConfig<T>
|
||||||
|
|
||||||
|
constructor(config: RuntimeConfig<T>) {
|
||||||
|
// if (!isProviderSupported(config.providerId)) {
|
||||||
|
// throw new Error(`Unsupported provider: ${config.providerId}`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 存储options供后续使用
|
||||||
|
// this.options = config.options
|
||||||
|
this.config = config
|
||||||
|
// 创建插件客户端
|
||||||
|
this.pluginEngine = new PluginEngine(config.providerId, config.plugins || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
private createResolveModelPlugin(middlewares?: LanguageModelV2Middleware[]) {
|
||||||
|
return definePlugin({
|
||||||
|
name: '_internal_resolveModel',
|
||||||
|
enforce: 'post',
|
||||||
|
|
||||||
|
resolveModel: async (modelId: string) => {
|
||||||
|
// 注意:extraModelConfig 暂时不支持,已在新架构中移除
|
||||||
|
return await this.resolveModel(modelId, middlewares)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private createResolveImageModelPlugin() {
|
||||||
|
return definePlugin({
|
||||||
|
name: '_internal_resolveImageModel',
|
||||||
|
enforce: 'post',
|
||||||
|
|
||||||
|
resolveModel: async (modelId: string) => {
|
||||||
|
return await this.resolveImageModel(modelId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private createConfigureContextPlugin() {
|
||||||
|
return definePlugin({
|
||||||
|
name: '_internal_configureContext',
|
||||||
|
configureContext: async (context: AiRequestContext) => {
|
||||||
|
context.executor = this
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 高阶重载:直接使用模型 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式文本生成
|
||||||
|
*/
|
||||||
|
async streamText(
|
||||||
|
params: streamTextParams,
|
||||||
|
options?: {
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
}
|
||||||
|
): Promise<ReturnType<typeof _streamText>> {
|
||||||
|
const { model } = params
|
||||||
|
|
||||||
|
// 根据 model 类型决定插件配置
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
this.pluginEngine.usePlugins([
|
||||||
|
this.createResolveModelPlugin(options?.middlewares),
|
||||||
|
this.createConfigureContextPlugin()
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pluginEngine.executeStreamWithPlugins(
|
||||||
|
'streamText',
|
||||||
|
params,
|
||||||
|
(resolvedModel, transformedParams, streamTransforms) => {
|
||||||
|
const experimental_transform =
|
||||||
|
params?.experimental_transform ?? (streamTransforms.length > 0 ? streamTransforms : undefined)
|
||||||
|
|
||||||
|
return _streamText({
|
||||||
|
...transformedParams,
|
||||||
|
model: resolvedModel,
|
||||||
|
experimental_transform
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 其他方法的重载 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文本
|
||||||
|
*/
|
||||||
|
async generateText(
|
||||||
|
params: generateTextParams,
|
||||||
|
options?: {
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
}
|
||||||
|
): Promise<ReturnType<typeof _generateText>> {
|
||||||
|
const { model } = params
|
||||||
|
|
||||||
|
// 根据 model 类型决定插件配置
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
this.pluginEngine.usePlugins([
|
||||||
|
this.createResolveModelPlugin(options?.middlewares),
|
||||||
|
this.createConfigureContextPlugin()
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pluginEngine.executeWithPlugins<Parameters<typeof _generateText>[0], ReturnType<typeof _generateText>>(
|
||||||
|
'generateText',
|
||||||
|
params,
|
||||||
|
(resolvedModel, transformedParams) => _generateText({ ...transformedParams, model: resolvedModel })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成结构化对象
|
||||||
|
*/
|
||||||
|
async generateObject(
|
||||||
|
params: generateObjectParams,
|
||||||
|
options?: {
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
}
|
||||||
|
): Promise<ReturnType<typeof _generateObject>> {
|
||||||
|
const { model } = params
|
||||||
|
|
||||||
|
// 根据 model 类型决定插件配置
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
this.pluginEngine.usePlugins([
|
||||||
|
this.createResolveModelPlugin(options?.middlewares),
|
||||||
|
this.createConfigureContextPlugin()
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pluginEngine.executeWithPlugins<generateObjectParams, ReturnType<typeof _generateObject>>(
|
||||||
|
'generateObject',
|
||||||
|
params,
|
||||||
|
async (resolvedModel, transformedParams) => _generateObject({ ...transformedParams, model: resolvedModel })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式生成结构化对象
|
||||||
|
*/
|
||||||
|
streamObject(
|
||||||
|
params: streamObjectParams,
|
||||||
|
options?: {
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
}
|
||||||
|
): Promise<ReturnType<typeof _streamObject>> {
|
||||||
|
const { model } = params
|
||||||
|
|
||||||
|
// 根据 model 类型决定插件配置
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
this.pluginEngine.usePlugins([
|
||||||
|
this.createResolveModelPlugin(options?.middlewares),
|
||||||
|
this.createConfigureContextPlugin()
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pluginEngine.executeStreamWithPlugins('streamObject', params, (resolvedModel, transformedParams) =>
|
||||||
|
_streamObject({ ...transformedParams, model: resolvedModel })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成图像
|
||||||
|
*/
|
||||||
|
generateImage(params: generateImageParams): Promise<ReturnType<typeof _generateImage>> {
|
||||||
|
try {
|
||||||
|
const { model } = params
|
||||||
|
|
||||||
|
// 根据 model 类型决定插件配置
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
this.pluginEngine.usePlugins([this.createResolveImageModelPlugin(), this.createConfigureContextPlugin()])
|
||||||
|
} else {
|
||||||
|
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.pluginEngine.executeImageWithPlugins('generateImage', params, (resolvedModel, transformedParams) =>
|
||||||
|
_generateImage({ ...transformedParams, model: resolvedModel })
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const modelId = typeof params.model === 'string' ? params.model : params.model.modelId
|
||||||
|
throw new ImageGenerationError(
|
||||||
|
`Failed to generate image: ${error.message}`,
|
||||||
|
this.config.providerId,
|
||||||
|
modelId,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 辅助方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析模型:如果是字符串则创建模型,如果是模型则直接返回
|
||||||
|
*/
|
||||||
|
private async resolveModel(
|
||||||
|
modelOrId: LanguageModel,
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<LanguageModelV2> {
|
||||||
|
if (typeof modelOrId === 'string') {
|
||||||
|
// 🎯 字符串modelId,使用新的ModelResolver解析,传递完整参数
|
||||||
|
return await globalModelResolver.resolveLanguageModel(
|
||||||
|
modelOrId, // 支持 'gpt-4' 和 'aihubmix:anthropic:claude-3.5-sonnet'
|
||||||
|
this.config.providerId, // fallback provider
|
||||||
|
this.config.providerSettings, // provider options
|
||||||
|
middlewares // 中间件数组
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 已经是模型,直接返回
|
||||||
|
return modelOrId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析图像模型:如果是字符串则创建图像模型,如果是模型则直接返回
|
||||||
|
*/
|
||||||
|
private async resolveImageModel(modelOrId: ImageModelV2 | string): Promise<ImageModelV2> {
|
||||||
|
try {
|
||||||
|
if (typeof modelOrId === 'string') {
|
||||||
|
// 字符串modelId,使用新的ModelResolver解析
|
||||||
|
return await globalModelResolver.resolveImageModel(
|
||||||
|
modelOrId, // 支持 'dall-e-3' 和 'aihubmix:openai:dall-e-3'
|
||||||
|
this.config.providerId // fallback provider
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 已经是模型,直接返回
|
||||||
|
return modelOrId
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new ImageModelResolutionError(
|
||||||
|
typeof modelOrId === 'string' ? modelOrId : modelOrId.modelId,
|
||||||
|
this.config.providerId,
|
||||||
|
error instanceof Error ? error : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 静态工厂方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建执行器 - 支持已知provider的类型安全
|
||||||
|
*/
|
||||||
|
static create<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ModelConfig<T>['providerSettings'],
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
): RuntimeExecutor<T> {
|
||||||
|
return new RuntimeExecutor({
|
||||||
|
providerId,
|
||||||
|
providerSettings: options,
|
||||||
|
plugins
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建OpenAI Compatible执行器
|
||||||
|
*/
|
||||||
|
static createOpenAICompatible(
|
||||||
|
options: ModelConfig<'openai-compatible'>['providerSettings'],
|
||||||
|
plugins: AiPlugin[] = []
|
||||||
|
): RuntimeExecutor<'openai-compatible'> {
|
||||||
|
return new RuntimeExecutor({
|
||||||
|
providerId: 'openai-compatible',
|
||||||
|
providerSettings: options,
|
||||||
|
plugins
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* Runtime 模块导出
|
||||||
|
* 专注于运行时插件化AI调用处理
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 主要的运行时执行器
|
||||||
|
export { RuntimeExecutor } from './executor'
|
||||||
|
|
||||||
|
// 导出类型
|
||||||
|
export type { RuntimeConfig } from './types'
|
||||||
|
|
||||||
|
// === 便捷工厂函数 ===
|
||||||
|
|
||||||
|
import { LanguageModelV2Middleware } from '@ai-sdk/provider'
|
||||||
|
|
||||||
|
import { type AiPlugin } from '../plugins'
|
||||||
|
import { type ProviderId, type ProviderSettingsMap } from '../providers/types'
|
||||||
|
import { RuntimeExecutor } from './executor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建运行时执行器 - 支持类型安全的已知provider
|
||||||
|
*/
|
||||||
|
export function createExecutor<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
): RuntimeExecutor<T> {
|
||||||
|
return RuntimeExecutor.create(providerId, options, plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建OpenAI Compatible执行器
|
||||||
|
*/
|
||||||
|
export function createOpenAICompatibleExecutor(
|
||||||
|
options: ProviderSettingsMap['openai-compatible'] & { mode?: 'chat' | 'responses' },
|
||||||
|
plugins: AiPlugin[] = []
|
||||||
|
): RuntimeExecutor<'openai-compatible'> {
|
||||||
|
return RuntimeExecutor.createOpenAICompatible(options, plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 直接调用API(无需创建executor实例)===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接流式文本生成 - 支持middlewares
|
||||||
|
*/
|
||||||
|
export async function streamText<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
params: Parameters<RuntimeExecutor<T>['streamText']>[0],
|
||||||
|
plugins?: AiPlugin[],
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<ReturnType<RuntimeExecutor<T>['streamText']>> {
|
||||||
|
const executor = createExecutor(providerId, options, plugins)
|
||||||
|
return executor.streamText(params, { middlewares })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接生成文本 - 支持middlewares
|
||||||
|
*/
|
||||||
|
export async function generateText<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
params: Parameters<RuntimeExecutor<T>['generateText']>[0],
|
||||||
|
plugins?: AiPlugin[],
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<ReturnType<RuntimeExecutor<T>['generateText']>> {
|
||||||
|
const executor = createExecutor(providerId, options, plugins)
|
||||||
|
return executor.generateText(params, { middlewares })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接生成结构化对象 - 支持middlewares
|
||||||
|
*/
|
||||||
|
export async function generateObject<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
params: Parameters<RuntimeExecutor<T>['generateObject']>[0],
|
||||||
|
plugins?: AiPlugin[],
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<ReturnType<RuntimeExecutor<T>['generateObject']>> {
|
||||||
|
const executor = createExecutor(providerId, options, plugins)
|
||||||
|
return executor.generateObject(params, { middlewares })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接流式生成结构化对象 - 支持middlewares
|
||||||
|
*/
|
||||||
|
export async function streamObject<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
params: Parameters<RuntimeExecutor<T>['streamObject']>[0],
|
||||||
|
plugins?: AiPlugin[],
|
||||||
|
middlewares?: LanguageModelV2Middleware[]
|
||||||
|
): Promise<ReturnType<RuntimeExecutor<T>['streamObject']>> {
|
||||||
|
const executor = createExecutor(providerId, options, plugins)
|
||||||
|
return executor.streamObject(params, { middlewares })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接生成图像 - 支持middlewares
|
||||||
|
*/
|
||||||
|
export async function generateImage<T extends ProviderId>(
|
||||||
|
providerId: T,
|
||||||
|
options: ProviderSettingsMap[T] & { mode?: 'chat' | 'responses' },
|
||||||
|
params: Parameters<RuntimeExecutor<T>['generateImage']>[0],
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
): Promise<ReturnType<RuntimeExecutor<T>['generateImage']>> {
|
||||||
|
const executor = createExecutor(providerId, options, plugins)
|
||||||
|
return executor.generateImage(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Agent 功能预留 ===
|
||||||
|
// 未来将在 ../agents/ 文件夹中添加:
|
||||||
|
// - AgentExecutor.ts
|
||||||
|
// - WorkflowManager.ts
|
||||||
|
// - ConversationManager.ts
|
||||||
|
// 并在此处导出相关API
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
/* eslint-disable @eslint-react/naming-convention/context-name */
|
||||||
|
import { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import { experimental_generateImage, generateObject, generateText, LanguageModel, streamObject, streamText } from 'ai'
|
||||||
|
|
||||||
|
import { type AiPlugin, createContext, PluginManager } from '../plugins'
|
||||||
|
import { type ProviderId } from '../providers/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件增强的 AI 客户端
|
||||||
|
* 专注于插件处理,不暴露用户API
|
||||||
|
*/
|
||||||
|
export class PluginEngine<T extends ProviderId = ProviderId> {
|
||||||
|
private pluginManager: PluginManager
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly providerId: T,
|
||||||
|
// private readonly options: ProviderSettingsMap[T],
|
||||||
|
plugins: AiPlugin[] = []
|
||||||
|
) {
|
||||||
|
this.pluginManager = new PluginManager(plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加插件
|
||||||
|
*/
|
||||||
|
use(plugin: AiPlugin): this {
|
||||||
|
this.pluginManager.use(plugin)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量添加插件
|
||||||
|
*/
|
||||||
|
usePlugins(plugins: AiPlugin[]): this {
|
||||||
|
plugins.forEach((plugin) => this.use(plugin))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除插件
|
||||||
|
*/
|
||||||
|
removePlugin(pluginName: string): this {
|
||||||
|
this.pluginManager.remove(pluginName)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取插件统计
|
||||||
|
*/
|
||||||
|
getPluginStats() {
|
||||||
|
return this.pluginManager.getStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有插件
|
||||||
|
*/
|
||||||
|
getPlugins() {
|
||||||
|
return this.pluginManager.getPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行带插件的操作(非流式)
|
||||||
|
* 提供给AiExecutor使用
|
||||||
|
*/
|
||||||
|
async executeWithPlugins<
|
||||||
|
TParams extends Parameters<typeof generateText | typeof generateObject>[0],
|
||||||
|
TResult extends ReturnType<typeof generateText | typeof generateObject>
|
||||||
|
>(
|
||||||
|
methodName: string,
|
||||||
|
params: TParams,
|
||||||
|
executor: (model: LanguageModel, transformedParams: TParams) => TResult,
|
||||||
|
_context?: ReturnType<typeof createContext>
|
||||||
|
): Promise<TResult> {
|
||||||
|
// 统一处理模型解析
|
||||||
|
let resolvedModel: LanguageModel | undefined
|
||||||
|
let modelId: string
|
||||||
|
const { model } = params
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
// 字符串:需要通过插件解析
|
||||||
|
modelId = model
|
||||||
|
} else {
|
||||||
|
// 模型对象:直接使用
|
||||||
|
resolvedModel = model
|
||||||
|
modelId = model.modelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正确的createContext创建请求上下文
|
||||||
|
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||||
|
|
||||||
|
// 🔥 为上下文添加递归调用能力
|
||||||
|
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||||
|
// 递归调用自身,重新走完整的插件流程
|
||||||
|
context.isRecursiveCall = true
|
||||||
|
const result = await this.executeWithPlugins(methodName, newParams, executor, context)
|
||||||
|
context.isRecursiveCall = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 0. 配置上下文
|
||||||
|
await this.pluginManager.executeConfigureContext(context)
|
||||||
|
|
||||||
|
// 1. 触发请求开始事件
|
||||||
|
await this.pluginManager.executeParallel('onRequestStart', context)
|
||||||
|
|
||||||
|
// 2. 解析模型(如果是字符串)
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
const resolved = await this.pluginManager.executeFirst<LanguageModel>('resolveModel', modelId, context)
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error(`Failed to resolve model: ${modelId}`)
|
||||||
|
}
|
||||||
|
resolvedModel = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedModel) {
|
||||||
|
throw new Error(`Model resolution failed: no model available`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换请求参数
|
||||||
|
const transformedParams = await this.pluginManager.executeSequential('transformParams', params, context)
|
||||||
|
|
||||||
|
// 4. 执行具体的 API 调用
|
||||||
|
const result = await executor(resolvedModel, transformedParams)
|
||||||
|
|
||||||
|
// 5. 转换结果(对于非流式调用)
|
||||||
|
const transformedResult = await this.pluginManager.executeSequential('transformResult', result, context)
|
||||||
|
|
||||||
|
// 6. 触发完成事件
|
||||||
|
await this.pluginManager.executeParallel('onRequestEnd', context, transformedResult)
|
||||||
|
|
||||||
|
return transformedResult
|
||||||
|
} catch (error) {
|
||||||
|
// 7. 触发错误事件
|
||||||
|
await this.pluginManager.executeParallel('onError', context, undefined, error as Error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行带插件的图像生成操作
|
||||||
|
* 提供给AiExecutor使用
|
||||||
|
*/
|
||||||
|
async executeImageWithPlugins<
|
||||||
|
TParams extends Omit<Parameters<typeof experimental_generateImage>[0], 'model'> & { model: string | ImageModelV2 },
|
||||||
|
TResult extends ReturnType<typeof experimental_generateImage>
|
||||||
|
>(
|
||||||
|
methodName: string,
|
||||||
|
params: TParams,
|
||||||
|
executor: (model: ImageModelV2, transformedParams: TParams) => TResult,
|
||||||
|
_context?: ReturnType<typeof createContext>
|
||||||
|
): Promise<TResult> {
|
||||||
|
// 统一处理模型解析
|
||||||
|
let resolvedModel: ImageModelV2 | undefined
|
||||||
|
let modelId: string
|
||||||
|
const { model } = params
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
// 字符串:需要通过插件解析
|
||||||
|
modelId = model
|
||||||
|
} else {
|
||||||
|
// 模型对象:直接使用
|
||||||
|
resolvedModel = model
|
||||||
|
modelId = model.modelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用正确的createContext创建请求上下文
|
||||||
|
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||||
|
|
||||||
|
// 🔥 为上下文添加递归调用能力
|
||||||
|
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||||
|
// 递归调用自身,重新走完整的插件流程
|
||||||
|
context.isRecursiveCall = true
|
||||||
|
const result = await this.executeImageWithPlugins(methodName, newParams, executor, context)
|
||||||
|
context.isRecursiveCall = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 0. 配置上下文
|
||||||
|
await this.pluginManager.executeConfigureContext(context)
|
||||||
|
|
||||||
|
// 1. 触发请求开始事件
|
||||||
|
await this.pluginManager.executeParallel('onRequestStart', context)
|
||||||
|
|
||||||
|
// 2. 解析模型(如果是字符串)
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
const resolved = await this.pluginManager.executeFirst<ImageModelV2>('resolveModel', modelId, context)
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error(`Failed to resolve image model: ${modelId}`)
|
||||||
|
}
|
||||||
|
resolvedModel = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedModel) {
|
||||||
|
throw new Error(`Image model resolution failed: no model available`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换请求参数
|
||||||
|
const transformedParams = await this.pluginManager.executeSequential('transformParams', params, context)
|
||||||
|
|
||||||
|
// 4. 执行具体的 API 调用
|
||||||
|
const result = await executor(resolvedModel, transformedParams)
|
||||||
|
|
||||||
|
// 5. 转换结果
|
||||||
|
const transformedResult = await this.pluginManager.executeSequential('transformResult', result, context)
|
||||||
|
|
||||||
|
// 6. 触发完成事件
|
||||||
|
await this.pluginManager.executeParallel('onRequestEnd', context, transformedResult)
|
||||||
|
|
||||||
|
return transformedResult
|
||||||
|
} catch (error) {
|
||||||
|
// 7. 触发错误事件
|
||||||
|
await this.pluginManager.executeParallel('onError', context, undefined, error as Error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行流式调用的通用逻辑(支持流转换器)
|
||||||
|
* 提供给AiExecutor使用
|
||||||
|
*/
|
||||||
|
async executeStreamWithPlugins<
|
||||||
|
TParams extends Parameters<typeof streamText | typeof streamObject>[0],
|
||||||
|
TResult extends ReturnType<typeof streamText | typeof streamObject>
|
||||||
|
>(
|
||||||
|
methodName: string,
|
||||||
|
params: TParams,
|
||||||
|
executor: (model: LanguageModel, transformedParams: TParams, streamTransforms: any[]) => TResult,
|
||||||
|
_context?: ReturnType<typeof createContext>
|
||||||
|
): Promise<TResult> {
|
||||||
|
// 统一处理模型解析
|
||||||
|
let resolvedModel: LanguageModel | undefined
|
||||||
|
let modelId: string
|
||||||
|
const { model } = params
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
// 字符串:需要通过插件解析
|
||||||
|
modelId = model
|
||||||
|
} else {
|
||||||
|
// 模型对象:直接使用
|
||||||
|
resolvedModel = model
|
||||||
|
modelId = model.modelId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建请求上下文
|
||||||
|
const context = _context ? _context : createContext(this.providerId, model, params)
|
||||||
|
|
||||||
|
// 🔥 为上下文添加递归调用能力
|
||||||
|
context.recursiveCall = async (newParams: any): Promise<TResult> => {
|
||||||
|
// 递归调用自身,重新走完整的插件流程
|
||||||
|
context.isRecursiveCall = true
|
||||||
|
const result = await this.executeStreamWithPlugins(methodName, newParams, executor, context)
|
||||||
|
context.isRecursiveCall = false
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 0. 配置上下文
|
||||||
|
await this.pluginManager.executeConfigureContext(context)
|
||||||
|
|
||||||
|
// 1. 触发请求开始事件
|
||||||
|
await this.pluginManager.executeParallel('onRequestStart', context)
|
||||||
|
|
||||||
|
// 2. 解析模型(如果是字符串)
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
const resolved = await this.pluginManager.executeFirst<LanguageModel>('resolveModel', modelId, context)
|
||||||
|
if (!resolved) {
|
||||||
|
throw new Error(`Failed to resolve model: ${modelId}`)
|
||||||
|
}
|
||||||
|
resolvedModel = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolvedModel) {
|
||||||
|
throw new Error(`Model resolution failed: no model available`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转换请求参数
|
||||||
|
const transformedParams = await this.pluginManager.executeSequential('transformParams', params, context)
|
||||||
|
|
||||||
|
// 4. 收集流转换器
|
||||||
|
const streamTransforms = this.pluginManager.collectStreamTransforms(transformedParams, context)
|
||||||
|
|
||||||
|
// 5. 执行流式 API 调用
|
||||||
|
const result = await executor(resolvedModel, transformedParams, streamTransforms)
|
||||||
|
|
||||||
|
const transformedResult = await this.pluginManager.executeSequential('transformResult', result, context)
|
||||||
|
|
||||||
|
// 6. 触发完成事件(注意:对于流式调用,这里触发的是开始流式响应的事件)
|
||||||
|
await this.pluginManager.executeParallel('onRequestEnd', context, transformedResult)
|
||||||
|
|
||||||
|
return transformedResult
|
||||||
|
} catch (error) {
|
||||||
|
// 7. 触发错误事件
|
||||||
|
await this.pluginManager.executeParallel('onError', context, undefined, error as Error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Runtime 层类型定义
|
||||||
|
*/
|
||||||
|
import { ImageModelV2 } from '@ai-sdk/provider'
|
||||||
|
import { experimental_generateImage, generateObject, generateText, streamObject, streamText } from 'ai'
|
||||||
|
|
||||||
|
import { type ModelConfig } from '../models/types'
|
||||||
|
import { type AiPlugin } from '../plugins'
|
||||||
|
import { type ProviderId } from '../providers/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时执行器配置
|
||||||
|
*/
|
||||||
|
export interface RuntimeConfig<T extends ProviderId = ProviderId> {
|
||||||
|
providerId: T
|
||||||
|
providerSettings: ModelConfig<T>['providerSettings'] & { mode?: 'chat' | 'responses' }
|
||||||
|
plugins?: AiPlugin[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type generateImageParams = Omit<Parameters<typeof experimental_generateImage>[0], 'model'> & {
|
||||||
|
model: string | ImageModelV2
|
||||||
|
}
|
||||||
|
export type generateObjectParams = Parameters<typeof generateObject>[0]
|
||||||
|
export type generateTextParams = Parameters<typeof generateText>[0]
|
||||||
|
export type streamObjectParams = Parameters<typeof streamObject>[0]
|
||||||
|
export type streamTextParams = Parameters<typeof streamText>[0]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Cherry Studio AI Core Package
|
||||||
|
* 基于 Vercel AI SDK 的统一 AI Provider 接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导入内部使用的类和函数
|
||||||
|
|
||||||
|
// ==================== 主要用户接口 ====================
|
||||||
|
export {
|
||||||
|
createExecutor,
|
||||||
|
createOpenAICompatibleExecutor,
|
||||||
|
generateImage,
|
||||||
|
generateObject,
|
||||||
|
generateText,
|
||||||
|
streamText
|
||||||
|
} from './core/runtime'
|
||||||
|
|
||||||
|
// ==================== 高级API ====================
|
||||||
|
export { globalModelResolver as modelResolver } from './core/models'
|
||||||
|
|
||||||
|
// ==================== 插件系统 ====================
|
||||||
|
export type { AiPlugin, AiRequestContext, HookResult, PluginManagerConfig } from './core/plugins'
|
||||||
|
export { createContext, definePlugin, PluginManager } from './core/plugins'
|
||||||
|
// export { createPromptToolUsePlugin, webSearchPlugin } from './core/plugins/built-in'
|
||||||
|
export { PluginEngine } from './core/runtime/pluginEngine'
|
||||||
|
|
||||||
|
// ==================== AI SDK 常用类型导出 ====================
|
||||||
|
// 直接导出 AI SDK 的常用类型,方便使用
|
||||||
|
export type { LanguageModelV2Middleware, LanguageModelV2StreamPart } from '@ai-sdk/provider'
|
||||||
|
export type { ToolCall } from '@ai-sdk/provider-utils'
|
||||||
|
export type { ReasoningPart } from '@ai-sdk/provider-utils'
|
||||||
|
|
||||||
|
// ==================== 选项 ====================
|
||||||
|
export {
|
||||||
|
createAnthropicOptions,
|
||||||
|
createGoogleOptions,
|
||||||
|
createOpenAIOptions,
|
||||||
|
type ExtractProviderOptions,
|
||||||
|
mergeProviderOptions,
|
||||||
|
type ProviderOptionsMap,
|
||||||
|
type TypedProviderOptions
|
||||||
|
} from './core/options'
|
||||||
|
|
||||||
|
// ==================== 包信息 ====================
|
||||||
|
export const AI_CORE_VERSION = '1.0.0'
|
||||||
|
export const AI_CORE_NAME = '@cherrystudio/ai-core'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// 重新导出插件类型
|
||||||
|
export type { AiPlugin, AiRequestContext, HookResult, PluginManagerConfig } from './core/plugins/types'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noEmitOnError": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"rootDir": "./src",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ES2020"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'tsdown'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: {
|
||||||
|
index: 'src/index.ts',
|
||||||
|
'built-in/plugins/index': 'src/core/plugins/built-in/index.ts',
|
||||||
|
'provider/index': 'src/core/providers/index.ts'
|
||||||
|
},
|
||||||
|
outDir: 'dist',
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
clean: true,
|
||||||
|
dts: true,
|
||||||
|
tsconfig: 'tsconfig.json'
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': './src'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
target: 'node18'
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -67,13 +67,13 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@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",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"tsdown": "^0.13.3"
|
"tsdown": "^0.13.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsdown",
|
||||||
"lint": "prettier ./src/ --write && eslint --fix ./src/"
|
"lint": "biome format ./src/ --write && eslint --fix ./src/"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.9.1"
|
"packageManager": "yarn@4.9.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ export enum IpcChannel {
|
|||||||
App_InstallBunBinary = 'app:install-bun-binary',
|
App_InstallBunBinary = 'app:install-bun-binary',
|
||||||
App_LogToMain = 'app:log-to-main',
|
App_LogToMain = 'app:log-to-main',
|
||||||
App_SaveData = 'app:save-data',
|
App_SaveData = 'app:save-data',
|
||||||
|
App_GetDiskInfo = 'app:get-disk-info',
|
||||||
App_SetFullScreen = 'app:set-full-screen',
|
App_SetFullScreen = 'app:set-full-screen',
|
||||||
App_IsFullScreen = 'app:is-full-screen',
|
App_IsFullScreen = 'app:is-full-screen',
|
||||||
|
App_GetSystemFonts = 'app:get-system-fonts',
|
||||||
|
|
||||||
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
|
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
|
||||||
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
|
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
|
||||||
@@ -83,10 +85,13 @@ export enum IpcChannel {
|
|||||||
Mcp_UploadDxt = 'mcp:upload-dxt',
|
Mcp_UploadDxt = 'mcp:upload-dxt',
|
||||||
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',
|
||||||
// Python
|
// Python
|
||||||
Python_Execute = 'python:execute',
|
Python_Execute = 'python:execute',
|
||||||
|
|
||||||
|
// agent messages
|
||||||
|
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
|
||||||
|
|
||||||
//copilot
|
//copilot
|
||||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||||
@@ -123,6 +128,12 @@ export enum IpcChannel {
|
|||||||
Windows_SetMinimumSize = 'window:set-minimum-size',
|
Windows_SetMinimumSize = 'window:set-minimum-size',
|
||||||
Windows_Resize = 'window:resize',
|
Windows_Resize = 'window:resize',
|
||||||
Windows_GetSize = 'window:get-size',
|
Windows_GetSize = 'window:get-size',
|
||||||
|
Windows_Minimize = 'window:minimize',
|
||||||
|
Windows_Maximize = 'window:maximize',
|
||||||
|
Windows_Unmaximize = 'window:unmaximize',
|
||||||
|
Windows_Close = 'window:close',
|
||||||
|
Windows_IsMaximized = 'window:is-maximized',
|
||||||
|
Windows_MaximizedChanged = 'window:maximized-changed',
|
||||||
|
|
||||||
KnowledgeBase_Create = 'knowledge-base:create',
|
KnowledgeBase_Create = 'knowledge-base:create',
|
||||||
KnowledgeBase_Reset = 'knowledge-base:reset',
|
KnowledgeBase_Reset = 'knowledge-base:reset',
|
||||||
@@ -296,6 +307,21 @@ export enum IpcChannel {
|
|||||||
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
|
||||||
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
|
TRACE_ADD_STREAM_MESSAGE = 'trace:addStreamMessage',
|
||||||
|
|
||||||
|
// API Server
|
||||||
|
ApiServer_Start = 'api-server:start',
|
||||||
|
ApiServer_Stop = 'api-server:stop',
|
||||||
|
ApiServer_Restart = 'api-server:restart',
|
||||||
|
ApiServer_GetStatus = 'api-server:get-status',
|
||||||
|
ApiServer_GetConfig = 'api-server:get-config',
|
||||||
|
|
||||||
|
// Anthropic OAuth
|
||||||
|
Anthropic_StartOAuthFlow = 'anthropic:start-oauth-flow',
|
||||||
|
Anthropic_CompleteOAuthWithCode = 'anthropic:complete-oauth-with-code',
|
||||||
|
Anthropic_CancelOAuthFlow = 'anthropic:cancel-oauth-flow',
|
||||||
|
Anthropic_GetAccessToken = 'anthropic:get-access-token',
|
||||||
|
Anthropic_HasCredentials = 'anthropic:has-credentials',
|
||||||
|
Anthropic_ClearCredentials = 'anthropic:clear-credentials',
|
||||||
|
|
||||||
// CodeTools
|
// CodeTools
|
||||||
CodeTools_Run = 'code-tools:run',
|
CodeTools_Run = 'code-tools:run',
|
||||||
|
|
||||||
|
|||||||
@@ -216,5 +216,6 @@ export enum codeTools {
|
|||||||
qwenCode = 'qwen-code',
|
qwenCode = 'qwen-code',
|
||||||
claudeCode = 'claude-code',
|
claudeCode = 'claude-code',
|
||||||
geminiCli = 'gemini-cli',
|
geminiCli = 'gemini-cli',
|
||||||
openaiCodex = 'openai-codex'
|
openaiCodex = 'openai-codex',
|
||||||
|
iFlowCli = 'iflow-cli'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type LoaderReturn = {
|
|||||||
loaderType: string
|
loaderType: string
|
||||||
status?: ProcessingStatus
|
status?: ProcessingStatus
|
||||||
message?: string
|
message?: string
|
||||||
messageSource?: 'preprocess' | 'embedding'
|
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
||||||
@@ -17,3 +17,8 @@ export type FileChangeEvent = {
|
|||||||
filePath: string
|
filePath: string
|
||||||
watchPath: string
|
watchPath: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MCPProgressEvent = {
|
||||||
|
callId: string
|
||||||
|
progress: number // 0-1 range
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const defaultAppHeaders = () => {
|
||||||
|
return {
|
||||||
|
'HTTP-Referer': 'https://cherry-ai.com',
|
||||||
|
'X-Title': 'Cherry Studio'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,199 +1,274 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>许可协议 | License Agreement</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body class="bg-gray-50">
|
||||||
<meta charset="UTF-8">
|
<div class="mx-auto max-w-4xl px-4 py-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<!-- 中文版本 -->
|
||||||
<title>许可协议 | License Agreement</title>
|
<div class="mb-12">
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<h1 class="mb-8 text-3xl font-bold text-gray-900">许可协议</h1>
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-gray-50">
|
<p class="mb-6 text-gray-700">
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。
|
||||||
<!-- 中文版本 -->
|
|
||||||
<div class="mb-12">
|
|
||||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
|
||||||
|
|
||||||
<p class="mb-6 text-gray-700">本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。</p>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">核心原则</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li><strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用 <strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。</li>
|
|
||||||
<li><strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取 <strong>商业许可证 (Commercial License)</strong>。</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">定义:"10人及以下"</h2>
|
|
||||||
<p class="text-gray-700">
|
|
||||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
|
||||||
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
|
||||||
</p>
|
</p>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">核心原则</h2>
|
||||||
</h2>
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
<li>
|
||||||
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
<strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用
|
||||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
<strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。
|
||||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a> 获取。
|
</li>
|
||||||
</li>
|
<li>
|
||||||
<li><strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
<strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取
|
||||||
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。</li>
|
<strong>商业许可证 (Commercial License)</strong>。
|
||||||
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">定义:"10人及以下"</h2>
|
||||||
义务的用户</h2>
|
<p class="text-gray-700">
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
||||||
<li><strong>强制要求:</strong>
|
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||||
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
</p>
|
||||||
Cherry Studio。</li>
|
</section>
|
||||||
<li><strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong>无法满足 AGPLv3
|
|
||||||
的条款要求</strong>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3 <strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
|
||||||
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。</li>
|
|
||||||
<li><strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
|
||||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
|
||||||
<li>您的组织规模超过10人。</li>
|
|
||||||
<li>(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3 公开您修改部分的源代码。</li>
|
|
||||||
<li>(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据 AGPLv3 向服务使用者提供修改后的源代码。</li>
|
|
||||||
<li>(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>获取商业许可:</strong> 请通过邮箱 <a href="mailto:bd@cherry-ai.com"
|
|
||||||
class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry Studio 开发团队洽谈商业授权事宜。</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. 贡献 (Contributions)</h2>
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
||||||
<li>我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 <strong>AGPLv3</strong> 许可证下提供。</li>
|
</h2>
|
||||||
<li>通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。</li>
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
<li>
|
||||||
</ul>
|
如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在
|
||||||
</section>
|
<strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
||||||
|
<a href="https://www.gnu.org/licenses/agpl-3.0.html" class="text-blue-600 hover:underline"
|
||||||
|
>https://www.gnu.org/licenses/agpl-3.0.html</a
|
||||||
|
>
|
||||||
|
获取。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio
|
||||||
|
并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
||||||
|
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。
|
||||||
|
</li>
|
||||||
|
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. 其他条款 (Other Terms)</h2>
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3 义务的用户
|
||||||
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
</h2>
|
||||||
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
</ul>
|
<li>
|
||||||
</section>
|
<strong>强制要求:</strong>
|
||||||
|
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
||||||
|
Cherry Studio。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong
|
||||||
|
>无法满足 AGPLv3 的条款要求</strong
|
||||||
|
>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3
|
||||||
|
<strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
||||||
|
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
||||||
|
<ul class="mt-2 list-disc space-y-1 pl-6">
|
||||||
|
<li>您的组织规模超过10人。</li>
|
||||||
|
<li>
|
||||||
|
(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3
|
||||||
|
公开您修改部分的源代码。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据
|
||||||
|
AGPLv3 向服务使用者提供修改后的源代码。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>获取商业许可:</strong> 请通过邮箱
|
||||||
|
<a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry
|
||||||
|
Studio 开发团队洽谈商业授权事宜。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">3. 贡献 (Contributions)</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在
|
||||||
|
<strong>AGPLv3</strong> 许可证下提供。
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3
|
||||||
|
许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。
|
||||||
|
</li>
|
||||||
|
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">4. 其他条款 (Other Terms)</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
||||||
|
<li>
|
||||||
|
项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-12 border-gray-300" />
|
||||||
|
|
||||||
|
<!-- English Version -->
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-8 text-3xl font-bold text-gray-900">Licensing</h1>
|
||||||
|
|
||||||
|
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">Core Principle</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
<strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
||||||
|
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
||||||
|
<strong>Commercial License</strong>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
||||||
|
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly
|
||||||
|
benefit from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is
|
||||||
|
not limited to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||||
|
1. Open Source License: AGPLv3 - For Individuals and Organizations of 10 or Fewer
|
||||||
|
</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
||||||
|
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
||||||
|
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at
|
||||||
|
<a href="https://www.gnu.org/licenses/agpl-3.0.html" class="text-blue-600 hover:underline"
|
||||||
|
>https://www.gnu.org/licenses/agpl-3.0.html</a
|
||||||
|
>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
||||||
|
make it available over a network, or distribute the modified version, you must provide the
|
||||||
|
<strong>complete corresponding source code</strong> under the AGPLv3 license to the recipients. Even if
|
||||||
|
you qualify under the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure
|
||||||
|
obligation, you will need to obtain a Commercial License (see below).
|
||||||
|
</li>
|
||||||
|
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">
|
||||||
|
2. Commercial License - For Organizations with More Than 10 Individuals, or Users Needing to Avoid AGPLv3
|
||||||
|
Obligations
|
||||||
|
</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
<strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
||||||
|
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
||||||
|
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
||||||
|
Studio.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
||||||
|
condition, if your intended use case
|
||||||
|
<strong>cannot comply with the terms of the AGPLv3</strong> (particularly the obligations regarding
|
||||||
|
<strong>source code disclosure</strong>), or if you require specific commercial terms
|
||||||
|
<strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom from copyleft
|
||||||
|
restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial License.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
||||||
|
<ul class="mt-2 list-disc space-y-1 pl-6">
|
||||||
|
<li>
|
||||||
|
Your organization has more than 10 individuals who can access, use, or benefit from the software.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
||||||
|
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
||||||
|
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
||||||
|
of the service under AGPLv3.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
||||||
|
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and
|
||||||
|
confidentiality.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
||||||
|
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
||||||
|
discuss commercial licensing options.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">3. Contributions</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
||||||
|
considered to be offered under the <strong>AGPLv3</strong> license.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
||||||
|
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
||||||
|
operate under AGPLv3 or a Commercial License).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
||||||
|
offered under our commercial license.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">4. Other Terms</h2>
|
||||||
|
<ul class="list-disc space-y-2 pl-6 text-gray-700">
|
||||||
|
<li>
|
||||||
|
The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
||||||
|
agreement signed by both parties.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The project maintainers reserve the right to update this licensing policy (including the definition and
|
||||||
|
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
||||||
|
code repository, official website).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</body>
|
||||||
<hr class="my-12 border-gray-300">
|
</html>
|
||||||
|
|
||||||
<!-- English Version -->
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">Licensing</h1>
|
|
||||||
|
|
||||||
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Core Principle</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li><strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
|
||||||
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.</li>
|
|
||||||
<li><strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
|
||||||
<strong>Commercial License</strong>.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
|
||||||
<p class="text-gray-700">
|
|
||||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
|
||||||
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit
|
|
||||||
from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited
|
|
||||||
to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. Open Source License: AGPLv3 - For Individuals and
|
|
||||||
Organizations of 10 or Fewer</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li>If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
|
||||||
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
|
||||||
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at <a
|
|
||||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
|
||||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a>.
|
|
||||||
</li>
|
|
||||||
<li><strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
|
||||||
make it available over a network, or distribute the modified version, you must provide the <strong>complete
|
|
||||||
corresponding source code</strong> under the AGPLv3 license to the recipients. Even if you qualify under
|
|
||||||
the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will
|
|
||||||
need to obtain a Commercial License (see below).</li>
|
|
||||||
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. Commercial License - For Organizations with More Than 10
|
|
||||||
Individuals, or Users Needing to Avoid AGPLv3 Obligations</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li><strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
|
||||||
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
|
||||||
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
|
||||||
Studio.</li>
|
|
||||||
<li><strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
|
||||||
condition, if your intended use case <strong>cannot comply with the terms of the AGPLv3</strong>
|
|
||||||
(particularly the obligations regarding <strong>source code disclosure</strong>), or if you require specific
|
|
||||||
commercial terms <strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom
|
|
||||||
from copyleft restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial
|
|
||||||
License.</li>
|
|
||||||
<li><strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
|
||||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
|
||||||
<li>Your organization has more than 10 individuals who can access, use, or benefit from the software.</li>
|
|
||||||
<li>(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
|
||||||
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
|
||||||
</li>
|
|
||||||
<li>(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
|
||||||
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
|
||||||
of the service under AGPLv3.</li>
|
|
||||||
<li>(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
|
||||||
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li><strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
|
||||||
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
|
||||||
discuss commercial licensing options.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. Contributions</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li>We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
|
||||||
considered to be offered under the <strong>AGPLv3</strong> license.</li>
|
|
||||||
<li>By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
|
||||||
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
|
||||||
operate under AGPLv3 or a Commercial License).</li>
|
|
||||||
<li>You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
|
||||||
offered under our commercial license.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. Other Terms</h2>
|
|
||||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
|
||||||
<li>The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
|
||||||
agreement signed by both parties.</li>
|
|
||||||
<li>The project maintainers reserve the right to update this licensing policy (including the definition and
|
|
||||||
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
|
||||||
code repository, official website).</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -12,18 +12,18 @@
|
|||||||
|
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
|
||||||
<div class="max-w-3xl mx-auto py-12 px-4">
|
<div class="mx-auto max-w-3xl px-4 py-12">
|
||||||
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
<h1 class="mb-8 text-3xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
|
||||||
|
|
||||||
<!-- Loading状态 -->
|
<!-- Loading状态 -->
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="py-8 text-center">
|
||||||
<div
|
<div
|
||||||
class="inline-block animate-spin rounded-full h-8 w-8 border-4"
|
class="inline-block h-8 w-8 animate-spin rounded-full border-4"
|
||||||
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
:class="isDark ? 'border-gray-700 border-t-blue-500' : 'border-gray-300 border-t-blue-500'"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error 状态 -->
|
<!-- Error 状态 -->
|
||||||
<div v-else-if="error" class="text-red-500 text-center py-8">{{ error }}</div>
|
<div v-else-if="error" class="py-8 text-center text-red-500">{{ error }}</div>
|
||||||
|
|
||||||
<!-- Release 列表 -->
|
<!-- Release 列表 -->
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-8">
|
||||||
@@ -32,21 +32,21 @@
|
|||||||
:key="release.id"
|
:key="release.id"
|
||||||
class="relative pl-8"
|
class="relative pl-8"
|
||||||
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
:class="isDark ? 'border-l-2 border-gray-700' : 'border-l-2 border-gray-200'">
|
||||||
<div class="absolute -left-2 top-0 w-4 h-4 rounded-full bg-green-500"></div>
|
<div class="absolute top-0 -left-2 h-4 w-4 rounded-full bg-green-500"></div>
|
||||||
<div
|
<div
|
||||||
class="rounded-lg shadow-sm p-6 transition-shadow"
|
class="rounded-lg p-6 shadow-sm transition-shadow"
|
||||||
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
:class="isDark ? 'bg-black hover:shadow-md hover:shadow-black' : 'bg-white hover:shadow-md'">
|
||||||
<div class="flex items-start justify-between mb-4">
|
<div class="mb-4 flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||||
{{ release.name || release.tag_name }}
|
{{ release.name || release.tag_name }}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm mt-1" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
<p class="mt-1 text-sm" :class="isDark ? 'text-gray-400' : 'text-gray-500'">
|
||||||
{{ formatDate(release.published_at) }}
|
{{ formatDate(release.published_at) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium"
|
||||||
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
:class="isDark ? 'bg-green-900 text-green-200' : 'bg-green-100 text-green-800'">
|
||||||
{{ release.tag_name }}
|
{{ release.tag_name }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+3120
-5976
File diff suppressed because one or more lines are too long
@@ -0,0 +1,53 @@
|
|||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `migrations` (
|
||||||
|
`version` integer PRIMARY KEY NOT NULL,
|
||||||
|
`tag` text NOT NULL,
|
||||||
|
`executed_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `agents` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`accessible_paths` text,
|
||||||
|
`instructions` text,
|
||||||
|
`model` text NOT NULL,
|
||||||
|
`plan_model` text,
|
||||||
|
`small_model` text,
|
||||||
|
`mcps` text,
|
||||||
|
`allowed_tools` text,
|
||||||
|
`configuration` text,
|
||||||
|
`created_at` text NOT NULL,
|
||||||
|
`updated_at` text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`agent_type` text NOT NULL,
|
||||||
|
`agent_id` text NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`description` text,
|
||||||
|
`accessible_paths` text,
|
||||||
|
`instructions` text,
|
||||||
|
`model` text NOT NULL,
|
||||||
|
`plan_model` text,
|
||||||
|
`small_model` text,
|
||||||
|
`mcps` text,
|
||||||
|
`allowed_tools` text,
|
||||||
|
`configuration` text,
|
||||||
|
`created_at` text NOT NULL,
|
||||||
|
`updated_at` text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session_messages` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`session_id` text NOT NULL,
|
||||||
|
`role` text NOT NULL,
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`metadata` text,
|
||||||
|
`created_at` text NOT NULL,
|
||||||
|
`updated_at` text NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `session_messages` ADD `agent_session_id` text DEFAULT '';
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "35efb412-0230-4767-9c76-7b7c4d40369f",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"tables": {
|
||||||
|
"agents": {
|
||||||
|
"name": "agents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"accessible_paths": {
|
||||||
|
"name": "accessible_paths",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "instructions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"plan_model": {
|
||||||
|
"name": "plan_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"small_model": {
|
||||||
|
"name": "small_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mcps": {
|
||||||
|
"name": "mcps",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"name": "allowed_tools",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"name": "configuration",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session_messages": {
|
||||||
|
"name": "session_messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"migrations": {
|
||||||
|
"name": "migrations",
|
||||||
|
"columns": {
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"name": "tag",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"executed_at": {
|
||||||
|
"name": "executed_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"agent_type": {
|
||||||
|
"name": "agent_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"agent_id": {
|
||||||
|
"name": "agent_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"accessible_paths": {
|
||||||
|
"name": "accessible_paths",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "instructions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"plan_model": {
|
||||||
|
"name": "plan_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"small_model": {
|
||||||
|
"name": "small_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mcps": {
|
||||||
|
"name": "mcps",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"name": "allowed_tools",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"name": "configuration",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "dabab6db-a2cd-4e96-b06e-6cb87d445a87",
|
||||||
|
"prevId": "35efb412-0230-4767-9c76-7b7c4d40369f",
|
||||||
|
"tables": {
|
||||||
|
"agents": {
|
||||||
|
"name": "agents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"accessible_paths": {
|
||||||
|
"name": "accessible_paths",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "instructions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"plan_model": {
|
||||||
|
"name": "plan_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"small_model": {
|
||||||
|
"name": "small_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mcps": {
|
||||||
|
"name": "mcps",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"name": "allowed_tools",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"name": "configuration",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"session_messages": {
|
||||||
|
"name": "session_messages",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"agent_session_id": {
|
||||||
|
"name": "agent_session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"migrations": {
|
||||||
|
"name": "migrations",
|
||||||
|
"columns": {
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"name": "tag",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"executed_at": {
|
||||||
|
"name": "executed_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"name": "sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"agent_type": {
|
||||||
|
"name": "agent_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"agent_id": {
|
||||||
|
"name": "agent_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"accessible_paths": {
|
||||||
|
"name": "accessible_paths",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "instructions",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"plan_model": {
|
||||||
|
"name": "plan_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"small_model": {
|
||||||
|
"name": "small_model",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mcps": {
|
||||||
|
"name": "mcps",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"allowed_tools": {
|
||||||
|
"name": "allowed_tools",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"name": "configuration",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user