From 61a01161550149f055c8aaf5e72cb3225fff98dd Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Fri, 26 Sep 2025 22:24:27 +0800 Subject: [PATCH 1/8] Enables AI extension integration Adds support for AI-powered features via a new extension mechanism. This includes dynamically loading the AI extension's CSS and JS, and retrying the plugin mounting process with exponential backoff to ensure proper initialization. Also, provides a basic GRPC querying functionality which could call AI methods, and converts the AI response to standard data format. The AI plugin can be built from source or downloaded from a binary URL. --- cmd/testdata/stores.yaml | 2 +- console/atest-ui/src/views/Extension.vue | 46 ++++++++++++---- pkg/testing/remote/grpc_store.go | 70 ++++++++++++++++++++++++ tools/make/run.mk | 18 +++++- 4 files changed, 122 insertions(+), 14 deletions(-) diff --git a/cmd/testdata/stores.yaml b/cmd/testdata/stores.yaml index cfa52051..ac1cdcf1 100644 --- a/cmd/testdata/stores.yaml +++ b/cmd/testdata/stores.yaml @@ -10,7 +10,7 @@ stores: kind: name: atest-ext-ai enabled: true - url: "" + url: "unix:///tmp/atest-ext-ai.sock" readonly: false disabled: false properties: diff --git a/console/atest-ui/src/views/Extension.vue b/console/atest-ui/src/views/Extension.vue index e8287a57..6b3c2a00 100644 --- a/console/atest-ui/src/views/Extension.vue +++ b/console/atest-ui/src/views/Extension.vue @@ -9,33 +9,55 @@ const props = defineProps() const loading = ref(true) const loadPlugin = async (): Promise => { try { + // First load CSS API.GetPageOfCSS(props.name, (d) => { const style = document.createElement('style'); style.textContent = d.message; document.head.appendChild(style); }); + // Then load JS and mount plugin API.GetPageOfJS(props.name, (d) => { const script = document.createElement('script'); script.type = 'text/javascript'; script.textContent = d.message; document.head.appendChild(script); - const plugin = window.ATestPlugin; - - if (plugin && plugin.mount) { - console.log('extension load success'); - const container = document.getElementById("plugin-container"); - if (container) { - container.innerHTML = ''; // Clear previous content - plugin.mount(container); + // Implement retry mechanism with exponential backoff + const checkPluginLoad = (retries = 0, maxRetries = 10) => { + const plugin = (window as any).ATestPlugin; + + console.log(`Plugin load attempt ${retries + 1}/${maxRetries + 1}`); + + if (plugin && plugin.mount) { + console.log('extension load success'); + const container = document.getElementById("plugin-container"); + if (container) { + container.innerHTML = ''; // Clear previous content + plugin.mount(container); + loading.value = false; + } else { + console.error('Plugin container not found'); + loading.value = false; + } + } else if (retries < maxRetries) { + // Incremental retry mechanism: 50ms, 100ms, 150ms... + const delay = 50 + retries * 50; + console.log(`ATestPlugin not ready, retrying in ${delay}ms (attempt ${retries + 1}/${maxRetries + 1})`); + setTimeout(() => checkPluginLoad(retries + 1, maxRetries), delay); + } else { + console.error('ATestPlugin not found or missing mount method after max retries'); + console.error('Window.ATestPlugin value:', (window as any).ATestPlugin); + loading.value = false; } - } + }; + + // Start the retry mechanism + checkPluginLoad(); }); } catch (error) { - console.log(`extension load error: ${(error as Error).message}`) - } finally { - console.log('extension load finally'); + console.log(`extension load error: ${(error as Error).message}`); + loading.value = false; // Set loading to false on error } }; try { diff --git a/pkg/testing/remote/grpc_store.go b/pkg/testing/remote/grpc_store.go index 8e25cf9e..aa75961c 100644 --- a/pkg/testing/remote/grpc_store.go +++ b/pkg/testing/remote/grpc_store.go @@ -18,9 +18,11 @@ package remote import ( "context" + "encoding/json" "errors" "fmt" "strconv" + "strings" "time" "github.com/linuxsuren/api-testing/pkg/logging" @@ -316,6 +318,12 @@ func (g *gRPCLoader) PProf(name string) []byte { } func (g *gRPCLoader) Query(query map[string]string) (result testing.DataResult, err error) { + // Detect AI method calls + if method := query["method"]; strings.HasPrefix(method, "ai.") { + return g.handleAIQuery(query) + } + + // Original standard query logic var dataResult *server.DataQueryResult offset, _ := strconv.ParseInt(query["offset"], 10, 64) limit, _ := strconv.ParseInt(query["limit"], 10, 64) @@ -444,3 +452,65 @@ func (g *gRPCLoader) Close() { g.conn.Close() } } + +// handleAIQuery handles AI-specific queries +func (g *gRPCLoader) handleAIQuery(query map[string]string) (testing.DataResult, error) { + method := query["method"] + + var dataQuery *server.DataQuery + switch method { + case "ai.generate": + dataQuery = &server.DataQuery{ + Type: "ai", + Key: "generate", + Sql: g.encodeAIGenerateParams(query), + } + case "ai.capabilities": + dataQuery = &server.DataQuery{ + Type: "ai", + Key: "capabilities", + Sql: "", // No additional parameters needed + } + default: + return testing.DataResult{}, fmt.Errorf("unsupported AI method: %s", method) + } + + // Call existing gRPC Query + dataResult, err := g.client.Query(g.ctx, dataQuery) + if err != nil { + return testing.DataResult{}, err + } + + // Convert response to testing.DataResult format + return g.convertAIResponse(dataResult), nil +} + +// encodeAIGenerateParams encodes AI generation parameters into SQL field +func (g *gRPCLoader) encodeAIGenerateParams(query map[string]string) string { + params := map[string]string{ + "model": query["model"], + "prompt": query["prompt"], + "config": query["config"], + } + data, _ := json.Marshal(params) + return string(data) +} + +// convertAIResponse converts AI response to standard format +func (g *gRPCLoader) convertAIResponse(dataResult *server.DataQueryResult) testing.DataResult { + result := testing.DataResult{ + Pairs: pairToMap(dataResult.Data), + } + + // Map AI-specific response fields + if content := result.Pairs["generated_sql"]; content != "" { + result.Pairs["content"] = content // Follow AI interface standard + } + if result.Pairs["error"] != "" { + result.Pairs["success"] = "false" + } else { + result.Pairs["success"] = "true" + } + + return result +} diff --git a/tools/make/run.mk b/tools/make/run.mk index f4fcc4b8..c82057a7 100644 --- a/tools/make/run.mk +++ b/tools/make/run.mk @@ -5,12 +5,28 @@ include tools/make/env.mk ATEST_UI = console/atest-ui +AI_PLUGIN_DIR := $(or $(AI_PLUGIN_SOURCE),../atest-ext-ai) ##@ Local runs & init env +.PHONY: build-ai-plugin +build-ai-plugin: + @if [ -n "$(AI_PLUGIN_BINARY_URL)" ]; then \ + echo "πŸ“₯ Downloading AI plugin binary from $(AI_PLUGIN_BINARY_URL)..."; \ + mkdir -p bin; \ + curl -L "$(AI_PLUGIN_BINARY_URL)" | tar xz -C bin/ --strip-components=1; \ + echo "βœ… AI plugin binary downloaded"; \ + elif [ -d "$(AI_PLUGIN_DIR)" ]; then \ + echo "πŸ”¨ Building AI plugin from source..."; \ + cd $(AI_PLUGIN_DIR) && make build; \ + echo "βœ… AI plugin built from source"; \ + else \ + echo "⚠️ AI plugin directory not found, skipping"; \ + fi + .PHONY: run-server run-server: ## Run the API Testing server -run-server: build-ui run-backend +run-server: build-ui build-ai-plugin run-backend run-backend: go run . server --local-storage 'bin/*.yaml' --console-path ${ATEST_UI}/dist \ --extension-registry ghcr.io --download-timeout 10m From e45ac153cd60620a53f8273f2323811c5ebf5436 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Fri, 26 Sep 2025 22:39:48 +0800 Subject: [PATCH 2/8] fix: restore critical AI plugin integration fixes Restore two essential bug fixes that were incorrectly removed: 1. vite.config.ts fixes: - Fix test-id removal logic: only remove in production, preserve for E2E tests - Improve build performance: replace single chunk with optimized chunk splitting - Separate vue, element-plus, and vendor chunks for better caching 2. App.vue fix: - Fix Extension component prop: use menu.index instead of menu.name - Ensures AI plugin can be correctly identified and loaded These fixes are critical for AI plugin functionality and should not be reverted. --- console/atest-ui/src/App.vue | 2 +- console/atest-ui/vite.config.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/console/atest-ui/src/App.vue b/console/atest-ui/src/App.vue index 33eddafa..1e79d867 100644 --- a/console/atest-ui/src/App.vue +++ b/console/atest-ui/src/App.vue @@ -178,7 +178,7 @@ API.GetMenus((menus) => { - + diff --git a/console/atest-ui/vite.config.ts b/console/atest-ui/vite.config.ts index 49be2379..f541bdfb 100644 --- a/console/atest-ui/vite.config.ts +++ b/console/atest-ui/vite.config.ts @@ -20,7 +20,7 @@ export default defineConfig({ vue({ template: { compilerOptions: { - nodeTransforms: true ? [removeDataTestAttrs] : [], + nodeTransforms: process.env.NODE_ENV === 'production' ? [removeDataTestAttrs] : [], }, }, }), @@ -34,7 +34,12 @@ export default defineConfig({ build: { rollupOptions: { output: { - manualChunks: () => 'everything' + // Enable automatic chunk splitting for better performance + manualChunks: { + vue: ['vue'], + 'element-plus': ['element-plus'], + vendor: ['@vueuse/core', 'vue-router', 'vue-i18n'] + } } } }, From 05cd94d4888f33d803505d592c48e028f35d92a4 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Sun, 28 Sep 2025 22:54:55 +0800 Subject: [PATCH 3/8] Simplifies chunking config Consolidates the vite build configuration to output a single chunk, simplifying the config and potentially improving build times in some scenarios. The previous manual chunk configuration is removed. --- console/atest-ui/vite.config.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/console/atest-ui/vite.config.ts b/console/atest-ui/vite.config.ts index f541bdfb..f1115cfe 100644 --- a/console/atest-ui/vite.config.ts +++ b/console/atest-ui/vite.config.ts @@ -34,12 +34,7 @@ export default defineConfig({ build: { rollupOptions: { output: { - // Enable automatic chunk splitting for better performance - manualChunks: { - vue: ['vue'], - 'element-plus': ['element-plus'], - vendor: ['@vueuse/core', 'vue-router', 'vue-i18n'] - } + manualChunks: () => 'everything' } } }, From 85a21a64ff2a7b67a719db0f8f23d2dd8d6b9229 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Sun, 28 Sep 2025 21:27:32 +0800 Subject: [PATCH 4/8] fix: Clarifies AI model name description Updates the AI model name description to indicate that the model is automatically detected from available models. Sets the default value for the model to an empty string, reflecting the auto-detection behavior. --- cmd/testdata/stores.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/testdata/stores.yaml b/cmd/testdata/stores.yaml index ac1cdcf1..994e3446 100644 --- a/cmd/testdata/stores.yaml +++ b/cmd/testdata/stores.yaml @@ -18,8 +18,8 @@ stores: description: "AI provider (local, openai, claude)" defaultValue: "local" - key: "model" - description: "AI model name" - defaultValue: "codellama" + description: "AI model name (auto-detected from available models)" + defaultValue: "" - key: "endpoint" description: "AI service endpoint" defaultValue: "http://localhost:11434" From 81359b548ee830a45bb36396692afa9c96160bf7 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Sun, 28 Sep 2025 23:34:27 +0800 Subject: [PATCH 5/8] refactor(testdata): Update stores.yaml configuration to support more AI parameters. Adjust the stores.yaml file structure and add more configuration parameters such as provider and endpoint for AI plug-ins. --- cmd/testdata/stores.yaml | 115 +++++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 29 deletions(-) diff --git a/cmd/testdata/stores.yaml b/cmd/testdata/stores.yaml index 994e3446..1a075bc0 100644 --- a/cmd/testdata/stores.yaml +++ b/cmd/testdata/stores.yaml @@ -1,35 +1,92 @@ stores: - - name: git - kind: - name: atest-store-git - enabled: true - url: xxx - readonly: false - disabled: false - - name: ai - kind: - name: atest-ext-ai - enabled: true + - name: git + kind: + name: atest-store-git + dependencies: [] + url: "unix:///tmp/atest-store-git.sock" + params: [] + link: "" + enabled: true + categories: [] + description: "" + url: xxx + username: "" + password: "" + readonly: false + disabled: false + properties: {} + - name: ai + kind: + name: atest-ext-ai + dependencies: [] # 无依衖 url: "unix:///tmp/atest-ext-ai.sock" - readonly: false - disabled: false - properties: + params: - key: "provider" - description: "AI provider (local, openai, claude)" - defaultValue: "local" - - key: "model" - description: "AI model name (auto-detected from available models)" - defaultValue: "" + description: "AI provider (ollama, openai, deepseek)" + defaultValue: "ollama" - key: "endpoint" - description: "AI service endpoint" + description: "AI service endpoint URL" defaultValue: "http://localhost:11434" -plugins: - - name: atest-store-git - url: unix:///tmp/atest-store-git.sock - enabled: true - - name: atest-ext-ai - url: unix:///tmp/atest-ext-ai.sock + - key: "api_key" + description: "API key for OpenAI/Deepseek providers" + defaultValue: "" + - key: "model" + description: "AI model name (auto-discovered for ollama)" + defaultValue: "" + - key: "max_tokens" + description: "Maximum tokens for AI generation" + defaultValue: "4096" + - key: "temperature" + description: "Generation temperature (0.0-2.0)" + defaultValue: "0.7" + - key: "timeout" + description: "Request timeout duration" + defaultValue: "30s" + link: "https://github.com/LinuxSuRen/atest-ext-ai" enabled: true - description: "AI Extension Plugin for intelligent SQL generation and execution" - version: "latest" - registry: "ghcr.io/linuxsuren/atest-ext-ai" + categories: ["ai", "sql-generation"] + description: "AI Extension Plugin for natural language to SQL conversion" + url: "unix:///tmp/atest-ext-ai.sock" + username: "" + password: "" + readonly: false + disabled: false + properties: + provider: "ollama" + endpoint: "http://localhost:11434" + api_key: "" + model: "" + max_tokens: "4096" + temperature: "0.7" + timeout: "30s" + +plugins: + - name: atest-store-git + dependencies: [] + url: "unix:///tmp/atest-store-git.sock" + params: [] + link: "" + enabled: true + categories: [] + - name: atest-ext-ai + dependencies: [] + url: "unix:///tmp/atest-ext-ai.sock" + params: + - key: "provider" + description: "AI provider (ollama, openai, deepseek)" + defaultValue: "ollama" + - key: "endpoint" + description: "AI service endpoint" + defaultValue: "http://localhost:11434" + - key: "api_key" + description: "API key for external AI services" + defaultValue: "" + - key: "model" + description: "AI model name (auto-discovered for ollama)" + defaultValue: "" + link: "https://github.com/LinuxSuRen/atest-ext-ai" + enabled: true + categories: ["ai", "sql-generation"] + description: "AI Extension Plugin for natural language to SQL conversion" + version: "v0.1.0" + registry: "ghcr.io/linuxsuren/atest-ext-ai" From 95a5496245414b7c3412c9404bebebc4ec7e9451 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Mon, 29 Sep 2025 14:54:36 +0800 Subject: [PATCH 6/8] Removes AI plugin build process The AI plugin build process is removed from the makefile. The AI plugin is assumed to be pre-built or handled by a separate process, simplifying the build and execution flow. --- tools/make/run.mk | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tools/make/run.mk b/tools/make/run.mk index c82057a7..f4fcc4b8 100644 --- a/tools/make/run.mk +++ b/tools/make/run.mk @@ -5,28 +5,12 @@ include tools/make/env.mk ATEST_UI = console/atest-ui -AI_PLUGIN_DIR := $(or $(AI_PLUGIN_SOURCE),../atest-ext-ai) ##@ Local runs & init env -.PHONY: build-ai-plugin -build-ai-plugin: - @if [ -n "$(AI_PLUGIN_BINARY_URL)" ]; then \ - echo "πŸ“₯ Downloading AI plugin binary from $(AI_PLUGIN_BINARY_URL)..."; \ - mkdir -p bin; \ - curl -L "$(AI_PLUGIN_BINARY_URL)" | tar xz -C bin/ --strip-components=1; \ - echo "βœ… AI plugin binary downloaded"; \ - elif [ -d "$(AI_PLUGIN_DIR)" ]; then \ - echo "πŸ”¨ Building AI plugin from source..."; \ - cd $(AI_PLUGIN_DIR) && make build; \ - echo "βœ… AI plugin built from source"; \ - else \ - echo "⚠️ AI plugin directory not found, skipping"; \ - fi - .PHONY: run-server run-server: ## Run the API Testing server -run-server: build-ui build-ai-plugin run-backend +run-server: build-ui run-backend run-backend: go run . server --local-storage 'bin/*.yaml' --console-path ${ATEST_UI}/dist \ --extension-registry ghcr.io --download-timeout 10m From 6171a02a286515ca4819ba1ab7c95b3a081332d8 Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Fri, 10 Oct 2025 13:28:35 +0800 Subject: [PATCH 7/8] refactor: address code review feedback - Remove console.log/error from Extension.vue for production readiness - Add comments to encodeAIGenerateParams explaining field filtering logic Resolves review feedback from yuluo-yx and LinuxSuRen --- console/atest-ui/src/views/Extension.vue | 10 +--------- pkg/testing/remote/grpc_store.go | 6 +++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/console/atest-ui/src/views/Extension.vue b/console/atest-ui/src/views/Extension.vue index 6b3c2a00..5219b24f 100644 --- a/console/atest-ui/src/views/Extension.vue +++ b/console/atest-ui/src/views/Extension.vue @@ -27,27 +27,20 @@ const loadPlugin = async (): Promise => { const checkPluginLoad = (retries = 0, maxRetries = 10) => { const plugin = (window as any).ATestPlugin; - console.log(`Plugin load attempt ${retries + 1}/${maxRetries + 1}`); - if (plugin && plugin.mount) { - console.log('extension load success'); const container = document.getElementById("plugin-container"); if (container) { container.innerHTML = ''; // Clear previous content plugin.mount(container); loading.value = false; } else { - console.error('Plugin container not found'); loading.value = false; } } else if (retries < maxRetries) { // Incremental retry mechanism: 50ms, 100ms, 150ms... const delay = 50 + retries * 50; - console.log(`ATestPlugin not ready, retrying in ${delay}ms (attempt ${retries + 1}/${maxRetries + 1})`); setTimeout(() => checkPluginLoad(retries + 1, maxRetries), delay); } else { - console.error('ATestPlugin not found or missing mount method after max retries'); - console.error('Window.ATestPlugin value:', (window as any).ATestPlugin); loading.value = false; } }; @@ -56,14 +49,13 @@ const loadPlugin = async (): Promise => { checkPluginLoad(); }); } catch (error) { - console.log(`extension load error: ${(error as Error).message}`); loading.value = false; // Set loading to false on error } }; try { loadPlugin(); } catch (error) { - console.error('extension load error:', error); + // Handle error silently, loading state will indicate failure } diff --git a/pkg/testing/remote/grpc_store.go b/pkg/testing/remote/grpc_store.go index aa75961c..16fd6d2a 100644 --- a/pkg/testing/remote/grpc_store.go +++ b/pkg/testing/remote/grpc_store.go @@ -485,8 +485,12 @@ func (g *gRPCLoader) handleAIQuery(query map[string]string) (testing.DataResult, return g.convertAIResponse(dataResult), nil } -// encodeAIGenerateParams encodes AI generation parameters into SQL field +// encodeAIGenerateParams filters and encodes AI generation parameters into JSON string. +// This function intentionally creates a new map to exclude the "method" field from the query, +// as "method" is used for routing decisions and should not be forwarded to AI plugins. +// Only the actual AI parameters (model, prompt, config) are encoded and sent via the SQL field. func (g *gRPCLoader) encodeAIGenerateParams(query map[string]string) string { + // Extract only AI-specific parameters, excluding the routing field "method" params := map[string]string{ "model": query["model"], "prompt": query["prompt"], From be856f5a55850fd6305b98ac613a8d22498448bf Mon Sep 17 00:00:00 2001 From: KarielHalling Date: Sun, 12 Oct 2025 19:24:06 +0800 Subject: [PATCH 8/8] refactor: align ui helpers with code quality guidance --- console/atest-ui/src/App.vue | 12 +++++++----- console/atest-ui/src/views/Extension.vue | 14 ++++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/console/atest-ui/src/App.vue b/console/atest-ui/src/App.vue index 1e79d867..02bf68d4 100644 --- a/console/atest-ui/src/App.vue +++ b/console/atest-ui/src/App.vue @@ -41,8 +41,8 @@ const appVersion = ref('') const appVersionLink = ref('https://github.com/LinuxSuRen/api-testing') API.GetVersion((d) => { appVersion.value = d.version - const version = d.version.match('^v\\d*.\\d*.\\d*') - const dirtyVersion = d.version.match('^v\\d*.\\d*.\\d*-\\d*-g') + const version = d.version.match(String.raw`^v\d*.\d*.\d*`) + const dirtyVersion = d.version.match(String.raw`^v\d*.\d*.\d*-\d*-g`) if (!version && !dirtyVersion) { return @@ -55,16 +55,18 @@ API.GetVersion((d) => { } }) +const hasLocalStorage = typeof globalThis !== 'undefined' && 'localStorage' in globalThis +const storage = hasLocalStorage ? globalThis.localStorage : undefined const isCollapse = ref(true) watch(isCollapse, (v: boolean) => { - window.localStorage.setItem('button.style', v ? 'simple' : '') + storage?.setItem('button.style', v ? 'simple' : '') }) -const lastActiveMenu = window.localStorage.getItem('activeMenu') +const lastActiveMenu = storage?.getItem('activeMenu') ?? 'welcome' const activeMenu = ref(lastActiveMenu === '' ? 'welcome' : lastActiveMenu) const panelName = ref(activeMenu) const handleSelect = (key: string) => { panelName.value = key - window.localStorage.setItem('activeMenu', key) + storage?.setItem('activeMenu', key) } const locale = ref(Cache.GetPreference().language) diff --git a/console/atest-ui/src/views/Extension.vue b/console/atest-ui/src/views/Extension.vue index 5219b24f..74ee83ca 100644 --- a/console/atest-ui/src/views/Extension.vue +++ b/console/atest-ui/src/views/Extension.vue @@ -25,7 +25,8 @@ const loadPlugin = async (): Promise => { // Implement retry mechanism with exponential backoff const checkPluginLoad = (retries = 0, maxRetries = 10) => { - const plugin = (window as any).ATestPlugin; + const globalScope = globalThis as { ATestPlugin?: { mount?: (el: Element) => void } }; + const plugin = globalScope.ATestPlugin; if (plugin && plugin.mount) { const container = document.getElementById("plugin-container"); @@ -50,13 +51,14 @@ const loadPlugin = async (): Promise => { }); } catch (error) { loading.value = false; // Set loading to false on error + console.error('Failed to load extension assets', error); } }; -try { - loadPlugin(); -} catch (error) { - // Handle error silently, loading state will indicate failure -} + +loadPlugin().catch((error) => { + loading.value = false; + console.error('Failed to initialize extension plugin', error); +});