@vincentbanana/bananacli 0.4.0 → 0.4.2
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 +5 -0
- package/dist/tools/web.js +60 -3
- package/package.json +2 -2
package/dist/server/ui-server.js
CHANGED
|
@@ -75,6 +75,11 @@ export async function startUiServer(options) {
|
|
|
75
75
|
const prefix = `/t/${token}`;
|
|
76
76
|
const assets = options.assetsDir ?? resolveAssetsDir();
|
|
77
77
|
activeAssetsDir = assets;
|
|
78
|
+
// 界面资源缺失时界面会是空白页,很容易被误判成「服务挂了」,这里说清原因
|
|
79
|
+
if (!existsSync(join(assets, 'index.html'))) {
|
|
80
|
+
process.stderr.write(`界面资源缺失(${assets}),页面会打不开。\n` +
|
|
81
|
+
'多半是内核版本太旧或安装不完整,重新安装即可:npm install -g @vincentbanana/bananacli\n');
|
|
82
|
+
}
|
|
78
83
|
let turn = null;
|
|
79
84
|
const provider = () => createProvider(config.current, config);
|
|
80
85
|
const backups = (sessionId) => new BackupStore(join(bananaHome(), 'backups'), sessionId);
|
package/dist/tools/web.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fail, ok } from "./types.js";
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
-
const USER_AGENT = 'Mozilla/5.0 (
|
|
3
|
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36';
|
|
4
4
|
async function fetchText(url, init, ctx) {
|
|
5
5
|
const controller = new AbortController();
|
|
6
6
|
const timeoutMs = init.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -26,6 +26,12 @@ function decodeEntities(text) {
|
|
|
26
26
|
.replace(/"/g, '"')
|
|
27
27
|
.replace(/'/g, "'")
|
|
28
28
|
.replace(/ /g, ' ')
|
|
29
|
+
.replace(/ /g, ' ')
|
|
30
|
+
.replace(/ /g, ' ')
|
|
31
|
+
.replace(/·/g, '·')
|
|
32
|
+
.replace(/…/g, '…')
|
|
33
|
+
.replace(/—/g, '—')
|
|
34
|
+
.replace(/–/g, '–')
|
|
29
35
|
.replace(/</g, '<')
|
|
30
36
|
.replace(/>/g, '>')
|
|
31
37
|
.replace(/&/g, '&');
|
|
@@ -84,6 +90,55 @@ async function searchBrave(query, count, ctx) {
|
|
|
84
90
|
snippet: stripTags(item.description ?? '').slice(0, 500),
|
|
85
91
|
}));
|
|
86
92
|
}
|
|
93
|
+
async function searchBing(query, count, ctx) {
|
|
94
|
+
const html = await fetchText(`https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${count}`, {
|
|
95
|
+
headers: {
|
|
96
|
+
'user-agent': USER_AGENT,
|
|
97
|
+
accept: 'text/html,application/xhtml+xml',
|
|
98
|
+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
99
|
+
},
|
|
100
|
+
timeoutMs: 20_000,
|
|
101
|
+
}, ctx);
|
|
102
|
+
return parseBing(html, count);
|
|
103
|
+
}
|
|
104
|
+
/** Bing 有时把结果包成 /ck/a?...&u=a1<base64> 的跳转链接,这里还原成真实地址。 */
|
|
105
|
+
export function unwrapBing(href) {
|
|
106
|
+
if (!/bing\.com\/ck\/a/i.test(href))
|
|
107
|
+
return href;
|
|
108
|
+
try {
|
|
109
|
+
const raw = new URL(href).searchParams.get('u');
|
|
110
|
+
if (!raw || !raw.startsWith('a1'))
|
|
111
|
+
return href;
|
|
112
|
+
const base64 = raw.slice(2).replace(/-/g, '+').replace(/_/g, '/');
|
|
113
|
+
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
|
114
|
+
const decoded = Buffer.from(padded, 'base64').toString('utf8');
|
|
115
|
+
return /^https?:/i.test(decoded) ? decoded : href;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return href;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function parseBing(html, count) {
|
|
122
|
+
const hits = [];
|
|
123
|
+
for (const block of html.matchAll(/<li class="b_algo"[\s\S]*?<\/li>/gi)) {
|
|
124
|
+
const item = block[0];
|
|
125
|
+
const link = item.match(/<h2[^>]*>\s*<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
126
|
+
if (!link)
|
|
127
|
+
continue;
|
|
128
|
+
const url = unwrapBing(decodeEntities(link[1] ?? ''));
|
|
129
|
+
if (!/^https?:/i.test(url))
|
|
130
|
+
continue;
|
|
131
|
+
const snippet = item.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
132
|
+
hits.push({
|
|
133
|
+
title: decodeEntities(stripTags(link[2] ?? '')).trim(),
|
|
134
|
+
url,
|
|
135
|
+
snippet: decodeEntities(stripTags(snippet?.[1] ?? '')).trim().slice(0, 500),
|
|
136
|
+
});
|
|
137
|
+
if (hits.length >= count)
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return hits;
|
|
141
|
+
}
|
|
87
142
|
async function searchDuckDuckGo(query, count, ctx) {
|
|
88
143
|
const errors = [];
|
|
89
144
|
const endpoints = [
|
|
@@ -125,7 +180,7 @@ export const webSearchTool = {
|
|
|
125
180
|
name: 'web_search',
|
|
126
181
|
description: [
|
|
127
182
|
'联网搜索,返回标题 + 链接 + 摘要。',
|
|
128
|
-
'优先使用 TAVILY_API_KEY / BRAVE_API_KEY;没有配置时自动回退到 DuckDuckGo。',
|
|
183
|
+
'优先使用 TAVILY_API_KEY / BRAVE_API_KEY;没有配置时自动回退到 Bing、DuckDuckGo。',
|
|
129
184
|
'搜不到就换关键词再试,别靠猜。',
|
|
130
185
|
].join(''),
|
|
131
186
|
parameters: {
|
|
@@ -148,13 +203,15 @@ export const webSearchTool = {
|
|
|
148
203
|
const order = [
|
|
149
204
|
() => searchTavily(query, count, ctx),
|
|
150
205
|
() => searchBrave(query, count, ctx),
|
|
206
|
+
() => searchBing(query, count, ctx),
|
|
151
207
|
];
|
|
152
208
|
const problems = [];
|
|
153
209
|
for (const attempt of order) {
|
|
154
210
|
try {
|
|
155
211
|
hits = await attempt();
|
|
156
|
-
if (hits)
|
|
212
|
+
if (hits && hits.length)
|
|
157
213
|
break;
|
|
214
|
+
hits = null;
|
|
158
215
|
}
|
|
159
216
|
catch (error) {
|
|
160
217
|
problems.push(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentbanana/bananacli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A multi-provider AI agent for your terminal: file tools, shell, web search, persistent sessions, diff preview and undo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"build": "tsc -p tsconfig.build.json && node scripts/copy-ui.mjs",
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
27
|
"dev": "node --watch src/index.ts",
|
|
28
|
-
"test": "node test/prompt.test.mjs && node test/ui.test.mjs && node test/p0.test.mjs && node test/features.test.mjs && node test/gui.test.mjs && node test/smoke.mjs",
|
|
28
|
+
"test": "node test/prompt.test.mjs && node test/ui.test.mjs && node test/p0.test.mjs && node test/features.test.mjs && node test/gui.test.mjs && node test/web-search.test.mjs && node test/smoke.mjs",
|
|
29
29
|
"gui:sync": "node scripts/sync-gui.mjs",
|
|
30
30
|
"prepare": "npm run build",
|
|
31
31
|
"prepublishOnly": "npm run typecheck && npm test && npm run build && node test/pack.test.mjs"
|