@bolloon/bolloon-agent 0.1.29 → 0.1.30

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.
@@ -2862,43 +2862,71 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
2862
2862
  loadHistory();
2863
2863
  }
2864
2864
 
2865
- // Phase 3: 我的 ID 按钮 → 弹窗显示并支持复制自己的 P2PDirect publicKey
2865
+ // Phase 3: 我的 ID 按钮 → modal (避免 confirm 在某些环境被禁用)
2866
2866
  const showMyIdBtn = document.getElementById('show-my-p2p-id-btn');
2867
2867
  if (showMyIdBtn) {
2868
2868
  showMyIdBtn.addEventListener('click', async (e) => {
2869
2869
  e.stopPropagation();
2870
+ // 移除已有 modal
2871
+ document.getElementById('my-p2p-id-modal')?.remove();
2872
+ // 立即弹出 loading 状态 modal
2873
+ const html = `
2874
+ <div id="my-p2p-id-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10003;display:flex;align-items:center;justify-content:center;">
2875
+ <div style="background:#fff;border-radius:8px;width:480px;max-width:92vw;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,0.2);">
2876
+ <div style="padding:14px 18px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;">
2877
+ <div style="font-size:15px;font-weight:600;">🪪 我的 P2P 身份</div>
2878
+ <button id="mpim-close" style="background:none;border:none;font-size:20px;color:#6b7280;cursor:pointer;">×</button>
2879
+ </div>
2880
+ <div id="mpim-body" style="padding:16px 18px;">
2881
+ <div style="color:#6b7280;font-size:13px;margin-bottom:10px;">正在获取 publicKey…</div>
2882
+ </div>
2883
+ </div>
2884
+ </div>
2885
+ `;
2886
+ document.body.insertAdjacentHTML('beforeend', html);
2887
+ document.getElementById('mpim-close').onclick = () => document.getElementById('my-p2p-id-modal').remove();
2888
+
2870
2889
  try {
2871
2890
  const res = await fetch('/api/p2p-publickey');
2872
2891
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
2873
2892
  const data = await res.json();
2874
2893
  const pk = data.publicKey || '';
2894
+ const body = document.getElementById('mpim-body');
2875
2895
  if (!pk || pk.length !== 64) {
2876
- alert('P2PDirect 还没启动, 刷新页面稍后再试');
2896
+ body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ P2PDirect 还没启动, 刷新页面稍后再试</div>`;
2877
2897
  return;
2878
2898
  }
2879
- // 显示 + 一键复制
2880
- const ok = confirm(
2881
- `我的 P2P publicKey (64 字符 hex):\n\n${pk}\n\n` +
2882
- `点 "确定" 复制到剪贴板, 发给好友.\n` +
2883
- `好友点 "+ 好友" 粘贴这个 ID 就能加我.`
2884
- );
2885
- if (ok) {
2899
+ body.innerHTML = `
2900
+ <div style="font-size:12px;color:#6b7280;margin-bottom:8px;">把下面这串发给好友, 好友在 P2P 好友区点 "+ 好友" 粘贴即可加你:</div>
2901
+ <div style="display:flex;gap:6px;align-items:center;margin-bottom:12px;">
2902
+ <code id="mpim-pk" style="flex:1;padding:8px 10px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:4px;font-family:monospace;font-size:11px;word-break:break-all;line-height:1.4;">${escapeHtml(pk)}</code>
2903
+ <button id="mpim-copy" style="padding:8px 14px;background:#2563eb;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;white-space:nowrap;">📋 复制</button>
2904
+ </div>
2905
+ <div id="mpim-status" style="font-size:12px;color:#059669;min-height:16px;"></div>
2906
+ <div style="margin-top:14px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:11px;color:#6b7280;">
2907
+ 💡 同一个 role 重启后 publicKey 不会变, 好友不需要重新加你.
2908
+ </div>
2909
+ `;
2910
+ document.getElementById('mpim-copy').onclick = async () => {
2911
+ const statusEl = document.getElementById('mpim-status');
2886
2912
  try {
2887
2913
  await navigator.clipboard.writeText(pk);
2888
- alert('✓ 已复制到剪贴板');
2914
+ statusEl.textContent = '✓ 已复制到剪贴板';
2889
2915
  } catch {
2890
- // 旧浏览器 fallback
2891
2916
  const ta = document.createElement('textarea');
2892
2917
  ta.value = pk;
2918
+ ta.style.position = 'fixed';
2919
+ ta.style.opacity = '0';
2893
2920
  document.body.appendChild(ta);
2894
2921
  ta.select();
2895
- document.execCommand('copy');
2922
+ try { document.execCommand('copy'); statusEl.textContent = '✓ 已复制 (fallback)'; }
2923
+ catch { statusEl.textContent = '✗ 复制失败, 请手动选中复制'; }
2896
2924
  document.body.removeChild(ta);
2897
- alert('✓ 已复制到剪贴板 (fallback)');
2898
2925
  }
2899
- }
2926
+ };
2900
2927
  } catch (err) {
2901
- alert('获取 publicKey 失败: ' + (err.message || err));
2928
+ const body = document.getElementById('mpim-body');
2929
+ if (body) body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ 获取失败: ${escapeHtml(err.message || String(err))}</div>`;
2902
2930
  }
2903
2931
  });
2904
2932
  }
@@ -14,10 +14,32 @@ import { irohTransport } from '../network/iroh-transport.js';
14
14
  import { createAgentDelegateApp } from './agent-delegate-server.js';
15
15
  import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
16
16
  import { verifyMessage, isAddress, getAddress } from 'viem';
17
- // 前端资源路径:在打包后会通过 CommonJS require 加载,使用 import.meta.url
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = dirname(__filename);
20
- const webRoot = path.join(__dirname, '..', '..', 'dist', 'web');
17
+ // 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
18
+ // - src (tsx): __dirname = .../src/web → .../dist/web
19
+ // - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
20
+ // - 环境变量覆盖: BOLLOON_WEB_ROOT=xxx
21
+ // ESM scope 没有 __dirname, 这里自己声明
22
+ const __filename_local = fileURLToPath(import.meta.url);
23
+ const __dirname_local = dirname(__filename_local);
24
+ let _baseDirname = __dirname_local;
25
+ function resolveWebRoot() {
26
+ if (process.env.BOLLOON_WEB_ROOT && fsSync.existsSync(process.env.BOLLOON_WEB_ROOT)) {
27
+ return process.env.BOLLOON_WEB_ROOT;
28
+ }
29
+ const d = _baseDirname;
30
+ const candidates = [
31
+ path.join(d), // dist/web
32
+ path.join(d, '..', '..', 'dist', 'web'), // src/web → dist/web
33
+ path.join(d, '..', 'web'), // dist/ → web/ 兄弟
34
+ ];
35
+ for (const c of candidates) {
36
+ if (fsSync.existsSync(path.join(c, 'index.html')))
37
+ return c;
38
+ }
39
+ return candidates[1];
40
+ }
41
+ const webRoot = resolveWebRoot();
42
+ console.log(`[web] webRoot = ${webRoot}`);
21
43
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
22
44
  const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');
23
45
  const CHANNELS_PATH = path.join(SHARED_SESSION_PATH, 'channels.json');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -6,7 +6,7 @@
6
6
  import * as fs from 'fs/promises';
7
7
  import * as path from 'path';
8
8
 
9
- export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'local';
9
+ export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'deepseek' | 'kimi' | 'glm' | 'qwen' | 'local';
10
10
 
11
11
  export interface ProviderConfig {
12
12
  enabled: boolean;
@@ -82,6 +82,42 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
82
82
  maxTokens: 4096,
83
83
  requiresApiKey: true
84
84
  },
85
+ deepseek: {
86
+ enabled: false,
87
+ apiKey: '',
88
+ baseUrl: 'https://api.deepseek.com/v1',
89
+ model: 'deepseek-chat',
90
+ temperature: 0.7,
91
+ maxTokens: 4096,
92
+ requiresApiKey: true
93
+ },
94
+ kimi: {
95
+ enabled: false,
96
+ apiKey: '',
97
+ baseUrl: 'https://api.moonshot.cn/v1',
98
+ model: 'moonshot-v1-8k',
99
+ temperature: 0.7,
100
+ maxTokens: 4096,
101
+ requiresApiKey: true
102
+ },
103
+ glm: {
104
+ enabled: false,
105
+ apiKey: '',
106
+ baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
107
+ model: 'glm-4-flash',
108
+ temperature: 0.7,
109
+ maxTokens: 4096,
110
+ requiresApiKey: true
111
+ },
112
+ qwen: {
113
+ enabled: false,
114
+ apiKey: '',
115
+ baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
116
+ model: 'qwen-plus',
117
+ temperature: 0.7,
118
+ maxTokens: 4096,
119
+ requiresApiKey: true
120
+ },
85
121
  local: {
86
122
  enabled: false,
87
123
  apiKey: '',
@@ -100,6 +136,10 @@ export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: s
100
136
  gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
101
137
  ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
102
138
  minimax: { name: 'MiniMax', description: '国产大模型服务', requiresApiKey: true },
139
+ deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true },
140
+ kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true },
141
+ glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true },
142
+ qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true },
103
143
  local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
104
144
  };
105
145
 
@@ -121,6 +161,18 @@ function getDefaultConfig(): LLMConfig {
121
161
  if (process.env.MINIMAX_API_KEY) {
122
162
  envConfigs.minimax = { ...DEFAULT_PROVIDER_CONFIGS.minimax, enabled: true, apiKey: process.env.MINIMAX_API_KEY };
123
163
  }
164
+ if (process.env.DEEPSEEK_API_KEY) {
165
+ envConfigs.deepseek = { ...DEFAULT_PROVIDER_CONFIGS.deepseek, enabled: true, apiKey: process.env.DEEPSEEK_API_KEY };
166
+ }
167
+ if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) {
168
+ envConfigs.kimi = { ...DEFAULT_PROVIDER_CONFIGS.kimi, enabled: true, apiKey: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '' };
169
+ }
170
+ if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) {
171
+ envConfigs.glm = { ...DEFAULT_PROVIDER_CONFIGS.glm, enabled: true, apiKey: process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || '' };
172
+ }
173
+ if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) {
174
+ envConfigs.qwen = { ...DEFAULT_PROVIDER_CONFIGS.qwen, enabled: true, apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '' };
175
+ }
124
176
  if (process.env.OLLAMA_BASE_URL) {
125
177
  envConfigs.ollama = { ...DEFAULT_PROVIDER_CONFIGS.ollama, enabled: true, baseUrl: process.env.OLLAMA_BASE_URL };
126
178
  }
@@ -131,6 +183,10 @@ function getDefaultConfig(): LLMConfig {
131
183
  else if (process.env.OPENROUTER_API_KEY) activeProvider = 'openrouter';
132
184
  else if (process.env.GEMINI_API_KEY) activeProvider = 'gemini';
133
185
  else if (process.env.MINIMAX_API_KEY) activeProvider = 'minimax';
186
+ else if (process.env.DEEPSEEK_API_KEY) activeProvider = 'deepseek';
187
+ else if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) activeProvider = 'kimi';
188
+ else if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) activeProvider = 'glm';
189
+ else if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) activeProvider = 'qwen';
134
190
  else if (process.env.OLLAMA_BASE_URL) activeProvider = 'ollama';
135
191
 
136
192
  const providers = { ...DEFAULT_PROVIDER_CONFIGS };
@@ -281,6 +337,10 @@ class LLMConfigStore {
281
337
  case 'openai':
282
338
  case 'openrouter':
283
339
  case 'minimax':
340
+ case 'deepseek':
341
+ case 'kimi':
342
+ case 'glm':
343
+ case 'qwen':
284
344
  if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
285
345
  break;
286
346
  case 'anthropic':
package/src/llm/pi-ai.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
 
4
- export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'local';
4
+ export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'deepseek' | 'kimi' | 'glm' | 'qwen' | 'local';
5
5
 
6
6
  export interface ModelConfig {
7
7
  provider: ModelProvider;
@@ -105,6 +105,10 @@ export class PiAIModel {
105
105
  switch (this.provider) {
106
106
  case 'openai':
107
107
  case 'minimax':
108
+ case 'deepseek':
109
+ case 'kimi':
110
+ case 'glm':
111
+ case 'qwen':
108
112
  return this.callOpenAI(messages, temperature, maxTokens);
109
113
  case 'anthropic':
110
114
  return this.callAnthropic(messages, temperature, maxTokens);
package/src/web/client.js CHANGED
@@ -2862,43 +2862,71 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
2862
2862
  loadHistory();
2863
2863
  }
2864
2864
 
2865
- // Phase 3: 我的 ID 按钮 → 弹窗显示并支持复制自己的 P2PDirect publicKey
2865
+ // Phase 3: 我的 ID 按钮 → modal (避免 confirm 在某些环境被禁用)
2866
2866
  const showMyIdBtn = document.getElementById('show-my-p2p-id-btn');
2867
2867
  if (showMyIdBtn) {
2868
2868
  showMyIdBtn.addEventListener('click', async (e) => {
2869
2869
  e.stopPropagation();
2870
+ // 移除已有 modal
2871
+ document.getElementById('my-p2p-id-modal')?.remove();
2872
+ // 立即弹出 loading 状态 modal
2873
+ const html = `
2874
+ <div id="my-p2p-id-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10003;display:flex;align-items:center;justify-content:center;">
2875
+ <div style="background:#fff;border-radius:8px;width:480px;max-width:92vw;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,0.2);">
2876
+ <div style="padding:14px 18px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;">
2877
+ <div style="font-size:15px;font-weight:600;">🪪 我的 P2P 身份</div>
2878
+ <button id="mpim-close" style="background:none;border:none;font-size:20px;color:#6b7280;cursor:pointer;">×</button>
2879
+ </div>
2880
+ <div id="mpim-body" style="padding:16px 18px;">
2881
+ <div style="color:#6b7280;font-size:13px;margin-bottom:10px;">正在获取 publicKey…</div>
2882
+ </div>
2883
+ </div>
2884
+ </div>
2885
+ `;
2886
+ document.body.insertAdjacentHTML('beforeend', html);
2887
+ document.getElementById('mpim-close').onclick = () => document.getElementById('my-p2p-id-modal').remove();
2888
+
2870
2889
  try {
2871
2890
  const res = await fetch('/api/p2p-publickey');
2872
2891
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
2873
2892
  const data = await res.json();
2874
2893
  const pk = data.publicKey || '';
2894
+ const body = document.getElementById('mpim-body');
2875
2895
  if (!pk || pk.length !== 64) {
2876
- alert('P2PDirect 还没启动, 刷新页面稍后再试');
2896
+ body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ P2PDirect 还没启动, 刷新页面稍后再试</div>`;
2877
2897
  return;
2878
2898
  }
2879
- // 显示 + 一键复制
2880
- const ok = confirm(
2881
- `我的 P2P publicKey (64 字符 hex):\n\n${pk}\n\n` +
2882
- `点 "确定" 复制到剪贴板, 发给好友.\n` +
2883
- `好友点 "+ 好友" 粘贴这个 ID 就能加我.`
2884
- );
2885
- if (ok) {
2899
+ body.innerHTML = `
2900
+ <div style="font-size:12px;color:#6b7280;margin-bottom:8px;">把下面这串发给好友, 好友在 P2P 好友区点 "+ 好友" 粘贴即可加你:</div>
2901
+ <div style="display:flex;gap:6px;align-items:center;margin-bottom:12px;">
2902
+ <code id="mpim-pk" style="flex:1;padding:8px 10px;background:#f3f4f6;border:1px solid #d1d5db;border-radius:4px;font-family:monospace;font-size:11px;word-break:break-all;line-height:1.4;">${escapeHtml(pk)}</code>
2903
+ <button id="mpim-copy" style="padding:8px 14px;background:#2563eb;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;white-space:nowrap;">📋 复制</button>
2904
+ </div>
2905
+ <div id="mpim-status" style="font-size:12px;color:#059669;min-height:16px;"></div>
2906
+ <div style="margin-top:14px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:11px;color:#6b7280;">
2907
+ 💡 同一个 role 重启后 publicKey 不会变, 好友不需要重新加你.
2908
+ </div>
2909
+ `;
2910
+ document.getElementById('mpim-copy').onclick = async () => {
2911
+ const statusEl = document.getElementById('mpim-status');
2886
2912
  try {
2887
2913
  await navigator.clipboard.writeText(pk);
2888
- alert('✓ 已复制到剪贴板');
2914
+ statusEl.textContent = '✓ 已复制到剪贴板';
2889
2915
  } catch {
2890
- // 旧浏览器 fallback
2891
2916
  const ta = document.createElement('textarea');
2892
2917
  ta.value = pk;
2918
+ ta.style.position = 'fixed';
2919
+ ta.style.opacity = '0';
2893
2920
  document.body.appendChild(ta);
2894
2921
  ta.select();
2895
- document.execCommand('copy');
2922
+ try { document.execCommand('copy'); statusEl.textContent = '✓ 已复制 (fallback)'; }
2923
+ catch { statusEl.textContent = '✗ 复制失败, 请手动选中复制'; }
2896
2924
  document.body.removeChild(ta);
2897
- alert('✓ 已复制到剪贴板 (fallback)');
2898
2925
  }
2899
- }
2926
+ };
2900
2927
  } catch (err) {
2901
- alert('获取 publicKey 失败: ' + (err.message || err));
2928
+ const body = document.getElementById('mpim-body');
2929
+ if (body) body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ 获取失败: ${escapeHtml(err.message || String(err))}</div>`;
2902
2930
  }
2903
2931
  });
2904
2932
  }
package/src/web/server.ts CHANGED
@@ -23,10 +23,31 @@ import { createAgentDelegateApp } from './agent-delegate-server.js';
23
23
  import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
24
24
  import { verifyMessage, isAddress, getAddress } from 'viem';
25
25
 
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');
26
+ // 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
27
+ // - src (tsx): __dirname = .../src/web → .../dist/web
28
+ // - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
29
+ // - 环境变量覆盖: BOLLOON_WEB_ROOT=xxx
30
+ // ESM scope 没有 __dirname, 这里自己声明
31
+ const __filename_local = fileURLToPath(import.meta.url);
32
+ const __dirname_local = dirname(__filename_local);
33
+ let _baseDirname = __dirname_local;
34
+ function resolveWebRoot(): string {
35
+ if (process.env.BOLLOON_WEB_ROOT && fsSync.existsSync(process.env.BOLLOON_WEB_ROOT)) {
36
+ return process.env.BOLLOON_WEB_ROOT;
37
+ }
38
+ const d = _baseDirname;
39
+ const candidates = [
40
+ path.join(d), // dist/web
41
+ path.join(d, '..', '..', 'dist', 'web'), // src/web → dist/web
42
+ path.join(d, '..', 'web'), // dist/ → web/ 兄弟
43
+ ];
44
+ for (const c of candidates) {
45
+ if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
46
+ }
47
+ return candidates[1];
48
+ }
49
+ const webRoot = resolveWebRoot();
50
+ console.log(`[web] webRoot = ${webRoot}`);
30
51
 
31
52
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
32
53
  const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');