mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
The two tools-sync scripts ran on every startup and used H2 MERGE INTO ... KEY(id), which overwrites every column on existing rows. That silently reverted UI-toggled `enabled` and was the proximate cause of a recent WriteFileTool/EditFileTool outage. They were also a strict subset of the fresh-install seed (data-zh.sql / data-en.sql register all 19 builtins; the sync scripts only 16) and out of date. Per-tool Flyway migrations (V3, V31) are already the canonical 'register a new builtin' path, so the sync layer was duplicated and error-prone. Delete both files and the runToolSyncScript() loader. Tool descriptions shown to the LLM come from @Tool annotations in code, not the DB row, so removing per-startup metadata refresh has no functional impact.
178 lines
6.7 KiB
Java
178 lines
6.7 KiB
Java
package vip.mate.config;
|
|
|
|
import lombok.Getter;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.ApplicationArguments;
|
|
import org.springframework.boot.ApplicationRunner;
|
|
import org.springframework.core.annotation.Order;
|
|
import org.springframework.core.io.ClassPathResource;
|
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import javax.sql.DataSource;
|
|
import java.sql.Connection;
|
|
import java.sql.DatabaseMetaData;
|
|
import java.sql.ResultSet;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
/**
|
|
* Database bootstrap runner.
|
|
* <p>
|
|
* Loads seed data after Flyway migrations on every startup.
|
|
* <p>
|
|
* For data.sql (seed data with locale-specific content):
|
|
* <ul>
|
|
* <li><b>Web/dev mode</b> (default, {@code mateclaw.setup.await-language-selection=false}):
|
|
* auto-initializes immediately with {@code mateclaw.setup.default-locale} (zh-CN).</li>
|
|
* <li><b>Desktop mode</b> ({@code mateclaw.setup.await-language-selection=true}):
|
|
* defers until the user selects a language via {@code POST /api/v1/setup/init}.</li>
|
|
* </ul>
|
|
*/
|
|
@Slf4j
|
|
@Component
|
|
@Order(1)
|
|
public class DatabaseBootstrapRunner implements ApplicationRunner {
|
|
|
|
private final DataSource dataSource;
|
|
private final JdbcTemplate jdbcTemplate;
|
|
|
|
/** Cached flag: true when running on MySQL/MariaDB, false for H2. */
|
|
private volatile Boolean isMySQL;
|
|
|
|
/**
|
|
* When true, wait for Desktop splash screen to call /setup/init with chosen language.
|
|
* When false (default), auto-initialize immediately on startup.
|
|
* <p>
|
|
* Desktop sets this via: {@code --mateclaw.setup.await-language-selection=true}
|
|
*/
|
|
@Value("${mateclaw.setup.await-language-selection:false}")
|
|
private boolean awaitLanguageSelection;
|
|
|
|
/** Default locale for auto-initialization. */
|
|
@Value("${mateclaw.setup.default-locale:zh-CN}")
|
|
private String defaultLocale;
|
|
|
|
/** Whether the database has been seeded with data (user table has rows). */
|
|
@Getter
|
|
private volatile boolean initialized = false;
|
|
|
|
/** Guards against concurrent init attempts. */
|
|
private final AtomicBoolean initInProgress = new AtomicBoolean(false);
|
|
|
|
public DatabaseBootstrapRunner(DataSource dataSource, JdbcTemplate jdbcTemplate) {
|
|
this.dataSource = dataSource;
|
|
this.jdbcTemplate = jdbcTemplate;
|
|
}
|
|
|
|
@Override
|
|
public void run(ApplicationArguments args) throws Exception {
|
|
// Schema creation and built-in tool registration are handled by
|
|
// Flyway (db/migration/). New built-in tools must ship as their own
|
|
// Vxx__register_<tool>_tool.sql migration (see V3, V31 for examples).
|
|
if (isDataAlreadySeeded()) {
|
|
initialized = true;
|
|
log.info("Database already initialized, skipping seed data");
|
|
return;
|
|
}
|
|
|
|
if (awaitLanguageSelection) {
|
|
// Desktop mode: wait for /api/v1/setup/init
|
|
log.info("Desktop mode: waiting for language selection via /api/v1/setup/init");
|
|
} else {
|
|
// Web/dev mode: auto-initialize immediately
|
|
log.info("Auto-initializing database with default locale: {}", defaultLocale);
|
|
initWithLocale(defaultLocale);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize seed data with the given locale.
|
|
*
|
|
* @param locale "zh-CN" or "en-US"
|
|
* @return true if initialization was performed, false if already initialized or in progress
|
|
*/
|
|
public boolean initWithLocale(String locale) {
|
|
if (initialized) {
|
|
log.info("Database already initialized, ignoring init request");
|
|
return false;
|
|
}
|
|
if (!initInProgress.compareAndSet(false, true)) {
|
|
log.info("Initialization already in progress, ignoring concurrent request");
|
|
return false;
|
|
}
|
|
try {
|
|
// Double-check after acquiring the lock
|
|
if (isDataAlreadySeeded()) {
|
|
initialized = true;
|
|
return false;
|
|
}
|
|
String scriptName;
|
|
if (isMySQL()) {
|
|
scriptName = "en-US".equals(locale) ? "db/data-mysql-en.sql" : "db/data-mysql-zh.sql";
|
|
} else {
|
|
scriptName = "en-US".equals(locale) ? "db/data-en.sql" : "db/data-zh.sql";
|
|
}
|
|
log.info("Initializing database with locale={} using {}", locale, scriptName);
|
|
runScript(scriptName);
|
|
initialized = true;
|
|
log.info("Database initialization completed successfully");
|
|
return true;
|
|
} catch (Exception e) {
|
|
log.error("Failed to initialize database with locale={}", locale, e);
|
|
throw new RuntimeException("Database initialization failed", e);
|
|
} finally {
|
|
initInProgress.set(false);
|
|
}
|
|
}
|
|
|
|
private boolean isDataAlreadySeeded() {
|
|
try {
|
|
if (!tableExists("mate_user")) {
|
|
return false;
|
|
}
|
|
Integer userCount = jdbcTemplate.queryForObject("SELECT COUNT(1) FROM mate_user", Integer.class);
|
|
return userCount != null && userCount > 0;
|
|
} catch (Exception e) {
|
|
log.warn("Error checking database state", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean tableExists(String tableName) throws Exception {
|
|
try (Connection connection = dataSource.getConnection()) {
|
|
DatabaseMetaData metaData = connection.getMetaData();
|
|
try (ResultSet rs = metaData.getTables(null, null, tableName.toUpperCase(), null)) {
|
|
if (rs.next()) {
|
|
return true;
|
|
}
|
|
}
|
|
try (ResultSet rs = metaData.getTables(null, null, tableName.toLowerCase(), null)) {
|
|
return rs.next();
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isMySQL() {
|
|
if (isMySQL == null) {
|
|
try (Connection connection = dataSource.getConnection()) {
|
|
String dbProduct = connection.getMetaData().getDatabaseProductName().toLowerCase();
|
|
isMySQL = dbProduct.contains("mysql") || dbProduct.contains("mariadb");
|
|
log.info("Detected database: {} (MySQL mode: {})", dbProduct, isMySQL);
|
|
} catch (Exception e) {
|
|
log.warn("Failed to detect database type, falling back to H2 mode", e);
|
|
isMySQL = false;
|
|
}
|
|
}
|
|
return isMySQL;
|
|
}
|
|
|
|
private void runScript(String path) {
|
|
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
|
|
populator.setContinueOnError(false);
|
|
populator.addScript(new ClassPathResource(path));
|
|
populator.execute(dataSource);
|
|
}
|
|
}
|