feat: 升级至 v0.2.2 — 工作空间路径注入、Provider 切换修复、编码与样式优化
功能增强: - System Prompt 注入当前工作空间路径到动态区(LLM 可使用绝对路径调用工具) - ContextBuilder.buildSystemPrompt 新增 workspacePath 参数 Provider 切换修复: - reloadAdapter 返回 boolean,失败时 sendMessage 中止发送 - 切换 Provider 时自动清空 apiKey(不同 Provider key 不通用) - apiKey 为空时显示警告提示 - Provider 切换事件加 sessionId 字段 编码修复: - run_command 使用 encoding: 'buffer' 获取原始字节 - 新增 decodeBuffer 智能解码:UTF-8 严格 → GBK → UTF-8 宽松 - 修复不响应 chcp 的 Windows 命令中文乱码问题 样式优化: - 代码块底色改用主题变量(灰底主题色文字,双主题适配) - 表格标题 th/td 底色和文字色改用主题变量 - CodeBlock 组件 pre 背景从 secondary.main 改为 background.default - 设置面板已自动执行工具行 opacity 0.8→0.15(淡绿底,文字清晰) 版本号 0.2.1 → 0.2.2
This commit is contained in:
@@ -25,6 +25,7 @@ interface ContextBuildParams {
|
||||
history?: MetonaMessage[];
|
||||
memories?: MetonaMemoryItem[];
|
||||
workspaceFiles?: WorkspaceFiles;
|
||||
workspacePath?: string;
|
||||
contextWindow?: number;
|
||||
}
|
||||
|
||||
@@ -43,10 +44,11 @@ export class ContextBuilder {
|
||||
history = [],
|
||||
memories = [],
|
||||
workspaceFiles,
|
||||
workspacePath,
|
||||
contextWindow = 128_000,
|
||||
} = params;
|
||||
|
||||
const systemPrompt = this.buildSystemPrompt(workspaceFiles);
|
||||
const systemPrompt = this.buildSystemPrompt(workspaceFiles, workspacePath);
|
||||
const estimatedTokens = this.estimateTokens(history, memories, availableTools, userInput, systemPrompt);
|
||||
|
||||
return {
|
||||
@@ -76,7 +78,7 @@ export class ContextBuilder {
|
||||
* 4. MEMORY.md(记忆)— 动态区
|
||||
* 5. 内置安全准则 — 尾部锚定
|
||||
*/
|
||||
buildSystemPrompt(workspaceFiles?: WorkspaceFiles): MetonaSystemPrompt {
|
||||
buildSystemPrompt(workspaceFiles?: WorkspaceFiles, workspacePath?: string): MetonaSystemPrompt {
|
||||
let staticUserProfile = '';
|
||||
|
||||
// ===== 静态区:用户画像(变化不频繁,放入静态区利用 LLM 缓存)=====
|
||||
@@ -99,6 +101,11 @@ export class ContextBuilder {
|
||||
// ===== 动态区:记忆 =====
|
||||
const dynamicParts: string[] = [];
|
||||
|
||||
// 注入当前工作空间路径(动态区,路径可能切换故不放入静态区)
|
||||
if (workspacePath) {
|
||||
dynamicParts.push(`## Current Workspace\nWorkspace root path: \`${workspacePath}\`\n\nAll relative paths in tool calls are resolved against this workspace root. Use this path when absolute paths are required (e.g., in run_command).`);
|
||||
}
|
||||
|
||||
if (workspaceFiles?.memory) {
|
||||
const memoryContent = this.extractContent(workspaceFiles.memory);
|
||||
if (memoryContent) {
|
||||
|
||||
@@ -24,6 +24,26 @@ import type { SandboxManager } from '../../sandbox/sandbox';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// ===== Windows 编码智能解码 =====
|
||||
// 某些 Windows 程序(dir、systeminfo、ipconfig 等)不响应 chcp 65001,
|
||||
// 仍按系统代码页(GBK/CP936)输出字节流。
|
||||
// 策略:先尝试严格 UTF-8 解码,失败则用 GBK 解码,最后降级为 UTF-8 宽松模式。
|
||||
function decodeBuffer(buf: Buffer): string {
|
||||
if (buf.length === 0) return '';
|
||||
try {
|
||||
// 严格模式:包含非法 UTF-8 字节序列时抛错
|
||||
return new TextDecoder('utf-8', { fatal: true }).decode(buf);
|
||||
} catch {
|
||||
// UTF-8 解码失败 → 用 GBK/CP936 解码(Windows 中文系统默认代码页)
|
||||
try {
|
||||
return new TextDecoder('gbk').decode(buf);
|
||||
} catch {
|
||||
// 最终降级:UTF-8 宽松模式(用 替换字符 代替非法字节)
|
||||
return buf.toString('utf-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 9. run_command =====
|
||||
|
||||
export class RunCommandTool implements IMetonaTool {
|
||||
@@ -100,10 +120,13 @@ export class RunCommandTool implements IMetonaTool {
|
||||
? `chcp 65001 >nul 2>&1 && ${command}`
|
||||
: command;
|
||||
|
||||
// 使用 encoding: 'buffer' 获取原始字节,手动智能解码
|
||||
// 部分程序(如 dir、systeminfo)不响应 chcp 65001,仍输出 GBK 字节流
|
||||
const { stdout, stderr } = await execAsync(finalCommand, {
|
||||
cwd: resolvedWorkdir,
|
||||
timeout,
|
||||
maxBuffer: 1024 * 1024, // 1MB
|
||||
encoding: 'buffer', // 返回 Buffer 而非字符串,便于智能解码
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
@@ -118,18 +141,18 @@ export class RunCommandTool implements IMetonaTool {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
stdout: stdout.slice(0, 50_000),
|
||||
stderr: stderr.slice(0, 10_000),
|
||||
stdout: decodeBuffer(stdout).slice(0, 50_000),
|
||||
stderr: decodeBuffer(stderr).slice(0, 10_000),
|
||||
command,
|
||||
};
|
||||
} catch (error) {
|
||||
const err = error as { message: string; code?: number; stdout?: string; stderr?: string };
|
||||
const err = error as { message: string; code?: number; stdout?: Buffer; stderr?: Buffer };
|
||||
return {
|
||||
success: false,
|
||||
error: err.message,
|
||||
exit_code: err.code,
|
||||
stdout: (err.stdout ?? '').slice(0, 10_000),
|
||||
stderr: (err.stderr ?? '').slice(0, 10_000),
|
||||
stdout: decodeBuffer(err.stdout ?? Buffer.alloc(0)).slice(0, 10_000),
|
||||
stderr: decodeBuffer(err.stderr ?? Buffer.alloc(0)).slice(0, 10_000),
|
||||
command,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function registerAllIPCHandlers(
|
||||
sessionRecorder: SessionRecorder,
|
||||
memoryManager: MemoryManager,
|
||||
mcpManager: MCPManager,
|
||||
reloadAdapter: () => void,
|
||||
reloadAdapter: () => boolean,
|
||||
promptInjectionDefender: PromptInjectionDefender,
|
||||
outputValidator: OutputValidator,
|
||||
confirmationHook: ConfirmationHook,
|
||||
@@ -56,8 +56,29 @@ export function registerAllIPCHandlers(
|
||||
ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
|
||||
log.info('[AGENT] sendMessage:', sessionId, userMessage.content.slice(0, 80));
|
||||
|
||||
// 发送消息前确保 Adapter 使用最新配置(防止 config:set 中间状态导致 reload 失败)
|
||||
reloadAdapter();
|
||||
// 发送消息前确保 Adapter 使用最新配置(失败则中止,防止用旧 Provider 的 adapter 发送)
|
||||
if (!reloadAdapter()) {
|
||||
const errorMsg = 'Adapter 加载失败,请检查 LLM 配置(Provider、API Key、Base URL、Model 是否完整)';
|
||||
log.error('[AGENT]', errorMsg);
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
const errorEvent: MetonaStreamEvent = {
|
||||
type: MetonaStreamEventType.ERROR,
|
||||
requestId: '',
|
||||
sessionId,
|
||||
iteration: 0,
|
||||
seq: 0,
|
||||
timestamp: Date.now(),
|
||||
error: { code: MetonaErrorCode.UNKNOWN, message: errorMsg, retryable: false },
|
||||
};
|
||||
mainWindow.webContents.send('agent:streamEvent', errorEvent);
|
||||
mainWindow.webContents.send('agent:streamEvent', {
|
||||
...errorEvent,
|
||||
type: MetonaStreamEventType.DONE,
|
||||
});
|
||||
}
|
||||
sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error' });
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
|
||||
// TRACE 层:开始录制
|
||||
sessionRecorder.startRecording(sessionId);
|
||||
@@ -88,9 +109,9 @@ export function registerAllIPCHandlers(
|
||||
timestamp: m.timestamp,
|
||||
}));
|
||||
|
||||
// 从工作空间文件构建 System Prompt
|
||||
// 从工作空间文件构建 System Prompt(注入当前工作空间路径,便于 LLM 在需要绝对路径时使用)
|
||||
const workspaceFiles = workspaceService.getFiles();
|
||||
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles);
|
||||
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles, workspaceService.getPath());
|
||||
|
||||
// 检索与用户消息相关的记忆,注入到 System Prompt 动态区
|
||||
try {
|
||||
|
||||
+4
-1
@@ -261,7 +261,7 @@ async function initialize(): Promise<void> {
|
||||
|
||||
// ===== 热重载 Adapter 回调(设置变更时触发)=====
|
||||
let lastProvider = configService.get<string>('llm.provider') ?? '';
|
||||
const reloadAdapter = () => {
|
||||
const reloadAdapter = (): boolean => {
|
||||
try {
|
||||
const newAdapter = createAdapter();
|
||||
agentLoop.setAdapter(newAdapter);
|
||||
@@ -282,6 +282,7 @@ async function initialize(): Promise<void> {
|
||||
from: lastProvider,
|
||||
to: provider,
|
||||
reason: 'config_changed',
|
||||
sessionId: '',
|
||||
});
|
||||
}
|
||||
mainWindow.webContents.send('toast:show', {
|
||||
@@ -290,6 +291,7 @@ async function initialize(): Promise<void> {
|
||||
});
|
||||
}
|
||||
lastProvider = provider;
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.error(`[CONFIG] Failed to reload adapter: ${(err as Error).message}`);
|
||||
// 通知渲染进程 Provider 切换失败(UI 显示错误 Toast)
|
||||
@@ -299,6 +301,7 @@ async function initialize(): Promise<void> {
|
||||
message: `Provider 切换失败: ${(err as Error).message}`,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user