feat: add open settings location endpoint

- Add `open_settings_location` method to `FileSystemHandler` to open OS file explorer at settings file location
- Register new POST route `/api/lm/settings/open-location` for settings file access
- Inject `SettingsManager` dependency into `FileSystemHandler` constructor
- Add cross-platform support for Windows, macOS, and Linux file explorers
- Include error handling for missing settings files and system exceptions
This commit is contained in:
Will Miao
2025-12-31 16:09:23 +08:00
parent 8120716cd8
commit 102defe29c
6 changed files with 140 additions and 129 deletions

View File

@@ -853,6 +853,9 @@ class MetadataArchiveHandler:
class FileSystemHandler: class FileSystemHandler:
def __init__(self, settings_service=None) -> None:
self._settings = settings_service or get_settings_manager()
async def open_file_location(self, request: web.Request) -> web.Response: async def open_file_location(self, request: web.Request) -> web.Response:
try: try:
data = await request.json() data = await request.json()
@@ -877,6 +880,30 @@ class FileSystemHandler:
logger.error("Failed to open file location: %s", exc, exc_info=True) logger.error("Failed to open file location: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500) return web.json_response({"success": False, "error": str(exc)}, status=500)
async def open_settings_location(self, request: web.Request) -> web.Response:
try:
settings_file = getattr(self._settings, "settings_file", None)
if not settings_file:
return web.json_response({"success": False, "error": "Settings file not found"}, status=404)
settings_file = os.path.abspath(settings_file)
if not os.path.isfile(settings_file):
return web.json_response({"success": False, "error": "Settings file does not exist"}, status=404)
if os.name == "nt":
subprocess.Popen(["explorer", "/select,", settings_file])
elif os.name == "posix":
if sys.platform == "darwin":
subprocess.Popen(["open", "-R", settings_file])
else:
folder = os.path.dirname(settings_file)
subprocess.Popen(["xdg-open", folder])
return web.json_response({"success": True, "message": f"Opened settings folder: {settings_file}"})
except Exception as exc: # pragma: no cover - defensive logging
logger.error("Failed to open settings location: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
class NodeRegistryHandler: class NodeRegistryHandler:
def __init__( def __init__(
@@ -1103,6 +1130,7 @@ class MiscHandlerSet:
"get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status, "get_metadata_archive_status": self.metadata_archive.get_metadata_archive_status,
"get_model_versions_status": self.model_library.get_model_versions_status, "get_model_versions_status": self.model_library.get_model_versions_status,
"open_file_location": self.filesystem.open_file_location, "open_file_location": self.filesystem.open_file_location,
"open_settings_location": self.filesystem.open_settings_location,
} }

View File

@@ -41,6 +41,7 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"), RouteDefinition("POST", "/api/lm/remove-metadata-archive", "remove_metadata_archive"),
RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"), RouteDefinition("GET", "/api/lm/metadata-archive-status", "get_metadata_archive_status"),
RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"), RouteDefinition("GET", "/api/lm/model-versions-status", "get_model_versions_status"),
RouteDefinition("POST", "/api/lm/settings/open-location", "open_settings_location"),
) )

View File

@@ -107,7 +107,7 @@ class MiscRoutes:
settings_service=self._settings, settings_service=self._settings,
metadata_provider_updater=self._metadata_provider_updater, metadata_provider_updater=self._metadata_provider_updater,
) )
filesystem = FileSystemHandler() filesystem = FileSystemHandler(settings_service=self._settings)
node_registry_handler = NodeRegistryHandler( node_registry_handler = NodeRegistryHandler(
node_registry=self._node_registry, node_registry=self._node_registry,
prompt_server=self._prompt_server, prompt_server=self._prompt_server,

View File

@@ -883,6 +883,7 @@ class SettingsManager:
if os.path.abspath(previous_path) != os.path.abspath(target_path): if os.path.abspath(previous_path) != os.path.abspath(target_path):
self._copy_model_cache_directory(previous_dir, target_dir) self._copy_model_cache_directory(previous_dir, target_dir)
logger.info("Switching settings file to: %s", target_path)
self._pending_portable_switch = {"other_path": other_path} self._pending_portable_switch = {"other_path": other_path}
self.settings_file = target_path self.settings_file = target_path
@@ -929,7 +930,12 @@ class SettingsManager:
and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir) and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir)
): ):
try: try:
shutil.copytree(source_cache_dir, target_cache_dir, dirs_exist_ok=True) shutil.copytree(
source_cache_dir,
target_cache_dir,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns("*.sqlite-shm", "*.sqlite-wal"),
)
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"Failed to copy model_cache directory from %s to %s: %s", "Failed to copy model_cache directory from %s to %s: %s",

View File

@@ -17,7 +17,6 @@ export class SettingsManager {
this.initializationPromise = null; this.initializationPromise = null;
this.availableLibraries = {}; this.availableLibraries = {};
this.activeLibrary = ''; this.activeLibrary = '';
this.settingsFilePath = null;
this.registeredStartupBannerIds = new Set(); this.registeredStartupBannerIds = new Set();
// Add initialization to sync with modal state // Add initialization to sync with modal state
@@ -56,7 +55,6 @@ export class SettingsManager {
const data = await response.json(); const data = await response.json();
if (data.success && data.settings) { if (data.success && data.settings) {
state.global.settings = this.mergeSettingsWithDefaults(data.settings); state.global.settings = this.mergeSettingsWithDefaults(data.settings);
this.settingsFilePath = data.settings.settings_file || this.settingsFilePath;
this.registerStartupMessages(data.messages); this.registerStartupMessages(data.messages);
console.log('Settings synced from backend'); console.log('Settings synced from backend');
} else { } else {
@@ -177,10 +175,6 @@ export class SettingsManager {
return; return;
} }
if (!this.settingsFilePath && typeof message.settings_file === 'string') {
this.settingsFilePath = message.settings_file;
}
const bannerId = `startup-${message.code || index}`; const bannerId = `startup-${message.code || index}`;
if (this.registeredStartupBannerIds.has(bannerId)) { if (this.registeredStartupBannerIds.has(bannerId)) {
return; return;
@@ -316,12 +310,8 @@ export class SettingsManager {
const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger'); const openSettingsLocationButton = document.querySelector('.settings-open-location-trigger');
if (openSettingsLocationButton) { if (openSettingsLocationButton) {
if (openSettingsLocationButton.dataset.settingsPath) {
this.settingsFilePath = openSettingsLocationButton.dataset.settingsPath;
}
openSettingsLocationButton.addEventListener('click', () => { openSettingsLocationButton.addEventListener('click', () => {
const filePath = openSettingsLocationButton.dataset.settingsPath; this.openSettingsFileLocation();
this.openSettingsFileLocation(filePath);
}); });
} }
@@ -364,29 +354,16 @@ export class SettingsManager {
this.initialized = true; this.initialized = true;
} }
async openSettingsFileLocation(filePath) { async openSettingsFileLocation() {
const targetPath = filePath || this.settingsFilePath || document.querySelector('.settings-open-location-trigger')?.dataset.settingsPath;
if (!targetPath) {
showToast('settings.openSettingsFileLocation.failed', {}, 'error');
return;
}
try { try {
const response = await fetch('/api/lm/open-file-location', { const response = await fetch('/api/lm/settings/open-location', {
method: 'POST', method: 'POST'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ file_path: targetPath }),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`); throw new Error(`Request failed with status ${response.status}`);
} }
this.settingsFilePath = targetPath;
showToast('settings.openSettingsFileLocation.success', {}, 'success'); showToast('settings.openSettingsFileLocation.success', {}, 'success');
} catch (error) { } catch (error) {
console.error('Failed to open settings file location:', error); console.error('Failed to open settings file location:', error);

View File

@@ -7,7 +7,6 @@
<button <button
type="button" type="button"
class="settings-action-link settings-open-location-trigger" class="settings-action-link settings-open-location-trigger"
data-settings-path="{{ settings.settings_file }}"
aria-label="{{ t('settings.openSettingsFileLocation.tooltip') }}" aria-label="{{ t('settings.openSettingsFileLocation.tooltip') }}"
title="{{ t('settings.openSettingsFileLocation.tooltip') }}"> title="{{ t('settings.openSettingsFileLocation.tooltip') }}">
<i class="fas fa-external-link-alt" aria-hidden="true"></i> <i class="fas fa-external-link-alt" aria-hidden="true"></i>