Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2

This commit is contained in:
fullex
2026-04-11 04:40:00 -07:00
19 changed files with 94 additions and 108 deletions

View File

@@ -1,4 +0,0 @@
---
---
Bump AI SDK dependencies and fix provider API host formatting

View File

@@ -1,10 +0,0 @@
---
'@cherrystudio/ai-core': patch
---
fix(providers): azure-anthropic variant uses correct Anthropic toolFactories for web search
- Add `TOutput` generic to `ProviderVariant` so `transform` output type flows to `toolFactories` and `resolveModel`
- Add Anthropic-specific `toolFactories` to `azure-anthropic` variant (fixes `provider.tools.webSearchPreview is not a function`)
- Fix `urlContext` factory incorrectly mapping to `webSearch` tool key instead of `urlContext`
- Fix `BedrockExtension` `satisfies` type to use `AmazonBedrockProvider` instead of `ProviderV3`

View File

@@ -144,7 +144,7 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
<!--LANG:en-->
Cherry Studio 1.9.0 - CherryClaw Agent & Skills System
Cherry Studio 1.9.1 - CherryClaw Agent & Skills System
⚠️ Breaking Changes
- [Agents] "Plugins" system renamed to "Skills". Plugin marketplace has been replaced by a unified Skills management interface
@@ -190,7 +190,7 @@ releaseInfo:
- [Agent] Improve session switch experience
<!--LANG:zh-CN-->
Cherry Studio 1.9.0 - CherryClaw 智能体与技能系统
Cherry Studio 1.9.1 - CherryClaw 智能体与技能系统
⚠️ 破坏性变更
- [智能体] "插件"系统更名为"技能Skills",插件市场已替换为统一的技能管理界面

View File

@@ -1,5 +1,16 @@
# @cherrystudio/ai-core
## 2.0.1
### Patch Changes
- [#14087](https://github.com/CherryHQ/cherry-studio/pull/14087) [`1f72f98`](https://github.com/CherryHQ/cherry-studio/commit/1f72f9890508c6fc0bc95793e286cf61b991c51c) Thanks [@DeJeune](https://github.com/DeJeune)! - fix(providers): azure-anthropic variant uses correct Anthropic toolFactories for web search
- Add `TOutput` generic to `ProviderVariant` so `transform` output type flows to `toolFactories` and `resolveModel`
- Add Anthropic-specific `toolFactories` to `azure-anthropic` variant (fixes `provider.tools.webSearchPreview is not a function`)
- Fix `urlContext` factory incorrectly mapping to `webSearch` tool key instead of `urlContext`
- Fix `BedrockExtension` `satisfies` type to use `AmazonBedrockProvider` instead of `ProviderV3`
## 2.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@cherrystudio/ai-core",
"version": "2.0.0",
"version": "2.0.1",
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
"main": "dist/index.js",
"module": "dist/index.mjs",

View File

@@ -14,7 +14,7 @@ import { crossPlatformSpawn, findExecutableInEnv, getBinaryPath, runInstallScrip
import getShellEnv, { refreshShellEnv } from '@main/utils/shell-env'
import type { OperationResult } from '@shared/config/types'
import { IpcChannel } from '@shared/IpcChannel'
import { hasAPIVersion, withoutTrailingSlash } from '@shared/utils'
import { formatApiHost, hasAPIVersion, withoutTrailingSlash } from '@shared/utils'
import type { Model, Provider, ProviderType, VertexProvider } from '@types'
import { parseCurrentVersion, parseUpdateStatus } from './utils/openClawParsers'
@@ -715,7 +715,9 @@ export class OpenClawService extends BaseService {
}
let url = `http://127.0.0.1:${this.gatewayPort}`
if (this.gatewayAuthToken) {
url += `#token=${encodeURIComponent(this.gatewayAuthToken)}`
// Use query string (not URL fragment) so dashboard app state can persist correctly.
// Fragment (#...) is often used by SPAs for transient client-side state.
url += `?token=${encodeURIComponent(this.gatewayAuthToken)}`
}
return url
}
@@ -1055,6 +1057,14 @@ export class OpenClawService extends BaseService {
* - Others: {host}/v1
*/
private formatOpenAIUrl(provider: Provider): string {
// Special-case built-in GitHub / Copilot providers: these hosts should
// not have a `/v1` suffix appended by default (renderer applies
// `formatApiHost(..., false)` for these). Mirror that behavior here
// to avoid constructing incorrect endpoints that return 404.
if (provider.id === 'copilot' || provider.id === 'github') {
return formatApiHost(provider.apiHost, false)
}
const url = withoutTrailingSlash(provider.apiHost)
const providerType = provider.type

View File

@@ -99,6 +99,16 @@ describe('OpenClawService gateway status state machine', () => {
vi.restoreAllMocks()
})
describe('getDashboardUrl', () => {
it('uses query string token to preserve dashboard UI state', () => {
// @ts-expect-error -- accessing private field for testing
service.gatewayAuthToken = 'a b+c'
const url = service.getDashboardUrl()
expect(url).toBe(`http://127.0.0.1:18790?token=${encodeURIComponent('a b+c')}`)
})
})
// ─── getStatus ───────────────────────────────────────────────
describe('getStatus', () => {

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "No skills installed",
"noResults": "No skills found",
"noSkillFile": "No SKILL.md found",
"searchPlaceholder": "Search skills across registries...",
"searchPlaceholder": "Discover more skills...",
"searchRegistryTitle": "Search skill registries online",
"searchTitle": "Search Skills",
"selectFile": "Select a file to view",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "暂无已安装的技能",
"noResults": "未找到技能",
"noSkillFile": "未找到 SKILL.md 文件",
"searchPlaceholder": "跨注册表搜索技能...",
"searchPlaceholder": "发现更多技能...",
"searchRegistryTitle": "在线搜索技能注册表",
"searchTitle": "搜索技能",
"selectFile": "选择一个文件查看",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "暫無已安裝的技能",
"noResults": "未找到技能",
"noSkillFile": "未找到 SKILL.md 檔案",
"searchPlaceholder": "跨登錄檔搜尋技能...",
"searchPlaceholder": "發現更多技能...",
"searchRegistryTitle": "在線搜尋技能登錄檔",
"searchTitle": "搜尋技能",
"selectFile": "選擇一個檔案查看",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Keine Fähigkeiten installiert",
"noResults": "Keine Fähigkeiten gefunden",
"noSkillFile": "Keine SKILL.md gefunden",
"searchPlaceholder": "Suchfähigkeiten über Registrierungsstellen hinweg...",
"searchPlaceholder": "Weitere Fähigkeiten entdecken...",
"searchRegistryTitle": "Durchsuche Fähigkeitsregister online",
"searchTitle": "Suchfähigkeiten",
"selectFile": "Wählen Sie eine Datei zum Anzeigen",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Δεν έχουν εγκατασταθεί δεξιότητες",
"noResults": "Δεν βρέθηκαν δεξιότητες",
"noSkillFile": "Δεν βρέθηκε SKILL.md",
"searchPlaceholder": "Αναζήτηση δεξιοτήτων στα μητρώα...",
"searchPlaceholder": "Ανακαλύψτε περισσότερες δεξιότητες...",
"searchRegistryTitle": "Αναζητήστε μητρώα δεξιοτήτων στο διαδίκτυο",
"searchTitle": "Δεξιότητες Αναζήτησης",
"selectFile": "Επιλέξτε ένα αρχείο για προβολή",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Sin habilidades instaladas",
"noResults": "Sin habilidades encontradas",
"noSkillFile": "No se encontró SKILL.md",
"searchPlaceholder": "Buscar habilidades en los registros...",
"searchPlaceholder": "Descubrir más habilidades...",
"searchRegistryTitle": "Buscar registros de habilidades en línea",
"searchTitle": "Habilidades de búsqueda",
"selectFile": "Selecciona un archivo para ver",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Aucune compétence installée",
"noResults": "Aucune compétence trouvée",
"noSkillFile": "Aucun SKILL.md trouvé",
"searchPlaceholder": "Rechercher des compétences dans les registres...",
"searchPlaceholder": "Découvrir plus de compétences...",
"searchRegistryTitle": "Rechercher des registres de compétences en ligne",
"searchTitle": "Compétences de recherche",
"selectFile": "Sélectionner un fichier à afficher",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "スキルがインストールされていません",
"noResults": "スキルが見つかりません",
"noSkillFile": "SKILL.mdが見つかりません",
"searchPlaceholder": "レジストリ全体でスキルを検索...",
"searchPlaceholder": "さらにスキルを見つける...",
"searchRegistryTitle": "オンラインでスキルレジストリを検索してください",
"searchTitle": "検索スキル",
"selectFile": "ファイルを選択して表示",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Nenhuma habilidade instalada",
"noResults": "Nenhuma habilidade encontrada",
"noSkillFile": "Nenhum SKILL.md encontrado",
"searchPlaceholder": "Pesquisar competências nos registros...",
"searchPlaceholder": "Descobrir mais competências...",
"searchRegistryTitle": "Procure registros de habilidades online",
"searchTitle": "Habilidades de Pesquisa",
"selectFile": "Selecione um arquivo para visualizar",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Nicio competență instalată",
"noResults": "Nicio competență găsită",
"noSkillFile": "Nu s-a găsit SKILL.md",
"searchPlaceholder": "Caută competențe în registre...",
"searchPlaceholder": "Descoperă mai multe competențe...",
"searchRegistryTitle": "Caută registre de competențe online",
"searchTitle": "Abilități de căutare",
"selectFile": "Selectați un fișier pentru vizualizare",

View File

@@ -5608,7 +5608,7 @@
"noInstalled": "Навыков не установлено",
"noResults": "Навыки не найдены",
"noSkillFile": "Файл SKILL.md не найден",
"searchPlaceholder": "Поиск навыков в реестрах...",
"searchPlaceholder": "Откройте больше навыков...",
"searchRegistryTitle": "Ищите реестры навыков онлайн",
"searchTitle": "Навыки поиска",
"selectFile": "Выберите файл для просмотра",

View File

@@ -227,7 +227,6 @@ const SkillsSettings: FC = () => {
const [expandedDirs, setExpandedDirs] = useState<Set<string>>(new Set())
// Search state (online registry)
const [searchOpen, setSearchOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const searchInputRef = useRef<HTMLInputElement>(null)
const searchContainerRef = useRef<HTMLDivElement>(null)
@@ -293,27 +292,18 @@ const SkillsSettings: FC = () => {
// Close search dropdown on outside click (but not when clicking inside a modal)
useEffect(() => {
if (!searchOpen) return
const handler = (e: MouseEvent) => {
const target = e.target as Node
if (searchContainerRef.current && !searchContainerRef.current.contains(target)) {
const modal = (target as Element).closest?.('.ant-modal-root, .ant-modal-wrap, .ant-modal')
if (modal) return
setSearchOpen(false)
setSearchQuery('')
clear()
}
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [searchOpen, clear])
// Focus input when search opens
useEffect(() => {
if (searchOpen) {
setTimeout(() => searchInputRef.current?.focus(), 50)
}
}, [searchOpen])
}, [clear])
// Filtered skills list
const filteredSkills = useMemo(() => {
@@ -466,9 +456,9 @@ const SkillsSettings: FC = () => {
}, [selectedFile])
const handleCloseSearch = useCallback(() => {
setSearchOpen(false)
setSearchQuery('')
clear()
searchInputRef.current?.blur()
}, [clear])
const handleZipInstall = useCallback(
@@ -671,60 +661,51 @@ const SkillsSettings: FC = () => {
) : null}
</DetailMeta>
) : null}
{searchOpen ? (
<SearchInputWrapper>
<Input
ref={searchInputRef as React.Ref<any>}
size="small"
placeholder={t('settings.skills.searchPlaceholder')}
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
suffix={<X size={12} style={CLOSE_ICON_STYLE} onClick={handleCloseSearch} />}
prefix={<Search size={12} />}
/>
{searching || results.length > 0 || (searchQuery && !searching) ? (
<SearchDropdown>
<SearchTabs>
{SEARCH_SOURCES.map((source) => {
const count = tabCounts.get(source) ?? 0
return (
<SearchTab key={source} $active={searchTab === source} onClick={() => setSearchTab(source)}>
{source.replace('.dev', '').replace('.ai', '')}
{count > 0 ? <TabCount>{count}</TabCount> : null}
</SearchTab>
)
})}
</SearchTabs>
<SearchResultsScroll>
{searching ? (
<DropdownLoading>
<Spin size="small" />
</DropdownLoading>
) : null}
{!searching && searchQuery && filteredResults.length === 0 ? (
<DropdownEmpty>{t('settings.skills.noResults')}</DropdownEmpty>
) : null}
{filteredResults.map((result) => (
<SearchResultRow
key={`${result.sourceRegistry}:${result.slug}`}
result={result}
isInstalling={isInstalling}
onInstall={handleInstall}
onPreview={setPreviewResult}
installLabel={t('settings.skills.install')}
/>
))}
</SearchResultsScroll>
</SearchDropdown>
) : null}
</SearchInputWrapper>
) : (
<Tooltip title={t('settings.skills.searchRegistryTitle')}>
<SearchIconButton onClick={() => setSearchOpen(true)}>
<Search size={16} />
</SearchIconButton>
</Tooltip>
)}
<SearchInputWrapper>
<Input
ref={searchInputRef as React.Ref<any>}
placeholder={t('settings.skills.searchPlaceholder')}
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
suffix={searchQuery ? <X size={12} style={CLOSE_ICON_STYLE} onClick={handleCloseSearch} /> : <span />}
prefix={<Search size={12} />}
/>
{searching || results.length > 0 || (searchQuery && !searching) ? (
<SearchDropdown>
<SearchTabs>
{SEARCH_SOURCES.map((source) => {
const count = tabCounts.get(source) ?? 0
return (
<SearchTab key={source} $active={searchTab === source} onClick={() => setSearchTab(source)}>
{source.replace('.dev', '').replace('.ai', '')}
{count > 0 ? <TabCount>{count}</TabCount> : null}
</SearchTab>
)
})}
</SearchTabs>
<SearchResultsScroll>
{searching ? (
<DropdownLoading>
<Spin size="small" />
</DropdownLoading>
) : null}
{!searching && searchQuery && filteredResults.length === 0 ? (
<DropdownEmpty>{t('settings.skills.noResults')}</DropdownEmpty>
) : null}
{filteredResults.map((result) => (
<SearchResultRow
key={`${result.sourceRegistry}:${result.slug}`}
result={result}
isInstalling={isInstalling}
onInstall={handleInstall}
onPreview={setPreviewResult}
installLabel={t('settings.skills.install')}
/>
))}
</SearchResultsScroll>
</SearchDropdown>
) : null}
</SearchInputWrapper>
</TopBarRight>
</TopBar>
@@ -934,18 +915,6 @@ const DetailMeta = styled.div`
gap: 6px;
`
const SearchIconButton = styled.div`
display: flex;
align-items: center;
cursor: pointer;
padding: 4px;
border-radius: 4px;
color: var(--color-text-2);
&:hover {
background: var(--color-background-soft);
}
`
const SearchInputWrapper = styled.div`
position: relative;
width: 280px;