@yeaft/webchat-agent 0.1.69 → 0.1.73
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/claude.js +42 -1
- package/cli.js +15 -3
- package/connection/upgrade.js +2 -3
- package/conversation.js +28 -0
- package/crew/persistence.js +2 -15
- package/crew/role-output.js +8 -61
- package/crew/routing.js +36 -2
- package/crew/session.js +54 -15
- package/index.js +5 -1
- package/package.json +1 -1
package/claude.js
CHANGED
|
@@ -408,7 +408,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
408
408
|
|
|
409
409
|
// 计算上下文使用百分比
|
|
410
410
|
const inputTokens = message.usage?.input_tokens || 0;
|
|
411
|
-
const maxContextTokens = 128000;
|
|
411
|
+
const maxContextTokens = ctx.CONFIG?.maxContextTokens || 128000;
|
|
412
412
|
if (inputTokens > 0) {
|
|
413
413
|
ctx.sendToServer({
|
|
414
414
|
type: 'context_usage',
|
|
@@ -497,6 +497,35 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
497
497
|
|
|
498
498
|
console.log(`[RolePlay] Auto-continuing to role: ${to}`);
|
|
499
499
|
|
|
500
|
+
// ★ Pre-send compact check for RolePlay auto-continue
|
|
501
|
+
const rpAutoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
502
|
+
const rpEstimatedNewTokens = Math.ceil(prompt.length / 3);
|
|
503
|
+
const rpEstimatedTotal = inputTokens + rpEstimatedNewTokens;
|
|
504
|
+
|
|
505
|
+
if (rpEstimatedTotal > rpAutoCompactThreshold) {
|
|
506
|
+
console.log(`[RolePlay] Pre-send compact: estimated ${rpEstimatedTotal} tokens (last: ${inputTokens} + new: ~${rpEstimatedNewTokens}) exceeds threshold ${rpAutoCompactThreshold}`);
|
|
507
|
+
ctx.sendToServer({
|
|
508
|
+
type: 'compact_status',
|
|
509
|
+
conversationId,
|
|
510
|
+
status: 'compacting',
|
|
511
|
+
message: `Auto-compacting before RolePlay continue: estimated ${rpEstimatedTotal} tokens (threshold: ${rpAutoCompactThreshold})`
|
|
512
|
+
});
|
|
513
|
+
// Store pending message and compact first
|
|
514
|
+
const userMessage = {
|
|
515
|
+
type: 'user',
|
|
516
|
+
message: { role: 'user', content: prompt }
|
|
517
|
+
};
|
|
518
|
+
state._pendingUserMessage = userMessage;
|
|
519
|
+
state.turnActive = true;
|
|
520
|
+
state.turnResultReceived = false;
|
|
521
|
+
state.inputStream.enqueue({
|
|
522
|
+
type: 'user',
|
|
523
|
+
message: { role: 'user', content: '/compact' }
|
|
524
|
+
});
|
|
525
|
+
sendConversationList();
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
|
|
500
529
|
// Re-activate the turn
|
|
501
530
|
state.turnActive = true;
|
|
502
531
|
state.turnResultReceived = false;
|
|
@@ -531,6 +560,18 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
531
560
|
workDir: state.workDir
|
|
532
561
|
});
|
|
533
562
|
sendConversationList();
|
|
563
|
+
|
|
564
|
+
// ★ Send pending user message after compact completes
|
|
565
|
+
if (state._pendingUserMessage && state.inputStream) {
|
|
566
|
+
const pendingMsg = state._pendingUserMessage;
|
|
567
|
+
state._pendingUserMessage = null;
|
|
568
|
+
console.log(`[${conversationId}] Sending pending message after compact`);
|
|
569
|
+
state.turnActive = true;
|
|
570
|
+
state.turnResultReceived = false;
|
|
571
|
+
sendOutput(conversationId, pendingMsg);
|
|
572
|
+
state.inputStream.enqueue(pendingMsg);
|
|
573
|
+
sendConversationList();
|
|
574
|
+
}
|
|
534
575
|
continue;
|
|
535
576
|
}
|
|
536
577
|
|
package/cli.js
CHANGED
|
@@ -152,6 +152,19 @@ function upgrade() {
|
|
|
152
152
|
} else {
|
|
153
153
|
execSync(`npm install -g ${pkg.name}@latest`, { stdio: 'inherit' });
|
|
154
154
|
console.log(`Successfully upgraded to ${latest}`);
|
|
155
|
+
|
|
156
|
+
// If PM2 is managing yeaft-agent, restart it so the new version takes effect
|
|
157
|
+
try {
|
|
158
|
+
const pm2List = execSync('pm2 jlist', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
159
|
+
const apps = JSON.parse(pm2List);
|
|
160
|
+
if (Array.isArray(apps) && apps.some(app => app.name === 'yeaft-agent')) {
|
|
161
|
+
console.log('Restarting yeaft-agent via PM2...');
|
|
162
|
+
execSync('pm2 restart yeaft-agent', { stdio: 'inherit' });
|
|
163
|
+
console.log('PM2 service restarted.');
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// PM2 not installed or not managing yeaft-agent — nothing to do
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
169
|
} catch (e) {
|
|
157
170
|
console.error('Upgrade failed:', e.message);
|
|
@@ -185,11 +198,11 @@ function upgradeWindows(latestVersion) {
|
|
|
185
198
|
'echo [Upgrade] Waiting for CLI process (PID %PID%) to exit... >> "%LOGFILE%"',
|
|
186
199
|
'',
|
|
187
200
|
':WAIT_LOOP',
|
|
188
|
-
'tasklist /FI "PID eq %PID%" 2>NUL | findstr /
|
|
201
|
+
'tasklist /FI "PID eq %PID%" /NH 2>NUL | findstr /C:"%PID%" >NUL',
|
|
189
202
|
'if errorlevel 1 goto PID_EXITED',
|
|
190
203
|
'set /A COUNT+=1',
|
|
191
204
|
'if %COUNT% GEQ %MAX_WAIT% (',
|
|
192
|
-
' echo [Upgrade] Timeout waiting for PID %PID% to exit >> "%LOGFILE%"',
|
|
205
|
+
' echo [Upgrade] Timeout waiting for PID %PID% to exit after %MAX_WAIT% iterations >> "%LOGFILE%"',
|
|
193
206
|
' goto PID_EXITED',
|
|
194
207
|
')',
|
|
195
208
|
'ping -n 3 127.0.0.1 >NUL',
|
|
@@ -210,7 +223,6 @@ function upgradeWindows(latestVersion) {
|
|
|
210
223
|
|
|
211
224
|
writeFileSync(batPath, batLines.join('\r\n'));
|
|
212
225
|
const child = spawn('cmd.exe', ['/c', batPath], {
|
|
213
|
-
detached: true,
|
|
214
226
|
stdio: 'ignore',
|
|
215
227
|
windowsHide: true,
|
|
216
228
|
});
|
package/connection/upgrade.js
CHANGED
|
@@ -152,11 +152,11 @@ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestV
|
|
|
152
152
|
// Wait for old process to exit (PM2 already deleted before exit, so no auto-restart race)
|
|
153
153
|
batLines.push(
|
|
154
154
|
':WAIT_LOOP',
|
|
155
|
-
'tasklist /FI "PID eq %PID%" 2>NUL | findstr /
|
|
155
|
+
'tasklist /FI "PID eq %PID%" /NH 2>NUL | findstr /C:"%PID%" >NUL',
|
|
156
156
|
'if errorlevel 1 goto PID_EXITED',
|
|
157
157
|
'set /A COUNT+=1',
|
|
158
158
|
'if %COUNT% GEQ %MAX_WAIT% (',
|
|
159
|
-
' echo [Upgrade] Timeout waiting for PID %PID% to exit after
|
|
159
|
+
' echo [Upgrade] Timeout waiting for PID %PID% to exit after %MAX_WAIT% iterations >> "%LOGFILE%"',
|
|
160
160
|
' goto PID_EXITED',
|
|
161
161
|
')',
|
|
162
162
|
'ping -n 3 127.0.0.1 >NUL',
|
|
@@ -201,7 +201,6 @@ function spawnWindowsUpgradeScript(pkgName, installDir, isGlobalInstall, latestV
|
|
|
201
201
|
|
|
202
202
|
writeFileSync(batPath, batLines.join('\r\n'));
|
|
203
203
|
const child = spawn('cmd.exe', ['/c', batPath], {
|
|
204
|
-
detached: true,
|
|
205
204
|
stdio: 'ignore',
|
|
206
205
|
windowsHide: true
|
|
207
206
|
});
|
package/conversation.js
CHANGED
|
@@ -515,6 +515,34 @@ export async function handleUserInput(msg) {
|
|
|
515
515
|
};
|
|
516
516
|
|
|
517
517
|
console.log(`[${conversationId}] Sending: ${prompt.substring(0, 100)}...`);
|
|
518
|
+
|
|
519
|
+
// ★ Pre-send compact check: estimate total tokens and compact before sending if needed
|
|
520
|
+
const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
521
|
+
const lastInputTokens = state.lastResultInputTokens || 0;
|
|
522
|
+
const estimatedNewTokens = Math.ceil(effectivePrompt.length / 3); // conservative: ~3 chars per token
|
|
523
|
+
const estimatedTotal = lastInputTokens + estimatedNewTokens;
|
|
524
|
+
|
|
525
|
+
if (estimatedTotal > autoCompactThreshold && state.inputStream) {
|
|
526
|
+
console.log(`[${conversationId}] Pre-send compact: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
|
|
527
|
+
ctx.sendToServer({
|
|
528
|
+
type: 'compact_status',
|
|
529
|
+
conversationId,
|
|
530
|
+
status: 'compacting',
|
|
531
|
+
message: `Auto-compacting before send: estimated ${estimatedTotal} tokens (threshold: ${autoCompactThreshold})`
|
|
532
|
+
});
|
|
533
|
+
// Send /compact first, then the user message will be sent after compact completes
|
|
534
|
+
// by storing it as a pending message
|
|
535
|
+
state._pendingUserMessage = userMessage;
|
|
536
|
+
state.turnActive = true;
|
|
537
|
+
state.turnResultReceived = false;
|
|
538
|
+
sendConversationList();
|
|
539
|
+
state.inputStream.enqueue({
|
|
540
|
+
type: 'user',
|
|
541
|
+
message: { role: 'user', content: '/compact' }
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
518
546
|
state.turnActive = true;
|
|
519
547
|
state.turnResultReceived = false; // 重置 per-turn 去重标志
|
|
520
548
|
sendConversationList(); // 在 turnActive=true 后通知 server,确保 processing 状态正确
|
package/crew/persistence.js
CHANGED
|
@@ -62,7 +62,6 @@ export async function removeFromCrewIndex(sessionId) {
|
|
|
62
62
|
const { crewSessions } = await import('./session.js');
|
|
63
63
|
|
|
64
64
|
const index = await loadCrewIndex();
|
|
65
|
-
const entry = index.find(e => e.sessionId === sessionId);
|
|
66
65
|
const filtered = index.filter(e => e.sessionId !== sessionId);
|
|
67
66
|
if (filtered.length !== index.length) {
|
|
68
67
|
await saveCrewIndex(filtered);
|
|
@@ -73,20 +72,8 @@ export async function removeFromCrewIndex(sessionId) {
|
|
|
73
72
|
crewSessions.delete(sessionId);
|
|
74
73
|
console.log(`[Crew] Removed session ${sessionId} from active sessions`);
|
|
75
74
|
}
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
if (sharedDir) {
|
|
79
|
-
try {
|
|
80
|
-
for (const file of ['session.json', 'messages.json']) {
|
|
81
|
-
await fs.unlink(join(sharedDir, file)).catch(() => {});
|
|
82
|
-
}
|
|
83
|
-
// Clean up message shard files
|
|
84
|
-
await cleanupMessageShards(sharedDir);
|
|
85
|
-
console.log(`[Crew] Cleaned session files in ${sharedDir}`);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.warn(`[Crew] Failed to clean session files:`, e.message);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
75
|
+
// 注意:不再删除磁盘上的 session.json、messages.json 等文件
|
|
76
|
+
// 这些文件在 recreate 时会被复用(合并统计数据 + 恢复消息历史)
|
|
90
77
|
}
|
|
91
78
|
|
|
92
79
|
// =====================================================================
|
package/crew/role-output.js
CHANGED
|
@@ -6,10 +6,10 @@ import { sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming } f
|
|
|
6
6
|
import { saveRoleSessionId, clearRoleSessionId, classifyRoleError, createRoleQuery } from './role-query.js';
|
|
7
7
|
import { parseRoutes, executeRoute, dispatchToRole } from './routing.js';
|
|
8
8
|
import { parseCompletedTasks, updateFeatureIndex, appendChangelog, saveRoleWorkSummary, updateKanban } from './task-files.js';
|
|
9
|
+
import ctx from '../context.js';
|
|
9
10
|
|
|
10
|
-
// Context
|
|
11
|
-
const
|
|
12
|
-
const CLEAR_THRESHOLD = 0.85; // 85% → 直接 clear + rebuild(不再走 compact)
|
|
11
|
+
// Context 使用率常量(运行时从 ctx.CONFIG 读取)
|
|
12
|
+
const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 处理角色的流式输出
|
|
@@ -99,14 +99,11 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
99
99
|
sessionId: session.id,
|
|
100
100
|
role: roleName,
|
|
101
101
|
inputTokens,
|
|
102
|
-
maxTokens:
|
|
103
|
-
percentage: Math.min(100, Math.round((inputTokens /
|
|
102
|
+
maxTokens: getMaxContext(),
|
|
103
|
+
percentage: Math.min(100, Math.round((inputTokens / getMaxContext()) * 100))
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const contextPercentage = inputTokens / MAX_CONTEXT;
|
|
108
|
-
const needClear = contextPercentage >= CLEAR_THRESHOLD;
|
|
109
|
-
|
|
110
107
|
// 解析路由
|
|
111
108
|
const routes = parseRoutes(roleState.accumulatedText);
|
|
112
109
|
|
|
@@ -136,8 +133,8 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
136
133
|
}
|
|
137
134
|
}
|
|
138
135
|
|
|
139
|
-
//
|
|
140
|
-
|
|
136
|
+
// 保存本 turn 文本(供 routing.js 预检时 saveRoleWorkSummary 使用)
|
|
137
|
+
roleState.lastTurnText = roleState.accumulatedText;
|
|
141
138
|
roleState.accumulatedText = '';
|
|
142
139
|
roleState.turnActive = false;
|
|
143
140
|
|
|
@@ -149,57 +146,7 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
149
146
|
|
|
150
147
|
sendStatusUpdate(session);
|
|
151
148
|
|
|
152
|
-
//
|
|
153
|
-
if (needClear) {
|
|
154
|
-
console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, clearing and rebuilding`);
|
|
155
|
-
|
|
156
|
-
// 保存工作摘要到 feature 文件
|
|
157
|
-
await saveRoleWorkSummary(session, roleName, turnText).catch(e =>
|
|
158
|
-
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
159
|
-
|
|
160
|
-
// Clear 角色
|
|
161
|
-
await clearRoleSessionId(session.sharedDir, roleName);
|
|
162
|
-
roleState.claudeSessionId = null;
|
|
163
|
-
|
|
164
|
-
if (roleState.abortController) roleState.abortController.abort();
|
|
165
|
-
roleState.query = null;
|
|
166
|
-
roleState.inputStream = null;
|
|
167
|
-
|
|
168
|
-
sendCrewMessage({
|
|
169
|
-
type: 'crew_role_cleared',
|
|
170
|
-
sessionId: session.id,
|
|
171
|
-
role: roleName,
|
|
172
|
-
contextPercentage: Math.round(contextPercentage * 100),
|
|
173
|
-
reason: 'context_limit'
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// 继承 task 到路由(如有)
|
|
177
|
-
const currentTask = roleState.currentTask;
|
|
178
|
-
if (routes.length > 0) {
|
|
179
|
-
for (const route of routes) {
|
|
180
|
-
if (!route.taskId && currentTask) {
|
|
181
|
-
route.taskId = currentTask.taskId;
|
|
182
|
-
route.taskTitle = currentTask.taskTitle;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 执行路由
|
|
188
|
-
if (routes.length > 0) {
|
|
189
|
-
session.round++;
|
|
190
|
-
const results = await Promise.allSettled(routes.map(route =>
|
|
191
|
-
executeRoute(session, roleName, route)
|
|
192
|
-
));
|
|
193
|
-
for (const r of results) {
|
|
194
|
-
if (r.status === 'rejected') {
|
|
195
|
-
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return; // query 已清空,退出
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 执行路由(无需 clear 时)
|
|
149
|
+
// 执行路由
|
|
203
150
|
if (routes.length > 0) {
|
|
204
151
|
session.round++;
|
|
205
152
|
|
package/crew/routing.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
|
|
7
|
-
import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban } from './task-files.js';
|
|
8
|
-
import { createRoleQuery } from './role-query.js';
|
|
7
|
+
import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban, saveRoleWorkSummary } from './task-files.js';
|
|
8
|
+
import { createRoleQuery, clearRoleSessionId } from './role-query.js';
|
|
9
|
+
import ctx from '../context.js';
|
|
9
10
|
|
|
10
11
|
/** Format role label */
|
|
11
12
|
function roleLabel(r) {
|
|
@@ -193,6 +194,39 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
193
194
|
timestamp: Date.now()
|
|
194
195
|
});
|
|
195
196
|
|
|
197
|
+
// ★ Pre-send compact check: estimate total tokens and clear+rebuild if needed
|
|
198
|
+
const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
199
|
+
const lastInputTokens = roleState.lastInputTokens || 0;
|
|
200
|
+
const estimatedNewTokens = Math.ceil((typeof content === 'string' ? content.length : 0) / 3);
|
|
201
|
+
const estimatedTotal = lastInputTokens + estimatedNewTokens;
|
|
202
|
+
|
|
203
|
+
if (lastInputTokens > 0 && estimatedTotal > autoCompactThreshold) {
|
|
204
|
+
console.log(`[Crew] Pre-send compact for ${roleName}: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
|
|
205
|
+
|
|
206
|
+
// Save work summary before clearing (use lastTurnText since accumulatedText is cleared after result)
|
|
207
|
+
await saveRoleWorkSummary(session, roleName, roleState.lastTurnText || roleState.accumulatedText || '').catch(e =>
|
|
208
|
+
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
209
|
+
|
|
210
|
+
// Clear role session and rebuild
|
|
211
|
+
await clearRoleSessionId(session.sharedDir, roleName);
|
|
212
|
+
roleState.claudeSessionId = null;
|
|
213
|
+
|
|
214
|
+
if (roleState.abortController) roleState.abortController.abort();
|
|
215
|
+
roleState.query = null;
|
|
216
|
+
roleState.inputStream = null;
|
|
217
|
+
|
|
218
|
+
sendCrewMessage({
|
|
219
|
+
type: 'crew_role_cleared',
|
|
220
|
+
sessionId: session.id,
|
|
221
|
+
role: roleName,
|
|
222
|
+
contextPercentage: Math.round((lastInputTokens / (ctx.CONFIG?.maxContextTokens || 128000)) * 100),
|
|
223
|
+
reason: 'pre_send_compact'
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Recreate the query (fresh Claude process)
|
|
227
|
+
roleState = await createRoleQuery(session, roleName);
|
|
228
|
+
}
|
|
229
|
+
|
|
196
230
|
// 发送
|
|
197
231
|
roleState.lastDispatchContent = content;
|
|
198
232
|
roleState.lastDispatchFrom = fromSource;
|
package/crew/session.js
CHANGED
|
@@ -147,6 +147,9 @@ export async function createCrewSession(msg) {
|
|
|
147
147
|
: join(projectDir, sharedDirRel || '.crew');
|
|
148
148
|
const decisionMaker = roles.find(r => r.isDecisionMaker)?.name || roles[0]?.name || null;
|
|
149
149
|
|
|
150
|
+
// 尝试读取旧 session.json,合并统计数据(deleteCrewDir 保留了该文件)
|
|
151
|
+
const oldMeta = await loadSessionMeta(sharedDir);
|
|
152
|
+
|
|
150
153
|
const session = {
|
|
151
154
|
id: sessionId,
|
|
152
155
|
projectDir,
|
|
@@ -156,28 +159,41 @@ export async function createCrewSession(msg) {
|
|
|
156
159
|
roleStates: new Map(),
|
|
157
160
|
decisionMaker,
|
|
158
161
|
status: 'initializing',
|
|
159
|
-
round: 0,
|
|
160
|
-
costUsd: 0,
|
|
161
|
-
totalInputTokens: 0,
|
|
162
|
-
totalOutputTokens: 0,
|
|
162
|
+
round: oldMeta?.round || 0,
|
|
163
|
+
costUsd: oldMeta?.costUsd || 0,
|
|
164
|
+
totalInputTokens: oldMeta?.totalInputTokens || 0,
|
|
165
|
+
totalOutputTokens: oldMeta?.totalOutputTokens || 0,
|
|
163
166
|
messageHistory: [],
|
|
164
167
|
uiMessages: [],
|
|
165
168
|
humanMessageQueue: [],
|
|
166
169
|
waitingHumanContext: null,
|
|
167
170
|
pendingRoutes: [],
|
|
168
|
-
features: new Map(),
|
|
169
|
-
_completedTaskIds: new Set(),
|
|
171
|
+
features: new Map((oldMeta?.features || []).map(f => [f.taskId, f])),
|
|
172
|
+
_completedTaskIds: new Set(oldMeta?._completedTaskIds || []),
|
|
170
173
|
initProgress: null,
|
|
171
174
|
userId,
|
|
172
175
|
username,
|
|
173
176
|
agentId: ctx.CONFIG?.agentName || null,
|
|
174
177
|
teamType,
|
|
175
178
|
language,
|
|
176
|
-
createdAt: Date.now()
|
|
179
|
+
createdAt: oldMeta?.createdAt || Date.now()
|
|
177
180
|
};
|
|
178
181
|
|
|
182
|
+
if (oldMeta) {
|
|
183
|
+
console.log(`[Crew] Merged stats from previous session: round=${session.round}, cost=$${session.costUsd.toFixed(4)}, inputTokens=${session.totalInputTokens}, outputTokens=${session.totalOutputTokens}`);
|
|
184
|
+
// 恢复旧消息历史(deleteCrewDir 保留了 messages*.json)
|
|
185
|
+
const loaded = await loadSessionMessages(sharedDir);
|
|
186
|
+
if (loaded.messages.length > 0) {
|
|
187
|
+
session.uiMessages = loaded.messages;
|
|
188
|
+
console.log(`[Crew] Restored ${loaded.messages.length} messages from previous session`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
179
192
|
crewSessions.set(sessionId, session);
|
|
180
193
|
|
|
194
|
+
// 如果有旧消息,检查是否有更早的分片
|
|
195
|
+
const hasOlderMessages = oldMeta ? await getMaxShardIndex(sharedDir) > 0 : false;
|
|
196
|
+
|
|
181
197
|
sendCrewMessage({
|
|
182
198
|
type: 'crew_session_created',
|
|
183
199
|
sessionId,
|
|
@@ -196,7 +212,10 @@ export async function createCrewSession(msg) {
|
|
|
196
212
|
})),
|
|
197
213
|
decisionMaker,
|
|
198
214
|
userId,
|
|
199
|
-
username
|
|
215
|
+
username,
|
|
216
|
+
// 旧消息(recreate 时保留的历史)
|
|
217
|
+
uiMessages: session.uiMessages.length > 0 ? session.uiMessages : undefined,
|
|
218
|
+
hasOlderMessages: hasOlderMessages || undefined
|
|
200
219
|
});
|
|
201
220
|
|
|
202
221
|
sendStatusUpdate(session);
|
|
@@ -335,19 +354,39 @@ export async function handleCheckCrewExists(msg) {
|
|
|
335
354
|
}
|
|
336
355
|
|
|
337
356
|
/**
|
|
338
|
-
*
|
|
357
|
+
* 删除 Crew 定义文件(模板/角色配置),保留所有用户数据和工作产出
|
|
358
|
+
*
|
|
359
|
+
* 删除: CLAUDE.md(共享模板)、roles/(角色模板)
|
|
360
|
+
* 清空: sessions/ 下的文件(旧角色的 Claude Code session IDs,已失效)
|
|
361
|
+
* 清除: crew-index 中的旧 entry(防止 createCrewSession 走 resume 而非 create)
|
|
362
|
+
* 保留: context/、session.json、messages*.json 及任何其他生成文件(截图、设计文档等)
|
|
339
363
|
*/
|
|
340
364
|
export async function handleDeleteCrewDir(msg) {
|
|
341
365
|
const { projectDir } = msg;
|
|
342
366
|
if (!isValidProjectDir(projectDir)) return;
|
|
343
367
|
const crewDir = join(projectDir, '.crew');
|
|
344
368
|
try {
|
|
345
|
-
|
|
346
|
-
await
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
);
|
|
369
|
+
// 删除 Crew 模板定义
|
|
370
|
+
await fs.rm(join(crewDir, 'CLAUDE.md'), { force: true }).catch(() => {});
|
|
371
|
+
await fs.rm(join(crewDir, 'roles'), { recursive: true, force: true }).catch(() => {});
|
|
372
|
+
|
|
373
|
+
// 清空 sessions/ 内容(旧角色的 session IDs 已失效),保留目录本身
|
|
374
|
+
const sessionsDir = join(crewDir, 'sessions');
|
|
375
|
+
try {
|
|
376
|
+
const sessionFiles = await fs.readdir(sessionsDir);
|
|
377
|
+
await Promise.all(
|
|
378
|
+
sessionFiles.map(f => fs.rm(join(sessionsDir, f), { recursive: true, force: true }).catch(() => {}))
|
|
379
|
+
);
|
|
380
|
+
} catch { /* sessions/ may not exist */ }
|
|
381
|
+
|
|
382
|
+
// 清除 crew-index 中的旧 entry(不删文件),确保新建时走 create → loadSessionMeta 合并统计
|
|
383
|
+
const normalizedDir = projectDir.replace(/\/+$/, '');
|
|
384
|
+
const index = await loadCrewIndex();
|
|
385
|
+
const match = index.find(e => e.projectDir.replace(/\/+$/, '') === normalizedDir);
|
|
386
|
+
if (match) {
|
|
387
|
+
await removeFromCrewIndex(match.sessionId);
|
|
388
|
+
console.log(`[Crew] Cleared index entry for ${projectDir} (sessionId: ${match.sessionId})`);
|
|
389
|
+
}
|
|
351
390
|
} catch {}
|
|
352
391
|
}
|
|
353
392
|
|
package/index.js
CHANGED
|
@@ -74,7 +74,11 @@ const CONFIG = {
|
|
|
74
74
|
return raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
75
75
|
})(),
|
|
76
76
|
// disallowedTools 会在 loadMcpServers() 中计算
|
|
77
|
-
disallowedTools: []
|
|
77
|
+
disallowedTools: [],
|
|
78
|
+
// 最大上下文 tokens(用于百分比计算的分母)
|
|
79
|
+
maxContextTokens: parseInt(process.env.MAX_CONTEXT_TOKENS || fileConfig.maxContextTokens, 10) || 128000,
|
|
80
|
+
// Auto-compact 阈值(tokens):context 超过此值时自动触发 compact
|
|
81
|
+
autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 110000
|
|
78
82
|
};
|
|
79
83
|
|
|
80
84
|
// 初始化共享上下文
|