coderfleet 0.1.0__py3-none-any.whl

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.
Files changed (45) hide show
  1. coderfleet/__init__.py +1 -0
  2. coderfleet/__main__.py +4 -0
  3. coderfleet/cli.py +212 -0
  4. coderfleet/compose.py +176 -0
  5. coderfleet/config.py +69 -0
  6. coderfleet/config_cmds.py +243 -0
  7. coderfleet/data/Dockerfile +92 -0
  8. coderfleet/data/__init__.py +0 -0
  9. coderfleet/data/accounts.conf.example +26 -0
  10. coderfleet/data/config.conf.example +31 -0
  11. coderfleet/data/entrypoint.sh +56 -0
  12. coderfleet/data/projects.conf.example +17 -0
  13. coderfleet/data/scripts/coderfleet_usage_status.py +138 -0
  14. coderfleet/docker_ops.py +385 -0
  15. coderfleet/init_wizard.py +227 -0
  16. coderfleet/login_cmd.py +168 -0
  17. coderfleet/server/__init__.py +0 -0
  18. coderfleet/server/docker_mgr.py +45 -0
  19. coderfleet/server/main.py +546 -0
  20. coderfleet/server/models.py +285 -0
  21. coderfleet/server/scheduler.py +1219 -0
  22. coderfleet/server/static/css/main.css +2906 -0
  23. coderfleet/server/static/index.html +378 -0
  24. coderfleet/server/static/js/accounts.js +85 -0
  25. coderfleet/server/static/js/app.js +28 -0
  26. coderfleet/server/static/js/chat.js +743 -0
  27. coderfleet/server/static/js/log.js +145 -0
  28. coderfleet/server/static/js/nav.js +46 -0
  29. coderfleet/server/static/js/projects.js +298 -0
  30. coderfleet/server/static/js/renderer.js +586 -0
  31. coderfleet/server/static/js/state.js +76 -0
  32. coderfleet/server/static/js/submit.js +200 -0
  33. coderfleet/server/static/js/tasks.js +92 -0
  34. coderfleet/server/static/js/terminal.js +347 -0
  35. coderfleet/server/static/js/utils.js +147 -0
  36. coderfleet/server/static/vendor/marked.min.js +6 -0
  37. coderfleet/server/static/vendor/xterm/addon-fit.js +2 -0
  38. coderfleet/server/static/vendor/xterm/xterm.css +218 -0
  39. coderfleet/server/static/vendor/xterm/xterm.js +2 -0
  40. coderfleet/server/terminal.py +129 -0
  41. coderfleet/task_cmds.py +311 -0
  42. coderfleet-0.1.0.dist-info/METADATA +492 -0
  43. coderfleet-0.1.0.dist-info/RECORD +45 -0
  44. coderfleet-0.1.0.dist-info/WHEEL +4 -0
  45. coderfleet-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,145 @@
1
+
2
+ // ══════════════════════════════════════════════════════════════
3
+ // 日志模态框
4
+ // ══════════════════════════════════════════════════════════════
5
+ async function openLogModal(taskId) {
6
+ currentTaskId = taskId;
7
+ currentTaskData = null;
8
+ followMode = false;
9
+ stopFollow();
10
+
11
+ document.getElementById('log-modal').style.display = 'flex';
12
+ document.getElementById('log-content').innerHTML = '<div style="color:#3a3a3e;font-size:12px;padding:20px">加载中...</div>';
13
+ document.getElementById('log-meta').innerHTML = '';
14
+ resetLogSummary();
15
+ document.getElementById('kill-btn').style.display = 'none';
16
+ document.getElementById('resume-btn').style.display = 'none';
17
+ setFollowButton(false);
18
+
19
+ // 初始化 renderer
20
+ renderer = new ChatLogRenderer(document.getElementById('log-content'), true, true);
21
+
22
+ try {
23
+ const [task, logText] = await Promise.all([
24
+ fetch(`${API}/api/tasks/${taskId}`).then(r => r.json()),
25
+ fetch(`${API}/api/tasks/${taskId}/logs`).then(r => r.text()).catch(() => ''),
26
+ ]);
27
+ currentTaskData = task;
28
+ updateLogSummary(task);
29
+
30
+ document.getElementById('log-modal-title').textContent = `任务日志 — ${taskId}`;
31
+ document.getElementById('log-meta').innerHTML = `
32
+ <span class="status-dot ${task.status}" style="font-size:12px">${statusLabel(task.status)}</span>
33
+ <span>·</span><span class="badge ${task.type}">${task.type}</span>
34
+ <span>${esc(task.account)}</span>
35
+ <span>·</span><span class="text-muted">${esc(task.project.split('/').pop())}</span>
36
+ <span>·</span><span class="text-muted">${fmtTime(task.created)}</span>`;
37
+
38
+ if (task.status === 'running') document.getElementById('kill-btn').style.display = '';
39
+ if (task.conversation_id || task.native_session_id) document.getElementById('resume-btn').style.display = '';
40
+
41
+ renderer.render(logText);
42
+ scrollChatToBottom();
43
+
44
+ if (task.status === 'running') startFollow();
45
+ } catch (e) {
46
+ document.getElementById('log-content').innerHTML =
47
+ `<div style="color:#f87171;padding:16px">加载失败:${esc(e.message)}</div>`;
48
+ }
49
+ }
50
+
51
+ // ── SSE 跟踪 ──────────────────────────────────────────────
52
+ function startFollow() {
53
+ stopFollow();
54
+ followMode = true;
55
+ setFollowButton(true);
56
+
57
+ // tail=0: 只推送新内容,避免与初次全量加载重复
58
+ sseSource = new EventSource(`${API}/api/tasks/${currentTaskId}/logs/stream?tail=0`);
59
+ sseSource.onmessage = e => {
60
+ if (e.data === '[DONE]') {
61
+ stopFollow();
62
+ // 刷新任务状态,隐藏终止按钮
63
+ fetch(`${API}/api/tasks/${currentTaskId}`).then(r => r.json()).then(t => {
64
+ if (t.status !== 'running') document.getElementById('kill-btn').style.display = 'none';
65
+ }).catch(() => { });
66
+ return;
67
+ }
68
+ if (renderer) renderer.push(e.data);
69
+ scrollChatToBottom();
70
+ };
71
+ sseSource.onerror = () => stopFollow();
72
+ }
73
+
74
+ function stopFollow() {
75
+ if (sseSource) { sseSource.close(); sseSource = null; }
76
+ followMode = false;
77
+ setFollowButton(false);
78
+ }
79
+
80
+ function toggleFollow() { followMode ? stopFollow() : startFollow(); }
81
+
82
+ function setFollowButton(active) {
83
+ const btn = document.getElementById('follow-btn');
84
+ if (!btn) return;
85
+ btn.innerHTML = active
86
+ ? '<span aria-hidden="true" id="follow-icon">■</span><span>停止</span>'
87
+ : '<span aria-hidden="true" id="follow-icon">▶</span><span>跟踪</span>';
88
+ }
89
+
90
+ function resetLogSummary() {
91
+ setText('log-summary-status', '-');
92
+ setText('log-summary-account', '-');
93
+ setText('log-summary-project', '-');
94
+ setText('log-summary-created', '-');
95
+ }
96
+
97
+ function updateLogSummary(task) {
98
+ setText('log-summary-status', statusLabel(task.status));
99
+ setText('log-summary-account', `${task.type} · ${task.account}`);
100
+ setText('log-summary-project', task.project ? task.project.split('/').pop() : '-');
101
+ setText('log-summary-created', fmtTime(task.created));
102
+ }
103
+
104
+ function scrollChatToBottom() {
105
+ const panel = document.getElementById('log-panel');
106
+ if (panel) panel.scrollTop = panel.scrollHeight;
107
+ }
108
+
109
+ function closeLogModal(e) {
110
+ if (e && e.target !== document.getElementById('log-modal')) return;
111
+ stopFollow();
112
+ document.getElementById('log-modal').style.display = 'none';
113
+ currentTaskId = null; currentTaskData = null;
114
+ }
115
+
116
+ // ── 续接任务 ──────────────────────────────────────────────
117
+ async function resumeCurrentTask() {
118
+ if (!currentTaskData) return;
119
+ const { id: taskId, conversation_id: convId, native_session_id: nativeId } = currentTaskData;
120
+
121
+ if (convId) {
122
+ closeLogModal();
123
+ await openTaskSubmitPanel({ mode: 'resume', conversationId: convId });
124
+ return;
125
+ }
126
+ if (nativeId) {
127
+ const name = prompt('该任务尚未加入任务链。请输入新任务链名称:', `续接-${taskId.slice(0, 8)}`);
128
+ if (!name?.trim()) return;
129
+ const btn = document.getElementById('resume-btn');
130
+ btn.disabled = true; btn.textContent = '创建中...';
131
+ try {
132
+ const r = await fetch(`${API}/api/conversations`, {
133
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({ name: name.trim(), task_id: taskId }),
135
+ });
136
+ const data = await r.json();
137
+ if (!r.ok) throw new Error(data.detail || r.statusText);
138
+ closeLogModal();
139
+ await openTaskSubmitPanel({ mode: 'resume', conversationId: data.id });
140
+ } catch (e) { alert('创建任务链失败:' + e.message); }
141
+ finally { btn.disabled = false; btn.textContent = '续接任务'; }
142
+ return;
143
+ }
144
+ alert('该任务没有可用的会话 ID,无法续接。');
145
+ }
@@ -0,0 +1,46 @@
1
+ // ── 页面切换 ──────────────────────────────────────────────
2
+ function showPage(name) {
3
+ if (name !== 'projects') disconnectProjectTerminal();
4
+ currentPage = name;
5
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
6
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
7
+ document.getElementById('page-' + name).classList.add('active');
8
+ document.querySelector(`[data-page="${name}"]`).classList.add('active');
9
+
10
+ const titles = { chat: '任务开发', tasks: '任务监控', projects: '项目管理', accounts: '账号状态' };
11
+ document.getElementById('page-title').textContent = titles[name] || name;
12
+
13
+ // 控制 Tab 系统和 + 按钮的显示/隐藏
14
+ const tabsEl = document.getElementById('topbar-tabs');
15
+ const addBtn = document.getElementById('add-tab-btn');
16
+ if (tabsEl) tabsEl.style.display = (name === 'chat' || name === 'terminal') ? '' : 'none';
17
+ if (addBtn) addBtn.style.display = (name === 'chat' || name === 'terminal') ? '' : 'none';
18
+
19
+ if (name === 'chat') {
20
+ activeTabId = 'chat';
21
+ renderTopbarTabs();
22
+ }
23
+
24
+ refreshCurrent();
25
+ }
26
+
27
+ function refreshCurrent() {
28
+ if (currentPage === 'chat') loadConversations();
29
+ else if (currentPage === 'tasks') loadTasks();
30
+ else if (currentPage === 'projects') loadProjectsDashboard();
31
+ else if (currentPage === 'accounts') loadAccounts();
32
+ }
33
+
34
+ // ── 健康检查 ──────────────────────────────────────────────
35
+ async function checkHealth() {
36
+ try {
37
+ const r = await fetch(`${API}/api/health`);
38
+ const dot = document.getElementById('health-dot');
39
+ const txt = document.getElementById('health-text');
40
+ dot.className = r.ok ? 'health-dot ok' : 'health-dot err';
41
+ txt.textContent = r.ok ? '服务正常' : '服务异常';
42
+ } catch {
43
+ document.getElementById('health-dot').className = 'health-dot err';
44
+ document.getElementById('health-text').textContent = '无法连接';
45
+ }
46
+ }
@@ -0,0 +1,298 @@
1
+ // ── 项目工作台 ────────────────────────────────────────────
2
+ function normalizeProjectPath(path) {
3
+ return String(path || '').replace(/\/+$/, '');
4
+ }
5
+
6
+ function projectPathContains(projectPath, recordPath) {
7
+ const base = normalizeProjectPath(projectPath);
8
+ const target = normalizeProjectPath(recordPath);
9
+ return !!base && (target === base || target.startsWith(base + '/'));
10
+ }
11
+
12
+ function canonicalProjectForLegacyRecord(record) {
13
+ const recordPath = String(record?.project || '');
14
+ return (projectsCache || []).find(p => projectPathContains(p.path, recordPath)) || null;
15
+ }
16
+
17
+ function legacyRecordBelongsToProject(record, project) {
18
+ if (!projectPathContains(project?.path, record?.project)) return false;
19
+
20
+ const canonical = canonicalProjectForLegacyRecord(record);
21
+ if (canonical) return canonical.name === project.name;
22
+
23
+ return true;
24
+ }
25
+
26
+ function taskBelongsToProject(task, project) {
27
+ if (task.project_name) return task.project_name === project.name;
28
+ return legacyRecordBelongsToProject(task, project);
29
+ }
30
+
31
+ function conversationBelongsToProject(conversation, project) {
32
+ if (conversation.project_name) return conversation.project_name === project.name;
33
+ return legacyRecordBelongsToProject(conversation, project);
34
+ }
35
+
36
+ async function loadProjectsDashboard() {
37
+ try {
38
+ const [projects, tasks, accounts] = await Promise.all([
39
+ fetch(`${API}/api/projects`).then(r => r.json()).catch(() => []),
40
+ fetch(`${API}/api/tasks?limit=100`).then(r => r.json()).catch(() => []),
41
+ fetch(`${API}/api/accounts`).then(r => r.json()).catch(() => []),
42
+ ]);
43
+ projectsCache = projects;
44
+ projectDashboardData = { tasks, accounts };
45
+ renderProjectCards(projects, tasks, accounts);
46
+ if (projectContext) {
47
+ const current = projects.find(p => p.name === projectContext.name);
48
+ current ? renderProjectDetail(current) : backToProjects();
49
+ }
50
+ } catch (e) {
51
+ document.getElementById('project-grid').innerHTML = `<div class="empty">加载失败:${esc(e.message)}</div>`;
52
+ }
53
+ }
54
+
55
+ function renderProjectCards(projects, tasks, accounts) {
56
+ const grid = document.getElementById('project-grid');
57
+ if (!projects.length) {
58
+ grid.innerHTML = `<div class="empty">暂无项目配置</div>`;
59
+ return;
60
+ }
61
+ const accountMap = new Map(accounts.map(a => [a.name, a]));
62
+ grid.innerHTML = projects.map(project => {
63
+ const projectTasks = tasks.filter(t => taskBelongsToProject(t, project));
64
+ const running = projectTasks.filter(t => t.status === 'running').length;
65
+ const failed = projectTasks.filter(t => t.status === 'failed').length;
66
+ const latest = [...projectTasks].sort((a, b) => new Date(b.created || 0) - new Date(a.created || 0))[0];
67
+ const account = accountMap.get(project.account);
68
+ const accountBadge = account
69
+ ? `<span class="badge ${account.type}">${account.type}</span><span class="badge proxy-${account.proxy || 'relay'}">proxy: ${esc(account.proxy || 'relay')}</span>`
70
+ : `<span class="badge offline">账号缺失</span>`;
71
+ let latestText = '还没有任务记录';
72
+ if (latest && latest.prompt) {
73
+ const cleanPrompt = String(latest.prompt || '').replace(/\s+/g, ' ');
74
+ const maxLen = 100;
75
+ const truncated = cleanPrompt.length > maxLen ? cleanPrompt.substring(0, maxLen) + '...' : cleanPrompt;
76
+ latestText = `最近:${esc(truncated)}`;
77
+ }
78
+ return `<div class="project-card" onclick="openProject('${esc(project.name)}')">
79
+ <div class="project-head">
80
+ <div style="min-width:0">
81
+ <div class="project-title">${esc(project.name)}</div>
82
+ <div class="project-path" title="${esc(project.path)}">${esc(project.path)}</div>
83
+ </div>
84
+ </div>
85
+ <div class="account-badges" style="margin-top:10px">${accountBadge}<span class="chip">${esc(project.account)}</span></div>
86
+ <div class="project-stats">
87
+ <div class="project-stat"><div class="account-stat-label">总任务</div><div class="account-stat-value">${projectTasks.length}</div></div>
88
+ <div class="project-stat"><div class="account-stat-label">运行中</div><div class="account-stat-value">${running}</div></div>
89
+ <div class="project-stat"><div class="account-stat-label">完成</div><div class="account-stat-value">${projectTasks.filter(t => t.status === 'done').length}</div></div>
90
+ <div class="project-stat"><div class="account-stat-label">失败</div><div class="account-stat-value">${failed}</div></div>
91
+ </div>
92
+ <div class="account-meta" style="margin-top:10px" ${latest ? `title="${esc(latest.prompt)}"` : ''}>${latestText}</div>
93
+ </div>`;
94
+ }).join('');
95
+ }
96
+
97
+ async function openProject(name) {
98
+ const project = projectsCache.find(p => p.name === name);
99
+ if (!project) return;
100
+ projectContext = project;
101
+ document.getElementById('project-list-view').style.display = 'none';
102
+ document.getElementById('project-detail-view').style.display = '';
103
+ document.getElementById('page-title').textContent = `项目 · ${project.name}`;
104
+ await loadProjectsDashboard();
105
+ }
106
+
107
+ function backToProjects() {
108
+ disconnectProjectTerminal();
109
+ projectContext = null;
110
+ document.getElementById('project-detail-view').style.display = 'none';
111
+ document.getElementById('project-list-view').style.display = '';
112
+ document.getElementById('page-title').textContent = '项目管理';
113
+ }
114
+
115
+ function renderProjectDetail(project) {
116
+ const { tasks, accounts } = projectDashboardData;
117
+ const account = accounts.find(a => a.name === project.account);
118
+ const projectTasks = tasks.filter(t => taskBelongsToProject(t, project));
119
+ const running = projectTasks.filter(t => t.status === 'running').length;
120
+ const done = projectTasks.filter(t => t.status === 'done').length;
121
+ const failed = projectTasks.filter(t => t.status === 'failed').length;
122
+
123
+ document.getElementById('project-detail-summary').innerHTML = `
124
+ <div class="project-title">${esc(project.name)}</div>
125
+ <div class="project-path" title="${esc(project.path)}">${esc(project.path)}</div>
126
+ <div class="account-badges" style="margin-top:12px">
127
+ <span class="chip">账号 ${esc(project.account)}</span>
128
+ ${account ? `<span class="badge ${account.type}">${account.type}</span><span class="badge proxy-${account.proxy || 'relay'}">proxy: ${esc(account.proxy || 'relay')}</span>` : `<span class="badge offline">账号缺失</span>`}
129
+ </div>
130
+ <div class="project-stats" style="margin-top:16px">
131
+ <div class="project-stat"><div class="account-stat-label">总任务</div><div class="account-stat-value">${projectTasks.length}</div></div>
132
+ <div class="project-stat"><div class="account-stat-label">运行中</div><div class="account-stat-value" style="${running > 0 ? 'color:var(--green)' : ''}">${running}</div></div>
133
+ <div class="project-stat"><div class="account-stat-label">完成</div><div class="account-stat-value">${done}</div></div>
134
+ <div class="project-stat"><div class="account-stat-label">失败</div><div class="account-stat-value" style="${failed > 0 ? 'color:var(--red)' : ''}">${failed}</div></div>
135
+ </div>`;
136
+
137
+ updateTerminalWarning();
138
+ }
139
+
140
+ function submitForCurrentProject() {
141
+ if (!projectContext) return;
142
+ startNewChat({ projectName: projectContext.name });
143
+ }
144
+
145
+ // ── 项目终端 ──────────────────────────────────────────────
146
+ function terminalWsUrl(projectName) {
147
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
148
+ return `${proto}//${location.host}${API}/api/projects/${encodeURIComponent(projectName)}/terminal`;
149
+ }
150
+
151
+ function setTerminalStatus(state, message) {
152
+ terminalContext.lastState = state;
153
+ const dot = document.getElementById('project-terminal-dot');
154
+ const status = document.getElementById('project-terminal-status');
155
+ if (dot) {
156
+ const cls = state === 'connected' ? 'running' : state === 'error' ? 'failed' : 'killed';
157
+ dot.className = `status-dot ${cls}`;
158
+ dot.textContent = state === 'connected' ? '已连接' : state === 'error' ? '错误' : '未连接';
159
+ }
160
+ if (status) status.textContent = message || '';
161
+ }
162
+
163
+ function updateTerminalWarning() {
164
+ const warning = document.getElementById('project-terminal-warning');
165
+ if (!warning || !projectContext) return;
166
+ const account = projectDashboardData.accounts.find(a => a.name === projectContext.account);
167
+ if (account?.busy) {
168
+ warning.textContent = `账号 ${account.name} 当前有运行中的调度任务,终端仍可使用。`;
169
+ warning.classList.add('active');
170
+ } else {
171
+ warning.textContent = '';
172
+ warning.classList.remove('active');
173
+ }
174
+ }
175
+
176
+ function openProjectTerminal() {
177
+ if (!projectContext) return;
178
+ updateTerminalWarning();
179
+ if (terminalContext.projectName !== projectContext.name) {
180
+ disconnectProjectTerminal();
181
+ }
182
+ terminalContext.projectName = projectContext.name;
183
+ if (!terminalContext.socket || terminalContext.socket.readyState === WebSocket.CLOSED) {
184
+ connectProjectTerminal();
185
+ } else {
186
+ resizeProjectTerminal();
187
+ }
188
+ }
189
+
190
+ function ensureTerminalMounted() {
191
+ const mount = document.getElementById('project-terminal');
192
+ if (!mount || !window.Terminal || !window.FitAddon) return false;
193
+ if (terminalContext.terminal && mount.childElementCount) return true;
194
+
195
+ mount.innerHTML = '';
196
+ terminalContext.fitAddon = new window.FitAddon.FitAddon();
197
+ terminalContext.terminal = new window.Terminal({
198
+ cursorBlink: true,
199
+ fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', monospace",
200
+ fontSize: 13,
201
+ scrollback: 5000,
202
+ theme: {
203
+ background: '#111827',
204
+ foreground: '#e5e7eb',
205
+ cursor: '#f97316',
206
+ selectionBackground:'#334155',
207
+ },
208
+ });
209
+ terminalContext.terminal.loadAddon(terminalContext.fitAddon);
210
+ terminalContext.terminal.open(mount);
211
+ terminalContext.terminal.onData(data => {
212
+ const socket = terminalContext.socket;
213
+ if (socket && socket.readyState === WebSocket.OPEN) {
214
+ socket.send(JSON.stringify({ type: 'input', data }));
215
+ }
216
+ });
217
+ resizeProjectTerminal();
218
+ return true;
219
+ }
220
+
221
+ function connectProjectTerminal() {
222
+ if (!projectContext) return;
223
+ disconnectProjectTerminal();
224
+ terminalContext.projectName = projectContext.name;
225
+ if (!ensureTerminalMounted()) {
226
+ setTerminalStatus('error', '终端资源未加载,请刷新页面后重试');
227
+ return;
228
+ }
229
+ updateTerminalWarning();
230
+ setTerminalStatus('closed', '正在连接项目容器...');
231
+ terminalContext.terminal.clear();
232
+ terminalContext.terminal.writeln(`Connecting to ${projectContext.name}...`);
233
+
234
+ const socket = new WebSocket(terminalWsUrl(projectContext.name));
235
+ terminalContext.socket = socket;
236
+
237
+ socket.addEventListener('open', () => {
238
+ terminalContext.connected = true;
239
+ resizeProjectTerminal();
240
+ });
241
+ socket.addEventListener('message', event => {
242
+ let message;
243
+ try { message = JSON.parse(event.data); } catch { return; }
244
+ if (message.type === 'output') {
245
+ terminalContext.terminal.write(String(message.data || ''));
246
+ } else if (message.type === 'status') {
247
+ terminalContext.connected = message.state === 'connected';
248
+ setTerminalStatus(message.state, message.message || '');
249
+ if (message.state === 'error') terminalContext.terminal.writeln(`\r\n${message.message || '连接失败'}`);
250
+ }
251
+ });
252
+ socket.addEventListener('close', () => {
253
+ terminalContext.connected = false;
254
+ if (terminalContext.lastState !== 'error') {
255
+ setTerminalStatus('closed', '终端连接已关闭');
256
+ }
257
+ });
258
+ socket.addEventListener('error', () => {
259
+ terminalContext.connected = false;
260
+ setTerminalStatus('error', '终端连接失败');
261
+ });
262
+ }
263
+
264
+ function resizeProjectTerminal() {
265
+ if (!terminalContext.terminal || !terminalContext.fitAddon) return;
266
+ clearTimeout(terminalContext.resizeTimer);
267
+ terminalContext.resizeTimer = setTimeout(() => {
268
+ try {
269
+ terminalContext.fitAddon.fit();
270
+ const socket = terminalContext.socket;
271
+ if (socket && socket.readyState === WebSocket.OPEN) {
272
+ socket.send(JSON.stringify({
273
+ type: 'resize',
274
+ cols: terminalContext.terminal.cols,
275
+ rows: terminalContext.terminal.rows,
276
+ }));
277
+ }
278
+ } catch { }
279
+ }, 30);
280
+ }
281
+
282
+ function disconnectProjectTerminal() {
283
+ const socket = terminalContext.socket;
284
+ terminalContext.socket = null;
285
+ terminalContext.connected = false;
286
+ if (socket && socket.readyState !== WebSocket.CLOSED) {
287
+ socket.close();
288
+ }
289
+ if (terminalContext.terminal) {
290
+ terminalContext.terminal.dispose();
291
+ }
292
+ terminalContext.terminal = null;
293
+ terminalContext.fitAddon = null;
294
+ terminalContext.projectName = '';
295
+ terminalContext.lastState = 'closed';
296
+ clearTimeout(terminalContext.resizeTimer);
297
+ setTerminalStatus('closed', '终端连接已关闭');
298
+ }