Compare commits
7 Commits
v1.4.3
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c518c9090b | ||
|
|
91045ecc2b | ||
|
|
748ca008b4 | ||
|
|
6ad9044cd1 | ||
|
|
9e9a1ec024 | ||
|
|
a214dca6fa | ||
|
|
b142e5647e |
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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 }}"}'
|
||||
635
docs/technical/topic-message-tree.md
Normal file
635
docs/technical/topic-message-tree.md
Normal 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 << M,S通常较小 |
|
||||
| **发送消息** | 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` 或在相关组件中作为本地状态管理。 |
|
||||
BIN
docs/technical/topic-message-tree.png
Normal file
BIN
docs/technical/topic-message-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -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:实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -321,7 +321,6 @@ mjx-container {
|
||||
|
||||
.cm-gutters {
|
||||
line-height: 1.6;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.cm-content {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2169,8 +2169,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'DeepSeek-R1满血版',
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
lanyun: []
|
||||
]
|
||||
}
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS = [
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "デフォルト機能にリセット(カスタム機能は保持)",
|
||||
|
||||
@@ -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": "Сбросить стандартные действия. Пользовательские останутся.",
|
||||
|
||||
@@ -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": "重置为默认功能,自定义功能不会被删除",
|
||||
|
||||
@@ -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": "重設為預設功能,自訂功能不會被刪除",
|
||||
|
||||
@@ -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": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον ειδικό;",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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 ?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 字段用于存储多语言信息。
|
||||
* 其中 key(lang)为语言代码(如 '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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user