fix: mysql migration compatibility + ollama tool-support gating + actionable error UI

- 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.
This commit is contained in:
matevip 2026-04-17 11:17:27 +08:00
parent a3e6724cf4
commit 7221d36bab
15 changed files with 268 additions and 49 deletions

View File

@ -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 时返回此文案
// "<model> 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 反序列化错误提取关键信息

View File

@ -37,6 +37,26 @@ public class OllamaAutoDiscoveryRunner implements ApplicationRunner {
private static final String OLLAMA_PROVIDER_ID = "ollama";
/**
* Ollama 模型族里**已知不支持 function calling / tools** 的家族名冒号前半段
* <p>
* Agent 默认带 24+ tool callbacks 发请求这类模型会返回
* {@code 400 "<model> does not support tools"}把它们从 auto-activate 默认候选里排除
* 仍然允许 enabled用户可以手工选它做纯文本问答但不会被意外设为默认
* <p>
* 参考https://ollama.com/search?c=tools 标了 "Tools" 标签的才支持
* 清单随 Ollama 生态演进必要时手工维护
*/
private static final Set<String> 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 toolsllama3.1 / llama3.3 支持
"qwen2", // qwen2 tool 支持qwen2.5 / qwen3 支持
"mistral" // 原始 mistral toolsmistral-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:latestOllama 没装
// 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<String> discoveredIds) {
List<ModelConfigEntity> ollamaModels = modelConfigService.listModelsByProvider(OLLAMA_PROVIDER_ID);
List<ModelConfigEntity> 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 **** 家族支持 toolsagent 能正常用工具
// 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);
}
}

View File

@ -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}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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)

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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`)
})

View File

@ -59,11 +59,16 @@ export function classifyHttpError(status: number, body?: any): ChatErrorInfo {
* SSE error errorType
*/
const BACKEND_ERROR_TYPE_MAP: Record<string, { category: ChatErrorCategory; retryable: boolean }> = {
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 },
}
/**