@bolloon/bolloon-agent 0.3.21 → 0.3.23
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/agents/deny-pipeline.js +116 -0
- package/dist/agents/parse-tool-call.js +26 -4
- package/dist/agents/pi-sdk.js +247 -64
- package/dist/agents/session-store.js +87 -2
- package/dist/bootstrap/snip-collapse.js +135 -0
- package/dist/cli/loading-tui.js +64 -10
- package/dist/electron/config.js +14 -9
- package/dist/electron/dialogs.js +53 -16
- package/dist/electron/first-run.js +65 -24
- package/dist/electron/ipc.js +14 -10
- package/dist/electron/logger.js +44 -7
- package/dist/electron/main.js +45 -42
- package/dist/electron/menu.js +18 -13
- package/dist/electron/paths.js +54 -12
- package/dist/electron/server.js +57 -18
- package/dist/electron/tray.js +53 -15
- package/dist/electron/window.js +61 -22
- package/dist/electron-preload.js +19 -16
- package/dist/electron.js +4 -1
- package/dist/external-engines/delegate.js +19 -0
- package/dist/hooks/hooks-engine.js +329 -0
- package/dist/index.js +49 -23
- package/dist/llm/pi-ai.js +5 -17
- package/dist/security/tool-gate.js +8 -1
- package/dist/social/dunbar-tier.js +409 -0
- package/dist/utils/auto-update.js +51 -12
- package/dist/web/client.js +1 -1
- package/dist/web/server.js +17 -3
- package/dist/web/style.css +2 -2
- package/dist/web/ui/step-timeline.js +1 -1
- package/package.json +24 -24
|
@@ -21,13 +21,80 @@ import * as path from 'path';
|
|
|
21
21
|
import * as os from 'os';
|
|
22
22
|
export class SessionStore {
|
|
23
23
|
cacheDir;
|
|
24
|
+
/** 2026-07-29: JSONL 目录 (~/.bolloon/sessions/jsonl/) */
|
|
25
|
+
jsonlDir;
|
|
24
26
|
constructor(config = {}) {
|
|
25
27
|
this.cacheDir = config.cacheDir ?? path.join(os.homedir(), '.bolloon', 'sessions', 'cache');
|
|
28
|
+
this.jsonlDir = path.join(os.homedir(), '.bolloon', 'sessions', 'jsonl');
|
|
26
29
|
}
|
|
27
|
-
/** 当前缓存目录 (只读). */
|
|
28
30
|
get dir() {
|
|
29
31
|
return this.cacheDir;
|
|
30
32
|
}
|
|
33
|
+
/** JSONL 文件路径 */
|
|
34
|
+
jsonlPathFor(key) {
|
|
35
|
+
if (!key || key.includes('/') || key.includes('..')) {
|
|
36
|
+
throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
|
|
37
|
+
}
|
|
38
|
+
return path.join(this.jsonlDir, `${SessionStore.filenameEscape(key)}.jsonl`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 2026-07-29: Append-only JSONL — 追加一条消息.
|
|
42
|
+
* 每条消息独立一行 JSON, 不破坏历史数据.
|
|
43
|
+
* 每行格式: {ts, role, content, toolCall?, toolCallId?, toolResult?}
|
|
44
|
+
*/
|
|
45
|
+
async appendMessageJsonl(key, msg) {
|
|
46
|
+
if (!key || key.includes('/') || key.includes('..')) {
|
|
47
|
+
throw new Error(`SessionStore: invalid key ${JSON.stringify(key)}`);
|
|
48
|
+
}
|
|
49
|
+
await fs.mkdir(this.jsonlDir, { recursive: true });
|
|
50
|
+
const filePath = this.jsonlPathFor(key);
|
|
51
|
+
const line = JSON.stringify({
|
|
52
|
+
ts: Date.now(),
|
|
53
|
+
role: msg.role,
|
|
54
|
+
content: msg.content,
|
|
55
|
+
toolCall: msg.toolCall || undefined,
|
|
56
|
+
toolCallId: msg.toolCallId || undefined,
|
|
57
|
+
toolResult: msg.toolResult || undefined,
|
|
58
|
+
}) + '\n';
|
|
59
|
+
await fs.appendFile(filePath, line, 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 2026-07-29: 从 JSONL 完整重建消息历史.
|
|
63
|
+
* 文件不存在 → 返回 []. 损坏行 → 跳过不抛错.
|
|
64
|
+
*/
|
|
65
|
+
async loadFromJsonl(key) {
|
|
66
|
+
const filePath = this.jsonlPathFor(key);
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const messages = [];
|
|
75
|
+
for (const rawLine of raw.split('\n')) {
|
|
76
|
+
const trimmed = rawLine.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const entry = JSON.parse(trimmed);
|
|
81
|
+
if (!entry.role)
|
|
82
|
+
continue;
|
|
83
|
+
messages.push({
|
|
84
|
+
role: entry.role,
|
|
85
|
+
content: entry.content || '',
|
|
86
|
+
toolCall: entry.toolCall,
|
|
87
|
+
toolCallId: entry.toolCallId,
|
|
88
|
+
toolResult: entry.toolResult,
|
|
89
|
+
timestamp: entry.ts,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// 跳过损坏行
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return messages;
|
|
97
|
+
}
|
|
31
98
|
/** 单文件路径 — 暴露出来方便测试和外部读取.
|
|
32
99
|
*
|
|
33
100
|
* 2026-07-04 fix: Windows 文件名禁止 `:` (NTFS). web server 用 `channelId:currentSessionId`
|
|
@@ -69,10 +136,28 @@ export class SessionStore {
|
|
|
69
136
|
},
|
|
70
137
|
};
|
|
71
138
|
const filePath = this.pathFor(key);
|
|
72
|
-
// 写临时文件 + rename — 防止半写损坏
|
|
73
139
|
const tmpPath = `${filePath}.tmp`;
|
|
74
140
|
await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2), 'utf-8');
|
|
75
141
|
await fs.rename(tmpPath, filePath);
|
|
142
|
+
// 2026-07-29: 同时写 JSONL (增量追加, 不覆盖)
|
|
143
|
+
// 每个 message 一个 append, 保证可审计 + 可重建
|
|
144
|
+
try {
|
|
145
|
+
await fs.mkdir(this.jsonlDir, { recursive: true });
|
|
146
|
+
const jsonlPath = this.jsonlPathFor(key);
|
|
147
|
+
const lines = [];
|
|
148
|
+
for (const msg of messages) {
|
|
149
|
+
lines.push(JSON.stringify({
|
|
150
|
+
ts: msg.timestamp || Date.now(),
|
|
151
|
+
role: msg.role,
|
|
152
|
+
content: msg.content,
|
|
153
|
+
toolCall: msg.toolCall || undefined,
|
|
154
|
+
toolCallId: msg.toolCallId || undefined,
|
|
155
|
+
toolResult: msg.toolResult || undefined,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
await fs.appendFile(jsonlPath, lines.join('\n') + '\n', 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
catch { /* JSONL 写入失败不阻塞主存储 */ }
|
|
76
161
|
}
|
|
77
162
|
/**
|
|
78
163
|
* 同步版 (PiAgentSession 里多条路径用同步 — 避免 async 链)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* snip-collapse.ts — Phase 2 & 3 (2026-07-29)
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 式: Snip (预算感知裁历史) + Context Collapse (读时虚拟投影)
|
|
5
|
+
*
|
|
6
|
+
* Phase 2 — Snip: 在每次模型调用前, 根据 token 预算裁掉最老的历史.
|
|
7
|
+
* 比 session-window LRU 更强: 保留工具调用链完整性, 不中断 invoke→result 对.
|
|
8
|
+
*
|
|
9
|
+
* Phase 3 — Context Collapse: 读时虚拟投影, 原始 messageHistory 永不破坏.
|
|
10
|
+
* 构建一个"投影"版本: 长工具结果被摘要代替, 模型看到的是压缩版.
|
|
11
|
+
*
|
|
12
|
+
* 两者都不修改 this.messageHistory — 只返回一个新的投影数组.
|
|
13
|
+
*/
|
|
14
|
+
/** 错误工具结果 / 空内容 最大保留长度 */
|
|
15
|
+
const MAX_TOOL_RESULT_SNIP_CHARS = 300;
|
|
16
|
+
/** 单条消息最大保留长度 (Budger Reduction) */
|
|
17
|
+
const MAX_MESSAGE_SNIP_CHARS = 2000;
|
|
18
|
+
/** 工具调用结果投影摘要长度 */
|
|
19
|
+
const MAX_COLLAPSED_TOOL_CHARS = 150;
|
|
20
|
+
/**
|
|
21
|
+
* Phase 2: Snip — 预算感知裁历史.
|
|
22
|
+
* 在系统提示装配前调用, 返回裁剪后的消息数组.
|
|
23
|
+
*
|
|
24
|
+
* 规则:
|
|
25
|
+
* 1. Budget Reduction: 每条消息 content 不超过 maxMessageChars
|
|
26
|
+
* 2. Snip: 如果消息数超过 maxMessages, 从最老的开始裁,
|
|
27
|
+
* 但保留最近的工具调用链 (assistant+tool 对不能拆)
|
|
28
|
+
* 3. 如果即使裁到 maxMessages 条还是太贵, 裁剪工具结果大小
|
|
29
|
+
*/
|
|
30
|
+
export function snipHistory(messages, opts = {}) {
|
|
31
|
+
const maxMessages = opts.maxMessages ?? 60;
|
|
32
|
+
const maxMessageChars = opts.maxMessageChars ?? MAX_MESSAGE_SNIP_CHARS;
|
|
33
|
+
const maxToolResultChars = opts.maxToolResultChars ?? MAX_TOOL_RESULT_SNIP_CHARS;
|
|
34
|
+
if (messages.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
// Step 1: Budget Reduction — 每条消息截断到上限
|
|
37
|
+
let budgeted = messages.map(m => {
|
|
38
|
+
if (m.content.length > maxMessageChars) {
|
|
39
|
+
return {
|
|
40
|
+
...m,
|
|
41
|
+
content: m.content.slice(0, maxMessageChars) + `\n[...截断, 原长 ${m.content.length} 字符]`,
|
|
42
|
+
originalLength: m.content.length,
|
|
43
|
+
transform: 'budget-reduce',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return m;
|
|
47
|
+
});
|
|
48
|
+
// Step 2: Snip — 超过 maxMessages 时裁最老的
|
|
49
|
+
if (budgeted.length <= maxMessages)
|
|
50
|
+
return budgeted;
|
|
51
|
+
// 工具调用链保护: 从尾部往前数, 保留最近的 assistant+tool 对
|
|
52
|
+
const keepCount = maxMessages;
|
|
53
|
+
const result = [];
|
|
54
|
+
const toRemove = budgeted.length - keepCount;
|
|
55
|
+
// 策略: 从最老的开始裁, 但要保证不会裁掉未配对的 assistant (tool_calls)
|
|
56
|
+
// 遍历时追踪 "悬空的 tool 消息" 保护
|
|
57
|
+
let removed = 0;
|
|
58
|
+
let protectedToolChain = 0; // 从尾部连续 tool 消息不裁
|
|
59
|
+
for (let i = budgeted.length - 1; i >= 0; i--) {
|
|
60
|
+
const m = budgeted[i];
|
|
61
|
+
if (m.role === 'tool' && protectedToolChain < 5) {
|
|
62
|
+
protectedToolChain++;
|
|
63
|
+
}
|
|
64
|
+
else if (m.role === 'assistant' && protectedToolChain > 0) {
|
|
65
|
+
// 遇到 assistant 代表这个工具链结束了
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
protectedToolChain = 0;
|
|
69
|
+
}
|
|
70
|
+
// 如果在保护区内, 不裁
|
|
71
|
+
if (i < budgeted.length - keepCount && budgeted.length - i > protectedToolChain) {
|
|
72
|
+
removed++;
|
|
73
|
+
if (removed <= toRemove) {
|
|
74
|
+
result.unshift({ ...m, content: '[已裁减, Snip]', transform: 'snip' });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
result.unshift(m);
|
|
79
|
+
}
|
|
80
|
+
// Step 3: 如果 tool result 仍然太长, 进一步截断
|
|
81
|
+
return result.map(m => {
|
|
82
|
+
if (m.role === 'tool' && m.content.length > maxToolResultChars) {
|
|
83
|
+
return {
|
|
84
|
+
...m,
|
|
85
|
+
content: m.content.slice(0, maxToolResultChars) + `\n[...工具结果截断]`,
|
|
86
|
+
originalLength: m.content.length,
|
|
87
|
+
transform: m.transform || 'snip-tool',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Phase 3: Context Collapse — 读时虚拟投影.
|
|
95
|
+
* 将 verbose 的工具结果替换为摘要, 但不修改原始数据.
|
|
96
|
+
*
|
|
97
|
+
* 适用场景:
|
|
98
|
+
* - 工具返回了很大的 JSON/日志 (> 500 字符)
|
|
99
|
+
* - 模型需要看到"结果", 但不需要完整内容
|
|
100
|
+
* - 原始 messageHistory 依然完整保存在 session store 里
|
|
101
|
+
*/
|
|
102
|
+
export function collapseContext(messages, opts = {}) {
|
|
103
|
+
const maxChars = opts.maxCollapsedToolChars ?? MAX_COLLAPSED_TOOL_CHARS;
|
|
104
|
+
return messages.map(m => {
|
|
105
|
+
// 只投影 tool role 和特别长的 assistant 消息
|
|
106
|
+
if (m.role === 'tool' && m.content.length > maxChars) {
|
|
107
|
+
const preview = m.content.slice(0, maxChars);
|
|
108
|
+
return {
|
|
109
|
+
...m,
|
|
110
|
+
content: `${preview}\n[...Context Collapse 投影: 原始 ${m.content.length} 字符, 已压缩为摘要]`,
|
|
111
|
+
originalLength: m.content.length,
|
|
112
|
+
transform: 'collapse',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// 也投影超长的 assistant 回复 (只保留开头)
|
|
116
|
+
if (m.role === 'assistant' && m.content.length > maxChars * 3) {
|
|
117
|
+
return {
|
|
118
|
+
...m,
|
|
119
|
+
content: m.content.slice(0, maxChars * 2) + `\n[...回复过长, 已投影, 原始 ${m.content.length} 字符]`,
|
|
120
|
+
originalLength: m.content.length,
|
|
121
|
+
transform: 'collapse',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return m;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 组合应用 Snip + Collapse (Claude Code 的 pre-model 管道).
|
|
129
|
+
* 顺序: Budget Reduction → Snip → Context Collapse
|
|
130
|
+
*/
|
|
131
|
+
export function applyPreModelPipeline(messages, snipOpts, collapseOpts) {
|
|
132
|
+
let result = snipHistory(messages, snipOpts);
|
|
133
|
+
result = collapseContext(result, collapseOpts);
|
|
134
|
+
return result;
|
|
135
|
+
}
|
package/dist/cli/loading-tui.js
CHANGED
|
@@ -319,9 +319,9 @@ function renderReference(opts) {
|
|
|
319
319
|
export function renderUserMessage(body) {
|
|
320
320
|
return renderMessageBox({ title: '✓ 已发送', body, color: C_OK, maxLines: DEFAULT_MAX_LINES });
|
|
321
321
|
}
|
|
322
|
-
/** 智能体回复框 */
|
|
322
|
+
/** 智能体回复框 (不压缩, 用户需要看到完整回复) */
|
|
323
323
|
export function renderAgentMessage(body) {
|
|
324
|
-
return renderMessageBox({ title: '◉ Bolloon Agent', body, color: C_ACCENT, maxLines:
|
|
324
|
+
return renderMessageBox({ title: '◉ Bolloon Agent', body, color: C_ACCENT, maxLines: 0 });
|
|
325
325
|
}
|
|
326
326
|
/** 循环工作流连接线: 用 ╼ ╾ 串联相邻工具框 */
|
|
327
327
|
export function flowConnector(width) {
|
|
@@ -331,7 +331,56 @@ export function flowConnector(width) {
|
|
|
331
331
|
s += unit;
|
|
332
332
|
return s.slice(0, width);
|
|
333
333
|
}
|
|
334
|
-
/**
|
|
334
|
+
/** 判断输出是否包含 git diff / 补丁内容 */
|
|
335
|
+
function isDiffOutput(text) {
|
|
336
|
+
return /^diff --git|^--- |^\+\+\+ |^@@ /m.test(text) ||
|
|
337
|
+
/^[\+\-]\s+\S/m.test(text); // + 开头的新增行 / - 开头的删除行
|
|
338
|
+
}
|
|
339
|
+
/** 把 diff 内容着色: + 行绿色, - 行红色, @@ 行蓝色, 其余不变 */
|
|
340
|
+
function colorizeDiff(text) {
|
|
341
|
+
const lines = text.split('\n');
|
|
342
|
+
const out = lines.map(line => {
|
|
343
|
+
if (line.startsWith('diff --git') || line.startsWith('---') || line.startsWith('+++')) {
|
|
344
|
+
return C_DIM + line + RESET;
|
|
345
|
+
}
|
|
346
|
+
if (line.startsWith('@@')) {
|
|
347
|
+
return fg(0x21, 0x96, 0xf3) + line + RESET; // blue
|
|
348
|
+
}
|
|
349
|
+
if (line.startsWith('+')) {
|
|
350
|
+
return C_OK + line + RESET; // green
|
|
351
|
+
}
|
|
352
|
+
if (line.startsWith('-')) {
|
|
353
|
+
return C_ERROR + line + RESET; // red
|
|
354
|
+
}
|
|
355
|
+
// 上下文行用灰色缩进
|
|
356
|
+
if (/^\s/.test(line) || /^[ \t]/.test(line)) {
|
|
357
|
+
return C_DIM + line + RESET;
|
|
358
|
+
}
|
|
359
|
+
return line;
|
|
360
|
+
});
|
|
361
|
+
return out.join('\n');
|
|
362
|
+
}
|
|
363
|
+
/** 渲染单条工具调用为紧凑列表项 (增量显示, 一行, 无 args) */
|
|
364
|
+
export function renderToolCallListItem(item, index, total) {
|
|
365
|
+
const dur = item.durationMs != null ? ` ${C_DIM}${item.durationMs}ms${RESET}` : '';
|
|
366
|
+
const label = item.status === 'error' ? `${C_ERROR}${item.tool}${RESET}` : `${C_ACCENT}${item.tool}${RESET}`;
|
|
367
|
+
return ` 🔧 ${label}${dur}`;
|
|
368
|
+
}
|
|
369
|
+
/** 紧凑渲染工具输出 (检测到 diff 则着色, 否则普通截断) — 当前不显示 body */
|
|
370
|
+
export function renderToolCallBody(item, width = 72) {
|
|
371
|
+
return '';
|
|
372
|
+
}
|
|
373
|
+
/** 增量列表标题行 (含步骤计数) */
|
|
374
|
+
export function renderToolCallsHeader(count) {
|
|
375
|
+
if (count === 0)
|
|
376
|
+
return '';
|
|
377
|
+
return ` ${C_DIM}╭─ 工具调用 (${count} 步)${RESET}`;
|
|
378
|
+
}
|
|
379
|
+
/** 增量列表结尾行 — 当前不显示 */
|
|
380
|
+
export function renderToolCallsFooter(count) {
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
/** 渲染单个工具调用为圆角框 (参数 / 状态 / 输出预览) — 旧版保留兼容 */
|
|
335
384
|
export function renderToolCall(v) {
|
|
336
385
|
const color = v.status === 'ok' ? C_OK : C_ERROR;
|
|
337
386
|
const sym = v.status === 'ok' ? '✅' : '❌';
|
|
@@ -344,12 +393,17 @@ export function renderToolCall(v) {
|
|
|
344
393
|
rows.push(`状态: ${sym} ${v.status === 'ok' ? '成功' : '失败'}${dur}`);
|
|
345
394
|
const body = v.status === 'ok' ? v.output : v.error;
|
|
346
395
|
if (body) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
396
|
+
if (isDiffOutput(body)) {
|
|
397
|
+
rows.push(colorizeDiff(body));
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const wrapped = wrapText(body, w - 6);
|
|
401
|
+
const shown = wrapped.slice(0, 3);
|
|
402
|
+
for (const l of shown)
|
|
403
|
+
rows.push(`▏ ${l}`);
|
|
404
|
+
if (wrapped.length > 3)
|
|
405
|
+
rows.push(`▏ … 已压缩 ${wrapped.length - 3} 行`);
|
|
406
|
+
}
|
|
353
407
|
}
|
|
354
408
|
const lines = [];
|
|
355
409
|
lines.push(boxTop(`${color}◉ ${v.tool}${RESET}`, w, RD));
|
|
@@ -358,7 +412,7 @@ export function renderToolCall(v) {
|
|
|
358
412
|
lines.push(boxBottom(w, RD));
|
|
359
413
|
return lines.join('\n');
|
|
360
414
|
}
|
|
361
|
-
const FRAMES = ['
|
|
415
|
+
const FRAMES = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)', 'ヽ(´▽`)/'];
|
|
362
416
|
export class LoadingTUI {
|
|
363
417
|
write;
|
|
364
418
|
timer = null;
|
package/dist/electron/config.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isDev = exports.MAIN_WINDOW_MIN = exports.MAIN_WINDOW_DEFAULT = exports.WEB_SERVER_STARTUP_TIMEOUT_MS = exports.DEFAULT_HOST = exports.DEFAULT_PORT = void 0;
|
|
4
|
+
exports.preferredPort = preferredPort;
|
|
1
5
|
/**
|
|
2
6
|
* 常量配置 (env 解析在这里集中, 不散在 main 流程)
|
|
3
7
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
const electron_1 = require("electron");
|
|
9
|
+
exports.DEFAULT_PORT = 54188;
|
|
6
10
|
/** Hard-pin to loopback; LAN exposure must be explicit. */
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
exports.DEFAULT_HOST = '127.0.0.1';
|
|
12
|
+
exports.WEB_SERVER_STARTUP_TIMEOUT_MS = 15_000;
|
|
13
|
+
exports.MAIN_WINDOW_DEFAULT = { width: 1200, height: 800 };
|
|
14
|
+
exports.MAIN_WINDOW_MIN = { width: 800, height: 600 };
|
|
15
|
+
function preferredPort() {
|
|
12
16
|
const raw = process.env.ELECTRON_PORT || process.env.PORT;
|
|
13
17
|
const n = parseInt(raw || '', 10);
|
|
14
|
-
return Number.isFinite(n) && n > 0 && n < 65536 ? n : DEFAULT_PORT;
|
|
18
|
+
return Number.isFinite(n) && n > 0 && n < 65536 ? n : exports.DEFAULT_PORT;
|
|
15
19
|
}
|
|
16
|
-
|
|
20
|
+
exports.isDev = process.env.NODE_ENV === 'development' || !electron_1.app.isPackaged;
|
|
21
|
+
//# sourceMappingURL=config.js.map
|
package/dist/electron/dialogs.js
CHANGED
|
@@ -1,25 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerDialogIpc = registerDialogIpc;
|
|
1
37
|
/**
|
|
2
38
|
* 文件 dialog 桥 (open / save / dir) + 安全的 fs 桥 (read / write / exists)
|
|
3
39
|
*
|
|
4
40
|
* 5MB read 上限保护 — 渲染进程直接 fs.readFile 没法做限制, 走主进程就有界
|
|
5
41
|
* 所有 handler 解析 event.sender 拿到 window, 让 dialog 模态在该窗口上
|
|
6
42
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
43
|
+
const electron_1 = require("electron");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const logger_1 = require("./logger");
|
|
11
47
|
const MAX_READ_BYTES = 5 * 1024 * 1024; // 5MB
|
|
12
48
|
function windowFor(event) {
|
|
13
|
-
return BrowserWindow.fromWebContents(event.sender);
|
|
49
|
+
return electron_1.BrowserWindow.fromWebContents(event.sender);
|
|
14
50
|
}
|
|
15
51
|
function resolveSafe(target) {
|
|
16
52
|
// 不去硬限制路径 — user 给 renderer 暴露 fs 已经信任了, 这里只 normalize
|
|
17
53
|
return path.resolve(target);
|
|
18
54
|
}
|
|
19
|
-
|
|
20
|
-
ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
|
|
55
|
+
function registerDialogIpc() {
|
|
56
|
+
electron_1.ipcMain.handle('dialog:open-file', async (event, opts = {}) => {
|
|
21
57
|
const win = windowFor(event);
|
|
22
|
-
const result = await dialog.showOpenDialog(win, {
|
|
58
|
+
const result = await electron_1.dialog.showOpenDialog(win, {
|
|
23
59
|
title: opts.title,
|
|
24
60
|
defaultPath: opts.defaultPath,
|
|
25
61
|
filters: opts.filters,
|
|
@@ -27,25 +63,25 @@ export function registerDialogIpc() {
|
|
|
27
63
|
});
|
|
28
64
|
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
29
65
|
});
|
|
30
|
-
ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
|
|
66
|
+
electron_1.ipcMain.handle('dialog:save-file', async (event, opts = {}) => {
|
|
31
67
|
const win = windowFor(event);
|
|
32
|
-
const result = await dialog.showSaveDialog(win, {
|
|
68
|
+
const result = await electron_1.dialog.showSaveDialog(win, {
|
|
33
69
|
title: opts.title,
|
|
34
70
|
defaultPath: opts.defaultPath,
|
|
35
71
|
filters: opts.filters,
|
|
36
72
|
});
|
|
37
73
|
return { canceled: result.canceled, filePath: result.filePath };
|
|
38
74
|
});
|
|
39
|
-
ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
|
|
75
|
+
electron_1.ipcMain.handle('dialog:open-directory', async (event, opts = {}) => {
|
|
40
76
|
const win = windowFor(event);
|
|
41
|
-
const result = await dialog.showOpenDialog(win, {
|
|
77
|
+
const result = await electron_1.dialog.showOpenDialog(win, {
|
|
42
78
|
title: opts.title,
|
|
43
79
|
defaultPath: opts.defaultPath,
|
|
44
80
|
properties: ['openDirectory', 'createDirectory'],
|
|
45
81
|
});
|
|
46
82
|
return { canceled: result.canceled, filePaths: result.filePaths };
|
|
47
83
|
});
|
|
48
|
-
ipcMain.handle('fs:read-text-file', async (_event, opts) => {
|
|
84
|
+
electron_1.ipcMain.handle('fs:read-text-file', async (_event, opts) => {
|
|
49
85
|
const target = resolveSafe(opts.path);
|
|
50
86
|
const stat = fs.statSync(target);
|
|
51
87
|
if (stat.size > MAX_READ_BYTES) {
|
|
@@ -53,12 +89,12 @@ export function registerDialogIpc() {
|
|
|
53
89
|
}
|
|
54
90
|
return fs.readFileSync(target, { encoding: opts.encoding ?? 'utf8' });
|
|
55
91
|
});
|
|
56
|
-
ipcMain.handle('fs:write-text-file', async (_event, opts) => {
|
|
92
|
+
electron_1.ipcMain.handle('fs:write-text-file', async (_event, opts) => {
|
|
57
93
|
const target = resolveSafe(opts.path);
|
|
58
94
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
95
|
fs.writeFileSync(target, opts.content, { encoding: opts.encoding ?? 'utf8' });
|
|
60
96
|
});
|
|
61
|
-
ipcMain.handle('fs:path-exists', async (_event, opts) => {
|
|
97
|
+
electron_1.ipcMain.handle('fs:path-exists', async (_event, opts) => {
|
|
62
98
|
try {
|
|
63
99
|
fs.accessSync(resolveSafe(opts.path));
|
|
64
100
|
return true;
|
|
@@ -67,5 +103,6 @@ export function registerDialogIpc() {
|
|
|
67
103
|
return false;
|
|
68
104
|
}
|
|
69
105
|
});
|
|
70
|
-
log('dialog + fs IPC handlers registered');
|
|
106
|
+
(0, logger_1.log)('dialog + fs IPC handlers registered');
|
|
71
107
|
}
|
|
108
|
+
//# sourceMappingURL=dialogs.js.map
|
|
@@ -1,14 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.hasSeenFirstRun = hasSeenFirstRun;
|
|
37
|
+
exports.markFirstRunSeen = markFirstRunSeen;
|
|
38
|
+
exports.showFirstRunOverlay = showFirstRunOverlay;
|
|
39
|
+
exports.registerFirstRunIpc = registerFirstRunIpc;
|
|
40
|
+
exports.maybeShowFirstRun = maybeShowFirstRun;
|
|
1
41
|
/**
|
|
2
42
|
* 首启检测 + 引导浮层
|
|
3
43
|
*
|
|
4
44
|
* 标记文件写在 userData (不是 ~/.bolloon/), 卸载 app 自然清掉
|
|
5
45
|
* 引导窗是父主窗的 modal, frame=false, 透明背景; 关闭时标记写入
|
|
6
46
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
47
|
+
const electron_1 = require("electron");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const paths_1 = require("./paths");
|
|
51
|
+
const logger_1 = require("./logger");
|
|
12
52
|
const OVERLAY_HTML = `
|
|
13
53
|
<!DOCTYPE html>
|
|
14
54
|
<html>
|
|
@@ -64,26 +104,26 @@ const OVERLAY_HTML = `
|
|
|
64
104
|
</body>
|
|
65
105
|
</html>
|
|
66
106
|
`;
|
|
67
|
-
|
|
107
|
+
function hasSeenFirstRun() {
|
|
68
108
|
try {
|
|
69
|
-
return fs.existsSync(firstRunFlagPath());
|
|
109
|
+
return fs.existsSync((0, paths_1.firstRunFlagPath)());
|
|
70
110
|
}
|
|
71
111
|
catch {
|
|
72
112
|
return false;
|
|
73
113
|
}
|
|
74
114
|
}
|
|
75
|
-
|
|
115
|
+
function markFirstRunSeen() {
|
|
76
116
|
try {
|
|
77
|
-
fs.mkdirSync(path.dirname(firstRunFlagPath()), { recursive: true });
|
|
78
|
-
fs.writeFileSync(firstRunFlagPath(), new Date().toISOString());
|
|
117
|
+
fs.mkdirSync(path.dirname((0, paths_1.firstRunFlagPath)()), { recursive: true });
|
|
118
|
+
fs.writeFileSync((0, paths_1.firstRunFlagPath)(), new Date().toISOString());
|
|
79
119
|
}
|
|
80
120
|
catch (err) {
|
|
81
|
-
log(`写入首启标记失败: ${err.message}`, 'warn');
|
|
121
|
+
(0, logger_1.log)(`写入首启标记失败: ${err.message}`, 'warn');
|
|
82
122
|
}
|
|
83
123
|
}
|
|
84
|
-
|
|
124
|
+
function showFirstRunOverlay(parent) {
|
|
85
125
|
return new Promise((resolve) => {
|
|
86
|
-
const overlay = new BrowserWindow({
|
|
126
|
+
const overlay = new electron_1.BrowserWindow({
|
|
87
127
|
parent,
|
|
88
128
|
modal: true,
|
|
89
129
|
frame: false,
|
|
@@ -103,27 +143,28 @@ export function showFirstRunOverlay(parent) {
|
|
|
103
143
|
markFirstRunSeen();
|
|
104
144
|
overlay.close();
|
|
105
145
|
};
|
|
106
|
-
ipcMain.once('first-run:ack', handler);
|
|
146
|
+
electron_1.ipcMain.once('first-run:ack', handler);
|
|
107
147
|
overlay.on('closed', () => {
|
|
108
|
-
ipcMain.removeListener('first-run:ack', handler);
|
|
148
|
+
electron_1.ipcMain.removeListener('first-run:ack', handler);
|
|
109
149
|
resolve();
|
|
110
150
|
});
|
|
111
151
|
});
|
|
112
152
|
}
|
|
113
153
|
/** 注册 IPC handlers (给 preload 桥用) */
|
|
114
|
-
|
|
115
|
-
ipcMain.handle('first-run:seen', () => hasSeenFirstRun());
|
|
116
|
-
ipcMain.handle('first-run:mark-seen', () => { markFirstRunSeen(); });
|
|
154
|
+
function registerFirstRunIpc() {
|
|
155
|
+
electron_1.ipcMain.handle('first-run:seen', () => hasSeenFirstRun());
|
|
156
|
+
electron_1.ipcMain.handle('first-run:mark-seen', () => { markFirstRunSeen(); });
|
|
117
157
|
// 同步值 (不用 ipcRenderer.invoke 的 await) — 用 exposeInMainWorld 的 sync getter 更顺
|
|
118
|
-
ipcMain.handle('first-run:data-dir', () => dataDir());
|
|
119
|
-
ipcMain.handle('first-run:logs-dir', () => logsDir());
|
|
120
|
-
log('first-run IPC handlers registered');
|
|
158
|
+
electron_1.ipcMain.handle('first-run:data-dir', () => (0, paths_1.dataDir)());
|
|
159
|
+
electron_1.ipcMain.handle('first-run:logs-dir', () => (0, paths_1.logsDir)());
|
|
160
|
+
(0, logger_1.log)('first-run IPC handlers registered');
|
|
121
161
|
}
|
|
122
162
|
/** 包装 — 决定要不要弹 overlay */
|
|
123
|
-
|
|
163
|
+
async function maybeShowFirstRun(parent) {
|
|
124
164
|
if (hasSeenFirstRun())
|
|
125
165
|
return;
|
|
126
|
-
log('首启 — 弹出引导');
|
|
166
|
+
(0, logger_1.log)('首启 — 弹出引导');
|
|
127
167
|
await showFirstRunOverlay(parent);
|
|
128
|
-
app.addRecentDocument(firstRunFlagPath()); // 跟踪最近文档, 让 user 知道有这文件
|
|
168
|
+
electron_1.app.addRecentDocument((0, paths_1.firstRunFlagPath)()); // 跟踪最近文档, 让 user 知道有这文件
|
|
129
169
|
}
|
|
170
|
+
//# sourceMappingURL=first-run.js.map
|
package/dist/electron/ipc.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCoreIpc = registerCoreIpc;
|
|
1
4
|
/**
|
|
2
5
|
* IPC handler 集中注册 — version / userData path / open-external (legacy 3 个)
|
|
3
6
|
* dialog/fs 的注册在 dialogs.ts; updater 的注册在 updater.ts
|
|
4
7
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ipcMain.handle('get-version', () => app.getVersion());
|
|
10
|
-
ipcMain.handle('get-user-data-path', () => userDataDir());
|
|
11
|
-
ipcMain.handle('get-data-path', () => dataDir()); // 跟 userData 分开, 渲染层要用
|
|
12
|
-
ipcMain.handle('open-external', async (_event, url) => {
|
|
13
|
-
await shell.openExternal(url);
|
|
8
|
+
const electron_1 = require("electron");
|
|
9
|
+
const paths_1 = require("./paths");
|
|
10
|
+
const logger_1 = require("./logger");
|
|
11
|
+
function registerCoreIpc() {
|
|
12
|
+
electron_1.ipcMain.handle('get-version', () => electron_1.app.getVersion());
|
|
13
|
+
electron_1.ipcMain.handle('get-user-data-path', () => (0, paths_1.userDataDir)());
|
|
14
|
+
electron_1.ipcMain.handle('get-data-path', () => (0, paths_1.dataDir)()); // 跟 userData 分开, 渲染层要用
|
|
15
|
+
electron_1.ipcMain.handle('open-external', async (_event, url) => {
|
|
16
|
+
await electron_1.shell.openExternal(url);
|
|
14
17
|
});
|
|
15
|
-
log('core IPC handlers registered');
|
|
18
|
+
(0, logger_1.log)('core IPC handlers registered');
|
|
16
19
|
}
|
|
20
|
+
//# sourceMappingURL=ipc.js.map
|