@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.
@@ -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
- // 确保依赖已安装(特别是 optionalDependencies node-pty
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.667",
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('node-pty/package.json'));
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
- // 动态加载 node-pty (optionalDependency)
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('node-pty');
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: 'node-pty is not installed. Run: npm install node-pty'
81
+ message: 'Terminal backend is not installed. Run: npm install'
73
82
  });
74
83
  return;
75
84
  }
@@ -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);