From 7221d36babccc3035e4cc56ab23712d3db07c723 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 17 Apr 2026 11:17:27 +0800 Subject: [PATCH] fix: mysql migration compatibility + ollama tool-support gating + actionable error UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - db/migration/mysql: replace ADD COLUMN/CREATE INDEX IF NOT EXISTS with idempotent checks via information_schema (MySQL 8.0 <8.0.29 and some forks don't support IF NOT EXISTS for ADD COLUMN). Affects V2/V4/V5/V7 /V8/V9/V11/V12/V13/V14. Fix: gitee#IIYHLJ. - application-mysql.yml: add createDatabaseIfNotExist=true so MySQL Connector/J auto-creates the schema on first connection (requires CREATE privilege — documented fallback for restricted accounts). - llm/OllamaAutoDiscoveryRunner: rewrite seed tag when fuzzy-matching, prefer exact tag for default; skip models without tool support when auto-activating a default (prevents the phantom ':latest' trap when users pulled a specific size). - agent/graph/NodeStreamingChatHelper: detect 'does not support tools' and 'model not found' errors from Ollama and emit actionable Chinese prompts guiding users to qwen3 / qwen2.5:7b+ / llama3.1:8b+ etc. - ui/MessageBubble + types/chatError: surface the backend's actionable rawMessage in the failed-message card instead of a generic '未知错误'; strip redundant prefixes (Bad request: / [错误] / LLM 调用失败:) since the title already conveys the category. --- .../agent/graph/NodeStreamingChatHelper.java | 36 +++++++- .../llm/config/OllamaAutoDiscoveryRunner.java | 91 +++++++++++++++++-- .../src/main/resources/application-mysql.yml | 7 +- .../migration/mysql/V11__skill_synthesis.sql | 10 +- .../migration/mysql/V12__wiki_chunk_table.sql | 16 +++- .../mysql/V13__wiki_chunk_embedding.sql | 10 +- .../mysql/V14__embedding_model_config.sql | 10 +- .../mysql/V2__workspace_base_path.sql | 65 ++++++++++--- .../mysql/V4__agent_thinking_level.sql | 5 +- .../mysql/V5__conversation_parent.sql | 10 +- .../mysql/V7__wiki_last_processed_hash.sql | 5 +- .../migration/mysql/V8__wiki_raw_progress.sql | 15 ++- .../mysql/V9__usage_cache_tokens.sql | 10 +- .../src/components/chat/MessageBubble.vue | 12 +++ mateclaw-ui/src/types/chatError.ts | 15 ++- 15 files changed, 268 insertions(+), 49 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java index 3dac6583..f06b29db 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/NodeStreamingChatHelper.java @@ -6,6 +6,7 @@ import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.web.reactive.function.client.WebClientResponseException; import vip.mate.channel.web.ChatStreamTracker; import reactor.core.Disposable; @@ -657,12 +658,41 @@ public class NodeStreamingChatHelper { /** 从异常链提取用户友好的错误信息 */ private static String extractUserFriendlyError(Throwable error) { String msg = error.getMessage(); - if (msg == null) return error.getClass().getSimpleName(); + if (msg == null) msg = error.getClass().getSimpleName(); + + // 若是 WebClientResponseException,先把 response body 也并入判定样本, + // 因为 Ollama 的 "does not support tools" 错误只在 body 里,不在 status line 里。 + String bodySample = ""; + Throwable cursor = error; + for (int i = 0; cursor != null && i < 5; i++, cursor = cursor.getCause()) { + if (cursor instanceof WebClientResponseException wre) { + try { + String body = wre.getResponseBodyAsString(); + if (body != null && !body.isEmpty()) { + bodySample = body.length() > 512 ? body.substring(0, 512) : body; + } + } catch (Exception ignored) { + } + break; + } + } + String combined = msg + " " + bodySample; + + // ↓↓↓ 具体错误翻译(优先级由高到低)↓↓↓ + + // Ollama / 其他 provider 在模型不支持 function calling 时返回此文案: + // " does not support tools" + // 这不是模型坏,而是用户选错了模型 —— 给出可操作的切换建议。 + if (bodySample.contains("does not support tools") || msg.contains("does not support tools")) { + return "当前模型不支持工具调用(function calling)。请在 设置 → 模型 里切换到支持 tools 的模型," + + "例如 qwen3、qwen2.5:7b+、llama3.1:8b+、mistral-nemo、command-r 等。"; + } + // DashScope "url error" is really "model name not mapped to any valid endpoint". - // Translate it so users see the real cause and the actionable next step. if (msg.contains("url error") || msg.contains("[InvalidParameter]") || msg.contains("Model not exist") || msg.contains("model_not_found") - || msg.contains("Model not found")) { + || msg.contains("Model not found") + || combined.contains("model not found") || combined.contains("not_found_error")) { return "Model name not available on this provider — verify the model exists and is supported (Settings → Models)"; } // 对 Jackson 反序列化错误,提取关键信息 diff --git a/mateclaw-server/src/main/java/vip/mate/llm/config/OllamaAutoDiscoveryRunner.java b/mateclaw-server/src/main/java/vip/mate/llm/config/OllamaAutoDiscoveryRunner.java index 8c3fa15e..8534cc2c 100644 --- a/mateclaw-server/src/main/java/vip/mate/llm/config/OllamaAutoDiscoveryRunner.java +++ b/mateclaw-server/src/main/java/vip/mate/llm/config/OllamaAutoDiscoveryRunner.java @@ -37,6 +37,26 @@ public class OllamaAutoDiscoveryRunner implements ApplicationRunner { private static final String OLLAMA_PROVIDER_ID = "ollama"; + /** + * Ollama 模型族里**已知不支持 function calling / tools** 的家族名(冒号前半段)。 + *

+ * Agent 默认带 24+ 个 tool callbacks 发请求,这类模型会返回 + * {@code 400 " does not support tools"}。把它们从 auto-activate 默认候选里排除, + * 仍然允许 enabled(用户可以手工选它做纯文本问答),但不会被意外设为默认。 + *

+ * 参考:https://ollama.com/search?c=tools (标了 "Tools" 标签的才支持)。 + * 清单随 Ollama 生态演进,必要时手工维护。 + */ + private static final Set KNOWN_NO_TOOL_FAMILIES = Set.of( + "deepseek-r1", // reasoning model, no tool calling + "gemma", "gemma2", "gemma3", + "phi3", "phi4", + "codellama", + "llama3.2", // 1b/3b variants lack tools(llama3.1 / llama3.3 支持) + "qwen2", // 旧 qwen2 无 tool 支持(qwen2.5 / qwen3 支持) + "mistral" // 原始 mistral 无 tools(mistral-nemo / mixtral 支持) + ); + private final ModelDiscoveryService modelDiscoveryService; private final ModelConfigService modelConfigService; private final ModelProviderService modelProviderService; @@ -77,23 +97,40 @@ public class OllamaAutoDiscoveryRunner implements ApplicationRunner { } String modelTag = model.getModelName(); String modelBase = modelTag.contains(":") ? modelTag.substring(0, modelTag.indexOf(":")) : modelTag; - boolean matches = discoveredIds.contains(modelTag) - || discoveredIds.contains(modelBase) - || discoveredIds.stream().anyMatch(id -> id.startsWith(modelBase + ":")); - if (matches) { + + // 精确匹配 / 裸名匹配 —— Ollama 能按原 tag 调用,model_name 无需改写 + if (discoveredIds.contains(modelTag) || discoveredIds.contains(modelBase)) { model.setEnabled(true); modelConfigService.updateModel(model); log.info("Ollama: auto-enabled model '{}'", modelTag); + continue; + } + + // Fuzzy 前缀匹配:种子里的 tag(例如 deepseek-r1:latest)Ollama 没装, + // 但 base 相同的另一个 tag 存在(例如 deepseek-r1:7b)。 + // 这里必须把 model_name 改写成实际存在的 tag,否则 /v1/chat/completions + // 会用原 tag 发请求,Ollama 返回 404 "model not found"。 + String actualTag = discoveredIds.stream() + .filter(id -> id.startsWith(modelBase + ":")) + .findFirst() + .orElse(null); + if (actualTag != null) { + log.info("Ollama: rewriting seed tag '{}' → '{}' to match installed model", + modelTag, actualTag); + model.setModelName(actualTag); + model.setEnabled(true); + modelConfigService.updateModel(model); + log.info("Ollama: auto-enabled model '{}'", actualTag); } } // Auto-activate first Ollama model if no valid default exists - tryAutoActivateOllamaModel(); + tryAutoActivateOllamaModel(discoveredIds); } catch (Exception e) { log.debug("Ollama auto-discovery skipped: {}", e.getMessage()); } } - private void tryAutoActivateOllamaModel() { + private void tryAutoActivateOllamaModel(Set discoveredIds) { List ollamaModels = modelConfigService.listModelsByProvider(OLLAMA_PROVIDER_ID); List enabledModels = ollamaModels.stream() .filter(m -> Boolean.TRUE.equals(m.getEnabled())) @@ -104,14 +141,48 @@ public class OllamaAutoDiscoveryRunner implements ApplicationRunner { boolean hasValidDefault; try { ModelConfigEntity current = modelConfigService.getDefaultModel(); - hasValidDefault = modelProviderService.isProviderAvailable(current.getProvider()); + boolean providerOk = modelProviderService.isProviderAvailable(current.getProvider()); + // 对 Ollama provider 额外要求:当前默认的 tag 必须实际存在于 Ollama, + // 否则视为"历史遗留的无效默认"(如之前 auto-activate 把 :latest 设为默认, + // 但 Ollama 只有 :7b),需要在本轮重新挑一个可用的默认。 + boolean tagOk = !OLLAMA_PROVIDER_ID.equals(current.getProvider()) + || discoveredIds.contains(current.getModelName()); + hasValidDefault = providerOk && tagOk; } catch (Exception e) { hasValidDefault = false; } if (!hasValidDefault) { - ModelConfigEntity first = enabledModels.get(0); - modelConfigService.setDefaultModel(OLLAMA_PROVIDER_ID, first.getModelName()); - log.info("Ollama: auto-activated default model '{}'", first.getModelName()); + // 挑默认的优先级(逐级 fallback): + // 1) tag 实际存在于 Ollama **且** 家族支持 tools(agent 能正常用工具) + // 2) tag 实际存在于 Ollama(哪怕不支持 tools,总比设个根本不存在的 tag 强) + // 3) 兜底:enabledModels 第一条(保留原来的行为) + ModelConfigEntity pick = enabledModels.stream() + .filter(m -> discoveredIds.contains(m.getModelName())) + .filter(m -> supportsTools(m.getModelName())) + .findFirst() + .orElseGet(() -> enabledModels.stream() + .filter(m -> discoveredIds.contains(m.getModelName())) + .findFirst() + .orElse(enabledModels.get(0))); + modelConfigService.setDefaultModel(OLLAMA_PROVIDER_ID, pick.getModelName()); + if (!supportsTools(pick.getModelName())) { + log.warn("Ollama: auto-activated default model '{}' but its family does not support tool calling; " + + "agents that require tools will fail. Consider pulling a tool-capable model " + + "(qwen3 / qwen2.5 / llama3.1:8b+ / mistral-nemo).", pick.getModelName()); + } else { + log.info("Ollama: auto-activated default model '{}'", pick.getModelName()); + } } } + + /** + * 粗粒度判断:以 tag 的冒号前半段(base)比对已知无工具支持家族集合。 + * 只用于 default picking;不影响模型是否被 enabled,用户仍可手工选任何模型。 + */ + private static boolean supportsTools(String modelTag) { + if (modelTag == null || modelTag.isEmpty()) return false; + int idx = modelTag.indexOf(':'); + String base = idx > 0 ? modelTag.substring(0, idx) : modelTag; + return !KNOWN_NO_TOOL_FAMILIES.contains(base); + } } diff --git a/mateclaw-server/src/main/resources/application-mysql.yml b/mateclaw-server/src/main/resources/application-mysql.yml index fa5816ba..9bdd2af4 100644 --- a/mateclaw-server/src/main/resources/application-mysql.yml +++ b/mateclaw-server/src/main/resources/application-mysql.yml @@ -1,6 +1,11 @@ spring: datasource: - url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:mateclaw}?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + # createDatabaseIfNotExist=true — MySQL Connector/J 首次连接时若 DB 不存在则自动 CREATE DATABASE。 + # 本仓库所有建表语句都显式指定 utf8mb4 / utf8mb4_unicode_ci,不依赖 DB 默认字符集,所以即便 + # 自动创建的库用了 server 默认字符集也不会影响数据。要求 DB user 具备 CREATE 权限(默认 root 可)。 + # 如果使用受限账号,请提前手工执行: + # CREATE DATABASE mateclaw CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:mateclaw}?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true driver-class-name: com.mysql.cj.jdbc.Driver username: ${DB_USERNAME:root} password: ${DB_PASSWORD:mateclaw123} diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V11__skill_synthesis.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V11__skill_synthesis.sql index a6e7e58f..bcf89606 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V11__skill_synthesis.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V11__skill_synthesis.sql @@ -1,5 +1,11 @@ -- V11: Auto Skill Synthesis (RFC-023) -- Agent 自治创建 skill 后记录来源对话和安全扫描状态 -ALTER TABLE mate_skill ADD COLUMN IF NOT EXISTS source_conversation_id VARCHAR(64) DEFAULT NULL; -ALTER TABLE mate_skill ADD COLUMN IF NOT EXISTS security_scan_status VARCHAR(16) DEFAULT NULL; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_skill' AND COLUMN_NAME = 'source_conversation_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_skill ADD COLUMN source_conversation_id VARCHAR(64) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + -- security_scan_status: NULL(旧数据/手动创建) / PASSED / FAILED +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_skill' AND COLUMN_NAME = 'security_scan_status'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_skill ADD COLUMN security_scan_status VARCHAR(16) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V12__wiki_chunk_table.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V12__wiki_chunk_table.sql index 0dd23f97..46ee1aed 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V12__wiki_chunk_table.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V12__wiki_chunk_table.sql @@ -13,6 +13,16 @@ CREATE TABLE IF NOT EXISTS mate_wiki_chunk ( update_time DATETIME NOT NULL, deleted INT NOT NULL DEFAULT 0 ); -CREATE INDEX IF NOT EXISTS idx_wiki_chunk_kb ON mate_wiki_chunk(kb_id); -CREATE INDEX IF NOT EXISTS idx_wiki_chunk_raw ON mate_wiki_chunk(raw_id); -CREATE INDEX IF NOT EXISTS idx_wiki_chunk_hash ON mate_wiki_chunk(content_hash); + +-- MySQL lacks `CREATE INDEX IF NOT EXISTS`; use INFORMATION_SCHEMA.STATISTICS guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_chunk' AND INDEX_NAME = 'idx_wiki_chunk_kb'); +SET @s := IF(@c = 0, 'CREATE INDEX idx_wiki_chunk_kb ON mate_wiki_chunk(kb_id)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_chunk' AND INDEX_NAME = 'idx_wiki_chunk_raw'); +SET @s := IF(@c = 0, 'CREATE INDEX idx_wiki_chunk_raw ON mate_wiki_chunk(raw_id)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_chunk' AND INDEX_NAME = 'idx_wiki_chunk_hash'); +SET @s := IF(@c = 0, 'CREATE INDEX idx_wiki_chunk_hash ON mate_wiki_chunk(content_hash)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V13__wiki_chunk_embedding.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V13__wiki_chunk_embedding.sql index 7ea10c8b..270fdb61 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V13__wiki_chunk_embedding.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V13__wiki_chunk_embedding.sql @@ -1,3 +1,9 @@ -- V13: Add embedding column to mate_wiki_chunk (RFC-011 Phase 2) -ALTER TABLE mate_wiki_chunk ADD COLUMN IF NOT EXISTS embedding BLOB DEFAULT NULL; -ALTER TABLE mate_wiki_chunk ADD COLUMN IF NOT EXISTS embedding_model VARCHAR(64) DEFAULT NULL; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_chunk' AND COLUMN_NAME = 'embedding'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_chunk ADD COLUMN embedding BLOB DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_chunk' AND COLUMN_NAME = 'embedding_model'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_chunk ADD COLUMN embedding_model VARCHAR(64) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V14__embedding_model_config.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V14__embedding_model_config.sql index e93c9940..16bd62bf 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V14__embedding_model_config.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V14__embedding_model_config.sql @@ -1,6 +1,12 @@ -- V14: Embedding model UI config (对标 Dify) -ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS model_type VARCHAR(32) DEFAULT 'chat'; -ALTER TABLE mate_wiki_knowledge_base ADD COLUMN IF NOT EXISTS embedding_model_id BIGINT DEFAULT NULL; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_config' AND COLUMN_NAME = 'model_type'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_config ADD COLUMN model_type VARCHAR(32) DEFAULT ''chat''', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_knowledge_base' AND COLUMN_NAME = 'embedding_model_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_knowledge_base ADD COLUMN embedding_model_id BIGINT DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 播种 DashScope embedding(与 chat 模型共享 provider apiKey) INSERT INTO mate_model_config (id, name, provider, model_name, description, temperature, max_tokens, top_p, builtin, enabled, is_default, model_type, create_time, update_time, deleted) diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql index 7e03930b..8730cb80 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V2__workspace_base_path.sql @@ -1,4 +1,8 @@ -- V2: Upgrade schema for databases created before Flyway was introduced. +-- MySQL does NOT support `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` (MariaDB-only). +-- We use INFORMATION_SCHEMA + dynamic SQL as an idempotent replacement so this migration +-- is safe on BOTH: (a) fresh MySQL installs whose V1 baseline already contains the columns, +-- and (b) legacy installs bootstrapped from the old schema.sql that predates those columns. CREATE TABLE IF NOT EXISTS mate_workspace ( id BIGINT NOT NULL PRIMARY KEY, @@ -13,7 +17,11 @@ CREATE TABLE IF NOT EXISTS mate_workspace ( deleted INT NOT NULL DEFAULT 0, UNIQUE KEY uk_workspace_slug (slug) ); -ALTER TABLE mate_workspace ADD COLUMN IF NOT EXISTS base_path VARCHAR(512); + +-- mate_workspace.base_path (legacy upgrade path) +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_workspace' AND COLUMN_NAME = 'base_path'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_workspace ADD COLUMN base_path VARCHAR(512)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS mate_workspace_member ( id BIGINT NOT NULL PRIMARY KEY, @@ -27,12 +35,30 @@ CREATE TABLE IF NOT EXISTS mate_workspace_member ( INDEX idx_ws_member_user (user_id) ); -ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; -ALTER TABLE mate_channel ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; -ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; -ALTER TABLE mate_wiki_knowledge_base ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; -ALTER TABLE mate_tool ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; -ALTER TABLE mate_skill ADD COLUMN IF NOT EXISTS workspace_id BIGINT NOT NULL DEFAULT 1; +-- workspace_id on pre-existing domain tables +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_agent ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_channel' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_channel ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_conversation' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_conversation ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_knowledge_base' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_knowledge_base ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_tool' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_tool ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_skill' AND COLUMN_NAME = 'workspace_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_skill ADD COLUMN workspace_id BIGINT NOT NULL DEFAULT 1', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS mate_workspace_file ( id BIGINT NOT NULL PRIMARY KEY, @@ -77,11 +103,26 @@ CREATE TABLE IF NOT EXISTS mate_audit_event ( INDEX idx_audit_ws_time (workspace_id, create_time) ); -ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS auth_type VARCHAR(16) NOT NULL DEFAULT 'api_key'; -ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_access_token TEXT; -ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_refresh_token TEXT; -ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_expires_at BIGINT; -ALTER TABLE mate_model_provider ADD COLUMN IF NOT EXISTS oauth_account_id VARCHAR(128); +-- model provider OAuth columns +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_provider' AND COLUMN_NAME = 'auth_type'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_provider ADD COLUMN auth_type VARCHAR(16) NOT NULL DEFAULT ''api_key''', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_provider' AND COLUMN_NAME = 'oauth_access_token'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_provider ADD COLUMN oauth_access_token TEXT', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_provider' AND COLUMN_NAME = 'oauth_refresh_token'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_provider ADD COLUMN oauth_refresh_token TEXT', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_provider' AND COLUMN_NAME = 'oauth_expires_at'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_provider ADD COLUMN oauth_expires_at BIGINT', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_model_provider' AND COLUMN_NAME = 'oauth_account_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_model_provider ADD COLUMN oauth_account_id VARCHAR(128)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS mate_agent_skill ( id BIGINT NOT NULL PRIMARY KEY, diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V4__agent_thinking_level.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V4__agent_thinking_level.sql index 4b98c14e..7d462ef5 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V4__agent_thinking_level.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V4__agent_thinking_level.sql @@ -1,3 +1,6 @@ -- V4: Add default_thinking_level to mate_agent -- Supports: off / low / medium / high / max (null = follow model default) -ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS default_thinking_level VARCHAR(32) DEFAULT NULL; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND COLUMN_NAME = 'default_thinking_level'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_agent ADD COLUMN default_thinking_level VARCHAR(32) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V5__conversation_parent.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V5__conversation_parent.sql index 8a0d82a4..18efcae7 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V5__conversation_parent.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V5__conversation_parent.sql @@ -1,3 +1,9 @@ -- V5: Add parent_conversation_id to mate_conversation for multi-agent delegation tracking -ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS parent_conversation_id VARCHAR(64) DEFAULT NULL; -CREATE INDEX idx_conversation_parent ON mate_conversation(parent_conversation_id); +-- MySQL lacks `ADD COLUMN IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS`; use INFORMATION_SCHEMA guards. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_conversation' AND COLUMN_NAME = 'parent_conversation_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_conversation ADD COLUMN parent_conversation_id VARCHAR(64) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_conversation' AND INDEX_NAME = 'idx_conversation_parent'); +SET @s := IF(@c = 0, 'CREATE INDEX idx_conversation_parent ON mate_conversation(parent_conversation_id)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V7__wiki_last_processed_hash.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V7__wiki_last_processed_hash.sql index 05dc0de2..605bb618 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V7__wiki_last_processed_hash.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V7__wiki_last_processed_hash.sql @@ -1,3 +1,6 @@ -- V7: Add last_processed_hash to mate_wiki_raw_material for skip-if-unchanged optimization -- RFC-012 Change 5: when reprocessing, skip LLM pipeline if content_hash == last_processed_hash -ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS last_processed_hash VARCHAR(64) DEFAULT NULL; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_raw_material' AND COLUMN_NAME = 'last_processed_hash'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_raw_material ADD COLUMN last_processed_hash VARCHAR(64) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V8__wiki_raw_progress.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V8__wiki_raw_progress.sql index d5c583aa..f81b5da0 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V8__wiki_raw_progress.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V8__wiki_raw_progress.sql @@ -1,6 +1,15 @@ -- V8: wiki raw material two-phase digest progress fields, for UI progress bar -- RFC-012 M2 v2 UI follow-up: expose per-raw progress (current phase + pages done / total planned) -- so the frontend can render a determinate progress bar instead of an opaque "处理中" badge. -ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_phase VARCHAR(32) DEFAULT NULL; -ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_total INT DEFAULT 0; -ALTER TABLE mate_wiki_raw_material ADD COLUMN IF NOT EXISTS progress_done INT DEFAULT 0; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_raw_material' AND COLUMN_NAME = 'progress_phase'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_raw_material ADD COLUMN progress_phase VARCHAR(32) DEFAULT NULL', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_raw_material' AND COLUMN_NAME = 'progress_total'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_raw_material ADD COLUMN progress_total INT DEFAULT 0', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_wiki_raw_material' AND COLUMN_NAME = 'progress_done'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_wiki_raw_material ADD COLUMN progress_done INT DEFAULT 0', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V9__usage_cache_tokens.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V9__usage_cache_tokens.sql index 87f45a4e..c19e3f69 100644 --- a/mateclaw-server/src/main/resources/db/migration/mysql/V9__usage_cache_tokens.sql +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V9__usage_cache_tokens.sql @@ -2,5 +2,11 @@ -- RFC-014 Change 4: per-call cache_creation_input_tokens / cache_read_input_tokens -- accumulated daily so the dashboard can show cache hit rate and cost savings. -- (was originally numbered V8 but collided with V8__wiki_raw_progress.sql; renumbered to V9.) -ALTER TABLE mate_usage_daily ADD COLUMN IF NOT EXISTS cache_read_tokens BIGINT DEFAULT 0; -ALTER TABLE mate_usage_daily ADD COLUMN IF NOT EXISTS cache_write_tokens BIGINT DEFAULT 0; +-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard instead. +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_usage_daily' AND COLUMN_NAME = 'cache_read_tokens'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_usage_daily ADD COLUMN cache_read_tokens BIGINT DEFAULT 0', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_usage_daily' AND COLUMN_NAME = 'cache_write_tokens'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_usage_daily ADD COLUMN cache_write_tokens BIGINT DEFAULT 0', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue index 86bbb2d9..ca0bb37a 100644 --- a/mateclaw-ui/src/components/chat/MessageBubble.vue +++ b/mateclaw-ui/src/components/chat/MessageBubble.vue @@ -322,6 +322,18 @@ const errorTitle = computed(() => { const errorDescription = computed(() => { if (!errorInfo.value) return '' + // 优先展示后端 extractUserFriendlyError 生成的具体消息(比泛化模板更有指向性, + // 比如"当前模型不支持工具调用,请切换到 qwen3 / qwen2.5 ...")。 + // 仅当 rawMessage 为空/过短时才回退到分类模板。 + const raw = errorInfo.value.rawMessage?.trim() || '' + if (raw.length > 8) { + // 去掉后端冗余前缀,错误卡标题已经表达了类别 + return raw + .replace(/^Bad request:\s*/i, '') + .replace(/^LLM 调用失败[::]\s*/, '') + .replace(/^认证失败[::]\s*/, '') + .replace(/^\[错误]\s*/, '') + } return t(`chat.error.${errorInfo.value.category}.description`) }) diff --git a/mateclaw-ui/src/types/chatError.ts b/mateclaw-ui/src/types/chatError.ts index feb07c38..3dbcba4c 100644 --- a/mateclaw-ui/src/types/chatError.ts +++ b/mateclaw-ui/src/types/chatError.ts @@ -59,11 +59,16 @@ export function classifyHttpError(status: number, body?: any): ChatErrorInfo { * 后端通过 SSE error 事件的 errorType 字段传递 */ const BACKEND_ERROR_TYPE_MAP: Record = { - RATE_LIMIT: { category: 'rate_limit', retryable: true }, - SERVER_ERROR: { category: 'server_error', retryable: true }, - PROMPT_TOO_LONG:{ category: 'bad_request', retryable: false }, - AUTH_ERROR: { category: 'auth_expired', retryable: false }, - UNKNOWN: { category: 'unknown', retryable: true }, + RATE_LIMIT: { category: 'rate_limit', retryable: true }, + SERVER_ERROR: { category: 'server_error', retryable: true }, + PROMPT_TOO_LONG: { category: 'bad_request', retryable: false }, + AUTH_ERROR: { category: 'auth_expired', retryable: false }, + // 后端 ErrorType.CLIENT_ERROR 对应 HTTP 400 类错误(比如模型不支持 tools、参数格式错误)。 + // 归类到 bad_request,配合 MessageBubble 优先展示 rawMessage, + // 让后端 extractUserFriendlyError 返回的具体中文提示能真正显示出来。 + CLIENT_ERROR: { category: 'bad_request', retryable: false }, + THINKING_BLOCK_ERROR: { category: 'bad_request', retryable: true }, + UNKNOWN: { category: 'unknown', retryable: true }, } /**