mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
Merge branch 'main' into betterSqlite
This commit is contained in:
commit
67e032344b
10
package.json
10
package.json
@ -74,9 +74,10 @@
|
|||||||
"format:check": "biome format && biome lint",
|
"format:check": "biome format && biome lint",
|
||||||
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
|
||||||
"claude": "dotenv -e .env -- claude",
|
"claude": "dotenv -e .env -- claude",
|
||||||
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --preid alpha --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
|
||||||
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --preid beta --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
|
||||||
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public",
|
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core build && yarn workspace @cherrystudio/ai-core npm publish --access public",
|
||||||
|
"release:ai-sdk-provider": "yarn workspace @cherrystudio/ai-sdk-provider version patch --immediate && yarn workspace @cherrystudio/ai-sdk-provider build && yarn workspace @cherrystudio/ai-sdk-provider npm publish --access public",
|
||||||
"rebuild": "electron-rebuild -f -w better-sqlite3",
|
"rebuild": "electron-rebuild -f -w better-sqlite3",
|
||||||
"postinstall": "electron-builder install-app-deps"
|
"postinstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
@ -118,6 +119,7 @@
|
|||||||
"@ai-sdk/google-vertex": "^3.0.68",
|
"@ai-sdk/google-vertex": "^3.0.68",
|
||||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch",
|
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch",
|
||||||
"@ai-sdk/mistral": "^2.0.23",
|
"@ai-sdk/mistral": "^2.0.23",
|
||||||
|
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch",
|
||||||
"@ai-sdk/perplexity": "^2.0.17",
|
"@ai-sdk/perplexity": "^2.0.17",
|
||||||
"@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",
|
||||||
@ -126,7 +128,7 @@
|
|||||||
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
|
||||||
"@aws-sdk/client-s3": "^3.910.0",
|
"@aws-sdk/client-s3": "^3.910.0",
|
||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
|
"@cherrystudio/ai-core": "workspace:^1.0.9",
|
||||||
"@cherrystudio/embedjs": "^0.1.31",
|
"@cherrystudio/embedjs": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cherrystudio/ai-sdk-provider",
|
"name": "@cherrystudio/ai-sdk-provider",
|
||||||
"version": "0.1.0",
|
"version": "0.1.2",
|
||||||
"description": "Cherry Studio AI SDK provider bundle with CherryIN routing.",
|
"description": "Cherry Studio AI SDK provider bundle with CherryIN routing.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ai-sdk",
|
"ai-sdk",
|
||||||
|
|||||||
@ -71,7 +71,7 @@ Cherry Studio AI Core 是一个基于 Vercel AI SDK 的统一 AI Provider 接口
|
|||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @cherrystudio/ai-core ai
|
npm install @cherrystudio/ai-core ai @ai-sdk/google @ai-sdk/openai
|
||||||
```
|
```
|
||||||
|
|
||||||
### React Native
|
### React Native
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cherrystudio/ai-core",
|
"name": "@cherrystudio/ai-core",
|
||||||
"version": "1.0.1",
|
"version": "1.0.9",
|
||||||
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
"description": "Cherry Studio AI Core - Unified AI Provider Interface Based on Vercel AI SDK",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
@ -33,19 +33,19 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/CherryHQ/cherry-studio#readme",
|
"homepage": "https://github.com/CherryHQ/cherry-studio#readme",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@ai-sdk/google": "^2.0.36",
|
||||||
|
"@ai-sdk/openai": "^2.0.64",
|
||||||
|
"@cherrystudio/ai-sdk-provider": "^0.1.2",
|
||||||
"ai": "^5.0.26"
|
"ai": "^5.0.26"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.43",
|
"@ai-sdk/anthropic": "^2.0.43",
|
||||||
"@ai-sdk/azure": "^2.0.66",
|
"@ai-sdk/azure": "^2.0.66",
|
||||||
"@ai-sdk/deepseek": "^1.0.27",
|
"@ai-sdk/deepseek": "^1.0.27",
|
||||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch",
|
|
||||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch",
|
|
||||||
"@ai-sdk/openai-compatible": "^1.0.26",
|
"@ai-sdk/openai-compatible": "^1.0.26",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
"@ai-sdk/provider-utils": "^3.0.16",
|
"@ai-sdk/provider-utils": "^3.0.16",
|
||||||
"@ai-sdk/xai": "^2.0.31",
|
"@ai-sdk/xai": "^2.0.31",
|
||||||
"@cherrystudio/ai-sdk-provider": "workspace:*",
|
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -4,12 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
export const BUILT_IN_PLUGIN_PREFIX = 'built-in:'
|
export const BUILT_IN_PLUGIN_PREFIX = 'built-in:'
|
||||||
|
|
||||||
export { googleToolsPlugin } from './googleToolsPlugin'
|
export * from './googleToolsPlugin'
|
||||||
export { createLoggingPlugin } from './logging'
|
export * from './toolUsePlugin/promptToolUsePlugin'
|
||||||
export { createPromptToolUsePlugin } from './toolUsePlugin/promptToolUsePlugin'
|
export * from './toolUsePlugin/type'
|
||||||
export type {
|
export * from './webSearchPlugin'
|
||||||
PromptToolUseConfig,
|
|
||||||
ToolUseRequestContext,
|
|
||||||
ToolUseResult
|
|
||||||
} from './toolUsePlugin/type'
|
|
||||||
export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin'
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 导出类型定义供开发者使用
|
// 导出类型定义供开发者使用
|
||||||
export type { WebSearchPluginConfig, WebSearchToolOutputSchema } from './helper'
|
export * from './helper'
|
||||||
|
|
||||||
// 默认导出
|
// 默认导出
|
||||||
export default webSearchPlugin
|
export default webSearchPlugin
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export {
|
|||||||
// ==================== 基础数据和类型 ====================
|
// ==================== 基础数据和类型 ====================
|
||||||
|
|
||||||
// 基础Provider数据源
|
// 基础Provider数据源
|
||||||
export { baseProviderIds, baseProviders } from './schemas'
|
export { baseProviderIds, baseProviders, isBaseProvider } from './schemas'
|
||||||
|
|
||||||
// 类型定义和Schema
|
// 类型定义和Schema
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { createAzure } from '@ai-sdk/azure'
|
|||||||
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
||||||
import { createDeepSeek } from '@ai-sdk/deepseek'
|
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||||
import { createHuggingFace } from '@ai-sdk/huggingface'
|
|
||||||
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||||
import type { LanguageModelV2 } from '@ai-sdk/provider'
|
import type { LanguageModelV2 } from '@ai-sdk/provider'
|
||||||
@ -33,8 +32,7 @@ export const baseProviderIds = [
|
|||||||
'deepseek',
|
'deepseek',
|
||||||
'openrouter',
|
'openrouter',
|
||||||
'cherryin',
|
'cherryin',
|
||||||
'cherryin-chat',
|
'cherryin-chat'
|
||||||
'huggingface'
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,12 +156,6 @@ export const baseProviders = [
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
supportsImageGeneration: true
|
supportsImageGeneration: true
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'huggingface',
|
|
||||||
name: 'HuggingFace',
|
|
||||||
creator: createHuggingFace,
|
|
||||||
supportsImageGeneration: true
|
|
||||||
}
|
}
|
||||||
] as const satisfies BaseProvider[]
|
] as const satisfies BaseProvider[]
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ describe('stripLocalCommandTags', () => {
|
|||||||
|
|
||||||
describe('Claude → AiSDK transform', () => {
|
describe('Claude → AiSDK transform', () => {
|
||||||
it('handles tool call streaming lifecycle', () => {
|
it('handles tool call streaming lifecycle', () => {
|
||||||
const state = new ClaudeStreamState()
|
const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id })
|
||||||
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
|
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
|
||||||
|
|
||||||
const messages: SDKMessage[] = [
|
const messages: SDKMessage[] = [
|
||||||
@ -182,14 +182,14 @@ describe('Claude → AiSDK transform', () => {
|
|||||||
(typeof parts)[number],
|
(typeof parts)[number],
|
||||||
{ type: 'tool-result' }
|
{ type: 'tool-result' }
|
||||||
>
|
>
|
||||||
expect(toolResult.toolCallId).toBe('tool-1')
|
expect(toolResult.toolCallId).toBe('session-123:tool-1')
|
||||||
expect(toolResult.toolName).toBe('Bash')
|
expect(toolResult.toolName).toBe('Bash')
|
||||||
expect(toolResult.input).toEqual({ command: 'ls' })
|
expect(toolResult.input).toEqual({ command: 'ls' })
|
||||||
expect(toolResult.output).toBe('ok')
|
expect(toolResult.output).toBe('ok')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles streaming text completion', () => {
|
it('handles streaming text completion', () => {
|
||||||
const state = new ClaudeStreamState()
|
const state = new ClaudeStreamState({ agentSessionId: baseStreamMetadata.session_id })
|
||||||
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
|
const parts: ReturnType<typeof transformSDKMessageToStreamParts>[number][] = []
|
||||||
|
|
||||||
const messages: SDKMessage[] = [
|
const messages: SDKMessage[] = [
|
||||||
|
|||||||
@ -10,8 +10,21 @@
|
|||||||
* Every Claude turn gets its own instance. `resetStep` should be invoked once the finish event has
|
* Every Claude turn gets its own instance. `resetStep` should be invoked once the finish event has
|
||||||
* been emitted to avoid leaking state into the next turn.
|
* been emitted to avoid leaking state into the next turn.
|
||||||
*/
|
*/
|
||||||
|
import { loggerService } from '@logger'
|
||||||
import type { FinishReason, LanguageModelUsage, ProviderMetadata } from 'ai'
|
import type { FinishReason, LanguageModelUsage, ProviderMetadata } from 'ai'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a namespaced tool call ID by combining session ID with raw tool call ID.
|
||||||
|
* This ensures tool calls from different sessions don't conflict even if they have
|
||||||
|
* the same raw ID from the SDK.
|
||||||
|
*
|
||||||
|
* @param sessionId - The agent session ID
|
||||||
|
* @param rawToolCallId - The raw tool call ID from SDK (e.g., "WebFetch_0")
|
||||||
|
*/
|
||||||
|
export function buildNamespacedToolCallId(sessionId: string, rawToolCallId: string): string {
|
||||||
|
return `${sessionId}:${rawToolCallId}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared fields for every block that Claude can stream (text, reasoning, tool).
|
* Shared fields for every block that Claude can stream (text, reasoning, tool).
|
||||||
*/
|
*/
|
||||||
@ -34,6 +47,7 @@ type ReasoningBlockState = BaseBlockState & {
|
|||||||
type ToolBlockState = BaseBlockState & {
|
type ToolBlockState = BaseBlockState & {
|
||||||
kind: 'tool'
|
kind: 'tool'
|
||||||
toolCallId: string
|
toolCallId: string
|
||||||
|
rawToolCallId: string
|
||||||
toolName: string
|
toolName: string
|
||||||
inputBuffer: string
|
inputBuffer: string
|
||||||
providerMetadata?: ProviderMetadata
|
providerMetadata?: ProviderMetadata
|
||||||
@ -48,12 +62,17 @@ type PendingUsageState = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PendingToolCall = {
|
type PendingToolCall = {
|
||||||
|
rawToolCallId: string
|
||||||
toolCallId: string
|
toolCallId: string
|
||||||
toolName: string
|
toolName: string
|
||||||
input: unknown
|
input: unknown
|
||||||
providerMetadata?: ProviderMetadata
|
providerMetadata?: ProviderMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClaudeStreamStateOptions = {
|
||||||
|
agentSessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks the lifecycle of Claude streaming blocks (text, thinking, tool calls)
|
* Tracks the lifecycle of Claude streaming blocks (text, thinking, tool calls)
|
||||||
* across individual websocket events. The transformer relies on this class to
|
* across individual websocket events. The transformer relies on this class to
|
||||||
@ -61,12 +80,20 @@ type PendingToolCall = {
|
|||||||
* usage/finish metadata once Anthropic closes a message.
|
* usage/finish metadata once Anthropic closes a message.
|
||||||
*/
|
*/
|
||||||
export class ClaudeStreamState {
|
export class ClaudeStreamState {
|
||||||
|
private logger
|
||||||
|
private readonly agentSessionId: string
|
||||||
private blocksByIndex = new Map<number, BlockState>()
|
private blocksByIndex = new Map<number, BlockState>()
|
||||||
private toolIndexById = new Map<string, number>()
|
private toolIndexByNamespacedId = new Map<string, number>()
|
||||||
private pendingUsage: PendingUsageState = {}
|
private pendingUsage: PendingUsageState = {}
|
||||||
private pendingToolCalls = new Map<string, PendingToolCall>()
|
private pendingToolCalls = new Map<string, PendingToolCall>()
|
||||||
private stepActive = false
|
private stepActive = false
|
||||||
|
|
||||||
|
constructor(options: ClaudeStreamStateOptions) {
|
||||||
|
this.logger = loggerService.withContext('ClaudeStreamState')
|
||||||
|
this.agentSessionId = options.agentSessionId
|
||||||
|
this.logger.silly('ClaudeStreamState', options)
|
||||||
|
}
|
||||||
|
|
||||||
/** Marks the beginning of a new AiSDK step. */
|
/** Marks the beginning of a new AiSDK step. */
|
||||||
beginStep(): void {
|
beginStep(): void {
|
||||||
this.stepActive = true
|
this.stepActive = true
|
||||||
@ -104,19 +131,21 @@ export class ClaudeStreamState {
|
|||||||
/** Caches tool metadata so subsequent input deltas and results can find it. */
|
/** Caches tool metadata so subsequent input deltas and results can find it. */
|
||||||
openToolBlock(
|
openToolBlock(
|
||||||
index: number,
|
index: number,
|
||||||
params: { toolCallId: string; toolName: string; providerMetadata?: ProviderMetadata }
|
params: { rawToolCallId: string; toolName: string; providerMetadata?: ProviderMetadata }
|
||||||
): ToolBlockState {
|
): ToolBlockState {
|
||||||
|
const toolCallId = buildNamespacedToolCallId(this.agentSessionId, params.rawToolCallId)
|
||||||
const block: ToolBlockState = {
|
const block: ToolBlockState = {
|
||||||
kind: 'tool',
|
kind: 'tool',
|
||||||
id: params.toolCallId,
|
id: toolCallId,
|
||||||
index,
|
index,
|
||||||
toolCallId: params.toolCallId,
|
toolCallId,
|
||||||
|
rawToolCallId: params.rawToolCallId,
|
||||||
toolName: params.toolName,
|
toolName: params.toolName,
|
||||||
inputBuffer: '',
|
inputBuffer: '',
|
||||||
providerMetadata: params.providerMetadata
|
providerMetadata: params.providerMetadata
|
||||||
}
|
}
|
||||||
this.blocksByIndex.set(index, block)
|
this.blocksByIndex.set(index, block)
|
||||||
this.toolIndexById.set(params.toolCallId, index)
|
this.toolIndexByNamespacedId.set(toolCallId, index)
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,13 +154,17 @@ export class ClaudeStreamState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToolBlockById(toolCallId: string): ToolBlockState | undefined {
|
getToolBlockById(toolCallId: string): ToolBlockState | undefined {
|
||||||
const index = this.toolIndexById.get(toolCallId)
|
const index = this.toolIndexByNamespacedId.get(toolCallId)
|
||||||
if (index === undefined) return undefined
|
if (index === undefined) return undefined
|
||||||
const block = this.blocksByIndex.get(index)
|
const block = this.blocksByIndex.get(index)
|
||||||
if (!block || block.kind !== 'tool') return undefined
|
if (!block || block.kind !== 'tool') return undefined
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getToolBlockByRawId(rawToolCallId: string): ToolBlockState | undefined {
|
||||||
|
return this.getToolBlockById(buildNamespacedToolCallId(this.agentSessionId, rawToolCallId))
|
||||||
|
}
|
||||||
|
|
||||||
/** Appends streamed text to a text block, returning the updated state when present. */
|
/** Appends streamed text to a text block, returning the updated state when present. */
|
||||||
appendTextDelta(index: number, text: string): TextBlockState | undefined {
|
appendTextDelta(index: number, text: string): TextBlockState | undefined {
|
||||||
const block = this.blocksByIndex.get(index)
|
const block = this.blocksByIndex.get(index)
|
||||||
@ -158,10 +191,12 @@ export class ClaudeStreamState {
|
|||||||
|
|
||||||
/** Records a tool call to be consumed once its result arrives from the user. */
|
/** Records a tool call to be consumed once its result arrives from the user. */
|
||||||
registerToolCall(
|
registerToolCall(
|
||||||
toolCallId: string,
|
rawToolCallId: string,
|
||||||
payload: { toolName: string; input: unknown; providerMetadata?: ProviderMetadata }
|
payload: { toolName: string; input: unknown; providerMetadata?: ProviderMetadata }
|
||||||
): void {
|
): void {
|
||||||
this.pendingToolCalls.set(toolCallId, {
|
const toolCallId = buildNamespacedToolCallId(this.agentSessionId, rawToolCallId)
|
||||||
|
this.pendingToolCalls.set(rawToolCallId, {
|
||||||
|
rawToolCallId,
|
||||||
toolCallId,
|
toolCallId,
|
||||||
toolName: payload.toolName,
|
toolName: payload.toolName,
|
||||||
input: payload.input,
|
input: payload.input,
|
||||||
@ -170,10 +205,10 @@ export class ClaudeStreamState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Retrieves and clears the buffered tool call metadata for the given id. */
|
/** Retrieves and clears the buffered tool call metadata for the given id. */
|
||||||
consumePendingToolCall(toolCallId: string): PendingToolCall | undefined {
|
consumePendingToolCall(rawToolCallId: string): PendingToolCall | undefined {
|
||||||
const entry = this.pendingToolCalls.get(toolCallId)
|
const entry = this.pendingToolCalls.get(rawToolCallId)
|
||||||
if (entry) {
|
if (entry) {
|
||||||
this.pendingToolCalls.delete(toolCallId)
|
this.pendingToolCalls.delete(rawToolCallId)
|
||||||
}
|
}
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
@ -183,12 +218,12 @@ export class ClaudeStreamState {
|
|||||||
* completion so that downstream tool results can reference the original call.
|
* completion so that downstream tool results can reference the original call.
|
||||||
*/
|
*/
|
||||||
completeToolBlock(toolCallId: string, input: unknown, providerMetadata?: ProviderMetadata): void {
|
completeToolBlock(toolCallId: string, input: unknown, providerMetadata?: ProviderMetadata): void {
|
||||||
|
const block = this.getToolBlockByRawId(toolCallId)
|
||||||
this.registerToolCall(toolCallId, {
|
this.registerToolCall(toolCallId, {
|
||||||
toolName: this.getToolBlockById(toolCallId)?.toolName ?? 'unknown',
|
toolName: block?.toolName ?? 'unknown',
|
||||||
input,
|
input,
|
||||||
providerMetadata
|
providerMetadata
|
||||||
})
|
})
|
||||||
const block = this.getToolBlockById(toolCallId)
|
|
||||||
if (block) {
|
if (block) {
|
||||||
block.resolvedInput = input
|
block.resolvedInput = input
|
||||||
}
|
}
|
||||||
@ -200,7 +235,7 @@ export class ClaudeStreamState {
|
|||||||
if (!block) return undefined
|
if (!block) return undefined
|
||||||
this.blocksByIndex.delete(index)
|
this.blocksByIndex.delete(index)
|
||||||
if (block.kind === 'tool') {
|
if (block.kind === 'tool') {
|
||||||
this.toolIndexById.delete(block.toolCallId)
|
this.toolIndexByNamespacedId.delete(block.toolCallId)
|
||||||
}
|
}
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
@ -227,7 +262,7 @@ export class ClaudeStreamState {
|
|||||||
/** Drops cached block metadata for the currently active message. */
|
/** Drops cached block metadata for the currently active message. */
|
||||||
resetBlocks(): void {
|
resetBlocks(): void {
|
||||||
this.blocksByIndex.clear()
|
this.blocksByIndex.clear()
|
||||||
this.toolIndexById.clear()
|
this.toolIndexByNamespacedId.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resets the entire step lifecycle after emitting a terminal frame. */
|
/** Resets the entire step lifecycle after emitting a terminal frame. */
|
||||||
@ -236,6 +271,10 @@ export class ClaudeStreamState {
|
|||||||
this.resetPendingUsage()
|
this.resetPendingUsage()
|
||||||
this.stepActive = false
|
this.stepActive = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNamespacedToolCallId(rawToolCallId: string): string {
|
||||||
|
return buildNamespacedToolCallId(this.agentSessionId, rawToolCallId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { PendingToolCall }
|
export type { PendingToolCall }
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { app } from 'electron'
|
|||||||
import type { GetAgentSessionResponse } from '../..'
|
import type { GetAgentSessionResponse } from '../..'
|
||||||
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
|
||||||
import { sessionService } from '../SessionService'
|
import { sessionService } from '../SessionService'
|
||||||
|
import { buildNamespacedToolCallId } from './claude-stream-state'
|
||||||
import { promptForToolApproval } from './tool-permissions'
|
import { promptForToolApproval } from './tool-permissions'
|
||||||
import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform'
|
import { ClaudeStreamState, transformSDKMessageToStreamParts } from './transform'
|
||||||
|
|
||||||
@ -150,7 +151,10 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
return { behavior: 'allow', updatedInput: input }
|
return { behavior: 'allow', updatedInput: input }
|
||||||
}
|
}
|
||||||
|
|
||||||
return promptForToolApproval(toolName, input, options)
|
return promptForToolApproval(toolName, input, {
|
||||||
|
...options,
|
||||||
|
toolCallId: buildNamespacedToolCallId(session.id, options.toolUseID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build SDK options from parameters
|
// Build SDK options from parameters
|
||||||
@ -346,7 +350,7 @@ class ClaudeCodeService implements AgentServiceInterface {
|
|||||||
const jsonOutput: SDKMessage[] = []
|
const jsonOutput: SDKMessage[] = []
|
||||||
let hasCompleted = false
|
let hasCompleted = false
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
const streamState = new ClaudeStreamState()
|
const streamState = new ClaudeStreamState({ agentSessionId: sessionId })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const message of query({ prompt: promptStream, options })) {
|
for await (const message of query({ prompt: promptStream, options })) {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ type RendererPermissionRequestPayload = {
|
|||||||
requestId: string
|
requestId: string
|
||||||
toolName: string
|
toolName: string
|
||||||
toolId: string
|
toolId: string
|
||||||
|
toolCallId: string
|
||||||
description?: string
|
description?: string
|
||||||
requiresPermissions: boolean
|
requiresPermissions: boolean
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
@ -206,10 +207,19 @@ const ensureIpcHandlersRegistered = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PromptForToolApprovalOptions = {
|
||||||
|
signal: AbortSignal
|
||||||
|
suggestions?: PermissionUpdate[]
|
||||||
|
|
||||||
|
// NOTICE: This ID is namespaced with session ID, not the raw SDK tool call ID.
|
||||||
|
// Format: `${sessionId}:${rawToolCallId}`, e.g., `session_123:WebFetch_0`
|
||||||
|
toolCallId: string
|
||||||
|
}
|
||||||
|
|
||||||
export async function promptForToolApproval(
|
export async function promptForToolApproval(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
input: Record<string, unknown>,
|
input: Record<string, unknown>,
|
||||||
options?: { signal: AbortSignal; suggestions?: PermissionUpdate[] }
|
options: PromptForToolApprovalOptions
|
||||||
): Promise<PermissionResult> {
|
): Promise<PermissionResult> {
|
||||||
if (shouldAutoApproveTools) {
|
if (shouldAutoApproveTools) {
|
||||||
logger.debug('promptForToolApproval auto-approving tool for test', {
|
logger.debug('promptForToolApproval auto-approving tool for test', {
|
||||||
@ -245,6 +255,7 @@ export async function promptForToolApproval(
|
|||||||
logger.info('Requesting user approval for tool usage', {
|
logger.info('Requesting user approval for tool usage', {
|
||||||
requestId,
|
requestId,
|
||||||
toolName,
|
toolName,
|
||||||
|
toolCallId: options.toolCallId,
|
||||||
description: toolMetadata?.description
|
description: toolMetadata?.description
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -252,6 +263,7 @@ export async function promptForToolApproval(
|
|||||||
requestId,
|
requestId,
|
||||||
toolName,
|
toolName,
|
||||||
toolId: toolMetadata?.id ?? toolName,
|
toolId: toolMetadata?.id ?? toolName,
|
||||||
|
toolCallId: options.toolCallId,
|
||||||
description: toolMetadata?.description,
|
description: toolMetadata?.description,
|
||||||
requiresPermissions: toolMetadata?.requirePermissions ?? false,
|
requiresPermissions: toolMetadata?.requirePermissions ?? false,
|
||||||
input: sanitizedInput,
|
input: sanitizedInput,
|
||||||
@ -266,6 +278,7 @@ export async function promptForToolApproval(
|
|||||||
logger.debug('Registering tool permission request', {
|
logger.debug('Registering tool permission request', {
|
||||||
requestId,
|
requestId,
|
||||||
toolName,
|
toolName,
|
||||||
|
toolCallId: options.toolCallId,
|
||||||
requiresPermissions: requestPayload.requiresPermissions,
|
requiresPermissions: requestPayload.requiresPermissions,
|
||||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||||
suggestionCount: sanitizedSuggestions.length
|
suggestionCount: sanitizedSuggestions.length
|
||||||
@ -273,7 +286,11 @@ export async function promptForToolApproval(
|
|||||||
|
|
||||||
return new Promise<PermissionResult>((resolve) => {
|
return new Promise<PermissionResult>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
logger.info('User tool permission request timed out', { requestId, toolName })
|
logger.info('User tool permission request timed out', {
|
||||||
|
requestId,
|
||||||
|
toolName,
|
||||||
|
toolCallId: options.toolCallId
|
||||||
|
})
|
||||||
finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout')
|
finalizeRequest(requestId, { behavior: 'deny', message: 'Timed out waiting for approval' }, 'timeout')
|
||||||
}, TOOL_APPROVAL_TIMEOUT_MS)
|
}, TOOL_APPROVAL_TIMEOUT_MS)
|
||||||
|
|
||||||
@ -287,7 +304,11 @@ export async function promptForToolApproval(
|
|||||||
|
|
||||||
if (options?.signal) {
|
if (options?.signal) {
|
||||||
const abortListener = () => {
|
const abortListener = () => {
|
||||||
logger.info('Tool permission request aborted before user responded', { requestId, toolName })
|
logger.info('Tool permission request aborted before user responded', {
|
||||||
|
requestId,
|
||||||
|
toolName,
|
||||||
|
toolCallId: options.toolCallId
|
||||||
|
})
|
||||||
finalizeRequest(requestId, defaultDenyUpdate, 'aborted')
|
finalizeRequest(requestId, defaultDenyUpdate, 'aborted')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -243,9 +243,10 @@ function handleAssistantToolUse(
|
|||||||
state: ClaudeStreamState,
|
state: ClaudeStreamState,
|
||||||
chunks: AgentStreamPart[]
|
chunks: AgentStreamPart[]
|
||||||
): void {
|
): void {
|
||||||
|
const toolCallId = state.getNamespacedToolCallId(block.id)
|
||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'tool-call',
|
type: 'tool-call',
|
||||||
toolCallId: block.id,
|
toolCallId,
|
||||||
toolName: block.name,
|
toolName: block.name,
|
||||||
input: block.input,
|
input: block.input,
|
||||||
providerExecuted: true,
|
providerExecuted: true,
|
||||||
@ -331,10 +332,11 @@ function handleUserMessage(
|
|||||||
if (block.type === 'tool_result') {
|
if (block.type === 'tool_result') {
|
||||||
const toolResult = block as ToolResultContent
|
const toolResult = block as ToolResultContent
|
||||||
const pendingCall = state.consumePendingToolCall(toolResult.tool_use_id)
|
const pendingCall = state.consumePendingToolCall(toolResult.tool_use_id)
|
||||||
|
const toolCallId = pendingCall?.toolCallId ?? state.getNamespacedToolCallId(toolResult.tool_use_id)
|
||||||
if (toolResult.is_error) {
|
if (toolResult.is_error) {
|
||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'tool-error',
|
type: 'tool-error',
|
||||||
toolCallId: toolResult.tool_use_id,
|
toolCallId,
|
||||||
toolName: pendingCall?.toolName ?? 'unknown',
|
toolName: pendingCall?.toolName ?? 'unknown',
|
||||||
input: pendingCall?.input,
|
input: pendingCall?.input,
|
||||||
error: toolResult.content,
|
error: toolResult.content,
|
||||||
@ -343,7 +345,7 @@ function handleUserMessage(
|
|||||||
} else {
|
} else {
|
||||||
chunks.push({
|
chunks.push({
|
||||||
type: 'tool-result',
|
type: 'tool-result',
|
||||||
toolCallId: toolResult.tool_use_id,
|
toolCallId,
|
||||||
toolName: pendingCall?.toolName ?? 'unknown',
|
toolName: pendingCall?.toolName ?? 'unknown',
|
||||||
input: pendingCall?.input,
|
input: pendingCall?.input,
|
||||||
output: toolResult.content,
|
output: toolResult.content,
|
||||||
@ -514,7 +516,7 @@ function handleContentBlockStart(
|
|||||||
}
|
}
|
||||||
case 'tool_use': {
|
case 'tool_use': {
|
||||||
const block = state.openToolBlock(index, {
|
const block = state.openToolBlock(index, {
|
||||||
toolCallId: contentBlock.id,
|
rawToolCallId: contentBlock.id,
|
||||||
toolName: contentBlock.name,
|
toolName: contentBlock.name,
|
||||||
providerMetadata
|
providerMetadata
|
||||||
})
|
})
|
||||||
|
|||||||
@ -99,9 +99,6 @@ export function buildProviderOptions(
|
|||||||
serviceTier: serviceTierSetting
|
serviceTier: serviceTierSetting
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'huggingface':
|
|
||||||
providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities)
|
|
||||||
break
|
|
||||||
case 'anthropic':
|
case 'anthropic':
|
||||||
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
|
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
|
||||||
break
|
break
|
||||||
@ -144,6 +141,9 @@ export function buildProviderOptions(
|
|||||||
case 'bedrock':
|
case 'bedrock':
|
||||||
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
|
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
|
||||||
break
|
break
|
||||||
|
case 'huggingface':
|
||||||
|
providerSpecificOptions = buildOpenAIProviderOptions(assistant, model, capabilities)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
// 对于其他 provider,使用通用的构建逻辑
|
// 对于其他 provider,使用通用的构建逻辑
|
||||||
providerSpecificOptions = {
|
providerSpecificOptions = {
|
||||||
@ -162,13 +162,17 @@ export function buildProviderOptions(
|
|||||||
...getCustomParameters(assistant)
|
...getCustomParameters(assistant)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawProviderKey =
|
let rawProviderKey =
|
||||||
{
|
{
|
||||||
'google-vertex': 'google',
|
'google-vertex': 'google',
|
||||||
'google-vertex-anthropic': 'anthropic',
|
'google-vertex-anthropic': 'anthropic',
|
||||||
'ai-gateway': 'gateway'
|
'ai-gateway': 'gateway'
|
||||||
}[rawProviderId] || rawProviderId
|
}[rawProviderId] || rawProviderId
|
||||||
|
|
||||||
|
if (rawProviderKey === 'cherryin') {
|
||||||
|
rawProviderKey = { gemini: 'google' }[actualProvider.type] || actualProvider.type
|
||||||
|
}
|
||||||
|
|
||||||
// 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions }
|
// 返回 AI Core SDK 要求的格式:{ 'providerId': providerOptions }
|
||||||
return {
|
return {
|
||||||
[rawProviderKey]: providerSpecificOptions
|
[rawProviderKey]: providerSpecificOptions
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
|
||||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { permissionModeCards } from '@renderer/config/agent'
|
import { permissionModeCards } from '@renderer/config/agent'
|
||||||
@ -9,7 +8,6 @@ import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAg
|
|||||||
import type {
|
import type {
|
||||||
AddAgentForm,
|
AddAgentForm,
|
||||||
AgentEntity,
|
AgentEntity,
|
||||||
AgentType,
|
|
||||||
ApiModel,
|
ApiModel,
|
||||||
BaseAgentForm,
|
BaseAgentForm,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
@ -17,30 +15,22 @@ import type {
|
|||||||
UpdateAgentForm
|
UpdateAgentForm
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||||
import { Avatar, Button, Input, Modal, Select } from 'antd'
|
import { Button, Input, Modal, Select } from 'antd'
|
||||||
import { AlertTriangleIcon } from 'lucide-react'
|
import { AlertTriangleIcon } from 'lucide-react'
|
||||||
import type { ChangeEvent, FormEvent } from 'react'
|
import type { ChangeEvent, FormEvent } from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import type { BaseOption } from './shared'
|
|
||||||
|
|
||||||
const { TextArea } = Input
|
const { TextArea } = Input
|
||||||
|
|
||||||
const logger = loggerService.withContext('AddAgentPopup')
|
const logger = loggerService.withContext('AddAgentPopup')
|
||||||
|
|
||||||
interface AgentTypeOption extends BaseOption {
|
|
||||||
type: 'type'
|
|
||||||
key: AgentEntity['type']
|
|
||||||
name: AgentEntity['name']
|
|
||||||
}
|
|
||||||
|
|
||||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||||
|
|
||||||
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||||
type: existing?.type ?? 'claude-code',
|
type: existing?.type ?? 'claude-code',
|
||||||
name: existing?.name ?? 'Claude Code',
|
name: existing?.name ?? 'Agent',
|
||||||
description: existing?.description,
|
description: existing?.description,
|
||||||
instructions: existing?.instructions,
|
instructions: existing?.instructions,
|
||||||
model: existing?.model ?? '',
|
model: existing?.model ?? '',
|
||||||
@ -100,54 +90,6 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// add supported agents type here.
|
|
||||||
const agentConfig = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
type: 'type',
|
|
||||||
key: 'claude-code',
|
|
||||||
label: 'Claude Code',
|
|
||||||
name: 'Claude Code',
|
|
||||||
avatar: ClaudeIcon
|
|
||||||
}
|
|
||||||
] as const satisfies AgentTypeOption[],
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const agentOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
agentConfig.map((option) => ({
|
|
||||||
value: option.key,
|
|
||||||
label: (
|
|
||||||
<OptionWrapper>
|
|
||||||
<Avatar src={option.avatar} size={24} />
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</OptionWrapper>
|
|
||||||
)
|
|
||||||
})),
|
|
||||||
[agentConfig]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onAgentTypeChange = useCallback(
|
|
||||||
(value: AgentType) => {
|
|
||||||
const prevConfig = agentConfig.find((config) => config.key === form.type)
|
|
||||||
let newName: string | undefined = form.name
|
|
||||||
if (prevConfig && prevConfig.name === form.name) {
|
|
||||||
const newConfig = agentConfig.find((config) => config.key === value)
|
|
||||||
if (newConfig) {
|
|
||||||
newName = newConfig.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
type: value,
|
|
||||||
name: newName
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
[agentConfig, form.name, form.type]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@ -155,12 +97,12 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
}))
|
}))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
// const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setForm((prev) => ({
|
// setForm((prev) => ({
|
||||||
...prev,
|
// ...prev,
|
||||||
description: e.target.value
|
// description: e.target.value
|
||||||
}))
|
// }))
|
||||||
}, [])
|
// }, [])
|
||||||
|
|
||||||
const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
@ -334,16 +276,6 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
<StyledForm onSubmit={onSubmit}>
|
<StyledForm onSubmit={onSubmit}>
|
||||||
<FormContent>
|
<FormContent>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<FormItem style={{ flex: 1 }}>
|
|
||||||
<Label>{t('agent.type.label')}</Label>
|
|
||||||
<Select
|
|
||||||
value={form.type}
|
|
||||||
onChange={onAgentTypeChange}
|
|
||||||
options={agentOptions}
|
|
||||||
disabled={isEditing(agent)}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem style={{ flex: 1 }}>
|
<FormItem style={{ flex: 1 }}>
|
||||||
<Label>
|
<Label>
|
||||||
{t('common.name')} <RequiredMark>*</RequiredMark>
|
{t('common.name')} <RequiredMark>*</RequiredMark>
|
||||||
@ -363,7 +295,7 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
avatarSize={24}
|
avatarSize={24}
|
||||||
iconSize={16}
|
iconSize={16}
|
||||||
buttonStyle={{
|
buttonStyle={{
|
||||||
padding: '8px 12px',
|
padding: '3px 8px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
@ -382,7 +314,6 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
onChange={onPermissionModeChange}
|
onChange={onPermissionModeChange}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||||
dropdownStyle={{ minWidth: '500px' }}
|
|
||||||
optionLabelProp="label">
|
optionLabelProp="label">
|
||||||
{permissionModeCards.map((item) => (
|
{permissionModeCards.map((item) => (
|
||||||
<Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
|
<Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
|
||||||
@ -438,10 +369,10 @@ const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
|||||||
<TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
|
<TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem>
|
{/* <FormItem>
|
||||||
<Label>{t('common.description')}</Label>
|
<Label>{t('common.description')}</Label>
|
||||||
<TextArea rows={2} value={form.description ?? ''} onChange={onDescChange} />
|
<TextArea rows={1} value={form.description ?? ''} onChange={onDescChange} />
|
||||||
</FormItem>
|
</FormItem> */}
|
||||||
</FormContent>
|
</FormContent>
|
||||||
|
|
||||||
<FormFooter>
|
<FormFooter>
|
||||||
@ -575,14 +506,7 @@ const FormFooter = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding-top: 16px;
|
padding: 10px;
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
`
|
|
||||||
|
|
||||||
const OptionWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const PermissionOptionWrapper = styled.div`
|
const PermissionOptionWrapper = styled.div`
|
||||||
|
|||||||
@ -686,7 +686,7 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
|
|||||||
name: 'AI Gateway',
|
name: 'AI Gateway',
|
||||||
type: 'ai-gateway',
|
type: 'ai-gateway',
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
apiHost: 'https://ai-gateway.vercel.sh/v1',
|
apiHost: 'https://ai-gateway.vercel.sh/v1/ai',
|
||||||
models: [],
|
models: [],
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
import { selectPendingPermission, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||||
import type { NormalToolResponse } from '@renderer/types'
|
import type { NormalToolResponse } from '@renderer/types'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
||||||
@ -17,9 +17,7 @@ interface Props {
|
|||||||
export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const request = useAppSelector((state) =>
|
const request = useAppSelector((state) => selectPendingPermission(state.toolPermissions, toolResponse.toolCallId))
|
||||||
selectPendingPermissionByToolName(state.toolPermissions, toolResponse.tool.name)
|
|
||||||
)
|
|
||||||
const [now, setNow] = useState(() => Date.now())
|
const [now, setNow] = useState(() => Date.now())
|
||||||
const [showDetails, setShowDetails] = useState(false)
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,13 @@
|
|||||||
import { getAgentTypeAvatar } from '@renderer/config/agent'
|
|
||||||
import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||||
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
|
||||||
import type { GetAgentResponse, GetAgentSessionResponse } from '@renderer/types'
|
import type { GetAgentResponse, GetAgentSessionResponse } from '@renderer/types'
|
||||||
import { isAgentEntity } from '@renderer/types'
|
|
||||||
import { Avatar } from 'antd'
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
|
import { AccessibleDirsSetting } from './AccessibleDirsSetting'
|
||||||
import { AvatarSetting } from './AvatarSetting'
|
|
||||||
import { DescriptionSetting } from './DescriptionSetting'
|
import { DescriptionSetting } from './DescriptionSetting'
|
||||||
import { ModelSetting } from './ModelSetting'
|
import { ModelSetting } from './ModelSetting'
|
||||||
import { NameSetting } from './NameSetting'
|
import { NameSetting } from './NameSetting'
|
||||||
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
import { SettingsContainer } from './shared'
|
||||||
|
|
||||||
// const logger = loggerService.withContext('AgentEssentialSettings')
|
|
||||||
|
|
||||||
type EssentialSettingsProps =
|
type EssentialSettingsProps =
|
||||||
| {
|
| {
|
||||||
@ -30,26 +22,10 @@ type EssentialSettingsProps =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, showModelSetting = true }) => {
|
const EssentialSettings: FC<EssentialSettingsProps> = ({ agentBase, update, showModelSetting = true }) => {
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (!agentBase) return null
|
if (!agentBase) return null
|
||||||
|
|
||||||
const isAgent = isAgentEntity(agentBase)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{isAgent && (
|
|
||||||
<SettingsItem inline>
|
|
||||||
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar size={24} src={getAgentTypeAvatar(agentBase.type)} className="h-6 w-6 text-lg" />
|
|
||||||
<span>{(agentBase?.name ?? agentBase?.type) ? getAgentTypeLabel(agentBase.type) : ''}</span>
|
|
||||||
</div>
|
|
||||||
</SettingsItem>
|
|
||||||
)}
|
|
||||||
{isAgent && (
|
|
||||||
<AvatarSetting agent={agentBase} update={update as ReturnType<typeof useUpdateAgent>['updateAgent']} />
|
|
||||||
)}
|
|
||||||
<NameSetting base={agentBase} update={update} />
|
<NameSetting base={agentBase} update={update} />
|
||||||
{showModelSetting && <ModelSetting base={agentBase} update={update} />}
|
{showModelSetting && <ModelSetting base={agentBase} update={update} />}
|
||||||
<AccessibleDirsSetting base={agentBase} update={update} />
|
<AccessibleDirsSetting base={agentBase} update={update} />
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
import { EmojiAvatarWithPicker } from '@renderer/components/Avatar/EmojiAvatarWithPicker'
|
||||||
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
|
||||||
|
import { AgentConfigurationSchema, isAgentEntity, isAgentType } from '@renderer/types'
|
||||||
import { Input } from 'antd'
|
import { Input } from 'antd'
|
||||||
import { useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { SettingsItem, SettingsTitle } from './shared'
|
import { SettingsItem, SettingsTitle } from './shared'
|
||||||
@ -13,26 +15,61 @@ export interface NameSettingsProps {
|
|||||||
export const NameSetting = ({ base, update }: NameSettingsProps) => {
|
export const NameSetting = ({ base, update }: NameSettingsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [name, setName] = useState<string | undefined>(base?.name?.trim())
|
const [name, setName] = useState<string | undefined>(base?.name?.trim())
|
||||||
|
|
||||||
const updateName = async (name: UpdateAgentBaseForm['name']) => {
|
const updateName = async (name: UpdateAgentBaseForm['name']) => {
|
||||||
if (!base) return
|
if (!base) return
|
||||||
return update({ id: base.id, name: name?.trim() })
|
return update({ id: base.id, name: name?.trim() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avatar logic
|
||||||
|
const isAgent = isAgentEntity(base)
|
||||||
|
const isDefault = isAgent ? isAgentType(base.configuration?.avatar) : false
|
||||||
|
const [emoji, setEmoji] = useState(isAgent && !isDefault ? (base.configuration?.avatar ?? '⭐️') : '⭐️')
|
||||||
|
|
||||||
|
const updateAvatar = useCallback(
|
||||||
|
(avatar: string) => {
|
||||||
|
if (!isAgent || !base) return
|
||||||
|
const parsedConfiguration = AgentConfigurationSchema.parse(base.configuration ?? {})
|
||||||
|
const payload = {
|
||||||
|
id: base.id,
|
||||||
|
configuration: {
|
||||||
|
...parsedConfiguration,
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update(payload)
|
||||||
|
},
|
||||||
|
[base, update, isAgent]
|
||||||
|
)
|
||||||
|
|
||||||
if (!base) return null
|
if (!base) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsItem inline>
|
<SettingsItem inline>
|
||||||
<SettingsTitle>{t('common.name')}</SettingsTitle>
|
<SettingsTitle>{t('common.name')}</SettingsTitle>
|
||||||
<Input
|
<div className="flex max-w-70 flex-1 items-center gap-1">
|
||||||
placeholder={t('common.agent_one') + t('common.name')}
|
{isAgent && (
|
||||||
value={name}
|
<EmojiAvatarWithPicker
|
||||||
onChange={(e) => setName(e.target.value)}
|
emoji={emoji}
|
||||||
onBlur={() => {
|
onPick={(emoji: string) => {
|
||||||
if (name !== base.name) {
|
setEmoji(emoji)
|
||||||
updateName(name)
|
if (isAgent && emoji === base?.configuration?.avatar) return
|
||||||
}
|
updateAvatar(emoji)
|
||||||
}}
|
}}
|
||||||
className="max-w-70 flex-1"
|
/>
|
||||||
/>
|
)}
|
||||||
|
<Input
|
||||||
|
placeholder={t('common.agent_one') + t('common.name')}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (name !== base.name) {
|
||||||
|
updateName(name)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,7 +109,6 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Alert
|
<Alert
|
||||||
type={isUvInstalled ? 'success' : 'warning'}
|
type={isUvInstalled ? 'success' : 'warning'}
|
||||||
banner
|
|
||||||
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
||||||
description={
|
description={
|
||||||
<VStack>
|
<VStack>
|
||||||
@ -140,7 +139,6 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
/>
|
/>
|
||||||
<Alert
|
<Alert
|
||||||
type={isBunInstalled ? 'success' : 'warning'}
|
type={isBunInstalled ? 'success' : 'warning'}
|
||||||
banner
|
|
||||||
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
style={{ borderRadius: 'var(--list-item-border-radius)' }}
|
||||||
description={
|
description={
|
||||||
<VStack>
|
<VStack>
|
||||||
|
|||||||
@ -140,7 +140,7 @@ const MCPSettings: FC = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="mcp-install"
|
path="mcp-install"
|
||||||
element={
|
element={
|
||||||
<SettingContainer theme={theme}>
|
<SettingContainer style={{ backgroundColor: 'inherit' }}>
|
||||||
<InstallNpxUv />
|
<InstallNpxUv />
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export type ToolPermissionRequestPayload = {
|
|||||||
requestId: string
|
requestId: string
|
||||||
toolName: string
|
toolName: string
|
||||||
toolId: string
|
toolId: string
|
||||||
|
toolCallId: string
|
||||||
description?: string
|
description?: string
|
||||||
requiresPermissions: boolean
|
requiresPermissions: boolean
|
||||||
input: Record<string, unknown>
|
input: Record<string, unknown>
|
||||||
@ -82,12 +83,12 @@ export const selectActiveToolPermission = (state: ToolPermissionsState): ToolPer
|
|||||||
return activeEntries[0]
|
return activeEntries[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectPendingPermissionByToolName = (
|
export const selectPendingPermission = (
|
||||||
state: ToolPermissionsState,
|
state: ToolPermissionsState,
|
||||||
toolName: string
|
toolCallId: string
|
||||||
): ToolPermissionEntry | undefined => {
|
): ToolPermissionEntry | undefined => {
|
||||||
const activeEntries = Object.values(state.requests)
|
const activeEntries = Object.values(state.requests)
|
||||||
.filter((entry) => entry.toolName === toolName)
|
.filter((entry) => entry.toolCallId === toolCallId)
|
||||||
.filter(
|
.filter(
|
||||||
(entry) => entry.status === 'pending' || entry.status === 'submitting-allow' || entry.status === 'submitting-deny'
|
(entry) => entry.status === 'pending' || entry.status === 'submitting-allow' || entry.status === 'submitting-deny'
|
||||||
)
|
)
|
||||||
|
|||||||
13
yarn.lock
13
yarn.lock
@ -1891,30 +1891,30 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@cherrystudio/ai-core@workspace:^1.0.0-alpha.18, @cherrystudio/ai-core@workspace:packages/aiCore":
|
"@cherrystudio/ai-core@workspace:^1.0.9, @cherrystudio/ai-core@workspace:packages/aiCore":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
|
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ai-sdk/anthropic": "npm:^2.0.43"
|
"@ai-sdk/anthropic": "npm:^2.0.43"
|
||||||
"@ai-sdk/azure": "npm:^2.0.66"
|
"@ai-sdk/azure": "npm:^2.0.66"
|
||||||
"@ai-sdk/deepseek": "npm:^1.0.27"
|
"@ai-sdk/deepseek": "npm:^1.0.27"
|
||||||
"@ai-sdk/google": "patch:@ai-sdk/google@npm%3A2.0.36#~/.yarn/patches/@ai-sdk-google-npm-2.0.36-6f3cc06026.patch"
|
|
||||||
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch"
|
|
||||||
"@ai-sdk/openai-compatible": "npm:^1.0.26"
|
"@ai-sdk/openai-compatible": "npm:^1.0.26"
|
||||||
"@ai-sdk/provider": "npm:^2.0.0"
|
"@ai-sdk/provider": "npm:^2.0.0"
|
||||||
"@ai-sdk/provider-utils": "npm:^3.0.16"
|
"@ai-sdk/provider-utils": "npm:^3.0.16"
|
||||||
"@ai-sdk/xai": "npm:^2.0.31"
|
"@ai-sdk/xai": "npm:^2.0.31"
|
||||||
"@cherrystudio/ai-sdk-provider": "workspace:*"
|
|
||||||
tsdown: "npm:^0.12.9"
|
tsdown: "npm:^0.12.9"
|
||||||
typescript: "npm:^5.0.0"
|
typescript: "npm:^5.0.0"
|
||||||
vitest: "npm:^3.2.4"
|
vitest: "npm:^3.2.4"
|
||||||
zod: "npm:^4.1.5"
|
zod: "npm:^4.1.5"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
"@ai-sdk/google": ^2.0.36
|
||||||
|
"@ai-sdk/openai": ^2.0.64
|
||||||
|
"@cherrystudio/ai-sdk-provider": ^0.1.2
|
||||||
ai: ^5.0.26
|
ai: ^5.0.26
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@cherrystudio/ai-sdk-provider@workspace:*, @cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider":
|
"@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider"
|
resolution: "@cherrystudio/ai-sdk-provider@workspace:packages/ai-sdk-provider"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -9919,6 +9919,7 @@ __metadata:
|
|||||||
"@ai-sdk/google-vertex": "npm:^3.0.68"
|
"@ai-sdk/google-vertex": "npm:^3.0.68"
|
||||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch"
|
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.8#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.8-d4d0aaac93.patch"
|
||||||
"@ai-sdk/mistral": "npm:^2.0.23"
|
"@ai-sdk/mistral": "npm:^2.0.23"
|
||||||
|
"@ai-sdk/openai": "patch:@ai-sdk/openai@npm%3A2.0.64#~/.yarn/patches/@ai-sdk-openai-npm-2.0.64-48f99f5bf3.patch"
|
||||||
"@ai-sdk/perplexity": "npm:^2.0.17"
|
"@ai-sdk/perplexity": "npm:^2.0.17"
|
||||||
"@ant-design/v5-patch-for-react-19": "npm:^1.0.3"
|
"@ant-design/v5-patch-for-react-19": "npm:^1.0.3"
|
||||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch"
|
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch"
|
||||||
@ -9928,7 +9929,7 @@ __metadata:
|
|||||||
"@aws-sdk/client-bedrock-runtime": "npm:^3.910.0"
|
"@aws-sdk/client-bedrock-runtime": "npm:^3.910.0"
|
||||||
"@aws-sdk/client-s3": "npm:^3.910.0"
|
"@aws-sdk/client-s3": "npm:^3.910.0"
|
||||||
"@biomejs/biome": "npm:2.2.4"
|
"@biomejs/biome": "npm:2.2.4"
|
||||||
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18"
|
"@cherrystudio/ai-core": "workspace:^1.0.9"
|
||||||
"@cherrystudio/embedjs": "npm:^0.1.31"
|
"@cherrystudio/embedjs": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-libsql": "npm:^0.1.31"
|
"@cherrystudio/embedjs-libsql": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-loader-csv": "npm:^0.1.31"
|
"@cherrystudio/embedjs-loader-csv": "npm:^0.1.31"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user