@vincentbanana/bananacli 0.4.3 → 0.4.4
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.
- package/dist/server/ui-server.js +22 -1
- package/dist/tools/web.js +41 -3
- package/dist/ui/app.js +46 -3
- package/dist/ui/style.css +47 -3
- package/package.json +1 -1
package/dist/server/ui-server.js
CHANGED
|
@@ -258,7 +258,28 @@ export async function startUiServer(options) {
|
|
|
258
258
|
config.systemPrompt = body.value;
|
|
259
259
|
}
|
|
260
260
|
else if (body.action === 'workspace' && body.value) {
|
|
261
|
-
|
|
261
|
+
const next = resolve(String(body.value).trim());
|
|
262
|
+
if (!existsSync(next)) {
|
|
263
|
+
return json(res, 400, { error: `目录不存在:${next}` });
|
|
264
|
+
}
|
|
265
|
+
config.workspace = next;
|
|
266
|
+
}
|
|
267
|
+
else if (body.action === 'terminal') {
|
|
268
|
+
// 在系统终端里打开当前工作区(GUI 跑在浏览器里,开终端必须借系统命令)
|
|
269
|
+
const dir = config.workspace;
|
|
270
|
+
if (!existsSync(dir))
|
|
271
|
+
return json(res, 400, { error: `目录不存在:${dir}` });
|
|
272
|
+
try {
|
|
273
|
+
const target = process.platform === 'win32'
|
|
274
|
+
? { file: 'cmd', args: ['/c', 'start', '', 'cmd', '/k', 'cd', '/d', dir] }
|
|
275
|
+
: process.platform === 'darwin'
|
|
276
|
+
? { file: 'open', args: ['-a', 'Terminal', dir] }
|
|
277
|
+
: { file: 'x-terminal-emulator', args: ['--working-directory', dir] };
|
|
278
|
+
spawn(target.file, target.args, { detached: true, stdio: 'ignore' }).unref();
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return json(res, 500, { error: `打开终端失败:${error instanceof Error ? error.message : String(error)}` });
|
|
282
|
+
}
|
|
262
283
|
}
|
|
263
284
|
const { saveConfig } = await import("../config/config.js");
|
|
264
285
|
await saveConfig(config);
|
package/dist/tools/web.js
CHANGED
|
@@ -54,7 +54,7 @@ function unwrapDuckDuckGo(href) {
|
|
|
54
54
|
return decodeURIComponent(match[1]);
|
|
55
55
|
}
|
|
56
56
|
async function searchTavily(query, count, ctx) {
|
|
57
|
-
const apiKey = process.env['TAVILY_API_KEY'];
|
|
57
|
+
const apiKey = process.env['TAVILY_API_KEY'] ?? process.env['BANANA_TAVILY_KEY'];
|
|
58
58
|
if (!apiKey)
|
|
59
59
|
return null;
|
|
60
60
|
const response = await fetch('https://api.tavily.com/search', {
|
|
@@ -73,10 +73,10 @@ async function searchTavily(query, count, ctx) {
|
|
|
73
73
|
}));
|
|
74
74
|
}
|
|
75
75
|
async function searchBrave(query, count, ctx) {
|
|
76
|
-
const apiKey = process.env['BRAVE_API_KEY'];
|
|
76
|
+
const apiKey = process.env['BRAVE_API_KEY'] ?? process.env['BANANA_BRAVE_KEY'];
|
|
77
77
|
if (!apiKey)
|
|
78
78
|
return null;
|
|
79
|
-
const url = `https://api.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
79
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
80
80
|
const response = await fetch(url, {
|
|
81
81
|
headers: { 'x-subscription-token': apiKey, accept: 'application/json' },
|
|
82
82
|
signal: ctx.signal,
|
|
@@ -176,6 +176,43 @@ function parseDuckDuckGo(html, count) {
|
|
|
176
176
|
}
|
|
177
177
|
return hits;
|
|
178
178
|
}
|
|
179
|
+
/** 归一化 URL:忽略大小写、www. 前缀、末尾斜杠和 #hash,用来去重。 */
|
|
180
|
+
export function normalizeUrl(href) {
|
|
181
|
+
try {
|
|
182
|
+
const url = new URL(href);
|
|
183
|
+
const host = url.hostname.replace(/^www\./i, '').toLowerCase();
|
|
184
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
185
|
+
return `${host}${path}${url.search}`;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return href.trim();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 搜索结果清洗:去掉站内跳转、重复链接和空条目。 */
|
|
192
|
+
export function sanitizeHits(hits, count) {
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const hit of hits) {
|
|
196
|
+
const url = (hit.url ?? '').trim();
|
|
197
|
+
if (!/^https?:\/\//i.test(url))
|
|
198
|
+
continue;
|
|
199
|
+
// 搜索引擎自己的搜索页 / 跳转页不是结果
|
|
200
|
+
if (/^https?:\/\/([^/]*\.)?(bing|duckduckgo|google|baidu)\.(com|cn)\//i.test(url) && /\/(search|ck\/a|url|link)\b/i.test(url))
|
|
201
|
+
continue;
|
|
202
|
+
const key = normalizeUrl(url);
|
|
203
|
+
if (seen.has(key))
|
|
204
|
+
continue;
|
|
205
|
+
seen.add(key);
|
|
206
|
+
out.push({
|
|
207
|
+
title: (hit.title ?? '').trim() || url,
|
|
208
|
+
url,
|
|
209
|
+
snippet: (hit.snippet ?? '').trim().slice(0, 500),
|
|
210
|
+
});
|
|
211
|
+
if (out.length >= count)
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
179
216
|
export const webSearchTool = {
|
|
180
217
|
name: 'web_search',
|
|
181
218
|
description: [
|
|
@@ -221,6 +258,7 @@ export const webSearchTool = {
|
|
|
221
258
|
hits = await searchDuckDuckGo(query, count, ctx);
|
|
222
259
|
if (problems.length)
|
|
223
260
|
ctx.log(`搜索 API 降级: ${problems.join('; ')}`);
|
|
261
|
+
hits = sanitizeHits(hits, count);
|
|
224
262
|
if (!hits.length)
|
|
225
263
|
return fail(`没有找到 "${query}" 的结果,换个关键词试试。`);
|
|
226
264
|
const body = hits
|
package/dist/ui/app.js
CHANGED
|
@@ -510,7 +510,7 @@ async function send() {
|
|
|
510
510
|
renderEmpty();
|
|
511
511
|
|
|
512
512
|
const userMsg = document.createElement('article');
|
|
513
|
-
userMsg.className = 'msg user';
|
|
513
|
+
userMsg.className = 'msg user enter';
|
|
514
514
|
userMsg.innerHTML = '<div class="msg-role">你</div>';
|
|
515
515
|
const userBody = document.createElement('div');
|
|
516
516
|
userBody.className = 'msg-body';
|
|
@@ -519,7 +519,7 @@ async function send() {
|
|
|
519
519
|
THREAD.appendChild(userMsg);
|
|
520
520
|
|
|
521
521
|
const botMsg = document.createElement('article');
|
|
522
|
-
botMsg.className = 'msg assistant';
|
|
522
|
+
botMsg.className = 'msg assistant enter';
|
|
523
523
|
botMsg.innerHTML = '<div class="msg-role">Banana</div>';
|
|
524
524
|
const toolWrap = document.createElement('div');
|
|
525
525
|
toolWrap.className = 'tool-group';
|
|
@@ -822,7 +822,7 @@ async function runCommand(text) {
|
|
|
822
822
|
THREAD.appendChild(userMsg);
|
|
823
823
|
|
|
824
824
|
const botMsg = document.createElement('article');
|
|
825
|
-
botMsg.className = 'msg assistant';
|
|
825
|
+
botMsg.className = 'msg assistant enter';
|
|
826
826
|
const role = document.createElement('div');
|
|
827
827
|
role.className = 'msg-role';
|
|
828
828
|
role.textContent = '命令输出';
|
|
@@ -945,6 +945,49 @@ function renderSettings() {
|
|
|
945
945
|
});
|
|
946
946
|
body.appendChild(keyField);
|
|
947
947
|
|
|
948
|
+
// 工作区:换目录后所有文件工具和命令都跟着换,所以放显眼位置
|
|
949
|
+
const workspaceField = document.createElement('div');
|
|
950
|
+
workspaceField.className = 'field';
|
|
951
|
+
workspaceField.innerHTML =
|
|
952
|
+
'<label>工作区</label>' +
|
|
953
|
+
`<div class="row"><input value="${escapeHtml(info.workspace ?? '')}" spellcheck="false" /><button class="btn">切换</button></div>` +
|
|
954
|
+
'<div class="hint">Banana 只能读写这个目录下的文件,命令也在这里执行。绝对路径,例如 D:\\project。</div>';
|
|
955
|
+
workspaceField.querySelector('button').addEventListener('click', async () => {
|
|
956
|
+
const input = workspaceField.querySelector('input');
|
|
957
|
+
const value = input.value.trim();
|
|
958
|
+
if (!value) {
|
|
959
|
+
toast('请填写目录路径');
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
await post('action', { action: 'workspace', value });
|
|
964
|
+
await refreshSettings();
|
|
965
|
+
renderHeader();
|
|
966
|
+
toast('工作区已切换到 ' + value);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
toast('切换失败:' + (error?.message ?? error));
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
workspaceField.querySelector('input').addEventListener('keydown', (event) => {
|
|
972
|
+
if (event.key === 'Enter') workspaceField.querySelector('button').click();
|
|
973
|
+
});
|
|
974
|
+
body.appendChild(workspaceField);
|
|
975
|
+
|
|
976
|
+
const terminalField = document.createElement('div');
|
|
977
|
+
terminalField.className = 'field';
|
|
978
|
+
terminalField.innerHTML =
|
|
979
|
+
'<label>终端</label><div class="row"><button class="btn">在工作区打开终端</button></div>' +
|
|
980
|
+
'<div class="hint">会弹出一个系统终端窗口,已切到当前工作区,方便你手动跑命令。</div>';
|
|
981
|
+
terminalField.querySelector('button').addEventListener('click', async () => {
|
|
982
|
+
try {
|
|
983
|
+
await post('action', { action: 'terminal' });
|
|
984
|
+
toast('终端已打开');
|
|
985
|
+
} catch (error) {
|
|
986
|
+
toast('打开终端失败:' + (error?.message ?? error));
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
body.appendChild(terminalField);
|
|
990
|
+
|
|
948
991
|
const permField = document.createElement('div');
|
|
949
992
|
permField.className = 'field';
|
|
950
993
|
permField.innerHTML = '<label>权限</label>';
|
package/dist/ui/style.css
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
--radius: 12px;
|
|
14
14
|
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
15
15
|
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
16
|
+
/* 带一点点回弹,用于按压力气和弹出层,别过量 */
|
|
17
|
+
--spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
16
18
|
--dur: 220ms;
|
|
17
19
|
--sidebar: 274px;
|
|
18
20
|
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
|
@@ -262,7 +264,48 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|
|
262
264
|
contain-intrinsic-size: auto 120px;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
/*
|
|
267
|
+
/* ---------- 轻微回弹 ----------
|
|
268
|
+
只给「按下去」和「弹出来」加一点过冲,不影响滚动手感。 */
|
|
269
|
+
.btn,
|
|
270
|
+
.icon-btn,
|
|
271
|
+
.send-btn,
|
|
272
|
+
.s-act,
|
|
273
|
+
.popover button,
|
|
274
|
+
.session-item {
|
|
275
|
+
transition:
|
|
276
|
+
background 140ms var(--ease),
|
|
277
|
+
color 140ms var(--ease),
|
|
278
|
+
border-color 140ms var(--ease),
|
|
279
|
+
transform 180ms var(--spring);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.btn:active,
|
|
283
|
+
.popover button:active,
|
|
284
|
+
.s-act:active {
|
|
285
|
+
transform: scale(0.95);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.btn:hover,
|
|
289
|
+
.icon-btn:hover {
|
|
290
|
+
transform: translateY(-1px);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.send-btn:not(:disabled):hover {
|
|
294
|
+
transform: translateY(-1px) scale(1.04);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.send-btn:not(:disabled):active {
|
|
298
|
+
transform: scale(0.93);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.session-item:active {
|
|
302
|
+
transform: scale(0.985);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* 新消息轻轻上浮,只有真正新产生的消息走这条 */
|
|
306
|
+
.msg.enter {
|
|
307
|
+
animation: rise 240ms var(--spring) both;
|
|
308
|
+
}
|
|
266
309
|
.thread { transition: opacity 120ms var(--ease); }
|
|
267
310
|
body.switching .thread { opacity: 0.45; }
|
|
268
311
|
body.switching { cursor: progress; }
|
|
@@ -520,7 +563,7 @@ pre.code code { border: 0; background: none; padding: 0; font-size: 12.6px; line
|
|
|
520
563
|
box-shadow: var(--shadow-lg);
|
|
521
564
|
display: flex;
|
|
522
565
|
flex-direction: column;
|
|
523
|
-
animation: slide
|
|
566
|
+
animation: slide 250ms var(--spring) both;
|
|
524
567
|
will-change: transform;
|
|
525
568
|
}
|
|
526
569
|
@keyframes slide {
|
|
@@ -774,7 +817,8 @@ pre.code code { border: 0; background: none; padding: 0; font-size: 12.6px; line
|
|
|
774
817
|
border-radius: 10px;
|
|
775
818
|
background: var(--bg);
|
|
776
819
|
box-shadow: var(--shadow-lg);
|
|
777
|
-
animation: pop
|
|
820
|
+
animation: pop 190ms var(--spring) both;
|
|
821
|
+
transform-origin: top right;
|
|
778
822
|
}
|
|
779
823
|
.popover button {
|
|
780
824
|
display: block;
|
package/package.json
CHANGED