v0.11.5: 搜索抓取增强 + 流式进度修复 + 超时/写入容错

feat: 搜索面板新增认证类型 Bearer/Basic,自动抓取条数/类型(顺序/随机)
feat: 随机抓取失败自动从未抓取池补1条,上限移除
fix: write_file 缺 path 时返回引导性报错(含工作空间路径+平台示例)
fix: Ollama tool_calls arguments JSON字符串解析
fix: 流式进度日志不再删除,改为完整时间线记录
fix: 进度日志同步appendChild确保排在工具日志之前
fix: 超时设置面板/主进程 remove max上限
chore: 版本号 0.11.4 → 0.11.5
This commit is contained in:
thzxx
2026-06-13 21:35:22 +08:00
parent 5fdcab444c
commit a336a66516
15 changed files with 207 additions and 67 deletions
+28 -6
View File
@@ -15,7 +15,7 @@ import {
import { searchMemories, buildMemoryContext, markMemoryUsed, isMemoryEnabled } from './memory-manager.js';
import { extractSkillsFromToolRecords, matchSkills, buildSkillContext } from './skill-manager.js';
import { showToast } from '../components/toast.js';
import { logInfo, logWarn, logSuccess, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse, logStreamProgress } from './log-service.js';
import { logInfo, logWarn, logSuccess, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse, logStreamProgress, resetStreamProgress } from './log-service.js';
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
import { generateId } from '../utils/utils.js';
import { buildContext, estimateTokens, shouldAutoCompress, compressWithLLM, AUTO_COMPRESS_THRESHOLD, recordActualTokens } from './context-manager.js';
@@ -720,6 +720,9 @@ Shell: ${osInfo.shell}
let lastContentTime = streamStartTime; // 追踪最后一次收到新内容的时间
const PROGRESS_INTERVAL = 15000; // 每15秒输出一次流式进度
// ── 同步创建本轮的进度条目(appendChild,保证在后续工具日志之前)──
resetStreamProgress();
// ── 独立定时器:即使没有新 chunk 也会持续输出进度 ──
progressTimer = setInterval(() => {
const elapsed = Date.now() - streamStartTime;
@@ -736,6 +739,9 @@ Shell: ${osInfo.shell}
if (idleSec >= 10) {
logStreamProgress(`⚠️ ${idleSec}s 无新内容,当前 ${content.length}`);
}
} else {
// 模型尚未开始输出(可能在思考/加载模型/生成大参数)→ 显示等待状态
logStreamProgress(`⏳ 等待模型响应… ${sec}s`);
}
}, PROGRESS_INTERVAL);
@@ -766,26 +772,42 @@ Shell: ${osInfo.shell}
if (chunk.message?.tool_calls?.length) {
for (const tc of chunk.message.tool_calls) {
if (tc.function?.name) {
// Ollama 可能返回 arguments 为 JSON 字符串,统一转为对象
let parsedArgs: Record<string, unknown> = {};
if (tc.function.arguments) {
if (typeof tc.function.arguments === 'string') {
try { parsedArgs = JSON.parse(tc.function.arguments); } catch { parsedArgs = {}; }
} else if (typeof tc.function.arguments === 'object') {
parsedArgs = tc.function.arguments as Record<string, unknown>;
}
}
// 首次看到工具名 → 提前在工作空间创建"准备中"卡片
// 此时参数还没生成完,但用户可以知道 AI 打算调哪个工具
const isNewTool = !toolCalls.some(existing => existing.function.name === tc.function.name);
if (isNewTool && callbacks.onToolCallPrepare) {
callbacks.onToolCallPrepare({
type: 'function',
function: { name: tc.function.name, arguments: tc.function.arguments || {} }
function: { name: tc.function.name, arguments: parsedArgs }
});
}
toolCalls.push({
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments || {}
arguments: parsedArgs
}
});
} else if (toolCalls.length > 0) {
const last = toolCalls[toolCalls.length - 1];
if (tc.function?.arguments && typeof tc.function.arguments === 'object') {
Object.assign(last.function.arguments, tc.function.arguments);
if (tc.function?.arguments) {
let chunkArgs: Record<string, unknown> | null = null;
if (typeof tc.function.arguments === 'string') {
try { chunkArgs = JSON.parse(tc.function.arguments); } catch { /* ignore */ }
} else if (typeof tc.function.arguments === 'object') {
chunkArgs = tc.function.arguments as Record<string, unknown>;
}
if (chunkArgs) {
Object.assign(last.function.arguments, chunkArgs);
}
}
}
}
+16 -18
View File
@@ -176,25 +176,23 @@ export function logThink(thinking: string): void {
export function logStream(msg: string): void { addLog('stream', msg); }
/** 更新流式进度日志(原地更新,不追加新条目 */
let _progressCreated = false;
/** 本轮流式开始:同步创建新进度条目到日志底部(不删旧,日志就是完整记录 */
export function resetStreamProgress(): void {
if (!logBodyEl) return;
const entry = document.createElement('div');
entry.className = 'log-entry log-stream log-stream-progress';
entry.innerHTML = `<span class="log-time">${formatTime(Date.now())}</span><span class="log-icon">📡</span><span class="log-msg">⏳ 等待模型响应… 0s</span>`;
logBodyEl.appendChild(entry);
}
/** 更新**最新**一条流式进度日志(原地更新消息,不改变 DOM 位置和时间戳) */
export function logStreamProgress(msg: string): void {
if (logBodyEl) {
const existing = document.getElementById('log-stream-progress');
if (existing) {
// 已在 DOM 中 → 原地更新
const timeSpan = existing.querySelector('.log-time');
if (timeSpan) timeSpan.textContent = formatTime(Date.now());
const msgSpan = existing.querySelector('.log-msg');
if (msgSpan) msgSpan.textContent = msg;
return;
}
}
// DOM 中不存在,且还没创建过 → addLog(customId 确保渲染后 ID 正确)
// _progressCreated 防止 addLog 在未渲染前被重复调用
if (!_progressCreated) {
_progressCreated = true;
addLog('stream', msg, undefined, undefined, 'log-stream-progress');
// 找最后一条进度日志(当前轮)
const all = logBodyEl?.querySelectorAll?.('.log-stream-progress');
if (all && all.length > 0) {
const latest = all[all.length - 1] as HTMLElement;
const msgSpan = latest.querySelector('.log-msg');
if (msgSpan) msgSpan.textContent = msg;
}
}
+3 -3
View File
@@ -33,12 +33,12 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
type: 'function',
function: {
name: 'write_file',
description: 'Write content to a local file. Creates parent directories automatically. Supports text (utf-8/latin1) and binary (base64) modes. Use mode="append" to add to existing file instead of overwriting. Max 10MB (overwrite) or 5MB (append). Returns file stats including overwrite info.',
description: 'Write content to a local file. Creates parent directories automatically. The "path" parameter is REQUIRED — always specify a file path (relative to workspace or absolute). For example: path="output.txt" writes to workspace. Supports text (utf-8/latin1) and binary (base64). Use mode="append" to add to existing file. Max 10MB overwrite / 5MB append.',
parameters: {
type: 'object',
required: ['path', 'content'],
properties: {
path: { type: 'string', description: 'The file path to write to. Absolute or relative to workspace.' },
path: { type: 'string', description: 'REQUIRED — File path. Relative paths use workspace as base. E.g. "output.txt", "docs/report.md", or absolute "/home/user/file.txt".' },
content: { type: 'string', description: 'The content to write. For binary files, pass base64-encoded string with encoding="base64".' },
encoding: { type: 'string', enum: ['utf-8', 'latin1', 'base64'], description: 'File encoding. Use "base64" to write binary content (images, PDFs, etc). Default: utf-8.' },
mode: { type: 'string', enum: ['overwrite', 'append'], description: 'Write mode. overwrite = replace file, append = add to end. Default: overwrite.' }
@@ -345,7 +345,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
max_results: { type: 'integer', description: 'Maximum number of results to return. Default: 30, max: 30.' },
time_range: { type: 'string', enum: ['day', 'week', 'month', 'year'], description: 'Filter results by time. Supported by Bing and Google. Use for recent news, latest version, etc.' },
enhance_snippets: { type: 'boolean', description: 'Auto-fetch detailed snippet for results with too-short descriptions. Default: true.' },
fetch_top: { type: 'integer', description: 'Number of top results to auto-fetch full content for. Min: 3, max: 10. Results shorter than this are useless — snippets lack detail.' }
fetch_top: { type: 'integer', description: 'Number of top results to auto-fetch full content for. Min: 3. Results shorter than this are useless — snippets lack detail.' }
}
}
}