diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-30 06:00:47 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-30 06:00:47 +0200 |
| commit | e3fb2d434c4f7317944b7c77781599528d766a39 (patch) | |
| tree | 8f9f9c94b19d46f42f8086adb6b68ee07dfce5c6 /modules/core | |
| parent | d570f86177c514ae888fb63fc0bb0da7dd4d5097 (diff) | |
auto: 2026-05-30_06:00 changes
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-aiassist.js | 113 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-system.js | 2 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 22 | ||||
| -rw-r--r-- | modules/core/terminal.js | 65 |
5 files changed, 102 insertions, 102 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 602ae3e..f6fadb5 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -543,42 +543,6 @@ RULES: } return null; } - _parseToolBlocksFromText(text) { - if (!text) return []; - const toolCalls = []; - // Match ```tool name(args) or ```tool\nname(args) blocks - const blockRegex = /```tool(?:\s*\n)?\s*(\w+)\s*({[^}]*})?\s*```/gi; - let match; - while ((match = blockRegex.exec(text)) !== null) { - const name = match[1]; - let args = {}; - try { - if (match[2]) args = JSON.parse(match[2]); - } catch (e) { - // If JSON parse fails, treat as raw string - args = match[2] ? { path: match[2].replace(/[{}]/g,'').trim() } : {}; - } - toolCalls.push({ - id: 'text_' + Date.now() + '_' + toolCalls.length, - type: 'function', - function: { name, arguments: args } - }); - } - // Also handle single-line format: ```tool get_status{} - const simpleRegex = /```tool\s+(\w+)\s*\{([^}]*)\}\s*```/gi; - while ((match = simpleRegex.exec(text)) !== null) { - // Avoid duplicates — skip if already caught by blockRegex - const name = match[1]; - if (!toolCalls.find(tc => tc.function.name === name)) { - toolCalls.push({ - id: 'text_' + Date.now() + '_' + toolCalls.length, - type: 'function', - function: { name, arguments: {} } - }); - } - } - return toolCalls; - } _trackToolFailure(toolName, error) { try { const key = 'webx11_tool_failures'; @@ -611,67 +575,7 @@ RULES: if (this._cancelled) return { finalAnswer: null, toolTurns }; if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; } if (res.message && !res.tool_calls) { - let text = res.message.content || ''; - // Fallback: parse ```tool blocks from text when model writes them instead of native tool_calls - let parsedToolCalls = this._parseToolBlocksFromText(text); - if (parsedToolCalls.length > 0) { - this.terminal.print(`🔧 Parsed ${parsedToolCalls.length} tool call(s) from text block — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info'); - // Extract any non-tool text as pending assistant content - const cleanText = text.replace(/```tool[\s\S]*?```/g, '').trim(); - if (cleanText) messages.push({ role: 'assistant', content: cleanText }); - messages.push({ role: 'assistant', content: '(calling tool via text block)' }); - let pendingText = cleanText || null; - const destructiveTools = parsedToolCalls.filter(tc => this.isDestructive(tc.function.name)); - const safeTools = parsedToolCalls.filter(tc => !this.isDestructive(tc.function.name)); - const safeResults = await Promise.all(safeTools.map(async (tc) => { - const fn = tc.function; - const t0 = Date.now(); - const toolResult = await this.runTool(fn.name, fn.arguments); - const t1 = Math.round((Date.now() - t0) / 1000); - return { fn, toolResult, t1 }; - })); - for (const { fn, toolResult, t1 } of safeResults) { - this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); - const obs = this._summarizeObservation(toolResult, 150); - const err = this._isToolError(toolResult); - if (err) { - this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`); - this._trackToolFailure(fn.name, err); - this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning'); - } else { - this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); - this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info'); - } - messages.push({ role: 'tool', content: toolResult }); - } - if (destructiveTools.length > 0) { - const approved = await this.confirmActionMulti(destructiveTools); - if (approved) { - for (const tc of destructiveTools) { - const fn = tc.function; - const t0 = Date.now(); - this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info'); - const toolResult = await this.runTool(fn.name, fn.arguments); - const t1 = Math.round((Date.now() - t0) / 1000); - const dObs = this._summarizeObservation(toolResult, 150); - const dErr = this._isToolError(toolResult); - if (dErr) { - this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`); - this._trackToolFailure(fn.name, dErr); - this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning'); - } else { - this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`); - this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info'); - } - messages.push({ role: 'tool', content: toolResult }); - } - } else { - messages.push({ role: 'tool', content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied.` }); - } - } - if (pendingText) messages.push({ role: 'assistant', content: pendingText }); - continue; - } + const text = res.message.content || ''; this.terminal.printAIResponse(text || '(no response)'); if (text) this.terminal.ollamaManager.fireResponseHooks(text); this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info'); @@ -734,6 +638,21 @@ RULES: }); } } +// Validator: check safe tool results for consistency + if (safeResults.length > 0 && pendingText) { + const om = this.terminal.ollamaManager; + if (om && om.validatorEnabled && om.validatorModel) { + let anyInvalid = false; + for (const { fn, toolResult } of safeResults) { + const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || ''); + if (!valid && !this._cancelled) { + anyInvalid = true; + this.terminal.print('🔍 Validator: ' + fn.name + ' result — AI note may not match (logged)', 'info'); + // Just log it — don't force retry (cloud models get 400 on retry) + } + } + } + } if (pendingText) { messages.push({ role: 'assistant', content: pendingText }); } diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 3a32f38..cc3a79c 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -156,7 +156,7 @@ // Phase 4: Ext modules (filtered) if (typeof webx11._loadExtModules === 'function') { - const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip']; + const extKeys = ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'tutorial']; const extSelected = selected ? extKeys.filter(k => isSelected(k + '.js', selected)) : extKeys; diff --git a/modules/core/module-system.js b/modules/core/module-system.js index b561ef2..29a9a87 100644 --- a/modules/core/module-system.js +++ b/modules/core/module-system.js @@ -207,7 +207,7 @@ ai: ['ai','ask','ai-clear','ai-system','ai-context','ai-allow','ai-deny','ai-stop','ai-model'], search: ['search','s','ddg','duck','fetch','web','extract'], tools: ['calc','cron-every','cron-list','cron-stop','cron-clear','wpm','mindmap','theme','voice','alias','unalias', - 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand'], + 'which','env','date','cal','yes','seq','base64','printf','cut','uniq','hostname','whoami','basename','dirname','sleep','expand','tutorial'], system: ['help','clear','cls','echo','mem','status','font-size','setfont','smart-prompt','logs','preview','factory-reset','reset-vfs','export','unset','script','set','test'], net: ['server','servers','server-check','server-remove','monitor'], mods: ['modules','listmods','loadmod','removemod','load-local','loadlocal','startup-modules'], diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 34ece29..420cbc3 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -927,6 +927,28 @@ class OllamaManager { this.saveSettings(); } + async validateToolResult(toolName, args, result, aiAnswer) { + if (!this.validatorEnabled || !this.validatorModel) return true; + const endpoint = this.validatorEndpoint || this.endpoint; + const prompt = 'Tool called: ' + toolName + '(' + JSON.stringify(args) + ')\n' + + 'Tool returned: ' + ((result || '').substring(0, 500)) + '\n' + + 'AI answered: ' + ((aiAnswer || '').substring(0, 500)) + '\n' + + 'QUESTION: Is the AI answer consistent with the tool result?\nAnswer only YES or NO.'; + try { + const res = await fetch(endpoint + '/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: this.validatorModel, messages: [{ role: 'user', content: prompt }], stream: false }) + }); + const data = await res.json(); + const answer = (data.message?.content || '').trim().toUpperCase(); + return answer === 'YES'; + } catch (e) { + if (this.terminal) this.terminal.print('⚠️ Validator error: ' + e.message, 'warning'); + return true; + } + } + getModels() { return this.models; } addResponseHook(fn) { this.responseHooks.push(fn); } diff --git a/modules/core/terminal.js b/modules/core/terminal.js index 5e6db6a..de2f4a7 100644 --- a/modules/core/terminal.js +++ b/modules/core/terminal.js @@ -211,6 +211,7 @@ this.suggestions = []; this._isCdAutocomplete = false; this._autocompletePrefix = ''; + this._welcomeShown = false; this.initEventListeners(); this.updateNetworkStatus(); this.loadBuiltInModules(); @@ -226,6 +227,8 @@ soundBtn.textContent = localStorage.getItem('webx11_config_ai_sound') === 'off' ? '🔇 Muted' : '🔊 Sound'; } setTimeout(() => this.inputField.focus(), 100); + // First-run welcome check — fires after ext modules are loaded + setTimeout(() => this._checkFirstRun(), 2000); } initEventListeners() { this.inputField.addEventListener('keydown', (e) => { @@ -943,7 +946,7 @@ catch (e) { console.warn(`[SafeRegister] Skipped ${label}: ${e.message}`); } } async _loadExtModules(filterKeys) { - const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell']; + const autoLoadKeys = filterKeys || ['calc', 'voice', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial']; const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14'; try { const res = await fetch('packages.json?v=' + cacheVer); @@ -1048,11 +1051,34 @@ for (const modName in this.moduleManager.modules) { const mod = this.moduleManager.modules[modName]; if (mod.commands && mod.commands[cmd]) { - try { await mod.commands[cmd](args); return; } + try { await mod.commands[cmd](args); + // Notify tutorial module for quest tracking + if (this.tutorialModule && cmd !== 'tutorial') this.tutorialModule.onCommandExecuted(cmd, args); + return; } catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; } } } - // No AI routing here — security manager handles that via checkAndRun + // Enhanced: check fuzzy suggestions before showing "not found" + const suggestion = this.security.suggest(cmd); + if (suggestion && suggestion.dist <= 2) { + this.print(`Command not found: ${cmd}`, 'error'); + this.print(`Did you mean: ${suggestion.cmd}? (yes) (no)`, 'info'); + // Render clickable yes/no + const out = this.outputContainer; + if (out) { + const yesSpan = document.createElement('span'); + yesSpan.textContent = ' [yes] '; + yesSpan.style.cssText = 'color:var(--success-color);cursor:pointer;text-decoration:underline;margin-right:12px'; + yesSpan.onclick = () => this.executeCommand(suggestion.cmd + (args.length ? ' ' + args.join(' ') : '')); + const noSpan = document.createElement('span'); + noSpan.textContent = ' [no] '; + noSpan.style.cssText = 'color:var(--error-color);cursor:pointer;text-decoration:underline'; + noSpan.onclick = () => { /* do nothing — user declined */ }; + const line = out.lastElementChild; + if (line) { line.appendChild(yesSpan); line.appendChild(noSpan); } + } + return; + } this.print(`Command not found: ${cmd}`, 'error'); } openFileBrowser() { @@ -1675,6 +1701,39 @@ } } catch (e) {} } + // ── First-run welcome ── + _checkFirstRun() { + if (localStorage.getItem('webx11_tutorial_complete') || localStorage.getItem('webx11_tutorial_skipped')) return; + // Check if tutorial module is loaded — retry if ext modules not ready yet + const tutMod = this.moduleManager.modules['TutorialModule']; + if (!tutMod) { + // Ext modules may still be loading; retry in 2s + if (!this._welcomeRetry) { + this._welcomeRetry = true; + setTimeout(() => this._checkFirstRun(), 2000); + } + return; + } + this.tutorialModule = tutMod; + // Only show welcome if user hasn't explicitly dismissed it + if (!localStorage.getItem('webx11_welcome_dismissed')) { + this.showWelcomeModal(); + } + } + showWelcomeModal() { + const modal = document.getElementById('welcome-modal'); + if (modal) { + modal.style.display = 'flex'; + this._welcomeShown = true; + } + } + dismissWelcome() { + const modal = document.getElementById('welcome-modal'); + if (modal) modal.style.display = 'none'; + localStorage.setItem('webx11_welcome_dismissed', '1'); + // Also mark tutorial as skipped so it doesn't reappear + if (this.tutorialModule) this.tutorialModule.skipTutorial(); + } log(level, source, message) { const d = new Date(); const ts = d.toISOString().split('T')[1].split('.')[0]; |