Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .blade/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"permissions": {
"allow": [
"TestTool"
"TestTool",
"Bash(git status)",
"Bash(git diff *)"
],
"ask": [],
"deny": []
Expand Down
16 changes: 9 additions & 7 deletions src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export class Agent {
baseUrl: modelConfig.baseUrl,
temperature: modelConfig.temperature ?? this.config.temperature,
maxContextTokens: modelConfig.maxContextTokens ?? this.config.maxContextTokens, // 上下文窗口(压缩判断)
maxOutputTokens: this.config.maxOutputTokens, // 输出限制(API max_tokens)
maxOutputTokens: modelConfig.maxOutputTokens ?? this.config.maxOutputTokens, // 输出限制(API max_tokens)
timeout: this.config.timeout,
});

Expand Down Expand Up @@ -278,8 +278,8 @@ export class Agent {
: await this.runLoop(enhancedMessage, context, loopOptions);

if (!result.success) {
// 如果是用户中止,返回空字符串(不抛出异常)
if (result.error?.type === 'aborted') {
// 如果是用户中止或用户拒绝,返回空字符串(不抛出异常)
if (result.error?.type === 'aborted' || result.metadata?.shouldExitLoop) {
return ''; // 返回空字符串,让调用方自行处理
}
// 其他错误则抛出异常
Expand Down Expand Up @@ -530,7 +530,6 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
// checkAndCompactInLoop 返回是否发生了压缩
// 🆕 传入上一轮 LLM 返回的真实 prompt tokens(比估算更准确)
const didCompact = await this.checkAndCompactInLoop(
messages,
context,
turnsCount,
lastPromptTokens, // 首轮为 undefined,使用估算;后续轮次使用真实值
Expand Down Expand Up @@ -669,6 +668,11 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
logger.debug('当前权限模式:', context.permissionMode);
logger.debug('================================\n');

// 🆕 如果 LLM 返回了 thinking 内容(DeepSeek R1 等),通知 UI
if (turnResult.reasoningContent && options?.onThinking) {
options.onThinking(turnResult.reasoningContent);
}

// 🆕 如果 LLM 返回了 content,立即显示
if (turnResult.content && turnResult.content.trim() && options?.onContent) {
options.onContent(turnResult.content);
Expand Down Expand Up @@ -858,7 +862,7 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
}
logger.debug('==================================\n');

// 🆕 检查是否应该退出循环(ExitPlanMode 返回时设置此标记
// 🆕 检查是否应该退出循环(ExitPlanMode 或用户拒绝时设置此标记
if (result.metadata?.shouldExitLoop) {
logger.debug('🚪 检测到退出循环标记,结束 Agent 循环');

Expand Down Expand Up @@ -1361,15 +1365,13 @@ IMPORTANT: Execute according to the approved plan above. Follow the steps exactl
* 在 Agent 循环中检查并执行压缩
* 仅使用 LLM 返回的真实 usage.promptTokens 进行判断(不再估算)
*
* @param messages - 实际发送给 LLM 的消息数组
* @param context - 聊天上下文
* @param currentTurn - 当前轮次
* @param actualPromptTokens - LLM 返回的真实 prompt tokens(必须,来自上一轮响应)
* @param onCompacting - 压缩状态回调
* @returns 是否发生了压缩
*/
private async checkAndCompactInLoop(
messages: Message[],
context: ChatContext,
currentTurn: number,
actualPromptTokens?: number,
Expand Down
2 changes: 1 addition & 1 deletion src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export interface LoopResult {
configuredMaxTurns?: number;
actualMaxTurns?: number;
hitSafetyLimit?: boolean;
shouldExitLoop?: boolean; // ExitPlanMode 设置此标记以退出循环
shouldExitLoop?: boolean; // ExitPlanMode 或用户拒绝时设置此标记以退出循环
targetMode?: PermissionMode; // Plan 模式批准后的目标权限模式
planContent?: string; // Plan 模式批准后的方案内容
};
Expand Down
5 changes: 5 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ export interface ModelConfig {
// 可选:模型特定参数
temperature?: number;
maxContextTokens?: number; // 上下文窗口大小
maxOutputTokens?: number; // 输出 token 限制
topP?: number;
topK?: number;

// Thinking 模型配置(如 DeepSeek R1)
supportsThinking?: boolean; // 手动覆盖自动检测结果
thinkingBudget?: number; // 思考 token 预算(可选)
}

export interface BladeConfig {
Expand Down
3 changes: 3 additions & 0 deletions src/services/ChatServiceInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export interface ChatConfig {
*/
export interface ChatResponse {
content: string;
reasoningContent?: string; // Thinking 模型的推理过程(如 DeepSeek R1)
toolCalls?: ChatCompletionMessageToolCall[];
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
reasoningTokens?: number; // Thinking 模型消耗的推理 tokens
};
}

Expand All @@ -54,6 +56,7 @@ export interface ChatResponse {
*/
export interface StreamChunk {
content?: string;
reasoningContent?: string; // Thinking 模型的推理过程片段
// biome-ignore lint/suspicious/noExplicitAny: 不同 provider 的 tool call 类型不同
toolCalls?: any[];
finishReason?: string;
Expand Down
25 changes: 25 additions & 0 deletions src/services/OpenAIChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,26 @@ export class OpenAIChatService implements IChatService {
(tc): tc is ChatCompletionMessageToolCall => tc.type === 'function'
);

// 提取 reasoning_content(DeepSeek R1 等 thinking 模型的扩展字段)
const extendedMessage = choice.message as typeof choice.message & {
reasoning_content?: string;
};
const reasoningContent = extendedMessage.reasoning_content || undefined;

// 提取 reasoning_tokens(thinking 模型的扩展 usage 字段)
const extendedUsage = completion.usage as typeof completion.usage & {
reasoning_tokens?: number;
};

const response = {
content: choice.message.content || '',
reasoningContent,
toolCalls: toolCalls,
usage: {
promptTokens: completion.usage?.prompt_tokens || 0,
completionTokens: completion.usage?.completion_tokens || 0,
totalTokens: completion.usage?.total_tokens || 0,
reasoningTokens: extendedUsage?.reasoning_tokens,
},
};

Expand Down Expand Up @@ -309,6 +322,7 @@ export class OpenAIChatService implements IChatService {

let chunkCount = 0;
let totalContent = '';
let totalReasoningContent = '';
let toolCallsReceived = false;

for await (const chunk of stream) {
Expand All @@ -326,10 +340,19 @@ export class OpenAIChatService implements IChatService {
continue;
}

// 提取 reasoning_content(DeepSeek R1 等 thinking 模型的扩展字段)
const extendedDelta = delta as typeof delta & {
reasoning_content?: string;
};

if (delta.content) {
totalContent += delta.content;
}

if (extendedDelta.reasoning_content) {
totalReasoningContent += extendedDelta.reasoning_content;
}

if (delta.tool_calls && !toolCallsReceived) {
toolCallsReceived = true;
_logger.debug('🔧 [ChatService] Tool calls detected in stream');
Expand All @@ -341,13 +364,15 @@ export class OpenAIChatService implements IChatService {
_logger.debug('📊 [ChatService] Stream summary:', {
totalChunks: chunkCount,
totalContentLength: totalContent.length,
totalReasoningContentLength: totalReasoningContent.length,
hadToolCalls: toolCallsReceived,
duration: Date.now() - startTime + 'ms',
});
}

yield {
content: delta.content || undefined,
reasoningContent: extendedDelta.reasoning_content || undefined,
toolCalls: delta.tool_calls,
finishReason: finishReason || undefined,
};
Expand Down
55 changes: 50 additions & 5 deletions src/services/VersionChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const CACHE_DIR = path.join(
);
const CACHE_FILE = path.join(CACHE_DIR, 'version-cache.json');

// 缓存有效期:24 小时
const CACHE_TTL = 24 * 60 * 60 * 1000;
// 缓存有效期:1 小时
const CACHE_TTL = 1 * 60 * 60 * 1000;

// npm registry URL
const NPM_REGISTRY_URL = `https://registry.npmmirror.com/${PACKAGE_NAME}/latest`;
Expand Down Expand Up @@ -224,13 +224,58 @@ export function formatUpdateMessage(result: VersionCheckResult): string | null {
}

/**
* 启动时版本检查(后台执行,不阻塞)
* 执行自动升级(后台进程,不阻塞主进程)
* @returns 升级提示消息
*/
async function performUpgrade(
currentVersion: string,
latestVersion: string
): Promise<string> {
const { spawn } = await import('child_process');

try {
const updateCommand = `npm install -g blade-code@${latestVersion} --registry https://registry.npmjs.org`;

// 使用 spawn + detached + unref 在后台运行升级
// 这样主进程退出后,升级进程会继续运行完成安装
const updateProcess = spawn(updateCommand, {
stdio: 'ignore',
shell: true,
detached: true,
});
updateProcess.unref();

return `⬆️ 正在后台升级 ${currentVersion} → ${latestVersion},下次启动生效`;
} catch {
return (
`\x1b[33m⚠️ Update available: ${currentVersion} → ${latestVersion}\x1b[0m\n` +
` Run \x1b[36mnpm install -g ${PACKAGE_NAME}@latest\x1b[0m to update`
);
}
}

/**
* 启动时版本检查并自动升级
*
* @returns Promise<string | null> 更新提示消息,如果没有更新则返回 null
* @param autoUpgrade - 是否自动升级(默认 true)
* @returns Promise<string | null> 提示消息,如果没有更新则返回 null
*/
export async function checkVersionOnStartup(): Promise<string | null> {
export async function checkVersionOnStartup(
autoUpgrade = true
): Promise<string | null> {
try {
const result = await checkVersion();

if (!result.hasUpdate || !result.latestVersion) {
return null;
}

// 自动升级
if (autoUpgrade) {
return await performUpgrade(result.currentVersion, result.latestVersion);
}

// 仅显示提示
return formatUpdateMessage(result);
} catch {
return null;
Expand Down
20 changes: 20 additions & 0 deletions src/store/selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,23 @@ export const useIsModal = (modal: ActiveModal) =>
*/
export const useIsBusy = () =>
useBladeStore((state) => state.session.isThinking || state.command.isProcessing);

// ==================== Thinking 模式选择器 ====================

/**
* 获取 Thinking 模式是否启用
*/
export const useThinkingModeEnabled = () =>
useBladeStore((state) => state.app.thinkingModeEnabled);

/**
* 获取当前 Thinking 内容(流式接收中)
*/
export const useCurrentThinkingContent = () =>
useBladeStore((state) => state.session.currentThinkingContent);

/**
* 获取 Thinking 内容是否展开
*/
export const useThinkingExpanded = () =>
useBladeStore((state) => state.session.thinkingExpanded);
21 changes: 21 additions & 0 deletions src/store/slices/appSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const initialAppState: AppState = {
modelEditorTarget: null,
todos: [],
awaitingSecondCtrlC: false,
thinkingModeEnabled: false, // Thinking 模式默认关闭
};

/**
Expand Down Expand Up @@ -137,5 +138,25 @@ export const createAppSlice: StateCreator<BladeStore, [], [], AppSlice> = (set)
app: { ...state.app, awaitingSecondCtrlC: awaiting },
}));
},

// ==================== Thinking 模式相关 actions ====================

/**
* 设置 Thinking 模式开关状态
*/
setThinkingModeEnabled: (enabled: boolean) => {
set((state) => ({
app: { ...state.app, thinkingModeEnabled: enabled },
}));
},

/**
* 切换 Thinking 模式开关
*/
toggleThinkingMode: () => {
set((state) => ({
app: { ...state.app, thinkingModeEnabled: !state.app.thinkingModeEnabled },
}));
},
},
});
Loading
Loading