@yeaft/webchat-agent 0.1.667 → 0.1.670
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/connection/message-router.js +24 -1
- package/index.js +2 -15
- package/package.json +2 -4
- package/terminal.js +14 -5
- package/unify/config-api.js +151 -0
- package/unify/tools/web-search.js +8 -0
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
|
-
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings } from '../unify/config-api.js';
|
|
38
|
+
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
39
|
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
@@ -355,6 +355,29 @@ export async function handleMessage(msg) {
|
|
|
355
355
|
break;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// Search settings (web-search backend + Tavily key) — read/write the
|
|
359
|
+
// `search` section of config.json. `get_tavily_usage` hits Tavily's
|
|
360
|
+
// /usage endpoint with the saved key and is fired from the UI only
|
|
361
|
+
// when the Search tab opens or the user clicks "Refresh" (no polling
|
|
362
|
+
// — the user explicitly asked for live read on open).
|
|
363
|
+
case 'get_search_settings': {
|
|
364
|
+
const settings = getSearchSettings(ctx.CONFIG?.yeaftDir);
|
|
365
|
+
sendToServer({ type: 'search_settings', ...settings });
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'update_search_settings': {
|
|
370
|
+
const result = updateSearchSettings(msg.settings || msg.config || {}, ctx.CONFIG?.yeaftDir);
|
|
371
|
+
sendToServer({ type: 'search_settings_updated', ...result });
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case 'get_tavily_usage': {
|
|
376
|
+
const usage = await fetchTavilyUsage(ctx.CONFIG?.yeaftDir);
|
|
377
|
+
sendToServer({ type: 'tavily_usage', ...usage });
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
358
381
|
// Unify — independent chat via Engine
|
|
359
382
|
case 'unify_chat':
|
|
360
383
|
await handleUnifyChat(msg);
|
package/index.js
CHANGED
|
@@ -126,7 +126,8 @@ async function detectCapabilities() {
|
|
|
126
126
|
return capabilities;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
//
|
|
129
|
+
// 确保依赖已安装。node-pty 已被 @homebridge/node-pty-prebuilt-multiarch
|
|
130
|
+
// 取代(regular dep + 全平台预编译),不再需要 optionalDependency 的特判。
|
|
130
131
|
async function ensureDependencies() {
|
|
131
132
|
const agentDir = new URL('.', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
|
|
132
133
|
const nodeModulesPath = join(agentDir, 'node_modules');
|
|
@@ -140,20 +141,6 @@ async function ensureDependencies() {
|
|
|
140
141
|
} catch (e) {
|
|
141
142
|
console.warn('[Startup] npm install failed:', e.message);
|
|
142
143
|
}
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 检查 node-pty 是否可用(optionalDependency,可能需要编译)
|
|
147
|
-
try {
|
|
148
|
-
await import('node-pty');
|
|
149
|
-
} catch (e) {
|
|
150
|
-
console.log('[Startup] node-pty not available, attempting install...');
|
|
151
|
-
try {
|
|
152
|
-
await execAsync('npm install node-pty', { cwd: agentDir, timeout: 120000 });
|
|
153
|
-
console.log('[Startup] node-pty installed successfully');
|
|
154
|
-
} catch (installErr) {
|
|
155
|
-
console.warn('[Startup] node-pty install failed (terminal will be unavailable):', installErr.message);
|
|
156
|
-
}
|
|
157
144
|
}
|
|
158
145
|
}
|
|
159
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.670",
|
|
4
4
|
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -54,13 +54,11 @@
|
|
|
54
54
|
"ext": "js"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
57
58
|
"dotenv": "^16.3.1",
|
|
58
59
|
"tweetnacl": "^1.0.3",
|
|
59
60
|
"tweetnacl-util": "^0.15.1",
|
|
60
61
|
"uuid": "^11.1.0",
|
|
61
62
|
"ws": "^8.16.0"
|
|
62
|
-
},
|
|
63
|
-
"optionalDependencies": {
|
|
64
|
-
"node-pty": "^1.0.0"
|
|
65
63
|
}
|
|
66
64
|
}
|
package/terminal.js
CHANGED
|
@@ -4,15 +4,20 @@ import { join, dirname } from 'path';
|
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import ctx from './context.js';
|
|
6
6
|
|
|
7
|
+
// Package name of the PTY backend. We use the Homebridge prebuilt fork
|
|
8
|
+
// because upstream node-pty ships no Linux prebuilds and falls back to
|
|
9
|
+
// node-gyp + C++20, which silently fails on older toolchains. The fork
|
|
10
|
+
// ships prebuilds for darwin/linux/win32 across x64/arm64 (incl. musl).
|
|
11
|
+
const PTY_PKG = '@homebridge/node-pty-prebuilt-multiarch';
|
|
12
|
+
|
|
7
13
|
// Ensure spawn-helper has executable permission on Unix systems.
|
|
8
14
|
// npm may strip execute bits from prebuilt binaries, causing
|
|
9
15
|
// "posix_spawnp failed" on macOS/Linux.
|
|
10
|
-
// TODO: Remove this workaround once node-pty ships with correct permissions.
|
|
11
16
|
function ensureSpawnHelperPermissions() {
|
|
12
17
|
if (platform() === 'win32') return;
|
|
13
18
|
try {
|
|
14
19
|
const cjsRequire = createRequire(import.meta.url);
|
|
15
|
-
const ptyPkgPath = dirname(cjsRequire.resolve(
|
|
20
|
+
const ptyPkgPath = dirname(cjsRequire.resolve(`${PTY_PKG}/package.json`));
|
|
16
21
|
const targets = [
|
|
17
22
|
join(ptyPkgPath, 'prebuilds', `${platform()}-${arch()}`, 'spawn-helper'),
|
|
18
23
|
join(ptyPkgPath, 'build', 'Release', 'spawn-helper'),
|
|
@@ -30,11 +35,15 @@ function ensureSpawnHelperPermissions() {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
//
|
|
38
|
+
// Load PTY backend. With the prebuilt fork this is essentially never
|
|
39
|
+
// expected to fail at runtime — it's a regular `dependencies` entry and
|
|
40
|
+
// every supported (platform, abi) combination ships a prebuilt binary.
|
|
41
|
+
// We still catch and degrade so a single missing binary doesn't crash
|
|
42
|
+
// the whole agent on an exotic host.
|
|
34
43
|
export async function loadNodePty() {
|
|
35
44
|
if (ctx.nodePty !== null) return ctx.nodePty;
|
|
36
45
|
try {
|
|
37
|
-
let pty = await import(
|
|
46
|
+
let pty = await import(PTY_PKG);
|
|
38
47
|
if (pty.default) pty = pty.default;
|
|
39
48
|
ensureSpawnHelperPermissions();
|
|
40
49
|
ctx.nodePty = pty;
|
|
@@ -69,7 +78,7 @@ export async function handleTerminalCreate(msg) {
|
|
|
69
78
|
type: 'terminal_error',
|
|
70
79
|
conversationId,
|
|
71
80
|
terminalId,
|
|
72
|
-
message: '
|
|
81
|
+
message: 'Terminal backend is not installed. Run: npm install'
|
|
73
82
|
});
|
|
74
83
|
return;
|
|
75
84
|
}
|
package/unify/config-api.js
CHANGED
|
@@ -215,3 +215,154 @@ export function updateUnifySettings(update, dir) {
|
|
|
215
215
|
|
|
216
216
|
return merged;
|
|
217
217
|
}
|
|
218
|
+
|
|
219
|
+
// ─── Search settings (web-search backend selection + Tavily key) ────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Valid backend values. `playwright` is reserved for the upcoming
|
|
223
|
+
* playwright-service tool — its UI option is currently disabled, but we
|
|
224
|
+
* accept the literal so a hand-edited config doesn't trip validation.
|
|
225
|
+
* Anything else is rejected on write and normalized to `tavily` on read.
|
|
226
|
+
*/
|
|
227
|
+
const VALID_BACKENDS = ['tavily', 'playwright'];
|
|
228
|
+
|
|
229
|
+
function maskKey(key) {
|
|
230
|
+
if (!key || typeof key !== 'string') return null;
|
|
231
|
+
if (key.length <= 10) return '***';
|
|
232
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read the `search` section of config.json. Tavily key is returned in
|
|
237
|
+
* masked form (`tvly-d...j3dgV`) — the raw key never leaves the agent.
|
|
238
|
+
* UI uses `tavilyKeyConfigured` to decide whether the input shows a
|
|
239
|
+
* "(unchanged)" placeholder vs an empty box.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} [dir]
|
|
242
|
+
* @returns {{ backend: string, tavilyKeyConfigured: boolean, tavilyKeyMasked: string|null, disableHtmlFallback: boolean } | { error: string }}
|
|
243
|
+
*/
|
|
244
|
+
export function getSearchSettings(dir) {
|
|
245
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
246
|
+
const configPath = join(root, 'config.json');
|
|
247
|
+
const defaults = {
|
|
248
|
+
backend: 'tavily',
|
|
249
|
+
tavilyKeyConfigured: false,
|
|
250
|
+
tavilyKeyMasked: null,
|
|
251
|
+
disableHtmlFallback: false,
|
|
252
|
+
};
|
|
253
|
+
if (!existsSync(configPath)) return defaults;
|
|
254
|
+
try {
|
|
255
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
256
|
+
const s = (json && typeof json.search === 'object' && json.search) || {};
|
|
257
|
+
const backend = VALID_BACKENDS.includes(s.backend) ? s.backend : 'tavily';
|
|
258
|
+
const key = typeof s.tavilyApiKey === 'string' ? s.tavilyApiKey : '';
|
|
259
|
+
return {
|
|
260
|
+
backend,
|
|
261
|
+
tavilyKeyConfigured: !!key,
|
|
262
|
+
tavilyKeyMasked: key ? maskKey(key) : null,
|
|
263
|
+
disableHtmlFallback: !!s.disableHtmlFallback,
|
|
264
|
+
};
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update the `search` section of config.json. Update is shallow-merged:
|
|
272
|
+
* any field omitted from `update` keeps its previous value. Pass
|
|
273
|
+
* `tavilyApiKey: ''` explicitly to clear the key; pass `undefined` (or
|
|
274
|
+
* omit) to keep it unchanged — this is what the UI relies on so the
|
|
275
|
+
* "(unchanged)" placeholder doesn't accidentally wipe a saved key when
|
|
276
|
+
* the user only touches the backend radio.
|
|
277
|
+
*
|
|
278
|
+
* @param {{ backend?: string, tavilyApiKey?: string, disableHtmlFallback?: boolean }} update
|
|
279
|
+
* @param {string} [dir]
|
|
280
|
+
* @returns {ReturnType<typeof getSearchSettings>}
|
|
281
|
+
*/
|
|
282
|
+
export function updateSearchSettings(update, dir) {
|
|
283
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
284
|
+
const configPath = join(root, 'config.json');
|
|
285
|
+
|
|
286
|
+
if (!update || typeof update !== 'object') {
|
|
287
|
+
return { error: 'update payload required' };
|
|
288
|
+
}
|
|
289
|
+
if (update.backend !== undefined && !VALID_BACKENDS.includes(update.backend)) {
|
|
290
|
+
return { error: `backend must be one of: ${VALID_BACKENDS.join(', ')}` };
|
|
291
|
+
}
|
|
292
|
+
if (update.tavilyApiKey !== undefined && typeof update.tavilyApiKey !== 'string') {
|
|
293
|
+
return { error: 'tavilyApiKey must be a string' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let existing = {};
|
|
297
|
+
if (existsSync(configPath)) {
|
|
298
|
+
try {
|
|
299
|
+
existing = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
300
|
+
} catch {
|
|
301
|
+
existing = {};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
|
|
305
|
+
const merged = { ...prev };
|
|
306
|
+
if (update.backend !== undefined) merged.backend = update.backend;
|
|
307
|
+
if (update.tavilyApiKey !== undefined) merged.tavilyApiKey = update.tavilyApiKey;
|
|
308
|
+
if (update.disableHtmlFallback !== undefined) merged.disableHtmlFallback = !!update.disableHtmlFallback;
|
|
309
|
+
existing.search = merged;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return { error: `Failed to write config.json: ${e.message}` };
|
|
315
|
+
}
|
|
316
|
+
return getSearchSettings(root);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Probe Tavily's `/usage` endpoint with the currently-saved key. Returns
|
|
321
|
+
* the plan + usage fields the UI shows, or `{ error }` for any of:
|
|
322
|
+
* - no key configured
|
|
323
|
+
* - HTTP error from Tavily (401 = bad key, etc.)
|
|
324
|
+
* - network failure
|
|
325
|
+
*
|
|
326
|
+
* Called only when the user opens the Search settings tab (the user
|
|
327
|
+
* explicitly asked for "open settings → live read, don't poll"). No
|
|
328
|
+
* caching here — a stale display is more confusing than a fresh probe.
|
|
329
|
+
*
|
|
330
|
+
* @param {string} [dir]
|
|
331
|
+
* @returns {Promise<{ plan: string, used: number, limit: number|null, paygoUsed: number, paygoLimit: number|null } | { error: string }>}
|
|
332
|
+
*/
|
|
333
|
+
export async function fetchTavilyUsage(dir) {
|
|
334
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
335
|
+
const configPath = join(root, 'config.json');
|
|
336
|
+
if (!existsSync(configPath)) return { error: 'config.json not found' };
|
|
337
|
+
let key;
|
|
338
|
+
try {
|
|
339
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
340
|
+
key = json?.search?.tavilyApiKey;
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return { error: `Failed to read config.json: ${e.message}` };
|
|
343
|
+
}
|
|
344
|
+
if (!key) return { error: 'Tavily API key not configured' };
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch('https://api.tavily.com/usage', {
|
|
348
|
+
method: 'GET',
|
|
349
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) {
|
|
352
|
+
const text = await res.text().catch(() => '');
|
|
353
|
+
return { error: `${res.status} ${res.statusText} ${text.slice(0, 200)}` };
|
|
354
|
+
}
|
|
355
|
+
const data = await res.json();
|
|
356
|
+
const account = data?.account || {};
|
|
357
|
+
return {
|
|
358
|
+
plan: account.current_plan || 'unknown',
|
|
359
|
+
used: Number(account.plan_usage) || 0,
|
|
360
|
+
limit: account.plan_limit ?? null,
|
|
361
|
+
paygoUsed: Number(account.paygo_usage) || 0,
|
|
362
|
+
paygoLimit: account.paygo_limit ?? null,
|
|
363
|
+
};
|
|
364
|
+
} catch (e) {
|
|
365
|
+
return { error: e.message || String(e) };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
@@ -60,6 +60,14 @@ Guidelines:
|
|
|
60
60
|
const signal = ctx?.signal;
|
|
61
61
|
const errors = [];
|
|
62
62
|
|
|
63
|
+
// Backend preference set in UnifySettings → Search tab. When the
|
|
64
|
+
// user picks `playwright` we'd normally call the playwright-service;
|
|
65
|
+
// that service is not yet shipped (next PR), so the preference is
|
|
66
|
+
// recorded for forward-compat and we transparently fall through to
|
|
67
|
+
// Tavily / HTML scrape. Once the service lands we'll insert a
|
|
68
|
+
// tryPlaywright backend here ahead of Tavily.
|
|
69
|
+
// Anything other than 'playwright' is treated as 'tavily'.
|
|
70
|
+
|
|
63
71
|
// 1. Tavily — default, fast, structured.
|
|
64
72
|
if (search.tavilyApiKey) {
|
|
65
73
|
const r = await tryTavily(query, limit, search.tavilyApiKey, signal);
|