Skip to content

Commit da1fbe6

Browse files
committed
Version 1.0.4 is expected to be released.
1 parent 4052875 commit da1fbe6

277 files changed

Lines changed: 5226 additions & 193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
- 优化了代码高亮文本.
1111
- 使用[archify](https://github.com/tt-a1i/archify)对对话进行流程图总结输出.
1212
- 更为可靠的版本工作流.
13-
13+
- 知识库功能.
14+
- 优化了MCP服务器的载入和启动.
15+
- 优化了Agent自主生成文件默认在工作目录下.
16+
-
1417

1518
## en
1619
- Optimized the issue of potential stuttering during long conversations.

assets/default_skills/toolshell/tools.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
"type": "function",
2121
"function": {
2222
"name": "toolshell_write",
23-
"description": "写入文件。支持新建、覆盖、追加。自动创建父目录。",
23+
"description": "写入文件。支持新建、覆盖、追加。自动创建父目录。除非用户明确要求写到其他位置,否则默认使用相对于项目根目录的路径,不要自行拼接绝对路径。",
2424
"parameters": {
2525
"type": "object",
2626
"properties": {
27-
"path": {"type": "string", "description": "目标文件路径"},
27+
"path": {"type": "string", "description": "目标文件路径(相对于项目根目录;无特殊要求时不要使用绝对路径)"},
2828
"content": {"type": "string", "description": "写入内容"},
2929
"mode": {"type": "string", "enum": ["create", "overwrite", "append"]}
3030
},
@@ -40,7 +40,7 @@
4040
"parameters": {
4141
"type": "object",
4242
"properties": {
43-
"path": {"type": "string", "description": "目标路径"},
43+
"path": {"type": "string", "description": "目标路径(相对于项目根目录)"},
4444
"recursive": {"type": "boolean", "default": false}
4545
},
4646
"required": ["path"]

lib/app.dart

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import 'package:google_fonts/google_fonts.dart';
88
import 'package:tray_manager/tray_manager.dart';
99
import 'package:window_manager/window_manager.dart';
1010
import 'core/font/custom_font_loader.dart';
11+
import 'core/logger/app_logger.dart';
1112
import 'core/shortcuts/shortcut_config.dart';
1213
import 'l10n/app_localizations.dart';
1314
import 'core/l10n/l10n_ext.dart';
15+
import 'providers/mcp_provider.dart';
1416
import 'providers/settings_provider.dart';
1517
import 'providers/api_server_provider.dart';
1618
import 'features/online_service/online_service_provider.dart';
@@ -288,8 +290,14 @@ class _WindowWrapperState extends ConsumerState<_WindowWrapper>
288290
_toggleOnlineService();
289291
break;
290292
case 'exit':
291-
windowManager.setPreventClose(false);
292-
windowManager.close();
293+
// 这里先关闭 preventClose 再 close(),走的是"直接原生关闭"路径,
294+
// 不保证 onWindowClose 监听器一定会被触发去做清理,所以清理动作
295+
// 显式放在这里做一遍 (与 onWindowClose 里的清理逻辑重复调用是安全的,
296+
// disconnectAll 对已清空的状态是幂等操作)。
297+
_cleanupBeforeExit().whenComplete(() {
298+
windowManager.setPreventClose(false);
299+
windowManager.close();
300+
});
293301
break;
294302
}
295303
}
@@ -302,12 +310,29 @@ class _WindowWrapperState extends ConsumerState<_WindowWrapper>
302310
if (shouldMinimize) {
303311
await windowManager.hide();
304312
} else {
313+
await _cleanupBeforeExit();
305314
await trayManager.destroy();
306315
await windowManager.setPreventClose(false);
307316
await windowManager.close();
308317
}
309318
}
310319

320+
/// 真正退出前的收尾清理——目前主要是 MCP 连接:stdio 模式的子进程、
321+
/// SSE/HTTP 模式下应用代为拉起的本地进程,若不在这里显式断开,
322+
/// 单靠 Riverpod 容器 dispose 并不保证会在窗口关闭流程里被同步触发,
323+
/// 遗留下来会变成占着端口/常驻后台的僵尸进程。加超时兜底,避免某个
324+
/// 进程杀不掉时把整个退出流程卡住。
325+
Future<void> _cleanupBeforeExit() async {
326+
try {
327+
await ref
328+
.read(mcpConnectionsProvider.notifier)
329+
.disconnectAll()
330+
.timeout(const Duration(seconds: 5));
331+
} catch (e) {
332+
AppLogger.instance.log('[App] 退出前清理 MCP 连接异常(已忽略,继续退出): $e');
333+
}
334+
}
335+
311336
Future<bool> _showCloseDialog() async {
312337
final s = context.s;
313338
final result = await showDialog<bool>(

lib/core/agent/agent_context.dart

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import 'dart:io';
44
import 'package:flutter/services.dart';
55
import 'package:flutter_riverpod/flutter_riverpod.dart';
66
import 'package:path/path.dart' as p;
7-
import 'package:path_provider/path_provider.dart';
87

8+
import '../settings/app_settings.dart';
99
import '../db/daos/memory_dao.dart';
1010
import '../llm/llm_client.dart';
1111
import '../llm/llm_provider.dart';
@@ -23,6 +23,7 @@ import '../toolshell/executor.dart';
2323
import '../toolshell/worktree_manager.dart';
2424
import '../../providers/database_provider.dart';
2525
import '../../providers/experts_provider.dart';
26+
import '../../providers/kb_provider.dart';
2627
import '../../providers/mcp_provider.dart';
2728
import '../../providers/search_provider.dart';
2829
import '../../providers/settings_provider.dart';
@@ -35,6 +36,7 @@ import 'tool_middleware.dart';
3536
import 'tool_pipeline.dart';
3637
import 'hooks/memory_recall_hook.dart';
3738
import 'hooks/memory_store_hook.dart';
39+
import 'hooks/kb_recall_hook.dart';
3840
import 'hooks/schedule_hook.dart';
3941
import 'hooks/system_probe_hook.dart';
4042
import 'middleware/logging_middleware.dart';
@@ -129,8 +131,8 @@ class AgentContextBuilder {
129131
var workDir = _ref.read(workingDirectoryProvider);
130132
final hasRealWorkDir = workDir.isNotEmpty;
131133
if (workDir.isEmpty) {
132-
final documentsDir = await getApplicationDocumentsDirectory();
133-
workDir = p.join(documentsDir.path, '.RemindAI', 'workspace');
134+
final root = await AppSettings.getRootDir();
135+
workDir = p.join(root, 'workspace');
134136
}
135137

136138
// ─── Worktree 隔离: 若存在有效的活跃隔离工作树,Executor 的实际
@@ -253,7 +255,11 @@ class AgentContextBuilder {
253255
// ─── 系统提示词 ───
254256
// 拆成稳定前缀(专家+元技能)与用户技能区两段,
255257
// 便于会话中途技能变化时只刷新技能区,保护 prompt cache。
256-
final systemPromptPrefix = await _buildSystemPromptPrefix();
258+
// 传入 effectiveRoot(而非原始 workDir)确保 worktree 隔离生效时
259+
// 模型看到的"当前工作目录"与 Executor 实际使用的根目录一致。
260+
final systemPromptPrefix = await _buildSystemPromptPrefix(
261+
workDirOverride: effectiveRoot,
262+
);
257263
final skillsSection = await _buildSkillsSection(
258264
userInput: userInput,
259265
existingMessages: existingMessages,
@@ -307,13 +313,16 @@ class AgentContextBuilder {
307313

308314
// ─── Hooks ───
309315
final hasWorkDir = _ref.read(workingDirectoryProvider).isNotEmpty;
316+
final selectedKbIds = _ref.read(sessionKnowledgeBasesProvider);
310317
final hooks = <AgentHook>[
311318
if (effectiveRecall && memoryManager != null && memoryCollection != null)
312319
MemoryRecallHook(
313320
manager: memoryManager,
314321
collection: memoryCollection,
315322
useQdrant: useQdrant,
316323
),
324+
if (selectedKbIds.isNotEmpty)
325+
KbRecallHook(dao: _ref.read(kbDaoProvider), kbIds: selectedKbIds),
317326
if (effectiveStore && memoryManager != null && memoryCollection != null)
318327
MemoryStoreHook(
319328
manager: memoryManager,
@@ -645,9 +654,13 @@ class AgentContextBuilder {
645654

646655
/// 构建 system prompt 的稳定前缀:领域专家 + 元技能(或全局模式提示)。
647656
/// 注意:领域专家为一次性消费(读取后置空),仅在新会话首次构建时注入。
648-
Future<String> _buildSystemPromptPrefix() async {
657+
/// [workDirOverride] 实际生效的根目录(考虑 worktree 隔离重定向后的值)。
658+
/// 不传时回退到 workingDirectoryProvider 的原始值,用于外部无该上下文的调用场景。
659+
Future<String> _buildSystemPromptPrefix({String? workDirOverride}) async {
649660
final parts = <String>[];
650-
final hasWorkspace = _ref.read(workingDirectoryProvider).isNotEmpty;
661+
final String currentWorkDir =
662+
workDirOverride ?? _ref.read(workingDirectoryProvider);
663+
final hasWorkspace = currentWorkDir.isNotEmpty;
651664

652665
// 领域专家
653666
final activeExpert = _ref.read(activeExpertProvider);
@@ -658,6 +671,18 @@ class AgentContextBuilder {
658671
}
659672

660673
if (hasWorkspace) {
674+
// 显式把当前工作目录的绝对路径告知模型,作为 ground truth。
675+
// 没有这一行,模型在用户未明确指定路径时只能自行猜测一个绝对路径,
676+
// 而写入工具默认不做越界拦截(allowOutsideRoot=true),猜错就会
677+
// 把文件写到工作目录之外。
678+
parts.add(
679+
'# 当前工作目录\n'
680+
'$currentWorkDir\n\n'
681+
'所有文件操作工具(读取/写入/搜索/删除等)的相对路径均基于此目录解析。'
682+
'除非用户明确要求写到其他位置,否则新建、修改文件默认使用相对路径,'
683+
'不要自行拼接其他绝对路径。\n\n---\n',
684+
);
685+
661686
final tsPrompt = await rootBundle.loadString(
662687
'assets/default_skills/toolshell/SKILL.md',
663688
);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import 'package:flutter/foundation.dart';
2+
3+
import '../../db/daos/kb_dao.dart';
4+
import '../../logger/app_logger.dart';
5+
import '../../memory/memory_manager.dart';
6+
import '../agent_hook.dart';
7+
8+
/// 知识库召回钩子 — 在用户消息发送前从选中的知识库中检索相关内容注入 context。
9+
///
10+
/// 与 MemoryRecallHook 的区别:
11+
/// - 支持多个知识库同时检索
12+
/// - 每个知识库使用各自快照的嵌入模型配置
13+
/// - 结果按知识库分组展示,标明来源文件名
14+
class KbRecallHook extends AgentHook {
15+
final KbDao dao;
16+
final List<String> kbIds;
17+
final int topK;
18+
final double scoreThreshold;
19+
20+
KbRecallHook({
21+
required this.dao,
22+
required this.kbIds,
23+
this.topK = 5,
24+
this.scoreThreshold = 0.45,
25+
});
26+
27+
@override
28+
Future<String?> onBeforeUserMessage(
29+
String input,
30+
List<Map<String, dynamic>> messages,
31+
) async {
32+
if (kbIds.isEmpty) return null;
33+
34+
debugPrint(
35+
'[KB Recall] 开始检索, 知识库数=${kbIds.length}, '
36+
'query="${input.length > 40 ? input.substring(0, 40) : input}"',
37+
);
38+
39+
final allResults = <String>[];
40+
41+
for (final kbId in kbIds) {
42+
try {
43+
final kb = await dao.getBase(kbId);
44+
if (kb == null || !kb.hasEmbedding) {
45+
debugPrint('[KB Recall] 跳过 $kbId (不存在或未配置嵌入模型)');
46+
continue;
47+
}
48+
49+
debugPrint('[KB Recall] 检索知识库: ${kb.name} (${kb.collection})');
50+
51+
final manager = MemoryManager(
52+
embeddingBaseUrl: kb.embeddingBaseUrl,
53+
embeddingApiKey: kb.embeddingApiKey,
54+
embeddingModel: kb.embeddingModel,
55+
memoryDao: null,
56+
);
57+
58+
final results = await manager.recall(
59+
query: input,
60+
collectionName: kb.collection,
61+
topK: topK,
62+
scoreThreshold: scoreThreshold,
63+
useQdrant: true,
64+
);
65+
66+
debugPrint('[KB Recall] ${kb.name}: 命中 ${results.length} 条');
67+
68+
if (results.isNotEmpty) {
69+
allResults.add('── ${kb.name} ──');
70+
for (final r in results) {
71+
final text = r['text'] as String? ?? '';
72+
final filename = r['filename'] as String? ?? '';
73+
final score = r['score'] as double? ?? 0;
74+
final prefix = filename.isNotEmpty ? '[$filename] ' : '';
75+
allResults.add('$prefix$text');
76+
debugPrint(
77+
'[KB Recall] ✓ score=${score.toStringAsFixed(3)}, '
78+
'file=$filename, len=${text.length}',
79+
);
80+
}
81+
}
82+
} catch (e) {
83+
debugPrint('[KB Recall] ✗ 知识库 $kbId 检索失败: $e');
84+
AppLogger.instance.log('[KB Recall] 知识库 $kbId 检索失败: $e');
85+
}
86+
}
87+
88+
if (allResults.isNotEmpty) {
89+
final context = allResults.join('\n');
90+
messages.add({
91+
'role': 'system',
92+
'content': '[以下是从知识库中检索到的相关参考内容,请结合这些信息回答用户问题]\n$context',
93+
});
94+
debugPrint('[KB Recall] ✓ 已注入 ${allResults.length} 条结果到 context');
95+
} else {
96+
debugPrint('[KB Recall] 无相关结果');
97+
}
98+
99+
return null;
100+
}
101+
}

0 commit comments

Comments
 (0)