v4.1.6: SMILES 구조식 위젯 교과서식 개편 + 화학표기 규칙 추가 + 코드블록 렌더링 버그 수정

- SMILES 위젯을 버튼 클릭식 큰 위젯 → 자동렌더링 소형 인라인(교과서식)으로 전면 개편
- 링크/이미지/볼드 등 후속 정규식이 SMILES 입체화학 표기([C@@H](C) 등)를 마크다운 링크로
  오인해 구조식이 깨지던 버그 수정 (코드블록 결과물을 플레이스홀더로 보호 후 최종 복원)
- 구조 복잡도(중원자 수)에 따라 SMILES 캔버스 크기 동적 조절 (벤젠은 작게, 콜레스테롤 등은 크게)
- SMILES 위젯 테두리/배경/여백 제거로 더 가벼운 인라인 표시
- 코드블록(.md-code-block) 검은 배경 제거 + 고정폭 폰트 누락으로 ASCII 다이어그램이
  틀어지던 버그 수정 (font-family 미지정 → 본문 가변폭 폰트 상속이 원인)
- code.trim()이 전체 문자열 기준으로만 동작해 ASCII 아트 첫 줄의 들여쓰기만 사라지던
  버그 수정 (줄 단위가 아닌 blank 줄만 제거하도록 변경)
- 시스템 프롬프트에 CHEMISTRY NOTATION 규칙 추가 (ASCII 아트 금지, LaTeX/mhchem·SMILES 사용 유도)
- Ollama Cloud 일시적 오류(5xx/타임아웃) 발생 시 즉시 실패 대신 백오프 재시도 추가
- weather_map_screenshot 툴을 ToolRegistry에도 등록 (buildImageMarkdown export 추가)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-07-14 15:18:23 +09:00
co-authored by Claude Sonnet 5
parent c8b4e251bf
commit 2f690fd999
6 changed files with 100 additions and 49 deletions
+18
View File
@@ -5785,6 +5785,7 @@ async function handleChat(
content: isTranslateSession ? `You are a medical translator. Translate the given text into natural Korean, preserving paragraph structure and markdown formatting (##, ###, **bold**, bullet lists). Output ONLY the translation — no commentary, no tool calls, no explanations.` : isProjSession ? `You are a project file designer. Output ONLY the project-files JSON block as instructed. No tool calls. No extra text.` : `${executionModeSystemBlock ? `${executionModeSystemBlock}\n\n` : ''}You are SmallClaw 🦞, a local AI assistant.\nCurrent date: ${dateStr}, ${timeStr}.\nNever search for or link SmallClaw repos unless the user is asking about SmallClaw itself.\nThis app runs on the user's own machine — browser/desktop automation requests are pre-authorized.\nKeep responses SHORT (1-2 sentences). Don't think out loud. Act and report. Greet naturally without tools.
ANTI-HALLUCINATION: When a tool returns a result, report EXACTLY what the tool returned — never contradict or ignore tool output. If a tool says "(no rows)", say so. Never invent data, file contents, table names, or command output. If you don't know something, call a tool to find out or say you don't know.
IMAGE EDITING RULE: NEVER call image_edit (or any editing tool) when a user uploads a photo without explicitly requesting edits. Uploading a photo is NOT a request to edit it. Only call image_edit when the user's message explicitly asks for an edit (e.g. "수채화로 바꿔줘", "회전해줘"). Violating this rule is a critical error.${browserRuleBlock}
CHEMISTRY NOTATION: NEVER draw molecular structures as ASCII art (H/C/#/=/\\/| characters arranged to look like a diagram) — it always renders as garbled, misaligned text. Instead: for a formula or reaction, use LaTeX inside $...$ (e.g. $\\ce{C4H10}$, $\\ce{2H2 + O2 -> 2H2O}$ — mhchem extension is loaded). For an actual 2D structure with real bond lines (rings, branches), output a \`\`\`smiles\`\`\` code block containing the SMILES string (e.g. \`\`\`smiles\\nc1ccccc1\\n\`\`\` for benzene, \`\`\`smiles\\nCC(=O)Oc1ccccc1C(=O)O\\n\`\`\` for aspirin) — the client automatically renders it as a proper 2D diagram with bond lines.
CODE OUTPUT: When writing code in a fenced code block, start with a filename comment on line 1: \`# filename: snake_game.py\` (Python), \`// filename: app.js\` (JS/C), \`<!-- filename: index.html -->\` (HTML). Never repeat code already written in this conversation. For modifications to existing files, use coder_overwrite_lines or coder_insert_lines (not coder_write_file — it only works for NEW files). All code changes are presented as diffs for the user to review before being applied. Write code directly — do not ask for permission.
PACKAGE INSTALL: NEVER run pip install, npm install, apt-get, or any package installation command. If a package is missing, just write the code and mention the package name in a comment — let the user decide whether to install it. Do NOT attempt to install packages yourself.
OUTPUT FORMAT: When presenting 3+ items (news articles, emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph. Example: news → table with columns 제목|요약|출처.${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsManager.buildPromptContextForUser(username ? getUserWorkspace(username) : null, 16000, message)}`,
@@ -6767,6 +6768,16 @@ RULES:
sendSSE('info', { message: 'Thinking...' });
// Transient cloud-provider errors (Ollama Cloud 500s, timeouts, connection resets) are
// currently surfaced straight to the user as `Error: ...` with zero retry — a brief
// provider-side blip becomes the entire assistant reply. Retry a couple of times with a
// short backoff before giving up; a separate counter from MAX_TOOL_ROUNDS so retries
// don't eat into the model's legitimate tool-calling budget.
let transientErrorRetryCount = 0;
const MAX_TRANSIENT_ERROR_RETRIES = 2;
const isTransientProviderError = (msg: string): boolean =>
/internal server error|bad gateway|service unavailable|gateway timeout|\b5\d{2}\b|econnreset|etimedout|socket hang up|fetch failed/i.test(String(msg || ''));
for (let round = 0; ; round++) {
if (round >= MAX_TOOL_ROUNDS) {
const allowExtendedFileOpLoop =
@@ -7184,6 +7195,13 @@ RULES:
}
} catch (err: any) {
console.error('[v2] Chat error:', err.message);
if (isTransientProviderError(err.message) && transientErrorRetryCount < MAX_TRANSIENT_ERROR_RETRIES) {
transientErrorRetryCount++;
const backoffMs = 1500 * transientErrorRetryCount;
sendSSE('info', { message: `Provider error, retrying (${transientErrorRetryCount}/${MAX_TRANSIENT_ERROR_RETRIES})...` });
await new Promise(r => setTimeout(r, backoffMs));
continue;
}
return { type: 'chat', text: `Error: ${err.message}` };
}
+1 -1
View File
@@ -30,7 +30,7 @@ function runPython(script: string, timeoutMs = 60_000): Promise<any> {
});
}
function buildImageMarkdown(filePath: string, workspacePath: string): string {
export function buildImageMarkdown(filePath: string, workspacePath: string): string {
const relPath = path.relative(workspacePath, filePath).replace(/\\/g, '/');
const baseName = path.basename(filePath);
return `![${baseName}](/api/files/${relPath})`;
+2 -1
View File
@@ -20,7 +20,7 @@ import { imageReadTool, imagePreviewTool, imageInfoTool, imageEditTool } from '.
import { audioTranscribeTool } from './audio-transcribe.js';
import { pythonEvalTool } from './python.js';
import { sqliteTool } from './sqlite.js';
import { weatherSearchTool, weatherAirPollutionTool, weatherOpenMeteoTool, weatherKmaTool, weatherAirKoreaTool, weatherNasaPowerTool, weatherEra5Tool, weatherCdsTool, weatherCmip6Tool } from './weather.js';
import { weatherSearchTool, weatherAirPollutionTool, weatherOpenMeteoTool, weatherKmaTool, weatherAirKoreaTool, weatherNasaPowerTool, weatherEra5Tool, weatherCdsTool, weatherCmip6Tool, weatherMapScreenshotTool } from './weather.js';
import { emailListTool, emailReadTool, emailSendTool, emailSearchTool, emailDeleteTool } from './email.js';
import { kakaoSendTool } from './kakao.js';
import { koreanLawSearchTool, koreanLawFetchTool, usCaseSearchTool } from './legal.js';
@@ -217,6 +217,7 @@ class ToolRegistry {
this.registerSafe(weatherEra5Tool);
this.registerSafe(weatherCdsTool);
this.registerSafe(weatherCmip6Tool);
this.registerSafe(weatherMapScreenshotTool);
// Email tools (IMAP + SMTP)
this.registerSafe(emailListTool);
this.registerSafe(emailReadTool);