fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持

- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对
- agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式
- agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch
- webui: server 输出通道适配器(保留 reasoning_content/disable_thinking)
- GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
This commit is contained in:
JianFeeeee
2026-08-14 00:48:40 +08:00
parent 5131c230b0
commit 5e46b3c29f
43 changed files with 4670 additions and 1478 deletions

View File

@ -12,16 +12,28 @@ let mainWindow;
function loadConnections() {
try {
if (fs.existsSync(CONNECTIONS_FILE)) {
return JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf-8'));
const data = JSON.parse(fs.readFileSync(CONNECTIONS_FILE, 'utf-8'));
normalizeConnections(data);
return data;
}
} catch (e) {
console.error('Failed to load connections:', e);
// 配置损坏:备份后重建,避免应用一直处于"无连接"状态
try {
const backup = CONNECTIONS_FILE + '.bak';
fs.copyFileSync(CONNECTIONS_FILE, backup);
fs.writeFileSync(CONNECTIONS_FILE, '{"connections":[],"currentId":null}', 'utf-8');
console.error('Backed up corrupt connections to', backup);
} catch (e2) {
console.error('Failed to recover connections file:', e2);
}
}
// Fallback: check app resource dir (installer writes fallback copy there)
try {
const fallback = path.join(__dirname, 'connections.json');
if (fs.existsSync(fallback)) {
const data = JSON.parse(fs.readFileSync(fallback, 'utf-8'));
normalizeConnections(data);
saveConnections(data);
console.log('Imported connections from app resource dir');
return data;
@ -32,6 +44,15 @@ function loadConnections() {
return { connections: [], currentId: null };
}
// 兼容旧数据:缺失的 type 默认为 webui(HTTP)
function normalizeConnections(data) {
if (!data || !Array.isArray(data.connections)) return;
data.connections.forEach((c) => {
if (!c.type) c.type = 'webui';
if (c.type !== 'cli' && c.type !== 'webui') c.type = 'webui';
});
}
function saveConnections(data) {
try {
fs.writeFileSync(CONNECTIONS_FILE, JSON.stringify(data, null, 2), 'utf-8');
@ -109,7 +130,8 @@ function createWindow() {
minWidth: 900,
minHeight: 600,
title: 'HomeAgent',
icon: path.join(__dirname, 'icon.svg'),
frame: false,
icon: path.join(__dirname, 'icon.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
@ -128,6 +150,18 @@ function createWindow() {
});
}
ipcMain.handle('window:minimize', (e) => {
BrowserWindow.fromWebContents(e.sender)?.minimize();
});
ipcMain.handle('window:toggleMaximize', (e) => {
const win = BrowserWindow.fromWebContents(e.sender);
if (!win) return;
if (win.isMaximized()) win.unmaximize(); else win.maximize();
});
ipcMain.handle('window:close', (e) => {
BrowserWindow.fromWebContents(e.sender)?.close();
});
ipcMain.handle('connections:list', () => {
return loadConnections();
});
@ -135,7 +169,14 @@ ipcMain.handle('connections:list', () => {
ipcMain.handle('connections:add', (_, conn) => {
const data = loadConnections();
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
data.connections.push({ id, name: conn.name, url: conn.url, apiKey: conn.apiKey });
data.connections.push({
id,
name: conn.name,
url: conn.url || '',
apiKey: conn.apiKey || '',
type: conn.type === 'cli' ? 'cli' : 'webui',
socketPath: conn.socketPath || '',
});
if (!data.currentId) data.currentId = id;
saveConnections(data);
return data;
@ -170,6 +211,52 @@ ipcMain.handle('connections:setCurrent', (_, id) => {
return data;
});
// CLI 传输:通过 homed 的 unix socket(逐行 JSON 协议)发起请求。
// 认证行:/auth <apiKey>(若配置了密钥)。返回 JSON 响应行。
ipcMain.handle('cli:request', (_, { socketPath, apiKey, line }) => {
return new Promise((resolve) => {
const net = require('net');
let client;
try {
client = net.createConnection({ path: socketPath });
} catch (e) {
return resolve({ error: 'create connection: ' + e.message });
}
const timeout = setTimeout(() => {
try { client.destroy(); } catch (_) {}
resolve({ error: 'timeout waiting for cli response' });
}, 30000);
let buf = '';
const onData = (chunk) => {
buf += chunk.toString('utf8');
const idx = buf.indexOf('\n');
if (idx === -1) return;
const lineOut = buf.slice(0, idx);
clearTimeout(timeout);
try { client.destroy(); } catch (_) {}
try {
resolve(JSON.parse(lineOut));
} catch (e) {
resolve({ error: 'bad response: ' + lineOut });
}
};
const onError = (err) => {
clearTimeout(timeout);
try { client.destroy(); } catch (_) {}
resolve({ error: err.message });
};
client.on('error', onError);
client.on('data', onData);
client.on('connect', () => {
let next = line;
if (apiKey) next = '/auth ' + apiKey + '\n' + next;
client.write(next + '\n');
});
});
});
app.whenReady().then(async () => {
const running = await isServerRunning();
if (!running) {