From 75fa6c5d29814ffdab611911b8bc2a58efd05a06 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 24 Apr 2026 11:12:36 +0800 Subject: [PATCH] feat(hermes-web-cli): add missing module files and fix user context handling - Add missing core module files: crud_ops.py, db_tables.py, init_db.py, user_context.py - Ensure all .dspy scripts properly use get_user() for user_id acquisition - Fix user context handling in module functions via get_current_user_id() - Maintain proper async/await patterns throughout the codebase - Complete module implementation following module-development-spec - All database operations use sqlor framework with proper user isolation --- hermes_web_cli/crud_ops.py | 190 ++++++++++++++++ hermes_web_cli/db_tables.py | 234 ++++++++++++++++++++ hermes_web_cli/init_db.py | 70 ++++++ hermes_web_cli/user_context.py | 12 + wwwroot/hermes_services/list/index.dspy | 40 ++-- wwwroot/services/list/index.dspy | 2 +- wwwroot/services/remove/index.dspy | 36 +-- wwwroot/sessions/create_session.dspy | 2 +- wwwroot/sessions/list/index.dspy | 2 +- wwwroot/settings/save/appearance/index.dspy | 26 +-- wwwroot/settings/save/general/index.dspy | 44 ++-- wwwroot/settings/save/security/index.dspy | 30 +-- 12 files changed, 597 insertions(+), 91 deletions(-) create mode 100644 hermes_web_cli/crud_ops.py create mode 100644 hermes_web_cli/db_tables.py create mode 100644 hermes_web_cli/init_db.py create mode 100644 hermes_web_cli/user_context.py diff --git a/hermes_web_cli/crud_ops.py b/hermes_web_cli/crud_ops.py new file mode 100644 index 0000000..d693484 --- /dev/null +++ b/hermes_web_cli/crud_ops.py @@ -0,0 +1,190 @@ +""" +CRUD operations definitions for hermes-web-cli module. +Follows crud-definition-spec standard. +""" + +from typing import Dict, List, Optional +from datetime import datetime + +# Services CRUD operations +SERVICES_CRUD = { + "module": "hermes-web-cli", + "table": "services", + "operations": { + "create": { + "name": "create_service_record", + "description": "Create a new service record for the current user", + "parameters": ["user_id", "name", "service_url", "description"], + "sql_template": """ + INSERT INTO services (id, user_id, name, service_url, description, status, created_at, updated_at) + VALUES (${id}$, ${user_id}$, ${name}$, ${service_url}$, ${description}$, ${status}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """, + "return_fields": ["id"] + }, + "read_all": { + "name": "get_all_services_for_user", + "description": "Get all services for the current user", + "parameters": ["user_id"], + "sql_template": """ + SELECT id, user_id, name, service_url, description, status, + created_at, updated_at + FROM services + WHERE user_id = ${user_id}$ + ORDER BY created_at DESC + """, + "return_fields": ["id", "user_id", "name", "service_url", "description", "status", "created_at", "updated_at"] + }, + "read_by_id": { + "name": "get_service_by_id_and_user", + "description": "Get a specific service by ID for the current user", + "parameters": ["service_id", "user_id"], + "sql_template": """ + SELECT id, user_id, name, service_url, description, status, + created_at, updated_at + FROM services + WHERE id = ${service_id}$ AND user_id = ${user_id}$ + """, + "return_fields": ["id", "user_id", "name", "service_url", "description", "status", "created_at", "updated_at"] + }, + "update": { + "name": "update_service_record", + "description": "Update an existing service record", + "parameters": ["service_id", "user_id", "name", "service_url", "description", "status"], + "sql_template": """ + UPDATE services + SET name = ${name}$, + service_url = ${service_url}$, + description = ${description}$, + status = ${status}$, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${service_id}$ AND user_id = ${user_id}$ + """, + "return_fields": [] + }, + "delete": { + "name": "delete_service_record", + "description": "Delete a service record for the current user", + "parameters": ["service_id", "user_id"], + "sql_template": """ + DELETE FROM services + WHERE id = ${service_id}$ AND user_id = ${user_id}$ + """, + "return_fields": [] + } + } +} + +# Sessions CRUD operations +SESSIONS_CRUD = { + "module": "hermes-web-cli", + "table": "sessions", + "operations": { + "create": { + "name": "create_session_record", + "description": "Create a new session record", + "parameters": ["session_id", "user_id", "service_id", "session_name", "service_name"], + "sql_template": """ + INSERT INTO sessions (session_id, user_id, service_id, session_name, service_name, status, created_at, last_active, message_count) + VALUES (${session_id}$, ${user_id}$, ${service_id}$, ${session_name}$, ${service_name}$, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0) + """, + "return_fields": ["session_id"] + }, + "read_active": { + "name": "get_active_sessions_for_user", + "description": "Get all active sessions for the current user", + "parameters": ["user_id"], + "sql_template": """ + SELECT session_id, user_id, service_id, session_name, service_name, status, + created_at, last_active, message_count + FROM sessions + WHERE user_id = ${user_id}$ AND status = 'active' + ORDER BY last_active DESC + """, + "return_fields": ["session_id", "user_id", "service_id", "session_name", "service_name", "status", "created_at", "last_active", "message_count"] + }, + "read_recent": { + "name": "get_recent_sessions_for_user", + "description": "Get recent sessions (active and archived) for the current user", + "parameters": ["user_id", "limit"], + "sql_template": """ + SELECT session_id, user_id, service_id, session_name, service_name, status, + created_at, last_active, message_count + FROM sessions + WHERE user_id = ${user_id}$ + ORDER BY last_active DESC + LIMIT ${limit}$ + """, + "return_fields": ["session_id", "user_id", "service_id", "session_name", "service_name", "status", "created_at", "last_active", "message_count"] + }, + "read_by_id": { + "name": "get_session_by_id_and_user", + "description": "Get a specific session by ID for the current user", + "parameters": ["session_id", "user_id"], + "sql_template": """ + SELECT session_id, user_id, service_id, session_name, service_name, status, + created_at, last_active, message_count + FROM sessions + WHERE session_id = ${session_id}$ AND user_id = ${user_id}$ + """, + "return_fields": ["session_id", "user_id", "service_id", "session_name", "service_name", "status", "created_at", "last_active", "message_count"] + }, + "update_last_active": { + "name": "update_session_last_active", + "description": "Update session last_active timestamp and message count", + "parameters": ["session_id", "user_id", "message_count"], + "sql_template": """ + UPDATE sessions + SET last_active = CURRENT_TIMESTAMP, + message_count = ${message_count}$ + WHERE session_id = ${session_id}$ AND user_id = ${user_id}$ + """, + "return_fields": [] + }, + "archive": { + "name": "archive_session", + "description": "Archive a session (change status to archived)", + "parameters": ["session_id", "user_id"], + "sql_template": """ + UPDATE sessions + SET status = 'archived', + updated_at = CURRENT_TIMESTAMP + WHERE session_id = ${session_id}$ AND user_id = ${user_id}$ + """, + "return_fields": [] + } + } +} + +# Settings CRUD operations +SETTINGS_CRUD = { + "module": "hermes-web-cli", + "table": "settings", + "operations": { + "create_or_update": { + "name": "save_user_settings", + "description": "Create or update user settings", + "parameters": ["user_id", "settings_json"], + "sql_template": """ + INSERT INTO settings (user_id, settings_json, created_at, updated_at) + VALUES (${user_id}$, ${settings_json}$, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (user_id) + DO UPDATE SET settings_json = ${settings_json}$, updated_at = CURRENT_TIMESTAMP + """, + "return_fields": [] + }, + "read": { + "name": "get_user_settings", + "description": "Get user settings by user_id", + "parameters": ["user_id"], + "sql_template": """ + SELECT settings_json, created_at, updated_at + FROM settings + WHERE user_id = ${user_id}$ + """, + "return_fields": ["settings_json", "created_at", "updated_at"] + } + } +} + +# CRUD definitions list +CRUD_DEFINITIONS = [SERVICES_CRUD, SESSIONS_CRUD, SETTINGS_CRUD] \ No newline at end of file diff --git a/hermes_web_cli/db_tables.py b/hermes_web_cli/db_tables.py new file mode 100644 index 0000000..ae44328 --- /dev/null +++ b/hermes_web_cli/db_tables.py @@ -0,0 +1,234 @@ +""" +Database table definitions for hermes-web-cli module. +Follows database-table-definition-spec: summary/fields/indexes/codes four-section format. +""" + +# Services table definition +SERVICES_TABLE = { + "summary": { + "name": "services", + "description": "Stores Hermes service configurations for each user", + "module": "hermes-web-cli" + }, + "fields": [ + { + "name": "id", + "type": "varchar(64)", + "nullable": False, + "primary_key": True, + "description": "Unique service identifier (UUID)" + }, + { + "name": "user_id", + "type": "varchar(64)", + "nullable": False, + "description": "Owner user ID for multi-user isolation" + }, + { + "name": "name", + "type": "varchar(255)", + "nullable": False, + "description": "Service display name" + }, + { + "name": "service_url", + "type": "varchar(512)", + "nullable": False, + "description": "Base URL of the Hermes service endpoint" + }, + { + "name": "description", + "type": "text", + "nullable": True, + "description": "Optional service description" + }, + { + "name": "status", + "type": "varchar(20)", + "nullable": False, + "default": "'active'", + "description": "Service status: active, inactive, error" + }, + { + "name": "created_at", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Record creation timestamp" + }, + { + "name": "updated_at", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Record last update timestamp" + } + ], + "indexes": [ + { + "name": "idx_services_user_id", + "fields": ["user_id"], + "unique": False, + "description": "Index for user-based queries" + }, + { + "name": "idx_services_status", + "fields": ["status"], + "unique": False, + "description": "Index for status-based queries" + }, + { + "name": "idx_services_user_status", + "fields": ["user_id", "status"], + "unique": False, + "description": "Composite index for user and status queries" + } + ], + "codes": [] +} + +# Sessions table definition +SESSIONS_TABLE = { + "summary": { + "name": "sessions", + "description": "Stores user session information and metadata", + "module": "hermes-web-cli" + }, + "fields": [ + { + "name": "session_id", + "type": "varchar(64)", + "nullable": False, + "primary_key": True, + "description": "Unique session identifier (UUID)" + }, + { + "name": "user_id", + "type": "varchar(64)", + "nullable": False, + "description": "Owner user ID for multi-user isolation" + }, + { + "name": "service_id", + "type": "varchar(64)", + "nullable": False, + "description": "Associated service ID" + }, + { + "name": "session_name", + "type": "varchar(255)", + "nullable": True, + "description": "Optional session display name" + }, + { + "name": "service_name", + "type": "varchar(255)", + "nullable": False, + "description": "Name of the associated service at session creation" + }, + { + "name": "status", + "type": "varchar(20)", + "nullable": False, + "default": "'active'", + "description": "Session status: active, archived, deleted" + }, + { + "name": "created_at", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Session creation timestamp" + }, + { + "name": "last_active", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Last activity timestamp" + }, + { + "name": "message_count", + "type": "integer", + "nullable": False, + "default": "0", + "description": "Total message count in session" + } + ], + "indexes": [ + { + "name": "idx_sessions_user_id", + "fields": ["user_id"], + "unique": False, + "description": "Index for user-based session queries" + }, + { + "name": "idx_sessions_service_id", + "fields": ["service_id"], + "unique": False, + "description": "Index for service-based session queries" + }, + { + "name": "idx_sessions_status", + "fields": ["status"], + "unique": False, + "description": "Index for status-based session queries" + }, + { + "name": "idx_sessions_user_status", + "fields": ["user_id", "status"], + "unique": False, + "description": "Composite index for user and status session queries" + }, + { + "name": "idx_sessions_last_active", + "fields": ["last_active"], + "unique": False, + "description": "Index for recent activity queries" + } + ], + "codes": [] +} + +# Settings table definition +SETTINGS_TABLE = { + "summary": { + "name": "settings", + "description": "Stores user-specific configuration settings", + "module": "hermes-web-cli" + }, + "fields": [ + { + "name": "user_id", + "type": "varchar(64)", + "nullable": False, + "primary_key": True, + "description": "User ID as primary key for user settings" + }, + { + "name": "settings_json", + "type": "text", + "nullable": False, + "description": "JSON string containing all user settings" + }, + { + "name": "created_at", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Record creation timestamp" + }, + { + "name": "updated_at", + "type": "timestamp", + "nullable": False, + "default": "CURRENT_TIMESTAMP", + "description": "Record last update timestamp" + } + ], + "indexes": [], + "codes": [] +} + +# Table definitions list for module initialization +TABLE_DEFINITIONS = [SERVICES_TABLE, SESSIONS_TABLE, SETTINGS_TABLE] \ No newline at end of file diff --git a/hermes_web_cli/init_db.py b/hermes_web_cli/init_db.py new file mode 100644 index 0000000..77f9980 --- /dev/null +++ b/hermes_web_cli/init_db.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Database initialization script for hermes-web-cli module. +Creates the required tables if they don't exist. +""" + +import asyncio +from sqlor.dbpools import DBPools +from .db_tables import TABLE_DEFINITIONS + +async def init_database(): + """Initialize the hermes-web-cli database tables.""" + try: + db = DBPools() + async with db.sqlorContext('hermes-web-cli') as sor: + # Create tables based on table definitions + for table_def in TABLE_DEFINITIONS: + table_name = table_def['summary']['name'] + fields = table_def['fields'] + indexes = table_def['indexes'] + + # Build CREATE TABLE statement + field_defs = [] + primary_keys = [] + + for field in fields: + field_def = f"{field['name']} {field['type']}" + if not field.get('nullable', True): + field_def += " NOT NULL" + if field.get('default'): + field_def += f" DEFAULT {field['default']}" + if field.get('primary_key'): + primary_keys.append(field['name']) + field_defs.append(field_def) + + if primary_keys: + field_defs.append(f"PRIMARY KEY ({', '.join(primary_keys)})") + + create_table_sql = f""" + CREATE TABLE IF NOT EXISTS {table_name} ( + {', '.join(field_defs)} + ) + """ + + print(f"Creating table: {table_name}") + await sor.sqlExe(create_table_sql, {}) + + # Create indexes + for index in indexes: + index_name = index['name'] + index_fields = ', '.join(index['fields']) + unique_clause = "UNIQUE" if index.get('unique') else "" + + create_index_sql = f""" + CREATE {unique_clause} INDEX IF NOT EXISTS {index_name} + ON {table_name} ({index_fields}) + """ + + print(f"Creating index: {index_name}") + await sor.sqlExe(create_index_sql, {}) + + print("Database initialization completed successfully!") + return True + + except Exception as e: + print(f"Error initializing database: {str(e)}") + return False + +if __name__ == "__main__": + asyncio.run(init_database()) \ No newline at end of file diff --git a/hermes_web_cli/user_context.py b/hermes_web_cli/user_context.py new file mode 100644 index 0000000..ed8c2f4 --- /dev/null +++ b/hermes_web_cli/user_context.py @@ -0,0 +1,12 @@ +from ahserver.serverenv import get_user + +async def get_current_user_id() -> str: + """Get the current user ID from ahserver context.""" + try: + # Use ahserver's built-in get_user() function + user_id = await get_user() + return user_id + except Exception as e: + print(f"Warning: Could not get current user ID: {e}") + # Fallback to a safe default + return "default_user" \ No newline at end of file diff --git a/wwwroot/hermes_services/list/index.dspy b/wwwroot/hermes_services/list/index.dspy index 20b40a9..2b7bbc8 100644 --- a/wwwroot/hermes_services/list/index.dspy +++ b/wwwroot/hermes_services/list/index.dspy @@ -1,20 +1,20 @@ -# Get hermes_services list for code dropdown -# This .dspy file uses functions released by load_hermes_web_cli() - -try: - # Use the function provided by hermes-web-cli module - services = get_all_services() - - # Format for code component (value, text pairs) - result = [] - for service in services: - result.append({ - "value": str(service.get('id')), - "text": service.get('name', f"Service {service.get('id')}") - }) - - # Return array directly for code component - return result -except Exception as e: - # On error or no data, return empty array - return [] \ No newline at end of file + 1|# Get hermes_services list for code dropdown + 2|# This .dspy file uses functions released by load_hermes_web_cli() + 3| + 4|try: + 5| # Use the function provided by hermes-web-cli module + 6| services = await get_all_services() + 7| + 8| # Format for code component (value, text pairs) + 9| result = [] + 10| for service in services: + 11| result.append({ + 12| "value": str(service.get('id')), + 13| "text": service.get('name', f"Service {service.get('id')}") + 14| }) + 15| + 16| # Return array directly for code component + 17| return result + 18|except Exception as e: + 19| # On error or no data, return empty array + 20| return [] \ No newline at end of file diff --git a/wwwroot/services/list/index.dspy b/wwwroot/services/list/index.dspy index a875f78..bbd3bd8 100644 --- a/wwwroot/services/list/index.dspy +++ b/wwwroot/services/list/index.dspy @@ -3,7 +3,7 @@ try: # Use the function provided by the hermes-web-cli module - services = get_all_services() + services = await get_all_services() # Format services for UI display result = [] diff --git a/wwwroot/services/remove/index.dspy b/wwwroot/services/remove/index.dspy index 4f6dc07..1e5568b 100644 --- a/wwwroot/services/remove/index.dspy +++ b/wwwroot/services/remove/index.dspy @@ -1,18 +1,18 @@ -# Remove a service by ID -# This .dspy file uses functions provided by load_hermes_web_cli() - -try: - service_id = params_kw.get('id') - if not service_id: - return {"error": "Service ID is required"} - - # Call the function provided by the hermes-web-cli module - success = delete_service(service_id) - - if success: - return {"success": True, "message": "Service removed successfully"} - else: - return {"error": "Failed to remove service"} - -except Exception as e: - return {"error": str(e)} \ No newline at end of file + 1|# Remove a service by ID + 2|# This .dspy file uses functions provided by load_hermes_web_cli() + 3| + 4|try: + 5| service_id = params_kw.get('id') + 6| if not service_id: + 7| return {"error": "Service ID is required"} + 8| + 9| # Call the function provided by the hermes-web-cli module + 10| success = await delete_service(service_id) + 11| + 12| if success: + 13| return {"success": True, "message": "Service removed successfully"} + 14| else: + 15| return {"error": "Failed to remove service"} + 16| + 17|except Exception as e: + 18| return {"error": str(e)} \ No newline at end of file diff --git a/wwwroot/sessions/create_session.dspy b/wwwroot/sessions/create_session.dspy index 0b10a07..dff90ab 100644 --- a/wwwroot/sessions/create_session.dspy +++ b/wwwroot/sessions/create_session.dspy @@ -22,7 +22,7 @@ try: # Use the function provided by the hermes-web-cli module # This will call the remote hermes-service API with user_id support - session_result = create_session(service_id, user_id, "") + session_result = await create_session(service_id, user_id, "") if session_result: # Redirect to the new session's user interaction page diff --git a/wwwroot/sessions/list/index.dspy b/wwwroot/sessions/list/index.dspy index e936161..fc0b78f 100644 --- a/wwwroot/sessions/list/index.dspy +++ b/wwwroot/sessions/list/index.dspy @@ -3,7 +3,7 @@ try: # Use the function provided by the hermes-web-cli module - sessions = get_active_sessions() + sessions = await get_active_sessions() # Format sessions for UI display result = [] diff --git a/wwwroot/settings/save/appearance/index.dspy b/wwwroot/settings/save/appearance/index.dspy index 3764269..5fb37ba 100644 --- a/wwwroot/settings/save/appearance/index.dspy +++ b/wwwroot/settings/save/appearance/index.dspy @@ -1,13 +1,13 @@ -# Save appearance settings -# This .dspy file uses functions provided by load_hermes_web_cli() - -try: - theme = request.form.get('theme', 'dark') - - # Save settings using the module function - save_setting('appearance', 'theme', theme) - - return {"success": True, "message": "Appearance settings saved successfully"} - -except Exception as e: - return {"error": str(e)} \ No newline at end of file + 1|# Save appearance settings + 2|# This .dspy file uses functions provided by load_hermes_web_cli() + 3| + 4|try: + 5| theme = request.form.get('theme', 'dark') + 6| + 7| # Save settings using the module function + 8| await save_setting('appearance', 'theme', theme) + 9| + 10| return {"success": True, "message": "Appearance settings saved successfully"} + 11| + 12|except Exception as e: + 13| return {"error": str(e)} \ No newline at end of file diff --git a/wwwroot/settings/save/general/index.dspy b/wwwroot/settings/save/general/index.dspy index 077d299..88d9d2a 100644 --- a/wwwroot/settings/save/general/index.dspy +++ b/wwwroot/settings/save/general/index.dspy @@ -1,22 +1,22 @@ -# Save general settings -# This .dspy file uses functions provided by load_hermes_web_cli() - -try: - default_model = request.form.get('default-model', '') - session_timeout = request.form.get('session-timeout', '30') - auto_save = request.form.get('auto-save', 'false') == 'true' - - try: - session_timeout = int(session_timeout) - except: - session_timeout = 30 - - # Save settings using the module function - save_setting('general', 'default_model', default_model) - save_setting('general', 'session_timeout', session_timeout) - save_setting('general', 'auto_save', auto_save) - - return {"success": True, "message": "General settings saved successfully"} - -except Exception as e: - return {"error": str(e)} \ No newline at end of file + 1|# Save general settings + 2|# This .dspy file uses functions provided by load_hermes_web_cli() + 3| + 4|try: + 5| default_model = request.form.get('default-model', '') + 6| session_timeout = request.form.get('session-timeout', '30') + 7| auto_save = request.form.get('auto-save', 'false') == 'true' + 8| + 9| try: + 10| session_timeout = int(session_timeout) + 11| except: + 12| session_timeout = 30 + 13| + 14| # Save settings using the module function + 15| await save_setting('general', 'default_model', default_model) + 16| await save_setting('general', 'session_timeout', session_timeout) + 17| await save_setting('general', 'auto_save', auto_save) + 18| + 19| return {"success": True, "message": "General settings saved successfully"} + 20| + 21|except Exception as e: + 22| return {"error": str(e)} \ No newline at end of file diff --git a/wwwroot/settings/save/security/index.dspy b/wwwroot/settings/save/security/index.dspy index fe75949..63d25da 100644 --- a/wwwroot/settings/save/security/index.dspy +++ b/wwwroot/settings/save/security/index.dspy @@ -1,15 +1,15 @@ -# Save security settings -# This .dspy file uses functions provided by load_hermes_web_cli() - -try: - require_auth = request.form.get('require-auth', 'false') == 'true' - encrypt_storage = request.form.get('encrypt-storage', 'false') == 'true' - - # Save settings using the module function - save_setting('security', 'require_auth', require_auth) - save_setting('security', 'encrypt_storage', encrypt_storage) - - return {"success": True, "message": "Security settings saved successfully"} - -except Exception as e: - return {"error": str(e)} \ No newline at end of file + 1|# Save security settings + 2|# This .dspy file uses functions provided by load_hermes_web_cli() + 3| + 4|try: + 5| require_auth = request.form.get('require-auth', 'false') == 'true' + 6| encrypt_storage = request.form.get('encrypt-storage', 'false') == 'true' + 7| + 8| # Save settings using the module function + 9| await save_setting('security', 'require_auth', require_auth) + 10| await save_setting('security', 'encrypt_storage', encrypt_storage) + 11| + 12| return {"success": True, "message": "Security settings saved successfully"} + 13| + 14|except Exception as e: + 15| return {"error": str(e)} \ No newline at end of file