cli-link 0.0.5 → 0.0.7
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/bin/agentpilot.js +1 -0
- package/dist/client/assets/History-D2xDopni.js +4 -0
- package/dist/client/assets/ImageViewer-DuegU_fC.js +1 -0
- package/dist/client/assets/MarkdownRenderer-CsyizEL3.js +1 -0
- package/dist/client/assets/{PageTopBar-C8j-5s_3.js → PageTopBar-CQwjO6Af.js} +1 -1
- package/dist/client/assets/Session-B0s5zBGg.js +7 -0
- package/dist/client/assets/Settings-CfHFmJdV.js +1 -0
- package/dist/client/assets/Workspace-Cfl0mbNE.js +4 -0
- package/dist/client/assets/WorkspaceLinkedText-DCVYd9x-.js +2 -0
- package/dist/client/assets/c-BIGW1oBm.js +1 -0
- package/dist/client/assets/cpp-DIPi6g--.js +1 -0
- package/dist/client/assets/csharp-DSvCPggb.js +1 -0
- package/dist/client/assets/dart-bE4Kk8sk.js +1 -0
- package/dist/client/assets/dotenv-Da5cRb03.js +1 -0
- package/dist/client/assets/go-C27-OAKa.js +1 -0
- package/dist/client/assets/graphql-pNE0_Gx8.js +1 -0
- package/dist/client/assets/groovy-gcz8RCvz.js +1 -0
- package/dist/client/assets/index-BCg3ymV3.css +1 -0
- package/dist/client/assets/index-CrJqHlc8.js +2 -0
- package/dist/client/assets/java-VnEXKtx_.js +148 -0
- package/dist/client/assets/jsx-g9-lgVsj.js +1 -0
- package/dist/client/assets/kotlin-BdnUsdx6.js +1 -0
- package/dist/client/assets/less-B1dDrJ26.js +1 -0
- package/dist/client/assets/lua-BaeVxFsk.js +1 -0
- package/dist/client/assets/makefile-CHLpvVh8.js +1 -0
- package/dist/client/assets/php-BcCyJq-p.js +1 -0
- package/dist/client/assets/properties-DTPjHERo.js +1 -0
- package/dist/client/assets/ruby-BwImf3Ka.js +1 -0
- package/dist/client/assets/rust-B1yitclQ.js +1 -0
- package/dist/client/assets/scss-lMagJa-5.js +1 -0
- package/dist/client/assets/sql-CRqJ_cUM.js +1 -0
- package/dist/client/assets/svelte-B4a9v_or.js +1 -0
- package/dist/client/assets/swift-D82vCrfD.js +1 -0
- package/dist/client/assets/toml-vGWfd6FD.js +1 -0
- package/dist/client/assets/{vendor-icons-CNN4EKVi.js → vendor-icons-CMXJHDEv.js} +125 -65
- package/dist/client/assets/vendor-markdown--d-T3AbU.js +37 -0
- package/dist/client/assets/{vendor-motion-n6Lx6G4a.js → vendor-motion-D0ZmPdi9.js} +1 -1
- package/dist/client/assets/{vendor-react-DSV5aFEg.js → vendor-react-CcDXZHn_.js} +1 -1
- package/dist/client/assets/{vendor-virtual-CcftJrIC.js → vendor-virtual-DJI7OicV.js} +1 -1
- package/dist/client/assets/vue-DBXACu8K.js +1 -0
- package/dist/client/assets/workspace-return-FrQUv7g3.js +1 -0
- package/dist/client/index.html +18 -4
- package/dist/server/cli-manager.js +151 -26
- package/dist/server/codex-history.js +119 -17
- package/dist/server/index.js +1051 -65
- package/dist/server/store.js +369 -27
- package/dist/server/terminal-qr.js +17 -314
- package/package.json +5 -3
- package/dist/client/assets/History-BxJVDFpN.js +0 -3
- package/dist/client/assets/MarkdownRenderer-BO-KS_L1.js +0 -1
- package/dist/client/assets/Session-CQFXA2Sr.js +0 -11
- package/dist/client/assets/Settings-DYmjRmoN.js +0 -1
- package/dist/client/assets/Workspace-D8kv9euM.js +0 -8
- package/dist/client/assets/WorkspaceLinkedText-DQyPLk-X.js +0 -2
- package/dist/client/assets/code-highlight-CEcsuMpw.js +0 -1
- package/dist/client/assets/index-BXT2BylN.css +0 -1
- package/dist/client/assets/index-DOgH1Kf3.js +0 -2
- package/dist/client/assets/vendor-markdown-BDwu-Ux6.js +0 -35
package/dist/server/index.js
CHANGED
|
@@ -3,11 +3,12 @@ import { createServer as createHttpsServer } from 'https';
|
|
|
3
3
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
4
|
import { CLIManager } from './cli-manager.js';
|
|
5
5
|
import { renderTerminalQr } from './terminal-qr.js';
|
|
6
|
+
import { syncCodexHistory } from './codex-history.js';
|
|
6
7
|
import * as store from './store.js';
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as os from 'os';
|
|
10
|
-
import { timingSafeEqual } from 'crypto';
|
|
11
|
+
import { createHash, randomUUID, timingSafeEqual } from 'crypto';
|
|
11
12
|
import { execFileSync } from 'child_process';
|
|
12
13
|
import { fileURLToPath } from 'url';
|
|
13
14
|
const PORT = parseInt(process.env.PORT || '3101', 10);
|
|
@@ -19,6 +20,11 @@ const MAC_IDLE_SLEEP_PREVENTED = process.env.AGENTPILOT_CAFFEINATE === '1';
|
|
|
19
20
|
const AUTH_COOKIE_NAME = 'agentpilot_token';
|
|
20
21
|
const AUTH_TOKEN = (process.env.AGENTPILOT_AUTH_TOKEN || '').trim();
|
|
21
22
|
const AUTH_ENABLED = AUTH_TOKEN.length > 0;
|
|
23
|
+
const PACKAGE_VERSION = (process.env.AGENTPILOT_PACKAGE_VERSION || '').trim();
|
|
24
|
+
const AUTH_REQUIRED_MESSAGE = '访问 token 缺失或无效,请使用启动终端打印的链接重新打开。';
|
|
25
|
+
const SHARE_TITLE = 'AgentPilot - AI Agent 遥控器';
|
|
26
|
+
const SHARE_DESCRIPTION = '在手机浏览器里遥控本机 Claude Code CLI 和 Codex CLI:发任务、看输出、审确认、切换工作目录、查看代码和历史。';
|
|
27
|
+
const SHARE_IMAGE_PATH = '/icons/icon-512.png';
|
|
22
28
|
function resolveEnvPath(value) {
|
|
23
29
|
if (value === '~')
|
|
24
30
|
return os.homedir();
|
|
@@ -99,6 +105,119 @@ server.on('upgrade', (req, socket, head) => {
|
|
|
99
105
|
});
|
|
100
106
|
const cliManager = new CLIManager();
|
|
101
107
|
const clients = new Set();
|
|
108
|
+
let promptQueue = [];
|
|
109
|
+
let promptQueuePaused = false;
|
|
110
|
+
let promptQueueIdCounter = 0;
|
|
111
|
+
let promptQueueDispatching = false;
|
|
112
|
+
let promptQueueHoldUntil = 0;
|
|
113
|
+
let promptQueueDrainTimer = null;
|
|
114
|
+
const PROMPT_QUEUE_IDLE_SETTLE_MS = 800;
|
|
115
|
+
function getPromptQueuePayload() {
|
|
116
|
+
return {
|
|
117
|
+
promptQueue: promptQueue.map(item => ({ ...item })),
|
|
118
|
+
promptQueuePaused,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function broadcastPromptQueueChanged() {
|
|
122
|
+
broadcast({
|
|
123
|
+
type: 'prompt_queue_changed',
|
|
124
|
+
...getPromptQueuePayload(),
|
|
125
|
+
time: now(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function clearPromptQueue() {
|
|
129
|
+
promptQueue = [];
|
|
130
|
+
promptQueuePaused = false;
|
|
131
|
+
promptQueueDispatching = false;
|
|
132
|
+
promptQueueHoldUntil = 0;
|
|
133
|
+
if (promptQueueDrainTimer) {
|
|
134
|
+
clearTimeout(promptQueueDrainTimer);
|
|
135
|
+
promptQueueDrainTimer = null;
|
|
136
|
+
}
|
|
137
|
+
broadcastPromptQueueChanged();
|
|
138
|
+
}
|
|
139
|
+
function hasPromptPayload(prompt) {
|
|
140
|
+
return !!prompt.content.trim() || prompt.attachments.length > 0;
|
|
141
|
+
}
|
|
142
|
+
function dispatchPromptToCli(prompt) {
|
|
143
|
+
if (!hasPromptPayload(prompt))
|
|
144
|
+
return;
|
|
145
|
+
cliManager.sendInput({
|
|
146
|
+
content: prompt.content,
|
|
147
|
+
attachments: prompt.attachments,
|
|
148
|
+
});
|
|
149
|
+
broadcast({
|
|
150
|
+
type: 'user_message',
|
|
151
|
+
content: prompt.content,
|
|
152
|
+
attachments: prompt.attachments,
|
|
153
|
+
details: prompt.attachments.length > 0 ? { attachments: prompt.attachments } : undefined,
|
|
154
|
+
time: now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function maybeDrainPromptQueue() {
|
|
158
|
+
const holdMs = promptQueueHoldUntil - Date.now();
|
|
159
|
+
if (holdMs > 0) {
|
|
160
|
+
schedulePromptQueueDrain(holdMs);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (promptQueueDispatching ||
|
|
164
|
+
promptQueuePaused ||
|
|
165
|
+
cliManager.status !== 'idle' ||
|
|
166
|
+
promptQueue.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const nextPrompt = promptQueue.shift();
|
|
170
|
+
if (!nextPrompt)
|
|
171
|
+
return;
|
|
172
|
+
promptQueueDispatching = true;
|
|
173
|
+
broadcastPromptQueueChanged();
|
|
174
|
+
dispatchPromptToCli(nextPrompt);
|
|
175
|
+
}
|
|
176
|
+
function schedulePromptQueueDrain(delayMs) {
|
|
177
|
+
if (promptQueueDrainTimer)
|
|
178
|
+
clearTimeout(promptQueueDrainTimer);
|
|
179
|
+
promptQueueDrainTimer = setTimeout(() => {
|
|
180
|
+
promptQueueDrainTimer = null;
|
|
181
|
+
maybeDrainPromptQueue();
|
|
182
|
+
}, Math.max(0, delayMs));
|
|
183
|
+
}
|
|
184
|
+
function shouldQueueIncomingPrompt() {
|
|
185
|
+
return (promptQueue.length > 0 ||
|
|
186
|
+
promptQueuePaused ||
|
|
187
|
+
promptQueueDispatching ||
|
|
188
|
+
promptQueueHoldUntil > Date.now() ||
|
|
189
|
+
cliManager.status !== 'idle');
|
|
190
|
+
}
|
|
191
|
+
function enqueuePrompt(prompt) {
|
|
192
|
+
if (!hasPromptPayload(prompt))
|
|
193
|
+
return;
|
|
194
|
+
promptQueueIdCounter += 1;
|
|
195
|
+
promptQueue.push({
|
|
196
|
+
id: `queued-${Date.now()}-${promptQueueIdCounter}`,
|
|
197
|
+
content: prompt.content,
|
|
198
|
+
attachments: prompt.attachments,
|
|
199
|
+
});
|
|
200
|
+
broadcastPromptQueueChanged();
|
|
201
|
+
maybeDrainPromptQueue();
|
|
202
|
+
}
|
|
203
|
+
function removePromptFromQueue(id) {
|
|
204
|
+
const nextQueue = promptQueue.filter(item => item.id !== id);
|
|
205
|
+
if (nextQueue.length === promptQueue.length)
|
|
206
|
+
return;
|
|
207
|
+
promptQueue = nextQueue;
|
|
208
|
+
if (promptQueue.length === 0) {
|
|
209
|
+
promptQueuePaused = false;
|
|
210
|
+
promptQueueDispatching = false;
|
|
211
|
+
}
|
|
212
|
+
broadcastPromptQueueChanged();
|
|
213
|
+
}
|
|
214
|
+
function resumePromptQueue() {
|
|
215
|
+
if (!promptQueuePaused)
|
|
216
|
+
return;
|
|
217
|
+
promptQueuePaused = false;
|
|
218
|
+
broadcastPromptQueueChanged();
|
|
219
|
+
maybeDrainPromptQueue();
|
|
220
|
+
}
|
|
102
221
|
function unquoteMetaValue(value) {
|
|
103
222
|
const trimmed = value.trim();
|
|
104
223
|
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
@@ -233,6 +352,17 @@ function parseJsonField(raw) {
|
|
|
233
352
|
return undefined;
|
|
234
353
|
}
|
|
235
354
|
}
|
|
355
|
+
function toClientAttachments(raw) {
|
|
356
|
+
try {
|
|
357
|
+
return normalizePromptAttachments(raw, {
|
|
358
|
+
maxBytes: MAX_GENERATED_IMAGE_ATTACHMENT_BYTES,
|
|
359
|
+
maxCount: MAX_GENERATED_IMAGE_ATTACHMENTS_PER_TURN,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
236
366
|
function getSessionConfig(session) {
|
|
237
367
|
const parsed = parseJsonField(session?.cli_config);
|
|
238
368
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
@@ -266,6 +396,7 @@ function toClientMessage(m) {
|
|
|
266
396
|
return null;
|
|
267
397
|
const parsedDetails = parseJsonField(m.details);
|
|
268
398
|
const isQuestion = mappedType === 'question' && m.type === 'ask_question';
|
|
399
|
+
const attachments = toClientAttachments(parsedDetails?.attachments);
|
|
269
400
|
return {
|
|
270
401
|
seq: m.seq,
|
|
271
402
|
id: m.id,
|
|
@@ -279,12 +410,64 @@ function toClientMessage(m) {
|
|
|
279
410
|
toolResult: m.toolResult || undefined,
|
|
280
411
|
permission: parseJsonField(m.permission),
|
|
281
412
|
details: parsedDetails,
|
|
413
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
282
414
|
question: isQuestion && parsedDetails ? { questions: parsedDetails.questions || [], toolUseId: parsedDetails.toolUseId } : undefined,
|
|
283
415
|
};
|
|
284
416
|
}
|
|
285
417
|
function toClientMessages(messages) {
|
|
286
418
|
return messages.map(toClientMessage).filter((m) => m !== null);
|
|
287
419
|
}
|
|
420
|
+
function cleanHistoryPreview(content, maxLength = 96) {
|
|
421
|
+
const normalized = content
|
|
422
|
+
.replace(/```[\s\S]*?```/g, ' 代码片段 ')
|
|
423
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
424
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
425
|
+
.replace(/^#+\s*/gm, '')
|
|
426
|
+
.replace(/[>*_~]/g, ' ')
|
|
427
|
+
.split(/\r?\n/)
|
|
428
|
+
.map(line => line.trim())
|
|
429
|
+
.filter(Boolean)
|
|
430
|
+
.join(' ')
|
|
431
|
+
.replace(/\s+/g, ' ')
|
|
432
|
+
.trim();
|
|
433
|
+
if (!normalized)
|
|
434
|
+
return '';
|
|
435
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength).trimEnd()}...` : normalized;
|
|
436
|
+
}
|
|
437
|
+
function buildHistoryPreview(messages) {
|
|
438
|
+
const recentFirst = [...messages].sort((a, b) => b.seq - a.seq);
|
|
439
|
+
const preferredTypes = new Set(['user', 'ai', 'question', 'confirm', 'error']);
|
|
440
|
+
const fallbackTypes = new Set(['tool', 'system', 'thinking']);
|
|
441
|
+
const labels = {
|
|
442
|
+
user: '你',
|
|
443
|
+
ai: 'AI',
|
|
444
|
+
question: '问题',
|
|
445
|
+
confirm: '确认',
|
|
446
|
+
error: '错误',
|
|
447
|
+
tool: '工具',
|
|
448
|
+
system: '系统',
|
|
449
|
+
thinking: '思考',
|
|
450
|
+
};
|
|
451
|
+
const pick = (allowedTypes) => {
|
|
452
|
+
for (const raw of recentFirst) {
|
|
453
|
+
const msg = toClientMessage(raw);
|
|
454
|
+
if (!msg || !allowedTypes.has(msg.type))
|
|
455
|
+
continue;
|
|
456
|
+
const attachmentCount = Array.isArray(msg.attachments) ? msg.attachments.length : 0;
|
|
457
|
+
const snippet = cleanHistoryPreview(msg.content) || (attachmentCount > 0 ? `图片附件 ${attachmentCount} 张` : '');
|
|
458
|
+
if (!snippet)
|
|
459
|
+
continue;
|
|
460
|
+
const label = labels[msg.type] || '消息';
|
|
461
|
+
return {
|
|
462
|
+
preview: `${label}:${snippet}`,
|
|
463
|
+
lastMessageType: msg.type,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return {};
|
|
467
|
+
};
|
|
468
|
+
const preferred = pick(preferredTypes);
|
|
469
|
+
return preferred.preview ? preferred : pick(fallbackTypes);
|
|
470
|
+
}
|
|
288
471
|
function readPositiveInt(value, fallback = 0) {
|
|
289
472
|
const parsed = Number(value || 0);
|
|
290
473
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
@@ -384,11 +567,16 @@ function buildEditPrompt(prefixMessages, editedContent) {
|
|
|
384
567
|
}
|
|
385
568
|
function toClientHistoryTask(t, includeMessages = false) {
|
|
386
569
|
const cliSessionId = getRestorableCliSessionId(t.session_id, t.messages);
|
|
570
|
+
const summaryMessages = t.messages || (t.session_id ? store.getRecentSessionMessages(t.session_id, 32) : []);
|
|
571
|
+
const summary = buildHistoryPreview(summaryMessages);
|
|
387
572
|
const response = {
|
|
388
573
|
id: t.id,
|
|
389
574
|
workDir: t.work_dir || undefined,
|
|
390
575
|
status: t.status,
|
|
391
576
|
title: t.title,
|
|
577
|
+
preview: summary.preview,
|
|
578
|
+
lastMessageType: summary.lastMessageType,
|
|
579
|
+
messageCount: t.messages ? t.messages.length : (t.session_id ? store.getMessageCount(t.session_id) : 0),
|
|
392
580
|
confirmCount: t.confirm_count,
|
|
393
581
|
toolCount: t.tool_count,
|
|
394
582
|
duration: t.duration || undefined,
|
|
@@ -409,6 +597,26 @@ function toClientWorkDir(item) {
|
|
|
409
597
|
createdAt: item.created_at,
|
|
410
598
|
};
|
|
411
599
|
}
|
|
600
|
+
function toClientPromptHistoryItem(item) {
|
|
601
|
+
return {
|
|
602
|
+
id: item.id,
|
|
603
|
+
workDir: item.work_dir || undefined,
|
|
604
|
+
text: item.text,
|
|
605
|
+
useCount: item.use_count,
|
|
606
|
+
lastUsedAt: item.last_used_at,
|
|
607
|
+
createdAt: item.created_at,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function normalizeHistoryScope(value, fallback = 'global') {
|
|
611
|
+
return value === 'current' || value === 'project' ? 'current' : fallback;
|
|
612
|
+
}
|
|
613
|
+
function getHistoryWorkDirForScope(scope) {
|
|
614
|
+
return scope === 'current' ? getCurrentWorkDir() : null;
|
|
615
|
+
}
|
|
616
|
+
function getCurrentHistoryWorkDir() {
|
|
617
|
+
const workDir = getCurrentWorkDir();
|
|
618
|
+
return typeof workDir === 'string' && workDir.trim() ? workDir : null;
|
|
619
|
+
}
|
|
412
620
|
function getCurrentWorkDir() {
|
|
413
621
|
return cliManager.getConfig().workDir;
|
|
414
622
|
}
|
|
@@ -431,7 +639,15 @@ const WORKSPACE_SKIP_NAMES = new Set([
|
|
|
431
639
|
]);
|
|
432
640
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
433
641
|
const MAX_DIFF_BYTES = 1024 * 1024;
|
|
434
|
-
const MAX_COMMIT_CONTEXT_BYTES =
|
|
642
|
+
const MAX_COMMIT_CONTEXT_BYTES = 96 * 1024;
|
|
643
|
+
const MAX_COMMIT_DIFF_BYTES = 64 * 1024;
|
|
644
|
+
const COMMIT_MESSAGE_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
645
|
+
const MAX_IMAGE_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
646
|
+
const MAX_GENERATED_IMAGE_ATTACHMENT_BYTES = 30 * 1024 * 1024;
|
|
647
|
+
const MAX_GENERATED_IMAGE_ATTACHMENTS_PER_TURN = 8;
|
|
648
|
+
const MAX_IMAGE_ATTACHMENTS_PER_PROMPT = 4;
|
|
649
|
+
const ATTACHMENT_ROOT = path.join(os.homedir(), '.agentpilot', 'attachments');
|
|
650
|
+
const CODEX_GENERATED_IMAGES_ROOT = path.join(resolveEnvPath(process.env.CODEX_HOME || '~/.codex'), 'generated_images');
|
|
435
651
|
const PREVIEW_IMAGE_MIME_BY_EXTENSION = {
|
|
436
652
|
'.avif': 'image/avif',
|
|
437
653
|
'.bmp': 'image/bmp',
|
|
@@ -443,6 +659,281 @@ const PREVIEW_IMAGE_MIME_BY_EXTENSION = {
|
|
|
443
659
|
'.svg': 'image/svg+xml',
|
|
444
660
|
'.webp': 'image/webp',
|
|
445
661
|
};
|
|
662
|
+
const ATTACHMENT_EXTENSION_BY_MIME = {
|
|
663
|
+
'image/avif': 'avif',
|
|
664
|
+
'image/bmp': 'bmp',
|
|
665
|
+
'image/gif': 'gif',
|
|
666
|
+
'image/jpeg': 'jpg',
|
|
667
|
+
'image/png': 'png',
|
|
668
|
+
'image/webp': 'webp',
|
|
669
|
+
};
|
|
670
|
+
let generatedImageCapture = null;
|
|
671
|
+
function normalizeImageMime(raw) {
|
|
672
|
+
const mime = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
673
|
+
return ATTACHMENT_EXTENSION_BY_MIME[mime] ? mime : '';
|
|
674
|
+
}
|
|
675
|
+
function sanitizeAttachmentName(raw, fallback = 'image') {
|
|
676
|
+
const base = path.basename(typeof raw === 'string' && raw.trim() ? raw.trim() : fallback);
|
|
677
|
+
return base.replace(/[\r\n"\\/:*?<>|\x00-\x1F]+/g, '_').slice(0, 120) || fallback;
|
|
678
|
+
}
|
|
679
|
+
function ensureAttachmentRoot() {
|
|
680
|
+
fs.mkdirSync(ATTACHMENT_ROOT, { recursive: true });
|
|
681
|
+
}
|
|
682
|
+
function assertAttachmentPath(rawPath) {
|
|
683
|
+
if (typeof rawPath !== 'string' || !rawPath.trim()) {
|
|
684
|
+
throw new Error('附件路径无效');
|
|
685
|
+
}
|
|
686
|
+
ensureAttachmentRoot();
|
|
687
|
+
const resolvedRoot = fs.realpathSync(ATTACHMENT_ROOT);
|
|
688
|
+
const resolvedPath = path.resolve(rawPath);
|
|
689
|
+
const realPath = fs.realpathSync(resolvedPath);
|
|
690
|
+
if (realPath !== resolvedRoot && !realPath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
691
|
+
throw new Error('附件路径不在允许目录内');
|
|
692
|
+
}
|
|
693
|
+
return realPath;
|
|
694
|
+
}
|
|
695
|
+
function normalizePromptAttachments(rawAttachments, options = {}) {
|
|
696
|
+
if (!Array.isArray(rawAttachments))
|
|
697
|
+
return [];
|
|
698
|
+
const maxBytes = options.maxBytes ?? MAX_IMAGE_ATTACHMENT_BYTES;
|
|
699
|
+
const maxCount = options.maxCount ?? MAX_IMAGE_ATTACHMENTS_PER_PROMPT;
|
|
700
|
+
return rawAttachments.slice(0, maxCount).map((raw, index) => {
|
|
701
|
+
if (!raw || typeof raw !== 'object') {
|
|
702
|
+
throw new Error('附件格式无效');
|
|
703
|
+
}
|
|
704
|
+
const item = raw;
|
|
705
|
+
const filePath = assertAttachmentPath(item.path);
|
|
706
|
+
const stat = fs.statSync(filePath);
|
|
707
|
+
if (!stat.isFile())
|
|
708
|
+
throw new Error('附件不是文件');
|
|
709
|
+
if (stat.size > maxBytes)
|
|
710
|
+
throw new Error('图片附件过大');
|
|
711
|
+
const mimeType = normalizeImageMime(item.mimeType) || getPreviewImageMime(filePath) || '';
|
|
712
|
+
if (!mimeType || !ATTACHMENT_EXTENSION_BY_MIME[mimeType]) {
|
|
713
|
+
throw new Error('仅支持图片附件');
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
id: typeof item.id === 'string' && item.id ? item.id : `attachment-${index + 1}`,
|
|
717
|
+
type: 'image',
|
|
718
|
+
name: sanitizeAttachmentName(item.name, path.basename(filePath)),
|
|
719
|
+
mimeType,
|
|
720
|
+
size: stat.size,
|
|
721
|
+
path: filePath,
|
|
722
|
+
createdAt: typeof item.createdAt === 'number' && Number.isFinite(item.createdAt) ? item.createdAt : stat.mtimeMs,
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function normalizePromptPayload(raw) {
|
|
727
|
+
const content = typeof raw === 'string'
|
|
728
|
+
? raw
|
|
729
|
+
: typeof raw?.content === 'string'
|
|
730
|
+
? raw.content
|
|
731
|
+
: '';
|
|
732
|
+
const attachments = normalizePromptAttachments(typeof raw === 'object' && raw ? raw.attachments : undefined);
|
|
733
|
+
return { content, attachments };
|
|
734
|
+
}
|
|
735
|
+
function parseImageUploadBody(body) {
|
|
736
|
+
const dataUrl = typeof body?.dataUrl === 'string' ? body.dataUrl : '';
|
|
737
|
+
const dataUrlMatch = dataUrl.match(/^data:([^;,]+);base64,(.*)$/s);
|
|
738
|
+
const name = sanitizeAttachmentName(body?.name, 'image');
|
|
739
|
+
const mimeType = normalizeImageMime(body?.mimeType || body?.type || dataUrlMatch?.[1])
|
|
740
|
+
|| normalizeImageMime(getPreviewImageMime(name));
|
|
741
|
+
const base64 = typeof body?.data === 'string' ? body.data : dataUrlMatch?.[2] || '';
|
|
742
|
+
if (!mimeType)
|
|
743
|
+
throw new Error('仅支持图片附件');
|
|
744
|
+
if (!base64)
|
|
745
|
+
throw new Error('图片数据为空');
|
|
746
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
747
|
+
if (buffer.length === 0)
|
|
748
|
+
throw new Error('图片数据为空');
|
|
749
|
+
if (buffer.length > MAX_IMAGE_ATTACHMENT_BYTES)
|
|
750
|
+
throw new Error('图片不能超过 10MB');
|
|
751
|
+
return {
|
|
752
|
+
buffer,
|
|
753
|
+
mimeType,
|
|
754
|
+
name,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function saveImageAttachment(body) {
|
|
758
|
+
const { buffer, mimeType, name } = parseImageUploadBody(body);
|
|
759
|
+
const sessionDir = currentSessionId || 'draft';
|
|
760
|
+
const dir = path.join(ATTACHMENT_ROOT, sessionDir);
|
|
761
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
762
|
+
const id = randomUUID();
|
|
763
|
+
const ext = ATTACHMENT_EXTENSION_BY_MIME[mimeType];
|
|
764
|
+
const filePath = path.join(dir, `${Date.now()}-${id}.${ext}`);
|
|
765
|
+
fs.writeFileSync(filePath, buffer, { flag: 'wx' });
|
|
766
|
+
return {
|
|
767
|
+
id,
|
|
768
|
+
type: 'image',
|
|
769
|
+
name,
|
|
770
|
+
mimeType,
|
|
771
|
+
size: buffer.length,
|
|
772
|
+
path: filePath,
|
|
773
|
+
createdAt: Date.now(),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function listGeneratedImageFiles() {
|
|
777
|
+
const files = [];
|
|
778
|
+
function walk(dir, depth) {
|
|
779
|
+
if (depth > 4)
|
|
780
|
+
return;
|
|
781
|
+
let entries;
|
|
782
|
+
try {
|
|
783
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
for (const entry of entries) {
|
|
789
|
+
const entryPath = path.join(dir, entry.name);
|
|
790
|
+
if (entry.isDirectory()) {
|
|
791
|
+
walk(entryPath, depth + 1);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (!entry.isFile())
|
|
795
|
+
continue;
|
|
796
|
+
const mimeType = getPreviewImageMime(entryPath);
|
|
797
|
+
if (mimeType && ATTACHMENT_EXTENSION_BY_MIME[mimeType]) {
|
|
798
|
+
files.push(path.resolve(entryPath));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
walk(CODEX_GENERATED_IMAGES_ROOT, 0);
|
|
803
|
+
return files;
|
|
804
|
+
}
|
|
805
|
+
function maybeStartGeneratedImageCapture() {
|
|
806
|
+
if (generatedImageCapture)
|
|
807
|
+
return;
|
|
808
|
+
if (cliManager.getConfig().cliType !== 'codex')
|
|
809
|
+
return;
|
|
810
|
+
generatedImageCapture = {
|
|
811
|
+
startedAt: Date.now(),
|
|
812
|
+
baseline: new Set(listGeneratedImageFiles()),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function copyGeneratedImageToAttachment(sourcePath, index) {
|
|
816
|
+
const mimeType = getPreviewImageMime(sourcePath);
|
|
817
|
+
if (!mimeType || !ATTACHMENT_EXTENSION_BY_MIME[mimeType])
|
|
818
|
+
return null;
|
|
819
|
+
let sourceStat;
|
|
820
|
+
try {
|
|
821
|
+
sourceStat = fs.statSync(sourcePath);
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
if (!sourceStat.isFile() || sourceStat.size <= 0 || sourceStat.size > MAX_GENERATED_IMAGE_ATTACHMENT_BYTES) {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
const id = randomUUID();
|
|
830
|
+
const ext = ATTACHMENT_EXTENSION_BY_MIME[mimeType];
|
|
831
|
+
const dir = path.join(ATTACHMENT_ROOT, currentSessionId || 'generated', 'generated');
|
|
832
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
833
|
+
const filePath = path.join(dir, `${Date.now()}-${id}.${ext}`);
|
|
834
|
+
try {
|
|
835
|
+
fs.copyFileSync(sourcePath, filePath, fs.constants.COPYFILE_EXCL);
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
console.warn(`[Attachments] Failed to copy generated image ${sourcePath}: ${err?.message || err}`);
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
const stat = fs.statSync(filePath);
|
|
842
|
+
return {
|
|
843
|
+
id,
|
|
844
|
+
type: 'image',
|
|
845
|
+
name: `generated-image-${index + 1}.${ext}`,
|
|
846
|
+
mimeType,
|
|
847
|
+
size: stat.size,
|
|
848
|
+
path: filePath,
|
|
849
|
+
createdAt: Date.now(),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function collectGeneratedImageAttachments(finish) {
|
|
853
|
+
const capture = generatedImageCapture;
|
|
854
|
+
if (!capture)
|
|
855
|
+
return [];
|
|
856
|
+
const minMtime = capture.startedAt - 5000;
|
|
857
|
+
const candidates = listGeneratedImageFiles()
|
|
858
|
+
.map((filePath) => {
|
|
859
|
+
try {
|
|
860
|
+
return { filePath, stat: fs.statSync(filePath) };
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
.filter((item) => !!item &&
|
|
867
|
+
item.stat.isFile() &&
|
|
868
|
+
item.stat.size > 0 &&
|
|
869
|
+
item.stat.size <= MAX_GENERATED_IMAGE_ATTACHMENT_BYTES &&
|
|
870
|
+
!capture.baseline.has(item.filePath) &&
|
|
871
|
+
item.stat.mtimeMs >= minMtime)
|
|
872
|
+
.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs)
|
|
873
|
+
.slice(-MAX_GENERATED_IMAGE_ATTACHMENTS_PER_TURN);
|
|
874
|
+
const attachments = candidates
|
|
875
|
+
.map((candidate, index) => copyGeneratedImageToAttachment(candidate.filePath, index))
|
|
876
|
+
.filter((item) => item !== null);
|
|
877
|
+
if (attachments.length > 0 || finish) {
|
|
878
|
+
generatedImageCapture = null;
|
|
879
|
+
}
|
|
880
|
+
return attachments;
|
|
881
|
+
}
|
|
882
|
+
function attachGeneratedImagesToEvent(event) {
|
|
883
|
+
if (event.type !== 'system' || event.details?.subtype !== 'result')
|
|
884
|
+
return [];
|
|
885
|
+
const attachments = collectGeneratedImageAttachments(false);
|
|
886
|
+
if (attachments.length === 0)
|
|
887
|
+
return [];
|
|
888
|
+
event.details = {
|
|
889
|
+
...(event.details || {}),
|
|
890
|
+
attachments: [
|
|
891
|
+
...toClientAttachments(event.details?.attachments),
|
|
892
|
+
...attachments,
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
event.attachments = event.details.attachments;
|
|
896
|
+
return attachments;
|
|
897
|
+
}
|
|
898
|
+
function emitGeneratedImagesFallback() {
|
|
899
|
+
const attachments = collectGeneratedImageAttachments(true);
|
|
900
|
+
if (attachments.length === 0)
|
|
901
|
+
return;
|
|
902
|
+
broadcast({
|
|
903
|
+
type: 'system',
|
|
904
|
+
content: '生成图片',
|
|
905
|
+
time: now(),
|
|
906
|
+
details: {
|
|
907
|
+
subtype: 'result',
|
|
908
|
+
attachments,
|
|
909
|
+
},
|
|
910
|
+
attachments,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function sendAttachmentFile(req, res, rawPath) {
|
|
914
|
+
const filePath = assertAttachmentPath(rawPath);
|
|
915
|
+
const stat = fs.statSync(filePath);
|
|
916
|
+
if (!stat.isFile()) {
|
|
917
|
+
sendJson(res, 404, { error: '附件不存在' });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const mimeType = getPreviewImageMime(filePath);
|
|
921
|
+
if (!mimeType || !ATTACHMENT_EXTENSION_BY_MIME[mimeType]) {
|
|
922
|
+
sendJson(res, 415, { error: '仅支持图片附件预览' });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
setCorsHeaders(res);
|
|
926
|
+
res.writeHead(200, {
|
|
927
|
+
'Content-Type': mimeType,
|
|
928
|
+
'Content-Length': String(stat.size),
|
|
929
|
+
'Cache-Control': 'private, max-age=3600',
|
|
930
|
+
});
|
|
931
|
+
if (req.method === 'HEAD') {
|
|
932
|
+
res.end();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
fs.createReadStream(filePath).pipe(res);
|
|
936
|
+
}
|
|
446
937
|
function decodeGitOctalPath(raw) {
|
|
447
938
|
return raw.replace(/(?:\\[0-7]{3})+/g, (sequence) => {
|
|
448
939
|
const bytes = sequence.match(/\\([0-7]{3})/g)?.map(item => parseInt(item.slice(1), 8)) || [];
|
|
@@ -616,6 +1107,28 @@ function runGit(args, allowExitCodes = [0]) {
|
|
|
616
1107
|
throw new Error(stderr || err?.message || 'Git 命令执行失败');
|
|
617
1108
|
}
|
|
618
1109
|
}
|
|
1110
|
+
function getCurrentGitInfo() {
|
|
1111
|
+
try {
|
|
1112
|
+
const insideWorkTree = runGit(['rev-parse', '--is-inside-work-tree'], [0, 128]).trim() === 'true';
|
|
1113
|
+
if (!insideWorkTree)
|
|
1114
|
+
return { isGitRepo: false, gitBranch: '' };
|
|
1115
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], [0, 128]).trim();
|
|
1116
|
+
if (branch && branch !== 'HEAD') {
|
|
1117
|
+
return { isGitRepo: true, gitBranch: branch };
|
|
1118
|
+
}
|
|
1119
|
+
const shortHash = runGit(['rev-parse', '--short', 'HEAD'], [0, 128]).trim();
|
|
1120
|
+
return { isGitRepo: true, gitBranch: shortHash ? `HEAD ${shortHash}` : branch || 'HEAD' };
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
return { isGitRepo: false, gitBranch: '' };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function getClientConfig() {
|
|
1127
|
+
return {
|
|
1128
|
+
...cliManager.getConfig(),
|
|
1129
|
+
...getCurrentGitInfo(),
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
619
1132
|
function unquoteGitPath(raw) {
|
|
620
1133
|
const trimmed = raw.trim();
|
|
621
1134
|
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
@@ -658,6 +1171,8 @@ function parseGitStatus(output, nulTerminated = false) {
|
|
|
658
1171
|
const indexStatus = line[0] || ' ';
|
|
659
1172
|
const worktreeStatus = line[1] || ' ';
|
|
660
1173
|
const filePath = line.slice(3);
|
|
1174
|
+
const isMoved = indexStatus === 'R' || indexStatus === 'C' || worktreeStatus === 'R' || worktreeStatus === 'C';
|
|
1175
|
+
const oldPath = isMoved ? records[index + 1] : undefined;
|
|
661
1176
|
files.push({
|
|
662
1177
|
path: filePath,
|
|
663
1178
|
name: path.basename(filePath),
|
|
@@ -665,8 +1180,9 @@ function parseGitStatus(output, nulTerminated = false) {
|
|
|
665
1180
|
indexStatus,
|
|
666
1181
|
worktreeStatus,
|
|
667
1182
|
label: describeGitStatus(indexStatus, worktreeStatus),
|
|
1183
|
+
oldPath,
|
|
668
1184
|
});
|
|
669
|
-
if (
|
|
1185
|
+
if (isMoved) {
|
|
670
1186
|
index += 1;
|
|
671
1187
|
}
|
|
672
1188
|
}
|
|
@@ -678,7 +1194,9 @@ function parseGitStatus(output, nulTerminated = false) {
|
|
|
678
1194
|
const indexStatus = line[0] || ' ';
|
|
679
1195
|
const worktreeStatus = line[1] || ' ';
|
|
680
1196
|
const rawPath = line.slice(3);
|
|
681
|
-
const
|
|
1197
|
+
const movedParts = rawPath.includes(' -> ') ? rawPath.split(' -> ') : [];
|
|
1198
|
+
const displayPath = movedParts.length > 0 ? movedParts[movedParts.length - 1] || rawPath : rawPath;
|
|
1199
|
+
const oldPath = movedParts.length > 1 ? unquoteGitPath(movedParts[0] || '') : undefined;
|
|
682
1200
|
const filePath = unquoteGitPath(displayPath);
|
|
683
1201
|
return {
|
|
684
1202
|
path: filePath,
|
|
@@ -687,9 +1205,88 @@ function parseGitStatus(output, nulTerminated = false) {
|
|
|
687
1205
|
indexStatus,
|
|
688
1206
|
worktreeStatus,
|
|
689
1207
|
label: describeGitStatus(indexStatus, worktreeStatus),
|
|
1208
|
+
oldPath,
|
|
690
1209
|
};
|
|
691
1210
|
}).filter((item) => item !== null);
|
|
692
1211
|
}
|
|
1212
|
+
function emptyChangeStats() {
|
|
1213
|
+
return { additions: 0, deletions: 0 };
|
|
1214
|
+
}
|
|
1215
|
+
function normalizeGitDiffPath(raw) {
|
|
1216
|
+
const trimmed = raw.trim();
|
|
1217
|
+
if (!trimmed || trimmed === '/dev/null')
|
|
1218
|
+
return '';
|
|
1219
|
+
return unquoteGitPath(trimmed.replace(/^[ab]\//, ''));
|
|
1220
|
+
}
|
|
1221
|
+
function rememberDiffStats(map, filePath) {
|
|
1222
|
+
const normalized = normalizeGitDiffPath(filePath);
|
|
1223
|
+
if (!normalized)
|
|
1224
|
+
return null;
|
|
1225
|
+
let stats = map.get(normalized);
|
|
1226
|
+
if (!stats) {
|
|
1227
|
+
stats = emptyChangeStats();
|
|
1228
|
+
map.set(normalized, stats);
|
|
1229
|
+
}
|
|
1230
|
+
return stats;
|
|
1231
|
+
}
|
|
1232
|
+
function parseDiffStats(diff) {
|
|
1233
|
+
const statsByPath = new Map();
|
|
1234
|
+
let currentStats = null;
|
|
1235
|
+
for (const line of diff.split('\n')) {
|
|
1236
|
+
if (line.startsWith('diff --git ')) {
|
|
1237
|
+
const parts = line.split(' ');
|
|
1238
|
+
currentStats = rememberDiffStats(statsByPath, parts[3] || parts[2] || '');
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (line.startsWith('+++ ')) {
|
|
1242
|
+
const nextPath = normalizeGitDiffPath(line.slice(4));
|
|
1243
|
+
if (nextPath)
|
|
1244
|
+
currentStats = rememberDiffStats(statsByPath, nextPath);
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (!currentStats)
|
|
1248
|
+
continue;
|
|
1249
|
+
if (line.startsWith('--- '))
|
|
1250
|
+
continue;
|
|
1251
|
+
if (line.startsWith('+')) {
|
|
1252
|
+
currentStats.additions += 1;
|
|
1253
|
+
}
|
|
1254
|
+
else if (line.startsWith('-')) {
|
|
1255
|
+
currentStats.deletions += 1;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return statsByPath;
|
|
1259
|
+
}
|
|
1260
|
+
function mergeStats(left, right) {
|
|
1261
|
+
return {
|
|
1262
|
+
additions: left.additions + right.additions,
|
|
1263
|
+
deletions: left.deletions + right.deletions,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function countSmallTextFileLines(rawPath) {
|
|
1267
|
+
try {
|
|
1268
|
+
const { target } = resolveWorkspacePath(rawPath, true);
|
|
1269
|
+
const stat = fs.statSync(target);
|
|
1270
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
|
|
1271
|
+
return null;
|
|
1272
|
+
const buffer = fs.readFileSync(target);
|
|
1273
|
+
if (buffer.includes(0))
|
|
1274
|
+
return null;
|
|
1275
|
+
const text = buffer.toString('utf8');
|
|
1276
|
+
if (!text)
|
|
1277
|
+
return 0;
|
|
1278
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
1279
|
+
return normalized.endsWith('\n')
|
|
1280
|
+
? normalized.split('\n').length - 1
|
|
1281
|
+
: normalized.split('\n').length;
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function sumStats(values) {
|
|
1288
|
+
return values.reduce((sum, stats) => mergeStats(sum, stats), emptyChangeStats());
|
|
1289
|
+
}
|
|
693
1290
|
function truncateText(text, maxBytes) {
|
|
694
1291
|
const buffer = Buffer.from(text, 'utf8');
|
|
695
1292
|
if (buffer.length <= maxBytes) {
|
|
@@ -850,8 +1447,23 @@ function getWorkspaceChanges(rawPath) {
|
|
|
850
1447
|
isGitRepo: false,
|
|
851
1448
|
branch: '',
|
|
852
1449
|
files: [],
|
|
1450
|
+
summary: {
|
|
1451
|
+
fileCount: 0,
|
|
1452
|
+
stagedFileCount: 0,
|
|
1453
|
+
unstagedFileCount: 0,
|
|
1454
|
+
additions: 0,
|
|
1455
|
+
deletions: 0,
|
|
1456
|
+
stagedAdditions: 0,
|
|
1457
|
+
stagedDeletions: 0,
|
|
1458
|
+
unstagedAdditions: 0,
|
|
1459
|
+
unstagedDeletions: 0,
|
|
1460
|
+
},
|
|
853
1461
|
diff: '',
|
|
1462
|
+
stagedDiff: '',
|
|
1463
|
+
unstagedDiff: '',
|
|
854
1464
|
truncated: false,
|
|
1465
|
+
stagedTruncated: false,
|
|
1466
|
+
unstagedTruncated: false,
|
|
855
1467
|
};
|
|
856
1468
|
}
|
|
857
1469
|
const selectedPath = normalizeWorkspacePath(rawPath);
|
|
@@ -860,36 +1472,84 @@ function getWorkspaceChanges(rawPath) {
|
|
|
860
1472
|
}
|
|
861
1473
|
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
862
1474
|
const statusOutput = runGit(['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.']);
|
|
863
|
-
const
|
|
1475
|
+
const rawFiles = parseGitStatus(statusOutput, true);
|
|
864
1476
|
const diffTarget = selectedPath || '.';
|
|
865
1477
|
const staged = runGit(['diff', '--cached', '--', diffTarget]);
|
|
866
|
-
|
|
1478
|
+
let unstaged = runGit(['diff', '--', diffTarget]);
|
|
867
1479
|
const parts = [];
|
|
868
1480
|
if (staged.trim())
|
|
869
1481
|
parts.push(`--- 已暂存变更 ---\n${staged}`);
|
|
870
1482
|
if (unstaged.trim())
|
|
871
1483
|
parts.push(`--- 未暂存变更 ---\n${unstaged}`);
|
|
872
1484
|
if (selectedPath && parts.length === 0) {
|
|
873
|
-
const selected =
|
|
1485
|
+
const selected = rawFiles.find(file => file.path === selectedPath);
|
|
874
1486
|
if (selected?.status === '??') {
|
|
875
1487
|
const { target } = resolveWorkspacePath(selectedPath, true);
|
|
876
1488
|
const stat = fs.statSync(target);
|
|
877
1489
|
if (stat.isFile() && stat.size <= MAX_FILE_BYTES) {
|
|
878
1490
|
const untrackedDiff = runGit(['diff', '--no-index', '--', '/dev/null', target], [0, 1]);
|
|
879
|
-
if (untrackedDiff.trim())
|
|
880
|
-
|
|
1491
|
+
if (untrackedDiff.trim()) {
|
|
1492
|
+
unstaged = untrackedDiff;
|
|
1493
|
+
parts.push(`--- 未暂存变更 ---\n${untrackedDiff}`);
|
|
1494
|
+
}
|
|
881
1495
|
}
|
|
882
1496
|
}
|
|
883
1497
|
}
|
|
1498
|
+
const stagedDiffResult = truncateText(staged, MAX_DIFF_BYTES);
|
|
1499
|
+
const unstagedDiffResult = truncateText(unstaged, MAX_DIFF_BYTES);
|
|
1500
|
+
const stagedStatsByPath = parseDiffStats(staged);
|
|
1501
|
+
const unstagedStatsByPath = parseDiffStats(unstaged);
|
|
1502
|
+
const files = rawFiles.map((file) => {
|
|
1503
|
+
const stagedStats = stagedStatsByPath.get(file.path) || emptyChangeStats();
|
|
1504
|
+
let unstagedStats = unstagedStatsByPath.get(file.path) || emptyChangeStats();
|
|
1505
|
+
if (file.status === '??' && unstagedStats.additions === 0 && unstagedStats.deletions === 0) {
|
|
1506
|
+
const additions = countSmallTextFileLines(file.path);
|
|
1507
|
+
if (additions != null) {
|
|
1508
|
+
unstagedStats = { additions, deletions: 0 };
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
const totalStats = mergeStats(stagedStats, unstagedStats);
|
|
1512
|
+
const hasStaged = file.indexStatus !== ' ' && file.indexStatus !== '?' && file.indexStatus !== '!';
|
|
1513
|
+
const hasUnstaged = file.status === '??' || (file.worktreeStatus !== ' ' && file.worktreeStatus !== '?' && file.worktreeStatus !== '!');
|
|
1514
|
+
return {
|
|
1515
|
+
...file,
|
|
1516
|
+
additions: totalStats.additions,
|
|
1517
|
+
deletions: totalStats.deletions,
|
|
1518
|
+
stagedAdditions: stagedStats.additions,
|
|
1519
|
+
stagedDeletions: stagedStats.deletions,
|
|
1520
|
+
unstagedAdditions: unstagedStats.additions,
|
|
1521
|
+
unstagedDeletions: unstagedStats.deletions,
|
|
1522
|
+
hasStaged,
|
|
1523
|
+
hasUnstaged,
|
|
1524
|
+
};
|
|
1525
|
+
});
|
|
1526
|
+
const stagedTotals = sumStats(files.map(file => ({ additions: file.stagedAdditions, deletions: file.stagedDeletions })));
|
|
1527
|
+
const unstagedTotals = sumStats(files.map(file => ({ additions: file.unstagedAdditions, deletions: file.unstagedDeletions })));
|
|
1528
|
+
const totalStats = mergeStats(stagedTotals, unstagedTotals);
|
|
884
1529
|
const diffResult = truncateText(parts.join('\n'), MAX_DIFF_BYTES);
|
|
885
1530
|
return {
|
|
886
1531
|
workDir: path.resolve(getCurrentWorkDir()),
|
|
887
1532
|
isGitRepo: true,
|
|
888
1533
|
branch,
|
|
889
1534
|
files,
|
|
1535
|
+
summary: {
|
|
1536
|
+
fileCount: files.length,
|
|
1537
|
+
stagedFileCount: files.filter(file => file.hasStaged).length,
|
|
1538
|
+
unstagedFileCount: files.filter(file => file.hasUnstaged).length,
|
|
1539
|
+
additions: totalStats.additions,
|
|
1540
|
+
deletions: totalStats.deletions,
|
|
1541
|
+
stagedAdditions: stagedTotals.additions,
|
|
1542
|
+
stagedDeletions: stagedTotals.deletions,
|
|
1543
|
+
unstagedAdditions: unstagedTotals.additions,
|
|
1544
|
+
unstagedDeletions: unstagedTotals.deletions,
|
|
1545
|
+
},
|
|
890
1546
|
diff: diffResult.text,
|
|
1547
|
+
stagedDiff: stagedDiffResult.text,
|
|
1548
|
+
unstagedDiff: unstagedDiffResult.text,
|
|
891
1549
|
diffPath: selectedPath || undefined,
|
|
892
1550
|
truncated: diffResult.truncated,
|
|
1551
|
+
stagedTruncated: stagedDiffResult.truncated,
|
|
1552
|
+
unstagedTruncated: unstagedDiffResult.truncated,
|
|
893
1553
|
};
|
|
894
1554
|
}
|
|
895
1555
|
function trackWorkspaceFile(rawPath) {
|
|
@@ -967,6 +1627,41 @@ function unstageWorkspacePath(rawPath) {
|
|
|
967
1627
|
unstaged: true,
|
|
968
1628
|
};
|
|
969
1629
|
}
|
|
1630
|
+
function isSameOrNestedWorkspacePath(pathname, targetPath) {
|
|
1631
|
+
return !!pathname && (pathname === targetPath || pathname.startsWith(`${targetPath}/`));
|
|
1632
|
+
}
|
|
1633
|
+
function discardWorkspaceChanges(rawPath) {
|
|
1634
|
+
ensureWorkspaceGitRepo();
|
|
1635
|
+
const selectedPath = normalizeWorkspacePath(rawPath);
|
|
1636
|
+
if (selectedPath) {
|
|
1637
|
+
resolveWorkspacePath(selectedPath, false);
|
|
1638
|
+
}
|
|
1639
|
+
const allStatusOutput = runGit(['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.']);
|
|
1640
|
+
const allFiles = parseGitStatus(allStatusOutput, true);
|
|
1641
|
+
const files = selectedPath
|
|
1642
|
+
? allFiles.filter(file => isSameOrNestedWorkspacePath(file.path, selectedPath) ||
|
|
1643
|
+
isSameOrNestedWorkspacePath(file.oldPath, selectedPath))
|
|
1644
|
+
: allFiles;
|
|
1645
|
+
if (files.length === 0) {
|
|
1646
|
+
throw new Error(selectedPath ? '文件没有可清空的变更' : '没有可清空的代码变更');
|
|
1647
|
+
}
|
|
1648
|
+
const trackedFiles = files.filter(file => file.status !== '??');
|
|
1649
|
+
if (trackedFiles.length > 0) {
|
|
1650
|
+
const targets = selectedPath
|
|
1651
|
+
? Array.from(new Set(trackedFiles.flatMap(file => [file.path, file.oldPath].filter(Boolean))))
|
|
1652
|
+
: ['.'];
|
|
1653
|
+
runGit(['restore', '--source=HEAD', '--staged', '--worktree', '--', ...targets]);
|
|
1654
|
+
}
|
|
1655
|
+
const hasUntrackedFiles = files.some(file => file.status === '??');
|
|
1656
|
+
if (hasUntrackedFiles) {
|
|
1657
|
+
runGit(['clean', '-fd', '--', selectedPath || '.']);
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
workDir: path.resolve(getCurrentWorkDir()),
|
|
1661
|
+
path: selectedPath || undefined,
|
|
1662
|
+
discarded: true,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
970
1665
|
function ensureWorkspaceGitRepo() {
|
|
971
1666
|
resolveWorkspacePath('', true);
|
|
972
1667
|
try {
|
|
@@ -988,52 +1683,98 @@ function normalizeAICommitMessage(rawMessage) {
|
|
|
988
1683
|
message = message.replace(/^["'“”‘’]+|["'“”‘’]+$/g, '').trim();
|
|
989
1684
|
return message.split('\n').map(line => line.replace(/^\s*[-*]\s+/, '').trimEnd()).join('\n').trim();
|
|
990
1685
|
}
|
|
1686
|
+
const commitMessageCache = new Map();
|
|
1687
|
+
function hashText(value) {
|
|
1688
|
+
return createHash('sha256').update(value).digest('hex');
|
|
1689
|
+
}
|
|
1690
|
+
function getCommitMessageCacheKey(workDir, diffHash) {
|
|
1691
|
+
return `${workDir}\0${diffHash}`;
|
|
1692
|
+
}
|
|
1693
|
+
function getCachedCommitMessage(cacheKey) {
|
|
1694
|
+
const entry = commitMessageCache.get(cacheKey);
|
|
1695
|
+
if (!entry)
|
|
1696
|
+
return null;
|
|
1697
|
+
if (Date.now() - entry.createdAt > COMMIT_MESSAGE_CACHE_TTL_MS) {
|
|
1698
|
+
commitMessageCache.delete(cacheKey);
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
return entry.message;
|
|
1702
|
+
}
|
|
1703
|
+
function setCachedCommitMessage(cacheKey, message) {
|
|
1704
|
+
commitMessageCache.set(cacheKey, {
|
|
1705
|
+
message,
|
|
1706
|
+
createdAt: Date.now(),
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
991
1709
|
function buildCommitMessagePrompt() {
|
|
992
1710
|
ensureWorkspaceGitRepo();
|
|
993
|
-
const
|
|
994
|
-
const
|
|
995
|
-
const
|
|
996
|
-
if (
|
|
997
|
-
throw new Error('没有可提交的代码变更');
|
|
998
|
-
}
|
|
999
|
-
const stagedFiles = runGit(['diff', '--cached', '--name-only']);
|
|
1000
|
-
if (!stagedFiles.trim()) {
|
|
1711
|
+
const workDir = path.resolve(getCurrentWorkDir());
|
|
1712
|
+
const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
1713
|
+
const stagedNameStatus = runGit(['diff', '--cached', '--name-status', '--find-renames']).trim();
|
|
1714
|
+
if (!stagedNameStatus) {
|
|
1001
1715
|
throw new Error('没有已暂存的代码变更,请先暂存要提交的文件');
|
|
1002
1716
|
}
|
|
1003
|
-
const recentCommits = runGit(['log', '--oneline', '-
|
|
1004
|
-
const
|
|
1005
|
-
const
|
|
1717
|
+
const recentCommits = runGit(['log', '--oneline', '-5'], [0, 128]).trim();
|
|
1718
|
+
const stagedNumstat = runGit(['diff', '--cached', '--numstat', '--find-renames']).trim();
|
|
1719
|
+
const stagedStat = runGit(['diff', '--cached', '--stat']).trim();
|
|
1720
|
+
const stagedSummary = runGit(['diff', '--cached', '--summary', '--find-renames']).trim();
|
|
1721
|
+
const stagedDiffForHash = runGit(['diff', '--cached', '--full-index', '--binary']);
|
|
1722
|
+
const stagedDiff = runGit(['diff', '--cached', '--find-renames', '--unified=3']);
|
|
1723
|
+
const diffHash = hashText(stagedDiffForHash || stagedDiff || stagedNameStatus);
|
|
1724
|
+
const diffContext = truncateText(stagedDiff.trim(), MAX_COMMIT_DIFF_BYTES);
|
|
1006
1725
|
const context = truncateText([
|
|
1007
|
-
`当前分支:${
|
|
1008
|
-
recentCommits ?
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1726
|
+
`当前分支:${branch}`,
|
|
1727
|
+
recentCommits ? `最近提交风格:\n${recentCommits}` : '',
|
|
1728
|
+
`已暂存文件:\n${stagedNameStatus}`,
|
|
1729
|
+
stagedNumstat ? `已暂存增删行:\n${stagedNumstat}` : '',
|
|
1730
|
+
stagedStat ? `已暂存统计:\n${stagedStat}` : '',
|
|
1731
|
+
stagedSummary ? `已暂存摘要:\n${stagedSummary}` : '',
|
|
1732
|
+
diffContext.text ? `已暂存 diff${diffContext.truncated ? ' 片段' : ''}:\n${diffContext.text}` : '',
|
|
1012
1733
|
].filter(Boolean).join('\n\n'), MAX_COMMIT_CONTEXT_BYTES);
|
|
1013
|
-
|
|
1014
|
-
'
|
|
1734
|
+
const prompt = [
|
|
1735
|
+
'你是 commit message 生成器。只基于下面的“已暂存”Git 上下文生成一条提交信息。',
|
|
1015
1736
|
'要求:',
|
|
1737
|
+
'- 不要调用命令或工具,不要读取文件。',
|
|
1016
1738
|
'- 只输出 commit message,不要解释,不要 Markdown 代码块。',
|
|
1739
|
+
'- 只描述已暂存变更,不要描述未暂存或未跟踪内容。',
|
|
1017
1740
|
'- 使用 Conventional Commits:<type>(<scope>): <subject> 或 <type>: <subject>。',
|
|
1018
1741
|
'- type 优先使用 feat/fix/docs/refactor/test/chore/style/build/ci/perf。',
|
|
1019
|
-
'-
|
|
1742
|
+
'- scope 优先从文件路径、模块名和最近提交风格判断。',
|
|
1743
|
+
'- subject 简短、具体、英文小写,尽量不超过 72 个字符;必要时可以补充 1-3 行 body。',
|
|
1020
1744
|
'- 不要提到 AI、不要提到你无法验证的内容。',
|
|
1021
1745
|
'',
|
|
1022
|
-
context.truncated ? '
|
|
1746
|
+
context.truncated || diffContext.truncated ? '注意:变更上下文已截断,请优先依据文件列表、统计和可见 diff。' : '',
|
|
1023
1747
|
'已暂存 Git 变更上下文:',
|
|
1024
1748
|
context.text,
|
|
1025
1749
|
].filter(Boolean).join('\n');
|
|
1750
|
+
return {
|
|
1751
|
+
prompt,
|
|
1752
|
+
cacheKey: getCommitMessageCacheKey(workDir, diffHash),
|
|
1753
|
+
diffHash,
|
|
1754
|
+
};
|
|
1026
1755
|
}
|
|
1027
1756
|
async function generateWorkspaceCommitMessage() {
|
|
1028
|
-
const
|
|
1029
|
-
const
|
|
1757
|
+
const payload = buildCommitMessagePrompt();
|
|
1758
|
+
const cachedMessage = getCachedCommitMessage(payload.cacheKey);
|
|
1759
|
+
if (cachedMessage) {
|
|
1760
|
+
return {
|
|
1761
|
+
workDir: path.resolve(getCurrentWorkDir()),
|
|
1762
|
+
message: cachedMessage,
|
|
1763
|
+
cached: true,
|
|
1764
|
+
diffHash: payload.diffHash,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
const output = await cliManager.runOneShot(payload.prompt, 60000);
|
|
1030
1768
|
const message = normalizeAICommitMessage(output);
|
|
1031
1769
|
if (!message) {
|
|
1032
1770
|
throw new Error('AI 未生成有效提交信息');
|
|
1033
1771
|
}
|
|
1772
|
+
setCachedCommitMessage(payload.cacheKey, message);
|
|
1034
1773
|
return {
|
|
1035
1774
|
workDir: path.resolve(getCurrentWorkDir()),
|
|
1036
1775
|
message,
|
|
1776
|
+
cached: false,
|
|
1777
|
+
diffHash: payload.diffHash,
|
|
1037
1778
|
};
|
|
1038
1779
|
}
|
|
1039
1780
|
function commitWorkspaceChanges(rawMessage) {
|
|
@@ -1135,9 +1876,120 @@ function maybeSetAuthCookie(req, res, url) {
|
|
|
1135
1876
|
const secure = httpsOptions ? '; Secure' : '';
|
|
1136
1877
|
appendSetCookie(res, `${AUTH_COOKIE_NAME}=${encodeURIComponent(AUTH_TOKEN)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400${secure}`);
|
|
1137
1878
|
}
|
|
1138
|
-
function
|
|
1879
|
+
function isPublicShareAssetPath(pathname) {
|
|
1880
|
+
let decodedPath;
|
|
1881
|
+
try {
|
|
1882
|
+
decodedPath = decodeURIComponent(pathname);
|
|
1883
|
+
}
|
|
1884
|
+
catch {
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
if (decodedPath.includes('..') || decodedPath.includes('\\'))
|
|
1888
|
+
return false;
|
|
1889
|
+
return (decodedPath === '/favicon.svg' ||
|
|
1890
|
+
decodedPath === '/favicon.ico' ||
|
|
1891
|
+
/^\/icons\/[A-Za-z0-9._-]+\.(?:png|jpg|jpeg|webp|ico|svg)$/.test(decodedPath));
|
|
1892
|
+
}
|
|
1893
|
+
function shouldSendAuthSharePage(req, url) {
|
|
1894
|
+
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
1895
|
+
return false;
|
|
1896
|
+
if (url.pathname.startsWith('/api/'))
|
|
1897
|
+
return false;
|
|
1898
|
+
if (isPublicShareAssetPath(url.pathname))
|
|
1899
|
+
return false;
|
|
1900
|
+
const ext = path.extname(url.pathname).toLowerCase();
|
|
1901
|
+
if (ext && ext !== '.html')
|
|
1902
|
+
return false;
|
|
1903
|
+
const accept = getHeaderValue(req.headers.accept).toLowerCase();
|
|
1904
|
+
return !accept || accept.includes('text/html') || accept.includes('*/*');
|
|
1905
|
+
}
|
|
1906
|
+
function escapeHtml(value) {
|
|
1907
|
+
return value
|
|
1908
|
+
.replace(/&/g, '&')
|
|
1909
|
+
.replace(/</g, '<')
|
|
1910
|
+
.replace(/>/g, '>')
|
|
1911
|
+
.replace(/"/g, '"')
|
|
1912
|
+
.replace(/'/g, ''');
|
|
1913
|
+
}
|
|
1914
|
+
function getRequestOrigin(req) {
|
|
1915
|
+
const forwardedProto = getHeaderValue(req.headers['x-forwarded-proto']).split(',')[0]?.trim().toLowerCase();
|
|
1916
|
+
const proto = forwardedProto === 'http' || forwardedProto === 'https' ? forwardedProto : httpScheme;
|
|
1917
|
+
const forwardedHost = getHeaderValue(req.headers['x-forwarded-host']).split(',')[0]?.trim();
|
|
1918
|
+
const host = forwardedHost || getHeaderValue(req.headers.host) || `localhost:${PORT}`;
|
|
1919
|
+
return `${proto}://${host}`;
|
|
1920
|
+
}
|
|
1921
|
+
function toAbsoluteRequestUrl(req, pathname) {
|
|
1922
|
+
try {
|
|
1923
|
+
return new URL(pathname, `${getRequestOrigin(req)}/`).toString();
|
|
1924
|
+
}
|
|
1925
|
+
catch {
|
|
1926
|
+
return pathname;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
function buildAuthRequiredHtml(req, url) {
|
|
1930
|
+
const title = escapeHtml(SHARE_TITLE);
|
|
1931
|
+
const description = escapeHtml(SHARE_DESCRIPTION);
|
|
1932
|
+
const message = escapeHtml(AUTH_REQUIRED_MESSAGE);
|
|
1933
|
+
const imageUrl = escapeHtml(toAbsoluteRequestUrl(req, SHARE_IMAGE_PATH));
|
|
1934
|
+
const pageUrl = escapeHtml(toAbsoluteRequestUrl(req, url.pathname));
|
|
1935
|
+
return `<!DOCTYPE html>
|
|
1936
|
+
<html lang="zh-CN">
|
|
1937
|
+
<head>
|
|
1938
|
+
<meta charset="UTF-8" />
|
|
1939
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1940
|
+
<meta name="description" content="${description}" />
|
|
1941
|
+
<meta name="theme-color" content="#030712" />
|
|
1942
|
+
<meta property="og:type" content="website" />
|
|
1943
|
+
<meta property="og:site_name" content="AgentPilot" />
|
|
1944
|
+
<meta property="og:title" content="${title}" />
|
|
1945
|
+
<meta property="og:description" content="${description}" />
|
|
1946
|
+
<meta property="og:url" content="${pageUrl}" />
|
|
1947
|
+
<meta property="og:image" content="${imageUrl}" />
|
|
1948
|
+
<meta property="og:image:width" content="512" />
|
|
1949
|
+
<meta property="og:image:height" content="512" />
|
|
1950
|
+
<meta name="twitter:card" content="summary" />
|
|
1951
|
+
<meta name="twitter:title" content="${title}" />
|
|
1952
|
+
<meta name="twitter:description" content="${description}" />
|
|
1953
|
+
<meta name="twitter:image" content="${imageUrl}" />
|
|
1954
|
+
<link rel="icon" type="image/png" sizes="512x512" href="${imageUrl}" />
|
|
1955
|
+
<title>${title}</title>
|
|
1956
|
+
<style>
|
|
1957
|
+
html { color-scheme: dark; background: #030712; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1958
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; box-sizing: border-box; }
|
|
1959
|
+
main { max-width: 520px; }
|
|
1960
|
+
h1 { margin: 0 0 12px; font-size: 28px; line-height: 1.2; }
|
|
1961
|
+
p { margin: 0; color: #9ca3af; line-height: 1.7; }
|
|
1962
|
+
</style>
|
|
1963
|
+
</head>
|
|
1964
|
+
<body>
|
|
1965
|
+
<main>
|
|
1966
|
+
<h1>${title}</h1>
|
|
1967
|
+
<p>${message}</p>
|
|
1968
|
+
</main>
|
|
1969
|
+
</body>
|
|
1970
|
+
</html>`;
|
|
1971
|
+
}
|
|
1972
|
+
function sendHtml(req, res, status, html) {
|
|
1973
|
+
if (res.writableEnded)
|
|
1974
|
+
return;
|
|
1975
|
+
setCorsHeaders(res);
|
|
1976
|
+
res.writeHead(status, {
|
|
1977
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1978
|
+
'Cache-Control': 'no-cache',
|
|
1979
|
+
});
|
|
1980
|
+
if (req.method === 'HEAD') {
|
|
1981
|
+
res.end();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
res.end(html);
|
|
1985
|
+
}
|
|
1986
|
+
function sendAuthRequired(req, res, url) {
|
|
1987
|
+
if (shouldSendAuthSharePage(req, url)) {
|
|
1988
|
+
sendHtml(req, res, 200, buildAuthRequiredHtml(req, url));
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1139
1991
|
sendJson(res, 401, {
|
|
1140
|
-
error:
|
|
1992
|
+
error: AUTH_REQUIRED_MESSAGE,
|
|
1141
1993
|
authRequired: true,
|
|
1142
1994
|
});
|
|
1143
1995
|
}
|
|
@@ -1278,6 +2130,10 @@ function getHistoryIdFromPath(pathname) {
|
|
|
1278
2130
|
const match = pathname.match(/^\/api\/history\/([^/]+)$/);
|
|
1279
2131
|
return match ? decodeURIComponent(match[1]) : null;
|
|
1280
2132
|
}
|
|
2133
|
+
function getPromptHistoryIdFromPath(pathname) {
|
|
2134
|
+
const match = pathname.match(/^\/api\/prompt-history\/([^/]+)$/);
|
|
2135
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
2136
|
+
}
|
|
1281
2137
|
function listSystemDir(rawDir, includeFiles) {
|
|
1282
2138
|
const baseDir = typeof rawDir === 'string' && rawDir ? rawDir : '~';
|
|
1283
2139
|
const dir = baseDir.startsWith('~') ? baseDir.replace('~', os.homedir()) : baseDir;
|
|
@@ -1332,12 +2188,16 @@ async function handleHttpRequest(req, res) {
|
|
|
1332
2188
|
res.end();
|
|
1333
2189
|
return;
|
|
1334
2190
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
2191
|
+
const pathname = url.pathname;
|
|
2192
|
+
const authorized = isRequestAuthorized(req, url);
|
|
2193
|
+
if (!authorized && isPublicShareAssetPath(pathname) && serveStaticClient(req, res, pathname)) {
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (!authorized) {
|
|
2197
|
+
sendAuthRequired(req, res, url);
|
|
1337
2198
|
return;
|
|
1338
2199
|
}
|
|
1339
2200
|
maybeSetAuthCookie(req, res, url);
|
|
1340
|
-
const pathname = url.pathname;
|
|
1341
2201
|
if (pathname === '/health') {
|
|
1342
2202
|
sendText(res, 200, 'AgentPilot Server');
|
|
1343
2203
|
return;
|
|
@@ -1356,7 +2216,7 @@ async function handleHttpRequest(req, res) {
|
|
|
1356
2216
|
try {
|
|
1357
2217
|
if (req.method === 'GET' && pathname === '/api/config') {
|
|
1358
2218
|
sendJson(res, 200, {
|
|
1359
|
-
config:
|
|
2219
|
+
config: getClientConfig(),
|
|
1360
2220
|
status: cliManager.status,
|
|
1361
2221
|
sessionId: currentSessionId,
|
|
1362
2222
|
time: now(),
|
|
@@ -1396,24 +2256,58 @@ async function handleHttpRequest(req, res) {
|
|
|
1396
2256
|
return;
|
|
1397
2257
|
}
|
|
1398
2258
|
if (req.method === 'GET' && pathname === '/api/history') {
|
|
1399
|
-
const
|
|
2259
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2260
|
+
const tasks = workDir ? store.getHistoryTasks({ workDir }) : [];
|
|
1400
2261
|
sendJson(res, 200, { tasks: tasks.map(task => toClientHistoryTask(task)) });
|
|
1401
2262
|
return;
|
|
1402
2263
|
}
|
|
2264
|
+
if (req.method === 'GET' && pathname === '/api/prompt-history') {
|
|
2265
|
+
const scope = normalizeHistoryScope(url.searchParams.get('scope'), 'current');
|
|
2266
|
+
const prompts = store.getPromptHistory({
|
|
2267
|
+
workDir: getHistoryWorkDirForScope(scope),
|
|
2268
|
+
query: url.searchParams.get('query') || undefined,
|
|
2269
|
+
limit: url.searchParams.get('limit') ? Number(url.searchParams.get('limit')) : undefined,
|
|
2270
|
+
});
|
|
2271
|
+
sendJson(res, 200, { prompts: prompts.map(toClientPromptHistoryItem) });
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (req.method === 'POST' && pathname === '/api/codex-history/sync') {
|
|
2275
|
+
const body = await readJsonBody(req);
|
|
2276
|
+
const result = syncCodexHistory({
|
|
2277
|
+
codexHome: process.env.AGENTPILOT_CODEX_HOME,
|
|
2278
|
+
limit: body.limit,
|
|
2279
|
+
workDir: getCurrentWorkDir(),
|
|
2280
|
+
});
|
|
2281
|
+
sendJson(res, 200, { result });
|
|
2282
|
+
broadcast({ type: 'history_changed', time: now() });
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
const promptHistoryId = getPromptHistoryIdFromPath(pathname);
|
|
2286
|
+
if (promptHistoryId && req.method === 'DELETE') {
|
|
2287
|
+
const scope = normalizeHistoryScope(url.searchParams.get('scope'), 'current');
|
|
2288
|
+
store.deletePromptHistory(promptHistoryId, getHistoryWorkDirForScope(scope));
|
|
2289
|
+
sendJson(res, 200, { id: promptHistoryId });
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
1403
2292
|
const historyId = getHistoryIdFromPath(pathname);
|
|
1404
2293
|
if (historyId && req.method === 'GET') {
|
|
1405
|
-
const
|
|
2294
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2295
|
+
const task = workDir ? store.getHistoryTask(historyId, workDir) : null;
|
|
1406
2296
|
sendJson(res, 200, { task: task ? toClientHistoryTask(task, true) : null });
|
|
1407
2297
|
return;
|
|
1408
2298
|
}
|
|
1409
2299
|
if (historyId && req.method === 'DELETE') {
|
|
1410
|
-
|
|
2300
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2301
|
+
if (workDir)
|
|
2302
|
+
store.deleteHistoryTask(historyId, workDir);
|
|
1411
2303
|
sendJson(res, 200, { id: historyId });
|
|
1412
2304
|
broadcast({ type: 'history_changed', time: now() });
|
|
1413
2305
|
return;
|
|
1414
2306
|
}
|
|
1415
2307
|
if (req.method === 'DELETE' && pathname === '/api/history') {
|
|
1416
|
-
|
|
2308
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2309
|
+
if (workDir)
|
|
2310
|
+
store.clearHistory(workDir);
|
|
1417
2311
|
sendJson(res, 200, { success: true });
|
|
1418
2312
|
broadcast({ type: 'history_changed', time: now() });
|
|
1419
2313
|
return;
|
|
@@ -1429,6 +2323,15 @@ async function handleHttpRequest(req, res) {
|
|
|
1429
2323
|
sendJson(res, 200, listSystemDir(url.searchParams.get('dir'), includeFiles));
|
|
1430
2324
|
return;
|
|
1431
2325
|
}
|
|
2326
|
+
if (req.method === 'POST' && pathname === '/api/attachments/image') {
|
|
2327
|
+
const body = await readJsonBody(req, MAX_IMAGE_ATTACHMENT_BYTES * 2);
|
|
2328
|
+
sendJson(res, 200, { attachment: saveImageAttachment(body) });
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
if ((req.method === 'GET' || req.method === 'HEAD') && pathname === '/api/attachments/file') {
|
|
2332
|
+
sendAttachmentFile(req, res, url.searchParams.get('path') || '');
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
1432
2335
|
if (req.method === 'GET' && pathname === '/api/workdir/tree') {
|
|
1433
2336
|
sendJson(res, 200, listWorkspaceDir(url.searchParams.get('path') || ''));
|
|
1434
2337
|
return;
|
|
@@ -1468,6 +2371,11 @@ async function handleHttpRequest(req, res) {
|
|
|
1468
2371
|
sendJson(res, 200, unstageWorkspacePath(body.path));
|
|
1469
2372
|
return;
|
|
1470
2373
|
}
|
|
2374
|
+
if (req.method === 'POST' && pathname === '/api/workdir/discard') {
|
|
2375
|
+
const body = await readJsonBody(req);
|
|
2376
|
+
sendJson(res, 200, discardWorkspaceChanges(body.path));
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
1471
2379
|
if (req.method === 'POST' && pathname === '/api/workdir/commit-message') {
|
|
1472
2380
|
sendJson(res, 200, await generateWorkspaceCommitMessage());
|
|
1473
2381
|
return;
|
|
@@ -1557,6 +2465,14 @@ const PERSISTABLE_TYPES = new Set([
|
|
|
1557
2465
|
'status',
|
|
1558
2466
|
]);
|
|
1559
2467
|
function broadcast(event) {
|
|
2468
|
+
if (event.type === 'status' && event.status === 'running') {
|
|
2469
|
+
maybeStartGeneratedImageCapture();
|
|
2470
|
+
}
|
|
2471
|
+
attachGeneratedImagesToEvent(event);
|
|
2472
|
+
if (event.type === 'status' && event.status !== 'idle') {
|
|
2473
|
+
promptQueueDispatching = false;
|
|
2474
|
+
promptQueueHoldUntil = 0;
|
|
2475
|
+
}
|
|
1560
2476
|
// Persist CLI session ID when we get it from system init or result
|
|
1561
2477
|
if (currentSessionId && event.type === 'system' && event.details?.session_id) {
|
|
1562
2478
|
try {
|
|
@@ -1637,6 +2553,14 @@ function broadcast(event) {
|
|
|
1637
2553
|
if (!event.id)
|
|
1638
2554
|
event.id = persisted.id;
|
|
1639
2555
|
event.seq = persisted.seq;
|
|
2556
|
+
if (event.type === 'user_message') {
|
|
2557
|
+
store.recordPromptHistory({
|
|
2558
|
+
text: event.content || '',
|
|
2559
|
+
workDir: getCurrentWorkDir(),
|
|
2560
|
+
sessionId: currentSessionId,
|
|
2561
|
+
messageId: persisted.id,
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
1640
2564
|
}
|
|
1641
2565
|
}
|
|
1642
2566
|
catch (err) {
|
|
@@ -1653,16 +2577,25 @@ function broadcast(event) {
|
|
|
1653
2577
|
catch { }
|
|
1654
2578
|
}
|
|
1655
2579
|
}
|
|
2580
|
+
if (event.type === 'status' && event.status === 'idle') {
|
|
2581
|
+
emitGeneratedImagesFallback();
|
|
2582
|
+
promptQueueHoldUntil = Date.now() + PROMPT_QUEUE_IDLE_SETTLE_MS;
|
|
2583
|
+
schedulePromptQueueDrain(PROMPT_QUEUE_IDLE_SETTLE_MS);
|
|
2584
|
+
}
|
|
1656
2585
|
}
|
|
1657
2586
|
cliManager.setEventHandler(broadcast);
|
|
2587
|
+
if (cliManager.status === 'disconnected') {
|
|
2588
|
+
cliManager.restoreReady();
|
|
2589
|
+
}
|
|
1658
2590
|
wss.on('connection', (ws) => {
|
|
1659
2591
|
clients.add(ws);
|
|
1660
2592
|
console.log(`[WS] Client connected (total: ${clients.size})`);
|
|
1661
2593
|
ws.send(JSON.stringify({
|
|
1662
2594
|
type: 'connected',
|
|
1663
|
-
config:
|
|
2595
|
+
config: getClientConfig(),
|
|
1664
2596
|
status: cliManager.status,
|
|
1665
2597
|
sessionId: currentSessionId,
|
|
2598
|
+
...getPromptQueuePayload(),
|
|
1666
2599
|
}));
|
|
1667
2600
|
ws.on('message', (data) => {
|
|
1668
2601
|
let msg;
|
|
@@ -1693,6 +2626,7 @@ wss.on('connection', (ws) => {
|
|
|
1693
2626
|
function handleMessage(ws, msg) {
|
|
1694
2627
|
switch (msg.type) {
|
|
1695
2628
|
case 'start_cli': {
|
|
2629
|
+
clearPromptQueue();
|
|
1696
2630
|
// Archive current session
|
|
1697
2631
|
if (currentSessionId) {
|
|
1698
2632
|
store.archiveSession(currentSessionId, 'completed');
|
|
@@ -1716,20 +2650,40 @@ function handleMessage(ws, msg) {
|
|
|
1716
2650
|
broadcast({
|
|
1717
2651
|
type: 'session_reset',
|
|
1718
2652
|
sessionId: currentSessionId,
|
|
1719
|
-
config:
|
|
2653
|
+
config: getClientConfig(),
|
|
1720
2654
|
time: now(),
|
|
1721
2655
|
});
|
|
1722
2656
|
broadcast({ type: 'history_changed', time: now() });
|
|
1723
2657
|
broadcast({ type: 'workdirs_changed', current: getCurrentWorkDir(), time: now() });
|
|
1724
2658
|
break;
|
|
1725
2659
|
}
|
|
1726
|
-
case 'send_message':
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
type: '
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
2660
|
+
case 'send_message': {
|
|
2661
|
+
const prompt = normalizePromptPayload(msg);
|
|
2662
|
+
if (cliManager.status === 'disconnected') {
|
|
2663
|
+
broadcast({ type: 'error', content: 'CLI 未连接,无法发送', time: now() });
|
|
2664
|
+
}
|
|
2665
|
+
else if (!hasPromptPayload(prompt)) {
|
|
2666
|
+
broadcast({ type: 'error', content: '消息不能为空', time: now() });
|
|
2667
|
+
}
|
|
2668
|
+
else if (shouldQueueIncomingPrompt()) {
|
|
2669
|
+
enqueuePrompt(prompt);
|
|
2670
|
+
}
|
|
2671
|
+
else {
|
|
2672
|
+
dispatchPromptToCli(prompt);
|
|
2673
|
+
}
|
|
2674
|
+
break;
|
|
2675
|
+
}
|
|
2676
|
+
case 'queue_prompt':
|
|
2677
|
+
enqueuePrompt(normalizePromptPayload(msg));
|
|
2678
|
+
break;
|
|
2679
|
+
case 'remove_queued_prompt':
|
|
2680
|
+
removePromptFromQueue(msg.id);
|
|
2681
|
+
break;
|
|
2682
|
+
case 'clear_prompt_queue':
|
|
2683
|
+
clearPromptQueue();
|
|
2684
|
+
break;
|
|
2685
|
+
case 'resume_prompt_queue':
|
|
2686
|
+
resumePromptQueue();
|
|
1733
2687
|
break;
|
|
1734
2688
|
case 'confirm_response':
|
|
1735
2689
|
cliManager.confirmResponse(msg.approved);
|
|
@@ -1738,9 +2692,14 @@ function handleMessage(ws, msg) {
|
|
|
1738
2692
|
cliManager.questionResponse(msg.answer, msg.toolUseId);
|
|
1739
2693
|
break;
|
|
1740
2694
|
case 'interrupt':
|
|
2695
|
+
if (promptQueue.length > 0) {
|
|
2696
|
+
promptQueuePaused = true;
|
|
2697
|
+
broadcastPromptQueueChanged();
|
|
2698
|
+
}
|
|
1741
2699
|
cliManager.interrupt();
|
|
1742
2700
|
break;
|
|
1743
2701
|
case 'restart_cli': {
|
|
2702
|
+
clearPromptQueue();
|
|
1744
2703
|
// Archive current session before restarting
|
|
1745
2704
|
if (currentSessionId) {
|
|
1746
2705
|
const taskStatus = cliManager.status === 'running' || cliManager.status === 'confirm' ? 'running' :
|
|
@@ -1754,7 +2713,7 @@ function handleMessage(ws, msg) {
|
|
|
1754
2713
|
broadcast({
|
|
1755
2714
|
type: 'session_reset',
|
|
1756
2715
|
sessionId: currentSessionId,
|
|
1757
|
-
config:
|
|
2716
|
+
config: getClientConfig(),
|
|
1758
2717
|
time: now(),
|
|
1759
2718
|
});
|
|
1760
2719
|
broadcast({ type: 'history_changed', time: now() });
|
|
@@ -1772,7 +2731,7 @@ function handleMessage(ws, msg) {
|
|
|
1772
2731
|
case 'get_config':
|
|
1773
2732
|
ws.send(JSON.stringify({
|
|
1774
2733
|
type: 'config',
|
|
1775
|
-
config:
|
|
2734
|
+
config: getClientConfig(),
|
|
1776
2735
|
status: cliManager.status,
|
|
1777
2736
|
sessionId: currentSessionId,
|
|
1778
2737
|
time: now(),
|
|
@@ -1815,7 +2774,8 @@ function handleMessage(ws, msg) {
|
|
|
1815
2774
|
}
|
|
1816
2775
|
// --- New protocol: History ---
|
|
1817
2776
|
case 'get_history': {
|
|
1818
|
-
const
|
|
2777
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2778
|
+
const tasks = workDir ? store.getHistoryTasks({ workDir }) : [];
|
|
1819
2779
|
ws.send(JSON.stringify({
|
|
1820
2780
|
type: 'history_data',
|
|
1821
2781
|
requestId: msg.requestId,
|
|
@@ -1824,7 +2784,8 @@ function handleMessage(ws, msg) {
|
|
|
1824
2784
|
break;
|
|
1825
2785
|
}
|
|
1826
2786
|
case 'get_history_task': {
|
|
1827
|
-
const
|
|
2787
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2788
|
+
const task = workDir ? store.getHistoryTask(msg.id, workDir) : null;
|
|
1828
2789
|
ws.send(JSON.stringify({
|
|
1829
2790
|
type: 'history_task_data',
|
|
1830
2791
|
requestId: msg.requestId,
|
|
@@ -1842,7 +2803,8 @@ function handleMessage(ws, msg) {
|
|
|
1842
2803
|
}));
|
|
1843
2804
|
break;
|
|
1844
2805
|
}
|
|
1845
|
-
const
|
|
2806
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2807
|
+
const task = workDir ? store.getHistoryTask(msg.id, workDir) : null;
|
|
1846
2808
|
const cliSessionId = task ? getRestorableCliSessionId(task.session_id, task.messages) : undefined;
|
|
1847
2809
|
if (!task || !task.session_id || !cliSessionId) {
|
|
1848
2810
|
ws.send(JSON.stringify({
|
|
@@ -1853,7 +2815,7 @@ function handleMessage(ws, msg) {
|
|
|
1853
2815
|
}));
|
|
1854
2816
|
break;
|
|
1855
2817
|
}
|
|
1856
|
-
const restored = store.resumeHistoryTask(msg.id, currentSessionId,
|
|
2818
|
+
const restored = store.resumeHistoryTask(msg.id, currentSessionId, workDir);
|
|
1857
2819
|
if (!restored) {
|
|
1858
2820
|
ws.send(JSON.stringify({
|
|
1859
2821
|
type: 'history_resumed',
|
|
@@ -1868,6 +2830,9 @@ function handleMessage(ws, msg) {
|
|
|
1868
2830
|
...getSessionConfig(restored.session),
|
|
1869
2831
|
cliSessionId,
|
|
1870
2832
|
};
|
|
2833
|
+
if (typeof restoredConfig.workDir !== 'string' && typeof restored.work_dir === 'string') {
|
|
2834
|
+
restoredConfig.workDir = restored.work_dir;
|
|
2835
|
+
}
|
|
1871
2836
|
cliManager.restoreConfig(restoredConfig);
|
|
1872
2837
|
cliManager.restoreSessionId(cliSessionId);
|
|
1873
2838
|
if (typeof restoredConfig.workDir === 'string') {
|
|
@@ -1883,7 +2848,7 @@ function handleMessage(ws, msg) {
|
|
|
1883
2848
|
broadcast({
|
|
1884
2849
|
type: 'session_restored',
|
|
1885
2850
|
sessionId: currentSessionId,
|
|
1886
|
-
config:
|
|
2851
|
+
config: getClientConfig(),
|
|
1887
2852
|
status: cliManager.status,
|
|
1888
2853
|
messages: sessionMessages,
|
|
1889
2854
|
lastSeq,
|
|
@@ -1905,7 +2870,7 @@ function handleMessage(ws, msg) {
|
|
|
1905
2870
|
requestId: msg.requestId,
|
|
1906
2871
|
success: true,
|
|
1907
2872
|
sessionId: currentSessionId,
|
|
1908
|
-
config:
|
|
2873
|
+
config: getClientConfig(),
|
|
1909
2874
|
messages: sessionMessages,
|
|
1910
2875
|
lastSeq,
|
|
1911
2876
|
sent: !!followUp,
|
|
@@ -1961,7 +2926,7 @@ function handleMessage(ws, msg) {
|
|
|
1961
2926
|
broadcast({
|
|
1962
2927
|
type: 'session_restored',
|
|
1963
2928
|
sessionId: currentSessionId,
|
|
1964
|
-
config:
|
|
2929
|
+
config: getClientConfig(),
|
|
1965
2930
|
status: cliManager.status,
|
|
1966
2931
|
messages: sessionMessages,
|
|
1967
2932
|
lastSeq,
|
|
@@ -1983,7 +2948,9 @@ function handleMessage(ws, msg) {
|
|
|
1983
2948
|
break;
|
|
1984
2949
|
}
|
|
1985
2950
|
case 'delete_history': {
|
|
1986
|
-
|
|
2951
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2952
|
+
if (workDir)
|
|
2953
|
+
store.deleteHistoryTask(msg.id, workDir);
|
|
1987
2954
|
ws.send(JSON.stringify({
|
|
1988
2955
|
type: 'history_deleted',
|
|
1989
2956
|
requestId: msg.requestId,
|
|
@@ -1994,7 +2961,9 @@ function handleMessage(ws, msg) {
|
|
|
1994
2961
|
break;
|
|
1995
2962
|
}
|
|
1996
2963
|
case 'clear_history': {
|
|
1997
|
-
|
|
2964
|
+
const workDir = getCurrentHistoryWorkDir();
|
|
2965
|
+
if (workDir)
|
|
2966
|
+
store.clearHistory(workDir);
|
|
1998
2967
|
ws.send(JSON.stringify({
|
|
1999
2968
|
type: 'history_cleared',
|
|
2000
2969
|
requestId: msg.requestId,
|
|
@@ -2155,7 +3124,7 @@ function handleMessage(ws, msg) {
|
|
|
2155
3124
|
}
|
|
2156
3125
|
}
|
|
2157
3126
|
function now() {
|
|
2158
|
-
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
3127
|
+
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
|
|
2159
3128
|
}
|
|
2160
3129
|
function formatHostForUrl(host) {
|
|
2161
3130
|
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
@@ -2182,6 +3151,14 @@ function getNetworkUrls() {
|
|
|
2182
3151
|
function shouldUseColor() {
|
|
2183
3152
|
return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
2184
3153
|
}
|
|
3154
|
+
function getQrRenderMode() {
|
|
3155
|
+
const requested = (process.env.AGENTPILOT_QR_STYLE || '').trim().toLowerCase();
|
|
3156
|
+
if (requested === 'compact' || requested === 'small')
|
|
3157
|
+
return 'compact';
|
|
3158
|
+
if (requested === 'large' || requested === 'safe')
|
|
3159
|
+
return 'large';
|
|
3160
|
+
return process.env.TERM_PROGRAM === 'Apple_Terminal' ? 'large' : 'compact';
|
|
3161
|
+
}
|
|
2185
3162
|
const terminalStyle = {
|
|
2186
3163
|
reset: '\x1b[0m',
|
|
2187
3164
|
bold: '\x1b[1m',
|
|
@@ -2225,8 +3202,10 @@ function printMacSleepHint() {
|
|
|
2225
3202
|
console.log(' 这样电脑接入电源且显示器关闭后,AgentPilot 后端和底层 CLI 更不容易被系统睡眠中断。');
|
|
2226
3203
|
console.log('');
|
|
2227
3204
|
}
|
|
2228
|
-
function printStartupInfo() {
|
|
3205
|
+
async function printStartupInfo() {
|
|
2229
3206
|
if (!IS_NPX_ENTRY) {
|
|
3207
|
+
if (PACKAGE_VERSION)
|
|
3208
|
+
console.log(`[AgentPilot] Version ${PACKAGE_VERSION}`);
|
|
2230
3209
|
console.log(`[AgentPilot] Server running on ${withStartupToken(`${httpScheme}://${HOST}:${PORT}`)}`);
|
|
2231
3210
|
console.log(`[AgentPilot] WebSocket ready on ${wsScheme}://${HOST}:${PORT}`);
|
|
2232
3211
|
if (AUTH_ENABLED)
|
|
@@ -2246,7 +3225,10 @@ function printStartupInfo() {
|
|
|
2246
3225
|
printSection('open on mobile');
|
|
2247
3226
|
printLine('Mobile', networkUrls[0], true);
|
|
2248
3227
|
printLine('Scan', 'use the QR code below');
|
|
2249
|
-
const qr = renderTerminalQr(networkUrls[0], {
|
|
3228
|
+
const qr = await renderTerminalQr(networkUrls[0], {
|
|
3229
|
+
color: shouldUseColor(),
|
|
3230
|
+
mode: getQrRenderMode(),
|
|
3231
|
+
});
|
|
2250
3232
|
if (qr) {
|
|
2251
3233
|
console.log('');
|
|
2252
3234
|
console.log(colorize('QR CODE', terminalStyle.bold, terminalStyle.dim));
|
|
@@ -2270,6 +3252,8 @@ function printStartupInfo() {
|
|
|
2270
3252
|
printLine('Token', tokenStatus, AUTH_ENABLED);
|
|
2271
3253
|
console.log('');
|
|
2272
3254
|
printSection('runtime');
|
|
3255
|
+
if (PACKAGE_VERSION)
|
|
3256
|
+
printLine('Version', PACKAGE_VERSION);
|
|
2273
3257
|
printLine('Workdir', config.workDir);
|
|
2274
3258
|
printLine('CLI', `${config.cliCommand} (${config.cliType})`);
|
|
2275
3259
|
console.log('');
|
|
@@ -2287,5 +3271,7 @@ process.on('SIGTERM', () => {
|
|
|
2287
3271
|
process.exit(0);
|
|
2288
3272
|
});
|
|
2289
3273
|
server.listen(PORT, HOST, () => {
|
|
2290
|
-
printStartupInfo()
|
|
3274
|
+
printStartupInfo().catch((err) => {
|
|
3275
|
+
console.warn(`[AgentPilot] Failed to print startup info: ${err?.message || err}`);
|
|
3276
|
+
});
|
|
2291
3277
|
});
|