fix: 搜索引擎DuckDuckGo→百度, 移除全部超时限制, 默认85轮可配置, 执行日志导出

- 搜索引擎从 DuckDuckGo 换成百度(Baidu),解决国内不可达问题
- 移除全部超时限制:Agent Loop、流式响应、工具执行、web_fetch、download_file
- Agent 最大轮数从 15 改为默认 85 轮,设置面板可配置
- 日志面板新增导出按钮,支持导出为 TXT 文件
- TOOL_USAGE_GUIDE 新增禁止猜 URL 和中文搜索结果利用规则
- 版本号更新至 5.1.1
This commit is contained in:
thzxx
2026-04-19 17:08:46 +08:00
parent fc60d5f2e8
commit e2cb87a060
8 changed files with 173 additions and 75 deletions
+9
View File
@@ -176,6 +176,15 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi
## 构建日志 ## 构建日志
### v5.1.12026-04-19 — metona-ollama-desktop 仓库
- 🔧 搜索引擎:DuckDuckGo → 百度(Baidu),解决国内不可达问题
- 🔧 移除全部超时限制:Agent Loop、流式响应、工具执行、web_fetch、download_file
- 🔧 Agent 最大轮数默认 85 轮,设置面板可配置
- ✨ 执行日志导出功能(日志面板新增导出按钮)
- 🔧 TOOL_USAGE_GUIDE 新增禁止猜 URL 和中文搜索结果利用规则
- 📝 版本号更新至 5.1.1
### v5.1.02026-04-18 — metona-ollama-desktop 仓库 ### v5.1.02026-04-18 — metona-ollama-desktop 仓库
- ✨ 新增 4 个工具:memory_replace、memory_remove、skill_list、skill_view(总数 38 个) - ✨ 新增 4 个工具:memory_replace、memory_remove、skill_list、skill_view(总数 38 个)
+28
View File
@@ -11,6 +11,34 @@
## 桌面版(Desktop ## 桌面版(Desktop
### Desktop v5.1.1 — 搜索引擎替换 & 超时移除 & 日志导出
- 🔧 **搜索引擎:DuckDuckGo → 百度**
- 移除 DuckDuckGo 搜索引擎(国内不可达,每次浪费 8 秒超时)
- 首选百度搜索(Baidu),Bing 和 Google 作为备用
- 新增 `parseBaiduResults()` 解析函数
- 移除所有搜索引擎的超时限制
- 🔧 **移除全部超时限制**
- 移除 Agent Loop 全局 10 分钟总超时(`MAX_LOOP_TIME`
- 移除流式响应 2 分钟超时(`STREAM_TIMEOUT`
- 移除工具执行 30 秒超时(`TOOL_EXEC_TIMEOUT`
- 移除 web_fetch 30 秒超时
- 移除 download_file 60 秒超时
- 现在所有操作无超时限制,宁可等待也不中断
- 🔧 **Agent 最大轮数改为 85 轮(默认)**
- 从硬编码 15 轮改为从 state 读取,默认 85 轮
- 设置面板新增「Agent 最大轮数」配置项
- 启动时从数据库恢复设置值
-**执行日志导出**
- 日志面板新增「📥 导出」按钮
- 导出为带时间戳的 TXT 文件
- 支持原生保存对话框(桌面端)和浏览器下载(回退)
- 🔧 **改进 TOOL_USAGE_GUIDE**
- 新增规则 13:严禁猜测或编造 URL 路径,所有 URL 必须从搜索结果获取
- 新增规则 14:不要忽视来自知乎、CSDN 等中文来源的搜索结果
- 修复工具去重缓存 key 逻辑,失败工具不再被误杀
- 📝 **版本号更新至 5.1.1**7 个文件同步更新)
### Desktop v5.1.0 — Hermes 借鉴 & 记忆/Skill 系统增强 ### Desktop v5.1.0 — Hermes 借鉴 & 记忆/Skill 系统增强
-**记忆 replace/remove 操作**(借鉴 Hermes Agent -**记忆 replace/remove 操作**(借鉴 Hermes Agent
+37 -31
View File
@@ -441,8 +441,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}, }
signal: AbortSignal.timeout(30000)
}); });
if (!resp.ok) { if (!resp.ok) {
@@ -490,7 +489,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
/** /**
* Web Search — 联网搜索 * Web Search — 联网搜索
* 使用 DuckDuckGo HTML 搜索并提取结果 * 使用百度搜索(国内可用)+ Bing 作为备用
*/ */
export async function handleWebSearch(params: { query: string; max_results?: number }): Promise<ToolResult> { export async function handleWebSearch(params: { query: string; max_results?: number }): Promise<ToolResult> {
try { try {
@@ -509,31 +508,29 @@ export async function handleWebSearch(params: { query: string; max_results?: num
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8' 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}; };
// 1. 尝试 DuckDuckGo HTML 搜索(8 秒超时,国内环境通常不可用 // 1. 百度搜索(国内首选
let results: Array<{ title: string; url: string; snippet: string }> = []; let results: Array<{ title: string; url: string; snippet: string }> = [];
try { try {
const ddgUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; const baiduUrl = `https://www.baidu.com/s?wd=${encodeURIComponent(query)}&rn=${maxResults}`;
const resp = await fetch(ddgUrl, { const resp = await fetch(baiduUrl, {
headers: searchHeaders, headers: searchHeaders,
signal: AbortSignal.timeout(8000)
}); });
if (resp.ok) { if (resp.ok) {
const html = await resp.text(); const html = await resp.text();
results = parseDDGResults(html, maxResults); results = parseBaiduResults(html, maxResults);
} }
} catch (e) { } catch (e) {
sendLog('warn', `🔍 DuckDuckGo 搜索失败(8s 超时)`, (e as Error).message); sendLog('warn', `🔍 百度搜索失败`, (e as Error).message);
} }
// 2. DuckDuckGo 无结果,尝试 Bing // 2. 百度无结果,尝试 Bing
if (results.length === 0) { if (results.length === 0) {
try { try {
const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`; const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
const resp = await fetch(bingUrl, { const resp = await fetch(bingUrl, {
headers: searchHeaders, headers: searchHeaders,
signal: AbortSignal.timeout(15000)
}); });
if (resp.ok) { if (resp.ok) {
@@ -545,13 +542,12 @@ export async function handleWebSearch(params: { query: string; max_results?: num
} }
} }
// 3. Bing 也失败,尝试 Google(通过 google.com/search // 3. Bing 也失败,尝试 Google
if (results.length === 0) { if (results.length === 0) {
try { try {
const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`; const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`;
const resp = await fetch(googleUrl, { const resp = await fetch(googleUrl, {
headers: searchHeaders, headers: searchHeaders,
signal: AbortSignal.timeout(15000)
}); });
if (resp.ok) { if (resp.ok) {
@@ -586,27 +582,39 @@ export async function handleWebSearch(params: { query: string; max_results?: num
} }
} }
/** 解析 DuckDuckGo HTML 搜索结果 */ /** 解析百度 HTML 搜索结果 */
function parseDDGResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> { function parseBaiduResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> {
const results: Array<{ title: string; url: string; snippet: string }> = []; const results: Array<{ title: string; url: string; snippet: string }> = [];
// 匹配结果块:<a class="result__a" href="...">title</a> ... <a class="result__snippet">snippet</a> // 百度结果块:<div class="result" ...> 或 <div class="result-op" ...>
const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi; // 标题:<h3 class="t"> 内的 <a href="...">title</a>
// 摘要:<div class="c-abstract">...</div>
const blockRegex = /<div class="result[^"]*"[^>]*>([\s\S]*?)(?=<div class="result|<div id="content_right|$)/gi;
let blockMatch;
let match; while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) {
while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) { const block = blockMatch[1];
let url = match[1].trim();
// DuckDuckGo 使用重定向链接,提取真实 URL // 提取标题和 URL
const uddgMatch = url.match(/[?&]uddg=([^&]+)/); const titleMatch = block.match(/<h3[^>]*class="[^"]*t[^"]*"[^>]*>[\s\S]*?<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/);
if (uddgMatch) { if (!titleMatch) continue;
url = decodeURIComponent(uddgMatch[1]);
let url = titleMatch[1].trim();
const title = decodeHTML(titleMatch[2].replace(/<[^>]+>/g, '').trim());
// 百度有时使用 data-url 属性
if (!url.startsWith('http')) {
const dataUrlMatch = block.match(/data-url="(https?:\/\/[^"]*)"/);
if (dataUrlMatch) url = dataUrlMatch[1];
} }
const title = decodeHTML(match[2].replace(/<[^>]+>/g, '').trim()); // 提取摘要
const snippet = decodeHTML(match[3].replace(/<[^>]+>/g, '').trim()); const snippetMatch = block.match(/<div class="c-abstract"[^>]*>([\s\S]*?)<\/div>/) ||
block.match(/<span class="content-right_[^"]*"[^>]*>([\s\S]*?)<\/span>/);
const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : '';
if (title && url.startsWith('http')) { if (title) {
results.push({ title, url, snippet }); results.push({ title, url: url.startsWith('http') ? url : `https://www.baidu.com${url}`, snippet });
} }
} }
@@ -829,9 +837,7 @@ export async function handleDownloadFile(params: { url: string; destination: str
} }
sendLog('info', `⬇️ download_file`, `${params.url}${destPath}`); sendLog('info', `⬇️ download_file`, `${params.url}${destPath}`);
const resp = await fetch(params.url, { const resp = await fetch(params.url);
signal: AbortSignal.timeout(60000)
});
if (!resp.ok) return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` }; if (!resp.ok) return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` };
const buf = Buffer.from(await resp.arrayBuffer()); const buf = Buffer.from(await resp.arrayBuffer());
+66 -1
View File
@@ -6,7 +6,7 @@ import { state, KEYS } from '../state/state.js';
import { debounce } from '../utils/utils.js'; import { debounce } from '../utils/utils.js';
import { encryptData, decryptData } from '../services/crypto.js'; import { encryptData, decryptData } from '../services/crypto.js';
import { setMemoryEnabled, setEmbeddingModel, getEmbeddingModel, isVectorMemoryEnabled, getMemoryCache } from '../services/memory-manager.js'; import { setMemoryEnabled, setEmbeddingModel, getEmbeddingModel, isVectorMemoryEnabled, getMemoryCache } from '../services/memory-manager.js';
import { logSetting, logInfo, logWarn, logError, logSuccess } from '../services/log-service.js'; import { logSetting, logInfo, logWarn, logError, logSuccess, getLogs } from '../services/log-service.js';
import { checkConnection, updateConnectionInfo, updateRunningModels } from './header.js'; import { checkConnection, updateConnectionInfo, updateRunningModels } from './header.js';
import { loadModels } from './model-bar.js'; import { loadModels } from './model-bar.js';
import { showToast } from './toast.js'; import { showToast } from './toast.js';
@@ -73,6 +73,20 @@ export function initSettingsModal(): void {
}, 300); }, 300);
tempSlider.addEventListener('change', saveTemperature); tempSlider.addEventListener('change', saveTemperature);
// ── v5.1.1 Agent 最大轮数 ──
const saveMaxTurns = debounce(async () => {
const db = state.get<ChatDB | null>(KEYS.DB);
const val = (document.querySelector('#inputMaxTurns') as HTMLInputElement).value.trim();
const maxTurns = val ? parseInt(val) : 85;
state.set('maxTurns', maxTurns);
if (db) await db.saveSetting('maxTurns', maxTurns);
logSetting('Agent 最大轮数', `${maxTurns}`);
}, 500);
document.querySelector('#inputMaxTurns')!.addEventListener('input', saveMaxTurns);
// ── v5.1.1 执行日志导出 ──
document.querySelector('#btnExportLog')!.addEventListener('click', exportExecutionLog);
// ── v4.2 人格模式切换 ── // ── v4.2 人格模式切换 ──
const PERSONALITY_TEMPERATURES: Record<string, number> = { const PERSONALITY_TEMPERATURES: Record<string, number> = {
default: -1, // 使用用户设置 default: -1, // 使用用户设置
@@ -378,6 +392,57 @@ export function closeSettingsModal(): void {
settingsModalEl.style.display = 'none'; settingsModalEl.style.display = 'none';
} }
/** 导出执行日志为 TXT 文件 */
async function exportExecutionLog(): Promise<void> {
const logs = getLogs();
if (logs.length === 0) {
showToast('没有可导出的日志', 'warning');
return;
}
const formatTime = (ts: number): string => {
const d = new Date(ts);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
};
const lines = logs.map(e => {
let line = `[${formatTime(e.time)}] ${e.level.toUpperCase()} ${e.icon} ${e.message}`;
if (e.detail) line += `\n ${e.detail}`;
return line;
});
const content = `Metona Ollama Desktop — 执行日志\n导出时间: ${formatTime(Date.now())}\n日志条数: ${logs.length}\n${'═'.repeat(60)}\n\n${lines.join('\n')}`;
try {
const bridge = (window as any).metonaDesktop;
if (bridge) {
const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
const filePath = await bridge.dialog.saveFile({
defaultPath: `metona-log-${ts}.txt`,
filters: [{ name: '文本文件', extensions: ['txt'] }]
});
if (!filePath) return;
const result = await bridge.fs.writeFile(filePath, content, 'utf-8');
if (!result.success) {
showToast(`导出失败: ${result.error}`, 'error');
return;
}
} else {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `metona-log-${Date.now()}.txt`;
a.click();
}
showToast(`已导出 ${logs.length} 条日志`, 'success');
logSuccess(`日志导出完成: ${logs.length}`);
} catch (err) {
showToast(`导出失败: ${(err as Error).message}`, 'error');
logError('日志导出失败', (err as Error).message);
}
}
async function exportAllSessions(): Promise<void> { async function exportAllSessions(): Promise<void> {
const db = state.get<ChatDB | null>(KEYS.DB); const db = state.get<ChatDB | null>(KEYS.DB);
if (!db) return; if (!db) return;
+7
View File
@@ -15,6 +15,7 @@
<aside class="log-panel" id="logPanel"> <aside class="log-panel" id="logPanel">
<div class="log-panel-header"> <div class="log-panel-header">
<span class="log-panel-title">📋 执行日志</span> <span class="log-panel-title">📋 执行日志</span>
<button class="btn btn-sm" id="btnExportLog" title="导出日志" style="padding:2px 8px;font-size:11px;opacity:0.7;">📥 导出</button>
</div> </div>
<div class="log-panel-body" id="logPanelBody"></div> <div class="log-panel-body" id="logPanelBody"></div>
</aside> </aside>
@@ -259,6 +260,12 @@
<label class="setting-label" style="margin-top:12px;">温度(Temperature):<span id="tempValue">0.7</span></label> <label class="setting-label" style="margin-top:12px;">温度(Temperature):<span id="tempValue">0.7</span></label>
<input type="range" id="inputTemperature" class="temp-slider" min="0" max="2" step="0.1" value="0.7"> <input type="range" id="inputTemperature" class="temp-slider" min="0" max="2" step="0.1" value="0.7">
<p class="text-muted" style="margin-top:4px;font-size:11px;">低值更精确稳定,高值更有创意多样(0 = 确定性,2 = 最大随机)</p> <p class="text-muted" style="margin-top:4px;font-size:11px;">低值更精确稳定,高值更有创意多样(0 = 确定性,2 = 最大随机)</p>
<label class="setting-label" style="margin-top:12px;">Agent 最大轮数(maxTurns</label>
<div style="display:flex;gap:8px;align-items:center;">
<input class="setting-input" id="inputMaxTurns" type="number" min="1" max="500" step="1" value="85" style="margin-bottom:0;">
<span class="text-muted" style="white-space:nowrap;"></span>
</div>
<p class="text-muted" style="margin-top:4px;font-size:11px;">Agent Loop 最大工具调用轮数。默认 85 轮,复杂任务可能需要更多轮次。</p>
</div> </div>
<div class="setting-group"> <div class="setting-group">
<label class="setting-label">🎭 人格模式</label> <label class="setting-label">🎭 人格模式</label>
+5
View File
@@ -353,6 +353,11 @@ async function init(): Promise<void> {
(document.querySelector('#inputTemperature') as HTMLInputElement).value = String(temperature); (document.querySelector('#inputTemperature') as HTMLInputElement).value = String(temperature);
document.querySelector('#tempValue')!.textContent = parseFloat(temperature).toFixed(1); document.querySelector('#tempValue')!.textContent = parseFloat(temperature).toFixed(1);
// ── v5.1.1 Agent 最大轮数 ──
const maxTurns = await db.getSetting('maxTurns', 85);
state.set('maxTurns', maxTurns);
(document.querySelector('#inputMaxTurns') as HTMLInputElement).value = String(maxTurns);
// ── Tool Calling 设置 ── // ── Tool Calling 设置 ──
const toolCallingEnabled = await db.getSetting('toolCallingEnabled', false); const toolCallingEnabled = await db.getSetting('toolCallingEnabled', false);
const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled'; const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled';
+14 -41
View File
@@ -28,9 +28,6 @@ import type {
TraceEntry TraceEntry
} from '../types.js'; } from '../types.js';
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
const MAX_RETRIES = 2; // 工具错误自动重试次数 const MAX_RETRIES = 2; // 工具错误自动重试次数
/** 每个工具返回给模型的最大字符数 */ /** 每个工具返回给模型的最大字符数 */
@@ -89,7 +86,9 @@ const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】
9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。 9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。 10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。
11. 管理记忆:可以用 memory_replace 更新已有记忆(子串匹配 old_text),用 memory_remove 删除不再需要的记忆。 11. 管理记忆:可以用 memory_replace 更新已有记忆(子串匹配 old_text),用 memory_remove 删除不再需要的记忆。
12. 查看技能:可以用 skill_list 列出所有可用技能,用 skill_view 查看特定技能的详细工具链。`; 12. 查看技能:可以用 skill_list 列出所有可用技能,用 skill_view 查看特定技能的详细工具链。
13. 【严禁猜 URL】绝对不要猜测或编造 URL 路径(如 https://example.com/docs/install)。所有 URL 必须从搜索结果中获取。如果你猜了一个 URL 并得到 404 错误,不要继续猜其他路径,应该回到搜索结果中找正确的链接。
14. 搜索结果来源:搜索结果中每条都带有标题、URL 和摘要。摘要已经包含关键信息,优先从摘要中提取答案。不要忽视来自知乎、CSDN、博客园等中文来源的结果,它们通常包含有效的技术信息。`;`;
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */ /** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
const VALID_TOOL_NAMES = new Set([ const VALID_TOOL_NAMES = new Set([
@@ -488,8 +487,8 @@ export async function runAgentLoop(
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`); logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
// 迭代预算:从 state 读取,默认 15 // 迭代预算:从 state 读取,默认 85
const maxLoops = state.get<number>('maxTurns', 15); const maxLoops = state.get<number>('maxTurns', 85);
let loopCount = 0; let loopCount = 0;
const allToolRecords: ToolCallRecord[] = []; const allToolRecords: ToolCallRecord[] = [];
@@ -509,13 +508,6 @@ export async function runAgentLoop(
while (loopCount < maxLoops) { while (loopCount < maxLoops) {
loopCount++; loopCount++;
// 全局超时检查
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
logWarn('ReAct Agent Loop 达到最大运行时间(10分钟),自动停止');
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
return;
}
// 检查是否已中止 // 检查是否已中止
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) { if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
logInfo('ReAct Agent Loop 已中止'); logInfo('ReAct Agent Loop 已中止');
@@ -534,9 +526,8 @@ export async function runAgentLoop(
state.set(KEYS.ABORT_CONTROLLER, abortController); state.set(KEYS.ABORT_CONTROLLER, abortController);
try { try {
// 带超时保护的流式调用 // 流式调用(无超时限制)
await Promise.race([ await api.chatStream(
api.chatStream(
{ {
model, model,
messages, messages,
@@ -578,11 +569,7 @@ export async function runAgentLoop(
} }
}, },
abortController abortController
), );
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('模型响应超时(2分钟无数据)')), STREAM_TIMEOUT)
)
]);
} catch (err) { } catch (err) {
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
logInfo('流式调用已中止'); logInfo('流式调用已中止');
@@ -592,16 +579,13 @@ export async function runAgentLoop(
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
if ((err as Error).message.includes('超时')) { logError('流式调用异常', (err as Error).message);
logError('流式调用超时', (err as Error).message);
if (content || thinking) { if (content || thinking) {
messages.push({ role: 'assistant', content: content || '(模型响应超时', ...(thinking && { thinking }) }); messages.push({ role: 'assistant', content: content || '(模型响应异常', ...(thinking && { thinking }) });
} }
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats()); callbacks.onDone(content || '(模型响应异常,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
throw err;
}
// 提取 ReAct 思考过程 // 提取 ReAct 思考过程
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i); const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
@@ -781,24 +765,13 @@ export async function runAgentLoop(
let lastError = ''; let lastError = '';
for (let retry = 0; retry <= MAX_RETRIES; retry++) { for (let retry = 0; retry <= MAX_RETRIES; retry++) {
try { try {
let result: ToolResult; const result = await executeTool(call.function.name, call.function.arguments)
if (call.function.name === 'run_command') { .catch(err => ({ success: false, error: err?.message || String(err) }) as ToolResult);
result = await executeTool(call.function.name, call.function.arguments);
} else {
result = await Promise.race([
executeTool(call.function.name, call.function.arguments).catch(err =>
({ success: false, error: err?.message || String(err) }) as ToolResult
),
new Promise<ToolResult>((_, reject) =>
setTimeout(() => reject(new Error('工具执行超时(30秒)')), TOOL_EXEC_TIMEOUT)
)
]);
}
logToolResult(call.function.name, result.success, result.success ? undefined : result.error); logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
return [{ return [{
name: call.function.name, arguments: call.function.arguments, name: call.function.name, arguments: call.function.arguments,
result, status: result.success ? 'success' as const : 'error' as const, timestamp: Date.now() result, status: result.success ? 'success' as const : 'error' as const, timestamp: Date.now()
}, result.success && call.function.name !== 'run_command' ? cacheKey : null]; }, result.success ? cacheKey : null];
} catch (err) { } catch (err) {
lastError = (err as Error).message; lastError = (err as Error).message;
if (retry < MAX_RETRIES) { if (retry < MAX_RETRIES) {
+5
View File
@@ -185,3 +185,8 @@ export function logSkill(action: string, detail?: string): void { addLog('debug'
export function logRAG(action: string, detail?: string): void { addLog('info', `向量: ${action}`, detail); } export function logRAG(action: string, detail?: string): void { addLog('info', `向量: ${action}`, detail); }
export function logSetting(key: string, value: string): void { addLog('debug', `设置: ${key}`, value); } export function logSetting(key: string, value: string): void { addLog('debug', `设置: ${key}`, value); }
export function logInit(msg: string): void { addLog('info', `初始化: ${msg}`); } export function logInit(msg: string): void { addLog('info', `初始化: ${msg}`); }
/** 获取当前所有日志条目(供导出使用) */
export function getLogs(): LogEntry[] {
return [...logs];
}