Skip to content

Commit db1318e

Browse files
committed
fix(agent): 修复令牌计数使用 totalTokens 而不是计算值
fix(command): 中止现有控制器时添加中断原因 style(theme): 调整警告颜色以提高对比度 feat(cli): 支持逗号分隔的工具列表参数 feat(ui): 添加任务中断提示和内容保留逻辑 feat(shutdown): 实现双击 SIGINT 退出功能 fix(ui): 正确处理恢复会话中的 ContentPart 数组 fix(ai): 支持更多令牌计数字段并修复状态检查
1 parent 5281f6f commit db1318e

8 files changed

Lines changed: 89 additions & 27 deletions

File tree

packages/cli/src/agent/loop/executeLoopGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ export async function* executeLoopGenerator(
623623
usage: {
624624
inputTokens: turnResult.usage.promptTokens ?? 0,
625625
outputTokens: turnResult.usage.completionTokens ?? 0,
626-
totalTokens,
626+
totalTokens: turnResult.usage.totalTokens ?? 0,
627627
maxContextTokens: deps.currentModelMaxContextTokens,
628628
},
629629
};

packages/cli/src/cli/middleware.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export const validatePermissions: MiddlewareFunction = (argv) => {
2727
argv.permissionMode = 'yolo';
2828
}
2929

30+
// 处理逗号分隔的工具列表
31+
if (Array.isArray(argv.allowedTools)) {
32+
argv.allowedTools = argv.allowedTools.flatMap((tool: string) =>
33+
tool.split(',').map((t) => t.trim()).filter((t) => t.length > 0)
34+
);
35+
}
36+
if (Array.isArray(argv.disallowedTools)) {
37+
argv.disallowedTools = argv.disallowedTools.flatMap((tool: string) =>
38+
tool.split(',').map((t) => t.trim()).filter((t) => t.length > 0)
39+
);
40+
}
41+
3042
// 验证工具列表冲突
3143
if (Array.isArray(argv.allowedTools) && Array.isArray(argv.disallowedTools)) {
3244
const intersection = argv.allowedTools.filter((tool: string) =>

packages/cli/src/services/GracefulShutdown.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class GracefulShutdownManager {
8383
private cleanupHandlers: CleanupHandler[] = [];
8484
private isShuttingDown = false;
8585
private initialized = false;
86+
private lastSigintTime = 0; // 记录上次 SIGINT 的时间戳
87+
private readonly SIGINT_DOUBLE_CLICK_WINDOW = 3000; // 3 秒内的第二次 SIGINT 才退出
8688

8789
private constructor() {}
8890

@@ -120,14 +122,23 @@ class GracefulShutdownManager {
120122
this.shutdown('SIGTERM', 0);
121123
});
122124

123-
// 注意:SIGINT 由 useCtrlCHandler 处理,这里不重复处理
124-
// 但如果是非 UI 模式(如 print 模式),需要处理 SIGINT
125-
if (process.env.BLADE_NON_INTERACTIVE === 'true') {
126-
process.on('SIGINT', () => {
127-
logger.info('[GracefulShutdown] 收到 SIGINT 信号(非交互模式)');
125+
// 处理 SIGINT(Ctrl+C 或 kill -2)
126+
// 在交互模式下实现双击退出逻辑,与键盘 Ctrl+C 行为一致
127+
process.on('SIGINT', () => {
128+
const now = Date.now();
129+
const isDoubleClick = now - this.lastSigintTime < this.SIGINT_DOUBLE_CLICK_WINDOW;
130+
131+
if (isDoubleClick) {
132+
// 第二次 SIGINT,执行退出
133+
logger.info('[GracefulShutdown] 收到第二次 SIGINT 信号,执行退出');
128134
this.shutdown('SIGINT', 0);
129-
});
130-
}
135+
} else {
136+
// 第一次 SIGINT,记录时间并提示
137+
logger.info('[GracefulShutdown] 收到第一次 SIGINT 信号,再按一次退出');
138+
this.lastSigintTime = now;
139+
console.log('\n再按一次 Ctrl+C 退出\n');
140+
}
141+
});
131142

132143
this.initialized = true;
133144
logger.debug('[GracefulShutdown] 全局错误处理器已初始化');

packages/cli/src/services/VercelAIChatService.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ export class VercelAIChatService implements IChatService {
306306
promptTokens?: number;
307307
completionTokens?: number;
308308
totalTokens?: number;
309+
inputTokens?: number;
310+
outputTokens?: number;
309311
},
310312
providerMetadata?: {
311313
anthropic?: {
@@ -315,8 +317,9 @@ export class VercelAIChatService implements IChatService {
315317
}
316318
): UsageInfo | undefined {
317319
if (!usage) return undefined;
318-
const prompt = usage.promptTokens ?? 0;
319-
const completion = usage.completionTokens ?? 0;
320+
// Vercel AI SDK 可能返回 inputTokens/outputTokens 或 promptTokens/completionTokens
321+
const prompt = usage.promptTokens ?? usage.inputTokens ?? 0;
322+
const completion = usage.completionTokens ?? usage.outputTokens ?? 0;
320323
const result: UsageInfo = {
321324
promptTokens: prompt,
322325
completionTokens: completion,
@@ -342,7 +345,7 @@ export class VercelAIChatService implements IChatService {
342345
const status = statusMatch
343346
? parseInt(statusMatch[1], 10)
344347
: (error as Error & { status?: number }).status;
345-
return [429, 529, 503].includes(status);
348+
return status !== undefined && [429, 529, 503].includes(status);
346349
}
347350

348351
async chat(
@@ -495,7 +498,7 @@ export class VercelAIChatService implements IChatService {
495498
yield {
496499
finishReason: (part as { finishReason?: string }).finishReason,
497500
usage: this.convertUsage(
498-
(part as { totalUsage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number } }).totalUsage,
501+
(part as { totalUsage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number; inputTokens?: number; outputTokens?: number } }).totalUsage,
499502
(part as { providerMetadata?: { anthropic?: { cacheCreationInputTokens?: number; cacheReadInputTokens?: number } } }).providerMetadata
500503
),
501504
};

packages/cli/src/store/slices/commandSlice.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ export const createCommandSlice: StateCreator<BladeStore, [], [], CommandSlice>
5252
*/
5353
createAbortController: () => {
5454
const existing = get().command.abortController;
55-
// 如果已有未被中止的 controller,直接返回
55+
// 如果已有未被中止的 controller,先中止它(用户提交新消息时中断旧任务)
5656
if (existing && !existing.signal.aborted) {
57-
return existing;
57+
existing.abort('interrupted-by-new-command');
5858
}
5959
// 创建新的 controller
6060
const controller = new AbortController();

packages/cli/src/ui/components/BladeInterface.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -392,17 +392,33 @@ export const BladeInterface: React.FC<BladeInterfaceProps> = ({
392392
try {
393393
const messages = await SessionService.loadSession(sessionId);
394394

395-
const sessionMessages = messages.map((msg, index) => ({
396-
id: `restored-${Date.now()}-${index}`,
397-
role: msg.role,
398-
content:
399-
typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
400-
timestamp: Date.now() - (messages.length - index) * 1000,
401-
metadata:
402-
msg.metadata && typeof msg.metadata === 'object'
403-
? (msg.metadata as Record<string, unknown>)
404-
: undefined,
405-
}));
395+
const sessionMessages = messages.map((msg, index) => {
396+
// 提取消息内容:如果是 ContentPart[] 数组,只提取文本部分
397+
let content: string;
398+
if (typeof msg.content === 'string') {
399+
content = msg.content;
400+
} else if (Array.isArray(msg.content)) {
401+
// 从 ContentPart[] 中提取文本
402+
content = msg.content
403+
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
404+
.map((part) => part.text)
405+
.join('');
406+
} else {
407+
// 其他情况(不应该发生)
408+
content = '';
409+
}
410+
411+
return {
412+
id: `restored-${Date.now()}-${index}`,
413+
role: msg.role,
414+
content,
415+
timestamp: Date.now() - (messages.length - index) * 1000,
416+
metadata:
417+
msg.metadata && typeof msg.metadata === 'object'
418+
? (msg.metadata as Record<string, unknown>)
419+
: undefined,
420+
};
421+
});
406422

407423
sessionActions.restoreSession(sessionId, sessionMessages);
408424
appActions.closeModal();

packages/cli/src/ui/hooks/useCommandHandler.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,28 @@ export const useCommandHandler = (
143143
// ensureStoreInitialized 是唯一的初始化点,必须在 processSlashCommand 前调用
144144
await ensureStoreInitialized();
145145

146+
// 检查是否有正在运行的任务
147+
const wasProcessing = isProcessing;
148+
146149
const abortController = commandActions.createAbortController();
147150

151+
// 如果之前有任务在运行,显示中断消息
152+
if (wasProcessing) {
153+
// drain 缓冲区,保留已接收内容
154+
const { extraContent, extraThinking } = streamingBuffer.drainPendingBuffers();
155+
156+
// 用 drain 结果 finalize,确保已收内容提交到 store
157+
const streamingId = getState().session.currentStreamingMessageId;
158+
if (streamingId) {
159+
if (extraContent) appendMarkdownDelta(streamingId, extraContent);
160+
finalizeMarkdownCache(streamingId);
161+
}
162+
sessionActions.finalizeStreamingMessage(extraContent, extraThinking);
163+
164+
// 显示中断消息
165+
sessionActions.addAssistantMessage('上一条消息已中断');
166+
}
167+
148168
const slashResult = await processSlashCommand(
149169
resolved,
150170
appActions,

packages/cli/src/ui/themes/presets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ const solarizedLight: Theme = {
338338
secondary: '#2aa198',
339339
accent: '#d33682',
340340
success: '#859900',
341-
warning: '#b58900',
341+
warning: '#cb4b16', // 从 #b58900 改为更深的橙色,提高对比度
342342
error: '#dc322f',
343343
info: '#268bd2',
344344
light: '#fdf6e3',
@@ -583,7 +583,7 @@ const github: Theme = {
583583
secondary: '#8250df',
584584
accent: '#bc4c00',
585585
success: '#1a7f37',
586-
warning: '#9a6700',
586+
warning: '#bc4c00', // 从 #9a6700 改为更深的橙色,提高对比度
587587
error: '#d1242f',
588588
info: '#0969da',
589589
light: '#f6f8fa',

0 commit comments

Comments
 (0)