Skip to content

Commit 02082a2

Browse files
author
langlang03
committed
feat: 新增Bing RSS免费网页搜索支持,优化搜索配置逻辑
1. 新增WebSearchProvider接口parseRawResponse默认方法,支持非JSON响应解析 2. 添加BingRssSearchProvider实现,支持无密钥的Bing RSS搜索 3. 新增默认搜索源为Bing RSS免费版,完善搜索配置校验逻辑 4. 重构工具设置界面,适配免密钥搜索源,优化权限和上下文压缩弹窗 5. 调整WebSearchConfigRepository为可继承,完善单元测试
1 parent 6a84b29 commit 02082a2

16 files changed

Lines changed: 552 additions & 68 deletions

File tree

app/src/main/java/cn/lineai/data/repository/ToolSettingsRepository.java

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cn.lineai.data.repository;
22

33
import android.content.Context;
4+
import cn.lineai.R;
45
import cn.lineai.model.ChatMode;
56
import cn.lineai.model.McpSettingsState;
67
import cn.lineai.model.McpToolConfig;
@@ -41,44 +42,72 @@ public final class ToolSettingsRepository implements ToolSettingsStore {
4142
private static final Set<String> MODE_REMOTE = java.util.Collections.unmodifiableSet(new HashSet<>(java.util.Arrays.asList(EXECUTION_SSH, EXECUTION_TERMINAL_PROVIDER)));
4243
private static final Set<String> MODE_ALL = java.util.Collections.unmodifiableSet(new HashSet<>(java.util.Arrays.asList(EXECUTION_LOCAL, EXECUTION_SSH, EXECUTION_TERMINAL_PROVIDER)));
4344

44-
private static final McpToolConfig[] DEFAULT_CONFIGS = new McpToolConfig[] {
45-
new McpToolConfig("file_ops", "文件操作", "读取、写入、编辑和删除文件", true,
46-
new String[] {ToolNames.FILE_READ, ToolNames.FILE_WRITE, ToolNames.FILE_EDIT, ToolNames.FILE_DELETE, ToolNames.GLOB, ToolNames.LIST_DIR},
47-
MODE_LOCAL, "file_ops"),
48-
new McpToolConfig(ToolNames.AGENT, "Agent", "分派 Agent 处理任务", true,
49-
new String[] {ToolNames.AGENT, ToolNames.AGENT_PIPELINE},
50-
MODE_ALL, "agent"),
51-
new McpToolConfig("phone_control", "手机控制", "通过无障碍服务控制本机操作", true,
52-
new String[] {ToolNames.PHONE_SCREENSHOT, ToolNames.PHONE_CLICK, ToolNames.PHONE_SWIPE, ToolNames.PHONE_LONG_PRESS, ToolNames.PHONE_VIEW_HIERARCHY, ToolNames.PHONE_CLICK_VIEW, ToolNames.PHONE_GLOBAL_ACTION},
53-
MODE_ALL, "phone_control"),
54-
new McpToolConfig("todo", "任务清单", "维护当前会话的 TODO 列表,状态会注入到 system prompt", true,
55-
new String[] {ToolNames.TODO_UPDATE},
56-
MODE_ALL, "todo"),
57-
new McpToolConfig(ToolNames.IMAGE_UNDERSTANDING, "图片理解", "读取本地或 SSH 工作区图片并调用已选择的视觉模型理解内容", false,
58-
new String[] {ToolNames.IMAGE_UNDERSTANDING},
59-
MODE_ALL, "image_understanding"),
60-
new McpToolConfig(ToolNames.IMAGE_GENERATION, "图片生成", "调用已选择的生图模型生成图片,并以内联 Markdown 图片返回", false,
61-
new String[] {ToolNames.IMAGE_GENERATION},
62-
MODE_ALL, "image_generation"),
63-
new McpToolConfig("shell", "SSH Shell", "通过 SSH 执行 shell 命令", true,
64-
new String[] {ToolNames.SHELL_EXECUTE},
65-
MODE_REMOTE, "shell"),
66-
new McpToolConfig(ToolNames.WEB_SEARCH, "网页搜索", "搜索互联网并查看网页内容", false,
67-
new String[] {ToolNames.WEB_SEARCH, ToolNames.WEB_FETCH},
68-
MODE_ALL, "web_search")
69-
};
70-
45+
private final Context context;
7146
private final SettingsRepository settingsRepository;
7247
private final WebSearchConfigRepository webSearchConfigRepository;
7348
private final PhoneControlRepository phoneControlRepository;
7449
private ToolRegistry toolRegistry;
7550

7651
public ToolSettingsRepository(Context context) {
52+
this.context = context.getApplicationContext();
7753
settingsRepository = new SettingsRepository(context);
7854
webSearchConfigRepository = new WebSearchConfigRepository(context);
7955
phoneControlRepository = new PhoneControlRepository(context);
8056
}
8157

58+
private List<McpToolConfig> buildDefaultConfigs() {
59+
List<McpToolConfig> configs = new ArrayList<>();
60+
configs.add(new McpToolConfig("file_ops",
61+
context.getString(R.string.tool_group_file_ops_name),
62+
context.getString(R.string.tool_group_file_ops_desc),
63+
true,
64+
new String[] {ToolNames.FILE_READ, ToolNames.FILE_WRITE, ToolNames.FILE_EDIT, ToolNames.FILE_DELETE, ToolNames.GLOB, ToolNames.LIST_DIR},
65+
MODE_LOCAL, "file_ops"));
66+
configs.add(new McpToolConfig(ToolNames.AGENT,
67+
"Agent",
68+
context.getString(R.string.tool_group_agent_desc),
69+
true,
70+
new String[] {ToolNames.AGENT, ToolNames.AGENT_PIPELINE},
71+
MODE_ALL, "agent"));
72+
configs.add(new McpToolConfig("phone_control",
73+
context.getString(R.string.tool_group_phone_control_name),
74+
context.getString(R.string.tool_group_phone_control_desc),
75+
true,
76+
new String[] {ToolNames.PHONE_SCREENSHOT, ToolNames.PHONE_CLICK, ToolNames.PHONE_SWIPE, ToolNames.PHONE_LONG_PRESS, ToolNames.PHONE_VIEW_HIERARCHY, ToolNames.PHONE_CLICK_VIEW, ToolNames.PHONE_GLOBAL_ACTION},
77+
MODE_ALL, "phone_control"));
78+
configs.add(new McpToolConfig("todo",
79+
context.getString(R.string.tool_group_todo_name),
80+
context.getString(R.string.tool_group_todo_desc),
81+
true,
82+
new String[] {ToolNames.TODO_UPDATE},
83+
MODE_ALL, "todo"));
84+
configs.add(new McpToolConfig(ToolNames.IMAGE_UNDERSTANDING,
85+
context.getString(R.string.tool_group_image_understanding_name),
86+
context.getString(R.string.tool_group_image_understanding_desc),
87+
false,
88+
new String[] {ToolNames.IMAGE_UNDERSTANDING},
89+
MODE_ALL, "image_understanding"));
90+
configs.add(new McpToolConfig(ToolNames.IMAGE_GENERATION,
91+
context.getString(R.string.tool_group_image_generation_name),
92+
context.getString(R.string.tool_group_image_generation_desc),
93+
false,
94+
new String[] {ToolNames.IMAGE_GENERATION},
95+
MODE_ALL, "image_generation"));
96+
configs.add(new McpToolConfig("shell",
97+
"SSH Shell",
98+
context.getString(R.string.tool_group_shell_desc),
99+
true,
100+
new String[] {ToolNames.SHELL_EXECUTE},
101+
MODE_REMOTE, "shell"));
102+
configs.add(new McpToolConfig(ToolNames.WEB_SEARCH,
103+
context.getString(R.string.tool_group_web_search_name),
104+
context.getString(R.string.tool_group_web_search_desc),
105+
true,
106+
new String[] {ToolNames.WEB_SEARCH, ToolNames.WEB_FETCH},
107+
MODE_ALL, "web_search"));
108+
return configs;
109+
}
110+
82111
public void setToolRegistry(ToolRegistry toolRegistry) {
83112
this.toolRegistry = toolRegistry;
84113
}
@@ -107,9 +136,9 @@ public synchronized void setExecutionMode(String mode) {
107136
public synchronized List<McpToolConfig> getConfigs() {
108137
String executionMode = getExecutionMode();
109138
ArrayList<McpToolConfig> configs = new ArrayList<>();
110-
for (McpToolConfig config : DEFAULT_CONFIGS) {
139+
for (McpToolConfig config : buildDefaultConfigs()) {
111140
boolean enabled = getMcpEnabled(executionMode, config);
112-
configs.add(displayConfigForMode(executionMode, config, enabled));
141+
configs.add(displayConfigForMode(executionMode, config, enabled, context));
113142
}
114143
return configs;
115144
}
@@ -185,8 +214,15 @@ static String mcpEnabledKey(String executionMode, String id) {
185214
}
186215

187216
static McpToolConfig displayConfigForMode(String executionMode, McpToolConfig config, boolean enabled) {
217+
return displayConfigForMode(executionMode, config, enabled, null);
218+
}
219+
220+
static McpToolConfig displayConfigForMode(String executionMode, McpToolConfig config, boolean enabled, Context context) {
188221
if ("shell".equals(config.getId()) && EXECUTION_TERMINAL_PROVIDER.equals(normalizeExecutionMode(executionMode))) {
189-
return new McpToolConfig(config.getId(), "IPC Shell", "通过终端提供者 IPC 执行 shell 命令", enabled, config.getTools(), config.getSupportedExecutionModes(), config.getIconKey());
222+
String ipcDesc = context == null
223+
? "通过终端提供者 IPC 执行 shell 命令"
224+
: context.getString(R.string.tool_group_ipc_shell_desc);
225+
return new McpToolConfig(config.getId(), "IPC Shell", ipcDesc, enabled, config.getTools(), config.getSupportedExecutionModes(), config.getIconKey());
190226
}
191227
return new McpToolConfig(config.getId(), config.getName(), config.getDescription(), enabled, config.getTools(), config.getSupportedExecutionModes(), config.getIconKey());
192228
}

app/src/main/java/cn/lineai/mvp/AttachmentPickerCoordinator.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cn.lineai.mvp;
22

3+
import android.content.Context;
4+
import cn.lineai.R;
35
import cn.lineai.data.repository.FileTreeStore;
46
import cn.lineai.data.repository.IpcFileTreeStore;
57
import cn.lineai.data.repository.SshFileTreeStore;
@@ -25,6 +27,7 @@ public interface Host {
2527
void showAttachmentPicker(String title, FileTreeNode tree, boolean loading, String message, String source);
2628
}
2729

30+
private final Context context;
2831
private final FileTreeStore fileTreeRepository;
2932
private final SshFileTreeStore sshFileTreeRepository;
3033
private final IpcFileTreeStore ipcFileTreeRepository;
@@ -42,13 +45,15 @@ public interface Host {
4245
private int loadGeneration;
4346

4447
public AttachmentPickerCoordinator(
48+
Context context,
4549
FileTreeStore fileTreeRepository,
4650
SshFileTreeStore sshFileTreeRepository,
4751
IpcFileTreeStore ipcFileTreeRepository,
4852
BackgroundRunner backgroundRunner,
4953
UiDispatcher uiDispatcher,
5054
Host host
5155
) {
56+
this.context = context.getApplicationContext();
5257
this.fileTreeRepository = fileTreeRepository;
5358
this.sshFileTreeRepository = sshFileTreeRepository;
5459
this.ipcFileTreeRepository = ipcFileTreeRepository;
@@ -125,7 +130,7 @@ private void refreshAttachmentPicker(boolean resetTree) {
125130

126131
private void refreshSshAttachmentPicker() {
127132
loading = true;
128-
message = "正在通过 SFTP 读取 SSH 文件...";
133+
message = context.getString(R.string.attachment_picker_loading_ssh);
129134
renderAttachmentPicker();
130135
int generation = ++loadGeneration;
131136
String root = rootPath;
@@ -160,7 +165,7 @@ private void refreshSshAttachmentPicker() {
160165

161166
private void refreshIpcAttachmentPicker() {
162167
loading = true;
163-
message = "正在通过 IPC 读取终端提供者目录...";
168+
message = context.getString(R.string.attachment_picker_loading_terminal_provider);
164169
renderAttachmentPicker();
165170
int generation = ++loadGeneration;
166171
String root = rootPath;
@@ -199,11 +204,11 @@ private void renderAttachmentPicker() {
199204
}
200205
String title;
201206
if (InputAttachment.SOURCE_SSH.equals(source)) {
202-
title = "选择 SSH 文件";
207+
title = context.getString(R.string.attachment_picker_title_ssh);
203208
} else if (InputAttachment.SOURCE_TERMINAL_PROVIDER.equals(source)) {
204-
title = "选择终端提供者文件";
209+
title = context.getString(R.string.attachment_picker_title_terminal_provider);
205210
} else {
206-
title = "选择本地文件";
211+
title = context.getString(R.string.attachment_picker_title_local);
207212
}
208213
host.showAttachmentPicker(title, tree, loading, message, source);
209214
}

app/src/main/java/cn/lineai/mvp/ContextCompactionController.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,13 @@ boolean shouldAutoSoftCompactBeforeRequest(ModelConfig selectedModel, String act
137137

138138
void showCompactConfirmation() {
139139
ArrayList<SheetOption> options = new ArrayList<>();
140-
options.add(new SheetOption("compact:confirm", "确认压缩",
141-
"把早期上下文总结成隐藏摘要,旧消息仍保留在历史中。", false));
142-
options.add(new SheetOption("compact:cancel", context.getString(R.string.common_cancel), "返回当前对话", false));
143-
host.showSheet("压缩上下文", options);
140+
options.add(new SheetOption("compact:confirm",
141+
context.getString(R.string.context_compact_confirm),
142+
context.getString(R.string.context_compact_confirm_desc), false));
143+
options.add(new SheetOption("compact:cancel",
144+
context.getString(R.string.common_cancel),
145+
context.getString(R.string.context_compact_cancel_desc), false));
146+
host.showSheet(context.getString(R.string.sheet_more_compact), options);
144147
}
145148

146149
void startManualContextCompaction() {
@@ -149,11 +152,11 @@ void startManualContextCompaction() {
149152
}
150153
ModelConfig selectedModel = modelRepository.getSelectedModel();
151154
if (selectedModel == null) {
152-
host.showNotice("还没有可用模型。请先配置模型,再压缩上下文。");
155+
host.showNotice(context.getString(R.string.context_compact_no_model));
153156
return;
154157
}
155158
if (messages.size() < 4) {
156-
host.showNotice("当前上下文不足,无需压缩。");
159+
host.showNotice(context.getString(R.string.context_compact_insufficient));
157160
return;
158161
}
159162
chatSessionStore.ensureCurrentConversation(System.currentTimeMillis());

app/src/main/java/cn/lineai/mvp/MainControllerInitializer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public void render() {
144144
coordinator.permissionModeController = new PermissionModeController(
145145
toolSettingsRepository,
146146
chatModeRepository,
147+
context,
147148
new PermissionModeController.Host() {
148149
@Override
149150
public boolean hasExternalStorageAccess() {
@@ -644,6 +645,7 @@ public void enterMessageSelectMode() {
644645
}
645646
);
646647
coordinator.attachmentPickerController = new AttachmentPickerCoordinator(
648+
context,
647649
fileTreeRepository,
648650
sshFileTreeRepository,
649651
ipcFileTreeRepository,

app/src/main/java/cn/lineai/mvp/OverlayActionController.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,25 @@ void showMoreActions() {
8585
return;
8686
}
8787
ArrayList<SheetOption> options = new ArrayList<>();
88-
options.add(new SheetOption("tutorial", "教程", "打开初学者教程", false));
89-
options.add(new SheetOption("settings", context.getString(R.string.screen_settings_title), "模型、主题、数据管理", false));
90-
options.add(new SheetOption("export", "导出对话", "将当前对话导出为 Markdown 或分享", false));
91-
options.add(new SheetOption("select_export", "选择消息导出", "多选消息后合并导出/分享", false));
92-
options.add(new SheetOption("compact", "压缩上下文", "将早期上下文总结为隐藏摘要", false));
93-
options.add(new SheetOption("clear", "清空对话", "清空当前对话消息", false));
94-
host.showSheet("更多", options);
88+
options.add(new SheetOption("tutorial",
89+
context.getString(R.string.sheet_more_tutorial),
90+
context.getString(R.string.sheet_more_tutorial_desc), false));
91+
options.add(new SheetOption("settings",
92+
context.getString(R.string.screen_settings_title),
93+
context.getString(R.string.sheet_more_settings_desc), false));
94+
options.add(new SheetOption("export",
95+
context.getString(R.string.sheet_more_export),
96+
context.getString(R.string.sheet_more_export_desc), false));
97+
options.add(new SheetOption("select_export",
98+
context.getString(R.string.sheet_more_select_export),
99+
context.getString(R.string.sheet_more_select_export_desc), false));
100+
options.add(new SheetOption("compact",
101+
context.getString(R.string.sheet_more_compact),
102+
context.getString(R.string.sheet_more_compact_desc), false));
103+
options.add(new SheetOption("clear",
104+
context.getString(R.string.sheet_more_clear),
105+
context.getString(R.string.sheet_more_clear_desc), false));
106+
host.showSheet(context.getString(R.string.common_more), options);
95107
}
96108

97109
void handleSheetOption(String id) {

app/src/main/java/cn/lineai/mvp/PermissionModeController.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cn.lineai.mvp;
22

3+
import android.content.Context;
4+
import cn.lineai.R;
35
import cn.lineai.data.repository.ChatModeRepository;
46
import cn.lineai.data.repository.ToolSettingsRepository;
57
import cn.lineai.data.repository.ToolSettingsStore;
@@ -73,23 +75,31 @@ public void rememberRestorablePermission(String mode) {
7375

7476
private final PermissionStore permissionStore;
7577
private final ChatModeStore chatModeStore;
78+
private final Context context;
7679
private final Host host;
7780

7881
public PermissionModeController(
7982
ToolSettingsStore toolSettingsRepository,
8083
ChatModeRepository chatModeRepository,
84+
Context context,
8185
Host host
8286
) {
8387
this(
8488
new ToolSettingsPermissionStore(toolSettingsRepository),
8589
new RepositoryChatModeStore(chatModeRepository),
90+
context,
8691
host
8792
);
8893
}
8994

9095
PermissionModeController(PermissionStore permissionStore, ChatModeStore chatModeStore, Host host) {
96+
this(permissionStore, chatModeStore, null, host);
97+
}
98+
99+
PermissionModeController(PermissionStore permissionStore, ChatModeStore chatModeStore, Context context, Host host) {
91100
this.permissionStore = permissionStore;
92101
this.chatModeStore = chatModeStore;
102+
this.context = context == null ? null : context.getApplicationContext();
93103
this.host = host;
94104
}
95105

@@ -98,31 +108,37 @@ public void showPermissionSheet() {
98108
ArrayList<SheetOption> options = new ArrayList<>();
99109
options.add(new SheetOption(
100110
ToolSettingsRepository.PERMISSION_AUTO,
101-
"自动",
102-
"自动执行已启用工具,危险工具按策略确认",
111+
localizedString(R.string.permission_mode_auto, "自动"),
112+
localizedString(R.string.permission_mode_auto_desc, "自动执行已启用工具,危险工具按策略确认"),
103113
ToolSettingsRepository.PERMISSION_AUTO.equals(permissionMode)
104114
));
105115
options.add(new SheetOption(
106116
ToolSettingsRepository.PERMISSION_CONFIRM,
107-
"确认",
108-
"危险操作需要确认后执行",
117+
localizedString(R.string.permission_mode_confirm, "确认"),
118+
localizedString(R.string.permission_mode_confirm_desc, "危险操作需要确认后执行"),
109119
ToolSettingsRepository.PERMISSION_CONFIRM.equals(permissionMode)
110120
));
111121
options.add(new SheetOption(
112122
ToolSettingsRepository.PERMISSION_READONLY,
113-
"只读",
114-
"仅允许读取、搜索和列目录,禁止写入与 Shell",
123+
localizedString(R.string.permission_mode_readonly, "只读"),
124+
localizedString(R.string.permission_mode_readonly_desc, "仅允许读取、搜索和列目录,禁止写入与 Shell"),
115125
ToolSettingsRepository.PERMISSION_READONLY.equals(permissionMode)
116126
));
117127
options.add(new SheetOption(
118128
"storage:manage_all_files",
119-
"管理所有文件权限",
120-
host.hasExternalStorageAccess() ? "已授权,可访问文件存储" : host.storagePermissionMessage(),
129+
localizedString(R.string.permission_mode_manage_all_files, "管理所有文件权限"),
130+
host.hasExternalStorageAccess()
131+
? localizedString(R.string.permission_mode_storage_granted, "已授权,可访问文件存储")
132+
: host.storagePermissionMessage(),
121133
host.hasExternalStorageAccess()
122134
));
123135
host.showPermissionSheet(options);
124136
}
125137

138+
private String localizedString(int resId, String fallback) {
139+
return context == null ? fallback : context.getString(resId);
140+
}
141+
126142
public boolean isPermissionModeOption(String id) {
127143
return ToolSettingsRepository.PERMISSION_AUTO.equals(id)
128144
|| ToolSettingsRepository.PERMISSION_CONFIRM.equals(id)

0 commit comments

Comments
 (0)