xshat-lite 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,367 @@
1
+ import { input as promptInput, select } from '@inquirer/prompts';
2
+ import { getUserConfigPath, listProviders, updateConfigValue } from '../utils/config.mjs';
3
+
4
+ function getProviderStatusLabel(status = '') {
5
+ if (status === 'working') {
6
+ return '可用';
7
+ }
8
+ if (status === 'experimental') {
9
+ return '实验性';
10
+ }
11
+ if (status === 'disabled') {
12
+ return '停用';
13
+ }
14
+ return status || '未知';
15
+ }
16
+
17
+ function getProviderTransportLabel(provider) {
18
+ return provider?.transport === 'api' ? 'API' : 'Web';
19
+ }
20
+
21
+ function formatProviderValue(provider) {
22
+ if (!provider) {
23
+ return '(未设置)';
24
+ }
25
+ return `${provider.name} · ${getProviderTransportLabel(provider)} · ${getProviderStatusLabel(provider.status)}`;
26
+ }
27
+
28
+ function buildProviderChoiceName(provider, currentProviderId) {
29
+ const currentTag = provider.id === currentProviderId ? '(当前)' : '';
30
+ const summary = `${getProviderTransportLabel(provider)} / ${getProviderStatusLabel(provider.status)}`;
31
+ return `${provider.name}${currentTag} ${summary} ${provider.description || ''}`.trim();
32
+ }
33
+
34
+ function buildSettingItems(config) {
35
+ const currentProvider = listProviders().find((provider) => provider.id === config.defaultProvider);
36
+ return [
37
+ {
38
+ key: 'defaultProvider',
39
+ label: '默认提供方',
40
+ description: '新建聊天和任务默认使用的站点或 API',
41
+ type: 'select',
42
+ value: formatProviderValue(currentProvider)
43
+ },
44
+ {
45
+ key: 'providersRuntime.autoFallback',
46
+ label: '自动故障切换',
47
+ description: '当前 provider 多次重试仍失败后,是否自动切换到下一个提供方',
48
+ type: 'boolean',
49
+ value: Boolean(config.providersRuntime?.autoFallback)
50
+ },
51
+ {
52
+ key: 'providersRuntime.includeApiFallback',
53
+ label: 'API 兜底',
54
+ description: '网页模型都不稳时是否允许切换到 API 提供方',
55
+ type: 'boolean',
56
+ value: Boolean(config.providersRuntime?.includeApiFallback)
57
+ },
58
+ {
59
+ key: 'api.baseUrl',
60
+ label: 'API Base URL',
61
+ description: 'OpenAI-compatible API 根地址',
62
+ type: 'text',
63
+ value: config.api?.baseUrl || ''
64
+ },
65
+ {
66
+ key: 'api.model',
67
+ label: 'API 默认模型',
68
+ description: 'API 提供方默认模型名',
69
+ type: 'text',
70
+ value: config.api?.model || ''
71
+ },
72
+ {
73
+ key: 'agent.enabled',
74
+ label: 'Agent 模式',
75
+ description: '是否默认开启 Agent 结构化对话',
76
+ type: 'boolean',
77
+ value: Boolean(config.agent?.enabled)
78
+ },
79
+ {
80
+ key: 'agent.autoExecuteCommands',
81
+ label: '自动执行命令',
82
+ description: '是否自动执行模型建议的命令',
83
+ type: 'boolean',
84
+ value: Boolean(config.agent?.autoExecuteCommands)
85
+ },
86
+ {
87
+ key: 'agent.allowShell',
88
+ label: 'Shell 权限',
89
+ description: '是否允许执行系统命令',
90
+ type: 'boolean',
91
+ value: Boolean(config.agent?.allowShell)
92
+ },
93
+ {
94
+ key: 'agent.allowFileWrite',
95
+ label: '文件写入',
96
+ description: '是否允许增删改工作区文件',
97
+ type: 'boolean',
98
+ value: Boolean(config.agent?.allowFileWrite)
99
+ },
100
+ {
101
+ key: 'agent.maxAutoSteps',
102
+ label: '最大自动步数',
103
+ description: '单次 Agent 自动续跑的最大步数',
104
+ type: 'number',
105
+ value: Number(config.agent?.maxAutoSteps) || 4,
106
+ min: 1,
107
+ max: 20
108
+ },
109
+ {
110
+ key: 'agent.longRunning',
111
+ label: '长时运行',
112
+ description: '是否启用更适合长任务的运行模式',
113
+ type: 'boolean',
114
+ value: Boolean(config.agent?.longRunning)
115
+ },
116
+ {
117
+ key: 'agent.multiAgent',
118
+ label: '多 Agent 协作',
119
+ description: '是否启用主控 + 专项角色的协作模式',
120
+ type: 'boolean',
121
+ value: Boolean(config.agent?.multiAgent)
122
+ },
123
+ {
124
+ key: 'browser.headless',
125
+ label: '浏览器 Headless',
126
+ description: '是否默认无界面启动浏览器',
127
+ type: 'boolean',
128
+ value: Boolean(config.browser?.headless)
129
+ },
130
+ {
131
+ key: 'browser.humanize',
132
+ label: '浏览器 Humanize',
133
+ description: '是否启用更拟人的交互节奏',
134
+ type: 'boolean',
135
+ value: Boolean(config.browser?.humanize)
136
+ },
137
+ {
138
+ key: 'browser.autoRecover',
139
+ label: '自动恢复',
140
+ description: '浏览器异常后是否尝试自动恢复',
141
+ type: 'boolean',
142
+ value: Boolean(config.browser?.autoRecover)
143
+ },
144
+ {
145
+ key: 'browser.allowVisibleBrowser',
146
+ label: '允许可见浏览器',
147
+ description: '是否允许程序主动切换到可见浏览器窗口',
148
+ type: 'boolean',
149
+ value: Boolean(config.browser?.allowVisibleBrowser)
150
+ },
151
+ {
152
+ key: 'browser.shareAuthState',
153
+ label: '统一登录态',
154
+ description: '在聊天通道和 Agent 通道之间同步登录状态',
155
+ type: 'boolean',
156
+ value: Boolean(config.browser?.shareAuthState)
157
+ },
158
+ {
159
+ key: 'session.compressThreshold',
160
+ label: '上下文压缩阈值',
161
+ description: '达到多少轮后开始自动压缩上下文',
162
+ type: 'number',
163
+ value: Number(config.session?.compressThreshold) || 12,
164
+ min: 4,
165
+ max: 100
166
+ },
167
+ {
168
+ key: 'session.keepRecentMessages',
169
+ label: '保留最近轮数',
170
+ description: '压缩时保留多少轮原始消息',
171
+ type: 'number',
172
+ value: Number(config.session?.keepRecentMessages) || 6,
173
+ min: 2,
174
+ max: 20
175
+ }
176
+ ];
177
+ }
178
+
179
+ function buildSettingGroups(config) {
180
+ const items = buildSettingItems(config);
181
+ const pick = (...keys) => items.filter((item) => keys.includes(item.key));
182
+
183
+ return [
184
+ {
185
+ key: 'provider',
186
+ label: '提供方与兜底',
187
+ items: pick(
188
+ 'defaultProvider',
189
+ 'providersRuntime.autoFallback',
190
+ 'providersRuntime.includeApiFallback',
191
+ 'api.baseUrl',
192
+ 'api.model'
193
+ )
194
+ },
195
+ {
196
+ key: 'agent',
197
+ label: 'Agent 能力',
198
+ items: pick(
199
+ 'agent.enabled',
200
+ 'agent.autoExecuteCommands',
201
+ 'agent.allowShell',
202
+ 'agent.allowFileWrite',
203
+ 'agent.maxAutoSteps',
204
+ 'agent.longRunning',
205
+ 'agent.multiAgent'
206
+ )
207
+ },
208
+ {
209
+ key: 'browser',
210
+ label: '浏览器',
211
+ items: pick(
212
+ 'browser.headless',
213
+ 'browser.humanize',
214
+ 'browser.autoRecover',
215
+ 'browser.allowVisibleBrowser',
216
+ 'browser.shareAuthState'
217
+ )
218
+ },
219
+ {
220
+ key: 'session',
221
+ label: '会话与上下文',
222
+ items: pick(
223
+ 'session.compressThreshold',
224
+ 'session.keepRecentMessages'
225
+ )
226
+ }
227
+ ];
228
+ }
229
+
230
+ function getDisplayValue(item) {
231
+ if (item.type === 'boolean') {
232
+ return item.value ? '开启' : '关闭';
233
+ }
234
+ return String(item.value ?? '');
235
+ }
236
+
237
+ async function editSetting(item, config, ui) {
238
+ if (item.type === 'boolean') {
239
+ const nextValue = !item.value;
240
+ updateConfigValue(item.key, nextValue);
241
+ ui.printSuccess(`${item.label} 已切换为${nextValue ? '开启' : '关闭'}`);
242
+ return;
243
+ }
244
+
245
+ if (item.type === 'number') {
246
+ const answer = await promptInput({
247
+ message: `${item.label} (${item.min}-${item.max})`,
248
+ default: String(item.value)
249
+ });
250
+ if (!answer) {
251
+ return;
252
+ }
253
+ const parsed = Number.parseInt(answer, 10);
254
+ if (!Number.isFinite(parsed) || parsed < item.min || parsed > item.max) {
255
+ ui.printError('输入无效');
256
+ return;
257
+ }
258
+ updateConfigValue(item.key, parsed);
259
+ ui.printSuccess(`${item.label} 已更新为 ${parsed}`);
260
+ return;
261
+ }
262
+
263
+ if (item.type === 'text') {
264
+ const answer = await promptInput({
265
+ message: item.label,
266
+ default: String(item.value ?? '')
267
+ });
268
+ if (answer === undefined) {
269
+ return;
270
+ }
271
+ updateConfigValue(item.key, answer.trim());
272
+ ui.printSuccess(`${item.label} 已更新`);
273
+ return;
274
+ }
275
+
276
+ if (item.type === 'select') {
277
+ const providers = listProviders();
278
+ const value = await select({
279
+ message: '选择默认提供方',
280
+ choices: providers.map((provider) => ({
281
+ name: buildProviderChoiceName(provider, config.defaultProvider),
282
+ value: provider.id
283
+ })),
284
+ default: config.defaultProvider
285
+ });
286
+ updateConfigValue(item.key, value);
287
+ const provider = providers.find((item) => item.id === value);
288
+ ui.printSuccess(`默认提供方已切换为 ${provider?.name || value}`);
289
+ }
290
+ }
291
+
292
+ export async function showSettingsMenu({
293
+ ui,
294
+ config
295
+ }) {
296
+ while (true) {
297
+ const items = buildSettingItems(config);
298
+ const groups = buildSettingGroups(config);
299
+
300
+ ui.clear();
301
+ ui.printTitle('系统设置');
302
+ ui.printNewline();
303
+ ui.printKeyValueTable('配置概览', [
304
+ ...items.map((item) => ({
305
+ label: item.label,
306
+ value: `${getDisplayValue(item)} · ${item.description}`
307
+ })),
308
+ { label: '用户配置', value: getUserConfigPath() }
309
+ ]);
310
+ ui.printNewline();
311
+
312
+ const choice = await select({
313
+ message: '选择分组或直接返回',
314
+ choices: [
315
+ ...groups.map((group) => ({
316
+ name: `${group.label} ${group.items.length} 项`,
317
+ value: group.key
318
+ })),
319
+ { name: '全部设置项', value: '__all__' },
320
+ { name: '返回', value: '__back__' }
321
+ ]
322
+ });
323
+
324
+ if (choice === '__back__') {
325
+ ui.printNewline();
326
+ ui.printInfo('设置已保存到用户配置文件。');
327
+ ui.printNewline();
328
+ return;
329
+ }
330
+
331
+ const scopedItems =
332
+ choice === '__all__'
333
+ ? items
334
+ : groups.find((group) => group.key === choice)?.items || [];
335
+
336
+ if (scopedItems.length === 0) {
337
+ continue;
338
+ }
339
+
340
+ const settingChoice = await select({
341
+ message: '上下选择设置项,回车编辑或切换',
342
+ choices: [
343
+ ...scopedItems.map((item) => ({
344
+ name: `${item.label} ${getDisplayValue(item)} ${item.description}`,
345
+ value: item.key
346
+ })),
347
+ { name: '返回上一级', value: '__group_back__' }
348
+ ]
349
+ });
350
+
351
+ if (settingChoice === '__group_back__') {
352
+ continue;
353
+ }
354
+
355
+ const currentItem = items.find((item) => item.key === settingChoice);
356
+ if (!currentItem) {
357
+ continue;
358
+ }
359
+
360
+ await editSetting(currentItem, config, ui);
361
+ await new Promise((resolve) => setTimeout(resolve, 700));
362
+ }
363
+ }
364
+
365
+ export default {
366
+ showSettingsMenu
367
+ };