Compare commits

..

371 Commits

Author SHA1 Message Date
suyao
86a2780e2c Add initial chunk emission for agent response processing
- Emit `ChunkType.LLM_RESPONSE_CREATED` event to mirror assistant behavior
- Ensure UI reflects pending state immediately during response processing
- Maintain consistency with existing stream processing callback structure
2025-09-20 16:58:24 +08:00
suyao
d960a42d6e Set default active agent and session on load
- Automatically select first agent if none active after loading
- Automatically select first session per agent if none active after loading
- Prevent empty selection states in UI components
2025-09-20 16:41:15 +08:00
suyao
3ae1b3d4cb Improve model name resolution for agent sessions
- Extract actual model ID from session model string and resolve model details
- Use resolved model name, provider, and group when available instead of defaults
- Remove redundant API model handling in getModelName function
2025-09-20 16:33:56 +08:00
suyao
1c045231c8 Extract model name from session model string
- Use split and pop to isolate the model name after the colon
- Fall back to the full model string if no colon is present
- Maintain provider and group identifiers for model object consistency
2025-09-20 16:28:16 +08:00
suyao
282aa6e81a Simplify provider display name logic and add debug logging
- Replace complex fallback chain for provider display name with direct provider name access
- Add console.log for model debugging in getModelName function
2025-09-20 16:23:37 +08:00
suyao
117e390cf1 Support API models with provider_name field in getModelName
- Add ApiModel type import and update function signature to accept ApiModel
- Return formatted name using provider_name field for API models
- Maintain backward compatibility for legacy models by looking up provider in store
2025-09-20 16:05:18 +08:00
suyao
34b05a138b Add loading state with spinner and i18n support to SessionsTab
- Replace static "No active agent" message with a spinner and loading text
- Integrate react-i18next for translation of loading message
- Adjust animation timing and styling for smoother loading state transition
2025-09-20 15:58:33 +08:00
suyao
6c233fef9f Add smooth animations to SessionsTab and Sessions components
- Replace static conditional rendering with Framer Motion animations for no-agent and session states
- Animate session list items with staggered entrance and exit transitions
- Add loading spinner animation with fade effect
- Apply motion to session creation button with delayed entrance
2025-09-20 15:55:42 +08:00
suyao
1c813aa6c3 Merge branch 'refactor/agent-align' of https://github.com/CherryHQ/cherry-studio into refactor/agent-align 2025-09-20 15:52:32 +08:00
suyao
dd5592ddbb Optimize chat components with memoization and shared layout
- Wrap `SessionMessages` and `SessionInputBar` in `useMemo` to prevent unnecessary re-renders
- Refactor `AgentSessionMessages` to use shared layout components and message grouping
- Extract common styled components to `shared.tsx` for reuse across message components
2025-09-20 15:49:40 +08:00
GitHub Action
6e9d8a1747 fix(i18n): Auto update translations for PR #10276 2025-09-20 07:24:06 +00:00
suyao
cee78c6610 Add provider_name field to model objects and improve display
- Add provider_name field to ApiModel schema and transformation logic
- Update model options to include providerName for better display
- Improve provider label fallback chain in model transformation
- Fix agent hook to use proper SWR key and conditional fetching
- Enhance option rendering with better truncation and provider display
2025-09-20 15:19:11 +08:00
suyao
0b2dfbb88f Add accessible paths management to agent configuration
Move accessible paths functionality from session modal to agent modal,
add validation requiring at least one path, and update form handling to
inherit agent paths in sessions.
2025-09-20 14:53:08 +08:00
suyao
1fd44a68b0 Refactor agent streaming from EventEmitter to ReadableStream
Replaced EventEmitter-based agent streaming with ReadableStream for
better compatibility with AI SDK patterns. Modified
SessionMessageService to return stream/completion pair instead of event
emitter, updated HTTP handlers to use stream pumping, and added IPC
contract for renderer-side message persistence.
2025-09-20 13:31:29 +08:00
icarus
fcacc50fdc Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-20 00:53:55 +08:00
icarus
009b58c9c3 feat(i18n): add error message for failed session retrieval 2025-09-20 00:53:49 +08:00
icarus
77c64cf868 refactor(hooks): improve error handling in useSessions hook
- Remove redundant agentId checks as they're handled by the API client
- Add consistent error formatting using formatErrorMessageWithPrefix
- Update error messages for all session operations
2025-09-20 00:52:39 +08:00
icarus
f5acddbfeb refactor(agent): extract shared components and improve model handling
- Extract common option components to shared.tsx for reuse
- Make useModels filter parameter optional
- Update SessionModal to use real model data from API
2025-09-20 00:48:17 +08:00
icarus
ae35d689ec feat(AgentModal): add dynamic model options from API
Fetch model options from API instead of using mocked data
Display provider label for model options
2025-09-20 00:32:51 +08:00
icarus
825b5e1be4 refactor(error-handling): move error formatting functions to error utils
Consolidate error formatting functions (formatAgentServerError and formatAxiosError) into error.ts utility file to improve code organization and maintainability
2025-09-20 00:32:43 +08:00
Vaayne
17df1db120 ♻️ refactor: simplify streaming message lifecycle management
- Remove unused persistence tracking variables in message handler
- Simplify finalizeResponse logic by removing unnecessary checks
- Change 'finish' event type to 'complete' for consistency
- Add debug logging for streaming events
- Clean up dead code and improve readability
2025-09-20 00:14:02 +08:00
Vaayne
d56521260c ️ perf: add caching layer for MCP servers and providers data access
- Extract getServersFromRedux to shared utility getMCPServersFromRedux
- Implement 5-minute TTL cache for MCP servers and providers
- Reduce redundant Redux store queries in API server
- Improve response times for frequently accessed data
2025-09-20 00:13:26 +08:00
icarus
8efafc6ba9 feat(hooks): add useModels hook for fetching agent models 2025-09-19 23:51:46 +08:00
icarus
f35987a9a9 refactor(api): rename modelsPath to getModelsPath for consistency
Align method naming with other path getter methods in the class for better code maintainability and consistency
2025-09-19 23:50:04 +08:00
icarus
c7ec55c69a refactor(types): rename ApiModelsRequest to ApiModelsFilter for clarity
Update type name and related imports to better reflect its purpose as a filter type rather than a request type
2025-09-19 23:48:58 +08:00
icarus
c77d7dff78 feat(api): add models endpoint to agent client and type response
Add new models endpoint to AgentApiClient with query parameter support
Ensure type safety by adding ApiModelsResponse type and schema validation
2025-09-19 23:43:20 +08:00
Vaayne
b282e4d729 feat: implement robust AbortController for Claude Code agent streams
- Add AbortController support to agent service interface and implementations
- Enhance client disconnect detection with multiple HTTP event listeners (req.close, req.aborted, res.close)
- Implement proper abort error handling in ClaudeCodeService with 'cancelled' event type
- Add comprehensive documentation explaining SSE disconnect detection behavior
- Clean up stream interfaces by removing unused properties and simplifying event structure
- Extend timeout from 5 to 10 minutes for longer-running agent tasks
- Ensure proper resource cleanup and prevent orphaned processes on client disconnect

This enables reliable cancellation of long-running Claude Code processes when clients
disconnect unexpectedly (browser tab close, curl Ctrl+C, network issues, etc.)
2025-09-19 23:42:46 +08:00
Vaayne
c426876d0d feat(transform): refactor message handling to unify user and assistant processing 2025-09-19 22:36:19 +08:00
Vaayne
027ef17a2e feat(agents): enhance ClaudeCodeService to support MCP configurations and additional directories 2025-09-19 21:51:33 +08:00
Vaayne
f0ac74dccf feat(agents): enhance error messages for agent and session operations; update accessible paths handling 2025-09-19 20:20:20 +08:00
Vaayne
d6468f33c5 feat(agents): implement model validation for agent and session creation/updating 2025-09-19 20:20:20 +08:00
Vaayne
1515f511a1 feat(agents): refactor agent invocation to use session object and enhance error handling 2025-09-19 20:20:20 +08:00
Vaayne
1c2211aefb feat(agents): add ensurePathsExist method to validate and create accessible paths 2025-09-19 20:20:20 +08:00
Vaayne
49f9dff9da feat(models): update models filtering to use providerType and enhance API schemas 2025-09-19 20:20:19 +08:00
icarus
92ba1e4fc3 fix(SessionMessageService): stringify user message content to ensure proper formatting 2025-09-19 17:54:25 +08:00
icarus
7060aab33d fix: reverse message order and fix mutation revalidation
Reverse message display order for better UX and prevent unnecessary revalidation during message creation
2025-09-19 17:52:17 +08:00
icarus
0cce8220ce Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-19 17:46:32 +08:00
icarus
1722d9f435 feat(i18n): add error message for failed message creation
Add "create_message" error translation in multiple language files
2025-09-19 17:44:58 +08:00
icarus
078fd57eb5 fix(agent): update message id and improve content handling
- Change default message id from -1 to 77777 in useSession
- Remove schema validation for session response temporarily
- Add proper content parsing for agent session messages
2025-09-19 17:44:08 +08:00
icarus
4bd6087dc0 feat(chat): add agent session inputbar component
Implement input bar for agent sessions with message sending functionality
2025-09-19 17:39:24 +08:00
icarus
e45231376c feat(useSession): add optimistic updates for message creation
Implement optimistic UI updates when creating new messages to improve perceived performance. The changes include cloning the current session data, adding a draft message immediately, and handling rollback on error.
2025-09-19 17:39:04 +08:00
icarus
01c7e509fd feat(hooks): add createSessionMessage to useSession hook
Expose new function to create messages for agent sessions and automatically refresh session data
2025-09-19 17:01:47 +08:00
icarus
5ddf9683b4 feat(chat): add session messages view and active state tracking
Implement agent session messages display component and track active topic/session state
Add AgentSessionMessages component and integrate with chat view
Update topic and session selection to set active state in store
2025-09-19 16:39:02 +08:00
Vaayne
d91df12dbc feat(models): refactor models service to use new API models schema and types 2025-09-19 16:36:11 +08:00
icarus
64e3de9ada feat(chat): add activeTopicOrSession state to track current view
Add new state field and action to track whether the user is viewing topics or sessions in the chat interface. This enables proper UI state management when switching between views.
2025-09-19 16:27:50 +08:00
Vaayne
2cf2f04a70 feat(chat): enhance chat completion error handling and streaming support
feat(messages): improve message validation and add streaming support
feat(tests): add HTTP tests for chat and message endpoints
2025-09-19 16:16:33 +08:00
Vaayne
73380d76df feat(models): enhance models endpoint with filtering and pagination support 2025-09-19 16:16:33 +08:00
Vaayne
38076babcf feat(messages): add messages route and service for handling Anthropic messages 2025-09-19 16:16:33 +08:00
icarus
00cc410dcc refactor(SessionItem): remove confirmation modal for delete action
Simplify delete flow by removing the confirmation modal and directly calling onDelete
2025-09-19 15:59:18 +08:00
icarus
7fc676bbc3 fix(useSession): provide default empty array for messages when undefined
Make messages field optional in GetAgentSessionResponseSchema to match actual API behavior and prevent potential undefined access
2025-09-19 15:56:09 +08:00
icarus
798126d39c feat(session): add session modal component to session item
Implement session modal for editing session details, using existing disclosure hooks
2025-09-19 15:46:52 +08:00
icarus
874d74cf6e feat(chat): add tab state management to redux store
Move tab state from component local state to redux store for better state management across components
2025-09-19 15:43:39 +08:00
icarus
d73834e7f6 feat(i18n): add session add/edit translation keys
Add new translation keys for session add/edit functionality in multiple languages
2025-09-19 15:28:11 +08:00
icarus
cb3afaceab refactor(agent): consolidate agent update logic into useAgents hook 2025-09-19 15:27:23 +08:00
icarus
fc0ba5d0d5 feat(hooks): add useSession hook for managing agent sessions 2025-09-19 15:27:10 +08:00
icarus
7ce4fc50ea refactor(hooks): remove unused useUpdateAgent hook 2025-09-19 15:24:39 +08:00
icarus
b5ef8a93ca refactor(useAgent): simplify mutate call in agent update
The previous implementation unnecessarily spread the previous state when only the result is needed. This simplifies the mutation logic while maintaining the same behavior.
2025-09-19 15:24:33 +08:00
icarus
8ead4e9c0f feat(sessions): add session creation modal and improve session item styling
- Implement SessionModal component for creating/editing sessions
- Replace Button wrapper with fragment in SessionItem for cleaner styling
- Add translation support and proper form handling for session creation
2025-09-19 15:21:27 +08:00
icarus
432d84cda5 refactor(sessions): simplify session update API by using form object
Remove redundant sessionId parameter from updateSession methods since it's already included in the UpdateSessionForm. This makes the API more consistent and reduces potential for mismatched IDs.
2025-09-19 15:08:53 +08:00
icarus
d3378dcf78 refactor(types): use inferred type for CreateSessionRequest 2025-09-19 14:56:50 +08:00
icarus
f0724af2aa refactor(agent-modal): move AgentModal component to agent subdirectory
Restructure the component organization by moving AgentModal.tsx into a dedicated agent subdirectory under Popups. This improves maintainability by grouping related agent components together.

Update all import paths to reflect the new location. The component functionality remains unchanged.
2025-09-19 14:55:26 +08:00
icarus
f127150ea1 feat(sessions): implement session management ui and state
- Rename Container to ButtonContainer for consistency
- Add activeSessionId state to track active sessions per agent
- Implement Sessions and SessionItem components with loading state
- Add session selection and deletion functionality
2025-09-19 14:54:04 +08:00
icarus
b3ef6d4534 feat(i18n): add error messages for session operations
Add error messages for session deletion and update failures in all supported languages. Also include a generic delete failure message.
2025-09-19 14:09:05 +08:00
icarus
1ce791d517 feat(AgentsTab): add loading spinner for agents list
Show spinner while agents are loading to improve user experience
2025-09-19 14:07:56 +08:00
icarus
3d561ad8e3 refactor(sessions): extract sessions component and update hook to use agentId
Move sessions rendering logic to a separate component and modify useSessions hook to work with agentId instead of full agent entity. This improves code organization and simplifies the hook interface.
2025-09-19 14:07:23 +08:00
icarus
14509d1077 feat(hooks): add useAgent hook for managing agent state and updates 2025-09-19 14:07:03 +08:00
icarus
a424e3a039 feat(agents): add success toast notifications for agent operations 2025-09-19 13:32:16 +08:00
icarus
eaa5ec5545 refactor(agents): remove unused useRemoveAgent hook and use deleteAgent from useAgents 2025-09-19 13:30:50 +08:00
icarus
5850e5da66 refactor(AgentItem): simplify component structure and remove unused logger
- Remove Button wrapper from AgentLabel component
- Replace div container with Button component for better semantics
- Clean up unused logger service and related click handler
2025-09-19 13:28:36 +08:00
icarus
eb3ff6f570 fix(agents): convert null values to undefined in database responses
Ensure type consistency by converting null values from database to undefined as specified in type definitions
2025-09-19 13:14:20 +08:00
icarus
ae9c78e643 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-19 12:58:21 +08:00
icarus
445528aff7 refactor(agent): replace interface with zod schema for message request
Add createMessage method to AgentApiClient to handle posting messages to sessions
2025-09-19 12:58:14 +08:00
Vaayne
d13c25444c feat(agents, sessions): enhance agent and session schemas with detailed properties and CRUD API documentation 2025-09-19 12:56:20 +08:00
Vaayne
5386716ebe feat(session messages): enhance session message persistence with improved error handling and completion notifications 2025-09-19 12:56:20 +08:00
Vaayne
da3cd62486 feat(sessions): update session creation and update requests with new session details 2025-09-19 12:56:20 +08:00
Vaayne
d8b47e30c4 feat(session messages): implement user message persistence and retrieve last agent session ID 2025-09-19 12:56:20 +08:00
icarus
1c19e529ac Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-19 12:51:08 +08:00
Vaayne
514b60f704 feat(agents, sessions): implement replace functionality for agent and session updates 2025-09-19 11:13:05 +08:00
Vaayne
df1d4cd62b refactor(sessions): update getSession and related methods to include agentId parameter 2025-09-19 10:41:26 +08:00
Vaayne
4839b91cef chore(docs): remove session tracking protocol from AI Assistant Guide 2025-09-19 10:27:09 +08:00
Vaayne
5048d6987d refactor(validators): migrate validation logic to Zod for agents and sessions 2025-09-19 10:27:02 +08:00
icarus
809736dd33 feat(sessions): add update session functionality
Introduce UpdateSessionResponse type and schema to support session updates. Implement update session methods across client, service, and handler layers to enable session modifications.
2025-09-18 22:58:49 +08:00
icarus
369cc37071 feat(sessions): add delete session functionality to agent api and hook
Implement session deletion by adding deleteSession method to AgentApiClient and corresponding hook in useSessions. This enables removing sessions from the UI with proper error handling and cache invalidation.
2025-09-18 22:52:46 +08:00
icarus
d0b64dabc2 fix(agents): correct agent update and retrieval logic
Fix mutation logic to use result.id instead of form.id for consistency
Make getAgent async and update cache with fetched agent data
2025-09-18 22:51:02 +08:00
icarus
02d2838424 fix(agent): add response validation for ID mismatches
Add checks to ensure response IDs match expected values in agent API calls
2025-09-18 22:50:05 +08:00
icarus
4c4039283f fix(useSessions): correct session fetching logic to use API call
Previously the getSession hook was only searching local data. Now it properly fetches from the API and updates the cache. This ensures data consistency when sessions are modified elsewhere.
2025-09-18 22:46:24 +08:00
icarus
77df6fd58e feat(sessions): add getSession method to retrieve specific session
Implement session retrieval functionality in both hook and API client to enable fetching individual sessions by ID
2025-09-18 22:37:02 +08:00
icarus
100801821f fix(agents): update agents list response structure to match API
Align frontend and backend types for agents list response. The API now returns paginated data with limit/offset and renamed 'agents' field to 'data' for consistency. Update related type definitions and usage across the codebase.
2025-09-18 22:26:54 +08:00
icarus
2201ebbb88 feat(i18n): add error messages for agent and session operations
Add error messages for agent deletion, update, and session creation operations
Add "no response" error message for all supported languages
2025-09-18 22:21:27 +08:00
icarus
9810f01330 feat(sessions): add create session functionality to agent api and hook
Implement session creation in the agent API client and expose it through the useSessions hook. The hook now provides a createSession method that updates the session list upon successful creation.
2025-09-18 22:19:52 +08:00
icarus
7b428be93d feat(types): add session form interfaces for agent operations
Add BaseSessionForm, CreateSessionForm and UpdateSessionForm interfaces to support session management functionality
2025-09-18 22:10:53 +08:00
icarus
a4c2ed5328 fix(api): update error handling to match new error structure
The AgentServerError schema was updated to nest error properties under an 'error' object. This commit aligns the error formatting function with the new schema structure.
2025-09-18 22:02:42 +08:00
icarus
934cc0dd33 refactor(useAgents): simplify agent data structure and mutations
Remove unnecessary nesting of agents array in SWR response and simplify mutation logic to work directly with the array
2025-09-18 22:00:25 +08:00
icarus
da61500e34 feat(hooks): add useSessions hook for agent session management 2025-09-18 21:57:18 +08:00
icarus
db2042800b feat(agent): add listSessions method to AgentApiClient
Implement session listing functionality for agents by adding the listSessions method. This enables retrieving all sessions associated with a specific agent.
2025-09-18 21:50:14 +08:00
icarus
08772741e6 feat(agent-sessions): add schema and type for listing agent sessions
Add ListAgentSessionsResponseSchema and type to support paginated session listing
2025-09-18 21:49:02 +08:00
icarus
f5f542911f feat(sessions): add CreateSessionResponse type for better type safety
Introduce CreateSessionResponse type and schema to clearly define the return type of session creation operations. This improves type safety and consistency across the codebase when handling session responses.
2025-09-18 21:43:06 +08:00
icarus
3b5b1986e6 feat(hooks): add getAgent function to useAgents hook 2025-09-18 21:34:25 +08:00
icarus
3b0995c8ef feat(agents): add deleteAgent hook to useAgents
Implement agent deletion functionality with optimistic updates and error handling
2025-09-18 21:22:20 +08:00
icarus
34c95ca787 feat(agents): add update agent functionality and error message
Add updateAgent method to useAgents hook and update agent API client
2025-09-18 21:20:58 +08:00
icarus
a4c2a1d435 feat(i18n): add new translation keys for agent errors and session management
Add "failed" error message for agent addition and implement session-related translations including delete confirmation and labels
2025-09-18 20:49:04 +08:00
icarus
e4f0743e2f fix(api): replace Axios constructor with axios.create for better defaults
Using axios.create() provides better default configuration and error handling compared to the Axios constructor
2025-09-18 20:47:53 +08:00
icarus
7632efda88 fix(api): handle ZodError in processError and formatErrorMessage
Add explicit handling of ZodError in processError to return the error directly and in formatErrorMessage to use formatZodError for better error reporting
2025-09-18 20:40:30 +08:00
icarus
ab90eb2aab refactor(AgentModal): replace useAddAgent with useAgents hook 2025-09-18 20:29:04 +08:00
icarus
4c5bed0b1f fix(agents): add http protocol to agent client base url 2025-09-18 20:28:08 +08:00
icarus
302331043a refactor(agent): add error logging in agent api client
Use logger service to log errors when processing API requests
2025-09-18 20:25:40 +08:00
icarus
09f5e7af8c feat(agents): implement add agent functionality in useAgents hook
Add createAgent method to useAgents hook and remove unused useAddAgent hook
Use formatErrorMessageWithPrefix for better error handling
2025-09-18 20:25:28 +08:00
icarus
664304241a feat(i18n): make base locale configurable via env var
Add support for BASE_LOCALE environment variable to override default locale
Add file existence check for base locale file in auto-translate script
Update npm scripts to load .env for i18n commands
2025-09-18 19:31:24 +08:00
icarus
27f98b02a6 feat(sessions): add sessions tab with basic functionality and translations
- Create new SessionsTab component with mock data
- Add session item component with context menu for edit/delete
- Include session tab in main navigation
- Add English translations for session-related strings
2025-09-18 19:26:25 +08:00
icarus
af6a3c87d6 fix(AgentItem): add missing onPress dependency to useCallback
Ensure the callback is updated when onPress prop changes to avoid stale closures
2025-09-18 19:17:10 +08:00
icarus
d1819274bb Revert "feat(agent): add name field to AgentSessionEntitySchema"
This reverts commit 8058ed21b3.
2025-09-18 19:13:24 +08:00
icarus
8058ed21b3 feat(agent): add name field to AgentSessionEntitySchema
Add name field to agent session schema to support session naming functionality
2025-09-18 19:11:52 +08:00
icarus
eaf302bb40 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-18 19:04:14 +08:00
icarus
3405b7e429 refactor(ui): rename agents to assistantPresets for consistency
Update route paths, i18n keys and tab identifiers to use 'assistantPresets' instead of 'agents'
Add new agents tab component while maintaining backward compatibility
2025-09-18 19:04:05 +08:00
icarus
2fc1df8793 refactor(ui): remove unused UI elements and commented code
clean up UI by removing unused text spans and commented out code
remove unused timer functionality from AgentModal
2025-09-18 19:00:41 +08:00
icarus
ec82eb2881 build: add @types alias to electron vite config 2025-09-18 18:52:36 +08:00
icarus
1c978e0684 feat(agents): add agent selection functionality
- Replace onTagClick with onPress handler in AgentItem
- Add active agent state management in AgentsTab
- Wrap AgentItem content in Button for better interaction
2025-09-18 18:47:37 +08:00
icarus
e938e1572c feat(chat): add activeAgentId to track current agent state
Add activeAgentId field to chat state to track which agent is currently active. This enables UI to reflect the active agent state and handle agent-specific interactions.
2025-09-18 18:43:10 +08:00
icarus
9a7681c5c8 refactor(agent): update agent form types and mutation hooks
- Add proper type definitions for AddAgentForm and UpdateAgentForm
- Split agent mutation hooks into separate files
- Update AgentModal to use new form types and hooks
- Remove redundant fields from form submissions
2025-09-18 18:39:02 +08:00
icarus
259f2157f6 refactor(agent): split AgentForm into BaseAgentForm and specific types
Improve type safety by separating AgentForm into BaseAgentForm for shared fields and specific types for add/update operations. This better reflects the actual usage patterns in the API client and modal components.
2025-09-18 18:21:52 +08:00
icarus
21ce139df0 refactor(agents): replace unimplemented mutation with simple function
Replace the placeholder mutation implementation in useUpdateAgent hook with a simpler function that shows a toast notification. This removes the unnecessary query client usage for an unimplemented feature.
2025-09-18 18:10:53 +08:00
icarus
71536d6ef5 refactor(agents): replace useMutation with simple function in useAddAgent
The mutation logic was removed and replaced with a simple function that shows a toast message. This is a temporary solution until the actual API implementation is ready.
2025-09-18 18:09:58 +08:00
icarus
ef1a035701 feat: add AgentsTab component for managing agents 2025-09-18 18:08:59 +08:00
icarus
2b76c326ee refactor(agents): replace useMutation with simpler implementation in useRemoveAgent 2025-09-18 18:08:45 +08:00
icarus
64ee5c528b fix(useAgents): use right agents data 2025-09-18 18:08:40 +08:00
icarus
136d343c18 refactor(hooks): replace redux with swr in useAgents hook
Simplify agents management by using SWR for data fetching instead of redux store operations
2025-09-18 18:05:55 +08:00
icarus
0b1b9a913f feat(hooks): add useAgentClient hook for agent API client initialization 2025-09-18 18:02:05 +08:00
icarus
cb0833a915 refactor(api): centralize agent and session paths for better maintainability
Add path builder methods to avoid hardcoding URLs and improve consistency
2025-09-18 17:58:53 +08:00
Vaayne
984c28d4be refactor(SessionMessageService): remove unused PermissionMode import 2025-09-18 17:51:45 +08:00
Vaayne
49add96dc0 feat(database): add agent_session_id to session_messages table and update related services 2025-09-18 17:46:05 +08:00
icarus
db58762a13 refactor(AssistantsTab): remove agent-related code and imports 2025-09-18 17:40:12 +08:00
icarus
6e89d0037f Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-18 17:12:10 +08:00
icarus
e1ab17387c build: add swr dependency for data fetching 2025-09-18 17:11:56 +08:00
icarus
54de2341bd refactor(agents): remove draggable functionality and simplify agent list rendering
Remove unused agent management hooks and replace DraggableList with simple map
Add cursor-pointer style to AgentItem for better UX
2025-09-18 16:24:27 +08:00
beyondkmp
b131f0c48c pack optimization 2025-09-18 15:25:16 +08:00
Vaayne
9e4b792fc3 🐛 fix: resolve duplicate migration key '156' to '157' 2025-09-18 14:54:45 +08:00
Vaayne
7abd5da57d ♻️ refactor: replace ClaudeCodeService child process with SDK query
- Replace process spawning with @anthropic-ai/claude-code SDK query function
- Remove complex process management, stdout/stderr parsing, and JSON buffering
- Directly iterate over typed SDKMessages from AsyncGenerator
- Simplify error handling and completion logic
- Maintain full compatibility with existing SessionMessageService interface
- Eliminate ~130 lines of process management code
- Improve reliability by removing JSON parsing edge cases
2025-09-18 14:54:45 +08:00
icarus
be7399b3c4 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-18 14:49:18 +08:00
icarus
8d92b515ab feat(agent): return updated agent data from updateAgent method
Add response parsing and return type to updateAgent to provide updated agent data to callers
2025-09-18 14:35:57 +08:00
icarus
524098d6d3 feat(agent): add error processing utility and update agent method
Introduce processError utility to handle axios errors consistently
Add updateAgent method to support partial updates of agent data
2025-09-18 14:34:05 +08:00
icarus
42fa2d94be feat(api): add formatAxiosError utility for handling axios errors 2025-09-18 14:33:45 +08:00
icarus
a65b30f3a1 chore: update vscode settings for i18n configuration
Remove display language setting and clean up comments
2025-09-18 14:33:10 +08:00
icarus
f1991b356b feat(agent): add get and delete agent methods with error handling
Implement getAgent and deleteAgent methods in AgentClient with proper error handling
Add formatAgentServerError utility for consistent error messages
2025-09-18 14:20:48 +08:00
beyondkmp
352c23180a add patch for sdk to run cli in electron 2025-09-18 14:11:05 +08:00
icarus
825c376c5c feat(agent): add create agent endpoint to agent client 2025-09-18 14:08:31 +08:00
icarus
231a923c9d feat(types): add AgentForm type and move from component to types file
Centralize the AgentForm type definition in the types file for better maintainability and reuse across components
2025-09-18 14:03:10 +08:00
icarus
dbf01652f8 feat(agent): add avatar support for agent items
Implement getAgentAvatar function to provide avatar images based on agent type. Update AgentItem component to display agent-specific avatars instead of generic ones.
2025-09-18 13:59:37 +08:00
icarus
842a6cb178 style(AssistantsTab): reorder tailwind classes for consistency 2025-09-18 13:54:59 +08:00
icarus
d56c526709 feat(api): add AgentClient class for listing agents
Implement a new API client class to handle agent listing operations with proper error handling and validation
2025-09-18 13:48:57 +08:00
icarus
70a68bef27 Revert "feat(types): add DeleteAgentResponse type and update AgentService"
This reverts commit 219844cb74.
2025-09-18 13:31:23 +08:00
icarus
f9b49ffde6 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-18 13:28:36 +08:00
icarus
a264fd42e4 feat(types): add AgentServerError schema for error handling 2025-09-18 13:26:51 +08:00
icarus
219844cb74 feat(types): add DeleteAgentResponse type and update AgentService
Update AgentService to use DeleteAgentResponse type instead of boolean for better type safety
2025-09-18 13:24:00 +08:00
icarus
230205d210 feat(types): add UpdateAgentResponse type and schema
Update agent service to use new response type for consistency
2025-09-18 13:21:37 +08:00
icarus
f9fb0f9125 feat(agent): add ListAgentsResponse type and update service
Add ListAgentsResponse schema and type to handle agent listing responses
Update AgentService to use the new type for listAgents method
2025-09-18 13:19:57 +08:00
icarus
0f777e357d refactor(types): convert GetAgentSessionResponse interface to zod schema
Improve type safety by using zod schema definition and inference instead of manual interface
2025-09-18 13:17:32 +08:00
icarus
5c578c191b refactor(types): convert AgentSessionMessageEntity interface to zod schema
Replace interface with zod schema for better type safety and validation
2025-09-18 13:15:58 +08:00
icarus
7a4952f773 refactor(types): convert AgentSessionEntity interface to zod schema
Use zod schema for better type safety and validation capabilities
2025-09-18 13:01:57 +08:00
icarus
71a1daddef refactor(types): reorder CreateAgentResponse and its schema
Place schema definition before type alias for better readability and consistency
2025-09-18 13:00:41 +08:00
icarus
0a82955e91 refactor(types): replace GetAgentResponse interface with zod schema
Use zod schema for better type safety and validation
2025-09-18 12:59:49 +08:00
icarus
62d2da3815 refactor(types): convert Tool interface to zod schema
Use zod schema for better type safety and validation capabilities
2025-09-18 12:58:56 +08:00
icarus
84aab66aa6 refactor(types): add CreateAgentResponseSchema for consistency 2025-09-18 12:56:29 +08:00
icarus
2d0d599ac8 refactor(types): convert AgentEntity interface to zod schema
Use zod schema for better runtime validation and type safety
2025-09-18 12:55:01 +08:00
icarus
dca6be45b0 refactor(types): replace hardcoded agent type check with zod schema
Use zod schema validation for type safety and maintainability instead of manual array checking
2025-09-18 12:52:58 +08:00
icarus
5a71807cc9 feat(types): add CreateAgentResponse type and update AgentService return type 2025-09-18 12:50:42 +08:00
icarus
0d0ab4dcf5 refactor(types): convert AgentBase interface to zod schema
This change improves type safety and validation by replacing the TypeScript interface with a zod schema definition. The schema provides runtime validation while maintaining the same type inference capabilities.
2025-09-18 12:49:17 +08:00
icarus
ac3da51890 refactor(types): replace AgentConfiguration interface with zod schema
Use zod schema for better type safety and validation capabilities
2025-09-18 12:46:45 +08:00
icarus
9ea361f7e8 refactor(types): migrate PermissionMode to zod schema for type safety 2025-09-18 12:45:00 +08:00
icarus
cc6160892a refactor(types): reorganize agent type interfaces for better clarity
Move API DTO interfaces to a dedicated section at the bottom of the file to improve code organization and maintainability
2025-09-18 12:42:01 +08:00
Vaayne
49eed449c3 refactor: Remove outdated validation and planning documents for agents service 2025-09-18 12:02:11 +08:00
Vaayne
8ada7ffaf6 Merge remote-tracking branch 'origin/main' into feat/agents-new 2025-09-18 11:51:34 +08:00
Vaayne
e7c37231e0 feat: Enhance message handling with user message persistence and improved stream management 2025-09-18 00:30:43 +08:00
Phantom
ca597b9b9b CI: improve claude translator for review, quote and email (#10230)
* ci(claude-translator): extend workflow to handle pull request review events

- Add support for pull_request_review and pull_request_review_comment events
- Update condition logic to include new event types
- Expand claude_args to include pull request review related API commands
- Enhance prompt to handle new event types and more translation scenarios

* ci(workflows): update concurrency group in claude-translator workflow

Add github.event.review.id as additional fallback for concurrency group naming

* fix(workflow): correct API method for pull_request_review event

Use PATCH instead of PUT and update body parameter to match API requirements

* ci: clarify comment ID label in workflow output

Update the label for comment ID in workflow output to explicitly indicate when it refers to review comments

* ci: fix syntax error in GitHub workflow file

* fix(workflow): correct HTTP method for pull_request_review event

Use PUT instead of PATCH for updating pull request reviews as per GitHub API requirements
2025-09-17 23:24:02 +08:00
SuYao
c76df7fb16 fix: Remove maxTokens check from Anthropic thinking budget (#10240)
Remove maxTokens check from Anthropic thinking budget
2025-09-17 23:10:58 +08:00
SuYao
89d5bd817b fix: Add AWS Bedrock reasoning extraction middleware (#10231)
* Add AWS Bedrock reasoning extraction middleware

- Add 'reasoning' tag to tagNameArray for broader reasoning support
- Add AWS Bedrock case with gpt-oss model-specific reasoning extraction
- Add openai-chat and openrouter cases to provider options switch
- Remove unused zod import

* Add OpenRouter provider support

Updates ai-core to version alpha.18 with OpenRouter integration and
improves provider ID resolution for OpenAI API hosts.
2025-09-17 20:01:47 +08:00
kangfenmao
6c9fc598d4 bump: version 1.6.0-rc.2
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 18:57:08 +08:00
kangfenmao
273475881e refactor: update HeroUIProvider import in MiniWindowApp component
- Changed the import of HeroUIProvider from '@heroui/react' to '@renderer/context/HeroUIProvider' for better context management.
2025-09-17 18:02:30 +08:00
SuYao
6afaf6244c Fix Anthropic API URL and add endpoint path handling (#10229)
* Fix Anthropic API URL and add endpoint path handling

- Remove trailing slash from Anthropic API base URL
- Add isAnthropicProvider utility function
- Update provider settings to show full endpoint URL for Anthropic
- Add migration to clean up existing Anthropic provider URLs

* Update src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx

Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com>

---------

Co-authored-by: Phantom <59059173+EurFelux@users.noreply.github.com>
2025-09-17 17:23:23 +08:00
Phantom
77535b002a feat(Inputbar): cache mentioned models state between renders (#10197)
Use a ref to track mentioned models state and cache it in a module-level variable when component unmounts to preserve state between renders
2025-09-17 16:33:10 +08:00
SuYao
dec68ee297 Fix/ts (#10226)
* chore: ts

* chore: yarn.lock
2025-09-17 15:54:54 +08:00
SuYao
a8bf55abc2 refactor: 将网络搜索参数用于模型内置搜索 (#10213)
* Remove local provider option files and use external packages

Replace local implementation of XAI and OpenRouter provider options with
external packages (@ai-sdk/xai and @openrouter/ai-sdk-provider). Update
web search plugin to support additional providers including OpenAI Chat
and OpenRouter, with improved configuration mapping.

* Bump @cherrystudio/ai-core to v1.0.0-alpha.17

fix i18n

* fix(i18n): Auto update translations for PR #10213

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-17 15:10:30 +08:00
Vaayne
c196a02c95 feat: Implement database migration system and update agent/session handling 2025-09-17 14:59:08 +08:00
one
1481149e51 fix: wrap MessageEditor with QuickPanelProvider (#10223)
* fix: wrap MessageEditor with QuickPanelProvider

* style: revert formatting
2025-09-17 14:18:48 +08:00
Vaayne
d1ff8591a6 refactor: Rename message stream handler and update session creation logic 2025-09-17 14:10:10 +08:00
SuYao
4f91a321a0 chore: update dependencies and VSCode settings (#10206)
* chore: update dependencies and VSCode settings

* chore

* chore: ts
2025-09-17 14:07:45 +08:00
LiuVaayne
219d162e1a feat: Add automatic database migration system for agents service (#10215)
* feat: Add automatic database migration system for agents service

- Add migrations tracking schema with version, tag, and timestamp
- Implement MigrationService to automatically run pending migrations
- Integrate migration check into BaseService initialization
- Read migration files from drizzle/ directory and journal.json
- Track applied migrations to prevent re-execution
- Ensure database is always at latest version on service startup

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* refactor: Improve migration logging and enhance database path configuration

* chore: harden migration bootstrap flow

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2025-09-17 12:08:59 +08:00
Vaayne
669f60273c rewrite agents schema and types 2025-09-16 23:55:31 +08:00
kangfenmao
7319fc5ef4 chore: bump version to v1.6.0-rc.1 2025-09-16 22:52:57 +08:00
kangfenmao
cc860e48b1 fix: update navbar position handling in useAppInit hook
- Added isLeftNavbar to the useNavbarPosition hook for improved layout management.
- Adjusted background style logic to use isLeftNavbar instead of isTopNavbar for better compatibility with left-aligned navigation.
- Simplified condition for transparent window styling on macOS.
2025-09-16 22:45:22 +08:00
Phantom
a9093b1dea chore: update biome format command to ignore unmatched files (#10207)
Add --no-errors-on-unmatched flag to biome format commands in lint-staged configuration to prevent errors when no matching files are found
2025-09-16 22:31:38 +08:00
Vaayne
697f7d1946 refactor: Update session tracking documentation for clarity and consistency 2025-09-16 22:06:41 +08:00
liyuyun-lyy
ee95fad7e5 feat: add support for iflow cli (#10198)
Signed-off-by: 本x <liyuyun.lyy@alibaba-inc.com>
2025-09-16 21:24:24 +08:00
Vaayne
e4d04f8346 refactor: Remove unused variable linting directive in useUpdateAgent hook 2025-09-16 18:13:37 +08:00
Vaayne
c37af25525 Merge remote-tracking branch 'origin/main' into feat/agents-new 2025-09-16 18:08:14 +08:00
Vaayne
ea90c6c9cb feat: Enhance message handling by adding raw message metadata to assistant, stream, system, and result handlers 2025-09-16 17:57:40 +08:00
Vaayne
58dbb514e0 feat: Implement Claude Code service with streaming support and tool integration
- Added `aisdk-stream-protocel.md` to document text and data stream protocols.
- Created `ClaudeCodeService` for invoking and streaming responses from the Claude Code CLI.
- Introduced built-in tools for Claude Code, including Bash, Edit, and WebFetch.
- Developed transformation functions to convert Claude Code messages to AI SDK format.
- Enhanced OCR utility with delayed loading of the Sharp module.
- Updated agent types and session message structures to accommodate new features.
- Modified API tests to reflect changes in session creation and message streaming.
- Upgraded `uuid` package to version 13.0.0 for improved UUID generation.
2025-09-16 15:12:03 +08:00
one
1a5138c5b1 refactor: quick panel and inputbar tools (#10142)
* refactor: add QuickPanelReservedSymbol

* refactor: pass assistant id instead of assistant object

* refactor: simplify InputbarTools props

* refactor: add root symbol

* refactor: merge file panel to attachment button

* refactor: extract ActionButton for reuse

* chore: update CLAUDE.md

* fix: colors

* refactor: add more reserved symbols

* fix: keep updateAssistant stable

* refactor(components): replace styled-components with cn utility in ActionIconButton

- Replace styled-components with cn utility from @heroui/react for better maintainability
- Add new --icon and --color-icon CSS variables for consistent icon coloring
- Simplify button styling using Tailwind CSS classes

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-16 11:10:14 +08:00
SuYao
5451e2f34a Perf/tsgo (#10188)
* feat: update tsgo

* chore: update alpha.15

* feat: add script

* chore: update ai-core version to alpha.16 and add npm registry settings

* chore
2025-09-16 10:16:42 +08:00
Phantom
c641b116ba fix(markdown): broken layout in translate page if rendered as long markdown (#10187)
* style(markdown): improve code block styling and layout

- Add text-wrap to pre elements for better readability
- Force background color for code blocks with !important
- Change display to grid in translate page for consistent layout
- Add overflow hidden to output container

* style(markdown): remove redundant !important from code block styling

Move !important declaration to output container's markdown pre selector where it's actually needed
2025-09-16 02:10:28 +08:00
Phantom
d44654f003 chore: add tailwindcss and vitest extensions to vscode config (#10169) 2025-09-16 00:53:46 +08:00
Phantom
85b8724c73 chore: add eslint cache to gitignore and enable cache in lint commands (#10167)
Enable eslint cache to improve linting performance and add .eslintcache to gitignore
2025-09-16 00:11:47 +08:00
Vaayne
a8e2df6bed feat: add electron-reload for development auto-restart
- Install electron-reload package for automatic app reloading during development
- Configure electron-reload in main process with development-only activation
- Enable automatic restart when source files change during yarn dev
- Use hardResetMethod: 'exit' for clean app restarts
2025-09-15 21:25:42 +08:00
Vaayne
2f74becb31 feat(dependencies): add @anthropic-ai/claude-code package for enhanced functionality 2025-09-15 19:59:47 +08:00
Vaayne
b31ac74f96 refactor: remove obsolete claude-code files and update logger imports 2025-09-15 19:59:15 +08:00
Phantom
4d1d3e316f feat: use oxlint to speed up lint (#10168)
* build: add eslint-plugin-oxlint dependency

Add new eslint plugin to enhance linting capabilities with oxlint rules

* build(eslint): add oxlint plugin to eslint config

Add oxlint plugin as recommended in the documentation to enhance linting capabilities

* build: add oxlint v1.15.0 as a dependency

* build: add oxlint to linting commands

Add oxlint alongside eslint in test:lint and lint scripts for enhanced static analysis

* build: add oxlint configuration file

Configure oxlint with a comprehensive set of rules for JavaScript/TypeScript code quality checks

* chore: update oxlint configuration and related settings

- Add oxc to editor code actions on save
- Update oxlint configs to use eslint, typescript, and unicorn presets
- Extend ignore patterns in oxlint configuration
- Simplify oxlint command in package.json scripts
- Add oxlint-tsgolint dependency

* fix: lint warning

* chore: update oxlintrc from eslint.recommended

* refactor(lint): update eslint and oxlint configurations

- Add src/preload to eslint ignore patterns
- Update oxlint env to es2022 and add environment overrides
- Adjust several lint rule severities and configurations

* fix: lint error

* fix(file): replace eslint-disable with oxlint-disable in sanitizeFilename

The linter was changed from ESLint to oxlint, so the directive needs to be updated accordingly.

* fix: enforce stricter linting by failing on warnings in test:lint script

* feat: add recommended ts-eslint rules into exlint

* docs: remove outdated comment in oxlint config file

* style: disable typescript/no-require-imports rule in oxlint config

* docs(utils): fix comment typo from NODE to NOTE

* fix(MessageErrorBoundary): correct error description display condition

The error description was incorrectly showing in production and hiding in development. Fix the logic to show detailed errors only in development mode

* chore: add oxc-vscode extension to recommended list

* ci(workflows): reorder format check step in pr-ci.yml

* chore: update yarn.lock
2025-09-15 19:42:13 +08:00
Phantom
e5ccf68476 feat: use biome to format files (#10170)
* build: add @biomejs/biome as a dependency

* chore: add biome extension to vscode recommendations

* chore: migrate from prettier to biome for code formatting

Update VSCode settings to use Biome as the default formatter for multiple languages
Add Biome to code actions on save and reorder search exclude patterns

* build: add biome.json configuration file for code formatting

* build: migrate from prettier to biome for formatting

Update package.json scripts and biome.json configuration to use biome instead of prettier for code formatting. Adjust biome formatter includes/excludes patterns for better file matching.

* refactor(eslint): remove unused prettier config and imports

* chore: update biome.json configuration

- Enable linter and set custom rules
- Change jsxQuoteStyle to single quotes
- Add json parser configuration
- Set formatWithErrors to true

* chore: migrate biome config from json to jsonc format

The new jsonc format allows for comments in the configuration file, making it more maintainable and easier to document configuration choices.

* style(biome): update ignore patterns and jsx quote style

Update file ignore patterns from `/*` to `/**` for consistency
Change jsxQuoteStyle from single to double quotes for alignment with project standards

* refactor: simplify error type annotations from Error | any to any

The change standardizes error handling by using 'any' type instead of union types with Error | any, making the code more consistent and reducing unnecessary type complexity.

* chore: exclude tailwind.css from biome formatting

* style: standardize quote usage and fix JSX formatting

- Replace single quotes with double quotes in CSS imports and selectors
- Fix JSX element closing bracket alignment and formatting
- Standardize JSON formatting in package.json files

* Revert "style: standardize quote usage and fix JSX formatting"

This reverts commit 0947f8505d.

* fix: remove json import assertion for biome compatibility

The import assertion syntax is not supported by biome, so it was replaced with a standard import statement.

* style: change quote styles in biome.jsonc to use single quotes for JSX and double quotes for JS

* style: change quote style from double to single in biome config

* style: change JSX quote style from single to double

* chore: update biome.jsonc to use single quotes for CSS formatting

* chore: update biome config and format commands

- Exclude tailwind.css from linter includes
- Add biome lint to format commands

* style: format JSX closing brackets for better readability

* style: set bracketSameLine to true in biome config

The change aligns with common JSX formatting preferences where brackets on the same line improve readability for many developers

* Revert "style: format JSX closing brackets for better readability"

This reverts commit d442c934ee.

* style: format code and clean up whitespace

- Remove unnecessary whitespace in CSS and TS files
- Format package.json files to consistent style
- Reorder tsconfig.json properties alphabetically
- Improve code formatting in React components

* style(biome): update biome.jsonc config with clearer comment

Add explanation for keeping bracketSameLine as true to minimize changes in current PR while noting false would be better for future

* chore: remove prettier dependency and format package.json files

- Remove prettier from dependencies as it's no longer needed
- Reformat package.json files for better readability

* chore: replace prettier with biome for code formatting

Remove all prettier-related configuration, dependencies, and references
Update formatting scripts and documentation to use biome instead
Adjust electron-builder config to exclude biome.jsonc

* build: replace prettier with biome for formatting

Use biome as the default formatter instead of prettier for better performance and modern tooling support

* ci(i18n): replace prettier with biome for i18n formatting

Update the auto-i18n workflow to use Biome instead of Prettier for formatting translated files. This change simplifies the dependencies by removing multiple Prettier plugins and using a single tool for formatting.

* fix(i18n): Auto update translations for PR #10170

* style: format package.json files by consolidating array formatting

Consolidate multi-line array formatting into single-line format for better readability and consistency across package.json files

* Revert "fix(i18n): Auto update translations for PR #10170"

This reverts commit a7edd32efd.

* ci(workflows): specify biome config path in auto-i18n workflow

* chore: update biome.jsonc to use lexicographic sort order for json keys

* ci(workflows): update biome format command to use --config-path flag

* chore: exclude package.json from biome formatting

* ci: update biome.jsonc linter configuration

Update linter includes to target specific files and modify useSortedClasses rule

* chore: reorder search exclude patterns in vscode settings

* style(OGCard): reorder tailwind classes for consistent styling

* fix(biome): update tailwind classes sorting to safe and warn level

* docs(dev): update ide setup instructions in dev docs

Replace Prettier with Biome as the recommended formatter and clarify editor options

* build(extension-table-plus): replace prettier with biome for formatting

- Add biome.json configuration file
- Update package.json to use biome instead of prettier
- Remove prettier from dependencies
- Update lint script to use biome format

* chore: replace biome.json with biome.jsonc for extended configuration

Update biome configuration file to use JSONC format for comments and more detailed settings

* chore: remove unused biome.jsonc configuration file

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-15 19:21:15 +08:00
Chen Tao
e3d2bb2ec6 feat: remove faiss database (#10178) 2025-09-15 17:59:46 +08:00
beyondkmp
7f9f5514a4 feat(CodeTools): enhance OpenAI Codex integration with configurable parameters (#10180)
* feat(CodeTools): enhance OpenAI Codex integration with configurable parameters

- Added support for custom OpenAI model provider and model configuration in CodeToolsService.
- Updated CodeToolsPage to filter providers based on the selected CLI tool, including OpenAI Codex.
- Introduced OPENAI_CODEX_SUPPORTED_PROVIDERS constant for better provider management.
- Refactored environment variable generation to include OpenAI-specific settings.

* fix(CodeTools): correct environment variable generation for OpenAI Codex integration

- Added a break statement in the environment generation logic for OpenAI Codex to ensure proper handling of API key and base URL.
- Moved the import of codeTools to maintain consistency in the CodeToolsPage component.
2025-09-15 17:32:40 +08:00
Phantom
d5487ba6ac fix(error): improve error response body parsing and message handling (#10181)
* fix(error): improve error response body parsing and message handling

Handle JSON parsing of error response bodies and extract internal messages when available. Combine messages when both top-level and internal messages exist.

* refactor(error): simplify response body assignment in serializeError

Remove redundant conditional logic and directly assign error.responseBody to serializedError.responseBody

* fix(serializeError): handle responseBody assignment consistently

Ensure responseBody is always assigned from error.responseBody when available, otherwise stringify the body. This prevents potential undefined behavior when error.responseBody exists but body is not available.
2025-09-15 16:41:03 +08:00
Vaayne
54b4e6a80b feat(agents): implement Drizzle ORM for database management and schema synchronization 2025-09-15 12:01:29 +08:00
Vaayne
079d2c3cb3 feat(docs): update CLAUDE.md to enhance AI assistant guidelines and add session tracking details 2025-09-15 11:34:19 +08:00
Vaayne
911f9d8bc9 feat(agents): update README for migration commands and remove obsolete database module documentation 2025-09-15 09:39:47 +08:00
fullex
95ed17aa69 Merge branch 'main' of github.com:CherryHQ/cherry-studio 2025-09-14 23:57:57 +08:00
fullex
4f64afd8f4 chore: update .prettierignore to include additional directories and file types for improved formatting consistency 2025-09-14 23:57:50 +08:00
Vaayne
f90bda861f feat(agents): add automatic database schema synchronization
- Add schemaSyncer.ts with Drizzle Kit push integration
- Integrate auto schema sync into BaseService.initialize()
- Database schema now automatically updates on agent service startup
- Users no longer need manual migration commands
- Ensures schema consistency across app updates

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2025-09-14 23:07:54 +08:00
Jason Young
4f8b5c1250 QuickPanel/Inputbar: refine Enter handling with collapsed panel (#9781)
- QuickPanel (view.tsx): do not intercept Enter/NumpadEnter when the panel is visible but collapsed (no non-pinned matches), so the event falls through to the input bar and sending works.
- Inputbar (Inputbar.tsx): prioritize the configured send shortcut on Enter; remove pre-blocking based on quickPanel visibility; keep Shift+Enter as native newline; insert newline for other Enter variants.
- Update inline comments to clearly document the behavior.
2025-09-14 22:36:20 +08:00
Vaayne
71ed94de31 docs: streamline CLAUDE.md and add session tracking
- Reduce file size by 53% while keeping essential info
- Add session tracking requirements for plan mode
- Add Must Follow Rules section with conditional ast-grep usage
- Consolidate architecture to multi-file concepts only

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2025-09-14 22:30:04 +08:00
fullex
6eccb92817 chore: add CODEOWNERS file to improve pr reviews handling for data refactor 2025-09-14 21:05:39 +08:00
fullex
734ddd7489 Update pr-ci.yml to include v2 branch 2025-09-14 18:41:19 +08:00
GitHub Action
dc16cf2aa7 fix(i18n): Auto update translations for PR #10096 2025-09-14 09:12:20 +00:00
Vaayne
be12898b7b feat(claude-code): initialize project with Bun setup, including configuration files and example code 2025-09-14 17:09:37 +08:00
Vaayne
3fc92e093b feat(sessions): add session message creation endpoint and update session details 2025-09-14 17:09:32 +08:00
Vaayne
b6187ad637 feat(sessions): include session messages in session retrieval response 2025-09-14 17:09:19 +08:00
Vaayne
ca8ac9911e feat(i18n): add translations for agent and assistant features in multiple languages 2025-09-14 16:31:52 +08:00
Vaayne
95a1e210b6 Refactor: Remove sessions route and related validation logic
- Deleted the sessions route file, consolidating session management logic.
- Removed validation middleware and error handling for session operations.
- Updated the SessionMessageService to improve query formatting for retrieving session messages.
2025-09-14 16:31:15 +08:00
Phantom
ea4db1c864 refactor(context): move HeroUIProvider to context directory (#10155)
Centralize context providers in a dedicated directory for better organization and maintainability
2025-09-14 16:01:30 +08:00
Phantom
f9171f3df8 fix(ThemeProvider): set document language based on user settings (#10154)
* fix(ThemeProvider): set document language based on user settings

Add language from settings to ThemeProvider context and update document language attribute accordingly

* fix(ThemeProvider): add language to dependency array to prevent stale closures
2025-09-14 15:41:43 +08:00
icarus
b55f419a95 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-14 12:58:21 +08:00
icarus
8953961a51 feat(i18n): add plural forms for agent and assistant labels
Add singular and plural forms for "agent" and "assistant" labels in both English and Chinese locales
Display plural labels in AssistantsTab component
2025-09-14 12:53:57 +08:00
Phantom
9b1e9552d6 fix: correct disk quota display by converting bytes to GB (#10160)
* fix: correct disk quota display by converting bytes to GB

The free disk space was being logged in bytes instead of GB, making the log message misleading. Convert the value to GB for accurate reporting.

* fix: format disk quota log to 2 decimal places
2025-09-14 12:14:15 +08:00
Vaayne
8836663c35 🧪 test: add HTTP test files for agents and sessions APIs
- Add agents.http with comprehensive API endpoint tests
- Add sessions.http for session management testing
- Include authentication setup and request examples
- Support testing of CRUD operations for both agents and sessions

These files enable easy API testing and validation during development.
2025-09-14 10:17:45 +08:00
Vaayne
aaba77c360 feat: add comprehensive PUT/PATCH support for agents and sessions APIs
- Add PATCH method to agents API for partial updates alongside existing PUT
- Add PUT method to sessions API for complete replacement alongside existing PATCH
- Update API documentation with clear PUT vs PATCH usage examples
- Refactor session status updates to use standard PATCH endpoint
- Ensure both methods use same validation middleware for consistency
- Add comprehensive Swagger documentation for new endpoints

This provides REST-compliant update operations where:
- PUT: Complete resource replacement (idempotent)
- PATCH: Partial resource updates (only specified fields)

Both agents and sessions now support flexible update patterns for different use cases.
2025-09-14 10:17:45 +08:00
icarus
532bad8eb7 refactor(agents): migrate agent hooks to react-query and reorganize structure
- Replace redux-based agent management with react-query hooks
- Move agent-related hooks to dedicated agents directory
- Add useAddAgent, useRemoveAgent, and useUpdateAgent hooks
- Update imports to reflect new hook locations
- Keep redux store temporarily for backward compatibility
2025-09-14 09:51:00 +08:00
icarus
e5b43c8176 Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new 2025-09-14 09:21:18 +08:00
icarus
5f9c2d7f6a feat(agents): add edit functionality and update translations
- Refactor AddAgentModal into AgentModal to support both add and edit operations
- Add edit button to AgentItem with corresponding modal functionality
- Update translations for edit and update success messages
2025-09-14 09:16:04 +08:00
Vaayne
568257e7b6 docs(agents): add comprehensive UI integration guide for Agent API
- Document migration from Redux to database-backed API architecture
- Provide complete API reference for agents, sessions, and messages
- Include TypeScript interfaces and practical implementation examples
- Cover message streaming integration with AI SDK compatibility
- Add error handling patterns and best practices
- Include step-by-step migration guide from existing Redux implementation

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2025-09-14 08:25:51 +08:00
icarus
df31629c5f refactor(AgentItem): replace antd dropdown with custom context menu component
The antd dropdown component was replaced with a custom context menu implementation to improve consistency with the application's UI components and remove dependency on antd. The functionality remains the same but is now implemented using the custom context menu component.
2025-09-14 07:24:57 +08:00
icarus
e6dc8619d9 feat(ui): add context menu component using radix-ui
Implement a comprehensive context menu component with submenus, checkboxes, radio items, and separators. The component is built using @radix-ui/react-context-menu and includes proper styling and accessibility features.

Add required dependencies and update yarn.lock accordingly.
2025-09-14 07:17:53 +08:00
icarus
9a71a01b66 feat(agents): add agent management with delete confirmation
- Add agent deletion confirmation dialog with translations
- Implement agent list display and drag-and-drop functionality
- Include avatar support for agent types
- Add success message for agent deletion
2025-09-14 06:43:24 +08:00
icarus
941a6666e6 feat(agents): implement add agent functionality and migration
- Add migration for new agents structure (version 155)
- Enable addAgent call in AddAgentModal
- Update dependencies array with addAgent
- Replace TODO comment with FIXME for model type issue
2025-09-14 05:59:08 +08:00
icarus
1bf63865a8 Merge branch 'main' into feat/agents-new 2025-09-14 05:57:24 +08:00
icarus
ba41d8021f docs(AddAgentModal): add comment explaining modal button pattern
Add explanatory comment for combining Button and Modal components in Hero UI pattern to clarify focus management requirements
2025-09-14 04:44:53 +08:00
icarus
1e919a908f refactor(agent-popup): replace AddAgentPopup with AddAgentModal component
- Remove deprecated AddAgentPopup implementation
- Implement new AddAgentModal component with improved UI using Button component
- Update related imports and usage in AssistantsTab
- Clean up unused styles and code
2025-09-14 04:39:14 +08:00
icarus
702612e3f9 feat(agent): implement AddAgentPopup
- Add isAgentType function to validate agent type strings
- Update i18n files with new agent type validation messages
- Add common validation error messages and success notifications
2025-09-14 03:59:21 +08:00
icarus
fa380619ce chore: update vscode i18n settings and comments
Clean up i18n-related settings in vscode config by commenting out display language and fixing comment alignment
2025-09-14 03:56:02 +08:00
icarus
a30b2e2cb2 refactor(utils): move getClaudeSupportedProviders to utils/provider
Improve code organization by moving provider-related utility function to dedicated utils file
2025-09-14 03:18:44 +08:00
icarus
ae0cee9ef4 style: change outline property to outline-style for better clarity 2025-09-14 01:50:38 +08:00
icarus
dc9fb381f7 feat(agent): implement add agent popup functionality
add new AddAgentPopup component with type selection
move agent-related translations to dedicated section
update UI to use new popup instead of placeholder toast
2025-09-14 01:39:35 +08:00
icarus
943fccf655 refactor(useAgents): simplify addAgent_ by accepting complete agent entity 2025-09-14 00:15:45 +08:00
icarus
e0d2d44f35 feat(config): add default agent configuration constants
Add DEFAULT_AGENT_CONFIG and DEFAULT_CLAUDE_CODE_CONFIG constants to define base agent configurations. These will serve as templates for future agent configurations.
2025-09-14 00:13:22 +08:00
icarus
5a6413f356 feat(hooks): add useAgents hook for managing agent state
Implement a custom hook to handle agent CRUD operations in the store
2025-09-14 00:12:50 +08:00
icarus
f3ef4c77f5 feat(agents): add CRUD operations for agents in store
Add setAgents, addAgent, removeAgent and updateAgent actions to manage agents in the store. The updateAgent action uses lodash's mergeWith to handle array references properly and includes error logging when agent is not found.
2025-09-14 00:00:07 +08:00
icarus
751e391db6 refactor(assistant-presets): rename agents to assistant presets and update related components
- Rename agents to assistant presets across the codebase
- Update components, hooks, and pages to reflect the new naming
- Add new components for managing assistant presets
- Improve localization and grouping of presets
- Maintain existing functionality while updating the UI
2025-09-13 23:16:22 +08:00
icarus
3e04c9493f refactor(agents): rename agents to presets and add new agents array
The old 'agents' array was actually storing presets, so it's renamed for clarity. Added new 'agentsNew' array for actual agent entities in preparation for autonomous agent feature.
2025-09-13 22:52:10 +08:00
icarus
6b0a1a42ad refactor(types): rename Agent type to AssistantPreset for clarity
The type was renamed to better reflect its purpose as a preset configuration for assistants rather than representing an active agent. This change improves code readability and maintainability by using more accurate terminology throughout the codebase.
2025-09-13 22:43:26 +08:00
icarus
ee82b23886 feat(chat): add agent creation button with placeholder implementation
Add a new button for creating agents in the chat interface. The button is currently a placeholder with a "Not implemented" toast message. Includes necessary i18n translations and component props.
2025-09-13 21:49:24 +08:00
Phantom
c9bb3ff6f6 chore(store): bump persisted reducer version to 154 (#10148)
* chore(store): bump persisted reducer version to 154

* fix(i18n): Auto update translations for PR #10148

* Update fr-fr.json

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Pleasure1234 <3196812536@qq.com>
2025-09-13 21:38:24 +08:00
Licardo
993d497aad feature: add option to change font (#10133)
* feature: add option to change font

1. set app global font
2. set code block font

Signed-off-by: Albert Abdilim <albert.abdilim@foxmail.com>

* formatted code with Prettier

* fix ci errors

1.add migration in `migrate.ts`
2.add to-be-translated strings by running `yarn sync:i18n`

* chore: update yarn.lock to include font-list package version 2.0.0

* fix migration issue

---------

Signed-off-by: Albert Abdilim <albert.abdilim@foxmail.com>
Co-authored-by: suyao <sy20010504@gmail.com>
2025-09-13 20:36:13 +08:00
Vaayne
0d2dc2c257 🏗️ refactor: migrate agents service from custom migrations to Drizzle ORM
- Replace custom migration system with modern Drizzle ORM implementation
- Add drizzle-orm and drizzle-kit dependencies for type-safe database operations
- Refactor BaseService to use Drizzle client with full type safety
- Create schema definitions in /database/schema/ using Drizzle patterns
- Remove legacy migration files, queries, and migrator classes
- Add comprehensive documentation for new Drizzle-based architecture
- Maintain backward compatibility in service layer APIs
- Simplify database operations with modern ORM patterns

This migration eliminates custom SQL generation in favor of a proven,
type-safe ORM solution that provides better developer experience and
maintainability.
2025-09-13 19:51:16 +08:00
Vaayne
c785be82dd ♻️ refactor: rename SessionLog to SessionMessage for semantic clarity
- Rename SessionLogEntity → SessionMessageEntity type definition
- Rename SessionLogService → SessionMessageService with all methods
- Rename API routes /logs → /messages for better REST semantics
- Update database queries and service layer naming
- Update all Swagger documentation and validation middleware
- Maintain backward compatibility in database schema

This improves code readability by using more accurate terminology
for conversational message data rather than generic "log" naming.
2025-09-13 12:06:02 +08:00
SuYao
80afb3a86e feat: add Perplexity SDK integration (#10137)
* feat: add Perplexity SDK integration

* feat: enhance AiSdkToChunkAdapter with web search capabilities

- Added support for web search in AiSdkToChunkAdapter, allowing for dynamic link conversion based on provider type.
- Updated constructor to accept provider type and web search enablement flag.
- Improved link handling logic to buffer and process incomplete links.
- Enhanced message block handling in the store to accommodate new message structure.
- Updated middleware configuration to include web search option.

* fix

* fix

* chore: remove unuseful code

* fix: ci

* chore: log
2025-09-13 00:06:18 +08:00
Phantom
f5d8974d04 fix(provider): wrong new api provider id (#10136)
refactor(provider): wrap system provider checks in isSystemProvider

Centralize system provider checks to improve maintainability and reduce code duplication
2025-09-12 20:57:32 +08:00
Vaayne
a4bb82a02d 📝 docs: update validation and refactoring documentation for agents service 2025-09-12 18:00:33 +08:00
Vaayne
e8c94f3584 Merge remote-tracking branch 'origin/main' into feat/agents-new 2025-09-12 17:56:10 +08:00
Vaayne
d123eec476 ♻️ refactor: eliminate database schema redundancy + add comprehensive documentation
Refactor database to migration-only approach and add complete documentation

### Database Architecture Improvements:
- **Remove redundant schema files**: Eliminated duplicate table/index definitions
- **Single source of truth**: Migration files now exclusively define database schema
- **Simplified maintenance**: No more sync issues between schema files and migrations

### Files Removed:
- `database/schema/tables.ts` - Redundant table definitions
- `database/schema/indexes.ts` - Redundant index definitions

### Files Updated:
- `database/schema/index.ts` - Now only exports migration utilities
- `database/index.ts` - Simplified exports, removed redundant schema references
- `BaseService.ts` - Updated documentation for migration-only approach
- `migrator.ts` - Enhanced documentation and clarity

### Documentation Added:
- **`database/README.md`** - Comprehensive 400+ line guide covering:
  - Architecture overview and migration-only approach
  - Complete directory structure explanation
  - Migration system lifecycle with diagrams
  - Query organization and API reference
  - Development workflow and best practices
  - Troubleshooting guide and examples

### Benefits:
-  Eliminated redundancy between schema and migration files
-  Reduced maintenance overhead and potential sync issues
-  Established single source of truth for database schema
-  Added comprehensive documentation for team development
-  Maintained full backward compatibility
-  All tests continue to pass (1420/1420)

The database system now follows industry best practices with migrations as the sole
schema definition method, while providing complete documentation for developers.
2025-09-12 17:54:12 +08:00
Vaayne
002a443281 🏗️ refactor: restructure agents service with migration system and modular architecture
BREAKING CHANGE: Major refactoring of agents service structure
- Split monolithic db.ts into focused query modules (agent, session, sessionLog)
- Implement comprehensive migration system with transaction support
- Reorganize services into dedicated services/ subdirectory
- Add production-ready schema versioning with rollback capability

### New Architecture:
- database/migrations/: Version-controlled schema evolution
- database/queries/: Entity-specific CRUD operations
- database/schema/: Table and index definitions
- services/: Business logic layer (AgentService, SessionService, SessionLogService)

### Key Features:
-  Migration system with atomic transactions and checksums
-  Modular query organization by entity type
-  Backward compatibility maintained for existing code
-  Production-ready rollback support
-  Comprehensive validation and testing

### Benefits:
- Single responsibility: Each file handles one specific concern
- Better maintainability: Easy to locate and modify entity-specific code
- Team-friendly: Reduced merge conflicts with smaller focused files
- Scalable: Simple to add new entities without cluttering existing code
- Production-ready: Safe schema evolution with migration tracking

All existing functionality preserved. Comprehensive testing completed (1420 tests pass).
2025-09-12 17:31:30 +08:00
Vaayne
64f3d08d4e ♻️ refactor: split AgentService into focused service modules
- **BaseService**: Shared database connection and JSON serialization utilities
- **AgentService**: Agent management operations (CRUD for agents)
- **SessionService**: Session management operations (CRUD for sessions)
- **SessionLogService**: Session log management operations (CRUD for session logs)

Updated API routes to use appropriate services:
- sessions.ts now uses SessionService for session operations
- session-logs.ts now uses SessionLogService and SessionService as needed
- Maintains backward compatibility with existing API endpoints

Benefits:
- Single Responsibility Principle - each service has a clear focus
- Better code organization and maintainability
- Easier testing and debugging
- Improved separation of concerns
- Shared database infrastructure via BaseService

All TypeScript compilation and build checks pass.
2025-09-12 16:25:50 +08:00
Vaayne
9c956a30ea feat: initialize AgentService in ApiServer and improve ID generation logic 2025-09-12 16:04:04 +08:00
Phantom
276269e583 style(MinApp): responsive layout for MinAppPage (#10125)
style(MinApp): adjust container styles for better layout

- Add min-height to MinApp container
- Remove absolute positioning from search bar
- Improve flex and overflow handling in containers
- Adjust margins and widths for better spacing
2025-09-12 16:00:39 +08:00
Vaayne
5eaa90a7a2 feat: implement comprehensive CRUD APIs for agent management with type support 2025-09-12 15:27:36 +08:00
SuYao
8f36c4e793 fix: referer (#10124)
* fix: referer

* fix(i18n): Auto update translations for PR #10124

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-12 13:31:18 +08:00
Pleasure1234
5f999d3c84 fix: improve notes sidebar functionality and i18n text updates (#10112)
* fix: improve notes sidebar functionality and i18n text updates

- Update breadcrumb navigation with clickable folder navigation and proper overflow handling
- Add file selection dialog for markdown imports alongside drag-and-drop
- Improve button order in sidebar header (new note first, then new folder)
- Update i18n text: "markdown" → ".md" for clarity and "star" → "favorite note" for consistency
- Fix file upload logic for better parent node handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: code review

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-12 12:56:30 +08:00
one
d68aafea15 fix: infinite loops in KnowledgeBaseButton and MentionModelsButton (#10118) 2025-09-12 09:30:47 +08:00
one
e093ae72da fix: toast style across windows (#10113) 2025-09-11 22:23:38 +08:00
Phantom
d17362d8c4 refactor(OGCard): replace static image with dynamic generated graph (#10115)
* refactor(OGCard): replace static image with dynamic generated graph

- Replace CherryLogo import with GeneratedGraph component for dynamic preview
- Extract image height to constant for consistency
- Use useCallback for GeneratedGraph to optimize performance

* chore: remove unused banner.png asset

* style(OGCard): change image height from pixels to rem units

Use rem units for better responsiveness and consistency with the design system
2025-09-11 22:20:52 +08:00
Phantom
5e19e7ac6c fix(toolUsePlugin): handle empty tools case in prompt generation (#10111)
Return null when no tools are available and skip tool section in system prompt
2025-09-11 21:21:55 +08:00
beyondkmp
871565c687 feat: add data limit warning notification (#8866)
* feat: add disk space checking functionality

- Introduced a new IPC channel for retrieving disk information.
- Integrated the 'check-disk-space' package to fetch available and total disk space.
- Updated the preload API to expose the new disk info retrieval method to the renderer.

* feat: implement disk space warning and data limit checks

- Added functionality to check available disk space and display warnings when storage is low.
- Updated IPC methods to pass directory paths for disk info retrieval.
- Introduced periodic checks for app data disk quota and internal storage quota.
- Enhanced user notifications with localized messages for low storage warnings.

* fix: enhance disk space warning logic and improve logging

- Added additional conditions for displaying disk space warnings based on free percentage thresholds.
- Improved logging format for app data disk quota, providing clearer output in GB.
- Refactored the checkDataLimit function to be asynchronous for better performance.

* format code

* update log format

* fix: improve error handling and logging in disk quota checks

- Added try-catch block in checkAppDataDiskQuota to handle potential errors when retrieving disk information.
- Ensured that errors are logged for better debugging and visibility.
- Updated checkDataLimit to await the checkAppDataDiskQuota function for proper asynchronous handling.

* fix comments

* fix: remove redundant appStorageQuota message from localization files

* lint

* fix: enhance disk space warning logic for USB disks

- Added a condition to warn users when free space on USB disks falls below 5% of total capacity.
- Improved the existing logic for displaying disk space warnings based on total disk size thresholds.

* update i18n

* Refactor data limit notification logic and update i18n messages for disk space warnings. Adjusted check intervals and improved toast notifications for low disk space alerts.

* Fix disk quota check logic in useDataLimit hook to correctly compare free space against 1GB threshold.

* refactor: update styles and improve navbar handling

- Removed unnecessary margin-bottom style from bubble markdown.
- Adjusted margin in Prompt component for better layout.
- Enhanced useAppInit hook to include navbar position logic for background styling.
- Added alignment to ErrorBlock alert for improved visual consistency.

* refactor: relocate checkDataLimit function to utils and update import in useAppInit hook

- Moved checkDataLimit function from useDataLimit hook to utils for better organization.
- Updated import path in useAppInit to reflect the new location of checkDataLimit.
- Removed the now obsolete useDataLimit hook file.

* refactor: update getDiskInfo API to specify return type

- Enhanced getDiskInfo function to explicitly define the return type as a Promise containing disk information or null.

* lint err

* fix: handle null response from getDiskInfo in checkAppDataDiskQuota

- Added a check for null response from getDiskInfo to prevent errors.
- Updated the logic to extract the free disk space only if diskInfo is valid.

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-09-11 21:04:20 +08:00
SuYao
6104b7803b refactor: MCPService for improved error handling and header management (#10100)
* refactor MCPService for improved error handling and header management

* refactor MCPService: reorder header preparation for improved clarity

* refactor: enhance MCP server type determination and clean up error handling
2025-09-11 20:52:13 +08:00
one
95a332f38a fix: api key hints (#10109) 2025-09-11 19:05:41 +08:00
one
4c4cc52c07 fix: websearch rag concurrency (#10107)
* fix(WebSearchService): serialize rag operations

* refactor: improve a hint in websearch settings
2025-09-11 19:05:17 +08:00
Pleasure1234
4714442d6e feat: add note folder upload (#9996)
* feat: add note folder upload

* Update NotesService.ts

* Update NotesService.ts

* Update NotesService.ts

* fix: window message warning

* test: ci auto i18n

* fix(i18n): Auto update translations for PR #9996

* fix: ci i18n error

* fix: i18n

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-11 16:59:39 +08:00
SuYao
66115ca306 refactor: update Scrollbar component and integrate horizontal scrolling in TabContainer and KnowledgeBaseInput (#9988)
* refactor: update Scrollbar component and integrate horizontal scrolling in TabContainer and KnowledgeBaseInput

- Renamed Props interface to ScrollbarProps for clarity.
- Implemented useHorizontalScroll hook in TabContainer to manage horizontal scrolling.
- Removed deprecated scroll handling logic and replaced it with the new hook.
- Enhanced KnowledgeBaseInput to utilize horizontal scrolling for better UI management.
- Cleaned up unused imports and components for improved code maintainability.

* refactor: update dependencies type in useHorizontalScroll hook to readonly unknown[] for better type safety

* feat: add scrollDistance parameter to useHorizontalScroll hook for customizable scrolling behavior

* refactor: replace useHorizontalScroll with HorizontalScrollContainer in TabContainer, KnowledgeBaseInput, and MentionModelsInput components

- Updated TabContainer to utilize HorizontalScrollContainer for improved scrolling functionality.
- Refactored KnowledgeBaseInput and MentionModelsInput to replace the custom horizontal scroll implementation with HorizontalScrollContainer, simplifying the code and enhancing maintainability.

* refactor(HorizontalScrollContainer): remove paddingRight prop and update scroll handling

- Removed the unused paddingRight prop from HorizontalScrollContainerProps and its implementation.
- Updated handleScrollRight to accept the event parameter and stop propagation.
- Simplified the Container styled component by eliminating the padding-right style.

* fix: sync issue

* fix: isLeftNavbar inputbar display issue

* feat(HorizontalScrollContainer): add scroll end detection and disable button hover effect

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-09-11 16:56:37 +08:00
kangfenmao
7fec4c0dac refactor: update styles and improve navbar handling
- Removed unnecessary margin-bottom style from bubble markdown.
- Adjusted margin in Prompt component for better layout.
- Enhanced useAppInit hook to include navbar position logic for background styling.
- Added alignment to ErrorBlock alert for improved visual consistency.
2025-09-11 15:13:43 +08:00
Vaayne
e3f5033bc4 chore: add express-validator dependency
Add express-validator for API request validation and data sanitization
in agent management endpoints.
2025-09-11 14:35:54 +08:00
Vaayne
2ec3b20b23 🚀 feat: add comprehensive REST API for agent management
Implement full REST API with Express routes for agents, sessions, and logs:
- CRUD operations for agents with validation and OpenAPI documentation
- Session management with nested resource endpoints
- Hierarchical logging system with bulk operations support
- Request validation using express-validator
- Proper error handling and structured responses
2025-09-11 14:35:34 +08:00
Vaayne
d26d02babc feat: implement AgentService for autonomous agent management
Add comprehensive agent service with full CRUD operations, session management,
and structured logging capabilities. Includes database operations for agents,
sessions, and hierarchical log entries with proper type definitions.
2025-09-11 14:35:14 +08:00
Vaayne
675671688b 📝 docs: add ast-grep tool guidance to CLAUDE.md
Add guidance to use ast-grep for code pattern searches when available,
improving development workflow documentation.
2025-09-11 14:34:55 +08:00
Vaayne
bcdd48615d 🗃️ feat: implement comprehensive agent database schema and queries
- Add complete SQL schema for agents, sessions, and session_logs tables
- Implement CRUD operations for all agent-related entities
- Add SessionLogEntity type with hierarchical logging support
- Include proper indexes and foreign key constraints for performance
- Support agent configuration inheritance in sessions via COALESCE
- Add metadata field for extensible session log tracking
2025-09-11 13:10:31 +08:00
Vaayne
1f974558f8 feat: enhance agent and session entity types with additional properties 2025-09-11 12:28:43 +08:00
Vaayne
0f1ad59e58 feat: add agent and session entity types 2025-09-11 10:00:29 +08:00
LiuVaayne
2e31a5bbcb Feat/api server (#9855)
* feat: add api server

This reverts commit c76aa03566.

* update yarn.lock

* fix: correct import paths in ToolSettings component

Update import paths for PreprocessSettings and WebSearchSettings to reference correct locations in DocProcessSettings and WebSearchSettings directories.

* feat(settings): add API server settings link and route

* fix(auth): improve authorization handling and error responses

* feat(chat): enhance model validation and logging for chat completions
feat(models): improve logging for model retrieval and filtering
feat(utils): add model ID validation and support for OpenAI providers

* feat(api-server): refactor config loading and remove unused ToolSettings component

* refactor(ApiServerService): simplify config retrieval and improve error handling in ApiServerSettings

* fix(mcp): remove unnecessary await in listTools return statement

* refactor(ApiServerSettings): replace window.message with window.toast for notifications

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-09-11 09:51:29 +08:00
Phantom
d6a320490a ci(github-actions): update workflow permissions for claude-translator (#10080)
Update pull-requests permission from read to write and add allowed_non_write_users config
Add security warning comment about fine-grained token control
2025-09-10 23:27:15 +08:00
Phantom
125353c5a3 fix: don't review on draft pr (#10084) 2025-09-10 20:51:59 +08:00
Phantom
cf7584bb63 refactor(toast): migrate message to toast (#10023)
* feat(toast): add toast utility functions to global interface

Expose error, success, warning, and info toast functions globally for consistent notification handling

* refactor(toast): use ToastPropsColored type to enforce color consistency

Create ToastPropsColored type to explicitly omit color property from ToastProps

* refactor(toast): simplify toast functions using factory pattern

Create a factory function to generate toast functions instead of repeating similar code. This improves maintainability and reduces code duplication.

* fix: replace window.message with window.toast for copy notifications

* refactor(toast): update type definition to use Parameters utility

Use Parameters utility type to derive ToastPropsColored from addToast parameters for better type safety

* feat(types): add RequireSome utility type for making specific properties required

* feat(toast): add loading toast functionality

Add loading toast type to support promises in toast notifications. This enables showing loading states for async operations.

* chore: add claude script to package.json

* build(eslint): add packages dist folder to ignore patterns

* refactor: migrate message to toast

* refactor: update toast import path from @heroui/react to @heroui/toast

* docs(toast): add JSDoc comments for toast functions

* fix(toast): set default timeout for loading toasts

Make loading toasts disappear immediately by default when timeout is not specified

* fix(translate): replace window.message with window.toast for consistency

Use window.toast consistently across the translation page for displaying notifications to maintain a uniform user experience and simplify the codebase.

* refactor: remove deprecated message interface from window

The MessageInstance interface from antd was marked as deprecated and is no longer needed in the window object.

* refactor(toast): consolidate toast utilities into single export

Move all toast-related functions into a single utility export to reduce code duplication and improve maintainability. Update all imports to use the new utility function.

* docs(useOcr): remove redundant comment in ocr function

* docs: update comments from Chinese to English

Update error log messages in CodeToolsPage to use English instead of Chinese for better consistency and maintainability

* feat(toast): add no-drag style and adjust toast placement

add custom CSS class to disable drag on toast elements
move toast placement to top-center and adjust timeout settings
2025-09-10 15:16:53 +08:00
MyPrototypeWhat
e10042a433 Feat/provider options and built-in tools (#10068)
* refactor(RuntimeExecutor, PluginEngine): streamline parameter handling and improve type definitions for model operations

- Updated parameter handling in RuntimeExecutor methods to use specific types for generate and stream functions.
- Refactored PluginEngine methods to enhance type safety and reduce redundancy in model resolution.
- Introduced new type definitions for generate and stream parameters in types.ts for better clarity and maintainability.
- Adjusted provider options mapping in buildProviderOptions to accommodate new provider types.

* feat(googleToolsPlugin): enhance Google tools integration and update dependencies

- Updated the `ai` package version to `5.0.38` and `@ai-sdk/gateway` to `1.0.20` in `package.json` and `yarn.lock`.
- Introduced `googleToolsPlugin` with improved parameter handling and configuration options for Google tools.
- Added support for `urlContext` in middleware configuration and plugin builder.
- Refactored web search tool to streamline response handling and citation formatting.
- Updated various service methods to include `enableUrlContext` capability.

* fix: update tool response handling and clean up unused code

- Modified the `processKnowledgeReferences` function to accept the full response object instead of just `knowledgeReferences`.
- Cleaned up the `searchOrchestrationPlugin` by removing unnecessary blank lines.
- Removed commented-out code in `telemetryPlugin` to improve readability.
- Updated `KnowledgeSearchTool` to streamline the execution flow and return results more efficiently.
- Adjusted `MessageKnowledgeSearch` components to reflect changes in the data structure returned from the knowledge search tool.
- Enhanced `MemorySearchTool` by simplifying error handling and removing redundant code.

* refactor: clean up KnowledgeSearchTool and WebSearchTool by removing commented-out code

- Removed unnecessary commented-out code in `KnowledgeSearchTool` and `WebSearchTool` to improve code readability and maintainability.
- Simplified the `processKnowledgeReferences` function by eliminating the console log statement for cleaner output.

* chore: bump version to 1.0.0-alpha.14 in aiCore package.json

* chore: update @ai-sdk/google-vertex to version 3.0.25 and add @ai-sdk/anthropic@2.0.15 to dependencies

- Bumped the version of `@ai-sdk/google-vertex` in `package.json` and `yarn.lock` to 3.0.25.
- Added `@ai-sdk/anthropic` version 2.0.15 to `yarn.lock` with updated dependencies.
- Refactored `parameterBuilder.ts` to integrate new tools from `@ai-sdk/google-vertex` for enhanced functionality.
2025-09-10 11:08:34 +08:00
George·Dong
3eee8faad4 fix(minapps): optimize minapps (#10076)
* fix(minapp): simplify webview ref handling and use toast for copy

* fix(minapp): add body border radius to popup container
2025-09-10 09:38:34 +08:00
RieN 7z
42eb61434d fix: Navbar css (#10025)
* fix: NavbarContainer padding-left in fullscreen

* fix: use `NavbarIcon` in mac search icon

* fix: NarrowIcon in KnowledgeContent

* fix: Fix NavbarIcon & NarrowIcon for search & expand btn

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-09-10 03:56:48 +08:00
beyondkmp
2962ef79dc refactor(Navbar): improve WindowControls visibility and clean up event listener management (#10066)
* refactor(Navbar): improve WindowControls visibility and clean up event listener management

- Updated Navbar component to conditionally render WindowControls based on minappShow state.
- Refactored IPC event listener management in preload script for better clarity and performance.

* feat(WindowControls): replace custom restore icon with a new SVG component

- Introduced a new `WindowRestoreIcon` component with enhanced SVG structure and styling.
- Updated `WindowControls` to use the new `WindowRestoreIcon` for better visual consistency and scalability.

* feat(WindowControls): update WindowRestoreIcon SVG for improved design

- Enhanced the SVG structure of the `WindowRestoreIcon` component with updated dimensions and styling for better visual appeal.
- Adjusted the path and rectangle properties to refine the icon's appearance and maintain consistency across the application.

* lint error
2025-09-09 23:18:15 +08:00
Phantom
87bdfbeeeb ci(workflow): update github token secret in claude translator workflow (#10074) 2025-09-09 22:54:51 +08:00
Phantom
493b0d4a11 ci(claude-translator): add github_token to workflow for authentication (#10053)
* ci(claude-translator): add github_token to workflow for authentication

* ci(workflows): restrict code review to main repo PRs

Fix OIDC issues by only triggering reviews for PRs from the main repository

* ci(workflows): re-enable issues trigger for claude translator

The upstream bug has been fixed, so we can now re-enable the issues trigger.
Also update the claude-code-action to use main branch instead of v1 tag.
2025-09-09 20:32:41 +08:00
RieN 7z
0c589a6f79 feat: open message text file attachment in preview (#9644)
* feat: add text file preview (#7023)

* feat: open message text file attachment in preview

* refractor: use `window.api.fs.readText`

* fix: use `FileTypes.TEXT`

* fix: trim prefix "file://" with `replace`

* refactor(FileAction): centralize file click handling for text preview

- Use FileAction.handleClick in AttachmentPreview and MessageAttachments
- Show i18n error modal on failure (zh-cn: files.click.error)

* fix: i18n

* fix: update i18n on field `files.click.error` with codex

* fix: use hook

* fix: rename `handleClick` to `preview`

* feat: support lang highlight

* fix: remove prefix '.' of extension

* fix: code editor style

* fix: editor cursor text style

* fix: add `FileTypes` check

* fix: move parseFileType into utils

* fix: move `parseFileTypes` into utils/file
2025-09-09 16:31:42 +08:00
Chen Tao
9b1aa3cd36 fix(websearch-rag): rag error (#10064) 2025-09-09 16:01:57 +08:00
George·Dong
de860ac316 fix/miniapp-tab-cache (#10024)
* feat(minapps): add Tabs-mode webview pool and integrate page shell

* fix(minapp): position tabs pool below toolbar and preserve layout

* style(minapp): fix format issues

* style(minapps): optimize var name

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat(minapps): stabilize tab webview lifecycle and mount logic

* refactor(minapps): improve webview detection and state handling

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-09 14:03:18 +08:00
Phantom
2495871c48 fix(translate): improve pasting and file processing (#10060)
* fix(translate): prevent default paste behavior only when handling files

Only call event.preventDefault() when handling file pasting to allow text pasting by default. This fixes the issue where text pasting was being blocked unnecessarily.

* fix(translate): append new text to existing content instead of replacing

Ensure OCR and file reading operations append new content to existing text rather than replacing it entirely. This maintains user's previous input when processing additional files.
2025-09-09 12:42:26 +08:00
SuYao
56c1851848 feat: add font size and table of contents settings to RichEditor (#10034)
* feat: add font size and table of contents settings to RichEditor

- Introduced font size customization in the RichEditor component, allowing users to adjust the font size for better readability.
- Added a toggle for displaying a table of contents in the editor settings.
- Updated localization files to include new settings descriptions.
- Enhanced the NotesSettings component with a slider for font size adjustment and a switch for the table of contents feature.
- Migrated state management to include new settings in the Redux store.

* feat: enhance CodeEditor with customizable font size and responsive layout

* feat: enhance markdown conversion to preserve square brackets

- Improved the htmlToMarkdown function to correctly handle and preserve wiki-style double brackets [[foo]] and single brackets [foo] while maintaining proper Markdown link syntax.
- Added unit tests to verify the preservation of these bracket formats during conversion.

* feat: enhance YamlFrontMatterNodeView with editor content check

* fix

* chore

* chore: bump store persistence version to 153

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-09 11:55:41 +08:00
George·Dong
86f9e93e97 fix(codetool): incorrect codetool workdir on macOS (#10056)
fix(codetool): run command and cd in same shell on macOS
2025-09-09 10:02:53 +08:00
Phantom
11502edad2 feat(translate): Auto copy when translation is done (#10032)
* feat(translate): add settings with autoCopy option to translate state

Add settings object to translate state to store user preferences like autoCopy functionality. Implement updateSettings reducer to handle settings updates.

* docs(translate): add todo comment for settings field

* refactor(translate): simplify settings update and expose in hook

Remove dependency on objectEntriesStrict and use Object.entries directly
Expose translate settings and update function in useTranslate hook

* fix(useTranslate): use dispatch to update

* feat(translate): add auto-copy setting for translated content

Add auto-copy functionality that automatically copies translated text to clipboard when enabled. Includes settings toggle in TranslateSettings component and integration with translate logic.

* chore: add tailwindcss file association to vscode settings

* feat(translate): add auto copy setting and improve switch styling

Add new translation strings for auto copy feature and apply consistent primary color to all switches in translation settings

* fix(theme): update hero UI primary color variable

Add --primary CSS variable to match --color-primary for hero UI components

* refactor(hooks): rename _updateSettings to handleUpdateSettings for clarity

Improve variable naming consistency and better reflect the function's purpose

* refactor(translate): simplify settings update using Object.assign

* fix(translate): handle clipboard write errors in copy functionality

Add error handling for clipboard operations to prevent silent failures and show user feedback when copy fails

* feat(i18n): add translation placeholders for new UI strings

Add new translation keys for front matter operations and auto-copy setting
Include additional Anthropic OAuth related messages for better user feedback

* fix(i18n): Auto update translations for PR #10032

* fix(i18n): correct translation errors in multiple language files

Fix incorrect translations and fill missing values in Japanese, Russian, Portuguese, French and Spanish localization files. Changes include correcting property name in Japanese, adding missing empty value in Russian, fixing editValue in Portuguese, correcting date and empty values in French, and fixing multiple terms in Spanish.

* fix: update error message in migration from 151 to 152

* fix(translate): await copy operation and show success message

Ensure the copy operation completes before proceeding and notify user of successful copy

* feat(translate): add delay timer for auto-copy functionality

Use setTimeoutTimer to introduce a 100ms delay before auto-copy to ensure UI stability

* fix(translate): increase modal width from 420 to 520 for better content display

* fix(ThemeProvider): ensure proper theme class is applied to body

Add logic to toggle 'light' and 'dark' classes on body element when theme changes

* fix(translate): only copy when success

* fix(translate): remove redundant error message display on translation failure

* fix(translate): handle abort and empty translation cases properly

Improve error handling for translation abort scenarios and empty responses. Show appropriate user messages when translation is aborted and properly handle NoOutputGeneratedError cases.

* fix(translate): handle translation errors by showing user-friendly message

Display a localized error message to users when translation fails instead of just logging it

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-09 00:49:20 +08:00
MyPrototypeWhat
f6ffd574bf refactor(ToolCall): refactor:mcp-tool-state-management (#10028) 2025-09-08 23:29:34 +08:00
beyondkmp
7df1060370 fix(Navbar): some bugs in old ui for custom max/min/close menu (#10045)
* feat(Navbar): add WindowControls for Windows and Linux, clean up NavbarCenter component

* feat(Navbar): add NavbarRight component for improved layout in MinAppsPage

* feat(Navbar): enhance layout and styling for WindowControls and Navbar components

* lint err

* fix new ui
2025-09-08 20:58:40 +08:00
RieN 7z
ce26828e41 style: remove heroui default background setting (#10029)
* style: remove heroui default background setting

* fix: import order

* fix: remove layer
2025-09-08 20:50:10 +08:00
Phantom
2bae30bd11 fix(translate): state management is passed when early return (#10022)
fix(translate): wrap file drop handling in async function to ensure setIsProcessing(false) is executed
2025-09-08 18:42:23 +08:00
fullex
e16db26d18 chore: update selection-hook dependency to version 1.0.12 in package.… (#10051)
chore: update selection-hook dependency to version 1.0.12 in package.json and yarn.lock
2025-09-08 18:36:16 +08:00
one
9f81a77943 feat(HtmlArtifacts): make fancy code block optional (#10043)
* feat(HtmlArtifacts): make fancy code block optional

* test: fix mocks
2025-09-08 16:02:28 +08:00
one
f9c60423a8 refactor(HtmlArtifacts): remove HeaderRight padding (#10042)
refactor(HtmlArtifacts): remove  HeaderRight padding
2025-09-08 14:10:21 +08:00
beyondkmp
0a554661ad refactor(Navbar): simplify and modularize button components for search and expand functionality (#10036)
* fix macos

* delete
2025-09-08 13:03:46 +08:00
beyondkmp
8a67a87461 fix(DataSettings): update dependency array in useEffect to prevent unnecessary re-renders (#10039) 2025-09-08 13:02:23 +08:00
SuYao
1cd89561ab refactor(logging): change logger level and remove unused log statements (#10030)
* refactor(logging): change logger level and remove unused log statements

- Updated logger level from info to silly in AiSdkToChunkAdapter for more granular logging.
- Removed unused logger statements in AiSdkMiddlewareBuilder and PluginBuilder to clean up the code.
- Enhanced condition check in ApiService to include prompt tool usage.

* chore
2025-09-08 11:54:14 +08:00
Carlton
79592e2c27 refactor: 重构大文件上传相关逻辑,适配OpenAI标准文件服务,增加qwen-long和qwen-doc系列模型原生上传支持 (#9997)
* refactor: 重构大文件上传相关逻辑,适配OpenAI标准文件服务,增加qwen-long和qwen-doc的原生上传支持

* chore: 优化大文件上传相关逻辑与类型,优化上传文件资源释放

* fix: 修复原生上传时,用户没有输入内容会导致错误的问题

* chore: 优化函数名称
2025-09-08 00:16:48 +08:00
Phantom
4a72d40394 chore: update CLAUDE.md with UI design migration details (#10021)
docs: update CLAUDE.md with UI design migration details

Add section about migrating from antd & styled-components to HeroUI, including link to HeroUI docs.
2025-09-07 22:27:06 +08:00
Phantom
95f6e4dd3b fix(selection): improve prompt of refine action (#9900)
fix(selection): 优化refine操作的提示词格式

将refine操作的提示词格式改为使用XML标签<INPUT>包裹用户输入内容,并明确要求输出语言与输入一致且不包含解释和XML标签
2025-09-07 21:05:56 +08:00
LeaderOnePro
778b8718c9 docs: fix spelling errors in README files (#10015)
- Fix Peplexity -> Perplexity in both English and Chinese README
- Fix Itapano -> Italiano in Chinese README language links
- Improve documentation accuracy and professionalism

Signed-off-by: LeaderOnePro <leaderonepro@outlook.com>
2025-09-07 20:34:14 +08:00
359156687
b81c3b8f1b fix: workaround for electron build issue on rpm package (#9986)
* fix: add workaround for electron build issue on rpm package

Signed-off-by: 33671 <error_z@yeah.net>

* fix format

Signed-off-by: 33671 <error_z@yeah.net>

---------

Signed-off-by: 33671 <error_z@yeah.net>
2025-09-07 20:27:23 +08:00
Phantom
1d8760742e ci(workflow): fix missing args (#10014)
ci(workflow): 更新claude-translator.yml中的claude_args参数格式
2025-09-07 20:21:00 +08:00
MyPrototypeWhat
eb00ceb1c7 style: update HeroUIProvider dimensions and reset body background color (#10013)
- Adjusted HeroUIProvider to use full height and width for better layout.
- Reset body background color to unset for improved styling consistency.
2025-09-07 18:33:57 +08:00
one
33f8ea5acb refactor(Sortable): improve sortable props, support custom modifiers (#9879)
* refactor(Sortable): improve props, support modifiers

* refactor: update id and index
2025-09-07 17:21:55 +08:00
MyPrototypeWhat
4b65dfa6ea feat: integrate HeroUI and Tailwind CSS for enhanced styling (#9973) 2025-09-07 16:49:30 +08:00
Phantom
0187f1780e Refactor/migrate zod v4 (#10002)
* build: 更新 zod 依赖至 v4.1.5

* refactor: 将zod导入从'zod/v4'更新为'zod'

统一使用zod主包而非v4子路径,简化依赖管理

* refactor: 统一使用命名导入方式导入zod库

* refactor(mcpServers): 更新RequestPayloadSchema的url和headers类型定义

* refactor(mcp): 将默认值从pipe移动到string以简化schema

默认值'stdio'从pipe操作中移动到string操作,使schema定义更清晰

* refactor(Markdown): 简化 CitationSchema 中 url 的验证方式

* refactor: 将类型断言改为使用 satisfies 操作符

* build: 更新 aiCore 的 zod 依赖至 v4.1.5

* refactor: 更新zod导入方式并简化基础provider类型定义

将zod从'zod/v4'更新为'zod'导入
将baseProviderSchema替换为更简洁的接口类型定义

* fix(providers): 优化provider配置的schema定义并添加注释

调整creator函数的类型定义,增加输入输出类型约束
2025-09-07 16:39:22 +08:00
SuYao
4a5f374b7c refactor(Messages): remove clean up clearTopic function (#9993) 2025-09-07 13:50:52 +08:00
Phantom
c4e22a23ea fix: add responsive design for sidebar tabs to prevent content cutoff (#9913)
* fix: add responsive design for sidebar tabs to prevent content cutoff at minimum width

- Add responsive CSS variables and media queries for narrow screens
- Update Container, AssistantItem, and TopicListItem to use responsive widths
- Ensures settings tab content remains visible at minimum window width (520px)
- Addresses issue where right sidebar content gets cut off on smaller screens

Fixes #9894

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Pleasure1234 <Pleasurecruise@users.noreply.github.com>

* fix(i18n): Auto update translations for PR #9913

* feat(样式): 添加响应式断点配置和媒体查询工具

添加 breakpoints.scss 文件定义断点变量和 CSS 变量
在 style.ts 中新增 breakpoints 配置和 media 工具函数,用于生成响应式媒体查询

* style: 添加 breakpoints.scss 到主样式文件

* style(styles): 添加响应式断点变量并更新引用

将 breakpoints.scss 重命名为 responsive.scss 并添加断点变量定义
更新 index.scss 中的引用以使用新的文件名

* style(color.scss): 移除未使用的窄屏幕助手宽度变量

* refactor(styles): 将布局变量从color.scss移动到responsive.scss并添加响应式设计

将布局相关的CSS变量从color.scss迁移到responsive.scss以更好地组织代码
添加针对大屏幕的响应式布局调整

* refactor(布局): 简化容器宽度设置并添加过渡动画

移除响应式宽度媒体查询,统一使用变量控制宽度
为容器和标签内容添加宽度过渡动画效果
调整导航栏图标顺序和样式

* style: 移除响应式宽度媒体查询以简化样式

* fix(style): 将媒体查询条件从min-width改为max-width

* refactor(style): 移除未使用的响应式样式工具代码

清理不再使用的断点常量和媒体查询工具函数,这些代码当前未被项目使用

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Pleasure1234 <Pleasurecruise@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-09-07 13:36:33 +08:00
Phantom
8b7776b545 CI: launch claude translate for all comments (#9983)
* ci(claude-translator): 临时禁用issues触发并更新条件

暂时禁用issues触发以解决上游bug
更新issue_comment的触发条件,简化claude_args参数
修改翻译结果的原始内容标题格式

* ci: 更新Claude翻译器工作流的名称和并发组配置

* fix(workflow): 修正Claude翻译工作流中的拼写错误

* ci(workflow): 更新claude-translator触发条件

添加对评论发送者类型的检查,避免机器人触发工作流
2025-09-07 13:36:01 +08:00
Phantom
f3787beade ci(workflow): remove code review on sync & add more env info (#9985)
ci(workflow): 简化claude代码审查工作流的触发条件

移除synchronize事件触发,仅保留pull_request opened事件
添加PR编号和仓库信息到审查提示中
2025-09-07 13:33:32 +08:00
RieN 7z
3607341413 fix: limit titlebar-area-* in mac (#9974)
* fix: limit titlebar-area-* in mac

* fix: unused padding-right
2025-09-07 10:19:17 +08:00
George·Dong
b33b14b4b7 fix(parameter-builder): avoid enabling built-in web search when external provider is configured (#9995) 2025-09-07 10:17:01 +08:00
Pleasure1234
d09743d254 fix: improve note sorting behavior for drag and drop operations (#9971)
* fix: improve note sorting behavior for drag and drop operations

- Skip automatic sorting when performing same-level drag reordering
- Preserve treePath during same-level moves to maintain manual ordering
- Return special indicator for manual reorder operations to prevent conflicts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: type safety issue

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-06 23:15:51 +08:00
SuYao
2361c1b211 fix: implement Anthropic thinking budget calculation in reasoning logic (#9991) 2025-09-06 22:29:36 +08:00
SuYao
ff74e9035d fix: add message processing status check to Message component (#9982) 2025-09-06 17:50:47 +08:00
SuYao
cb4b26c8a4 fix: add YAML front matter support in markdown processing (#9963)
* feat: add YAML front matter support in markdown processing

- Introduced a new plugin to parse and render YAML front matter in markdown documents.
- Updated the markdown converter to handle YAML front matter, preserving its structure during conversion.
- Added corresponding tests to ensure YAML front matter is retained correctly in markdown to HTML and vice versa.
- Enhanced i18n files with new translations for front matter properties in English, Chinese (Simplified and Traditional).
- Included the 'yaml' package as a dependency in package.json.

* feat(i18n): add new translations for editing properties in Traditional Chinese

* chore: fix
2025-09-06 17:10:16 +08:00
Phantom
1640ba2e9e CI: update translator workflow (#9972)
* Create translator.yml

* update

* try with cc

* ci: 修改文件名translator为claude-translator

* ci(workflow): 更新Claude翻译工作流的配置和提示词

重构工作流配置,简化权限设置并优化翻译逻辑。更新提示词以提供更清晰的翻译指令和格式要求,确保翻译结果的一致性。

* ci(workflows): 更新Claude工作流配置

添加track_progress选项以跟踪代码审查进度
补充环境信息变量和注释说明

* ci(workflow): 更新翻译工作流的触发条件和gh命令参数

修改工作流触发条件,仅当issue或comment不含翻译内容时执行
更新gh命令参数以使用正确的环境变量

* Revert "ci(workflows): 更新Claude工作流配置"

This reverts commit 29f6a1dc8b.

* fix(ci): 修复GitHub Actions工作流中的条件判断和变量引用

修复工作流文件中的条件判断逻辑,添加括号确保正确执行
修复comment处理中的变量引用,使用正确的环境变量
添加注释说明Comment ID可能未定义的情况

* ci(workflow): 为翻译工作流添加并发控制

* ci(workflow): 仅协作者的issue或评论触发翻译工作流

* ci: 更新Claude翻译工具的allowed-tools和输出格式

更新Claude翻译工具的配置,添加mcp__github_comment__update_claude_comment到allowed-tools中
优化翻译输出的格式,使用Markdown的NOTE提示和折叠面板展示原始内容

* ci(workflow): 更新Claude翻译器的触发条件

将检测翻译标记从'**English Translation:**'改为'> [!NOTE]\n> This issue/comment was translated by Claude.'

* fix(workflow): 简化Claude翻译器的触发条件检测

移除翻译标记中的Markdown注释格式,使检测更直接

* ci(workflow): 简化issues触发条件,仅响应opened事件

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
2025-09-06 16:22:46 +08:00
yyhhyyyyyy
ca003943a9 feat: add qwen3-max-preview model support for DashScope provider (#9969) 2025-09-06 14:07:13 +08:00
Pleasure1234
71783aea21 Create translator.yml (#9955)
* Create translator.yml

* update

* try with cc

* ci: 修改文件名translator为claude-translator

* ci(workflow): 更新Claude翻译工作流的配置和提示词

重构工作流配置,简化权限设置并优化翻译逻辑。更新提示词以提供更清晰的翻译指令和格式要求,确保翻译结果的一致性。

* ci(workflows): 更新Claude工作流配置

添加track_progress选项以跟踪代码审查进度
补充环境信息变量和注释说明

* ci(workflow): 更新翻译工作流的触发条件和gh命令参数

修改工作流触发条件,仅当issue或comment不含翻译内容时执行
更新gh命令参数以使用正确的环境变量

* Revert "ci(workflows): 更新Claude工作流配置"

This reverts commit 29f6a1dc8b.

* fix(ci): 修复GitHub Actions工作流中的条件判断和变量引用

修复工作流文件中的条件判断逻辑,添加括号确保正确执行
修复comment处理中的变量引用,使用正确的环境变量
添加注释说明Comment ID可能未定义的情况

* ci(workflow): 为翻译工作流添加并发控制

* ci(workflow): 仅协作者的issue或评论触发翻译工作流

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-06 13:32:50 +08:00
SuYao
b5632b0097 fix: refine Qwen model reasoning checks in isQwenReasoningModel and isSupportedThinkingTokenQwenModel functions (#9966) 2025-09-06 10:59:09 +08:00
yyhhyyyyyy
c506ff6872 ci: add format check to PR workflow and documentation (#9810) 2025-09-06 09:28:50 +08:00
Phantom
fd83834fca Feat: more error blocks (#9960)
* refactor(utils): 提取 API 调用错误序列化逻辑到独立函数

将 serializeError 中的 API 调用错误处理逻辑提取为独立的 serializeAPICallError 函数,提高代码可维护性

* feat(错误处理): 添加下载错误的序列化支持

新增 SerializedAiSdkDownloadError 接口用于表示下载错误
实现 serializeDownloadError 方法处理 DownloadError 的序列化
在 serializeError 中添加对 DownloadError 的判断处理

* feat(错误类型): 添加序列化AI SDK错误类型检查函数

修改 isSerializedAiSdkAPICallError
新增 isSerializedAiSdkDownloadError
新增 SerializedAiSdkInvalidArgumentError

* feat(错误处理): 添加对InvalidArgumentError的序列化支持

添加对InvalidArgumentError类型的序列化处理,包括定义序列化接口和实现序列化函数

* feat(错误处理): 添加对InvalidDataContentError的序列化支持

* feat(错误处理): 添加对InvalidMessageRoleError的序列化支持

新增对InvalidMessageRoleError错误的序列化处理,包括类型定义和序列化函数

* feat(错误处理): 添加对无效提示错误的序列化支持

新增 SerializedAiSdkInvalidPromptError 接口及序列化函数
在 serializeError 中添加对 InvalidPromptError 的处理逻辑

* feat(错误处理): 添加对无效工具输入错误的序列化支持

* feat(错误处理): 添加对JSON解析错误的序列化支持

* feat(错误处理): 添加消息转换错误的序列化支持

添加 SerializedAiSdkMessageConversionError 接口及序列化函数,用于处理消息转换错误的序列化

* feat(types): 添加 SerializedAiSdkNoAudioGeneratedError 类型及校验函数

* feat(错误处理): 添加对NoObjectGeneratedError的序列化支持

新增SerializedAiSdkNoObjectGeneratedError接口及序列化函数,用于处理AI SDK中对象未生成的错误情况

* feat(错误处理): 添加对NoSuchModelError的序列化支持

添加对AI SDK中NoSuchModelError类型的序列化支持,包括类型定义和序列化函数

* feat(错误处理): 添加对NoSuchProviderError的序列化支持

* feat(错误处理): 添加对NoSuchToolError的序列化支持

新增SerializedAiSdkNoSuchToolError接口及序列化函数,用于处理工具不存在错误
简化isSerializedAiSdkNoSuchProviderError的校验逻辑

* feat(错误处理): 添加序列化重试错误类型和函数

添加 SerializedAiSdkRetryError 接口和序列化函数,用于处理重试错误的序列化

* feat(types): 添加SerializedAiSdkTooManyEmbeddingValuesForCallError类型

添加未由aisdk导出的SerializedAiSdkTooManyEmbeddingValuesForCallError类型及其类型守卫,用于处理嵌入值过多错误

* feat(错误处理): 添加工具调用修复错误的序列化支持

* feat(错误处理): 添加类型验证错误的序列化支持

* feat(错误处理): 添加对不支持功能的错误序列化支持

* feat(types): 添加 AiSdkErrorUnion 类型用于聚合所有 AI SDK 错误类型

* refactor(types): 移除未使用的AiSdkErrorUnion类型并清理导入

* refactor(error): 重构错误处理逻辑,统一序列化方法

简化错误序列化逻辑,使用统一的方法处理所有可能的错误类型
移除重复的序列化函数,提高代码可维护性
更新错误类型定义,添加缺失的错误类型

* feat(error): 添加对InvalidToolInputError和NoSuchToolError的序列化支持

新增对AI SDK中InvalidToolInputError和NoSuchToolError错误的序列化处理,完善错误处理逻辑

* feat(错误处理): 完善AI SDK错误类型和错误详情展示

添加SerializedAiSdkNoSpeechGeneratedError类型并重命名相关函数
实现isSerializedAiSdkErrorUnion函数统一检查所有AI SDK错误类型
在ErrorBlock中扩展错误详情展示逻辑,支持所有AI SDK错误类型

* feat(i18n): 添加缺失的翻译字段

* docs(i18n): 添加待翻译的多语言文本字段

* fix(ErrorBlock): 修复重复的错误类型检查条件

* feat(types): 添加AISDKError到AiSdkErrorUnion类型中

* refactor(ErrorBlock): 移除未使用的类型验证错误显示逻辑

* fix(i18n): Auto update translations for PR #9960

* docs(i18n): 更新多语言翻译文件中的缺失翻译

* fix(i18n): 修正多语言文件中的翻译错误

更新法语、葡萄牙语、日语和俄语翻译文件,将中文词汇替换为正确的目标语言词汇

* fix(类型): 修正SerializedError和SerializedAiSdkJSONParseError的类型判断

移除SerializedAiSdkJSONParseError类型判断中冗余的'message'检查,因为父类型SerializedAiSdkError已包含此检查

* fix(错误处理): 修复类型验证错误判断并添加缺失的错误类型

修复 isSerializedAiSdkTypeValidationError 判断逻辑,排除包含 parameter 属性的情况
在 SerializedAiSdkErrorUnion 联合类型中添加缺失的 SerializedAiSdkNoSpeechGeneratedError 类型

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-05 22:24:13 +08:00
RieN 7z
973cab57ab Fix: Traffic light safe area in css (#9925)
* feat: add CSS Environment Variables

* fix: lint

* fix: import
2025-09-05 20:48:30 +08:00
Pleasure1234
d1535e1789 feat: add ByteDance and Ideogram model provider logos (#9950)
Add SVG logos for ByteDance and Ideogram AI model providers to improve visual identification in the model selection UI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-05 20:40:39 +08:00
Murphy
60e1f15e42 feat: add shortcuts to rename topic and edit last user message (#9466)
* feat: add rename topic shortcut (Ctrl/Cmd+T)

* fix: add migration for rename topic shortcut

* feat: add shortcut to edit last user message (Ctrl/Cmd+Shift+E)
- 用于在用户提示词生成的响应不符合预期时,便捷地激活提示词编辑,从而配合编辑器编辑支持的Enter键提交绑定来生成新的模型响应
- messages:绑定 useShortcut('edit_last_user_message'),定位最后一条非 clear 的用户消息并发出 EDIT_MESSAGE
- message:监听 EDIT_MESSAGE,调用 startEditing(message.id) 激活内联编辑(沿用现有自动滚动)

* fix: lint errors and sync i18n

* fix(i18n): complete missing translations in ES, PT, EL and FR locales

* disable new shortcuts by default

* show topic tab on rename shortcut

* refactor: use findLast to simplify find last user message

* add esc key to cancel message editing (discard changes)

* fix missing comma

* remove extra linebreak

* fix: version

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-09-05 19:59:59 +08:00
王叔叔
6531d40386 docs: Update LICENSE (#9948) 2025-09-05 19:00:47 +08:00
Pleasure1234
c940e0bc92 feat: add Anthropic OAuth settings UI and logic (#8905)
* feat: add Anthropic OAuth settings UI and logic

Introduces AnthropicSettings component for managing Anthropic OAuth authentication in provider settings. Adds Anthropic OAuth logic in a new anthropicOAuth.ts file, including PKCE flow, token exchange, and credential management stubs. Integrates AnthropicSettings into ProviderSetting to enable UI for login, logout, and code entry.

* feat: add Anthropic OAuth authentication support

Introduces OAuth authentication for Anthropic provider, including UI changes for selecting authentication method and handling authorization code input. Updates i18n files with new Anthropic OAuth-related strings in multiple languages and adds the 'authType' property to the Provider type.

* fix: oauth

* refactor: Anthropic OAuth to main process service

Moved Anthropic OAuth logic from renderer to main process as a singleton service. Updated IPC channels and preload API to support Anthropic OAuth actions. Refactored AnthropicSettings component to use new IPC-based API for authentication flow.

* fix: add 'authenticating' translation and update AnthropicSettings

Added the 'authenticating' key to Anthropic provider translations across multiple languages. Updated AnthropicSettings.tsx to remove the unused 'authenticating_detail' description and set the modal to be centered.

* fix: add reference

* Update AnthropicAPIClient.ts

* fix: update credentials path and improve OAuth handling in AnthropicAPIClient

* feat: add support for Anthropic OAuth provider handling in ProviderSetting

* feat: enhance OAuth authentication messages in multiple languages

* feat: add support for Anthropic provider with OAuth authentication and system message handling for new aisdk provider

* fix: update credential path and use net.fetch for OAuth token requests

* fix: setting page ui

---------

Co-authored-by: Vaayne <liu.vaayne@gmail.com>
2025-09-05 17:43:20 +08:00
SuYao
86e3776fff fix(mcp): enhance progress event structure to include callId for specific tool tracking (#9946)
* fix(mcp): enhance progress event structure to include callId for specific tool tracking

* refactor(mcp): add MCP progress event with callId and progress percentage
2025-09-05 17:26:20 +08:00
kangfenmao
469b29c941 chore: remove chinese issue templates for bug reports, feature requests, questions, and other inquiries 2025-09-05 16:59:45 +08:00
kangfenmao
1b57b272f9 chore: bump version to v1.6.0-beta.7 2025-09-05 16:48:20 +08:00
Phantom
c906307a33 fix(inputbar): fix infinite state update loop (#9933)
fix(模型检测): 修复状态更新无限循环的问题

修复isGenerateImageModel函数中provider类型检查错误,同时优化Inputbar中图像生成功能的自动启用逻辑
2025-09-05 15:40:46 +08:00
beyondkmp
b914613e80 fix: standardize mouse enter delay for window control tooltips (#9936) 2025-09-05 15:08:35 +08:00
Phantom
ba99d83d17 ci(workflows): fix claude code review ci to be triggered by pull_request (#9934)
* ci(workflows): 更新claude代码审查工作流以支持PR分支

添加ref参数以确保检出正确的PR分支

* ci: 移除claude代码审查工作流中的ref参数

* ci: 将工作流触发器从pull_request_target改为pull_request

* ci: 添加Claude代码审查测试工作流

* ci: 删除claude-review-test.yml工作流文件
2025-09-05 14:11:50 +08:00
beyondkmp
a58b58cd95 fix: update User-Agent handling in WebviewService to conditionally set based on URL (#9931) 2025-09-05 13:49:55 +08:00
LiuVaayne
96d41ae8f6 workflows: restrict Claude triggers to collaborators/members/owners and fix fork PR reviews (#9924)
* workflows: restrict Claude triggers to collaborators/members/owners and fix fork PR reviews

- claude.yml: gate by author_association in [COLLABORATOR, MEMBER, OWNER]
- claude-code-review.yml: use pull_request_target, add pull-requests: write and id-token: write to enable OIDC + commenting on forks

* fix(workflows): remove 'reopened' and 'assigned' types from triggers
2025-09-05 13:12:15 +08:00
Konv Suu
e3afab765d fix(miniapp): title container background style align with sidebar (#9915) 2025-09-05 12:49:02 +08:00
Konv Suu
507a653d81 fix: 对齐模型设置中 avatar 的样式 (#9829)
* fix: 对齐模型设置中 avatar 的样式

* update

* update

* fix: 修复上传弹出两次文件夹的问题

* update
2025-09-05 12:33:43 +08:00
beyondkmp
b1a39e9b38 feat: support custom minimize, maximize and close (#9847)
* feat: add window control functionality for Windows and Linux

- Introduced new IPC channels for window management: minimize, maximize, unmaximize, close, and check maximized state.
- Implemented window control buttons in the UI, allowing users to minimize, maximize, and close the application.
- Enhanced Navbar and TabContainer components to include window controls, improving user experience on non-Mac platforms.
- Styled window control buttons for better visual integration.

This update enhances the application's usability by providing essential window management features.

* add tooltip

* fix macos

* lint error

* update i18n

* lint

* fix: add WindowControls to MinApp popup and improve hover styles

- Add WindowControls component to MinappPopupContainer title bar for Windows/Linux
- Fix ButtonsGroup overlap with WindowControls by adding proper margin
- Improve WindowControls hover background visibility by using rgba(128,128,128,0.3)
- Ensure WindowControls is positioned at the right edge of title bar

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* lint

* add types

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-05 12:30:01 +08:00
Phantom
19846c7e01 ci(github): fix auto-i18n condition (#9921)
* ci(github): 修复自动i18n工作流的仓库名称条件

修复条件判断中的仓库名称变量,确保工作流在正确的仓库中运行

* ci(workflow): 简化自动i18n工作流的条件表达式

* fix(translate): fix warn
2025-09-05 12:07:14 +08:00
Chen Tao
e3e8783eb8 fix: remove youtube render (#9920) 2025-09-05 11:15:26 +08:00
Phantom
e9db53c973 fix: handle multiple content source when pasting to translate input (#9919)
* fix(translate): 处理粘贴事件时增加处理中状态检查

* fix(translate): 修复粘贴文本时未阻止默认行为的问题

添加event.preventDefault()以防止粘贴文本时触发默认行为
同时优化粘贴逻辑,优先处理文本内容
2025-09-05 10:35:58 +08:00
beyondkmp
fd0645fbb4 chore(package): update dependencies and reorganize package.json (#9914) 2025-09-05 09:57:51 +08:00
Phantom
2849cbf257 fix: wrong ci repo check (#9916)
fix: wrong repo check
2025-09-05 09:53:46 +08:00
Phantom
b2459d8f48 fix: only allow auto-i18n running on branches in main repo (#9905)
* ci(workflow): 移除不必要的checkout参数

* ci(workflow): 更新自动i18n工作流的checkout步骤

添加pull request相关参数以确保正确检出代码

* ci(workflow): 在auto-i18n工作流中添加repository参数

* ci(workflow): 添加仓库条件限制以仅对cherry-studio运行
2025-09-05 09:19:34 +08:00
George·Dong
935189f3f7 refactor(miniapp): 适配顶部状态栏 (#9695)
* feat(minapp): add top-navbar fixed toolbar and layout adjustments

* refactor(minapps): optimize toolbar

* fix(minapps): hide redundant components

* feat(minapp): improve webview load handling and popup visibility

* feat(minapps): improve WebView load handling and clean up launchpad

* feat(minapp): 实现活跃小程序数量限制与关闭缓存清理

* fix(minapp): 修复WebView高度不正确的问题

* fix(minapp): show popup only for left navbar mode

* feat(minapps): add full-screen loading mask for webview

* fix: lint error

* feat(minapp): fix drawer sizing and layout when side navbar present

* refactor(minapp): 移除固定工具栏组件,优化弹窗容器布局

* feat(minapps): memoize app lookup to avoid unnecessary recompute

* chore(minapps): optimize comments

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(renderer): remove stray blank line in MinAppFullPageView

* refactor(minapps): remove top navbar opened minapps component

* refactor(tab): remove unused TopNavbarOpenedMinappTabs import

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-04 23:59:54 +08:00
Phantom
b647017c43 ci: auto i18n (#9889)
* refactor(i18n): 将日语和俄语迁移到translate目录

* refactor(i18n): 将日语和俄语翻译文件移动到机器翻译目录

* docs(i18n): 更新翻译目录的README说明

* ci: 添加自动国际化翻译的 GitHub Actions 工作流

* Potential fix for code scanning alert no. 48: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix(i18n): 修复国际化文件末尾缺少换行符的问题

* ci(workflow): 修改自动i18n工作流触发条件为pull_request

将触发条件从push到main分支改为在pull_request的opened、synchronize和reopened事件时触发

* ci(workflow): 更新自动i18n工作流的权限和推送配置

添加pull-requests写入权限以支持PR操作
将推送动作更新为github-push-action并改为推送至PR分支

* ci(github): 更新自动i18n工作流以支持PR分支

修改自动i18n工作流,在检出步骤中添加ref参数以支持PR分支
更新提交消息格式以包含PR编号

* ci(workflows): 更新自动i18n工作流以使用yarn

将依赖管理从npm切换到yarn,并添加corepack启用步骤

* ci(workflow): 优化自动翻译工作流的依赖安装步骤

修改依赖安装命令,使用更精确的参数避免不必要的锁定文件生成,并确保安装开发依赖

* ci(workflow): 更新i18n翻译脚本的执行方式

* ci(workflow): 移除不必要的生产环境标志以简化依赖安装

移除 yarn add 命令中的 --production=false 标志,因为在此上下文中不需要区分生产环境依赖

* ci(workflow): 更新依赖安装命令以跳过构建步骤

* fix(i18n): Auto update translations for PR #9889

* ci(workflow): 优化依赖安装步骤以减少构建时间

将直接使用yarn安装依赖改为在临时目录中通过npm安装,避免全局安装影响

* ci(i18n): 在自动翻译工作流中忽略 package.json 和 yarn.lock 的更改

重置 package.json 和 yarn.lock 的更改,避免在自动更新翻译时提交这些文件

* ci(workflow): 简化自动i18n工作流的依赖安装步骤

移除不必要的corepack启用步骤,直接使用npm安装所需依赖

* Revert "fix(i18n): Auto update translations for PR #9889"

This reverts commit 23a4a366e1.

* ci(workflow): 将 npm 安装改为 yarn 全局安装依赖以简化命令

* ci(workflow): 将依赖安装从yarn改为npm

* ci(workflow): 修改i18n工作流以隔离依赖安装

将全局依赖安装改为在临时目录中安装,避免污染全局环境
使用npx运行tsx以确保使用正确版本的依赖

* fix(i18n): Auto update translations for PR #9889

* ci(workflow): 更新自动翻译工作流配置

移除 yarn 相关配置并更新翻译脚本文件名

* fix(i18n): Auto update translations for PR #9889

* ci(workflow): 在自动翻译工作流中添加prettier格式化步骤

添加prettier及其json排序插件用于自动格式化翻译后的i18n文件,确保代码风格一致

* Revert "fix(i18n): Auto update translations for PR #9889"

This reverts commit 423ca8fb03.

* ci(workflow): 修复自动翻译工作流中prettier路径问题

* fix(i18n): Auto update translations for PR #9889

* fix(i18n): 修正西班牙语翻译中的字幕文件字段

* ci(i18n): 添加无变更时的检查逻辑

避免在没有翻译变更时生成空提交

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-09-04 19:51:15 +08:00
LiuVaayne
3a8a603527 ci: Add Claude Code GitHub Workflow (#9893)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"

* fix: update OAuth token variable in Claude workflows
2025-09-04 19:43:04 +08:00
Chen Tao
b6d10656f9 feat: refactor Knowledge Base (#8384)
Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: eeee0717 <chentao020717@outlook.com>
2025-09-04 17:23:31 +08:00
Lin Manhui
7de31d8cb6 feat: Add PaddleOCR as a new OCR provider (#9876)
* feat: support PaddleOCR as an OCR provider

* style: fix format

* fix: update persistReducer version

* update wrt comments

* fix(ocr): 修复迁移147中OCR提供商的设置错误

将直接赋值改为使用addOcrProvider方法添加内置PaddleOCR提供商,确保正确初始化OCR服务

* Replace bare fetch with net.fetch

* Use '\n' as delimiter

* Optimize code wrt comments

* Add tip

---------

Co-authored-by: icarus <eurfelux@gmail.com>
2025-09-04 17:13:58 +08:00
LiuVaayne
cac84a8795 refactor(mcp): enhance MCPService logging and error handling (#9878) 2025-09-04 14:52:47 +08:00
620 changed files with 42119 additions and 18051 deletions

4
.github/CODEOWNERS vendored Normal file
View File

@@ -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

View File

@@ -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: 任何能让我们对你所遇到的问题有更多了解的东西

View File

@@ -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: 在此添加任何其他与功能建议相关的上下文或截图

View File

@@ -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

View File

@@ -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: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接

66
.github/workflows/auto-i18n.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -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:*)"'

107
.github/workflows/claude-translator.yml vendored Normal file
View File

@@ -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

60
.github/workflows/claude.yml vendored Normal file
View File

@@ -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:*)'

View File

@@ -9,6 +9,7 @@ on:
branches:
- main
- develop
- v2
jobs:
build:
@@ -48,6 +49,9 @@ jobs:
- name: Lint Check
run: yarn test:lint
- name: Format Check
run: yarn format:check
- name: Type Check
run: yarn typecheck

3
.gitignore vendored
View File

@@ -37,6 +37,7 @@ dist
out
mcp_server
stats.html
.eslintcache
# ENV
.env
@@ -68,3 +69,5 @@ playwright-report
test-results
YOUR_MEMORY_FILE_PATH
.sessions/

215
.oxlintrc.json Normal file
View File

@@ -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": []
}
}
}

View File

@@ -1,11 +0,0 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
CHANGELOG*.md
agents.json
src/renderer/src/integration/nutstore/sso/lib
AGENT.md
src/main/integration/cherryin/index.js

View File

@@ -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"
}

View File

@@ -1,8 +1,11 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"editorconfig.editorconfig",
"lokalise.i18n-ally"
"lokalise.i18n-ally",
"bradlc.vscode-tailwindcss",
"vitest.explorer",
"oxc.oxc-vscode",
"biomejs.biome"
]
}

23
.vscode/settings.json vendored
View File

@@ -1,38 +1,43 @@
{
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.organizeImports": "never"
},
"editor.formatOnSave": true,
"files.associations": {
"*.css": "tailwindcss"
},
"files.eol": "\n",
"i18n-ally.displayLanguage": "zh-cn",
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
"i18n-ally.fullReloadOnChanged": true, // 界面显示语言
"i18n-ally.fullReloadOnChanged": true,
"i18n-ally.keystyle": "nested", // 翻译路径格式
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
// "i18n-ally.namespace": true, // 开启命名空间

View File

@@ -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,

View File

@@ -5,3 +5,5 @@ httpTimeout: 300000
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.1.cjs
npmRegistryServer: https://registry.npmjs.org
npmPublishRegistry: https://registry.npmjs.org

1
AGENTS.md Symbolic link
View File

@@ -0,0 +1 @@
CLAUDE.md

146
CLAUDE.md
View File

@@ -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
### 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
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.9.1 --activate`
- **Install Dependencies**: `yarn install`
## Project Architecture
### 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
- **Debug Mode**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
### Testing & Quality
- **Run Tests**: `yarn test` - Runs all tests (Vitest)
- **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
### Key Components
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
### Logging
```typescript
// Main process
import { loggerService } from '@logger'
const logger = loggerService.withContext('moduleName')
// Renderer process (set window source first)
loggerService.initWindowSource('windowName')
const logger = loggerService.withContext('moduleName')
// Logging
// Renderer: loggerService.initWindowSource('windowName') first
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

45
LICENSE
View File

@@ -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**
This project employs a **User-Segmented Dual Licensing** model.

View File

@@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl
1. **Diverse LLM Provider Support**:
- ☁️ 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
2. **AI Assistants & Conversations**:

97
biome.jsonc Normal file
View File

@@ -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 }
}

21
components.json Normal file
View File

@@ -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
}

View File

@@ -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=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=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=pt">Português</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 服务支持**
- ☁️ 支持主流 LLM 云服务OpenAI、Gemini、Anthropic、硅基流动等
- 🔗 集成流行 AI Web 服务Claude、Peplexity、Poe、腾讯元宝、知乎直答等
- 🔗 集成流行 AI Web 服务Claude、Perplexity、Poe、腾讯元宝、知乎直答等
- 💻 支持 Ollama、LM Studio 本地模型部署
2. **智能助手与对话**

View File

@@ -2,7 +2,9 @@
## 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

View File

@@ -8,9 +8,9 @@
| 字段名 | 类型 | 是否主键 | 索引 | 说明 |
| ---------- | ------ | -------- | ---- | ------------------------------------------------------------------------ |
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji用户输入 |
| `id` | string | ✅ 是 | ✅ | 唯一标识符,主键 |
| `langCode` | string | ❌ 否 | ✅ | 语言代码(如:`zh-cn`, `en-us`, `ja-jp` 等,均为小写),支持普通索引查询 |
| `value` | string | ❌ 否 | ❌ | 语言的名称,用户输入 |
| `emoji` | string | ❌ 否 | ❌ | 语言的emoji用户输入 |
> `langCode` 虽非主键,但在业务层应当避免重复插入相同语言代码。

View File

@@ -17,52 +17,52 @@ protocols:
schemes:
- cherrystudio
files:
- '**/*'
- '!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}'
- '!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}'
- '!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}'
- '!**/{.editorconfig,.jekyll-metadata}'
- '!src'
- '!scripts'
- '!local'
- '!docs'
- '!packages'
- '!.swc'
- '!.bin'
- '!._*'
- '!*.log'
- '!stats.html'
- '!*.md'
- '!**/*.{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}'
- '!**/{test,tests,__tests__,powered-test,coverage}/**'
- '!**/{example,examples}/**'
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
- '!**/*.min.*.map'
- '!**/*.d.ts'
- '!**/dist/es6/**'
- '!**/dist/demo/**'
- '!**/amd/**'
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
- '!node_modules/rollup-plugin-visualizer'
- '!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/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
- '!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/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-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
- '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files
- "**/*"
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
- "!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,biome.jsonc}"
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
- "!**/{.editorconfig,.jekyll-metadata}"
- "!src"
- "!scripts"
- "!local"
- "!docs"
- "!packages"
- "!.swc"
- "!.bin"
- "!._*"
- "!*.log"
- "!stats.html"
- "!*.md"
- "!**/*.{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}"
- "!**/{test,tests,__tests__,powered-test,coverage}/**"
- "!**/{example,examples}/**"
- "!**/*.{spec,test}.{js,jsx,ts,tsx}"
- "!**/*.min.*.map"
- "!**/*.d.ts"
- "!**/dist/es6/**"
- "!**/dist/demo/**"
- "!**/amd/**"
- "!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}"
- "!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}"
- "!node_modules/rollup-plugin-visualizer"
- "!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/mammoth/{mammoth.browser.js,mammoth.browser.min.js}"
- "!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/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-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
- "!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}" # filter .node build files
asarUnpack:
- resources/**
- '**/*.{metal,exp,lib}'
- 'node_modules/@img/sharp-libvips-*/**'
- "**/*.{metal,exp,lib}"
- "node_modules/@img/sharp-libvips-*/**"
win:
executableName: Cherry Studio
artifactName: ${productName}-${version}-${arch}-setup.${ext}
@@ -88,7 +88,7 @@ mac:
entitlementsInherit: build/entitlements.mac.plist
notarize: false
artifactName: ${productName}-${version}-${arch}.${ext}
minimumSystemVersion: '20.1.0' # 最低支持 macOS 11.0
minimumSystemVersion: "20.1.0" # 最低支持 macOS 11.0
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -110,6 +110,10 @@ linux:
StartupWMClass: CherryStudio
mimeTypes:
- 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:
provider: generic
url: https://releases.cherry-ai.com
@@ -121,12 +125,16 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
🔧 性能优化:
- 优化AI服务连接方式提升响应速度和稳定性
- 改进模型列表获取功能,减少不必要的网络请求
- 增强各AI服务商的兼容性和连接可靠性
🐛 问题修复:
- 修复部分AI服务商连接失败的问题
- 修复模型配置加载时的潜在错误
- 提升应用整体稳定性和容错能力
- 修复 Anthropic API URL 处理,移除尾部斜杠并添加端点路径处理
- 修复 MessageEditor 缺少 QuickPanelProvider 包装的问题
- 修复 MiniWindow 高度问题
🚀 性能优化:
- 优化输入栏提及模型状态缓存,在渲染间保持状态
- 重构网络搜索参数支持模型内置搜索,新增 OpenAI Chat 和 OpenRouter 支持
🔧 重构改进:
- 更新 HeroUIProvider 导入路径,改善上下文管理
- 更新依赖项和 VSCode 开发环境配置
- 升级 @cherrystudio/ai-core 到 v1.0.0-alpha.17

View File

@@ -4,7 +4,9 @@ import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import pkg from './package.json' assert { type: 'json' }
// assert not supported by biome
// import pkg from './package.json' assert { type: 'json' }
import pkg from './package.json'
const visualizerPlugin = (type: 'renderer' | 'main') => {
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
@@ -23,10 +25,7 @@ export default defineConfig({
'@shared': resolve('packages/shared'),
'@logger': resolve('src/main/services/LoggerService'),
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node'),
'@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')
'@mcp-trace/trace-node': resolve('packages/mcp-trace/trace-node')
}
},
build: {
@@ -63,6 +62,7 @@ export default defineConfig({
},
renderer: {
plugins: [
(async () => (await import('@tailwindcss/vite')).default())(),
react({
tsDecorators: true,
plugins: [
@@ -84,6 +84,7 @@ export default defineConfig({
alias: {
'@renderer': resolve('src/renderer/src'),
'@shared': resolve('packages/shared'),
'@types': resolve('src/renderer/src/types'),
'@logger': resolve('src/renderer/src/services/LoggerService'),
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),

View File

@@ -1,8 +1,8 @@
import electronConfigPrettier from '@electron-toolkit/eslint-config-prettier'
import tseslint from '@electron-toolkit/eslint-config-ts'
import eslint from '@eslint/js'
import eslintReact from '@eslint-react/eslint-plugin'
import { defineConfig } from 'eslint/config'
import oxlint from 'eslint-plugin-oxlint'
import reactHooks from 'eslint-plugin-react-hooks'
import simpleImportSort from 'eslint-plugin-simple-import-sort'
import unusedImports from 'eslint-plugin-unused-imports'
@@ -10,7 +10,6 @@ import unusedImports from 'eslint-plugin-unused-imports'
export default defineConfig([
eslint.configs.recommended,
tseslint.configs.recommended,
electronConfigPrettier,
eslintReact.configs['recommended-typescript'],
reactHooks.configs['recommended-latest'],
{
@@ -26,7 +25,6 @@ export default defineConfig([
'simple-import-sort/exports': 'error',
'unused-imports/no-unused-imports': 'error',
'@eslint-react/no-prop-types': 'error',
'prettier/prettier': ['error']
}
},
// 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
files: ['src/**/*.{ts,tsx,js,jsx}'],
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*'],
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
rules: {
'no-restricted-syntax': [
process.env.PRCI ? 'error' : 'warn',
@@ -123,7 +121,14 @@ export default defineConfig([
'.gitignore',
'scripts/cloudflare-worker.js',
'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']
])

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.6.0-beta.6",
"version": "1.6.0-rc.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -27,6 +27,7 @@
"scripts": {
"start": "electron-vite preview",
"dev": "dotenv electron-vite dev",
"dev:main": "dotenv electron-vite dev --watch",
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
"build": "npm run typecheck && electron-vite build",
"build:check": "yarn lint && yarn test",
@@ -43,15 +44,18 @@
"release": "node scripts/version.js",
"publish": "yarn build:check && yarn release patch push",
"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",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"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:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "tsx scripts/check-i18n.ts",
"sync:i18n": "tsx scripts/sync-i18n.ts",
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
"update:languages": "tsx scripts/update-languages.ts",
@@ -63,26 +67,37 @@
"test:ui": "vitest --ui",
"test:watch": "vitest",
"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",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix && yarn typecheck && yarn check:i18n",
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky"
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n",
"format": "biome format --write && biome lint --write",
"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": {
"@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/win32-x64-msvc": "^0.4.7",
"@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",
"ai-sdk-provider-claude-code": "^1.1.3",
"@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",
"jsdom": "26.1.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.2.0",
"os-proxy-config": "^1.1.2",
"selection-hook": "^1.0.11",
"selection-hook": "^1.0.12",
"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",
"turndown": "7.2.0"
},
@@ -90,16 +105,18 @@
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.0",
"@ai-sdk/google-vertex": "^3.0.0",
"@ai-sdk/mistral": "^2.0.0",
"@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",
"@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",
"@aws-sdk/client-bedrock": "^3.840.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/client-s3": "^3.840.0",
"@cherrystudio/ai-core": "workspace:*",
"@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
"@cherrystudio/embedjs": "^0.1.31",
"@cherrystudio/embedjs-libsql": "^0.1.31",
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
@@ -117,7 +134,6 @@
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
@@ -128,11 +144,11 @@
"@eslint/js": "^9.22.0",
"@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",
"@heroui/react": "^2.8.3",
"@kangfenmao/keyv-storage": "^0.1.0",
"@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@langchain/community": "^0.3.50",
"@mistralai/mistralai": "^1.7.5",
"@modelcontextprotocol/sdk": "^1.17.0",
"@modelcontextprotocol/sdk": "^1.17.5",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@openrouter/ai-sdk-provider": "^1.1.2",
@@ -143,9 +159,11 @@
"@opentelemetry/sdk-trace-node": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@playwright/test": "^1.52.0",
"@radix-ui/react-context-menu": "^2.2.16",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.12.0",
"@swc/plugin-styled-components": "^8.0.4",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.85.5",
"@tanstack/react-virtual": "^3.13.12",
"@testing-library/dom": "^10.4.0",
@@ -171,21 +189,30 @@
"@truto/turndown-plugin-gfm": "^1.0.2",
"@tryfabric/martian": "^1.2.4",
"@types/cli-progress": "^3",
"@types/express": "^5.0.3",
"@types/content-type": "^1.1.9",
"@types/cors": "^2.8.19",
"@types/diff": "^7",
"@types/express": "^5",
"@types/fs-extra": "^11",
"@types/he": "^1",
"@types/html-to-text": "^9",
"@types/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/md5": "^2.3.5",
"@types/mime-types": "^3",
"@types/node": "^22.17.1",
"@types/pako": "^1.0.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@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/turndown": "^5.0.5",
"@types/word-extractor": "^1",
"@typescript/native-preview": "latest",
"@uiw/codemirror-extensions-langs": "^4.25.1",
"@uiw/codemirror-themes-all": "^4.25.1",
"@uiw/react-codemirror": "^4.25.1",
@@ -197,15 +224,18 @@
"@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4",
"ai": "^5.0.29",
"ai": "^5.0.44",
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"chardet": "^2.1.0",
"check-disk-space": "3.4.0",
"cheerio": "^1.1.2",
"chokidar": "^4.0.3",
"cli-progress": "^3.12.0",
"clsx": "^2.1.1",
"code-inspector-plugin": "^0.20.14",
"color": "^5.0.0",
"concurrently": "^9.2.1",
@@ -217,9 +247,11 @@
"docx": "^9.0.2",
"dompurify": "^3.2.6",
"dotenv-cli": "^7.4.2",
"drizzle-kit": "^0.31.4",
"electron": "37.4.0",
"electron-builder": "26.0.15",
"electron-devtools-installer": "^3.2.0",
"electron-reload": "^2.0.0-alpha.1",
"electron-store": "^8.2.0",
"electron-updater": "6.6.4",
"electron-vite": "4.0.0",
@@ -228,18 +260,21 @@
"emoji-picker-element": "^1.22.1",
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
"eslint": "^9.22.0",
"eslint-plugin-oxlint": "^1.15.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"fast-diff": "^1.3.0",
"fast-xml-parser": "^5.2.0",
"fetch-socks": "1.3.2",
"framer-motion": "^12.23.12",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1",
"he": "^1.2.0",
"html-tags": "^5.1.0",
"html-to-image": "^1.11.13",
"html-to-text": "^9.0.5",
"htmlparser2": "^10.0.0",
"husky": "^9.1.7",
"i18next": "^23.11.5",
@@ -256,15 +291,17 @@
"markdown-it": "^14.1.0",
"mermaid": "^11.10.1",
"mime": "^4.0.4",
"mime-types": "^3.0.1",
"motion": "^12.10.5",
"notion-helper": "^1.3.22",
"npx-scope-finder": "^1.2.0",
"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",
"pdf-lib": "^1.17.1",
"pdf-parse": "^1.1.1",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"prettier-plugin-sort-json": "^4.1.1",
"proxy-agent": "^6.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -274,6 +311,7 @@
"react-infinite-scroll-component": "^6.1.0",
"react-json-view": "^1.21.3",
"react-markdown": "^10.1.0",
"react-player": "^3.3.1",
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
@@ -293,21 +331,23 @@
"remark-math": "^6.0.0",
"remove-markdown": "^0.6.2",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.88.0",
"shiki": "^3.12.0",
"strict-url-sanitise": "^0.0.1",
"string-width": "^7.2.0",
"striptags": "^3.2.0",
"styled-components": "^6.1.11",
"swr": "^2.3.6",
"tailwindcss": "^4.1.13",
"tar": "^7.4.3",
"tiny-pinyin": "^1.3.2",
"tokenx": "^1.1.0",
"tsx": "^4.20.3",
"turndown-plugin-gfm": "^1.0.2",
"typescript": "^5.6.2",
"tw-animate-css": "^1.3.8",
"typescript": "~5.8.2",
"undici": "6.21.2",
"unified": "^11.0.5",
"uuid": "^10.0.0",
"uuid": "^13.0.0",
"vite": "npm:rolldown-vite@latest",
"vitest": "^3.2.4",
"webdav": "^5.8.0",
@@ -315,9 +355,11 @@
"winston-daily-rotate-file": "^5.0.0",
"word-extractor": "^1.0.4",
"y-protocols": "^1.0.6",
"yaml": "^2.8.1",
"yjs": "^13.6.27",
"youtubei.js": "^15.0.1",
"zipread": "^1.3.3",
"zod": "^3.25.74"
"zod": "^4.1.5"
},
"resolutions": {
"@codemirror/language": "6.11.3",
@@ -338,22 +380,16 @@
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
"undici": "6.21.2",
"vite": "npm:rolldown-vite@latest",
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {
"*.{js,jsx,ts,tsx,cjs,mjs,cts,mts}": [
"prettier --write",
"biome format --write --no-errors-on-unmatched",
"eslint --fix"
],
"*.{json,yml,yaml,css,scss,html}": [
"prettier --write"
"*.{json,yml,yaml,css,html}": [
"biome format --write --no-errors-on-unmatched"
]
}
}

View File

@@ -1,103 +0,0 @@
/**
* Hub Provider 使用示例
*
* 演示如何使用简化后的Hub Provider功能来路由到多个底层provider
*/
import { createHubProvider, initializeProvider, providerRegistry } from '../src/index'
async function demonstrateHubProvider() {
try {
// 1. 初始化底层providers
console.log('📦 初始化底层providers...')
initializeProvider('openai', {
apiKey: process.env.OPENAI_API_KEY || 'sk-test-key'
})
initializeProvider('anthropic', {
apiKey: process.env.ANTHROPIC_API_KEY || 'sk-ant-test-key'
})
// 2. 创建Hub Provider自动包含所有已初始化的providers
console.log('🌐 创建Hub Provider...')
const aihubmixProvider = createHubProvider({
hubId: 'aihubmix',
debug: true
})
// 3. 注册Hub Provider
providerRegistry.registerProvider('aihubmix', aihubmixProvider)
console.log('✅ Hub Provider "aihubmix" 注册成功')
// 4. 使用Hub Provider访问不同的模型
console.log('\n🚀 使用Hub模型...')
// 通过Hub路由到OpenAI
const openaiModel = providerRegistry.languageModel('aihubmix:openai:gpt-4')
console.log('✓ OpenAI模型已获取:', openaiModel.modelId)
// 通过Hub路由到Anthropic
const anthropicModel = providerRegistry.languageModel('aihubmix:anthropic:claude-3.5-sonnet')
console.log('✓ Anthropic模型已获取:', anthropicModel.modelId)
// 5. 演示错误处理
console.log('\n❌ 演示错误处理...')
try {
// 尝试访问未初始化的provider
providerRegistry.languageModel('aihubmix:google:gemini-pro')
} catch (error) {
console.log('预期错误:', error.message)
}
try {
// 尝试使用错误的模型ID格式
providerRegistry.languageModel('aihubmix:invalid-format')
} catch (error) {
console.log('预期错误:', error.message)
}
// 6. 多个Hub Provider示例
console.log('\n🔄 创建多个Hub Provider...')
const localHubProvider = createHubProvider({
hubId: 'local-ai'
})
providerRegistry.registerProvider('local-ai', localHubProvider)
console.log('✅ Hub Provider "local-ai" 注册成功')
console.log('\n🎉 Hub Provider演示完成')
} catch (error) {
console.error('💥 演示过程中发生错误:', error)
}
}
// 演示简化的使用方式
function simplifiedUsageExample() {
console.log('\n📝 简化使用示例:')
console.log(`
// 1. 初始化providers
initializeProvider('openai', { apiKey: 'sk-xxx' })
initializeProvider('anthropic', { apiKey: 'sk-ant-xxx' })
// 2. 创建并注册Hub Provider
const hubProvider = createHubProvider({ hubId: 'aihubmix' })
providerRegistry.registerProvider('aihubmix', hubProvider)
// 3. 直接使用
const model1 = providerRegistry.languageModel('aihubmix:openai:gpt-4')
const model2 = providerRegistry.languageModel('aihubmix:anthropic:claude-3.5-sonnet')
`)
}
// 运行演示
if (require.main === module) {
demonstrateHubProvider()
simplifiedUsageExample()
}
export { demonstrateHubProvider, simplifiedUsageExample }

View File

@@ -1,167 +0,0 @@
/**
* Image Generation Example
* 演示如何使用 aiCore 的文生图功能
*/
import { createExecutor, generateImage } from '../src/index'
async function main() {
// 方式1: 使用执行器实例
console.log('📸 创建 OpenAI 图像生成执行器...')
const executor = createExecutor('openai', {
apiKey: process.env.OPENAI_API_KEY!
})
try {
console.log('🎨 使用执行器生成图像...')
const result1 = await executor.generateImage('dall-e-3', {
prompt: 'A futuristic cityscape at sunset with flying cars',
size: '1024x1024',
n: 1
})
console.log('✅ 图像生成成功!')
console.log('📊 结果:', {
imagesCount: result1.images.length,
mediaType: result1.image.mediaType,
hasBase64: !!result1.image.base64,
providerMetadata: result1.providerMetadata
})
} catch (error) {
console.error('❌ 执行器生成失败:', error)
}
// 方式2: 使用直接调用 API
try {
console.log('🎨 使用直接 API 生成图像...')
const result2 = await generateImage('openai', { apiKey: process.env.OPENAI_API_KEY! }, 'dall-e-3', {
prompt: 'A magical forest with glowing mushrooms and fairy lights',
aspectRatio: '16:9',
providerOptions: {
openai: {
quality: 'hd',
style: 'vivid'
}
}
})
console.log('✅ 直接 API 生成成功!')
console.log('📊 结果:', {
imagesCount: result2.images.length,
mediaType: result2.image.mediaType,
hasBase64: !!result2.image.base64
})
} catch (error) {
console.error('❌ 直接 API 生成失败:', error)
}
// 方式3: 支持其他提供商 (Google Imagen)
if (process.env.GOOGLE_API_KEY) {
try {
console.log('🎨 使用 Google Imagen 生成图像...')
const googleExecutor = createExecutor('google', {
apiKey: process.env.GOOGLE_API_KEY!
})
const result3 = await googleExecutor.generateImage('imagen-3.0-generate-002', {
prompt: 'A serene mountain lake at dawn with mist rising from the water',
aspectRatio: '1:1'
})
console.log('✅ Google Imagen 生成成功!')
console.log('📊 结果:', {
imagesCount: result3.images.length,
mediaType: result3.image.mediaType,
hasBase64: !!result3.image.base64
})
} catch (error) {
console.error('❌ Google Imagen 生成失败:', error)
}
}
// 方式4: 支持插件系统
const pluginExample = async () => {
console.log('🔌 演示插件系统...')
// 创建一个示例插件,用于修改提示词
const promptEnhancerPlugin = {
name: 'prompt-enhancer',
transformParams: async (params: any) => {
console.log('🔧 插件: 增强提示词...')
return {
...params,
prompt: `${params.prompt}, highly detailed, cinematic lighting, 4K resolution`
}
},
transformResult: async (result: any) => {
console.log('🔧 插件: 处理结果...')
return {
...result,
enhanced: true
}
}
}
const executorWithPlugin = createExecutor(
'openai',
{
apiKey: process.env.OPENAI_API_KEY!
},
[promptEnhancerPlugin]
)
try {
const result4 = await executorWithPlugin.generateImage('dall-e-3', {
prompt: 'A cute robot playing in a garden'
})
console.log('✅ 插件系统生成成功!')
console.log('📊 结果:', {
imagesCount: result4.images.length,
enhanced: (result4 as any).enhanced,
mediaType: result4.image.mediaType
})
} catch (error) {
console.error('❌ 插件系统生成失败:', error)
}
}
await pluginExample()
}
// 错误处理演示
async function errorHandlingExample() {
console.log('⚠️ 演示错误处理...')
try {
const executor = createExecutor('openai', {
apiKey: 'invalid-key'
})
await executor.generateImage('dall-e-3', {
prompt: 'Test image'
})
} catch (error: any) {
console.log('✅ 成功捕获错误:', error.constructor.name)
console.log('📋 错误信息:', error.message)
console.log('🏷️ 提供商ID:', error.providerId)
console.log('🏷️ 模型ID:', error.modelId)
}
}
// 运行示例
if (require.main === module) {
main()
.then(() => {
console.log('🎉 所有示例完成!')
return errorHandlingExample()
})
.then(() => {
console.log('🎯 示例程序结束')
process.exit(0)
})
.catch((error) => {
console.error('💥 程序执行出错:', error)
process.exit(1)
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@cherrystudio/ai-core",
"version": "1.0.0-alpha.11",
"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",
@@ -36,16 +36,16 @@
"ai": "^5.0.26"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.5",
"@ai-sdk/azure": "^2.0.16",
"@ai-sdk/deepseek": "^1.0.9",
"@ai-sdk/google": "^2.0.7",
"@ai-sdk/openai": "^2.0.19",
"@ai-sdk/openai-compatible": "^1.0.9",
"@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.4",
"@ai-sdk/xai": "^2.0.9",
"zod": "^3.25.0"
"@ai-sdk/provider-utils": "^3.0.9",
"@ai-sdk/xai": "^2.0.18",
"zod": "^4.1.5"
},
"devDependencies": {
"tsdown": "^0.12.9",

View File

@@ -84,7 +84,6 @@ export class ModelResolver {
*/
private resolveTraditionalModel(providerId: string, modelId: string): LanguageModelV2 {
const fullModelId = `${providerId}${DEFAULT_SEPARATOR}${modelId}`
console.log('fullModelId', fullModelId)
return globalRegistryManagement.languageModel(fullModelId as any)
}

View File

@@ -59,7 +59,7 @@ export function createGoogleOptions(options: ExtractProviderOptions<'google'>) {
/**
* 创建OpenRouter供应商选项的便捷函数
*/
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'>) {
export function createOpenRouterOptions(options: ExtractProviderOptions<'openrouter'> | Record<string, any>) {
return createProviderOptions('openrouter', options)
}

View File

@@ -1,38 +0,0 @@
export type OpenRouterProviderOptions = {
models?: string[]
/**
* https://openrouter.ai/docs/use-cases/reasoning-tokens
* One of `max_tokens` or `effort` is required.
* If `exclude` is true, reasoning will be removed from the response. Default is false.
*/
reasoning?: {
exclude?: boolean
} & (
| {
max_tokens: number
}
| {
effort: 'high' | 'medium' | 'low'
}
)
/**
* A unique identifier representing your end-user, which can
* help OpenRouter to monitor and detect abuse.
*/
user?: string
extraBody?: Record<string, unknown>
/**
* Enable usage accounting to get detailed token usage information.
* https://openrouter.ai/docs/use-cases/usage-accounting
*/
usage?: {
/**
* When true, includes token usage information in the response.
*/
include: boolean
}
}

View File

@@ -2,9 +2,8 @@ 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 OpenRouterProviderOptions } from './openrouter'
import { type XaiProviderOptions } from './xai'
import { type XaiProviderOptions } from '@ai-sdk/xai'
import { type OpenRouterProviderOptions } from '@openrouter/ai-sdk-provider'
export type ProviderOptions<T extends keyof SharedV2ProviderMetadata> = SharedV2ProviderMetadata[T]

View File

@@ -1,86 +0,0 @@
// copy from @ai-sdk/xai/xai-chat-options.ts
// 如果@ai-sdk/xai暴露出了xaiProviderOptions就删除这个文件
import * as z from 'zod/v4'
const webSourceSchema = z.object({
type: z.literal('web'),
country: z.string().length(2).optional(),
excludedWebsites: z.array(z.string()).max(5).optional(),
allowedWebsites: z.array(z.string()).max(5).optional(),
safeSearch: z.boolean().optional()
})
const xSourceSchema = z.object({
type: z.literal('x'),
xHandles: z.array(z.string()).optional()
})
const newsSourceSchema = z.object({
type: z.literal('news'),
country: z.string().length(2).optional(),
excludedWebsites: z.array(z.string()).max(5).optional(),
safeSearch: z.boolean().optional()
})
const rssSourceSchema = z.object({
type: z.literal('rss'),
links: z.array(z.url()).max(1) // currently only supports one RSS link
})
const searchSourceSchema = z.discriminatedUnion('type', [
webSourceSchema,
xSourceSchema,
newsSourceSchema,
rssSourceSchema
])
export const xaiProviderOptions = z.object({
/**
* reasoning effort for reasoning models
* only supported by grok-3-mini and grok-3-mini-fast models
*/
reasoningEffort: z.enum(['low', 'high']).optional(),
searchParameters: z
.object({
/**
* search mode preference
* - "off": disables search completely
* - "auto": model decides whether to search (default)
* - "on": always enables search
*/
mode: z.enum(['off', 'auto', 'on']),
/**
* whether to return citations in the response
* defaults to true
*/
returnCitations: z.boolean().optional(),
/**
* start date for search data (ISO8601 format: YYYY-MM-DD)
*/
fromDate: z.string().optional(),
/**
* end date for search data (ISO8601 format: YYYY-MM-DD)
*/
toDate: z.string().optional(),
/**
* maximum number of search results to consider
* defaults to 20
*/
maxSearchResults: z.number().min(1).max(50).optional(),
/**
* data sources to search from
* defaults to ["web", "x"] if not specified
*/
sources: z.array(searchSourceSchema).optional()
})
.optional()
})
export type XaiProviderOptions = z.infer<typeof xaiProviderOptions>

View File

@@ -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
}
})

View File

@@ -4,7 +4,12 @@
*/
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 } from './webSearchPlugin'
export type {
PromptToolUseConfig,
ToolUseRequestContext,
ToolUseResult
} from './toolUsePlugin/type'
export { webSearchPlugin, type WebSearchPluginConfig } from './webSearchPlugin'

View File

@@ -27,10 +27,20 @@ export class StreamEventManager {
/**
* 发送步骤完成事件
*/
sendStepFinishEvent(controller: StreamController, chunk: any): void {
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: 'stop',
finishReason,
response: chunk.response,
usage: chunk.usage,
providerMetadata: chunk.providerMetadata
@@ -43,28 +53,32 @@ export class StreamEventManager {
async handleRecursiveCall(
controller: StreamController,
recursiveParams: any,
context: AiRequestContext,
stepId: string
context: AiRequestContext
): Promise<void> {
try {
console.log('[MCP Prompt] Starting recursive call after tool execution...')
// try {
// 重置工具执行状态,准备处理新的步骤
context.hasExecutedToolsInCurrentStep = false
const recursiveResult = await context.recursiveCall(recursiveParams)
const recursiveResult = await context.recursiveCall(recursiveParams)
if (recursiveResult && recursiveResult.fullStream) {
await this.pipeRecursiveStream(controller, recursiveResult.fullStream)
} else {
console.warn('[MCP Prompt] No fullstream found in recursive result:', recursiveResult)
}
} catch (error) {
this.handleRecursiveCallError(controller, error, stepId)
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): Promise<void> {
private async pipeRecursiveStream(
controller: StreamController,
recursiveStream: ReadableStream,
context?: AiRequestContext
): Promise<void> {
const reader = recursiveStream.getReader()
try {
while (true) {
@@ -73,9 +87,16 @@ export class StreamEventManager {
break
}
if (value.type === 'finish') {
// 迭代的流不发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)
}
@@ -87,25 +108,25 @@ export class StreamEventManager {
/**
* 处理递归调用错误
*/
private handleRecursiveCallError(controller: StreamController, error: unknown, stepId: string): void {
console.error('[MCP Prompt] Recursive call failed:', error)
// 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'
}
})
// // 使用 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[工具执行后递归调用失败,继续对话...]'
})
}
// // // 继续发送文本增量,保持流的连续性
// // controller.enqueue({
// // type: 'text-delta',
// // id: stepId,
// // text: '\n\n[工具执行后递归调用失败,继续对话...]'
// // })
// }
/**
* 构建递归调用的参数
@@ -136,4 +157,18 @@ export class StreamEventManager {
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)
}
}

View File

@@ -4,7 +4,7 @@
* 负责工具的执行、结果格式化和相关事件发送
* 从 promptToolUsePlugin.ts 中提取出来以降低复杂度
*/
import type { ToolSet } from 'ai'
import type { ToolSet, TypedToolError } from 'ai'
import type { ToolUseResult } from './type'
@@ -38,7 +38,6 @@ export class ToolExecutor {
controller: StreamController
): Promise<ExecutedResult[]> {
const executedResults: ExecutedResult[] = []
for (const toolUse of toolUses) {
try {
const tool = tools[toolUse.toolName]
@@ -46,17 +45,12 @@ export class ToolExecutor {
throw new Error(`Tool "${toolUse.toolName}" has no execute method`)
}
// 发送工具调用开始事件
this.sendToolStartEvents(controller, toolUse)
console.log(`[MCP Prompt Stream] Executing tool: ${toolUse.toolName}`, toolUse.arguments)
// 发送 tool-call 事件
controller.enqueue({
type: 'tool-call',
toolCallId: toolUse.id,
toolName: toolUse.toolName,
input: tool.inputSchema
input: toolUse.arguments
})
const result = await tool.execute(toolUse.arguments, {
@@ -111,45 +105,46 @@ export class ToolExecutor {
/**
* 发送工具调用开始相关事件
*/
private sendToolStartEvents(controller: StreamController, toolUse: ToolUseResult): void {
// 发送 tool-input-start 事件
controller.enqueue({
type: 'tool-input-start',
id: toolUse.id,
toolName: toolUse.toolName
})
}
// private sendToolStartEvents(controller: StreamController, toolUse: ToolUseResult): void {
// // 发送 tool-input-start 事件
// controller.enqueue({
// type: 'tool-input-start',
// id: toolUse.id,
// toolName: toolUse.toolName
// })
// }
/**
* 处理工具执行错误
*/
private handleToolError(
private handleToolError<T extends ToolSet>(
toolUse: ToolUseResult,
error: unknown,
controller: StreamController
// _tools: ToolSet
): ExecutedResult {
// 使用 AI SDK 标准错误格式
// const toolError: TypedToolError<typeof _tools> = {
// type: 'tool-error',
// toolCallId: toolUse.id,
// toolName: toolUse.toolName,
// input: toolUse.arguments,
// error: error instanceof Error ? error.message : String(error)
// }
const toolError: TypedToolError<T> = {
type: 'tool-error',
toolCallId: toolUse.id,
toolName: toolUse.toolName,
input: toolUse.arguments,
error
}
// controller.enqueue(toolError)
controller.enqueue(toolError)
// 发送标准错误事件
controller.enqueue({
type: 'error',
error: error instanceof Error ? error.message : String(error)
})
// 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 instanceof Error ? error.message : String(error),
result: error,
isError: true
}
}

View File

@@ -8,9 +8,19 @@ 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
*/
@@ -146,8 +156,10 @@ Assistant: The population of Shanghai is 26 million, while Guangzhou has a popul
/**
* 构建可用工具部分(提取自 Cherry Studio
*/
function buildAvailableTools(tools: ToolSet): string {
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 `
@@ -162,7 +174,7 @@ function buildAvailableTools(tools: ToolSet): string {
})
.join('\n')
return `<tools>
${availableTools}
${result}
</tools>`
}
@@ -171,6 +183,7 @@ ${availableTools}
*/
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)
@@ -249,13 +262,11 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
}
context.mcpTools = params.tools
console.log('tools stored in context', params.tools)
// 构建系统提示符
const userSystemPrompt = typeof params.system === 'string' ? params.system : ''
const systemPrompt = buildSystemPrompt(userSystemPrompt, params.tools)
let systemMessage: string | null = systemPrompt
console.log('config.context', context)
if (config.createSystemMessage) {
// 🎯 如果用户提供了自定义处理函数,使用它
systemMessage = config.createSystemMessage(systemPrompt, params, context)
@@ -268,20 +279,40 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
tools: undefined
}
context.originalParams = transformedParams
console.log('transformedParams', transformedParams)
return transformedParams
},
transformStream: (_: any, context: AiRequestContext) => () => {
let textBuffer = ''
let stepId = ''
// 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>>({
@@ -289,83 +320,106 @@ export const createPromptToolUsePlugin = (config: PromptToolUseConfig = {}) => {
chunk: TextStreamPart<TOOLS>,
controller: TransformStreamDefaultController<TextStreamPart<TOOLS>>
) {
// 收集文本内容
if (chunk.type === 'text-delta') {
textBuffer += chunk.text || ''
stepId = chunk.id || ''
controller.enqueue(chunk)
// Hold住text-start事件直到确认有非工具标签内容
if ((chunk as any).type === 'text-start') {
pendingTextStart = chunk
return
}
if (chunk.type === 'text-end' || chunk.type === 'finish-step') {
const tools = context.mcpTools
if (!tools || Object.keys(tools).length === 0) {
controller.enqueue(chunk)
return
}
// text-delta阶段收集文本内容并过滤工具标签
if (chunk.type === 'text-delta') {
textBuffer += chunk.text || ''
// stepId = chunk.id || ''
// 解析工具调用
const { results: parsedTools, content: parsedContent } = parseToolUse(textBuffer, tools)
const validToolUses = parsedTools.filter((t) => t.status === 'pending')
// 使用TagExtractor过滤工具标签只传递非标签内容到UI层
const extractionResults = tagExtractor.processText(chunk.text || '')
// 如果没有有效的工具调用,直接传递原始事件
if (validToolUses.length === 0) {
controller.enqueue(chunk)
return
}
if (chunk.type === 'text-end') {
controller.enqueue({
type: 'text-end',
id: stepId,
providerMetadata: {
text: {
value: parsedContent
}
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
}
})
return
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
}
}
controller.enqueue({
...chunk,
finishReason: 'tool-calls'
})
// 发送步骤开始事件
streamEventManager.sendStepStartEvent(controller)
// 执行工具调用
const executedResults = await toolExecutor.executeTools(validToolUses, tools, controller)
// 发送步骤完成事件
streamEventManager.sendStepFinishEvent(controller, chunk)
// 处理递归调用
if (validToolUses.length > 0) {
const toolResultsText = toolExecutor.formatToolResults(executedResults)
const recursiveParams = streamEventManager.buildRecursiveParams(
context,
textBuffer,
toolResultsText,
tools
)
await streamEventManager.handleRecursiveCall(controller, recursiveParams, context, stepId)
}
// 如果没有执行工具调用直接传递原始finish-step事件
controller.enqueue(chunk)
// 清理状态
textBuffer = ''
return
}
// 对于其他类型的事件,直接传递
controller.enqueue(chunk)
// 处理 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() {
// 流结束时的清理工作
console.log('[MCP Prompt] Stream ended, cleaning up...')
// 清理pending状态
pendingTextStart = null
hasStartedText = false
}
})
}

View File

@@ -3,13 +3,16 @@ import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai'
import { ProviderOptionsMap } from '../../../options/types'
import { OpenRouterSearchConfig } from './openrouter'
/**
* 从 AI SDK 的工具函数中提取参数类型,以确保类型安全。
*/
type OpenAISearchConfig = Parameters<typeof openai.tools.webSearchPreview>[0]
type AnthropicSearchConfig = Parameters<typeof anthropic.tools.webSearch_20250305>[0]
type GoogleSearchConfig = Parameters<typeof google.tools.googleSearch>[0]
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']>
/**
* 插件初始化时接收的完整配置对象
@@ -18,10 +21,12 @@ type GoogleSearchConfig = Parameters<typeof google.tools.googleSearch>[0]
*/
export interface WebSearchPluginConfig {
openai?: OpenAISearchConfig
'openai-chat'?: OpenAISearchPreviewConfig
anthropic?: AnthropicSearchConfig
xai?: ProviderOptionsMap['xai']['searchParameters']
google?: GoogleSearchConfig
'google-vertex'?: GoogleSearchConfig
openrouter?: OpenRouterSearchConfig
}
/**
@@ -31,6 +36,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
google: {},
'google-vertex': {},
openai: {},
'openai-chat': {},
xai: {
mode: 'on',
returnCitations: true,
@@ -39,6 +45,14 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
},
anthropic: {
maxUses: 5
},
openrouter: {
plugins: [
{
id: 'web',
max_results: 5
}
]
}
}

View File

@@ -6,7 +6,7 @@ import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai'
import { createXaiOptions, mergeProviderOptions } from '../../../options'
import { createOpenRouterOptions, createXaiOptions, mergeProviderOptions } from '../../../options'
import { definePlugin } from '../../'
import type { AiRequestContext } from '../../types'
import { DEFAULT_WEB_SEARCH_CONFIG, WebSearchPluginConfig } from './helper'
@@ -27,7 +27,14 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
case 'openai': {
if (config.openai) {
if (!params.tools) params.tools = {}
params.tools.web_search_preview = openai.tools.webSearchPreview(config.openai)
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
}
@@ -56,6 +63,14 @@ export const webSearchPlugin = (config: WebSearchPluginConfig = DEFAULT_WEB_SEAR
}
break
}
case 'openrouter': {
if (config.openrouter) {
const searchOptions = createOpenRouterOptions(config.openrouter)
params.providerOptions = mergeProviderOptions(params.providerOptions, searchOptions)
}
break
}
}
return params

View File

@@ -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
}
}

View File

@@ -1,5 +1,8 @@
// 核心类型和接口
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'
@@ -9,16 +12,16 @@ export { PluginManager } from './manager'
// 工具函数
export function createContext<T extends ProviderId>(
providerId: T,
modelId: string,
model: LanguageModel | ImageModelV2,
originalParams: any
): AiRequestContext {
return {
providerId,
modelId,
model,
originalParams,
metadata: {},
startTime: Date.now(),
requestId: `${providerId}-${modelId}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
requestId: `${providerId}-${typeof model === 'string' ? model : model?.modelId}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
// 占位
recursiveCall: () => Promise.resolve(null)
}

View File

@@ -14,7 +14,7 @@ export type RecursiveCallFn = (newParams: any) => Promise<any>
*/
export interface AiRequestContext {
providerId: ProviderId
modelId: string
model: LanguageModel | ImageModelV2
originalParams: any
metadata: Record<string, any>
startTime: number

View File

@@ -9,9 +9,11 @@ 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 { customProvider, type Provider } from 'ai'
import * as z from 'zod'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { customProvider, Provider } from 'ai'
import { z } from 'zod'
/**
* 基础 Provider IDs
@@ -25,7 +27,8 @@ export const baseProviderIds = [
'xai',
'azure',
'azure-responses',
'deepseek'
'deepseek',
'openrouter'
] as const
/**
@@ -38,14 +41,16 @@ export const baseProviderIdSchema = z.enum(baseProviderIds)
*/
export type BaseProviderId = z.infer<typeof baseProviderIdSchema>
export const baseProviderSchema = z.object({
id: baseProviderIdSchema,
name: z.string(),
creator: z.function().args(z.any()).returns(z.any()) as z.ZodType<(options: any) => Provider>,
supportsImageGeneration: z.boolean()
})
export const isBaseProvider = (id: ProviderId): id is BaseProviderId => {
return baseProviderIdSchema.safeParse(id).success
}
export type BaseProvider = z.infer<typeof baseProviderSchema>
type BaseProvider = {
id: BaseProviderId
name: string
creator: (options: any) => Provider | LanguageModelV2
supportsImageGeneration: boolean
}
/**
* 基础 Providers 定义
@@ -121,6 +126,12 @@ export const baseProviders = [
name: 'DeepSeek',
creator: createDeepSeek,
supportsImageGeneration: false
},
{
id: 'openrouter',
name: 'OpenRouter',
creator: createOpenRouter,
supportsImageGeneration: true
}
] as const satisfies BaseProvider[]
@@ -148,7 +159,12 @@ export const providerConfigSchema = z
.object({
id: customProviderIdSchema, // 只允许自定义ID
name: z.string().min(1),
creator: z.function().optional(),
creator: z
.function({
input: z.any(),
output: z.any()
})
.optional(),
import: z.function().optional(),
creatorFunctionName: z.string().optional(),
supportsImageGeneration: z.boolean().default(false),

View File

@@ -4,12 +4,12 @@
*/
import { ImageModelV2, LanguageModelV2, LanguageModelV2Middleware } from '@ai-sdk/provider'
import {
experimental_generateImage as generateImage,
generateObject,
generateText,
experimental_generateImage as _generateImage,
generateObject as _generateObject,
generateText as _generateText,
LanguageModel,
streamObject,
streamText
streamObject as _streamObject,
streamText as _streamText
} from 'ai'
import { globalModelResolver } from '../models'
@@ -18,7 +18,14 @@ import { type AiPlugin, type AiRequestContext, definePlugin } from '../plugins'
import { type ProviderId } from '../providers'
import { ImageGenerationError, ImageModelResolutionError } from './errors'
import { PluginEngine } from './pluginEngine'
import { type RuntimeConfig } from './types'
import type {
generateImageParams,
generateObjectParams,
generateTextParams,
RuntimeConfig,
streamObjectParams,
streamTextParams
} from './types'
export class RuntimeExecutor<T extends ProviderId = ProviderId> {
public pluginEngine: PluginEngine<T>
@@ -75,12 +82,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
* 流式文本生成
*/
async streamText(
params: Parameters<typeof streamText>[0],
params: streamTextParams,
options?: {
middlewares?: LanguageModelV2Middleware[]
}
): Promise<ReturnType<typeof streamText>> {
const { model, ...restParams } = params
): Promise<ReturnType<typeof _streamText>> {
const { model } = params
// 根据 model 类型决定插件配置
if (typeof model === 'string') {
@@ -94,19 +101,16 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
return this.pluginEngine.executeStreamWithPlugins(
'streamText',
model,
restParams,
async (resolvedModel, transformedParams, streamTransforms) => {
params,
(resolvedModel, transformedParams, streamTransforms) => {
const experimental_transform =
params?.experimental_transform ?? (streamTransforms.length > 0 ? streamTransforms : undefined)
const finalParams = {
model: resolvedModel,
return _streamText({
...transformedParams,
model: resolvedModel,
experimental_transform
} as Parameters<typeof streamText>[0]
return await streamText(finalParams)
})
}
)
}
@@ -117,12 +121,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
* 生成文本
*/
async generateText(
params: Parameters<typeof generateText>[0],
params: generateTextParams,
options?: {
middlewares?: LanguageModelV2Middleware[]
}
): Promise<ReturnType<typeof generateText>> {
const { model, ...restParams } = params
): Promise<ReturnType<typeof _generateText>> {
const { model } = params
// 根据 model 类型决定插件配置
if (typeof model === 'string') {
@@ -134,12 +138,10 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
}
return this.pluginEngine.executeWithPlugins(
return this.pluginEngine.executeWithPlugins<Parameters<typeof _generateText>[0], ReturnType<typeof _generateText>>(
'generateText',
model,
restParams,
async (resolvedModel, transformedParams) =>
generateText({ model: resolvedModel, ...transformedParams } as Parameters<typeof generateText>[0])
params,
(resolvedModel, transformedParams) => _generateText({ ...transformedParams, model: resolvedModel })
)
}
@@ -147,12 +149,12 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
* 生成结构化对象
*/
async generateObject(
params: Parameters<typeof generateObject>[0],
params: generateObjectParams,
options?: {
middlewares?: LanguageModelV2Middleware[]
}
): Promise<ReturnType<typeof generateObject>> {
const { model, ...restParams } = params
): Promise<ReturnType<typeof _generateObject>> {
const { model } = params
// 根据 model 类型决定插件配置
if (typeof model === 'string') {
@@ -164,25 +166,23 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
}
return this.pluginEngine.executeWithPlugins(
return this.pluginEngine.executeWithPlugins<generateObjectParams, ReturnType<typeof _generateObject>>(
'generateObject',
model,
restParams,
async (resolvedModel, transformedParams) =>
generateObject({ model: resolvedModel, ...transformedParams } as Parameters<typeof generateObject>[0])
params,
async (resolvedModel, transformedParams) => _generateObject({ ...transformedParams, model: resolvedModel })
)
}
/**
* 流式生成结构化对象
*/
async streamObject(
params: Parameters<typeof streamObject>[0],
streamObject(
params: streamObjectParams,
options?: {
middlewares?: LanguageModelV2Middleware[]
}
): Promise<ReturnType<typeof streamObject>> {
const { model, ...restParams } = params
): Promise<ReturnType<typeof _streamObject>> {
const { model } = params
// 根据 model 类型决定插件配置
if (typeof model === 'string') {
@@ -194,23 +194,17 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
}
return this.pluginEngine.executeWithPlugins(
'streamObject',
model,
restParams,
async (resolvedModel, transformedParams) =>
streamObject({ model: resolvedModel, ...transformedParams } as Parameters<typeof streamObject>[0])
return this.pluginEngine.executeStreamWithPlugins('streamObject', params, (resolvedModel, transformedParams) =>
_streamObject({ ...transformedParams, model: resolvedModel })
)
}
/**
* 生成图像
*/
async generateImage(
params: Omit<Parameters<typeof generateImage>[0], 'model'> & { model: string | ImageModelV2 }
): Promise<ReturnType<typeof generateImage>> {
generateImage(params: generateImageParams): Promise<ReturnType<typeof _generateImage>> {
try {
const { model, ...restParams } = params
const { model } = params
// 根据 model 类型决定插件配置
if (typeof model === 'string') {
@@ -219,13 +213,8 @@ export class RuntimeExecutor<T extends ProviderId = ProviderId> {
this.pluginEngine.usePlugins([this.createConfigureContextPlugin()])
}
return await this.pluginEngine.executeImageWithPlugins(
'generateImage',
model,
restParams,
async (resolvedModel, transformedParams) => {
return await generateImage({ model: resolvedModel, ...transformedParams })
}
return this.pluginEngine.executeImageWithPlugins('generateImage', params, (resolvedModel, transformedParams) =>
_generateImage({ ...transformedParams, model: resolvedModel })
)
} catch (error) {
if (error instanceof Error) {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @eslint-react/naming-convention/context-name */
import { ImageModelV2 } from '@ai-sdk/provider'
import { LanguageModel } from 'ai'
import { experimental_generateImage, generateObject, generateText, LanguageModel, streamObject, streamText } from 'ai'
import { type AiPlugin, createContext, PluginManager } from '../plugins'
import { type ProviderId } from '../providers/types'
@@ -62,17 +62,19 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
* 执行带插件的操作(非流式)
* 提供给AiExecutor使用
*/
async executeWithPlugins<TParams, TResult>(
async executeWithPlugins<
TParams extends Parameters<typeof generateText | typeof generateObject>[0],
TResult extends ReturnType<typeof generateText | typeof generateObject>
>(
methodName: string,
model: LanguageModel,
params: TParams,
executor: (model: LanguageModel, transformedParams: TParams) => Promise<TResult>,
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
@@ -83,13 +85,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
}
// 使用正确的createContext创建请求上下文
const context = _context ? _context : createContext(this.providerId, modelId, params)
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, model, newParams, executor, context)
const result = await this.executeWithPlugins(methodName, newParams, executor, context)
context.isRecursiveCall = false
return result
}
@@ -138,17 +140,19 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
* 执行带插件的图像生成操作
* 提供给AiExecutor使用
*/
async executeImageWithPlugins<TParams, TResult>(
async executeImageWithPlugins<
TParams extends Omit<Parameters<typeof experimental_generateImage>[0], 'model'> & { model: string | ImageModelV2 },
TResult extends ReturnType<typeof experimental_generateImage>
>(
methodName: string,
model: ImageModelV2 | string,
params: TParams,
executor: (model: ImageModelV2, transformedParams: TParams) => Promise<TResult>,
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
@@ -159,13 +163,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
}
// 使用正确的createContext创建请求上下文
const context = _context ? _context : createContext(this.providerId, modelId, params)
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, model, newParams, executor, context)
const result = await this.executeImageWithPlugins(methodName, newParams, executor, context)
context.isRecursiveCall = false
return result
}
@@ -214,17 +218,19 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
* 执行流式调用的通用逻辑(支持流转换器)
* 提供给AiExecutor使用
*/
async executeStreamWithPlugins<TParams, TResult>(
async executeStreamWithPlugins<
TParams extends Parameters<typeof streamText | typeof streamObject>[0],
TResult extends ReturnType<typeof streamText | typeof streamObject>
>(
methodName: string,
model: LanguageModel,
params: TParams,
executor: (model: LanguageModel, transformedParams: TParams, streamTransforms: any[]) => Promise<TResult>,
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
@@ -235,13 +241,13 @@ export class PluginEngine<T extends ProviderId = ProviderId> {
}
// 创建请求上下文
const context = _context ? _context : createContext(this.providerId, modelId, params)
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, model, newParams, executor, context)
const result = await this.executeStreamWithPlugins(methodName, newParams, executor, context)
context.isRecursiveCall = false
return result
}

View File

@@ -1,6 +1,9 @@
/**
* 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'
@@ -13,3 +16,11 @@ export interface RuntimeConfig<T extends ProviderId = ProviderId> {
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]

View File

@@ -1,26 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"allowSyntheticDefaultImports": true,
"declaration": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"noEmitOnError": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
"outDir": "./dist",
"resolveJsonModule": true,
"rootDir": "./src",
"skipLibCheck": true,
"strict": true,
"target": "ES2020"
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
"exclude": ["node_modules", "dist"],
"include": ["src/**/*"]
}

View File

@@ -67,13 +67,13 @@
"dist"
],
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@tiptap/core": "^3.2.0",
"@tiptap/pm": "^3.2.0",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"prettier": "^3.5.3",
"tsdown": "^0.13.3"
},
"peerDependencies": {
@@ -87,7 +87,7 @@
},
"scripts": {
"build": "tsdown",
"lint": "prettier ./src/ --write && eslint --fix ./src/"
"lint": "biome format ./src/ --write && eslint --fix ./src/"
},
"packageManager": "yarn@4.9.1"
}

View File

@@ -35,8 +35,10 @@ export enum IpcChannel {
App_InstallBunBinary = 'app:install-bun-binary',
App_LogToMain = 'app:log-to-main',
App_SaveData = 'app:save-data',
App_GetDiskInfo = 'app:get-disk-info',
App_SetFullScreen = 'app:set-full-screen',
App_IsFullScreen = 'app:is-full-screen',
App_GetSystemFonts = 'app:get-system-fonts',
App_MacIsProcessTrusted = 'app:mac-is-process-trusted',
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
@@ -83,10 +85,13 @@ export enum IpcChannel {
Mcp_UploadDxt = 'mcp:upload-dxt',
Mcp_AbortTool = 'mcp:abort-tool',
Mcp_GetServerVersion = 'mcp:get-server-version',
Mcp_Progress = 'mcp:progress',
// Python
Python_Execute = 'python:execute',
// agent messages
AgentMessage_PersistExchange = 'agent-message:persist-exchange',
//copilot
Copilot_GetAuthMessage = 'copilot:get-auth-message',
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
@@ -123,6 +128,12 @@ export enum IpcChannel {
Windows_SetMinimumSize = 'window:set-minimum-size',
Windows_Resize = 'window:resize',
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_Reset = 'knowledge-base:reset',
@@ -250,7 +261,6 @@ export enum IpcChannel {
// Provider
Provider_AddKey = 'provider:add-key',
Provider_GetClaudeCodePort = 'provider:get-claude-code-port',
//Selection Assistant
Selection_TextSelected = 'selection:text-selected',
@@ -297,6 +307,21 @@ export enum IpcChannel {
TRACE_CLEAN_LOCAL_DATA = 'trace:cleanLocalData',
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_Run = 'code-tools:run',

View File

@@ -216,5 +216,6 @@ export enum codeTools {
qwenCode = 'qwen-code',
claudeCode = 'claude-code',
geminiCli = 'gemini-cli',
openaiCodex = 'openai-codex'
openaiCodex = 'openai-codex',
iFlowCli = 'iflow-cli'
}

View File

@@ -7,7 +7,7 @@ export type LoaderReturn = {
loaderType: string
status?: ProcessingStatus
message?: string
messageSource?: 'preprocess' | 'embedding'
messageSource?: 'preprocess' | 'embedding' | 'validation'
}
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
@@ -17,3 +17,8 @@ export type FileChangeEvent = {
filePath: string
watchPath: string
}
export type MCPProgressEvent = {
callId: string
progress: number // 0-1 range
}

6
packages/shared/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
export const defaultAppHeaders = () => {
return {
'HTTP-Referer': 'https://cherry-ai.com',
'X-Title': 'Cherry Studio'
}
}

View File

@@ -1,199 +1,274 @@
<!DOCTYPE html>
<!doctype html>
<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>
<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>
<body class="bg-gray-50">
<div class="mx-auto max-w-4xl px-4 py-8">
<!-- 中文版本 -->
<div class="mb-12">
<h1 class="mb-8 text-3xl font-bold text-gray-900">许可协议</h1>
<body class="bg-gray-50">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- 中文版本 -->
<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 class="mb-6 text-gray-700">
本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。
</p>
</section>
<section class="mb-8">
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <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">
<h2 class="mb-4 text-xl font-semibold text-gray-900">核心原则</h2>
<ul class="list-disc space-y-2 pl-6 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">2. 商业许可证 (Commercial License) - 适用于超过10人的组织或希望规避 AGPLv3
义务的用户</h2>
<ul class="list-disc pl-6 space-y-2 text-gray-700">
<li><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="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">
<h2 class="mb-4 text-xl font-semibold text-gray-900">定义:"10人及以下"</h2>
<p class="text-gray-700">
指在您的组织包括公司、非营利组织、政府机构、教育机构等任何实体能够访问、使用或以任何方式直接或间接受益于本软件Cherry
Studio功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
</p>
</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>我们欢迎社区对 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">
1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
</h2>
<ul class="list-disc space-y-2 pl-6 text-gray-700">
<li>
如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在
<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">
<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>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
</ul>
</section>
<section class="mb-8">
<h2 class="mb-4 text-xl font-semibold text-gray-900">
2. 商业许可证 (Commercial License) - 适用于超过10人的组织或希望规避 AGPLv3 义务的用户
</h2>
<ul class="list-disc space-y-2 pl-6 text-gray-700">
<li>
<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>
<hr class="my-12 border-gray-300">
<!-- 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>
</body>
</html>

View File

@@ -12,18 +12,18 @@
<body id="app">
<div :class="isDark ? 'dark-bg' : 'bg'" class="min-h-screen">
<div class="max-w-3xl mx-auto py-12 px-4">
<h1 class="text-3xl font-bold mb-8" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<div class="mx-auto max-w-3xl px-4 py-12">
<h1 class="mb-8 text-3xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900'">Release Timeline</h1>
<!-- Loading状态 -->
<div v-if="loading" class="text-center py-8">
<div v-if="loading" class="py-8 text-center">
<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>
</div>
<!-- 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 列表 -->
<div v-else class="space-y-8">
@@ -32,21 +32,21 @@
:key="release.id"
class="relative pl-8"
: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
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'">
<div class="flex items-start justify-between mb-4">
<div class="mb-4 flex items-start justify-between">
<div>
<h2 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900'">
{{ release.name || release.tag_name }}
</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) }}
</p>
</div>
<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'">
{{ release.tag_name }}
</span>

File diff suppressed because one or more lines are too long

View File

@@ -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
);

View File

@@ -0,0 +1 @@
ALTER TABLE `session_messages` ADD `agent_session_id` text DEFAULT '';

View File

@@ -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": {}
}
}

View File

@@ -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": {}
}
}

View File

@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1758091173882,
"tag": "0000_confused_wendigo",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1758187378775,
"tag": "0001_woozy_captain_flint",
"breakpoints": true
}
]
}

View File

@@ -9,8 +9,9 @@ import * as path from 'path'
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
const baseLocale = 'zh-cn'
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
const baseFileName = `${baseLocale}.json`
const baseLocalePath = path.join(__dirname, '../src/renderer/src/i18n/locales', baseFileName)
type I18NValue = string | { [key: string]: I18NValue }
type I18N = { [key: string]: I18NValue }
@@ -105,6 +106,9 @@ const translateRecursively = async (originObj: I18N, systemPrompt: string): Prom
}
const main = async () => {
if (!fs.existsSync(baseLocalePath)) {
throw new Error(`${baseLocalePath} not found.`)
}
const localeFiles = fs
.readdirSync(localesDir)
.filter((file) => file.endsWith('.json') && file !== baseFileName)

View File

@@ -35,6 +35,9 @@ const allX64 = {
'@napi-rs/system-ocr-win32-x64-msvc': '1.0.2'
}
const claudeCodeVenderPath = '@anthropic-ai/claude-code/vendor'
const claudeCodeVenders = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
const platformToArch = {
mac: 'darwin',
windows: 'win32',
@@ -46,9 +49,6 @@ exports.default = async function (context) {
const archType = arch === Arch.arm64 ? 'arm64' : 'x64'
const platform = context.packager.platform.name
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
const downloadPackages = async (packages) => {
console.log('downloading packages ......')
const downloadPromises = []
@@ -67,25 +67,39 @@ exports.default = async function (context) {
await Promise.all(downloadPromises)
}
const changeFilters = async (packages, filtersToExclude, filtersToInclude) => {
await downloadPackages(packages)
const changeFilters = async (filtersToExclude, filtersToInclude) => {
// remove filters for the target architecture (allow inclusion)
let filters = context.packager.config.files[0].filter
filters = filters.filter((filter) => !filtersToInclude.includes(filter))
// add filters for other architectures (exclude them)
filters.push(...filtersToExclude)
context.packager.config.files[0].filter = filters
}
if (arch === Arch.arm64) {
await changeFilters(allArm64, x64Filters, arm64Filters)
return
}
await downloadPackages(arch === Arch.arm64 ? allArm64 : allX64)
if (arch === Arch.x64) {
await changeFilters(allX64, arm64Filters, x64Filters)
return
const arm64Filters = Object.keys(allArm64).map((f) => '!node_modules/' + f + '/**')
const x64Filters = Object.keys(allX64).map((f) => '!node_modules/' + f + '/*')
const excludeClaudeCodeRipgrepFilters = claudeCodeVenders
.filter((f) => f !== `${archType}-${platformToArch[platform]}`)
.map((f) => '!node_modules/' + claudeCodeVenderPath + '/ripgrep/' + f + '/**')
const excludeClaudeCodeJBPlutins = ['!node_modules/' + claudeCodeVenderPath + '/' + 'claude-code-jetbrains-plugin']
const includeClaudeCodeFilters = [
'!node_modules/' + claudeCodeVenderPath + '/' + `${archType}-${platformToArch[platform]}/**`
]
if (arch === Arch.arm64) {
await changeFilters(
[...x64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...arm64Filters, ...includeClaudeCodeFilters]
)
} else {
await changeFilters(
[...arm64Filters, ...excludeClaudeCodeRipgrepFilters, ...excludeClaudeCodeJBPlutins],
[...x64Filters, ...includeClaudeCodeFilters]
)
}
}

View File

@@ -4,7 +4,7 @@ import * as path from 'path'
import { sortedObjectByKeys } from './sort'
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const baseLocale = 'zh-cn'
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName)

View File

@@ -5,7 +5,7 @@ import { sortedObjectByKeys } from './sort'
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
const baseLocale = 'zh-cn'
const baseLocale = process.env.BASE_LOCALE ?? 'zh-cn'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(localesDir, baseFileName)

View File

@@ -1,6 +1,6 @@
import { exec } from 'child_process'
import * as fs from 'fs/promises'
import linguistLanguages from 'linguist-languages'
import * as linguistLanguages from 'linguist-languages'
import * as path from 'path'
import { promisify } from 'util'
@@ -75,17 +75,17 @@ export const languages: Record<string, LanguageData> = ${languagesObjectString};
}
/**
* Formats a file using Prettier.
* Formats a file using Biome.
* @param filePath The path to the file to format.
*/
async function formatWithPrettier(filePath: string): Promise<void> {
console.log('🎨 Formatting file with Prettier...')
async function format(filePath: string): Promise<void> {
console.log('🎨 Formatting file with Biome...')
try {
await execAsync(`yarn prettier --write ${filePath}`)
console.log('✅ Prettier formatting complete.')
await execAsync(`yarn biome format --write ${filePath}`)
console.log('✅ Biome formatting complete.')
} catch (e: any) {
console.error('❌ Prettier formatting failed:', e.stdout || e.stderr)
throw new Error('Prettier formatting failed.')
console.error('❌ Biome formatting failed:', e.stdout || e.stderr)
throw new Error('Biome formatting failed.')
}
}
@@ -116,7 +116,7 @@ async function updateLanguagesFile(): Promise<void> {
await fs.writeFile(LANGUAGES_FILE_PATH, fileContent, 'utf-8')
console.log(`✅ Successfully wrote to ${LANGUAGES_FILE_PATH}`)
await formatWithPrettier(LANGUAGES_FILE_PATH)
await format(LANGUAGES_FILE_PATH)
await checkTypeScript(LANGUAGES_FILE_PATH)
console.log('🎉 Successfully updated languages.ts file!')

129
src/main/apiServer/app.ts Normal file
View File

@@ -0,0 +1,129 @@
import { loggerService } from '@main/services/LoggerService'
import cors from 'cors'
import express from 'express'
import { v4 as uuidv4 } from 'uuid'
import { authMiddleware } from './middleware/auth'
import { errorHandler } from './middleware/error'
import { setupOpenAPIDocumentation } from './middleware/openapi'
import { agentsRoutes } from './routes/agents'
import { chatRoutes } from './routes/chat'
import { mcpRoutes } from './routes/mcp'
import { messagesRoutes } from './routes/messages'
import { modelsRoutes } from './routes/models'
const logger = loggerService.withContext('ApiServer')
const app = express()
// Global middleware
app.use((req, res, next) => {
const start = Date.now()
res.on('finish', () => {
const duration = Date.now() - start
logger.info(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`)
})
next()
})
app.use((_req, res, next) => {
res.setHeader('X-Request-ID', uuidv4())
next()
})
app.use(
cors({
origin: '*',
allowedHeaders: ['Content-Type', 'Authorization'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
})
)
/**
* @swagger
* /health:
* get:
* summary: Health check endpoint
* description: Check server status (no authentication required)
* tags: [Health]
* security: []
* responses:
* 200:
* description: Server is healthy
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: ok
* timestamp:
* type: string
* format: date-time
* version:
* type: string
* example: 1.0.0
*/
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0'
})
})
/**
* @swagger
* /:
* get:
* summary: API information
* description: Get basic API information and available endpoints
* tags: [General]
* security: []
* responses:
* 200:
* description: API information
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* example: Cherry Studio API
* version:
* type: string
* example: 1.0.0
* endpoints:
* type: object
*/
app.get('/', (_req, res) => {
res.json({
name: 'Cherry Studio API',
version: '1.0.0',
endpoints: {
health: 'GET /health'
}
})
})
// API v1 routes with auth
const apiRouter = express.Router()
apiRouter.use(authMiddleware)
apiRouter.use(express.json())
// Mount routes
apiRouter.use('/chat', chatRoutes)
apiRouter.use('/mcps', mcpRoutes)
apiRouter.use('/messages', messagesRoutes)
apiRouter.use('/models', modelsRoutes)
apiRouter.use('/agents', agentsRoutes)
app.use('/v1', apiRouter)
// Setup OpenAPI documentation
setupOpenAPIDocumentation(app)
// Error handling (must be last)
app.use(errorHandler)
export { app }

View File

@@ -0,0 +1,65 @@
import { ApiServerConfig } from '@types'
import { v4 as uuidv4 } from 'uuid'
import { loggerService } from '../services/LoggerService'
import { reduxService } from '../services/ReduxService'
const logger = loggerService.withContext('ApiServerConfig')
const defaultHost = 'localhost'
const defaultPort = 23333
class ConfigManager {
private _config: ApiServerConfig | null = null
private generateApiKey(): string {
return `cs-sk-${uuidv4()}`
}
async load(): Promise<ApiServerConfig> {
try {
const settings = await reduxService.select('state.settings')
const serverSettings = settings?.apiServer
let apiKey = serverSettings?.apiKey
if (!apiKey || apiKey.trim() === '') {
apiKey = this.generateApiKey()
await reduxService.dispatch({
type: 'settings/setApiServerApiKey',
payload: apiKey
})
}
this._config = {
enabled: serverSettings?.enabled ?? false,
port: serverSettings?.port ?? defaultPort,
host: defaultHost,
apiKey: apiKey
}
return this._config
} catch (error: any) {
logger.warn('Failed to load config from Redux, using defaults:', error)
this._config = {
enabled: false,
port: defaultPort,
host: defaultHost,
apiKey: this.generateApiKey()
}
return this._config
}
}
async get(): Promise<ApiServerConfig> {
if (!this._config) {
await this.load()
}
if (!this._config) {
throw new Error('Failed to load API server configuration')
}
return this._config
}
async reload(): Promise<ApiServerConfig> {
return await this.load()
}
}
export const config = new ConfigManager()

View File

@@ -0,0 +1,2 @@
export { config } from './config'
export { apiServer } from './server'

View File

@@ -0,0 +1,62 @@
import crypto from 'crypto'
import { NextFunction, Request, Response } from 'express'
import { config } from '../config'
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const auth = req.header('Authorization') || ''
const xApiKey = req.header('x-api-key') || ''
// Fast rejection if neither credential header provided
if (!auth && !xApiKey) {
return res.status(401).json({ error: 'Unauthorized: missing credentials' })
}
let token: string | undefined
// Prefer Bearer if wellformed
if (auth) {
const trimmed = auth.trim()
const bearerPrefix = /^Bearer\s+/i
if (bearerPrefix.test(trimmed)) {
const candidate = trimmed.replace(bearerPrefix, '').trim()
if (!candidate) {
return res.status(401).json({ error: 'Unauthorized: empty bearer token' })
}
token = candidate
}
}
// Fallback to x-api-key if token still not resolved
if (!token && xApiKey) {
if (!xApiKey.trim()) {
return res.status(401).json({ error: 'Unauthorized: empty x-api-key' })
}
token = xApiKey.trim()
}
if (!token) {
// At this point we had at least one header, but none yielded a usable token
return res.status(401).json({ error: 'Unauthorized: invalid credentials format' })
}
const { apiKey } = await config.get()
if (!apiKey) {
// If server not configured, treat as forbidden (or could be 500). Choose 403 to avoid leaking config state.
return res.status(403).json({ error: 'Forbidden' })
}
// Timing-safe compare when lengths match, else immediate forbidden
if (token.length !== apiKey.length) {
return res.status(403).json({ error: 'Forbidden' })
}
const tokenBuf = Buffer.from(token)
const keyBuf = Buffer.from(apiKey)
if (!crypto.timingSafeEqual(tokenBuf, keyBuf)) {
return res.status(403).json({ error: 'Forbidden' })
}
return next()
}

View File

@@ -0,0 +1,21 @@
import { NextFunction, Request, Response } from 'express'
import { loggerService } from '../../services/LoggerService'
const logger = loggerService.withContext('ApiServerErrorHandler')
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
export const errorHandler = (err: Error, _req: Request, res: Response, _next: NextFunction) => {
logger.error('API Server Error:', err)
// Don't expose internal errors in production
const isDev = process.env.NODE_ENV === 'development'
res.status(500).json({
error: {
message: isDev ? err.message : 'Internal server error',
type: 'server_error',
...(isDev && { stack: err.stack })
}
})
}

View File

@@ -0,0 +1,206 @@
import { Express } from 'express'
import swaggerJSDoc from 'swagger-jsdoc'
import swaggerUi from 'swagger-ui-express'
import { loggerService } from '../../services/LoggerService'
const logger = loggerService.withContext('OpenAPIMiddleware')
const swaggerOptions: swaggerJSDoc.Options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Cherry Studio API',
version: '1.0.0',
description: 'OpenAI-compatible API for Cherry Studio with additional Cherry-specific endpoints',
contact: {
name: 'Cherry Studio',
url: 'https://github.com/CherryHQ/cherry-studio'
}
},
servers: [
{
url: 'http://localhost:23333',
description: 'Local development server'
}
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Use the API key from Cherry Studio settings'
}
},
schemas: {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
properties: {
message: { type: 'string' },
type: { type: 'string' },
code: { type: 'string' }
}
}
}
},
ChatMessage: {
type: 'object',
properties: {
role: {
type: 'string',
enum: ['system', 'user', 'assistant', 'tool']
},
content: {
oneOf: [
{ type: 'string' },
{
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string' },
text: { type: 'string' },
image_url: {
type: 'object',
properties: {
url: { type: 'string' }
}
}
}
}
}
]
},
name: { type: 'string' },
tool_calls: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
type: { type: 'string' },
function: {
type: 'object',
properties: {
name: { type: 'string' },
arguments: { type: 'string' }
}
}
}
}
}
}
},
ChatCompletionRequest: {
type: 'object',
required: ['model', 'messages'],
properties: {
model: {
type: 'string',
description: 'The model to use for completion, in format provider:model-id'
},
messages: {
type: 'array',
items: { $ref: '#/components/schemas/ChatMessage' }
},
temperature: {
type: 'number',
minimum: 0,
maximum: 2,
default: 1
},
max_tokens: {
type: 'integer',
minimum: 1
},
stream: {
type: 'boolean',
default: false
},
tools: {
type: 'array',
items: {
type: 'object',
properties: {
type: { type: 'string' },
function: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
parameters: { type: 'object' }
}
}
}
}
}
}
},
Model: {
type: 'object',
properties: {
id: { type: 'string' },
object: { type: 'string', enum: ['model'] },
created: { type: 'integer' },
owned_by: { type: 'string' }
}
},
MCPServer: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
command: { type: 'string' },
args: {
type: 'array',
items: { type: 'string' }
},
env: { type: 'object' },
disabled: { type: 'boolean' }
}
}
}
},
security: [
{
BearerAuth: []
}
]
},
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
}
export function setupOpenAPIDocumentation(app: Express) {
try {
const specs = swaggerJSDoc(swaggerOptions)
// Serve OpenAPI JSON
app.get('/api-docs.json', (_req, res) => {
res.setHeader('Content-Type', 'application/json')
res.send(specs)
})
// Serve Swagger UI
app.use(
'/api-docs',
swaggerUi.serve,
swaggerUi.setup(specs, {
customCss: `
.swagger-ui .topbar { display: none; }
.swagger-ui .info .title { color: #1890ff; }
`,
customSiteTitle: 'Cherry Studio API Documentation'
})
)
logger.info('OpenAPI documentation setup complete')
logger.info('Documentation available at /api-docs')
logger.info('OpenAPI spec available at /api-docs.json')
} catch (error) {
logger.error('Failed to setup OpenAPI documentation:', error as Error)
}
}

View File

@@ -0,0 +1,532 @@
import { loggerService } from '@logger'
import { AgentModelValidationError, agentService } from '@main/services/agents'
import { ListAgentsResponse,type ReplaceAgentRequest, type UpdateAgentRequest } from '@types'
import { Request, Response } from 'express'
import type { ValidationRequest } from '../validators/zodValidator'
const logger = loggerService.withContext('ApiServerAgentsHandlers')
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
error: {
message: `Invalid ${error.context.field}: ${error.detail.message}`,
type: 'invalid_request_error',
code: error.detail.code
}
})
/**
* @swagger
* /v1/agents:
* post:
* summary: Create a new agent
* description: Creates a new autonomous agent with the specified configuration
* tags: [Agents]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateAgentRequest'
* responses:
* 201:
* description: Agent created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const createAgent = async (req: Request, res: Response): Promise<Response> => {
try {
logger.info('Creating new agent')
logger.debug('Agent data:', req.body)
const agent = await agentService.createAgent(req.body)
logger.info(`Agent created successfully: ${agent.id}`)
return res.status(201).json(agent)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Agent model validation error during create:', {
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error creating agent:', error)
return res.status(500).json({
error: {
message: `Failed to create agent: ${error.message}`,
type: 'internal_error',
code: 'agent_creation_failed'
}
})
}
}
/**
* @swagger
* /v1/agents:
* get:
* summary: List all agents
* description: Retrieves a paginated list of all agents
* tags: [Agents]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: Number of agents to return
* - in: query
* name: offset
* schema:
* type: integer
* minimum: 0
* default: 0
* description: Number of agents to skip
* responses:
* 200:
* description: List of agents
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/AgentEntity'
* total:
* type: integer
* description: Total number of agents
* limit:
* type: integer
* description: Number of agents returned
* offset:
* type: integer
* description: Number of agents skipped
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const listAgents = async (req: Request, res: Response): Promise<Response> => {
try {
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
logger.info(`Listing agents with limit=${limit}, offset=${offset}`)
const result = await agentService.listAgents({ limit, offset })
logger.info(`Retrieved ${result.agents.length} agents (total: ${result.total})`)
return res.json({
data: result.agents,
total: result.total,
limit,
offset
} satisfies ListAgentsResponse)
} catch (error: any) {
logger.error('Error listing agents:', error)
return res.status(500).json({
error: {
message: 'Failed to list agents',
type: 'internal_error',
code: 'agent_list_failed'
}
})
}
}
/**
* @swagger
* /v1/agents/{agentId}:
* get:
* summary: Get agent by ID
* description: Retrieves a specific agent by its ID
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* responses:
* 200:
* description: Agent details
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const getAgent = async (req: Request, res: Response): Promise<Response> => {
try {
const { agentId } = req.params
logger.info(`Getting agent: ${agentId}`)
const agent = await agentService.getAgent(agentId)
if (!agent) {
logger.warn(`Agent not found: ${agentId}`)
return res.status(404).json({
error: {
message: 'Agent not found',
type: 'not_found',
code: 'agent_not_found'
}
})
}
logger.info(`Agent retrieved successfully: ${agentId}`)
return res.json(agent)
} catch (error: any) {
logger.error('Error getting agent:', error)
return res.status(500).json({
error: {
message: 'Failed to get agent',
type: 'internal_error',
code: 'agent_get_failed'
}
})
}
}
/**
* @swagger
* /v1/agents/{agentId}:
* put:
* summary: Update agent
* description: Updates an existing agent with the provided data
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateAgentRequest'
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const updateAgent = async (req: Request, res: Response): Promise<Response> => {
const { agentId } = req.params
try {
logger.info(`Updating agent: ${agentId}`)
logger.debug('Update data:', req.body)
const { validatedBody } = req as ValidationRequest
const replacePayload = (validatedBody ?? {}) as ReplaceAgentRequest
const agent = await agentService.updateAgent(agentId, replacePayload, { replace: true })
if (!agent) {
logger.warn(`Agent not found for update: ${agentId}`)
return res.status(404).json({
error: {
message: 'Agent not found',
type: 'not_found',
code: 'agent_not_found'
}
})
}
logger.info(`Agent updated successfully: ${agentId}`)
return res.json(agent)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Agent model validation error during update:', {
agentId,
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error updating agent:', error)
return res.status(500).json({
error: {
message: 'Failed to update agent: ' + error.message,
type: 'internal_error',
code: 'agent_update_failed'
}
})
}
}
/**
* @swagger
* /v1/agents/{agentId}:
* patch:
* summary: Partially update agent
* description: Partially updates an existing agent with only the provided fields
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description: Agent name
* description:
* type: string
* description: Agent description
* avatar:
* type: string
* description: Agent avatar URL
* instructions:
* type: string
* description: System prompt/instructions
* model:
* type: string
* description: Main model ID
* plan_model:
* type: string
* description: Optional planning model ID
* small_model:
* type: string
* description: Optional small/fast model ID
* built_in_tools:
* type: array
* items:
* type: string
* description: Built-in tool IDs
* mcps:
* type: array
* items:
* type: string
* description: MCP tool IDs
* knowledges:
* type: array
* items:
* type: string
* description: Knowledge base IDs
* configuration:
* type: object
* description: Extensible settings
* accessible_paths:
* type: array
* items:
* type: string
* description: Accessible directory paths
* permission_mode:
* type: string
* enum: [readOnly, acceptEdits, bypassPermissions]
* description: Permission mode
* max_steps:
* type: integer
* description: Maximum steps the agent can take
* description: Only include the fields you want to update
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const patchAgent = async (req: Request, res: Response): Promise<Response> => {
const { agentId } = req.params
try {
logger.info(`Partially updating agent: ${agentId}`)
logger.debug('Partial update data:', req.body)
const { validatedBody } = req as ValidationRequest
const updatePayload = (validatedBody ?? {}) as UpdateAgentRequest
const agent = await agentService.updateAgent(agentId, updatePayload)
if (!agent) {
logger.warn(`Agent not found for partial update: ${agentId}`)
return res.status(404).json({
error: {
message: 'Agent not found',
type: 'not_found',
code: 'agent_not_found'
}
})
}
logger.info(`Agent partially updated successfully: ${agentId}`)
return res.json(agent)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Agent model validation error during partial update:', {
agentId,
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error partially updating agent:', error)
return res.status(500).json({
error: {
message: `Failed to partially update agent: ${error.message}`,
type: 'internal_error',
code: 'agent_patch_failed'
}
})
}
}
/**
* @swagger
* /v1/agents/{agentId}:
* delete:
* summary: Delete agent
* description: Deletes an agent and all associated sessions and logs
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* responses:
* 204:
* description: Agent deleted successfully
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
export const deleteAgent = async (req: Request, res: Response): Promise<Response> => {
try {
const { agentId } = req.params
logger.info(`Deleting agent: ${agentId}`)
const deleted = await agentService.deleteAgent(agentId)
if (!deleted) {
logger.warn(`Agent not found for deletion: ${agentId}`)
return res.status(404).json({
error: {
message: 'Agent not found',
type: 'not_found',
code: 'agent_not_found'
}
})
}
logger.info(`Agent deleted successfully: ${agentId}`)
return res.status(204).send()
} catch (error: any) {
logger.error('Error deleting agent:', error)
return res.status(500).json({
error: {
message: 'Failed to delete agent',
type: 'internal_error',
code: 'agent_delete_failed'
}
})
}
}

View File

@@ -0,0 +1,3 @@
export * as agentHandlers from './agents'
export * as messageHandlers from './messages'
export * as sessionHandlers from './sessions'

View File

@@ -0,0 +1,230 @@
import { loggerService } from '@logger'
import { Request, Response } from 'express'
import { agentService, sessionMessageService, sessionService } from '../../../../services/agents'
const logger = loggerService.withContext('ApiServerMessagesHandlers')
// Helper function to verify agent and session exist and belong together
const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
const agentExists = await agentService.agentExists(agentId)
if (!agentExists) {
throw { status: 404, code: 'agent_not_found', message: 'Agent not found' }
}
const session = await sessionService.getSession(agentId, sessionId)
if (!session) {
throw { status: 404, code: 'session_not_found', message: 'Session not found' }
}
if (session.agent_id !== agentId) {
throw { status: 404, code: 'session_not_found', message: 'Session not found for this agent' }
}
return session
}
export const createMessage = async (req: Request, res: Response): Promise<void> => {
try {
const { agentId, sessionId } = req.params
const session = await verifyAgentAndSession(agentId, sessionId)
const messageData = req.body
logger.info(`Creating streaming message for session: ${sessionId}`)
logger.debug('Streaming message data:', messageData)
// Set SSE headers
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control')
const abortController = new AbortController()
const { stream, completion } = await sessionMessageService.createSessionMessage(
session,
messageData,
abortController
)
const reader = stream.getReader()
// Track stream lifecycle so we keep the SSE connection open until persistence finishes
let responseEnded = false
let streamFinished = false
const finalizeResponse = () => {
if (responseEnded) {
return
}
if (!streamFinished) {
return
}
responseEnded = true
try {
// res.write('data: {"type":"finish"}\n\n')
res.write('data: [DONE]\n\n')
} catch (writeError) {
logger.error('Error writing final sentinel to SSE stream:', { error: writeError as Error })
}
res.end()
}
/**
* Client Disconnect Detection for Server-Sent Events (SSE)
*
* We monitor multiple HTTP events to reliably detect when a client disconnects
* from the streaming response. This is crucial for:
* - Aborting long-running Claude Code processes
* - Cleaning up resources and preventing memory leaks
* - Avoiding orphaned processes
*
* Event Priority & Behavior:
* 1. res.on('close') - Most common for SSE client disconnects (browser tab close, curl Ctrl+C)
* 2. req.on('aborted') - Explicit request abortion
* 3. req.on('close') - Request object closure (less common with SSE)
*
* When any disconnect event fires, we:
* - Abort the Claude Code SDK process via abortController
* - Clean up event listeners to prevent memory leaks
* - Mark the response as ended to prevent further writes
*/
const handleDisconnect = () => {
if (responseEnded) return
logger.info(`Client disconnected from streaming message for session: ${sessionId}`)
responseEnded = true
abortController.abort('Client disconnected')
reader.cancel('Client disconnected').catch(() => {})
}
req.on('close', handleDisconnect)
req.on('aborted', handleDisconnect)
res.on('close', handleDisconnect)
const pumpStream = async () => {
try {
while (!responseEnded) {
const { done, value } = await reader.read()
if (done) {
break
}
res.write(`data: ${JSON.stringify(value)}\n\n`)
}
streamFinished = true
finalizeResponse()
} catch (error) {
if (responseEnded) return
logger.error('Error reading agent stream:', { error })
try {
res.write(
`data: ${JSON.stringify({
type: 'error',
error: {
message: (error as Error).message || 'Stream processing error',
type: 'stream_error',
code: 'stream_processing_failed'
}
})}\n\n`
)
} catch (writeError) {
logger.error('Error writing stream error to SSE:', { error: writeError })
}
responseEnded = true
res.end()
}
}
pumpStream().catch((error) => {
logger.error('Pump stream failure:', { error })
})
completion
.then(() => {
streamFinished = true
finalizeResponse()
})
.catch((error) => {
if (responseEnded) return
logger.error(`Streaming message error for session: ${sessionId}:`, error)
try {
res.write(
`data: ${JSON.stringify({
type: 'error',
error: {
message: (error as { message?: string })?.message || 'Stream processing error',
type: 'stream_error',
code: 'stream_processing_failed'
}
})}\n\n`
)
} catch (writeError) {
logger.error('Error writing completion error to SSE stream:', { error: writeError })
}
responseEnded = true
res.end()
})
// Set a timeout to prevent hanging indefinitely
const timeout = setTimeout(
() => {
if (!responseEnded) {
logger.error(`Streaming message timeout for session: ${sessionId}`)
try {
res.write(
`data: ${JSON.stringify({
type: 'error',
error: {
message: 'Stream timeout',
type: 'timeout_error',
code: 'stream_timeout'
}
})}\n\n`
)
} catch (writeError) {
logger.error('Error writing timeout to SSE stream:', { error: writeError })
}
abortController.abort('stream timeout')
reader.cancel('stream timeout').catch(() => {})
responseEnded = true
res.end()
}
},
10 * 60 * 1000
) // 10 minutes timeout
// Clear timeout when response ends
res.on('close', () => clearTimeout(timeout))
res.on('finish', () => clearTimeout(timeout))
} catch (error: any) {
logger.error('Error in streaming message handler:', error)
// Send error as SSE if possible
if (!res.headersSent) {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
}
try {
const errorResponse = {
type: 'error',
error: {
message: error.status ? error.message : 'Failed to create streaming message',
type: error.status ? 'not_found' : 'internal_error',
code: error.status ? error.code : 'stream_creation_failed'
}
}
res.write(`data: ${JSON.stringify(errorResponse)}\n\n`)
} catch (writeError) {
logger.error('Error writing initial error to SSE stream:', { error: writeError })
}
res.end()
}
}

View File

@@ -0,0 +1,370 @@
import { loggerService } from '@logger'
import {
AgentModelValidationError,
sessionMessageService,
sessionService
} from '@main/services/agents'
import {
CreateSessionResponse,
ListAgentSessionsResponse,
type ReplaceSessionRequest,
UpdateSessionResponse
} from '@types'
import { Request, Response } from 'express'
import type { ValidationRequest } from '../validators/zodValidator'
const logger = loggerService.withContext('ApiServerSessionsHandlers')
const modelValidationErrorBody = (error: AgentModelValidationError) => ({
error: {
message: `Invalid ${error.context.field}: ${error.detail.message}`,
type: 'invalid_request_error',
code: error.detail.code
}
})
export const createSession = async (req: Request, res: Response): Promise<Response> => {
const { agentId } = req.params
try {
const sessionData = req.body
logger.info(`Creating new session for agent: ${agentId}`)
logger.debug('Session data:', sessionData)
const session = (await sessionService.createSession(agentId, sessionData)) satisfies CreateSessionResponse
logger.info(`Session created successfully: ${session.id}`)
return res.status(201).json(session)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Session model validation error during create:', {
agentId,
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error creating session:', error)
return res.status(500).json({
error: {
message: `Failed to create session: ${error.message}`,
type: 'internal_error',
code: 'session_creation_failed'
}
})
}
}
export const listSessions = async (req: Request, res: Response): Promise<Response> => {
try {
const { agentId } = req.params
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
const status = req.query.status as any
logger.info(`Listing sessions for agent: ${agentId} with limit=${limit}, offset=${offset}, status=${status}`)
const result = await sessionService.listSessions(agentId, { limit, offset })
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total}) for agent: ${agentId}`)
return res.json({
data: result.sessions,
total: result.total,
limit,
offset
})
} catch (error: any) {
logger.error('Error listing sessions:', error)
return res.status(500).json({
error: {
message: 'Failed to list sessions',
type: 'internal_error',
code: 'session_list_failed'
}
})
}
}
export const getSession = async (req: Request, res: Response): Promise<Response> => {
try {
const { agentId, sessionId } = req.params
logger.info(`Getting session: ${sessionId} for agent: ${agentId}`)
const session = await sessionService.getSession(agentId, sessionId)
if (!session) {
logger.warn(`Session not found: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
// // Verify session belongs to the agent
// logger.warn(`Session ${sessionId} does not belong to agent ${agentId}`)
// return res.status(404).json({
// error: {
// message: 'Session not found for this agent',
// type: 'not_found',
// code: 'session_not_found'
// }
// })
// }
// Fetch session messages
logger.info(`Fetching messages for session: ${sessionId}`)
const { messages } = await sessionMessageService.listSessionMessages(sessionId)
// Add messages to session
const sessionWithMessages = {
...session,
messages: messages
}
logger.info(`Session retrieved successfully: ${sessionId} with ${messages.length} messages`)
return res.json(sessionWithMessages)
} catch (error: any) {
logger.error('Error getting session:', error)
return res.status(500).json({
error: {
message: 'Failed to get session',
type: 'internal_error',
code: 'session_get_failed'
}
})
}
}
export const updateSession = async (req: Request, res: Response): Promise<Response> => {
const { agentId, sessionId } = req.params
try {
logger.info(`Updating session: ${sessionId} for agent: ${agentId}`)
logger.debug('Update data:', req.body)
// First check if session exists and belongs to agent
const existingSession = await sessionService.getSession(agentId, sessionId)
if (!existingSession || existingSession.agent_id !== agentId) {
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
return res.status(404).json({
error: {
message: 'Session not found for this agent',
type: 'not_found',
code: 'session_not_found'
}
})
}
const { validatedBody } = req as ValidationRequest
const replacePayload = (validatedBody ?? {}) as ReplaceSessionRequest
const session = await sessionService.updateSession(agentId, sessionId, replacePayload)
if (!session) {
logger.warn(`Session not found for update: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
logger.info(`Session updated successfully: ${sessionId}`)
return res.json(session satisfies UpdateSessionResponse)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Session model validation error during update:', {
agentId,
sessionId,
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error updating session:', error)
return res.status(500).json({
error: {
message: `Failed to update session: ${error.message}`,
type: 'internal_error',
code: 'session_update_failed'
}
})
}
}
export const patchSession = async (req: Request, res: Response): Promise<Response> => {
const { agentId, sessionId } = req.params
try {
logger.info(`Patching session: ${sessionId} for agent: ${agentId}`)
logger.debug('Patch data:', req.body)
// First check if session exists and belongs to agent
const existingSession = await sessionService.getSession(agentId, sessionId)
if (!existingSession || existingSession.agent_id !== agentId) {
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
return res.status(404).json({
error: {
message: 'Session not found for this agent',
type: 'not_found',
code: 'session_not_found'
}
})
}
const updateSession = { ...existingSession, ...req.body }
const session = await sessionService.updateSession(agentId, sessionId, updateSession)
if (!session) {
logger.warn(`Session not found for patch: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
logger.info(`Session patched successfully: ${sessionId}`)
return res.json(session)
} catch (error: any) {
if (error instanceof AgentModelValidationError) {
logger.warn('Session model validation error during patch:', {
agentId,
sessionId,
agentType: error.context.agentType,
field: error.context.field,
model: error.context.model,
detail: error.detail
})
return res.status(400).json(modelValidationErrorBody(error))
}
logger.error('Error patching session:', error)
return res.status(500).json({
error: {
message: `Failed to patch session, ${error.message}`,
type: 'internal_error',
code: 'session_patch_failed'
}
})
}
}
export const deleteSession = async (req: Request, res: Response): Promise<Response> => {
try {
const { agentId, sessionId } = req.params
logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`)
// First check if session exists and belongs to agent
const existingSession = await sessionService.getSession(agentId, sessionId)
if (!existingSession || existingSession.agent_id !== agentId) {
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
return res.status(404).json({
error: {
message: 'Session not found for this agent',
type: 'not_found',
code: 'session_not_found'
}
})
}
const deleted = await sessionService.deleteSession(agentId, sessionId)
if (!deleted) {
logger.warn(`Session not found for deletion: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
logger.info(`Session deleted successfully: ${sessionId}`)
return res.status(204).send()
} catch (error: any) {
logger.error('Error deleting session:', error)
return res.status(500).json({
error: {
message: 'Failed to delete session',
type: 'internal_error',
code: 'session_delete_failed'
}
})
}
}
// Convenience endpoints for sessions without agent context
export const listAllSessions = async (req: Request, res: Response): Promise<Response> => {
try {
const limit = req.query.limit ? parseInt(req.query.limit as string) : 20
const offset = req.query.offset ? parseInt(req.query.offset as string) : 0
const status = req.query.status as any
logger.info(`Listing all sessions with limit=${limit}, offset=${offset}, status=${status}`)
const result = await sessionService.listSessions(undefined, { limit, offset })
logger.info(`Retrieved ${result.sessions.length} sessions (total: ${result.total})`)
return res.json({
data: result.sessions,
total: result.total,
limit,
offset
} satisfies ListAgentSessionsResponse)
} catch (error: any) {
logger.error('Error listing all sessions:', error)
return res.status(500).json({
error: {
message: 'Failed to list sessions',
type: 'internal_error',
code: 'session_list_failed'
}
})
}
}
export const getSessionById = async (req: Request, res: Response): Promise<Response> => {
try {
const { sessionId } = req.params
logger.info(`Getting session: ${sessionId}`)
const session = await sessionService.getSessionById(sessionId)
if (!session) {
logger.warn(`Session not found: ${sessionId}`)
return res.status(404).json({
error: {
message: 'Session not found',
type: 'not_found',
code: 'session_not_found'
}
})
}
logger.info(`Session retrieved successfully: ${sessionId}`)
return res.json(session)
} catch (error: any) {
logger.error('Error getting session:', error)
return res.status(500).json({
error: {
message: 'Failed to get session',
type: 'internal_error',
code: 'session_get_failed'
}
})
}
}

View File

@@ -0,0 +1,927 @@
import express from 'express'
import { agentHandlers, messageHandlers, sessionHandlers } from './handlers'
import { checkAgentExists, handleValidationErrors } from './middleware'
import {
validateAgent,
validateAgentId,
validateAgentReplace,
validateAgentUpdate,
validatePagination,
validateSession,
validateSessionId,
validateSessionMessage,
validateSessionReplace,
validateSessionUpdate
} from './validators'
// Create main agents router
const agentsRouter = express.Router()
/**
* @swagger
* components:
* schemas:
* PermissionMode:
* type: string
* enum: [default, acceptEdits, bypassPermissions, plan]
* description: Permission mode for agent operations
*
* AgentType:
* type: string
* enum: [claude-code]
* description: Type of agent
*
* AgentConfiguration:
* type: object
* properties:
* permission_mode:
* $ref: '#/components/schemas/PermissionMode'
* default: default
* max_turns:
* type: integer
* default: 10
* description: Maximum number of interaction turns
* additionalProperties: true
*
* AgentBase:
* type: object
* properties:
* name:
* type: string
* description: Agent name
* description:
* type: string
* description: Agent description
* accessible_paths:
* type: array
* items:
* type: string
* description: Array of directory paths the agent can access
* instructions:
* type: string
* description: System prompt/instructions
* model:
* type: string
* description: Main model ID
* plan_model:
* type: string
* description: Optional planning model ID
* small_model:
* type: string
* description: Optional small/fast model ID
* mcps:
* type: array
* items:
* type: string
* description: Array of MCP tool IDs
* allowed_tools:
* type: array
* items:
* type: string
* description: Array of allowed tool IDs (whitelist)
* configuration:
* $ref: '#/components/schemas/AgentConfiguration'
* required:
* - model
* - accessible_paths
*
* AgentEntity:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* id:
* type: string
* description: Unique agent identifier
* type:
* $ref: '#/components/schemas/AgentType'
* created_at:
* type: string
* format: date-time
* description: ISO timestamp of creation
* updated_at:
* type: string
* format: date-time
* description: ISO timestamp of last update
* required:
* - id
* - type
* - created_at
* - updated_at
* CreateAgentRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* type:
* $ref: '#/components/schemas/AgentType'
* name:
* type: string
* minLength: 1
* description: Agent name (required)
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - type
* - name
* - model
*
* UpdateAgentRequest:
* type: object
* properties:
* name:
* type: string
* description: Agent name
* description:
* type: string
* description: Agent description
* accessible_paths:
* type: array
* items:
* type: string
* description: Array of directory paths the agent can access
* instructions:
* type: string
* description: System prompt/instructions
* model:
* type: string
* description: Main model ID
* plan_model:
* type: string
* description: Optional planning model ID
* small_model:
* type: string
* description: Optional small/fast model ID
* mcps:
* type: array
* items:
* type: string
* description: Array of MCP tool IDs
* allowed_tools:
* type: array
* items:
* type: string
* description: Array of allowed tool IDs (whitelist)
* configuration:
* $ref: '#/components/schemas/AgentConfiguration'
* description: Partial update - all fields are optional
*
* ReplaceAgentRequest:
* $ref: '#/components/schemas/AgentBase'
*
* SessionEntity:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* id:
* type: string
* description: Unique session identifier
* agent_id:
* type: string
* description: Primary agent ID for the session
* agent_type:
* $ref: '#/components/schemas/AgentType'
* created_at:
* type: string
* format: date-time
* description: ISO timestamp of creation
* updated_at:
* type: string
* format: date-time
* description: ISO timestamp of last update
* required:
* - id
* - agent_id
* - agent_type
* - created_at
* - updated_at
*
* CreateSessionRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - model
*
* UpdateSessionRequest:
* type: object
* properties:
* name:
* type: string
* description: Session name
* description:
* type: string
* description: Session description
* accessible_paths:
* type: array
* items:
* type: string
* description: Array of directory paths the agent can access
* instructions:
* type: string
* description: System prompt/instructions
* model:
* type: string
* description: Main model ID
* plan_model:
* type: string
* description: Optional planning model ID
* small_model:
* type: string
* description: Optional small/fast model ID
* mcps:
* type: array
* items:
* type: string
* description: Array of MCP tool IDs
* allowed_tools:
* type: array
* items:
* type: string
* description: Array of allowed tool IDs (whitelist)
* configuration:
* $ref: '#/components/schemas/AgentConfiguration'
* description: Partial update - all fields are optional
*
* ReplaceSessionRequest:
* allOf:
* - $ref: '#/components/schemas/AgentBase'
* - type: object
* properties:
* model:
* type: string
* minLength: 1
* description: Main model ID (required)
* required:
* - model
*
* CreateSessionMessageRequest:
* type: object
* properties:
* content:
* type: string
* minLength: 1
* description: Message content
* required:
* - content
*
* PaginationQuery:
* type: object
* properties:
* limit:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: Number of items to return
* offset:
* type: integer
* minimum: 0
* default: 0
* description: Number of items to skip
* status:
* type: string
* enum: [idle, running, completed, failed, stopped]
* description: Filter by session status
*
* ListAgentsResponse:
* type: object
* properties:
* agents:
* type: array
* items:
* $ref: '#/components/schemas/AgentEntity'
* total:
* type: integer
* description: Total number of agents
* limit:
* type: integer
* description: Number of items returned
* offset:
* type: integer
* description: Number of items skipped
* required:
* - agents
* - total
* - limit
* - offset
*
* ListSessionsResponse:
* type: object
* properties:
* sessions:
* type: array
* items:
* $ref: '#/components/schemas/SessionEntity'
* total:
* type: integer
* description: Total number of sessions
* limit:
* type: integer
* description: Number of items returned
* offset:
* type: integer
* description: Number of items skipped
* required:
* - sessions
* - total
* - limit
* - offset
*
* ErrorResponse:
* type: object
* properties:
* error:
* type: object
* properties:
* message:
* type: string
* description: Error message
* type:
* type: string
* description: Error type
* code:
* type: string
* description: Error code
* required:
* - message
* - type
* - code
* required:
* - error
*/
/**
* @swagger
* /api/agents:
* post:
* summary: Create a new agent
* tags: [Agents]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateAgentRequest'
* responses:
* 201:
* description: Agent created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
// Agent CRUD routes
agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent)
/**
* @swagger
* /api/agents:
* get:
* summary: List all agents with pagination
* tags: [Agents]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: Number of agents to return
* - in: query
* name: offset
* schema:
* type: integer
* minimum: 0
* default: 0
* description: Number of agents to skip
* - in: query
* name: status
* schema:
* type: string
* enum: [idle, running, completed, failed, stopped]
* description: Filter by agent status
* responses:
* 200:
* description: List of agents
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ListAgentsResponse'
*/
agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents)
/**
* @swagger
* /api/agents/{agentId}:
* get:
* summary: Get agent by ID
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* responses:
* 200:
* description: Agent details
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
/**
* @swagger
* /api/agents/{agentId}:
* put:
* summary: Replace agent (full update)
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ReplaceAgentRequest'
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.put('/:agentId', validateAgentId, validateAgentReplace, handleValidationErrors, agentHandlers.updateAgent)
/**
* @swagger
* /api/agents/{agentId}:
* patch:
* summary: Update agent (partial update)
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateAgentRequest'
* responses:
* 200:
* description: Agent updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AgentEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
/**
* @swagger
* /api/agents/{agentId}:
* delete:
* summary: Delete agent
* tags: [Agents]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* responses:
* 204:
* description: Agent deleted successfully
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent)
// Create sessions router with agent context
const createSessionsRouter = (): express.Router => {
const sessionsRouter = express.Router({ mergeParams: true })
// Session CRUD routes (nested under agent)
/**
* @swagger
* /api/agents/{agentId}/sessions:
* post:
* summary: Create a new session for an agent
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateSessionRequest'
* responses:
* 201:
* description: Session created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.post('/', validateSession, handleValidationErrors, sessionHandlers.createSession)
/**
* @swagger
* /api/agents/{agentId}/sessions:
* get:
* summary: List sessions for an agent
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* maximum: 100
* default: 20
* description: Number of sessions to return
* - in: query
* name: offset
* schema:
* type: integer
* minimum: 0
* default: 0
* description: Number of sessions to skip
* - in: query
* name: status
* schema:
* type: string
* enum: [idle, running, completed, failed, stopped]
* description: Filter by session status
* responses:
* 200:
* description: List of sessions
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ListSessionsResponse'
* 404:
* description: Agent not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.get('/', validatePagination, handleValidationErrors, sessionHandlers.listSessions)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* get:
* summary: Get session by ID
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* responses:
* 200:
* description: Session details
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.get('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.getSession)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* put:
* summary: Replace session (full update)
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ReplaceSessionRequest'
* responses:
* 200:
* description: Session updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.put(
'/:sessionId',
validateSessionId,
validateSessionReplace,
handleValidationErrors,
sessionHandlers.updateSession
)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* patch:
* summary: Update session (partial update)
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/UpdateSessionRequest'
* responses:
* 200:
* description: Session updated successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/SessionEntity'
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.patch(
'/:sessionId',
validateSessionId,
validateSessionUpdate,
handleValidationErrors,
sessionHandlers.patchSession
)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}:
* delete:
* summary: Delete session
* tags: [Sessions]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* responses:
* 204:
* description: Session deleted successfully
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
sessionsRouter.delete('/:sessionId', validateSessionId, handleValidationErrors, sessionHandlers.deleteSession)
return sessionsRouter
}
// Create messages router with agent and session context
const createMessagesRouter = (): express.Router => {
const messagesRouter = express.Router({ mergeParams: true })
// Message CRUD routes (nested under agent/session)
/**
* @swagger
* /api/agents/{agentId}/sessions/{sessionId}/messages:
* post:
* summary: Create a new message in a session
* tags: [Messages]
* parameters:
* - in: path
* name: agentId
* required: true
* schema:
* type: string
* description: Agent ID
* - in: path
* name: sessionId
* required: true
* schema:
* type: string
* description: Session ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateSessionMessageRequest'
* responses:
* 201:
* description: Message created successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: number
* description: Message ID
* session_id:
* type: string
* description: Session ID
* role:
* type: string
* enum: [assistant, user, system, tool]
* description: Message role
* content:
* type: object
* description: Message content (AI SDK format)
* agent_session_id:
* type: string
* description: Agent session ID for resuming
* metadata:
* type: object
* description: Additional metadata
* created_at:
* type: string
* format: date-time
* updated_at:
* type: string
* format: date-time
* 400:
* description: Invalid request body
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Agent or session not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/
messagesRouter.post('/', validateSessionMessage, handleValidationErrors, messageHandlers.createMessage)
return messagesRouter
}
// Mount nested resources with clear hierarchy
const sessionsRouter = createSessionsRouter()
const messagesRouter = createMessagesRouter()
// Mount sessions under specific agent
agentsRouter.use('/:agentId/sessions', validateAgentId, checkAgentExists, handleValidationErrors, sessionsRouter)
// Mount messages under specific agent/session
agentsRouter.use(
'/:agentId/sessions/:sessionId/messages',
validateAgentId,
validateSessionId,
handleValidationErrors,
messagesRouter
)
// Export main router and convenience router
export const agentsRoutes = agentsRouter

View File

@@ -0,0 +1,41 @@
import { Request, Response } from 'express'
import { agentService } from '../../../../services/agents'
import { loggerService } from '../../../../services/LoggerService'
const logger = loggerService.withContext('ApiServerMiddleware')
// Since Zod validators handle their own errors, this is now a pass-through
export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => {
next()
}
// Middleware to check if agent exists
export const checkAgentExists = async (req: Request, res: Response, next: any): Promise<void> => {
try {
const { agentId } = req.params
const exists = await agentService.agentExists(agentId)
if (!exists) {
res.status(404).json({
error: {
message: 'Agent not found',
type: 'not_found',
code: 'agent_not_found'
}
})
return
}
next()
} catch (error) {
logger.error('Error checking agent existence:', error as Error)
res.status(500).json({
error: {
message: 'Failed to validate agent',
type: 'internal_error',
code: 'agent_validation_failed'
}
})
}
}

View File

@@ -0,0 +1 @@
export * from './common'

View File

@@ -0,0 +1,24 @@
import {
AgentIdParamSchema,
CreateAgentRequestSchema,
ReplaceAgentRequestSchema,
UpdateAgentRequestSchema
} from '@types'
import { createZodValidator } from './zodValidator'
export const validateAgent = createZodValidator({
body: CreateAgentRequestSchema
})
export const validateAgentReplace = createZodValidator({
body: ReplaceAgentRequestSchema
})
export const validateAgentUpdate = createZodValidator({
body: UpdateAgentRequestSchema
})
export const validateAgentId = createZodValidator({
params: AgentIdParamSchema
})

View File

@@ -0,0 +1,7 @@
import { PaginationQuerySchema } from '@types'
import { createZodValidator } from './zodValidator'
export const validatePagination = createZodValidator({
query: PaginationQuerySchema
})

View File

@@ -0,0 +1,4 @@
export * from './agents'
export * from './common'
export * from './messages'
export * from './sessions'

View File

@@ -0,0 +1,7 @@
import { CreateSessionMessageRequestSchema } from '@types'
import { createZodValidator } from './zodValidator'
export const validateSessionMessage = createZodValidator({
body: CreateSessionMessageRequestSchema
})

View File

@@ -0,0 +1,24 @@
import {
CreateSessionRequestSchema,
ReplaceSessionRequestSchema,
SessionIdParamSchema,
UpdateSessionRequestSchema
} from '@types'
import { createZodValidator } from './zodValidator'
export const validateSession = createZodValidator({
body: CreateSessionRequestSchema
})
export const validateSessionReplace = createZodValidator({
body: ReplaceSessionRequestSchema
})
export const validateSessionUpdate = createZodValidator({
body: UpdateSessionRequestSchema
})
export const validateSessionId = createZodValidator({
params: SessionIdParamSchema
})

View File

@@ -0,0 +1,68 @@
import { NextFunction,Request, Response } from 'express'
import { ZodError, ZodType } from 'zod'
export interface ValidationRequest extends Request {
validatedBody?: any
validatedParams?: any
validatedQuery?: any
}
export interface ZodValidationConfig {
body?: ZodType
params?: ZodType
query?: ZodType
}
export const createZodValidator = (config: ZodValidationConfig) => {
return (req: ValidationRequest, res: Response, next: NextFunction): void => {
try {
if (config.body && req.body) {
req.validatedBody = config.body.parse(req.body)
}
if (config.params && req.params) {
req.validatedParams = config.params.parse(req.params)
}
if (config.query && req.query) {
req.validatedQuery = config.query.parse(req.query)
}
next()
} catch (error) {
if (error instanceof ZodError) {
const validationErrors = error.issues.map((err) => ({
type: 'field',
value: err.input,
msg: err.message,
path: err.path.map(p => String(p)).join('.'),
location: getLocationFromPath(err.path, config)
}))
res.status(400).json({
error: {
message: 'Validation failed',
type: 'validation_error',
details: validationErrors
}
})
return
}
res.status(500).json({
error: {
message: 'Internal validation error',
type: 'internal_error',
code: 'validation_processing_failed'
}
})
}
}
}
function getLocationFromPath(path: (string | number | symbol)[], config: ZodValidationConfig): string {
if (config.body && path.length > 0) return 'body'
if (config.params && path.length > 0) return 'params'
if (config.query && path.length > 0) return 'query'
return 'unknown'
}

View File

@@ -0,0 +1,244 @@
import express, { Request, Response } from 'express'
import { ChatCompletionCreateParams } from 'openai/resources'
import { loggerService } from '../../services/LoggerService'
import {
ChatCompletionModelError,
chatCompletionService,
ChatCompletionValidationError
} from '../services/chat-completion'
const logger = loggerService.withContext('ApiServerChatRoutes')
const router = express.Router()
interface ErrorResponseBody {
error: {
message: string
type: string
code: string
}
}
const mapChatCompletionError = (error: unknown): { status: number; body: ErrorResponseBody } => {
if (error instanceof ChatCompletionValidationError) {
logger.warn('Chat completion validation error:', {
errors: error.errors
})
return {
status: 400,
body: {
error: {
message: error.errors.join('; '),
type: 'invalid_request_error',
code: 'validation_failed'
}
}
}
}
if (error instanceof ChatCompletionModelError) {
logger.warn('Chat completion model error:', error.error)
return {
status: 400,
body: {
error: {
message: error.error.message,
type: 'invalid_request_error',
code: error.error.code
}
}
}
}
if (error instanceof Error) {
let statusCode = 500
let errorType = 'server_error'
let errorCode = 'internal_error'
if (error.message.includes('API key') || error.message.includes('authentication')) {
statusCode = 401
errorType = 'authentication_error'
errorCode = 'invalid_api_key'
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
statusCode = 429
errorType = 'rate_limit_error'
errorCode = 'rate_limit_exceeded'
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
statusCode = 502
errorType = 'server_error'
errorCode = 'upstream_error'
}
logger.error('Chat completion error:', { error })
return {
status: statusCode,
body: {
error: {
message: error.message || 'Internal server error',
type: errorType,
code: errorCode
}
}
}
}
logger.error('Chat completion unknown error:', { error })
return {
status: 500,
body: {
error: {
message: 'Internal server error',
type: 'server_error',
code: 'internal_error'
}
}
}
}
/**
* @swagger
* /v1/chat/completions:
* post:
* summary: Create chat completion
* description: Create a chat completion response, compatible with OpenAI API
* tags: [Chat]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ChatCompletionRequest'
* responses:
* 200:
* description: Chat completion response
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: string
* object:
* type: string
* example: chat.completion
* created:
* type: integer
* model:
* type: string
* choices:
* type: array
* items:
* type: object
* properties:
* index:
* type: integer
* message:
* $ref: '#/components/schemas/ChatMessage'
* finish_reason:
* type: string
* usage:
* type: object
* properties:
* prompt_tokens:
* type: integer
* completion_tokens:
* type: integer
* total_tokens:
* type: integer
* text/event-stream:
* schema:
* type: string
* description: Server-sent events stream (when stream=true)
* 400:
* description: Bad request
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 429:
* description: Rate limit exceeded
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.post('/completions', async (req: Request, res: Response) => {
try {
const request: ChatCompletionCreateParams = req.body
if (!request) {
return res.status(400).json({
error: {
message: 'Request body is required',
type: 'invalid_request_error',
code: 'missing_body'
}
})
}
logger.info('Chat completion request:', {
model: request.model,
messageCount: request.messages?.length || 0,
stream: request.stream,
temperature: request.temperature
})
const isStreaming = !!request.stream
if (isStreaming) {
const { stream } = await chatCompletionService.processStreamingCompletion(request)
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache, no-transform')
res.setHeader('Connection', 'keep-alive')
res.setHeader('X-Accel-Buffering', 'no')
res.flushHeaders()
try {
for await (const chunk of stream) {
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
}
res.write('data: [DONE]\n\n')
} catch (streamError: any) {
logger.error('Stream error:', streamError)
res.write(
`data: ${JSON.stringify({
error: {
message: 'Stream processing error',
type: 'server_error',
code: 'stream_error'
}
})}\n\n`
)
} finally {
res.end()
}
return
}
const { response } = await chatCompletionService.processCompletion(request)
return res.json(response)
} catch (error: unknown) {
const { status, body } = mapChatCompletionError(error)
return res.status(status).json(body)
}
})
export { router as chatRoutes }

View File

@@ -0,0 +1,153 @@
import express, { Request, Response } from 'express'
import { loggerService } from '../../services/LoggerService'
import { mcpApiService } from '../services/mcp'
const logger = loggerService.withContext('ApiServerMCPRoutes')
const router = express.Router()
/**
* @swagger
* /v1/mcps:
* get:
* summary: List MCP servers
* description: Get a list of all configured Model Context Protocol servers
* tags: [MCP]
* responses:
* 200:
* description: List of MCP servers
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/MCPServer'
* 503:
* description: Service unavailable
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* error:
* $ref: '#/components/schemas/Error'
*/
router.get('/', async (req: Request, res: Response) => {
try {
logger.info('Get all MCP servers request received')
const servers = await mcpApiService.getAllServers(req)
return res.json({
success: true,
data: servers
})
} catch (error: any) {
logger.error('Error fetching MCP servers:', error)
return res.status(503).json({
success: false,
error: {
message: `Failed to retrieve MCP servers: ${error.message}`,
type: 'service_unavailable',
code: 'servers_unavailable'
}
})
}
})
/**
* @swagger
* /v1/mcps/{server_id}:
* get:
* summary: Get MCP server info
* description: Get detailed information about a specific MCP server
* tags: [MCP]
* parameters:
* - in: path
* name: server_id
* required: true
* schema:
* type: string
* description: MCP server ID
* responses:
* 200:
* description: MCP server information
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/MCPServer'
* 404:
* description: MCP server not found
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* error:
* $ref: '#/components/schemas/Error'
*/
router.get('/:server_id', async (req: Request, res: Response) => {
try {
logger.info('Get MCP server info request received')
const server = await mcpApiService.getServerInfo(req.params.server_id)
if (!server) {
logger.warn('MCP server not found')
return res.status(404).json({
success: false,
error: {
message: 'MCP server not found',
type: 'not_found',
code: 'server_not_found'
}
})
}
return res.json({
success: true,
data: server
})
} catch (error: any) {
logger.error('Error fetching MCP server info:', error)
return res.status(503).json({
success: false,
error: {
message: `Failed to retrieve MCP server info: ${error.message}`,
type: 'service_unavailable',
code: 'server_info_unavailable'
}
})
}
})
// Connect to MCP server
router.all('/:server_id/mcp', async (req: Request, res: Response) => {
const server = await mcpApiService.getServerById(req.params.server_id)
if (!server) {
logger.warn('MCP server not found')
return res.status(404).json({
success: false,
error: {
message: 'MCP server not found',
type: 'not_found',
code: 'server_not_found'
}
})
}
return await mcpApiService.handleRequest(req, res, server)
})
export { router as mcpRoutes }

View File

@@ -0,0 +1,290 @@
import { MessageCreateParams } from '@anthropic-ai/sdk/resources'
import express, { Request, Response } from 'express'
import { loggerService } from '../../services/LoggerService'
import { messagesService } from '../services/messages'
import { validateModelId } from '../utils'
const logger = loggerService.withContext('ApiServerMessagesRoutes')
const router = express.Router()
/**
* @swagger
* /v1/messages:
* post:
* summary: Create message
* description: Create a message response using Anthropic's API format
* tags: [Messages]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - model
* - max_tokens
* - messages
* properties:
* model:
* type: string
* description: Model ID in format "provider:model_id"
* example: "my-anthropic:claude-3-5-sonnet-20241022"
* max_tokens:
* type: integer
* minimum: 1
* description: Maximum number of tokens to generate
* example: 1024
* messages:
* type: array
* items:
* type: object
* properties:
* role:
* type: string
* enum: [user, assistant]
* content:
* oneOf:
* - type: string
* - type: array
* system:
* type: string
* description: System message
* temperature:
* type: number
* minimum: 0
* maximum: 1
* description: Sampling temperature
* top_p:
* type: number
* minimum: 0
* maximum: 1
* description: Nucleus sampling
* top_k:
* type: integer
* minimum: 0
* description: Top-k sampling
* stream:
* type: boolean
* description: Whether to stream the response
* tools:
* type: array
* description: Available tools for the model
* responses:
* 200:
* description: Message response
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: string
* type:
* type: string
* example: message
* role:
* type: string
* example: assistant
* content:
* type: array
* items:
* type: object
* model:
* type: string
* stop_reason:
* type: string
* stop_sequence:
* type: string
* usage:
* type: object
* properties:
* input_tokens:
* type: integer
* output_tokens:
* type: integer
* text/event-stream:
* schema:
* type: string
* description: Server-sent events stream (when stream=true)
* 400:
* description: Bad request
* content:
* application/json:
* schema:
* type: object
* properties:
* type:
* type: string
* example: error
* error:
* type: object
* properties:
* type:
* type: string
* message:
* type: string
* 401:
* description: Unauthorized
* 429:
* description: Rate limit exceeded
* 500:
* description: Internal server error
*/
router.post('/', async (req: Request, res: Response) => {
try {
const request: MessageCreateParams = req.body
if (!request) {
return res.status(400).json({
type: 'error',
error: {
type: 'invalid_request_error',
message: 'Request body is required'
}
})
}
logger.info('Anthropic message request:', {
model: request.model,
messageCount: request.messages?.length || 0,
stream: request.stream,
max_tokens: request.max_tokens,
temperature: request.temperature
})
// Validate model ID and get provider
const modelValidation = await validateModelId(request.model)
if (!modelValidation.valid) {
const error = modelValidation.error!
logger.warn(`Model validation failed for '${request.model}':`, error)
return res.status(400).json({
type: 'error',
error: {
type: 'invalid_request_error',
message: error.message
}
})
}
const provider = modelValidation.provider!
// Ensure provider is Anthropic type
if (provider.type !== 'anthropic') {
return res.status(400).json({
type: 'error',
error: {
type: 'invalid_request_error',
message: `Invalid provider type '${provider.type}' for messages endpoint. Expected 'anthropic' provider.`
}
})
}
const modelId = modelValidation.modelId!
request.model = modelId
logger.info('Model validation successful:', {
provider: provider.id,
providerType: provider.type,
modelId: modelId,
fullModelId: request.model
})
// Validate request
const validation = messagesService.validateRequest(request)
if (!validation.isValid) {
return res.status(400).json({
type: 'error',
error: {
type: 'invalid_request_error',
message: validation.errors.join('; ')
}
})
}
// Handle streaming
if (request.stream) {
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache, no-transform')
res.setHeader('Connection', 'keep-alive')
res.setHeader('X-Accel-Buffering', 'no')
res.flushHeaders()
try {
for await (const chunk of messagesService.processStreamingMessage(request, provider)) {
res.write(`data: ${JSON.stringify(chunk)}\n\n`)
}
res.write('data: [DONE]\n\n')
} catch (streamError: any) {
logger.error('Stream error:', streamError)
res.write(
`data: ${JSON.stringify({
type: 'error',
error: {
type: 'api_error',
message: 'Stream processing error'
}
})}\n\n`
)
} finally {
res.end()
}
return
}
// Handle non-streaming
const response = await messagesService.processMessage(request, provider)
return res.json(response)
} catch (error: any) {
logger.error('Anthropic message error:', error)
let statusCode = 500
let errorType = 'api_error'
let errorMessage = 'Internal server error'
const anthropicStatus = typeof error?.status === 'number' ? error.status : undefined
const anthropicError = error?.error
if (anthropicStatus) {
statusCode = anthropicStatus
}
if (anthropicError?.type) {
errorType = anthropicError.type
}
if (anthropicError?.message) {
errorMessage = anthropicError.message
} else if (error instanceof Error && error.message) {
errorMessage = error.message
}
if (!anthropicStatus && error instanceof Error) {
if (error.message.includes('API key') || error.message.includes('authentication')) {
statusCode = 401
errorType = 'authentication_error'
} else if (error.message.includes('rate limit') || error.message.includes('quota')) {
statusCode = 429
errorType = 'rate_limit_error'
} else if (error.message.includes('timeout') || error.message.includes('connection')) {
statusCode = 502
errorType = 'api_error'
} else if (error.message.includes('validation') || error.message.includes('invalid')) {
statusCode = 400
errorType = 'invalid_request_error'
}
}
return res.status(statusCode).json({
type: 'error',
error: {
type: errorType,
message: errorMessage,
requestId: error?.request_id
}
})
}
})
export { router as messagesRoutes }

View File

@@ -0,0 +1,130 @@
import { ApiModelsFilterSchema, ApiModelsResponse } from '@types'
import express, { Request, Response } from 'express'
import { loggerService } from '../../services/LoggerService'
import { modelsService } from '../services/models'
const logger = loggerService.withContext('ApiServerModelsRoutes')
const router = express
.Router()
/**
* @swagger
* /v1/models:
* get:
* summary: List available models
* description: Returns a list of available AI models from all configured providers with optional filtering
* tags: [Models]
* parameters:
* - in: query
* name: providerType
* schema:
* type: string
* enum: [openai, openai-response, anthropic, gemini]
* description: Filter models by provider type
* - in: query
* name: offset
* schema:
* type: integer
* minimum: 0
* default: 0
* description: Pagination offset
* - in: query
* name: limit
* schema:
* type: integer
* minimum: 1
* description: Maximum number of models to return
* responses:
* 200:
* description: List of available models
* content:
* application/json:
* schema:
* type: object
* properties:
* object:
* type: string
* example: list
* data:
* type: array
* items:
* $ref: '#/components/schemas/Model'
* total:
* type: integer
* description: Total number of models (when using pagination)
* offset:
* type: integer
* description: Current offset (when using pagination)
* limit:
* type: integer
* description: Current limit (when using pagination)
* 400:
* description: Invalid query parameters
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 503:
* description: Service unavailable
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
.get('/', async (req: Request, res: Response) => {
try {
logger.info('Models list request received', { query: req.query })
// Validate query parameters using Zod schema
const filterResult = ApiModelsFilterSchema.safeParse(req.query)
if (!filterResult.success) {
logger.warn('Invalid query parameters:', filterResult.error.issues)
return res.status(400).json({
error: {
message: 'Invalid query parameters',
type: 'invalid_request_error',
code: 'invalid_parameters',
details: filterResult.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message
}))
}
})
}
const filter = filterResult.data
const response = await modelsService.getModels(filter)
if (response.data.length === 0) {
logger.warn(
'No models available from providers. This may be because no OpenAI/Anthropic providers are configured or enabled.',
{ filter }
)
}
logger.info(`Returning ${response.data.length} models`, {
filter,
total: response.total
})
logger.debug(
'Model IDs:',
response.data.map((m) => m.id)
)
return res.json(response satisfies ApiModelsResponse)
} catch (error: any) {
logger.error('Error fetching models:', error)
return res.status(503).json({
error: {
message: 'Failed to retrieve models from available providers',
type: 'service_unavailable',
code: 'models_unavailable'
}
})
}
})
export { router as modelsRoutes }

View File

@@ -0,0 +1,71 @@
import { createServer } from 'node:http'
import { agentService } from '../services/agents'
import { loggerService } from '../services/LoggerService'
import { app } from './app'
import { config } from './config'
const logger = loggerService.withContext('ApiServer')
export class ApiServer {
private server: ReturnType<typeof createServer> | null = null
async start(): Promise<void> {
if (this.server) {
logger.warn('Server already running')
return
}
// Load config
const { port, host, apiKey } = await config.load()
// Initialize AgentService
logger.info('Initializing AgentService...')
await agentService.initialize()
logger.info('AgentService initialized successfully')
// Create server with Express app
this.server = createServer(app)
// Start server
return new Promise((resolve, reject) => {
this.server!.listen(port, host, () => {
logger.info(`API Server started at http://${host}:${port}`)
logger.info(`API Key: ${apiKey}`)
resolve()
})
this.server!.on('error', reject)
})
}
async stop(): Promise<void> {
if (!this.server) return
return new Promise((resolve) => {
this.server!.close(() => {
logger.info('API Server stopped')
this.server = null
resolve()
})
})
}
async restart(): Promise<void> {
await this.stop()
await config.reload()
await this.start()
}
isRunning(): boolean {
const hasServer = this.server !== null
const isListening = this.server?.listening || false
const result = hasServer && isListening
logger.debug('isRunning check:', { hasServer, isListening, result })
return result
}
}
export const apiServer = new ApiServer()

View File

@@ -0,0 +1,248 @@
import { Provider } from '@types'
import OpenAI from 'openai'
import { ChatCompletionCreateParams, ChatCompletionCreateParamsStreaming } from 'openai/resources'
import { loggerService } from '../../services/LoggerService'
import { ModelValidationError, validateModelId } from '../utils'
const logger = loggerService.withContext('ChatCompletionService')
export interface ValidationResult {
isValid: boolean
errors: string[]
}
export class ChatCompletionValidationError extends Error {
constructor(public readonly errors: string[]) {
super(`Request validation failed: ${errors.join('; ')}`)
this.name = 'ChatCompletionValidationError'
}
}
export class ChatCompletionModelError extends Error {
constructor(public readonly error: ModelValidationError) {
super(`Model validation failed: ${error.message}`)
this.name = 'ChatCompletionModelError'
}
}
export type PrepareRequestResult =
| { status: 'validation_error'; errors: string[] }
| { status: 'model_error'; error: ModelValidationError }
| {
status: 'ok'
provider: Provider
modelId: string
client: OpenAI
providerRequest: ChatCompletionCreateParams
}
export class ChatCompletionService {
async resolveProviderContext(model: string): Promise<
| { ok: false; error: ModelValidationError }
| { ok: true; provider: Provider; modelId: string; client: OpenAI }
> {
const modelValidation = await validateModelId(model)
if (!modelValidation.valid) {
return {
ok: false,
error: modelValidation.error!
}
}
const provider = modelValidation.provider!
if (provider.type !== 'openai') {
return {
ok: false,
error: {
type: 'unsupported_provider_type',
message: `Provider '${provider.id}' of type '${provider.type}' is not supported for OpenAI chat completions`,
code: 'unsupported_provider_type'
}
}
}
const modelId = modelValidation.modelId!
const client = new OpenAI({
baseURL: provider.apiHost,
apiKey: provider.apiKey
})
return {
ok: true,
provider,
modelId,
client
}
}
async prepareRequest(request: ChatCompletionCreateParams, stream: boolean): Promise<PrepareRequestResult> {
const requestValidation = this.validateRequest(request)
if (!requestValidation.isValid) {
return {
status: 'validation_error',
errors: requestValidation.errors
}
}
const providerContext = await this.resolveProviderContext(request.model!)
if (!providerContext.ok) {
return {
status: 'model_error',
error: providerContext.error
}
}
const { provider, modelId, client } = providerContext
logger.info('Model validation successful:', {
provider: provider.id,
providerType: provider.type,
modelId,
fullModelId: request.model
})
return {
status: 'ok',
provider,
modelId,
client,
providerRequest: stream
? {
...request,
model: modelId,
stream: true as const
}
: {
...request,
model: modelId,
stream: false as const
}
}
}
validateRequest(request: ChatCompletionCreateParams): ValidationResult {
const errors: string[] = []
// Validate messages
if (!request.messages) {
errors.push('Messages array is required')
} else if (!Array.isArray(request.messages)) {
errors.push('Messages must be an array')
} else if (request.messages.length === 0) {
errors.push('Messages array cannot be empty')
} else {
// Validate each message
request.messages.forEach((message, index) => {
if (!message.role) {
errors.push(`Message ${index}: role is required`)
}
if (!message.content) {
errors.push(`Message ${index}: content is required`)
}
})
}
// Validate optional parameters
return {
isValid: errors.length === 0,
errors
}
}
async processCompletion(request: ChatCompletionCreateParams): Promise<{
provider: Provider
modelId: string
response: OpenAI.Chat.Completions.ChatCompletion
}> {
try {
logger.info('Processing chat completion request:', {
model: request.model,
messageCount: request.messages.length,
stream: request.stream
})
const preparation = await this.prepareRequest(request, false)
if (preparation.status === 'validation_error') {
throw new ChatCompletionValidationError(preparation.errors)
}
if (preparation.status === 'model_error') {
throw new ChatCompletionModelError(preparation.error)
}
const { provider, modelId, client, providerRequest } = preparation
logger.debug('Sending request to provider:', {
provider: provider.id,
model: modelId,
apiHost: provider.apiHost
})
const response = (await client.chat.completions.create(providerRequest)) as OpenAI.Chat.Completions.ChatCompletion
logger.info('Successfully processed chat completion')
return {
provider,
modelId,
response
}
} catch (error: any) {
logger.error('Error processing chat completion:', error)
throw error
}
}
async processStreamingCompletion(
request: ChatCompletionCreateParams
): Promise<{
provider: Provider
modelId: string
stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>
}> {
try {
logger.info('Processing streaming chat completion request:', {
model: request.model,
messageCount: request.messages.length
})
const preparation = await this.prepareRequest(request, true)
if (preparation.status === 'validation_error') {
throw new ChatCompletionValidationError(preparation.errors)
}
if (preparation.status === 'model_error') {
throw new ChatCompletionModelError(preparation.error)
}
const { provider, modelId, client, providerRequest } = preparation
logger.debug('Sending streaming request to provider:', {
provider: provider.id,
model: modelId,
apiHost: provider.apiHost
})
const streamRequest = providerRequest as ChatCompletionCreateParamsStreaming
const stream = (await client.chat.completions.create(streamRequest)) as AsyncIterable<
OpenAI.Chat.Completions.ChatCompletionChunk
>
logger.info('Successfully started streaming chat completion')
return {
provider,
modelId,
stream
}
} catch (error: any) {
logger.error('Error processing streaming chat completion:', error)
throw error
}
}
}
// Export singleton instance
export const chatCompletionService = new ChatCompletionService()

View File

@@ -0,0 +1,226 @@
import mcpService from '@main/services/MCPService'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp'
import {
isJSONRPCRequest,
JSONRPCMessage,
JSONRPCMessageSchema,
MessageExtraInfo
} from '@modelcontextprotocol/sdk/types'
import { MCPServer } from '@types'
import { randomUUID } from 'crypto'
import { EventEmitter } from 'events'
import { Request, Response } from 'express'
import { IncomingMessage, ServerResponse } from 'http'
import { loggerService } from '../../services/LoggerService'
import { getMcpServerById, getMCPServersFromRedux } from '../utils/mcp'
const logger = loggerService.withContext('MCPApiService')
const transports: Record<string, StreamableHTTPServerTransport> = {}
interface McpServerDTO {
id: MCPServer['id']
name: MCPServer['name']
type: MCPServer['type']
description: MCPServer['description']
url: string
}
interface McpServersResp {
servers: Record<string, McpServerDTO>
}
/**
* MCPApiService - API layer for MCP server management
*
* This service provides a REST API interface for MCP servers while integrating
* with the existing application architecture:
*
* 1. Uses ReduxService to access the renderer's Redux store directly
* 2. Syncs changes back to the renderer via Redux actions
* 3. Leverages existing MCPService for actual server connections
* 4. Provides session management for API clients
*/
class MCPApiService extends EventEmitter {
private transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
})
constructor() {
super()
this.initMcpServer()
logger.silly('MCPApiService initialized')
}
private initMcpServer() {
this.transport.onmessage = this.onMessage
}
// get all activated servers
async getAllServers(req: Request): Promise<McpServersResp> {
try {
const servers = await getMCPServersFromRedux()
logger.silly(`Returning ${servers.length} servers`)
const resp: McpServersResp = {
servers: {}
}
for (const server of servers) {
if (server.isActive) {
resp.servers[server.id] = {
id: server.id,
name: server.name,
type: 'streamableHttp',
description: server.description,
url: `${req.protocol}://${req.host}/v1/mcps/${server.id}/mcp`
}
}
}
return resp
} catch (error: any) {
logger.error('Failed to get all servers:', error)
throw new Error('Failed to retrieve servers')
}
}
// get server by id
async getServerById(id: string): Promise<MCPServer | null> {
try {
logger.silly(`getServerById called with id: ${id}`)
const servers = await getMCPServersFromRedux()
const server = servers.find((s) => s.id === id)
if (!server) {
logger.warn(`Server with id ${id} not found`)
return null
}
logger.silly(`Returning server with id ${id}`)
return server
} catch (error: any) {
logger.error(`Failed to get server with id ${id}:`, error)
throw new Error('Failed to retrieve server')
}
}
async getServerInfo(id: string): Promise<any> {
try {
logger.silly(`getServerInfo called with id: ${id}`)
const server = await this.getServerById(id)
if (!server) {
logger.warn(`Server with id ${id} not found`)
return null
}
logger.silly(`Returning server info for id ${id}`)
const client = await mcpService.initClient(server)
const tools = await client.listTools()
logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) })
// const [version, tools, prompts, resources] = await Promise.all([
// () => {
// try {
// return client.getServerVersion()
// } catch (error) {
// logger.error(`Failed to get server version for id ${id}:`, { error: error })
// return '1.0.0'
// }
// },
// (() => {
// try {
// return client.listTools()
// } catch (error) {
// logger.error(`Failed to list tools for id ${id}:`, { error: error })
// return []
// }
// })(),
// (() => {
// try {
// return client.listPrompts()
// } catch (error) {
// logger.error(`Failed to list prompts for id ${id}:`, { error: error })
// return []
// }
// })(),
// (() => {
// try {
// return client.listResources()
// } catch (error) {
// logger.error(`Failed to list resources for id ${id}:`, { error: error })
// return []
// }
// })()
// ])
return {
id: server.id,
name: server.name,
type: server.type,
description: server.description,
tools
}
} catch (error: any) {
logger.error(`Failed to get server info with id ${id}:`, error)
throw new Error('Failed to retrieve server info')
}
}
async handleRequest(req: Request, res: Response, server: MCPServer) {
const sessionId = req.headers['mcp-session-id'] as string | undefined
logger.silly(`Handling request for server with sessionId ${sessionId}`)
let transport: StreamableHTTPServerTransport
if (sessionId && transports[sessionId]) {
transport = transports[sessionId]
} else {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sessionId) => {
transports[sessionId] = transport
}
})
transport.onclose = () => {
logger.info(`Transport for sessionId ${sessionId} closed`)
if (transport.sessionId) {
delete transports[transport.sessionId]
}
}
const mcpServer = await getMcpServerById(server.id)
if (mcpServer) {
await mcpServer.connect(transport)
}
}
const jsonpayload = req.body
const messages: JSONRPCMessage[] = []
if (Array.isArray(jsonpayload)) {
for (const payload of jsonpayload) {
const message = JSONRPCMessageSchema.parse(payload)
messages.push(message)
}
} else {
const message = JSONRPCMessageSchema.parse(jsonpayload)
messages.push(message)
}
for (const message of messages) {
if (isJSONRPCRequest(message)) {
if (!message.params) {
message.params = {}
}
if (!message.params._meta) {
message.params._meta = {}
}
message.params._meta.serverId = server.id
}
}
logger.info(`Request body`, { rawBody: req.body, messages: JSON.stringify(messages) })
await transport.handleRequest(req as IncomingMessage, res as ServerResponse, messages)
}
private onMessage(message: JSONRPCMessage, extra?: MessageExtraInfo) {
logger.info(`Received message: ${JSON.stringify(message)}`, extra)
// Handle message here
}
}
export const mcpApiService = new MCPApiService()

View File

@@ -0,0 +1,106 @@
import Anthropic from '@anthropic-ai/sdk'
import { Message, MessageCreateParams, RawMessageStreamEvent } from '@anthropic-ai/sdk/resources'
import { Provider } from '@types'
import { loggerService } from '../../services/LoggerService'
const logger = loggerService.withContext('MessagesService')
export interface ValidationResult {
isValid: boolean
errors: string[]
}
export class MessagesService {
// oxlint-disable-next-line no-unused-vars
validateRequest(request: MessageCreateParams): ValidationResult {
// TODO: Implement comprehensive request validation
const errors: string[] = []
if (!request.model) {
errors.push('Model is required')
}
if (!request.max_tokens || request.max_tokens < 1) {
errors.push('max_tokens is required and must be at least 1')
}
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
errors.push('messages is required and must be a non-empty array')
}
return {
isValid: errors.length === 0,
errors
}
}
async processMessage(request: MessageCreateParams, provider: Provider): Promise<Message> {
logger.info('Processing Anthropic message request:', {
model: request.model,
messageCount: request.messages.length,
stream: request.stream,
max_tokens: request.max_tokens
})
// Create Anthropic client for the provider
const client = new Anthropic({
baseURL: provider.apiHost,
apiKey: provider.apiKey
})
// Prepare request with the actual model ID
const anthropicRequest: MessageCreateParams = {
...request,
stream: false
}
logger.debug('Sending request to Anthropic provider:', {
provider: provider.id,
apiHost: provider.apiHost
})
const response = await client.messages.create(anthropicRequest)
logger.info('Successfully processed Anthropic message')
return response
}
async *processStreamingMessage(
request: MessageCreateParams,
provider: Provider
): AsyncIterable<RawMessageStreamEvent> {
logger.info('Processing streaming Anthropic message request:', {
model: request.model,
messageCount: request.messages.length
})
// Create Anthropic client for the provider
const client = new Anthropic({
baseURL: provider.apiHost,
apiKey: provider.apiKey
})
// Prepare streaming request
const streamingRequest: MessageCreateParams = {
...request,
stream: true
}
logger.debug('Sending streaming request to Anthropic provider:', {
provider: provider.id,
apiHost: provider.apiHost
})
const stream = client.messages.stream(streamingRequest)
for await (const chunk of stream) {
yield chunk
}
logger.info('Successfully completed streaming Anthropic message')
}
}
// Export singleton instance
export const messagesService = new MessagesService()

View File

@@ -0,0 +1,93 @@
import { ApiModel, ApiModelsFilter, ApiModelsResponse } from '../../../renderer/src/types/apiModels'
import { loggerService } from '../../services/LoggerService'
import { getAvailableProviders, listAllAvailableModels, transformModelToOpenAI } from '../utils'
const logger = loggerService.withContext('ModelsService')
// Re-export for backward compatibility
export type ModelsFilter = ApiModelsFilter
export class ModelsService {
async getModels(filter: ModelsFilter): Promise<ApiModelsResponse> {
try {
logger.debug('Getting available models from providers', { filter })
const models = await listAllAvailableModels()
const providers = await getAvailableProviders()
// Use Map to deduplicate models by their full ID (provider:model_id)
const uniqueModels = new Map<string, ApiModel>()
for (const model of models) {
const openAIModel = transformModelToOpenAI(model, providers)
const fullModelId = openAIModel.id // This is already in format "provider:model_id"
// Only add if not already present (first occurrence wins)
if (!uniqueModels.has(fullModelId)) {
uniqueModels.set(fullModelId, openAIModel)
} else {
logger.debug(`Skipping duplicate model: ${fullModelId}`)
}
}
let modelData = Array.from(uniqueModels.values())
if (filter.providerType) {
// Apply filters
const providerType = filter.providerType
modelData = modelData.filter((model) => {
// Find the provider for this model and check its type
return model.provider_type === providerType
})
logger.debug(`Filtered by provider type '${providerType}': ${modelData.length} models`)
}
const total = modelData.length
// Apply pagination
const offset = filter?.offset || 0
const limit = filter?.limit
if (limit !== undefined) {
modelData = modelData.slice(offset, offset + limit)
logger.debug(
`Applied pagination: offset=${offset}, limit=${limit}, showing ${modelData.length} of ${total} models`
)
} else if (offset > 0) {
modelData = modelData.slice(offset)
logger.debug(`Applied offset: offset=${offset}, showing ${modelData.length} of ${total} models`)
}
logger.info(`Successfully retrieved ${modelData.length} models from ${models.length} total models`)
if (models.length > total) {
logger.debug(`Filtered out ${models.length - total} models after deduplication and filtering`)
}
const response: ApiModelsResponse = {
object: 'list',
data: modelData
}
// Add pagination metadata if applicable
if (filter?.limit !== undefined || filter?.offset !== undefined) {
response.total = total
response.offset = offset
if (filter?.limit !== undefined) {
response.limit = filter.limit
}
}
return response
} catch (error: any) {
logger.error('Error getting models:', error)
return {
object: 'list',
data: []
}
}
}
}
// Export singleton instance
export const modelsService = new ModelsService()

Some files were not shown because too many files have changed in this diff Show More