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:
@@ -176,6 +176,15 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi
|
||||
|
||||
## 构建日志
|
||||
|
||||
### v5.1.1(2026-04-19) — metona-ollama-desktop 仓库
|
||||
|
||||
- 🔧 搜索引擎:DuckDuckGo → 百度(Baidu),解决国内不可达问题
|
||||
- 🔧 移除全部超时限制:Agent Loop、流式响应、工具执行、web_fetch、download_file
|
||||
- 🔧 Agent 最大轮数默认 85 轮,设置面板可配置
|
||||
- ✨ 执行日志导出功能(日志面板新增导出按钮)
|
||||
- 🔧 TOOL_USAGE_GUIDE 新增禁止猜 URL 和中文搜索结果利用规则
|
||||
- 📝 版本号更新至 5.1.1
|
||||
|
||||
### v5.1.0(2026-04-18) — metona-ollama-desktop 仓库
|
||||
|
||||
- ✨ 新增 4 个工具:memory_replace、memory_remove、skill_list、skill_view(总数 38 个)
|
||||
|
||||
@@ -11,6 +11,34 @@
|
||||
|
||||
## 桌面版(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 系统增强
|
||||
|
||||
- ✨ **记忆 replace/remove 操作**(借鉴 Hermes Agent)
|
||||
|
||||
+37
-31
@@ -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',
|
||||
'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'
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
}
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
@@ -490,7 +489,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
||||
|
||||
/**
|
||||
* Web Search — 联网搜索
|
||||
* 使用 DuckDuckGo HTML 搜索并提取结果
|
||||
* 使用百度搜索(国内可用)+ Bing 作为备用
|
||||
*/
|
||||
export async function handleWebSearch(params: { query: string; max_results?: number }): Promise<ToolResult> {
|
||||
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'
|
||||
};
|
||||
|
||||
// 1. 尝试 DuckDuckGo HTML 搜索(8 秒超时,国内环境通常不可用)
|
||||
// 1. 百度搜索(国内首选)
|
||||
let results: Array<{ title: string; url: string; snippet: string }> = [];
|
||||
|
||||
try {
|
||||
const ddgUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
||||
const resp = await fetch(ddgUrl, {
|
||||
const baiduUrl = `https://www.baidu.com/s?wd=${encodeURIComponent(query)}&rn=${maxResults}`;
|
||||
const resp = await fetch(baiduUrl, {
|
||||
headers: searchHeaders,
|
||||
signal: AbortSignal.timeout(8000)
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
const html = await resp.text();
|
||||
results = parseDDGResults(html, maxResults);
|
||||
results = parseBaiduResults(html, maxResults);
|
||||
}
|
||||
} 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) {
|
||||
try {
|
||||
const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
|
||||
const resp = await fetch(bingUrl, {
|
||||
headers: searchHeaders,
|
||||
signal: AbortSignal.timeout(15000)
|
||||
});
|
||||
|
||||
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) {
|
||||
try {
|
||||
const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`;
|
||||
const resp = await fetch(googleUrl, {
|
||||
headers: searchHeaders,
|
||||
signal: AbortSignal.timeout(15000)
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
@@ -586,27 +582,39 @@ export async function handleWebSearch(params: { query: string; max_results?: num
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 DuckDuckGo HTML 搜索结果 */
|
||||
function parseDDGResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> {
|
||||
/** 解析百度 HTML 搜索结果 */
|
||||
function parseBaiduResults(html: string, maxResults: number): 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>
|
||||
const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]*class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
// 百度结果块:<div class="result" ...> 或 <div class="result-op" ...>
|
||||
// 标题:<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 ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
|
||||
let url = match[1].trim();
|
||||
// DuckDuckGo 使用重定向链接,提取真实 URL
|
||||
const uddgMatch = url.match(/[?&]uddg=([^&]+)/);
|
||||
if (uddgMatch) {
|
||||
url = decodeURIComponent(uddgMatch[1]);
|
||||
while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) {
|
||||
const block = blockMatch[1];
|
||||
|
||||
// 提取标题和 URL
|
||||
const titleMatch = block.match(/<h3[^>]*class="[^"]*t[^"]*"[^>]*>[\s\S]*?<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/);
|
||||
if (!titleMatch) continue;
|
||||
|
||||
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')) {
|
||||
results.push({ title, url, snippet });
|
||||
if (title) {
|
||||
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}`);
|
||||
const resp = await fetch(params.url, {
|
||||
signal: AbortSignal.timeout(60000)
|
||||
});
|
||||
const resp = await fetch(params.url);
|
||||
if (!resp.ok) return { success: false, error: `HTTP ${resp.status}: ${resp.statusText}` };
|
||||
|
||||
const buf = Buffer.from(await resp.arrayBuffer());
|
||||
|
||||
@@ -6,7 +6,7 @@ import { state, KEYS } from '../state/state.js';
|
||||
import { debounce } from '../utils/utils.js';
|
||||
import { encryptData, decryptData } from '../services/crypto.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 { loadModels } from './model-bar.js';
|
||||
import { showToast } from './toast.js';
|
||||
@@ -73,6 +73,20 @@ export function initSettingsModal(): void {
|
||||
}, 300);
|
||||
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 人格模式切换 ──
|
||||
const PERSONALITY_TEMPERATURES: Record<string, number> = {
|
||||
default: -1, // 使用用户设置
|
||||
@@ -378,6 +392,57 @@ export function closeSettingsModal(): void {
|
||||
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> {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (!db) return;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<aside class="log-panel" id="logPanel">
|
||||
<div class="log-panel-header">
|
||||
<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 class="log-panel-body" id="logPanelBody"></div>
|
||||
</aside>
|
||||
@@ -259,6 +260,12 @@
|
||||
<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">
|
||||
<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 class="setting-group">
|
||||
<label class="setting-label">🎭 人格模式</label>
|
||||
|
||||
@@ -353,6 +353,11 @@ async function init(): Promise<void> {
|
||||
(document.querySelector('#inputTemperature') as HTMLInputElement).value = String(temperature);
|
||||
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 设置 ──
|
||||
const toolCallingEnabled = await db.getSetting('toolCallingEnabled', false);
|
||||
const runCommandMode = await db.getSetting('runCommandMode', 'confirm') as 'auto' | 'confirm' | 'disabled';
|
||||
|
||||
@@ -28,9 +28,6 @@ import type {
|
||||
TraceEntry
|
||||
} from '../types.js';
|
||||
|
||||
const MAX_LOOP_TIME = 600000; // 10 分钟总超时
|
||||
const TOOL_EXEC_TIMEOUT = 30000; // 工具执行超时 30 秒
|
||||
const STREAM_TIMEOUT = 120000; // 单次流式调用超时 2 分钟
|
||||
const MAX_RETRIES = 2; // 工具错误自动重试次数
|
||||
|
||||
/** 每个工具返回给模型的最大字符数 */
|
||||
@@ -89,7 +86,9 @@ const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】
|
||||
9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
|
||||
10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。
|
||||
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([
|
||||
@@ -488,8 +487,8 @@ export async function runAgentLoop(
|
||||
|
||||
logInfo(`ReAct Agent Loop 启动: ${model}`, `工具: ${useTools ? '开启' : '关闭'}, 记忆: ${isMemoryEnabled() ? '开启' : '关闭'}, tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))}`);
|
||||
|
||||
// 迭代预算:从 state 读取,默认 15
|
||||
const maxLoops = state.get<number>('maxTurns', 15);
|
||||
// 迭代预算:从 state 读取,默认 85
|
||||
const maxLoops = state.get<number>('maxTurns', 85);
|
||||
|
||||
let loopCount = 0;
|
||||
const allToolRecords: ToolCallRecord[] = [];
|
||||
@@ -509,13 +508,6 @@ export async function runAgentLoop(
|
||||
while (loopCount < maxLoops) {
|
||||
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) {
|
||||
logInfo('ReAct Agent Loop 已中止');
|
||||
@@ -534,9 +526,8 @@ export async function runAgentLoop(
|
||||
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
||||
|
||||
try {
|
||||
// 带超时保护的流式调用
|
||||
await Promise.race([
|
||||
api.chatStream(
|
||||
// 流式调用(无超时限制)
|
||||
await api.chatStream(
|
||||
{
|
||||
model,
|
||||
messages,
|
||||
@@ -578,11 +569,7 @@ export async function runAgentLoop(
|
||||
}
|
||||
},
|
||||
abortController
|
||||
),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('模型响应超时(2分钟无数据)')), STREAM_TIMEOUT)
|
||||
)
|
||||
]);
|
||||
);
|
||||
} catch (err) {
|
||||
if (abortController.signal.aborted) {
|
||||
logInfo('流式调用已中止');
|
||||
@@ -592,16 +579,13 @@ export async function runAgentLoop(
|
||||
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
|
||||
return;
|
||||
}
|
||||
if ((err as Error).message.includes('超时')) {
|
||||
logError('流式调用超时', (err as Error).message);
|
||||
logError('流式调用异常', (err as Error).message);
|
||||
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;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 提取 ReAct 思考过程
|
||||
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
|
||||
@@ -781,24 +765,13 @@ export async function runAgentLoop(
|
||||
let lastError = '';
|
||||
for (let retry = 0; retry <= MAX_RETRIES; retry++) {
|
||||
try {
|
||||
let result: ToolResult;
|
||||
if (call.function.name === 'run_command') {
|
||||
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)
|
||||
)
|
||||
]);
|
||||
}
|
||||
const result = await executeTool(call.function.name, call.function.arguments)
|
||||
.catch(err => ({ success: false, error: err?.message || String(err) }) as ToolResult);
|
||||
logToolResult(call.function.name, result.success, result.success ? undefined : result.error);
|
||||
return [{
|
||||
name: call.function.name, arguments: call.function.arguments,
|
||||
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) {
|
||||
lastError = (err as Error).message;
|
||||
if (retry < MAX_RETRIES) {
|
||||
|
||||
@@ -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 logSetting(key: string, value: string): void { addLog('debug', `设置: ${key}`, value); }
|
||||
export function logInit(msg: string): void { addLog('info', `初始化: ${msg}`); }
|
||||
|
||||
/** 获取当前所有日志条目(供导出使用) */
|
||||
export function getLogs(): LogEntry[] {
|
||||
return [...logs];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user