@yeaft/webchat-agent 0.1.667 → 0.1.669
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/package.json +3 -2
- package/scripts/check-pty.js +87 -0
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yeaft/webchat-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.669",
|
|
4
4
|
"description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node index.js",
|
|
13
|
-
"dev": "nodemon index.js"
|
|
13
|
+
"dev": "nodemon index.js",
|
|
14
|
+
"postinstall": "node scripts/check-pty.js"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=22.5.0"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent/scripts/check-pty.js — postinstall sanity check for node-pty.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists:
|
|
6
|
+
* `node-pty` is an optionalDependency. npm install swallows install
|
|
7
|
+
* failures silently — when the native module fails to build (e.g.
|
|
8
|
+
* Linux x64, where node-pty's tarball ships no prebuilds and the
|
|
9
|
+
* host g++ is too old to compile C++20), the agent loses its
|
|
10
|
+
* `terminal` capability with zero user-facing signal. The web UI's
|
|
11
|
+
* Terminal tab silently disappears.
|
|
12
|
+
*
|
|
13
|
+
* This postinstall script makes that failure visible: if we're on a
|
|
14
|
+
* platform where node-pty was supposed to install but didn't, print
|
|
15
|
+
* a clear warning with the recovery command. We never fail the
|
|
16
|
+
* install — pty is genuinely optional, and required-feature mode
|
|
17
|
+
* would block users who don't need a Terminal tab.
|
|
18
|
+
*
|
|
19
|
+
* Exit code is always 0 — we only print, never abort.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import { createRequire } from 'module';
|
|
24
|
+
import { platform } from 'os';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
function tryResolveNodePty() {
|
|
29
|
+
try {
|
|
30
|
+
return require.resolve('node-pty');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryLoadBinary() {
|
|
37
|
+
// node-pty's lib/index.js does `require('../build/Release/pty.node')`
|
|
38
|
+
// — if the binary is missing or ABI-incompatible, that throws. We
|
|
39
|
+
// mimic the load eagerly so the warning fires at install time, not
|
|
40
|
+
// at agent startup.
|
|
41
|
+
try {
|
|
42
|
+
require('node-pty');
|
|
43
|
+
return { ok: true };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { ok: false, error: e.message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolved = tryResolveNodePty();
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
// node-pty was never installed at all — npm skipped it for the
|
|
52
|
+
// current platform/engine combination.
|
|
53
|
+
const plat = platform();
|
|
54
|
+
if (plat === 'linux' || plat === 'darwin' || plat === 'win32') {
|
|
55
|
+
console.warn('');
|
|
56
|
+
console.warn(' ⚠ node-pty was not installed on this host.');
|
|
57
|
+
console.warn(' The agent will run, but the web UI Terminal tab');
|
|
58
|
+
console.warn(' will be hidden (terminal capability missing).');
|
|
59
|
+
if (plat === 'linux') {
|
|
60
|
+
console.warn('');
|
|
61
|
+
console.warn(' Linux fix: install a C++20-capable compiler');
|
|
62
|
+
console.warn(' (g++-10 or newer) and rebuild:');
|
|
63
|
+
console.warn('');
|
|
64
|
+
console.warn(' sudo apt-get install -y g++-10');
|
|
65
|
+
console.warn(' CXX=g++-10 npm install node-pty --include=optional');
|
|
66
|
+
} else {
|
|
67
|
+
console.warn('');
|
|
68
|
+
console.warn(' Reinstall to retry: npm install node-pty');
|
|
69
|
+
}
|
|
70
|
+
console.warn('');
|
|
71
|
+
}
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const loaded = tryLoadBinary();
|
|
76
|
+
if (!loaded.ok) {
|
|
77
|
+
console.warn('');
|
|
78
|
+
console.warn(' ⚠ node-pty resolved but failed to load native binary.');
|
|
79
|
+
console.warn(` Error: ${loaded.error}`);
|
|
80
|
+
console.warn(' The agent will run without Terminal tab support.');
|
|
81
|
+
console.warn(' Recover: npm rebuild node-pty');
|
|
82
|
+
console.warn('');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Silent on success — clean install output.
|
|
87
|
+
process.exit(0);
|
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);
|