Compare commits

..

7 Commits

Author SHA1 Message Date
lizhixuan
c518c9090b docs: add example implementation of Redux Slice for message management
- Introduced a new section in the technical documentation detailing the implementation of a `messages` slice using `createEntityAdapter`.
- Provided TypeScript code for the slice, including actions for adding, updating, and removing messages.
- Summarized the core principles of the slice's design, emphasizing single responsibility, logical separation of concerns, and performance optimization.
- Included a migration strategy for transitioning from the previous state structure to the new message pool approach.
2025-06-12 18:34:36 +08:00
suyao
91045ecc2b docs: finalize technical report for message history version management system with multi-model support
- Updated the design document to reflect the final version, incorporating multi-model support and enhanced version management features.
- Expanded the data structure section to include new entities and relationships, such as `askId`, `parentMessageId`, and `siblingIds`.
- Improved the core operation processes, including sending new messages and managing message versions.
- Added detailed diagrams and performance analysis to illustrate the new architecture and its advantages over the previous system.
- Ensured backward compatibility while introducing new functionalities for branching conversations and version control.
2025-06-12 15:59:54 +08:00
suyao
748ca008b4 docs: add technical report for message history version management system
- Introduced a comprehensive design document outlining the architecture and requirements for a message history version management system.
- Added new entities `UserMessage` and `AssistantMessageGroup` to support a directed multi-branch conversation structure and version management.
- Updated existing entities to accommodate the new architecture while maintaining backward compatibility.
- Included performance analysis and migration strategies for transitioning to the new system.
2025-06-12 14:25:42 +08:00
kangfenmao
6ad9044cd1 refactor: replace 302ai PNG with WEBP format and update provider configurations
- Deleted the old PNG logo for 302ai and added a new WEBP version.
- Updated the provider configuration to use the new WEBP logo.
- Added translations for the new Cephalon provider in Japanese and Russian.
- Disabled the 302ai and Cephalon providers in the initial state of the store.
- Adjusted migration logic to accommodate the new provider setup.
2025-06-12 12:16:43 +08:00
JI4JUN
9e9a1ec024 feat: support 302ai provider (#7044)
* feat(porvider): add provider 302ai

* style(provider): change provider name 302AI to 302.AI

* style(provider): system models replacement of 302.AI provider

---------

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-06-12 12:04:21 +08:00
HzTTT
a214dca6fa feat:add cephalon provider (#7050)
* feat: add Cephalon provider and related assets

* add Cephalon logo image
* update models to include Cephalon's DeepSeek-R1
* add Cephalon provider configuration and API details
* include Cephalon translations in multiple languages
* update store to initialize Cephalon as a provider
* increment version for migration

* feat: update Cephalon provider configuration and assets

* add Cephalon logo image
* enable Cephalon provider in the store
* remove previous disabled configuration for Cephalon

* fix: update Cephalon provider model URL

* fix: update official website URL for Cephalon provider
2025-06-12 12:02:03 +08:00
one
b142e5647e fix(Markdown): inline math overflow (#7095) 2025-06-12 11:05:52 +08:00
40 changed files with 727 additions and 790 deletions

View File

@@ -27,7 +27,7 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Get release tag
id: get-tag
@@ -149,4 +149,4 @@ jobs:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: CherryHQ/cherry-studio-docs
event-type: update-download-version
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'
client-payload: '{"version": "${{ steps.get-tag.outputs.tag }}"}'

View File

@@ -0,0 +1,635 @@
# 消息历史版本管理系统设计技术报告(最终版 - 含多模型支持)
## 1. 系统概述
基于现有扁平化架构的最小化扩展,通过 **Topic快照 + Message字段扩展含siblingIds** 实现版本管理、分支对话和多模型并行回复功能。
### 1.1 核心设计理念
- **最小破坏性**:只扩展现有实体,不新增表
- **快照渲染**通过Topic简单快照管理主线渲染顺序
- **关系扩展**通过Message字段实现树状分支、双向链表版本、多模型兄弟关系
## 2. 数据结构设计
### 2.1 实体定义
```typescript
interface Topic {
// === 现有字段保持不变 ===
id: string
name: string
createdAt: string
updatedAt: string
// === 保持简单快照 ===
activeMessageIds: string[] // 当前活跃对话主线的消息ID顺序
}
interface Message {
// === 现有字段保持不变 ===
id: string
role: 'user' | 'assistant' | 'system'
topicId: string
blocks: MessageBlock['id'][]
// === 新增:关系字段 ===
askId?: string // 问答关系assistant指向对应的user消息
parentMessageId?: string // 分支关系:指向回复的目标消息
version?: number // 版本号assistant消息专用
prevVersionId?: string // 版本链表:前一版本
nextVersionId?: string // 版本链表:后一版本
groupRequestId?: string // 请求分组同次API请求的标识
siblingIds?: string[] // 兄弟关系同级多模型回复的ID列表
}
interface MessageBlock {
// === 完全不变 ===
id: string
messageId: string
type: MessageBlockType
content: string
// ...其他现有字段
}
```
### 2.2 数据关系图
```mermaid
graph TB
subgraph "Topic快照层 (主线)"
T[Topic.activeMessageIds: user1→asst1-gpt→user2]
end
subgraph "消息实体层"
U1[User Message 1<br/>id: user1]
A1G["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A1C["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
U2["User Message 2<br/>id: user2, parentMessageId: asst1-gpt"]
end
subgraph "版本链表层 (隐藏)"
A1GV0[GPT-4 v0<br/>askId: user1, version: 0]
A1GV1[GPT-4 v1<br/>askId: user1, version: 1]
A1GV0 -.->|nextVersionId| A1GV1
A1GV1 -.->|prevVersionId| A1GV0
end
subgraph "分支树层 (隐藏)"
U1B[User Branch 1<br/>parentMessageId: asst1-gpt]
A1B[Assistant Branch 1<br/>askId: user1b]
end
T --> U1
T --> A1G
T --> U2
A1G -.->|askId| U1
A1C -.->|askId| U1
A1G -.->|siblingIds| A1C
A1C -.->|siblingIds| A1G
U2 -.->|parentMessageId| A1G
U1B -.->|parentMessageId| A1G
A1B -.->|askId| U1B
```
## 3. 核心操作流程
### 3.1 发送新消息(多模型)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant API
UI->>Redux: sendMessage(userContent, models[])
Note over Redux: 1. 创建用户消息
Redux->>Redux: userMessage = { id: uuid(), role: 'user', ... }
Note over Redux: 2. 创建助手消息(多模型)
Redux->>Redux: groupRequestId = uuid()
Redux->>Redux: assistantMessages = models.map(m => createAssistant(userMessage.id, m))
Note over Redux: 3. 设置兄弟关系
Redux->>Redux: assistantIds = assistantMessages.map(m => m.id)
loop 每个助手消息
Redux->>Redux: msg.siblingIds = assistantIds.filter(id => id !== msg.id)
end
Note over Redux: 4. 更新Topic快照
Redux->>Redux: newActiveMessageIds = [<br/>...oldIds,<br/>userMessage.id,<br/>assistantMessages[0].id<br/>]
Note over Redux: 5. 原子保存
Redux->>DB: transaction([messages, topics])
DB->>DB: messages.bulkPut([userMessage, ...assistantMessages])
DB->>DB: topics.update(topicId, { activeMessageIds })
Note over Redux: 6. 发送API请求
loop 每个模型
Redux->>API: generateResponse(model, userContent)
end
Redux->>UI: 更新状态
```
**复杂度**O(M) where M = 模型数量
### 3.2 重发消息(版本管理)
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
UI->>Redux: resendMessage(userMessageId)
Note over Redux: 1. 查找现有版本
Redux->>DB: messages.where('askId').equals(userMessageId)
DB-->>Redux: existingVersions[]
Note over Redux: 2. 计算新版本号
Redux->>Redux: latestVersion = max(versions.map(v => v.version))
Redux->>Redux: newVersion = latestVersion + 1
Note over Redux: 3. 创建新版本消息(可能多模型)
Redux->>Redux: newGroupRequestId = uuid()
Redux->>Redux: newVersionMessages = models.map(m => createNewVersion(prevMsg, newVersion, newGroupRequestId))
Note over Redux: 4. 设置新版本的兄弟关系
Redux->>Redux: newVersionIds = newVersionMessages.map(m => m.id)
loop 每个新版本消息
Redux->>Redux: msg.siblingIds = newVersionIds.filter(id => id !== msg.id)
end
Note over Redux: 5. 更新版本链表
Redux->>DB: transaction(messages)
DB->>DB: messages.update(prevMessage.id, { nextVersionId })
DB->>DB: messages.bulkPut(newVersionMessages)
Redux->>UI: 更新状态
```
**复杂度**O(V) 查找 + O(M) 创建
### 3.3 切换活跃模型UI交互
```mermaid
flowchart TD
A[用户在UI上选择其他模型] --> B[获取当前快照]
B --> C[找到当前助手消息在快照中的位置]
C --> D[用新选择的模型消息ID替换快照中的ID]
D --> E[保存到数据库]
E --> F[Redux自动重新渲染]
style A fill:#e1f5fe
style F fill:#c8e6c9
```
```typescript
const switchActiveModel = async (topicId: string, messageIndex: number, newModelMessageId: string) => {
const topic = await db.topics.get(topicId)
const newActiveMessageIds = [...topic.activeMessageIds]
newActiveMessageIds[messageIndex] = newModelMessageId
await db.topics.update(topicId, { activeMessageIds: newActiveMessageIds })
}
```
**复杂度**O(1)
## 4. 字段作用详解
### 4.1 关键字段关系图
```mermaid
graph LR
subgraph "问答关系"
askId[askId<br/>assistant → user<br/>逻辑关系,永久不变]
end
subgraph "分支关系"
parentId[parentMessageId<br/>message → message<br/>分支对话,树状结构]
end
subgraph "版本关系"
version[version + prevVersionId + nextVersionId<br/>同askId下的版本链表]
end
subgraph "请求分组"
groupId[groupRequestId<br/>同次API请求标识<br/>一次性,每次重发都变]
end
subgraph "兄弟关系"
siblingId[siblingIds<br/>同级多模型回复<br/>双向引用]
end
askId -.-> version
askId -.-> siblingId
parentId -.-> askId
groupId -.-> askId
```
### 4.2 字段使用场景
| 字段 | 用途 | 查询场景 | 生命周期 |
| -------------------------------- | ---------- | -------------------------- | -------- |
| **askId** | 问答映射 | 查找用户问题的所有回复版本 | 永久不变 |
| **parentMessageId** | 分支对话 | 查找某消息的回复分支 | 永久不变 |
| **version + prev/nextVersionId** | 版本管理 | 版本历史导航 | 永久不变 |
| **groupRequestId** | 请求追踪 | 批量状态更新、请求监控 | 一次性 |
| **siblingIds** | 多模型并行 | 渲染同级多模型回复 | 永久不变 |
### 4.3 多模型并行渲染示例
```mermaid
graph TD
U1[User: 帮我写个函数<br/>id: user1]
subgraph "第一次请求 (groupRequestId: req1)"
A1["GPT-4 回复<br/>id: asst1-gpt, askId: user1<br/>siblingIds: [asst1-claude]"]
A2["Claude 回复<br/>id: asst1-claude, askId: user1<br/>siblingIds: [asst1-gpt]"]
end
subgraph "Topic快照 (主线)"
T["activeMessageIds: [user1, asst1-gpt]"]
end
subgraph "UI渲染 (通过siblingIds扩展)"
UI_U1[User: 帮我写个函数]
UI_A1["GPT-4 回复 (活跃)"]
UI_A2["Claude 回复 (可选)"]
end
U1 --> A1
U1 --> A2
T --> U1
T --> A1
A1 -.->|siblingIds| A2
A2 -.->|siblingIds| A1
UI_U1 -.-> UI_A1
UI_U1 -.-> UI_A2
```
## 5. 数据查询与状态管理
### 5.1 话题加载流程
```mermaid
sequenceDiagram
participant UI
participant Redux
participant DB
participant Selector
UI->>Redux: loadTopic(topicId)
Redux->>DB: 并行查询
par 查询消息
DB->>DB: messages.where('topicId').equals(topicId)
and 查询块
DB->>DB: messageBlocks.where('topicId').equals(topicId)
end
DB-->>Redux: { messages[], blocks[] }
Redux->>Redux: 更新实体状态
UI->>Selector: selectActiveConversationWithSiblings(topicId)
Selector->>Redux: 获取Topic.activeMessageIds
Selector->>Redux: 获取messages实体
Selector-->>UI: 按快照顺序的消息列表 (含兄弟节点)
Note over UI: 渲染对话界面 (支持多模型)
```
### 5.2 渲染选择器(含兄弟节点)
```typescript
export const selectActiveConversationWithSiblings = createSelector(
[
(state: RootState, topicId: string) => state.topics.entities[topicId]?.activeMessageIds || [],
(state: RootState) => state.messages.entities,
(state: RootState) => state.messageBlocks.entities
],
(activeMessageIds, messagesEntities, blocksEntities) => {
return activeMessageIds
.map((messageId) => {
const message = messagesEntities[messageId]
if (!message) return null
if (message.role === 'user') {
return { type: 'user', message, blocks: getMessageBlocks(message, blocksEntities) }
} else if (message.role === 'assistant') {
const siblingMessages = (message.siblingIds || []).map((id) => messagesEntities[id]).filter(Boolean)
const allAssistantMessages = [message, ...siblingMessages]
return {
type: 'assistant_group',
messages: allAssistantMessages.map((msg) => ({
message: msg,
blocks: getMessageBlocks(msg, blocksEntities),
isActive: msg.id === messageId
})),
activeMessageId: messageId
}
}
})
.filter(Boolean)
}
)
```
**复杂度**O(N + S) where N = 快照长度, S = 兄弟节点总数
## 6. 时空复杂度分析
### 6.1 核心操作复杂度对比
```mermaid
graph LR
subgraph "现有架构"
A1[加载话题: O(M+B)]
A2[渲染对话: O(M) 需要过滤排序]
A3[发送消息: O(1)]
end
subgraph "新架构 (含多模型)"
B1[加载话题: O(M+B) ✅相同]
B2[渲染对话: O(N+S) ✅更优]
B3[发送消息: O(M_models) ✅相同]
B4[版本切换: O(1) ➕新功能]
B5[重发消息: O(V)+O(M_models) ➕新功能]
B6[模型切换: O(1) ➕新功能]
end
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
style B4 fill:#fff3e0
style B5 fill:#fff3e0
style B6 fill:#fff3e0
```
### 6.2 性能优势分析
| 操作 | 现有架构 | 新架构 | 优势说明 |
| ------------ | -------------- | ---------------------------- | -------------------- |
| **话题加载** | O(M + B) | O(M + B) | 性能保持不变 |
| **对话渲染** | O(M) 过滤+排序 | **O(N+S)** 直接索引+兄弟扩展 | N << MS通常较小 |
| **发送消息** | O(1) | O(M_models) | 支持多模型,合理增长 |
| **版本切换** | 不支持 | **O(1)** | 新功能,极佳性能 |
| **模型切换** | 不支持 | **O(1)** | 新功能,极佳性能 |
**关键优势**
- **渲染性能提升**:从 O(M) 优化到 O(N+S),长对话场景收益显著
- **多模型支持**:通过 siblingIds 优雅实现
- **版本管理**O(1) 的版本/模型切换,用户体验极佳
- **向后兼容**:现有核心操作性能保持不变
## 7. 数据库Schema演进
### 7.1 Migration策略
```mermaid
flowchart TD
A[现有Schema] --> B[添加字段]
B --> C[创建索引]
C --> D[数据迁移]
D --> E[验证完整性]
B1[Topic: +activeMessageIds]
B2[Message: +askId, +parentMessageId<br/>+version, +prevVersionId<br/>+nextVersionId, +groupRequestId<br/>+siblingIds]
C1[idx_messages_askid_version]
C2[idx_messages_parent]
C3[idx_messages_group_request]
D1[生成activeMessageIds快照]
D2[设置现有assistant消息version=0]
B --> B1
B --> B2
C --> C1
C --> C2
C --> C3
D --> D1
D --> D2
```
### 7.2 SQL Migration
```sql
-- 1. 添加字段
ALTER TABLE topics ADD COLUMN activeMessageIds TEXT; -- JSON数组
ALTER TABLE messages ADD COLUMN askId TEXT;
ALTER TABLE messages ADD COLUMN parentMessageId TEXT;
ALTER TABLE messages ADD COLUMN version INTEGER;
ALTER TABLE messages ADD COLUMN prevVersionId TEXT;
ALTER TABLE messages ADD COLUMN nextVersionId TEXT;
ALTER TABLE messages ADD COLUMN groupRequestId TEXT;
ALTER TABLE messages ADD COLUMN siblingIds TEXT; -- JSON数组
-- 2. 创建索引
CREATE INDEX idx_messages_askid_version ON messages(askId, version);
CREATE INDEX idx_messages_parent ON messages(parentMessageId);
CREATE INDEX idx_messages_group_request ON messages(groupRequestId);
-- 3. 数据迁移
UPDATE messages SET version = 0 WHERE role = 'assistant';
```
## 8. 流式更新兼容性
### 8.1 MessageBlock更新流程
```mermaid
sequenceDiagram
participant Stream
participant Redux
participant DB
participant UI
Note over Stream: 流式内容到达
Stream->>Redux: updateBlock(blockId, content)
Redux->>Redux: updateOneBlock({ id, changes })
Redux->>UI: 立即更新显示
Note over Redux: 节流数据库写入
Redux->>DB: throttledDbUpdate(blockId, content)
Note over Stream,UI: 版本/兄弟关系不影响块更新
```
**关键点**
- MessageBlock 仍然直接关联到 Message
- 版本/兄弟关系在 Message 层面,不影响 Block 的流式更新
- 现有的节流机制和更新逻辑完全保持不变
## 9. 系统架构总览
### 9.1 整体架构图
```mermaid
graph TB
subgraph "UI层"
UI1[对话界面]
UI2[版本选择器]
UI3[分支导航]
UI4[模型切换器]
end
subgraph "Redux状态层"
R1[topics: EntityAdapter]
R2[messages: EntityAdapter]
R3[messageBlocks: EntityAdapter]
S1[selectActiveConversationWithSiblings]
S2[selectVersionHistory]
end
subgraph "数据库层"
DB1[(topics表)]
DB2[(messages表)]
DB3[(messageBlocks表)]
end
subgraph "API层"
API1[多模型并行请求]
API2[流式响应处理]
end
UI1 --> S1
UI2 --> S2
UI4 --> S1
S1 --> R1
S1 --> R2
S2 --> R2
R1 <--> DB1
R2 <--> DB2
R3 <--> DB3
R2 --> API1
API2 --> R3
style UI1 fill:#e3f2fd
style R1 fill:#f3e5f5
style R2 fill:#f3e5f5
style R3 fill:#f3e5f5
style DB1 fill:#e8f5e8
style DB2 fill:#e8f5e8
style DB3 fill:#e8f5e8
```
### 9.2 数据流向
```mermaid
flowchart LR
A[用户输入] --> B[创建User Message]
B --> C["创建Assistant Messages (多模型)"]
C --> C1[设置Sibling关系]
C1 --> D["更新Topic快照 (主线)"]
D --> E[API并行请求]
E --> F[流式更新Blocks]
F --> G["UI实时渲染 (含多模型)"]
H[版本切换] --> I[更新快照指针]
I --> G
J[分支对话] --> K[创建分支消息]
K --> D
L[模型切换] --> I
style A fill:#ffebee
style G fill:#e8f5e8
style H fill:#fff3e0
style J fill:#f3e5f5
style L fill:#e1f5fe
```
## 10. Redux Slice 实现范例
根据上述架构设计,`messages` slice 将演变为一个纯粹的、由 `createEntityAdapter` 管理的"消息池"。它只负责高效地存储和访问单个消息实体,而不再关心对话的顺序。
### `store/messagesSlice.ts`
```typescript
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from './store' // 你的store类型定义
import type { Message } from '@renderer/types/newMessage' // 假设 Message 类型定义在外部
// 1. 创建 Entity Adapter
// 它会自动生成管理实体的reducer逻辑实现一个高效的消息池。
const messagesAdapter = createEntityAdapter<Message>()
// 2. 定义 Slice 的初始状态
// adapter.getInitialState() 会自动创建 { ids: [], entities: {} } 结构
const initialState = messagesAdapter.getInitialState()
// 3. 创建 Slice
const messagesSlice = createSlice({
name: 'messages',
initialState,
// Reducers被极大简化多数直接引用adapter提供的方法
reducers: {
// Action: 添加一条消息
messageAdded: messagesAdapter.addOne,
// Action: 一次性添加或更新多个消息 (高性能)
// 用途: 加载话题历史、发送新一轮问答(user+assistants)
messagesUpserted: messagesAdapter.upsertMany,
// Action: 更新单个消息
// 用途: 流式更新结束、状态变更等
messageUpdated: messagesAdapter.updateOne,
// Action: 删除单个消息
messageRemoved: messagesAdapter.removeOne,
// Action: 删除多个消息
messagesRemoved: messagesAdapter.removeMany,
// Action: 用新数据完全替换消息池
// 用途: 首次加载或强制刷新
messagesSet: messagesAdapter.setAll
}
})
// 4. 导出 Actions
export const { messageAdded, messagesUpserted, messageUpdated, messageRemoved, messagesRemoved, messagesSet } =
messagesSlice.actions
// 5. 导出 Selectors
// Adapter 会自动创建高效的查询函数 (e.g., O(1) by ID)
export const messagesSelectors = messagesAdapter.getSelectors((state: RootState) => state.messages)
// 6. 导出 Reducer
export default messagesSlice.reducer
```
### 核心思想总结
1. **职责单一**: 此 Slice 只做一件事——管理 `Message` 实体。它像一个数据库表,高效地处理增删改查,但对业务逻辑(如对话顺序)一无所知。
2. **逻辑上移**: 所有涉及多个 Slice 的复杂业务逻辑(如发送消息、切换版本)都应封装在 **Thunks** 或其他中间件中。Thunk 作为流程协调者,会 `dispatch` 多个原子化的 Action 给 `messagesSlice``topicsSlice`,以完成一次完整的业务操作并保证数据一致性。
3. **性能保证**: `createEntityAdapter` 内部使用哈希表(对象)来存储实体,确保通过 ID 查询消息的操作为 O(1) 复杂度,性能极佳。
### 旧状态属性迁移
为了完成 `messagesSlice` 向纯粹"消息池"的演进,原有的混合状态属性需要被迁移或废弃,以实现彻底的职责分离。
| 原属性 (`newMessage.ts`) | 处理方式 | 新的归宿 / 说明 |
| :----------------------- | :------------ | :-------------------------------------------------------------------------------------------- |
| `messageIdsByTopic` | **废弃** | 核心职责转移。由 `topicsSlice` 中的 `activeMessageIds` 字段接管,作为渲染快照。 |
| `currentTopicId` | **迁移** | 属于UI当前上下文状态应迁移至 `topicsSlice`。 |
| `loadingByTopic` | **迁移** | 话题的加载状态与话题本身更相关,应迁移至 `topicsSlice`。 |
| `displayCount` | **废弃/迁移** | UI相关的显示逻辑不属于消息数据层。建议迁移至专门的 `Slice` 或在相关组件中作为本地状态管理。 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -107,7 +107,11 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
服务商新增端脑云、302.AI、蓝耘服务商
MCP: 新增蓝耘 MCP 服务器
实现话题重命名动画效果
错误修复
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能
复制功能新增纯文本复制去除Markdown格式符号
知识库支持设置向量维度修复Ollama分数错误和维度编辑问题
多语言:增加模型名称多语言提示和翻译源语言手动选择
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
模型修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
图像功能统一图片查看器支持Base64图片渲染修复图片预览相关问题
UI实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.4.3",
"version": "1.4.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",

View File

@@ -36,11 +36,6 @@ exports.default = async function (context) {
keepPackageNodeFiles(node_modules_path, '@libsql', ['win32-x64-msvc'])
}
}
if (platform === 'windows') {
fs.rmSync(path.join(context.appOutDir, 'LICENSE.electron.txt'), { force: true })
fs.rmSync(path.join(context.appOutDir, 'LICENSES.chromium.html'), { force: true })
}
}
/**

View File

@@ -21,13 +21,10 @@ export default abstract class BaseReranker {
return 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank'
}
let baseURL = this.base.rerankBaseURL
if (baseURL && baseURL.endsWith('/')) {
// `/` 结尾强制使用rerankBaseURL
return `${baseURL}rerank`
}
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
? this.base.rerankBaseURL.slice(0, -1)
: this.base.rerankBaseURL
// 必须携带/v1否则会404
if (baseURL && !baseURL.endsWith('/v1')) {
baseURL = `${baseURL}/v1`
}

View File

@@ -5,8 +5,7 @@ import { FeedUrl } from '@shared/config/constant'
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, NsisUpdater } from 'electron-updater'
import path from 'path'
import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager'
@@ -57,10 +56,6 @@ export default class AppUpdater {
logger.info('下载完成', releaseInfo)
})
if (isWin) {
;(autoUpdater as NsisUpdater).installDirectory = path.dirname(app.getPath('exe'))
}
this.autoUpdater = autoUpdater
}

View File

@@ -285,7 +285,7 @@ export class SelectionService {
this.processTriggerMode()
this.started = true
this.logInfo('SelectionService Started', true)
this.logInfo('SelectionService Started')
return true
}
@@ -319,7 +319,7 @@ export class SelectionService {
this.closePreloadedActionWindows()
this.started = false
this.logInfo('SelectionService Stopped', true)
this.logInfo('SelectionService Stopped')
return true
}
@@ -335,7 +335,7 @@ export class SelectionService {
this.selectionHook = null
this.initStatus = false
SelectionService.instance = null
this.logInfo('SelectionService Quitted', true)
this.logInfo('SelectionService Quitted')
}
/**
@@ -456,18 +456,8 @@ export class SelectionService {
x: posX,
y: posY
})
//set the window to always on top (highest level)
//should set every time the window is shown
this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver')
this.toolbarWindow!.show()
/**
* In Windows 10, setOpacity(1) will make the window completely transparent
* It's a strange behavior, so we don't use it for compatibility
*/
// this.toolbarWindow!.setOpacity(1)
this.toolbarWindow!.setOpacity(1)
this.startHideByMouseKeyListener()
}
@@ -477,7 +467,7 @@ export class SelectionService {
public hideToolbar(): void {
if (!this.isToolbarAlive()) return
// this.toolbarWindow!.setOpacity(0)
this.toolbarWindow!.setOpacity(0)
this.toolbarWindow!.hide()
this.stopHideByMouseKeyListener()
@@ -1274,10 +1264,8 @@ export class SelectionService {
this.isIpcHandlerRegistered = true
}
private logInfo(message: string, forceShow: boolean = false) {
if (isDev || forceShow) {
Logger.info('[SelectionService] Info: ', message)
}
private logInfo(message: string) {
isDev && Logger.info('[SelectionService] Info: ', message)
}
private logError(...args: [...string[], Error]) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -136,10 +136,6 @@ ul {
display: flow-root;
}
.block-wrapper:last-child > *:last-child {
margin-bottom: 0;
}
.message-content-container > *:last-child {
margin-bottom: 0;
}

View File

@@ -321,7 +321,6 @@ mjx-container {
.cm-gutters {
line-height: 1.6;
border-right: none;
}
.cm-content {

View File

@@ -22,7 +22,6 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
const [error, setError] = useState<string | null>(null)
const [isRendering, setIsRendering] = useState(false)
const [isVisible, setIsVisible] = useState(true)
// 使用通用图像工具
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(mermaidRef, {
@@ -76,55 +75,10 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
[renderMermaid]
)
/**
* 监听可见性变化,用于触发重新渲染。
* 这是为了解决 `MessageGroup` 组件的 `fold` 布局中被 `display: none` 隐藏的图标无法正确渲染的问题。
* 监听时向上遍历到第一个有 `fold` className 的父节点为止(也就是目前的 `MessageWrapper`)。
* FIXME: 将来 mermaid-js 修复此问题后可以移除这里的相关逻辑。
*/
useEffect(() => {
if (!mermaidRef.current) return
const checkVisibility = () => {
const element = mermaidRef.current
if (!element) return
const currentlyVisible = element.offsetParent !== null
setIsVisible(currentlyVisible)
}
// 初始检查
checkVisibility()
const observer = new MutationObserver(() => {
checkVisibility()
})
let targetElement = mermaidRef.current.parentElement
while (targetElement) {
observer.observe(targetElement, {
attributes: true,
attributeFilter: ['class', 'style']
})
if (targetElement.className?.includes('fold')) {
break
}
targetElement = targetElement.parentElement
}
return () => {
observer.disconnect()
}
}, [])
// 触发渲染
useEffect(() => {
if (isLoadingMermaid) return
if (mermaidRef.current?.offsetParent === null) return
if (children) {
setIsRendering(true)
debouncedRender(children)
@@ -136,7 +90,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
return () => {
debouncedRender.cancel()
}
}, [children, isLoadingMermaid, debouncedRender, isVisible])
}, [children, isLoadingMermaid, debouncedRender])
const isLoading = isLoadingMermaid || isRendering

View File

@@ -1,221 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import MermaidPreview from '../CodeBlockView/MermaidPreview'
const mocks = vi.hoisted(() => ({
useMermaid: vi.fn(),
usePreviewToolHandlers: vi.fn(),
usePreviewTools: vi.fn()
}))
// Mock hooks
vi.mock('@renderer/hooks/useMermaid', () => ({
useMermaid: () => mocks.useMermaid()
}))
vi.mock('@renderer/components/CodeToolbar', () => ({
usePreviewToolHandlers: () => mocks.usePreviewToolHandlers(),
usePreviewTools: () => mocks.usePreviewTools()
}))
// Mock nanoid
vi.mock('@reduxjs/toolkit', () => ({
nanoid: () => 'test-id-123456'
}))
// Mock lodash debounce
vi.mock('lodash', async () => {
const actual = await import('lodash')
return {
...actual,
debounce: vi.fn((fn) => {
const debounced = (...args: any[]) => fn(...args)
debounced.cancel = vi.fn()
return debounced
})
}
})
// Mock antd components
vi.mock('antd', () => ({
Flex: ({ children, vertical, ...props }: any) => (
<div data-testid="flex" data-vertical={vertical} {...props}>
{children}
</div>
),
Spin: ({ children, spinning, indicator }: any) => (
<div data-testid="spin" data-spinning={spinning}>
{spinning && indicator}
{children}
</div>
)
}))
describe('MermaidPreview', () => {
const mockMermaid = {
parse: vi.fn(),
render: vi.fn()
}
beforeEach(() => {
vi.clearAllMocks()
mocks.useMermaid.mockReturnValue({
mermaid: mockMermaid,
isLoading: false,
error: null
})
mocks.usePreviewToolHandlers.mockReturnValue({
handleZoom: vi.fn(),
handleCopyImage: vi.fn(),
handleDownload: vi.fn()
})
mocks.usePreviewTools.mockReturnValue({})
mockMermaid.parse.mockResolvedValue(true)
mockMermaid.render.mockResolvedValue({
svg: '<svg class="flowchart" viewBox="0 0 100 100"><g>test diagram</g></svg>'
})
// Mock MutationObserver
global.MutationObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
disconnect: vi.fn(),
takeRecords: vi.fn()
}))
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('visibility detection', () => {
it('should not render mermaid when element has display: none', async () => {
const mermaidCode = 'graph TD\nA-->B'
const { container } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
// Mock offsetParent to be null (simulating display: none)
const mermaidElement = container.querySelector('.mermaid')
if (mermaidElement) {
Object.defineProperty(mermaidElement, 'offsetParent', {
get: () => null,
configurable: true
})
}
// Re-render to trigger the effect
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
// Should not call mermaid render when offsetParent is null
expect(mockMermaid.render).not.toHaveBeenCalled()
const svgElement = mermaidElement?.querySelector('svg.flowchart')
expect(svgElement).not.toBeInTheDocument()
})
it('should setup MutationObserver to monitor parent elements', () => {
const mermaidCode = 'graph TD\nA-->B'
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
expect(global.MutationObserver).toHaveBeenCalledWith(expect.any(Function))
})
it('should observe parent elements up to fold className', () => {
const mermaidCode = 'graph TD\nA-->B'
// Create a DOM structure that simulates MessageGroup fold layout
const foldContainer = document.createElement('div')
foldContainer.className = 'fold selected'
const messageWrapper = document.createElement('div')
messageWrapper.className = 'message-wrapper'
const codeBlock = document.createElement('div')
codeBlock.className = 'code-block'
foldContainer.appendChild(messageWrapper)
messageWrapper.appendChild(codeBlock)
document.body.appendChild(foldContainer)
render(<MermaidPreview>{mermaidCode}</MermaidPreview>, {
container: codeBlock
})
const observerInstance = (global.MutationObserver as Mock).mock.results[0]?.value
expect(observerInstance.observe).toHaveBeenCalled()
// Cleanup
document.body.removeChild(foldContainer)
})
it('should trigger re-render when visibility changes from hidden to visible', async () => {
const mermaidCode = 'graph TD\nA-->B'
const { container, rerender } = render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
const mermaidElement = container.querySelector('.mermaid')
// Initially hidden (offsetParent is null)
Object.defineProperty(mermaidElement, 'offsetParent', {
get: () => null,
configurable: true
})
// Clear previous calls
mockMermaid.render.mockClear()
// Re-render with hidden state
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
// Should not render when hidden
expect(mockMermaid.render).not.toHaveBeenCalled()
// Now make it visible
Object.defineProperty(mermaidElement, 'offsetParent', {
get: () => document.body,
configurable: true
})
// Simulate MutationObserver callback
const observerCallback = (global.MutationObserver as Mock).mock.calls[0][0]
act(() => {
observerCallback([])
})
// Re-render to trigger visibility change effect
rerender(<MermaidPreview>{mermaidCode}</MermaidPreview>)
await waitFor(() => {
expect(mockMermaid.render).toHaveBeenCalledWith('mermaid-test-id-123456', mermaidCode, expect.any(Object))
const svgElement = mermaidElement?.querySelector('svg.flowchart')
expect(svgElement).toBeInTheDocument()
expect(svgElement).toHaveClass('flowchart')
})
})
it('should handle mermaid loading state', () => {
mocks.useMermaid.mockReturnValue({
mermaid: mockMermaid,
isLoading: true,
error: null
})
const mermaidCode = 'graph TD\nA-->B'
render(<MermaidPreview>{mermaidCode}</MermaidPreview>)
// Should not render when mermaid is loading
expect(mockMermaid.render).not.toHaveBeenCalled()
// Should show loading state
expect(screen.getByTestId('spin')).toHaveAttribute('data-spinning', 'true')
})
})
})

View File

@@ -2169,8 +2169,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'DeepSeek-R1满血版',
group: 'DeepSeek'
}
],
lanyun: []
]
}
export const TEXT_TO_IMAGES_MODELS = [

View File

@@ -22,7 +22,6 @@ import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import HyperbolicProviderLogo from '@renderer/assets/images/providers/hyperbolic.png'
import InfiniProviderLogo from '@renderer/assets/images/providers/infini.png'
import JinaProviderLogo from '@renderer/assets/images/providers/jina.png'
import LanyunProviderLogo from '@renderer/assets/images/providers/lanyun.png'
import LMStudioProviderLogo from '@renderer/assets/images/providers/lmstudio.png'
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
import MistralProviderLogo from '@renderer/assets/images/providers/mistral.png'
@@ -99,8 +98,7 @@ const PROVIDER_LOGO_MAP = {
voyageai: VoyageAIProviderLogo,
qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo,
cephalon: CephalonProviderLogo,
lanyun: LanyunProviderLogo
cephalon: CephalonProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@@ -118,7 +116,7 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://302.ai',
apiKey: 'https://share.302.ai/F1B71g',
apiKey: 'https://dash.302.ai/apis/list',
docs: 'https://302ai.apifox.cn/api-147522039',
models: 'https://302.ai/pricing/'
}
@@ -640,16 +638,5 @@ export const PROVIDER_CONFIG = {
docs: 'https://cephalon.cloud/apitoken/1864244127731589124',
models: 'https://cephalon.cloud/model'
}
},
lanyun: {
api: {
url: 'https://maas-api.lanyun.net'
},
websites: {
official: 'https://lanyun.net',
apiKey: 'https://maas.lanyun.net/api/#/system/apiKey',
docs: 'https://archive.lanyun.net/maas/doc/',
models: 'https://maas.lanyun.net/api/#/model/modelSquare'
}
}
}

View File

@@ -4,7 +4,6 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
@@ -14,6 +13,8 @@ import { useEffect, useState } from 'react'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>()
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
@@ -57,46 +58,13 @@ export async function getTopicById(topicId: string) {
return { ...topic, messages } as Topic
}
/**
* 开始重命名指定话题
*/
export const startTopicRenaming = (topicId: string) => {
const currentIds = store.getState().runtime.chat.renamingTopics
if (!currentIds.includes(topicId)) {
store.dispatch(setRenamingTopics([...currentIds, topicId]))
}
}
/**
* 完成重命名指定话题
*/
export const finishTopicRenaming = (topicId: string) => {
const state = store.getState()
// 1. 立即从 renamingTopics 移除
const currentRenaming = state.runtime.chat.renamingTopics
store.dispatch(setRenamingTopics(currentRenaming.filter((id) => id !== topicId)))
// 2. 立即添加到 newlyRenamedTopics
const currentNewlyRenamed = state.runtime.chat.newlyRenamedTopics
store.dispatch(setNewlyRenamedTopics([...currentNewlyRenamed, topicId]))
// 3. 延迟从 newlyRenamedTopics 移除
setTimeout(() => {
const current = store.getState().runtime.chat.newlyRenamedTopics
store.dispatch(setNewlyRenamedTopics(current.filter((id) => id !== topicId)))
}, 700)
}
const topicRenamingLocks = new Set<string>()
export const autoRenameTopic = async (assistant: Assistant, topicId: string) => {
if (topicRenamingLocks.has(topicId)) {
if (renamingTopics.has(topicId)) {
return
}
try {
topicRenamingLocks.add(topicId)
renamingTopics.add(topicId)
const topic = await getTopicById(topicId)
const enableTopicNaming = getStoreSetting('enableTopicNaming')
@@ -117,36 +85,24 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.join('\n\n')
.substring(0, 50)
if (topicName) {
try {
startTopicRenaming(topicId)
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} finally {
finishTopicRenaming(topicId)
}
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
return
}
if (topic && topic.name === i18n.t('chat.default.topic.name') && topic.messages.length >= 2) {
try {
startTopicRenaming(topicId)
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
} finally {
finishTopicRenaming(topicId)
const { fetchMessagesSummary } = await import('@renderer/services/ApiService')
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
}
} finally {
topicRenamingLocks.delete(topicId)
renamingTopics.delete(topicId)
}
}
@@ -161,18 +117,9 @@ export const TopicManager = {
return await db.topics.toArray()
},
/**
* 加载并返回指定话题的消息
*/
async getTopicMessages(id: string) {
const topic = await TopicManager.getTopic(id)
if (!topic) return []
await store.dispatch(loadTopicMessagesThunk(id))
// 获取更新后的话题
const updatedTopic = await TopicManager.getTopic(id)
return updatedTopic?.messages || []
return topic ? topic.messages : []
},
async removeTopic(id: string) {

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Enter prompt",
"add.prompt.variables.tip": {
"title": "Available variables",
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name\n{{username}}:\tUsername"
"content": "{{date}}:\tDate\n{{time}}:\tTime\n{{datetime}}:\tDate and time\n{{system}}:\tOperating system\n{{arch}}:\tCPU architecture\n{{language}}:\tLanguage\n{{model_name}}:\tModel name"
},
"add.title": "Create Agent",
"import": {
@@ -1020,8 +1020,7 @@
"voyageai": "Voyage AI",
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
"302ai": "302.AI"
},
"restore": {
"confirm": "Are you sure you want to restore data?",
@@ -1963,7 +1962,6 @@
},
"actions": {
"title": "Actions",
"custom": "Custom Action",
"reset": {
"button": "Reset",
"tooltip": "Reset to default actions. Custom actions will not be deleted.",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "プロンプトを入力",
"add.prompt.variables.tip": {
"title": "利用可能な変数",
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名\n{{username}}:\tユーザー名"
"content": "{{date}}:\t日付\n{{time}}:\t時間\n{{datetime}}:\t日付と時間\n{{system}}:\tオペレーティングシステム\n{{arch}}:\tCPUアーキテクチャ\n{{language}}:\t言語\n{{model_name}}:\tモデル名"
},
"add.title": "エージェントを作成",
"import": {
@@ -1020,8 +1020,7 @@
"qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"cephalon": "Cephalon",
"lanyun": "LANYUN"
"cephalon": "Cephalon"
},
"restore": {
"confirm": "データを復元しますか?",
@@ -1963,7 +1962,6 @@
},
"actions": {
"title": "機能設定",
"custom": "カスタム機能",
"reset": {
"button": "リセット",
"tooltip": "デフォルト機能にリセット(カスタム機能は保持)",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Введите промпт",
"add.prompt.variables.tip": {
"title": "Доступные переменные",
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели\n{{username}}:\tИмя пользователя"
"content": "{{date}}:\tДата\n{{time}}:\tВремя\n{{datetime}}:\tДата и время\n{{system}}:\tОперационная система\n{{arch}}:\tАрхитектура процессора\n{{language}}:\tЯзык\n{{model_name}}:\tНазвание модели"
},
"add.title": "Создать агента",
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
@@ -1020,8 +1020,7 @@
"voyageai": "Voyage AI",
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
"302ai": "302.AI"
},
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
@@ -1963,7 +1962,6 @@
},
"actions": {
"title": "Действия",
"custom": "Пользовательское действие",
"reset": {
"button": "Сбросить",
"tooltip": "Сбросить стандартные действия. Пользовательские останутся.",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "输入提示词",
"add.prompt.variables.tip": {
"title": "可用的变量",
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称\n{{username}}:\t用户名"
"content": "{{date}}:\t日期\n{{time}}:\t时间\n{{datetime}}:\t日期和时间\n{{system}}:\t操作系统\n{{arch}}:\tCPU架构\n{{language}}:\t语言\n{{model_name}}:\t模型名称"
},
"add.title": "创建智能体",
"import": {
@@ -1020,8 +1020,7 @@
"voyageai": "Voyage AI",
"qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "蓝耘科技"
"302ai": "302.AI"
},
"restore": {
"confirm": "确定要恢复数据吗?",
@@ -1928,7 +1927,7 @@
"selected": "划词",
"selected_note": "划词后立即显示工具栏",
"ctrlkey": "Ctrl 键",
"ctrlkey_note": "划词后,再 按 Ctrl键才显示工具栏",
"ctrlkey_note": "划词后,再 按 Ctrl键才显示工具栏",
"shortcut": "快捷键",
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
"shortcut_link": "前往快捷键设置"
@@ -1963,7 +1962,6 @@
},
"actions": {
"title": "功能",
"custom": "自定义功能",
"reset": {
"button": "重置",
"tooltip": "重置为默认功能,自定义功能不会被删除",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "輸入提示詞",
"add.prompt.variables.tip": {
"title": "可用的變數",
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱\n{{username}}:\t使用者名稱"
"content": "{{date}}:\t日期\n{{time}}:\t時間\n{{datetime}}:\t日期和時間\n{{system}}:\t作業系統\n{{arch}}:\tCPU架構\n{{language}}:\t語言\n{{model_name}}:\t模型名稱"
},
"add.title": "建立智慧代理人",
"import": {
@@ -1020,8 +1020,7 @@
"voyageai": "Voyage AI",
"qiniu": "七牛雲 AI 推理",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "藍耘"
"302ai": "302.AI"
},
"restore": {
"confirm": "確定要復原資料嗎?",
@@ -1963,7 +1962,6 @@
},
"actions": {
"title": "功能",
"custom": "自訂功能",
"reset": {
"button": "重設",
"tooltip": "重設為預設功能,自訂功能不會被刪除",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Εισαγάγετε φράση προκαλέσεως",
"add.prompt.variables.tip": {
"title": "Διαθέσιμες μεταβλητές",
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου\n{{username}}:\tΌνομα χρήστη"
"content": "{{date}}:\tΗμερομηνία\n{{time}}:\tΏρα\n{{datetime}}:\tΗμερομηνία και ώρα\n{{system}}:\tΛειτουργικό σύστημα\n{{arch}}:\tΑρχιτεκτονική CPU\n{{language}}:\tΓλώσσα\n{{model_name}}:\tΌνομα μοντέλου"
},
"add.title": "Δημιουργία νέου ειδικού",
"delete.popup.content": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Ingrese la palabra clave",
"add.prompt.variables.tip": {
"title": "Variables disponibles",
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo\n{{username}}:\tNombre de usuario"
"content": "{{date}}:\tFecha\n{{time}}:\tHora\n{{datetime}}:\tFecha y hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitectura de CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNombre del modelo"
},
"add.title": "Crear agente inteligente",
"delete.popup.content": "¿Está seguro de que desea eliminar este agente inteligente?",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Entrer le mot-clé",
"add.prompt.variables.tip": {
"title": "Variables disponibles",
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle\n{{username}}:\tNom d'utilisateur"
"content": "{{date}}:\tDate\n{{time}}:\tHeure\n{{datetime}}:\tDate et heure\n{{system}}:\tSystème d'exploitation\n{{arch}}:\tArchitecture du processeur\n{{language}}:\tLangue\n{{model_name}}:\tNom du modèle"
},
"add.title": "Créer un agent intelligent",
"delete.popup.content": "Êtes-vous sûr de vouloir supprimer cet agent intelligent ?",

View File

@@ -10,7 +10,7 @@
"add.prompt.placeholder": "Digite o Prompt",
"add.prompt.variables.tip": {
"title": "Variáveis disponíveis",
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo\n{{username}}:\tNome de utilizador"
"content": "{{date}}:\tData\n{{time}}:\tHora\n{{datetime}}:\tData e hora\n{{system}}:\tSistema operativo\n{{arch}}:\tArquitetura da CPU\n{{language}}:\tIdioma\n{{model_name}}:\tNome do modelo"
},
"add.title": "Criar Agente Inteligente",
"delete.popup.content": "Tem certeza de que deseja excluir este agente inteligente?",

View File

@@ -190,16 +190,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
)
}
const assistantWithTopicPrompt = topic.prompt
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
: assistant
if (topic.prompt) {
assistant.prompt = assistant.prompt ? `${assistant.prompt}\n${topic.prompt}` : topic.prompt
}
baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
const { message, blocks } = getUserMessage(baseUserMessage)
currentMessageId.current = message.id
dispatch(_sendMessage(message, blocks, assistantWithTopicPrompt, topic.id))
dispatch(_sendMessage(message, blocks, assistant, topic.id))
// Clear input
setText('')

View File

@@ -80,17 +80,14 @@ const MessageItem: FC<Props> = ({
const handleEditResend = useCallback(
async (blocks: MessageBlock[]) => {
const assistantWithTopicPrompt = topic.prompt
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
: assistant
try {
await resendUserMessageWithEdit(message, blocks, assistantWithTopicPrompt)
await resendUserMessageWithEdit(message, blocks, assistant)
stopEditing()
} catch (error) {
console.error('Failed to resend message:', error)
}
},
[message, resendUserMessageWithEdit, assistant, stopEditing, topic.prompt]
[message, resendUserMessageWithEdit, assistant, stopEditing]
)
const handleEditCancel = useCallback(() => {

View File

@@ -15,7 +15,6 @@ import type { Model } from '@renderer/types'
import type { Assistant, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { captureScrollableDivAsBlob, captureScrollableDivAsDataURL } from '@renderer/utils'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
import {
exportMarkdownToJoplin,
exportMarkdownToSiyuan,
@@ -24,6 +23,7 @@ import {
exportMessageToNotion,
messageToMarkdown
} from '@renderer/utils/export'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
// import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
@@ -124,13 +124,10 @@ const MessageMenubar: FC<Props> = (props) => {
const handleResendUserMessage = useCallback(
async (messageUpdate?: Message) => {
if (!loading) {
const assistantWithTopicPrompt = topic.prompt
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
: assistant
await resendMessage(messageUpdate ?? message, assistantWithTopicPrompt)
await resendMessage(messageUpdate ?? message, assistant)
}
},
[assistant, loading, message, resendMessage, topic.prompt]
[assistant, loading, message, resendMessage]
)
const { startEditing } = useMessageEditing()
@@ -319,12 +316,8 @@ const MessageMenubar: FC<Props> = (props) => {
// const _message = resetAssistantMessage(message, selectedModel)
// editMessage(message.id, { ..._message }) // REMOVED
const assistantWithTopicPrompt = topic.prompt
? { ...assistant, prompt: `${assistant.prompt}\n${topic.prompt}` }
: assistant
// Call the function from the hook
regenerateAssistantMessage(message, assistantWithTopicPrompt)
regenerateAssistantMessage(message, assistant)
}
const onMentionModel = async (e: React.MouseEvent) => {
@@ -406,8 +399,7 @@ const MessageMenubar: FC<Props> = (props) => {
menu={{
style: {
maxHeight: 250,
overflowY: 'auto',
backgroundClip: 'border-box'
overflowY: 'auto'
},
items: [
...TranslateLanguageOptions.map((item) => ({

View File

@@ -53,17 +53,15 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
)
return (
showTokens && (
<MessageMetadata className="message-tokens" onClick={locateMessage}>
{hasMetrics ? (
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
{tokensInfo}
</Popover>
) : (
tokensInfo
)}
</MessageMetadata>
)
<MessageMetadata className="message-tokens" onClick={locateMessage}>
{hasMetrics ? (
<Popover content={metrixs} placement="top" trigger="hover" styles={{ root: { fontSize: 11 } }}>
{showTokens && tokensInfo}
</Popover>
) : (
tokensInfo
)}
</MessageMetadata>
)
}

View File

@@ -18,7 +18,7 @@ import { isMac } from '@renderer/config/constant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
import { TopicManager } from '@renderer/hooks/useTopic'
import { fetchMessagesSummary } from '@renderer/services/ApiService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store from '@renderer/store'
@@ -57,9 +57,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
const renamingTopics = useSelector((state: RootState) => state.runtime.chat.renamingTopics)
const newlyRenamedTopics = useSelector((state: RootState) => state.runtime.chat.newlyRenamedTopics)
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
@@ -87,20 +84,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
[activeTopic.id, pendingTopics]
)
const isRenaming = useCallback(
(topicId: string) => {
return renamingTopics.includes(topicId)
},
[renamingTopics]
)
const isNewlyRenamed = useCallback(
(topicId: string) => {
return newlyRenamedTopics.includes(topicId)
},
[newlyRenamedTopics]
)
const handleDeleteClick = useCallback((topicId: string, e: React.MouseEvent) => {
e.stopPropagation()
@@ -187,22 +170,16 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.auto_rename'),
key: 'auto-rename',
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
disabled: isRenaming(topic.id),
async onClick() {
const messages = await TopicManager.getTopicMessages(topic.id)
if (messages.length >= 2) {
startTopicRenaming(topic.id)
try {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
updateTopic(updatedTopic)
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
} else {
window.message?.error(t('message.error.fetchTopicName'))
}
} finally {
finishTopicRenaming(topic.id)
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
const updatedTopic = { ...topic, name: summaryText, isNameManuallyEdited: false }
updateTopic(updatedTopic)
topic.id === activeTopic.id && setActiveTopic(updatedTopic)
} else {
window.message?.error(t('message.error.fetchTopicName'))
}
}
}
@@ -211,7 +188,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.edit.title'),
key: 'rename',
icon: <EditOutlined />,
disabled: isRenaming(topic.id),
async onClick() {
const name = await PromptPopup.show({
title: t('chat.topics.edit.title'),
@@ -412,7 +388,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
}, [
targetTopic,
t,
isRenaming,
exportMenuOptions.image,
exportMenuOptions.markdown,
exportMenuOptions.markdown_reason,
@@ -455,13 +430,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const topicName = topic.name.replace('`', '')
const topicPrompt = topic.prompt
const fullTopicPrompt = t('common.prompt') + ': ' + topicPrompt
const getTopicNameClassName = () => {
if (isRenaming(topic.id)) return 'shimmer'
if (isNewlyRenamed(topic.id)) return 'typing'
return ''
}
return (
<TopicListItem
onContextMenu={() => setTargetTopic(topic)}
@@ -470,7 +438,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
style={{ borderRadius }}>
{isPending(topic.id) && !isActive && <PendingIndicator />}
<TopicNameContainer>
<TopicName className={getTopicNameClassName()} title={topicName}>
<TopicName className="name" title={topicName}>
{topicName}
</TopicName>
{isActive && !topic.pinned && (
@@ -576,46 +544,6 @@ const TopicName = styled.div`
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 13px;
position: relative;
will-change: background-position, width;
--color-shimmer-mid: var(--color-text-1);
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
&.shimmer {
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: shimmer 3s linear infinite;
}
&.typing {
display: block;
-webkit-line-clamp: unset;
-webkit-box-orient: unset;
white-space: nowrap;
overflow: hidden;
animation: typewriter 0.5s steps(40, end);
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes typewriter {
from {
width: 0;
}
to {
width: 100%;
}
}
`
const PendingIndicator = styled.div.attrs({

View File

@@ -1,6 +1,5 @@
import { SyncOutlined } from '@ant-design/icons'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
@@ -8,14 +7,13 @@ import styled from 'styled-components'
const UpdateAppButton: FC = () => {
const { update } = useRuntime()
const { autoCheckUpdate } = useSettings()
const { t } = useTranslation()
if (!update) {
return null
}
if (!update.downloaded || !autoCheckUpdate) {
if (!update.downloaded) {
return null
}

View File

@@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { getModelScopeToken, saveModelScopeToken, syncModelScopeServers } from './modelscopeSyncUtils'
import { getTokenLanYunToken, LANYUN_KEY_HOST, saveTokenLanYunToken, syncTokenLanYunServers } from './providers/lanyun'
import { getTokenFluxToken, saveTokenFluxToken, syncTokenFluxServers, TOKENFLUX_HOST } from './providers/tokenflux'
// Provider configuration interface
@@ -46,17 +45,6 @@ const providers: ProviderConfig[] = [
getToken: getTokenFluxToken,
saveToken: saveTokenFluxToken,
syncServers: syncTokenFluxServers
},
{
key: 'lanyun',
name: '蓝耘科技',
description: '蓝耘科技云平台 MCP 服务',
discoverUrl: 'https://mcp.lanyun.net',
apiKeyUrl: LANYUN_KEY_HOST,
tokenFieldName: 'tokenLanyunToken',
getToken: getTokenLanYunToken,
saveToken: saveTokenLanYunToken,
syncServers: syncTokenLanYunServers
}
]

View File

@@ -1,178 +0,0 @@
import type { MCPServer } from '@renderer/types'
import i18next from 'i18next'
// Token storage constants and utilities
const TOKEN_STORAGE_KEY = 'tokenLanyunToken'
export const TOKENLANYUN_HOST = 'https://mcp.lanyun.net'
export const LANYUN_MCP_HOST = TOKENLANYUN_HOST + '/mcp/manager/selectListByApiKey'
export const LANYUN_KEY_HOST = TOKENLANYUN_HOST + '/#/manage/apiKey'
export const saveTokenLanYunToken = (token: string): void => {
localStorage.setItem(TOKEN_STORAGE_KEY, token)
}
export const getTokenLanYunToken = (): string | null => {
return localStorage.getItem(TOKEN_STORAGE_KEY)
}
export const clearTokenLanYunToken = (): void => {
localStorage.removeItem(TOKEN_STORAGE_KEY)
}
export const hasTokenLanYunToken = (): boolean => {
return !!getTokenLanYunToken()
}
interface TokenLanYunServer {
id: string
/**
* locales 字段用于存储多语言信息。
* 其中 keylang为语言代码如 'zh', 'en'
* value 为该语言下的 name 和 description。
* 例如:
* {
* "zh": { name: "文档处理工具", description: "..." },
* "en": { name: "Document Processor", description: "..." }
* }
*/
locales?: {
[lang: string]: {
description?: string
name?: string
}
}
chineseName?: string
description?: string
operationalUrls?: { url: string }[]
tags?: string[]
logoUrl?: string
}
interface TokenLanYunSyncResult {
success: boolean
message: string
addedServers: MCPServer[]
errorDetails?: string
}
// Function to fetch and process TokenLanYun servers
export const syncTokenLanYunServers = async (
token: string,
existingServers: MCPServer[]
): Promise<TokenLanYunSyncResult> => {
const t = i18next.t
try {
const response = await fetch(LANYUN_MCP_HOST, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
// Handle authentication errors
if (response.status === 401 || response.status === 403) {
clearTokenLanYunToken()
return {
success: false,
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
addedServers: []
}
}
// Handle server errors
if (response.status === 500 || !response.ok) {
return {
success: false,
message: t('settings.mcp.sync.error'),
addedServers: [],
errorDetails: `Status: ${response.status}`
}
}
// Process successful response
const data = await response.json()
if (data.code === 401) {
return {
success: false,
message: t('settings.mcp.sync.unauthorized', 'Sync Unauthorized'),
addedServers: [],
errorDetails: `Status: ${response.status}`
}
}
if (data.code === 500) {
return {
success: false,
message: t('settings.mcp.sync.error'),
addedServers: [],
errorDetails: `Status: ${response.status}`
}
}
const servers: TokenLanYunServer[] = data.data || []
if (servers.length === 0) {
return {
success: true,
message: t('settings.mcp.sync.noServersAvailable', 'No MCP servers available'),
addedServers: []
}
}
// Transform Token servers to MCP servers format
const addedServers: MCPServer[] = []
console.log('TokenLanYun servers:', servers)
for (const server of servers) {
try {
if (!server.operationalUrls?.[0]?.url) continue
// If any existing server id contains '@lanyun', clear them before adding new ones
// if (existingServers.some((s) => s.id.startsWith('@lanyun'))) {
// for (let i = existingServers.length - 1; i >= 0; i--) {
// if (existingServers[i].id.startsWith('@lanyun')) {
// existingServers.splice(i, 1)
// }
// }
// }
// Skip if server already exists after clearing
if (existingServers.some((s) => s.id === `@lanyun/${server.id}`)) continue
const mcpServer: MCPServer = {
id: `@lanyun/${server.id}`,
name:
server.chineseName || server.locales?.zh?.name || server.locales?.en?.name || `LanYun Server ${server.id}`,
description: server.description || '',
type: 'sse',
baseUrl: server.operationalUrls[0].url,
command: '',
args: [],
env: {},
isActive: true,
provider: '蓝耘科技',
providerUrl: server.operationalUrls[0].url,
logoUrl: server.logoUrl || '',
tags: server.tags ?? (server.chineseName ? [server.chineseName] : [])
}
addedServers.push(mcpServer)
} catch (err) {
console.error('Error processing LanYun server:', err)
}
}
return {
success: true,
message: t('settings.mcp.sync.success', { count: addedServers.length }),
addedServers
}
} catch (error) {
console.error('TokenLanyun sync error:', error)
return {
success: false,
message: t('settings.mcp.sync.error'),
addedServers: [],
errorDetails: String(error)
}
}
}

View File

@@ -1,9 +1,8 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
import { Button, Divider, Flex, Input, Modal, Popover, Switch } from 'antd'
import { Button, Divider, Input, Modal, Switch } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -37,8 +36,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
TopicNamingModalPopup.hide = onCancel
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
return (
<Modal
title={t('settings.models.topic_naming_model_setting_title')}
@@ -56,12 +53,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
</HStack>
<Divider style={{ margin: '10px 0' }} />
<div style={{ marginBottom: 20 }}>
<Flex align="center" style={{ marginBottom: 10, gap: 5 }}>
<div>{t('settings.models.topic_naming_prompt')}</div>
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
<QuestionCircleOutlined size={14} style={{ color: 'var(--color-text-2)' }} />
</Popover>
</Flex>
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
<Input.TextArea
rows={4}
value={topicNamingPrompt || t('prompts.title')}

View File

@@ -32,14 +32,7 @@ const SettingsActionsListHeader = memo(({ customItemsCount, maxCustomItems, onRe
? t('selection.settings.actions.add_tooltip.disabled', { max: maxCustomItems })
: t('selection.settings.actions.add_tooltip.enabled')
}>
<Button
type="primary"
icon={<Plus size={16} />}
onClick={onAdd}
disabled={isCustomItemLimitReached}
style={{ paddingInline: '8px' }}>
{t('selection.settings.actions.custom')}
</Button>
<Button type="primary" icon={<Plus size={16} />} onClick={onAdd} disabled={isCustomItemLimitReached} />
</Tooltip>
</Row>
)

View File

@@ -146,16 +146,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'lanyun',
name: 'LANYUN',
type: 'openai',
apiKey: '',
apiHost: 'https://maas-api.lanyun.net',
models: SYSTEM_MODELS.lanyun,
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',

View File

@@ -1563,10 +1563,8 @@ const migrateConfig = {
try {
addProvider(state, 'cephalon')
addProvider(state, '302ai')
addProvider(state, 'lanyun')
state.llm.providers = moveProvider(state.llm.providers, 'cephalon', 13)
state.llm.providers = moveProvider(state.llm.providers, '302ai', 14)
state.llm.providers = moveProvider(state.llm.providers, 'lanyun', 15)
return state
} catch (error) {
return state

View File

@@ -7,10 +7,6 @@ export interface ChatState {
isMultiSelectMode: boolean
selectedMessageIds: string[]
activeTopic: Topic | null
/** topic ids that are currently being renamed */
renamingTopics: string[]
/** topic ids that are newly renamed */
newlyRenamedTopics: string[]
}
export interface UpdateState {
@@ -69,9 +65,7 @@ const initialState: RuntimeState = {
chat: {
isMultiSelectMode: false,
selectedMessageIds: [],
activeTopic: null,
renamingTopics: [],
newlyRenamedTopics: []
activeTopic: null
}
}
@@ -124,12 +118,6 @@ const runtimeSlice = createSlice({
},
setActiveTopic: (state, action: PayloadAction<Topic>) => {
state.chat.activeTopic = action.payload
},
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
state.chat.renamingTopics = action.payload
},
setNewlyRenamedTopics: (state, action: PayloadAction<string[]>) => {
state.chat.newlyRenamedTopics = action.payload
}
}
})
@@ -149,9 +137,7 @@ export const {
// Chat related actions
toggleMultiSelectMode,
setSelectedMessageIds,
setActiveTopic,
setRenamingTopics,
setNewlyRenamedTopics
setActiveTopic
} = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@@ -204,16 +204,6 @@ export const buildSystemPrompt = async (userSystemPrompt: string, tools?: MCPToo
userSystemPrompt = userSystemPrompt.replace(/{{model_name}}/g, 'Unknown Model')
}
}
if (userSystemPrompt.includes('{{username}}')) {
try {
const username = store.getState().settings.userName || 'Unknown Username'
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, username)
} catch (error) {
console.error('Failed to get username:', error)
userSystemPrompt = userSystemPrompt.replace(/{{username}}/g, 'Unknown Username')
}
}
}
if (tools && tools.length > 0) {