refactor: 全面清理死代码 — 删除 348 行无用代码
删除文件: - training-export.ts: 训练数据导出(零引用,UI 已移除) 删除函数: - workspace.ts: isProcessAlive, getActiveProcessCount, execWorkspaceCommand, terminateWorkspaceCommand - sqlite.ts: deleteMessagesBySession, getSkill, closeDatabase - tool-registry.ts: isToolEnabled - cron-manager.ts: clearAllTimers - workspace-panel.ts: setOnToolTerminated, setWorkspaceDirPath 删除 UI: - index.html: 训练数据导出按钮及说明 - settings-modal.ts: 训练数据导出事件绑定
This commit is contained in:
@@ -377,10 +377,6 @@ export function getMessagesBySession(sessionId: string): MessageRow[] {
|
|||||||
return queryAll(getDb(), 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC', [sessionId]) as unknown as MessageRow[];
|
return queryAll(getDb(), 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC', [sessionId]) as unknown as MessageRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteMessagesBySession(sessionId: string): void {
|
|
||||||
runExec(getDb(), 'DELETE FROM messages WHERE session_id = ?', [sessionId]);
|
|
||||||
persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Memories CRUD ───
|
// ─── Memories CRUD ───
|
||||||
|
|
||||||
@@ -531,9 +527,6 @@ export function saveSkill(skill: SkillRow): string {
|
|||||||
return skill.id;
|
return skill.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSkill(id: string): SkillRow | null {
|
|
||||||
return queryOne(getDb(), 'SELECT * FROM skills WHERE id = ?', [id]) as unknown as SkillRow | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllSkills(): SkillRow[] {
|
export function getAllSkills(): SkillRow[] {
|
||||||
return queryAll(getDb(), 'SELECT * FROM skills ORDER BY success_count DESC, updated_at DESC') as unknown as SkillRow[];
|
return queryAll(getDb(), 'SELECT * FROM skills ORDER BY success_count DESC, updated_at DESC') as unknown as SkillRow[];
|
||||||
@@ -626,12 +619,3 @@ export function importSessions(data: ExportData): { imported: number; skipped: n
|
|||||||
return { imported, skipped };
|
return { imported, skipped };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Close ───
|
|
||||||
|
|
||||||
export function closeDatabase(): void {
|
|
||||||
if (db) {
|
|
||||||
persist();
|
|
||||||
db.close();
|
|
||||||
db = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -199,161 +199,3 @@ export function listWorkspaceDir(dirPath?: string): { success: boolean; entries?
|
|||||||
return { success: false, error: (err as Error).message };
|
return { success: false, error: (err as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 检查进程是否还活着 */
|
|
||||||
export function isProcessAlive(id: string): boolean {
|
|
||||||
return activeProcesses.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取活跃进程数量 */
|
|
||||||
export function getActiveProcessCount(): number {
|
|
||||||
return activeProcesses.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行命令并收集完整输出(无超时限制)
|
|
||||||
* 用于 AI Tool Calling 集成,通过 IPC invoke/handle 调用
|
|
||||||
* 返回完整的 stdout + stderr + exitCode
|
|
||||||
*/
|
|
||||||
export function execWorkspaceCommand(
|
|
||||||
command: string,
|
|
||||||
cwd?: string,
|
|
||||||
onOutput?: (type: 'stdout' | 'stderr', data: string) => void
|
|
||||||
): Promise<{ success: boolean; stdout: string; stderr: string; exitCode: number | null; duration: number; userTerminated: boolean; truncated: boolean }> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const workDir = cwd ? path.resolve(cwd) : _workspaceDir;
|
|
||||||
const cmdId = `_tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
|
|
||||||
// 安全检查
|
|
||||||
const cmdCheck = checkCommandAllowed(command);
|
|
||||||
if (!cmdCheck.ok) {
|
|
||||||
sendLog('warn', `🔧 工具命令被拦截`, `${command.slice(0, 100)} | ${cmdCheck.reason}`);
|
|
||||||
resolve({ success: false, stdout: '', stderr: cmdCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirCheck = checkPathAllowed(workDir, 'read');
|
|
||||||
if (!dirCheck.ok) {
|
|
||||||
sendLog('warn', `🔧 工具目录被拦截`, `${workDir} | ${dirCheck.reason}`);
|
|
||||||
resolve({ success: false, stdout: '', stderr: dirCheck.reason!, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(workDir)) {
|
|
||||||
resolve({ success: false, stdout: '', stderr: `工作目录不存在: ${workDir}`, exitCode: 1, duration: 0, userTerminated: false, truncated: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendLog('info', `🔧 execWorkspaceCommand`, `${command.slice(0, 200)} | cwd: ${workDir}`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const MAX_OUTPUT = 5 * 1024 * 1024; // 5MB 输出上限
|
|
||||||
let stdoutBuf = '';
|
|
||||||
let stderrBuf = '';
|
|
||||||
let truncated = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const isWin = process.platform === 'win32';
|
|
||||||
const shell = isWin ? 'cmd.exe' : 'bash';
|
|
||||||
const actualCmd = isWin ? `chcp 65001 >nul && ${command}` : command;
|
|
||||||
const shellArgs = isWin ? ['/c', actualCmd] : ['-c', actualCmd];
|
|
||||||
const proc = spawn(shell, shellArgs, {
|
|
||||||
cwd: workDir,
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
...(isWin ? {} : { TERM: 'xterm-256color' }),
|
|
||||||
LANG: 'en_US.UTF-8',
|
|
||||||
LC_ALL: 'en_US.UTF-8',
|
|
||||||
PYTHONIOENCODING: 'utf-8'
|
|
||||||
},
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
|
|
||||||
activeProcesses.set(cmdId, proc);
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data: Buffer) => {
|
|
||||||
const str = data.toString('utf-8');
|
|
||||||
if (stdoutBuf.length < MAX_OUTPUT) {
|
|
||||||
stdoutBuf += str;
|
|
||||||
if (stdoutBuf.length > MAX_OUTPUT) {
|
|
||||||
stdoutBuf = stdoutBuf.slice(0, MAX_OUTPUT);
|
|
||||||
truncated = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
truncated = true;
|
|
||||||
}
|
|
||||||
// 实时推送到渲染进程
|
|
||||||
onOutput?.('stdout', str);
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data: Buffer) => {
|
|
||||||
const str = data.toString('utf-8');
|
|
||||||
if (stderrBuf.length < MAX_OUTPUT) {
|
|
||||||
stderrBuf += str;
|
|
||||||
if (stderrBuf.length > MAX_OUTPUT) {
|
|
||||||
stderrBuf = stderrBuf.slice(0, MAX_OUTPUT);
|
|
||||||
truncated = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
truncated = true;
|
|
||||||
}
|
|
||||||
// 实时推送到渲染进程
|
|
||||||
onOutput?.('stderr', str);
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
activeProcesses.delete(cmdId);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
sendLog(
|
|
||||||
code === 0 ? 'success' : 'warn',
|
|
||||||
`🔧 execWorkspaceCommand 完成`,
|
|
||||||
`code: ${code} | ${duration}ms | stdout: ${stdoutBuf.length}B | stderr: ${stderrBuf.length}B`
|
|
||||||
);
|
|
||||||
resolve({
|
|
||||||
success: code === 0,
|
|
||||||
stdout: stdoutBuf,
|
|
||||||
stderr: stderrBuf,
|
|
||||||
exitCode: code,
|
|
||||||
duration,
|
|
||||||
userTerminated: false,
|
|
||||||
truncated
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
|
||||||
activeProcesses.delete(cmdId);
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
sendLog('error', `🔧 execWorkspaceCommand 失败`, `${cmdId} | ${err.message}`);
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
stdout: stdoutBuf,
|
|
||||||
stderr: stderrBuf + (stderrBuf ? '\n' : '') + `进程启动失败: ${err.message}`,
|
|
||||||
exitCode: 1,
|
|
||||||
duration,
|
|
||||||
userTerminated: false,
|
|
||||||
truncated
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
sendLog('error', `🔧 execWorkspaceCommand 异常`, (err as Error).message);
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
stdout: stdoutBuf,
|
|
||||||
stderr: (err as Error).message,
|
|
||||||
exitCode: 1,
|
|
||||||
duration,
|
|
||||||
userTerminated: false,
|
|
||||||
truncated: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终止 execWorkspaceCommand 启动的进程
|
|
||||||
* @returns 是否成功终止
|
|
||||||
*/
|
|
||||||
export function terminateWorkspaceCommand(commandId: string): boolean {
|
|
||||||
return killProcess(commandId);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -151,15 +151,6 @@ export function initSettingsModal(): void {
|
|||||||
logSetting('检查间隔', `${ms / 60000} 分钟`);
|
logSetting('检查间隔', `${ms / 60000} 分钟`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── v5.0 训练数据导出 ──
|
|
||||||
document.querySelector('#btnExportTraining')!.addEventListener('click', async () => {
|
|
||||||
const { saveTrainingData } = await import('../services/training-export.js');
|
|
||||||
showToast('正在导出训练数据...', 'info');
|
|
||||||
const ok = await saveTrainingData();
|
|
||||||
if (ok) showToast('训练数据导出成功', 'success');
|
|
||||||
else showToast('导出取消或无数据', 'warning');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── v5.0 Cron 定时任务 ──
|
// ── v5.0 Cron 定时任务 ──
|
||||||
document.querySelector('#btnAddCronTask')!.addEventListener('click', async () => {
|
document.querySelector('#btnAddCronTask')!.addEventListener('click', async () => {
|
||||||
const name = (document.querySelector('#inputCronName') as HTMLInputElement).value.trim();
|
const name = (document.querySelector('#inputCronName') as HTMLInputElement).value.trim();
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ let currentAiCommand: string | null = null;
|
|||||||
/** 终止通知回调(由外部设置) */
|
/** 终止通知回调(由外部设置) */
|
||||||
let onToolTerminated: ((command: string) => void) | null = null;
|
let onToolTerminated: ((command: string) => void) | null = null;
|
||||||
|
|
||||||
/** 设置终止通知回调 */
|
|
||||||
export function setOnToolTerminated(cb: (command: string) => void): void {
|
|
||||||
onToolTerminated = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
function genId(): string {
|
function genId(): string {
|
||||||
return `ws_${Date.now()}_${++_counter}`;
|
return `ws_${Date.now()}_${++_counter}`;
|
||||||
@@ -628,17 +624,3 @@ export function getWorkspaceDirPath(): string {
|
|||||||
return workspaceDir;
|
return workspaceDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置工作空间目录 */
|
|
||||||
export async function setWorkspaceDirPath(dir: string): Promise<boolean> {
|
|
||||||
const bridge = window.metonaDesktop;
|
|
||||||
if (!bridge?.isDesktop) return false;
|
|
||||||
|
|
||||||
const result = await bridge.workspace.setDir(dir);
|
|
||||||
if (result.success && result.dir) {
|
|
||||||
workspaceDir = result.dir;
|
|
||||||
currentFileDir = workspaceDir;
|
|
||||||
logInfo('工作空间目录已更新', workspaceDir);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -347,11 +347,6 @@
|
|||||||
<button class="btn btn-outline" id="btnDoctor" style="margin-bottom:8px;">🩺 一键诊断</button>
|
<button class="btn btn-outline" id="btnDoctor" style="margin-bottom:8px;">🩺 一键诊断</button>
|
||||||
<div id="doctorResults" style="display:none;margin-top:8px;padding:10px;background:var(--bg-layer);border-radius:var(--radius-control);font-size:12px;max-height:200px;overflow-y:auto;"></div>
|
<div id="doctorResults" style="display:none;margin-top:8px;padding:10px;background:var(--bg-layer);border-radius:var(--radius-control);font-size:12px;max-height:200px;overflow-y:auto;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
|
||||||
<label class="setting-label">📚 训练数据导出</label>
|
|
||||||
<button class="btn btn-outline" id="btnExportTraining" style="margin-bottom:4px;">📥 导出训练数据 (JSONL)</button>
|
|
||||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">将 Agent 执行轨迹导出为 SFT 格式,可用于模型微调训练。包含成功的工具调用链和对话记录。</p>
|
|
||||||
</div>
|
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label">⏰ 定时任务 (Cron)</label>
|
<label class="setting-label">⏰ 定时任务 (Cron)</label>
|
||||||
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
<div style="display:flex;gap:8px;margin-bottom:8px;">
|
||||||
|
|||||||
@@ -183,12 +183,3 @@ export async function restoreCronTasks(): Promise<void> {
|
|||||||
logInfo(`定时任务已恢复: ${scheduled} 个任务`);
|
logInfo(`定时任务已恢复: ${scheduled} 个任务`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有定时器(应用退出时调用)
|
|
||||||
*/
|
|
||||||
export function clearAllTimers(): void {
|
|
||||||
for (const [id] of activeTimers) {
|
|
||||||
clearTimer(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -572,9 +572,6 @@ export function setToolEnabled(toolName: string, enabled: boolean): void {
|
|||||||
else enabledTools.delete(toolName);
|
else enabledTools.delete(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isToolEnabled(toolName: string): boolean {
|
|
||||||
return enabledTools.has(toolName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEnabledToolDefinitions(): ToolDefinition[] {
|
export function getEnabledToolDefinitions(): ToolDefinition[] {
|
||||||
const builtIn = TOOL_DEFINITIONS.filter(def => enabledTools.has(def.function.name));
|
const builtIn = TOOL_DEFINITIONS.filter(def => enabledTools.has(def.function.name));
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* TrainingExport - 训练数据导出 (v5.0)
|
|
||||||
* 将 Agent 执行轨迹转换为 SFT 训练数据格式
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { state, KEYS } from '../state/state.js';
|
|
||||||
import { logInfo, logSuccess, logError } from './log-service.js';
|
|
||||||
|
|
||||||
interface SFTRecord {
|
|
||||||
instruction: string;
|
|
||||||
input: string;
|
|
||||||
output: string;
|
|
||||||
tools?: Array<{ name: string; arguments: Record<string, unknown> }>;
|
|
||||||
observations?: string[];
|
|
||||||
metadata?: {
|
|
||||||
session_id: string;
|
|
||||||
session_title: string;
|
|
||||||
timestamp: number;
|
|
||||||
model: string;
|
|
||||||
eval_count?: number;
|
|
||||||
total_duration?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出所有会话的训练数据
|
|
||||||
* 从 traces + messages 中提取成功的工具调用链
|
|
||||||
*/
|
|
||||||
export async function exportTrainingData(): Promise<string | null> {
|
|
||||||
const bridge = (window as any).metonaDesktop;
|
|
||||||
if (!bridge?.db) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sessions = await bridge.db.getAllSessions();
|
|
||||||
const records: SFTRecord[] = [];
|
|
||||||
|
|
||||||
for (const session of sessions) {
|
|
||||||
const messages = await bridge.db.getMessages(session.id);
|
|
||||||
const traces = await bridge.db.getTraces(session.id);
|
|
||||||
|
|
||||||
// 遍历消息对(user → assistant)
|
|
||||||
for (let i = 0; i < messages.length - 1; i++) {
|
|
||||||
const userMsg = messages[i];
|
|
||||||
const asstMsg = messages[i + 1];
|
|
||||||
|
|
||||||
if (userMsg.role !== 'user' || asstMsg.role !== 'assistant') continue;
|
|
||||||
if (!userMsg.content || !asstMsg.content) continue;
|
|
||||||
|
|
||||||
// 解析工具调用
|
|
||||||
let tools: Array<{ name: string; arguments: Record<string, unknown> }> | undefined;
|
|
||||||
let observations: string[] | undefined;
|
|
||||||
|
|
||||||
if (asstMsg.tool_calls) {
|
|
||||||
try {
|
|
||||||
const toolCalls = JSON.parse(asstMsg.tool_calls);
|
|
||||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
||||||
tools = toolCalls.map((tc: any) => ({
|
|
||||||
name: tc.name || tc.function?.name || 'unknown',
|
|
||||||
arguments: tc.arguments || tc.function?.arguments || {}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 traces 中提取 observation
|
|
||||||
const sessionTraces = traces.filter(t =>
|
|
||||||
t.created_at >= userMsg.created_at && t.created_at <= asstMsg.created_at
|
|
||||||
);
|
|
||||||
if (sessionTraces.length > 0) {
|
|
||||||
observations = sessionTraces.map(t => t.observation || '').filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
records.push({
|
|
||||||
instruction: userMsg.content,
|
|
||||||
input: '',
|
|
||||||
output: asstMsg.content,
|
|
||||||
...(tools && { tools }),
|
|
||||||
...(observations?.length && { observations }),
|
|
||||||
metadata: {
|
|
||||||
session_id: session.id,
|
|
||||||
session_title: session.title,
|
|
||||||
timestamp: userMsg.created_at,
|
|
||||||
model: session.model,
|
|
||||||
...(asstMsg.eval_count && { eval_count: asstMsg.eval_count }),
|
|
||||||
...(asstMsg.total_duration && { total_duration: asstMsg.total_duration })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
i++; // 跳过 assistant 消息
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (records.length === 0) {
|
|
||||||
logInfo('训练数据导出: 无可导出数据');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSuccess(`训练数据导出完成: ${records.length} 条记录`);
|
|
||||||
return records.map(r => JSON.stringify(r)).join('\n');
|
|
||||||
} catch (err) {
|
|
||||||
logError('训练数据导出失败', (err as Error).message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存训练数据到文件
|
|
||||||
*/
|
|
||||||
export async function saveTrainingData(): Promise<boolean> {
|
|
||||||
const bridge = (window as any).metonaDesktop;
|
|
||||||
if (!bridge?.dialog?.saveFile) return false;
|
|
||||||
|
|
||||||
const data = await exportTrainingData();
|
|
||||||
if (!data) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filePath = await bridge.dialog.saveFile({
|
|
||||||
defaultPath: `metona-training-data-${new Date().toISOString().slice(0, 10)}.jsonl`,
|
|
||||||
filters: [{ name: 'JSONL', extensions: ['jsonl'] }, { name: 'JSON', extensions: ['json'] }]
|
|
||||||
});
|
|
||||||
if (!filePath) return false;
|
|
||||||
|
|
||||||
await bridge.fs.writeFile(filePath, data);
|
|
||||||
logSuccess(`训练数据已保存: ${filePath}`);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
logError('保存训练数据失败', (err as Error).message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user