Skip to content

Commit 2dff87e

Browse files
committed
新增太多东西了懒得写
1 parent 67d143d commit 2dff87e

34 files changed

Lines changed: 4054 additions & 66 deletions

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,149 @@ TChat的作者是大陆人,交流最好用简体中文,如果不会使用那
2323
- 侧边栏聊天记录导航
2424
- 流式聊天回复
2525
- Material 3 UI 搭配 Jetpack Compose
26+
- 知识库(RAG)功能
27+
28+
29+
30+
# v1.3
31+
32+
### 新增功能
33+
34+
- **知识库(RAG)功能**
35+
- 支持创建和管理多个知识库
36+
- 内容导入支持:文本笔记、URL网页抓取、文件上传(TXT/MD)
37+
- 向量嵌入生成与相似度检索
38+
- 支持 OpenAI 和 Gemini 的 Embedding API
39+
40+
- **知识库管理**
41+
- 创建/编辑/删除知识库
42+
- 选择 Embedding 服务商和模型
43+
- 批量处理待处理条目
44+
- 处理状态显示(待处理/处理中/已完成/失败)
45+
46+
- **知识条目管理**
47+
- Tab 切换查看(全部/文件/笔记/URL)
48+
- 添加/编辑/删除条目
49+
- 单独处理或批量处理
50+
- 语义搜索功能
51+
52+
- **设置入口**
53+
- 设置页面新增"知识库"入口
54+
- 位于"通用"分组下
55+
56+
- **工具调用参数保存**
57+
- 保存完整的工具调用参数(JSON 格式)
58+
- 记录工具执行耗时(毫秒级)
59+
- 持久化存储到数据库,重载对话可查看历史调用
60+
61+
- **工具调用 UI 改进**
62+
- 全新的工具调用卡片设计
63+
- 显示工具名称、参数摘要、执行时间
64+
- 点击展开查看完整的输入参数和执行结果
65+
- 格式化 JSON 显示,更易读
66+
- 成功/失败状态图标区分
67+
68+
- **错误处理优化**
69+
- 安全处理无参数工具调用
70+
- 兼容旧版本损坏数据,提供友好提示
71+
- JSON 解析失败时显示友好错误信息
72+
73+
---
74+
75+
### 技术改进详情
76+
77+
#### 1. Embedding API 支持
78+
79+
**功能**:支持 OpenAI 和 Gemini 的向量嵌入 API
80+
81+
**实现**
82+
- `EmbeddingProvider` 接口定义嵌入操作
83+
- `OpenAIEmbeddingProvider` 调用 `/embeddings` 端点
84+
- `GeminiEmbeddingProvider` 调用 `embedContent` 端点
85+
- 支持批量嵌入处理
86+
87+
---
88+
89+
#### 2. 知识库数据层
90+
91+
**功能**:完整的知识库数据管理
92+
93+
**实现**
94+
- `KnowledgeRepository` 接口和实现
95+
- `KnowledgeService` 处理内容加载、分块、向量化
96+
- 文档加载器:`TextLoader``UrlLoader``FileLoader`
97+
- 数据库版本 6 → 7 迁移,添加 status/errorMessage 字段
98+
99+
---
100+
101+
#### 3. 向量检索
102+
103+
**功能**:基于余弦相似度的语义搜索
104+
105+
**实现**
106+
- 文本分块(按段落,支持重叠)
107+
- 向量存储为 JSON 格式
108+
- 余弦相似度计算
109+
- Top-K 结果返回,支持阈值过滤
110+
111+
---
112+
113+
#### 4. ToolResultData 模型扩展
114+
115+
**修改**
116+
- 新增 `arguments` 字段存储工具调用参数
117+
- 新增 `executionTimeMs` 字段记录执行耗时
118+
- JSON 序列化/反序列化支持新字段
119+
120+
---
121+
122+
#### 5. 无参数工具调用修复
123+
124+
**问题**:Gemini 等 API 返回无参数工具调用时,arguments 为空字符串导致解析失败
125+
126+
**修复**
127+
- 执行前检查 `toolCall.arguments.ifBlank { "{}" }`
128+
- 确保空参数被转换为有效的空 JSON 对象
129+
130+
---
131+
132+
#### 6. 旧数据兼容处理
133+
134+
**功能**:检测并处理之前保存的损坏数据
135+
136+
**实现**
137+
- 加载时检测 "End of input at character 0" 错误
138+
- 对损坏数据显示友好提示
139+
- 自动修正空参数字段
140+
141+
---
142+
143+
### 涉及文件
144+
145+
| 模块 | 文件 | 修改 |
146+
|------|------|------|
147+
| network | EmbeddingProvider.kt | Embedding 接口定义 |
148+
| network | OpenAIEmbeddingProvider.kt | OpenAI Embedding 实现 |
149+
| network | GeminiEmbeddingProvider.kt | Gemini Embedding 实现 |
150+
| network | EmbeddingProviderFactory.kt | Embedding 工厂类 |
151+
| data | KnowledgeItemEntity.kt | 添加 status/errorMessage 字段 |
152+
| data | KnowledgeRepository.kt | 知识库 Repository 接口 |
153+
| data | KnowledgeRepositoryImpl.kt | Repository 实现 |
154+
| data | KnowledgeService.kt | 知识库核心服务 |
155+
| data | DocumentLoader.kt | 文档加载器接口 |
156+
| data | TextLoader.kt | 文本加载器 |
157+
| data | UrlLoader.kt | URL 网页加载器 |
158+
| data | FileLoader.kt | 文件加载器 |
159+
| data | AppDatabase.kt | 版本 7,status 字段迁移 |
160+
| data | Message.kt | ToolResultData 添加 arguments、executionTimeMs 字段 |
161+
| data | ChatRepositoryImpl.kt | 工具执行记录参数和耗时,旧数据兼容处理 |
162+
| app | KnowledgeViewModel.kt | 知识库 ViewModel |
163+
| app | KnowledgeScreen.kt | 知识库列表页面 |
164+
| app | KnowledgeDetailScreen.kt | 知识库详情页面 |
165+
| app | SettingsScreen.kt | 添加知识库入口 |
166+
| feature-chat | MessageItem.kt | 新工具卡片 UI,安全 JSON 解析 |
167+
168+
---
26169

27170

28171

app/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ android {
1212
applicationId = "com.tchat.wanxiaot"
1313
minSdk = 26
1414
targetSdk = 36
15-
versionCode = 2
16-
versionName = "1.2"
15+
versionCode = 3
16+
versionName = "1.3"
1717

1818
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1919
}
@@ -33,6 +33,7 @@ android {
3333
}
3434
buildFeatures {
3535
compose = true
36+
buildConfig = true
3637
}
3738
}
3839

app/src/main/java/com/tchat/wanxiaot/MainActivity.kt

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ import com.tchat.data.database.entity.AssistantEntity
2929
import com.tchat.data.model.Assistant
3030
import com.tchat.data.model.LocalToolOption
3131
import com.tchat.data.repository.impl.ChatRepositoryImpl
32+
import com.tchat.data.repository.impl.KnowledgeRepositoryImpl
33+
import com.tchat.data.service.KnowledgeService
34+
import com.tchat.data.tool.KnowledgeSearchTool
3235
import com.tchat.data.tool.LocalTools
36+
import com.tchat.data.tool.Tool
3337
import com.tchat.feature.chat.ChatScreen
3438
import com.tchat.feature.chat.ChatViewModel
3539
import com.tchat.network.provider.AIProviderFactory
40+
import com.tchat.network.provider.EmbeddingProviderFactory
3641
import com.tchat.wanxiaot.settings.AIProviderType
3742
import com.tchat.wanxiaot.settings.SettingsManager
3843
import com.tchat.wanxiaot.ui.DrawerContent
@@ -133,6 +138,18 @@ fun MainScreen(
133138
val messageDao = database.messageDao()
134139
val assistantDao = database.assistantDao()
135140

141+
// 知识库相关
142+
val knowledgeRepository = remember(database) {
143+
KnowledgeRepositoryImpl(
144+
database.knowledgeBaseDao(),
145+
database.knowledgeItemDao(),
146+
database.knowledgeChunkDao()
147+
)
148+
}
149+
val knowledgeService = remember(knowledgeRepository) {
150+
KnowledgeService(knowledgeRepository)
151+
}
152+
136153
// null表示新对话(懒创建模式)
137154
var currentChatId by remember { mutableStateOf<String?>(null) }
138155
var chatList by remember { mutableStateOf(emptyList<com.tchat.data.model.Chat>()) }
@@ -349,6 +366,33 @@ fun MainScreen(
349366
LocalTools(context)
350367
}
351368

369+
// 计算知识库搜索工具(当 currentAssistant 变化时重新计算)
370+
val knowledgeTools = remember(currentAssistant?.knowledgeBaseId, knowledgeService, knowledgeRepository) {
371+
currentAssistant?.knowledgeBaseId?.let { kbId ->
372+
println("=== 知识库工具已启用 ===")
373+
println("知识库ID: $kbId")
374+
println("助手: ${currentAssistant?.name}")
375+
listOf(
376+
KnowledgeSearchTool.create(
377+
knowledgeService = knowledgeService,
378+
repository = knowledgeRepository,
379+
getEmbeddingProvider = { knowledgeBaseId ->
380+
// 从知识库配置获取 Embedding Provider
381+
getEmbeddingProviderForKnowledgeBase(
382+
knowledgeBaseId = knowledgeBaseId,
383+
knowledgeRepository = knowledgeRepository,
384+
settingsManager = settingsManager
385+
)
386+
},
387+
knowledgeBaseId = kbId
388+
)
389+
)
390+
} ?: run {
391+
println("=== 未绑定知识库 ===")
392+
emptyList()
393+
}
394+
}
395+
352396
ChatScreen(
353397
viewModel = viewModel,
354398
chatId = currentChatId,
@@ -372,6 +416,8 @@ fun MainScreen(
372416
getToolsForOptions = { options ->
373417
localTools.getToolsForOptions(options)
374418
},
419+
// 知识库搜索工具作为额外工具传递
420+
extraTools = knowledgeTools,
375421
systemPrompt = currentAssistant?.systemPrompt
376422
)
377423
}
@@ -435,7 +481,46 @@ private fun entityToAssistant(entity: AssistantEntity): Assistant {
435481
contextMessageSize = entity.contextMessageSize,
436482
streamOutput = entity.streamOutput,
437483
localTools = toolOptions,
484+
knowledgeBaseId = entity.knowledgeBaseId,
438485
createdAt = entity.createdAt,
439486
updatedAt = entity.updatedAt
440487
)
441488
}
489+
490+
/**
491+
* 根据知识库配置获取对应的 Embedding Provider
492+
* 知识库使用自己配置的 Embedding 服务商,与对话模型提供商独立
493+
*/
494+
private fun getEmbeddingProviderForKnowledgeBase(
495+
knowledgeBaseId: String,
496+
knowledgeRepository: KnowledgeRepositoryImpl,
497+
settingsManager: SettingsManager
498+
): com.tchat.network.provider.EmbeddingProvider? {
499+
return try {
500+
// 获取知识库配置(同步方式,因为我们在工具执行时需要)
501+
val base = kotlinx.coroutines.runBlocking {
502+
knowledgeRepository.getBaseById(knowledgeBaseId)
503+
} ?: return null
504+
505+
// 获取设置中的服务商配置
506+
val settings = settingsManager.settings.value
507+
val providerConfig = settings.providers.find { it.id == base.embeddingProviderId }
508+
?: return null
509+
510+
// 根据服务商类型创建 Embedding Provider
511+
val providerType = when (providerConfig.providerType) {
512+
AIProviderType.OPENAI -> EmbeddingProviderFactory.EmbeddingProviderType.OPENAI
513+
AIProviderType.GEMINI -> EmbeddingProviderFactory.EmbeddingProviderType.GEMINI
514+
else -> return null
515+
}
516+
517+
EmbeddingProviderFactory.create(
518+
type = providerType,
519+
apiKey = providerConfig.apiKey,
520+
baseUrl = providerConfig.endpoint.ifBlank { null }
521+
)
522+
} catch (e: Exception) {
523+
e.printStackTrace()
524+
null
525+
}
526+
}

0 commit comments

Comments
 (0)