mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
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:
parent
a3e6724cf4
commit
7221d36bab
@ -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 反序列化错误,提取关键信息
|
||||
|
||||
@ -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 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<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 **且** 家族支持 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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`)
|
||||
})
|
||||
|
||||
|
||||
@ -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 },
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user