@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.
- package/dist/web/client.js +43 -15
- package/dist/web/server.js +26 -4
- package/package.json +1 -1
- package/src/llm/config-store.ts +61 -1
- package/src/llm/pi-ai.ts +5 -1
- package/src/web/client.js +43 -15
- package/src/web/server.ts +25 -4
package/dist/web/client.js
CHANGED
|
@@ -2862,43 +2862,71 @@ function openRemoteChannelChat(peerPublicKey, channelId, channelName) {
|
|
|
2862
2862
|
loadHistory();
|
|
2863
2863
|
}
|
|
2864
2864
|
|
|
2865
|
-
// Phase 3: 我的 ID 按钮 →
|
|
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
|
-
|
|
2896
|
+
body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ P2PDirect 还没启动, 刷新页面稍后再试</div>`;
|
|
2877
2897
|
return;
|
|
2878
2898
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/dist/web/server.js
CHANGED
|
@@ -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
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
package/src/llm/config-store.ts
CHANGED
|
@@ -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 按钮 →
|
|
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
|
-
|
|
2896
|
+
body.innerHTML = `<div style="color:#b91c1c;font-size:13px;">✗ P2PDirect 还没启动, 刷新页面稍后再试</div>`;
|
|
2877
2897
|
return;
|
|
2878
2898
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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');
|