@bolloon/bolloon-agent 0.1.29 → 0.1.32

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/src/web/server.ts CHANGED
@@ -18,15 +18,38 @@ import { documentReader } from '../documents/reader.js';
18
18
  import { initMinimax, getMinimax } from '../constraints/index.js';
19
19
  import { createAgentSession, type AgentSession, type StreamCallback, type StreamEvent } from '../agents/pi-sdk.js';
20
20
  import { llmConfigStore, type ModelProvider, PROVIDER_INFO } from '../llm/config-store.js';
21
+ import { videoConfigStore, type VideoProvider } from '../llm/video-config-store.js';
22
+ import { audioConfigStore, type AudioProvider } from '../llm/audio-config-store.js';
21
23
  import { irohTransport } from '../network/iroh-transport.js';
22
24
  import { createAgentDelegateApp } from './agent-delegate-server.js';
23
25
  import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
24
26
  import { verifyMessage, isAddress, getAddress } from 'viem';
25
27
 
26
- // 前端资源路径:在打包后会通过 CommonJS require 加载,使用 import.meta.url
27
- const __filename = fileURLToPath(import.meta.url);
28
- const __dirname = dirname(__filename);
29
- const webRoot = path.join(__dirname, '..', '..', 'dist', 'web');
28
+ // 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
29
+ // - src (tsx): __dirname = .../src/web → .../dist/web
30
+ // - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
31
+ // - 环境变量覆盖: BOLLOON_WEB_ROOT=xxx
32
+ // ESM scope 没有 __dirname, 这里自己声明
33
+ const __filename_local = fileURLToPath(import.meta.url);
34
+ const __dirname_local = dirname(__filename_local);
35
+ let _baseDirname = __dirname_local;
36
+ function resolveWebRoot(): string {
37
+ if (process.env.BOLLOON_WEB_ROOT && fsSync.existsSync(process.env.BOLLOON_WEB_ROOT)) {
38
+ return process.env.BOLLOON_WEB_ROOT;
39
+ }
40
+ const d = _baseDirname;
41
+ const candidates = [
42
+ path.join(d), // dist/web
43
+ path.join(d, '..', '..', 'dist', 'web'), // src/web → dist/web
44
+ path.join(d, '..', 'web'), // dist/ → web/ 兄弟
45
+ ];
46
+ for (const c of candidates) {
47
+ if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
48
+ }
49
+ return candidates[1];
50
+ }
51
+ const webRoot = resolveWebRoot();
52
+ console.log(`[web] webRoot = ${webRoot}`);
30
53
 
31
54
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
32
55
  const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');
@@ -2255,6 +2278,130 @@ app.get('/channels', async (_req, res) => {
2255
2278
  }
2256
2279
  });
2257
2280
 
2281
+ // ==================== 视频生成配置 (Seedance 等) ====================
2282
+
2283
+ // 获取视频生成配置
2284
+ app.get('/api/video-config', async (req, res) => {
2285
+ try {
2286
+ const config = await videoConfigStore.getConfig();
2287
+ const providerInfo = videoConfigStore.getAllProviderInfo();
2288
+
2289
+ // 脱敏:不返回 apiKey 明文
2290
+ const masked = Object.fromEntries(
2291
+ Object.entries(config.providers).map(([key, val]) => [
2292
+ key,
2293
+ { ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
2294
+ ])
2295
+ );
2296
+
2297
+ res.json({
2298
+ activeProvider: config.activeProvider,
2299
+ providers: masked,
2300
+ providerInfo
2301
+ });
2302
+ } catch (err: any) {
2303
+ res.status(500).json({ error: err.message });
2304
+ }
2305
+ });
2306
+
2307
+ // 更新视频供应商配置
2308
+ app.post('/api/video-config', async (req, res) => {
2309
+ try {
2310
+ const { provider, config } = req.body;
2311
+
2312
+ if (!provider || !config) {
2313
+ return res.status(400).json({ error: 'provider and config required' });
2314
+ }
2315
+
2316
+ // 如果前端发的是掩码(***xxx),从当前配置里取真实 key
2317
+ const currentConfig = await videoConfigStore.getProvider(provider as VideoProvider);
2318
+ if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
2319
+ config.apiKey = currentConfig.apiKey;
2320
+ }
2321
+
2322
+ await videoConfigStore.updateProvider(provider as VideoProvider, config);
2323
+ res.json({ ok: true });
2324
+ } catch (err: any) {
2325
+ res.status(500).json({ error: err.message });
2326
+ }
2327
+ });
2328
+
2329
+ // 测试视频供应商连接
2330
+ app.post('/api/video-test', async (req, res) => {
2331
+ try {
2332
+ const { provider } = req.body;
2333
+
2334
+ if (!provider) {
2335
+ return res.status(400).json({ error: 'provider required' });
2336
+ }
2337
+
2338
+ const result = await videoConfigStore.testProvider(provider as VideoProvider);
2339
+ res.json(result);
2340
+ } catch (err: any) {
2341
+ res.status(500).json({ error: err.message });
2342
+ }
2343
+ });
2344
+
2345
+ // ==================== 音频生成配置 (TTS / Music) ====================
2346
+
2347
+ // 获取音频配置
2348
+ app.get('/api/audio-config', async (req, res) => {
2349
+ try {
2350
+ const config = await audioConfigStore.getConfig();
2351
+ const providerInfo = audioConfigStore.getAllProviderInfo();
2352
+
2353
+ const masked = Object.fromEntries(
2354
+ Object.entries(config.providers).map(([key, val]) => [
2355
+ key,
2356
+ { ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
2357
+ ])
2358
+ );
2359
+
2360
+ res.json({
2361
+ activeProvider: config.activeProvider,
2362
+ providers: masked,
2363
+ providerInfo
2364
+ });
2365
+ } catch (err: any) {
2366
+ res.status(500).json({ error: err.message });
2367
+ }
2368
+ });
2369
+
2370
+ // 更新音频供应商配置
2371
+ app.post('/api/audio-config', async (req, res) => {
2372
+ try {
2373
+ const { provider, config } = req.body;
2374
+ if (!provider || !config) {
2375
+ return res.status(400).json({ error: 'provider and config required' });
2376
+ }
2377
+
2378
+ // 掩码回写真实 key
2379
+ const currentConfig = await audioConfigStore.getProvider(provider as AudioProvider);
2380
+ if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
2381
+ config.apiKey = currentConfig.apiKey;
2382
+ }
2383
+
2384
+ await audioConfigStore.updateProvider(provider as AudioProvider, config);
2385
+ res.json({ ok: true });
2386
+ } catch (err: any) {
2387
+ res.status(500).json({ error: err.message });
2388
+ }
2389
+ });
2390
+
2391
+ // 测试音频供应商连接
2392
+ app.post('/api/audio-test', async (req, res) => {
2393
+ try {
2394
+ const { provider } = req.body;
2395
+ if (!provider) {
2396
+ return res.status(400).json({ error: 'provider required' });
2397
+ }
2398
+ const result = await audioConfigStore.testProvider(provider as AudioProvider);
2399
+ res.json(result);
2400
+ } catch (err: any) {
2401
+ res.status(500).json({ error: err.message });
2402
+ }
2403
+ });
2404
+
2258
2405
  // 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
2259
2406
  // 入参: { text, mimeType, fileName, fromNodeId, source }
2260
2407
  // 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
package/src/web/style.css CHANGED
@@ -2989,6 +2989,89 @@ body {
2989
2989
  max-width: 900px;
2990
2990
  margin: 0 auto;
2991
2991
  padding: 24px;
2992
+ min-height: 100vh;
2993
+ }
2994
+
2995
+ /* Standalone api-config page: enable page-level scrolling.
2996
+ Default body has overflow:hidden (for app shell with sidebar). */
2997
+ body:has(> .api-config-page) {
2998
+ height: auto;
2999
+ min-height: 100vh;
3000
+ overflow-y: auto;
3001
+ }
3002
+
3003
+ /* Tab switcher */
3004
+ .api-tabs {
3005
+ display: flex;
3006
+ gap: 4px;
3007
+ border-bottom: 1px solid var(--border);
3008
+ margin-bottom: 24px;
3009
+ }
3010
+
3011
+ .api-tab {
3012
+ display: flex;
3013
+ align-items: center;
3014
+ gap: 8px;
3015
+ padding: 12px 20px;
3016
+ background: transparent;
3017
+ border: none;
3018
+ border-bottom: 2px solid transparent;
3019
+ color: var(--text-muted);
3020
+ font-size: 14px;
3021
+ font-weight: 500;
3022
+ cursor: pointer;
3023
+ transition: all 0.2s;
3024
+ }
3025
+
3026
+ .api-tab:hover {
3027
+ color: var(--text);
3028
+ }
3029
+
3030
+ .api-tab.active {
3031
+ color: var(--accent);
3032
+ border-bottom-color: var(--accent);
3033
+ }
3034
+
3035
+ .api-tab-icon {
3036
+ font-size: 16px;
3037
+ }
3038
+
3039
+ .api-panel {
3040
+ animation: fadeIn 0.2s ease;
3041
+ }
3042
+
3043
+ @keyframes fadeIn {
3044
+ from { opacity: 0; transform: translateY(4px); }
3045
+ to { opacity: 1; transform: translateY(0); }
3046
+ }
3047
+
3048
+ .video-intro {
3049
+ background: var(--bg-sidebar);
3050
+ border: 1px solid var(--border);
3051
+ border-radius: var(--radius);
3052
+ padding: 12px 16px;
3053
+ margin-bottom: 16px;
3054
+ color: var(--text-muted);
3055
+ font-size: 13px;
3056
+ line-height: 1.6;
3057
+ }
3058
+
3059
+ .video-intro p {
3060
+ margin: 0;
3061
+ }
3062
+
3063
+ .provider-docs {
3064
+ display: inline-block;
3065
+ margin-top: 4px;
3066
+ font-size: 12px;
3067
+ color: var(--accent);
3068
+ text-decoration: none;
3069
+ opacity: 0.8;
3070
+ }
3071
+
3072
+ .provider-docs:hover {
3073
+ opacity: 1;
3074
+ text-decoration: underline;
2992
3075
  }
2993
3076
 
2994
3077
  .loading-state {