diff --git a/internal/agent/component/browser.go b/internal/agent/component/browser.go index 4b524ad396..b2f0f883bd 100644 --- a/internal/agent/component/browser.go +++ b/internal/agent/component/browser.go @@ -45,7 +45,7 @@ import ( "errors" "fmt" "net/url" - "regexp" + "strings" "ragflow/internal/agent/runtime" @@ -125,27 +125,6 @@ type browserParam struct { Timeout int `json:"timeout"` } -// llmIDPattern matches `ModelName@Factory`. The factory part is -// optional; when absent, the caller's tenant lookup will be -// `GetByTenantAndModelName` instead of -// `GetByTenantFactoryAndModelName`. -var llmIDPattern = regexp.MustCompile(`^(.+)@(.+)$`) - -// resolveLLMID splits `llm_id` (e.g. "deepseek-v4-pro@DeepSeek") into -// `(modelName, factory)`. When no `@` is present, factory is empty -// and the caller must use a single-key lookup. -// -// Mirrors the contract of `dao.splitModelNameAndFactory` (private); -// re-implemented here to keep the component free of an import -// dependency on a DB-validating private helper. -func resolveLLMID(llmID string) (modelName, factory string) { - m := llmIDPattern.FindStringSubmatch(strings.TrimSpace(llmID)) - if m == nil { - return strings.TrimSpace(llmID), "" - } - return strings.TrimSpace(m[1]), strings.TrimSpace(m[2]) -} - // Update copies a fresh param map into the receiver. The // `llm_id`/`model_id` and `prompts`/`prompt` alias pairs collapse // onto the same field; the first non-empty value wins. @@ -437,35 +416,24 @@ func (b *BrowserComponent) Outputs() map[string]string { } } -// resolveBrowserLLM resolves the Browser's selected model into the model name -// and credentials required by the stagehand runtime. It first tries a -// tenant_model.id lookup, then model@factory parsing via resolveTenantLLM. +// resolveBrowserLLM resolves tenant model credentials exclusively through the +// model_provider series tables (tenant_model → tenant_model_provider → +// tenant_model_instance). It no longer falls back to the legacy tenant_llm path. // -// Tests override the lookup via `tenantLLMLookupForTest` (a -// package-level function variable) so they don't need a real DB. -// Production code leaves the variable unset. +// Tests override the lookup via `browserLLMLookupForTest` (a package-level +// function variable) so they don't need a real DB. Production code leaves the +// variable unset. func resolveBrowserLLM(ctx context.Context, db *gorm.DB, tenantID, llmID string) (providerName, modelName, apiKey, baseURL string, err error) { - if tenantLLMLookupForTest != nil { - oldModelName, factory := resolveLLMID(llmID) - apiKey, baseURL, err = tenantLLMLookupForTest(tenantID, oldModelName, factory) - baseURL = browserOpenAICompatibleBaseURL(baseURL, factory) - return factory, oldModelName, apiKey, baseURL, err + if browserLLMLookupForTest != nil { + return browserLLMLookupForTest(ctx, db, tenantID, llmID) } providerName, modelName, apiKey, baseURL, err = resolveTenantModelBrowserLLM(ctx, db, tenantID, llmID) - if err == nil { - baseURL = browserOpenAICompatibleBaseURL(baseURL, providerName) - return providerName, modelName, apiKey, baseURL, nil + if err != nil { + return "", "", "", "", fmt.Errorf("tenant model lookup (%q): %w", llmID, err) } - modelErr := err - - oldModelName, factory := resolveLLMID(llmID) - apiKey, baseURL, oldErr := resolveTenantLLM(ctx, db, tenantID, oldModelName, factory) - if oldErr == nil { - baseURL = browserOpenAICompatibleBaseURL(baseURL, factory) - return factory, oldModelName, apiKey, baseURL, nil - } - return "", "", "", "", fmt.Errorf("tenant_model lookup: %v; tenant_llm fallback: %w", modelErr, oldErr) + baseURL = browserOpenAICompatibleBaseURL(baseURL, providerName) + return providerName, modelName, apiKey, baseURL, nil } func resolveTenantModelBrowserLLM(ctx context.Context, db *gorm.DB, tenantID, modelID string) (providerName, modelName, apiKey, baseURL string, err error) { @@ -531,44 +499,10 @@ func browserOpenAICompatibleBaseURL(baseURL, provider string) string { return browserFactoryDefaultBaseURL[strings.ToLower(provider)] } -// resolveTenantLLM looks up the legacy tenant_llm config and returns -// (apiKey, baseURL). baseURL may be empty when the tenant's provider doesn't -// configure a custom endpoint. -// -// TODO(v2): this helper can move to `internal/dao` so the LLM -// component (`llm.go`) and other future components can share it. -func resolveTenantLLM(ctx context.Context, db *gorm.DB, tenantID, modelName, factory string) (apiKey, baseURL string, err error) { - llmDAO := dao.NewTenantLLMDAO() - var ( - row *entity.TenantLLM - ) - if factory != "" { - row, err = llmDAO.GetByTenantFactoryAndModelName(ctx, db, tenantID, factory, modelName) - } else { - // No factory suffix on llm_id; fall back to a single-key - // lookup (errors if the model is registered under multiple - // factories — caller must use the explicit form). - row, err = llmDAO.GetByTenantAndModelName(ctx, db, tenantID, "", modelName) - } - if err != nil { - return "", "", err - } - if row == nil { - return "", "", fmt.Errorf("tenant LLM not found") - } - if row.APIKey != nil { - apiKey = *row.APIKey - } - if row.APIBase != nil { - baseURL = *row.APIBase - } - return apiKey, baseURL, nil -} - -// tenantLLMLookupForTest is the test seam for `resolveTenantLLM`. +// browserLLMLookupForTest is the test seam for `resolveBrowserLLM`. // When non-nil, it's called instead of the real DAO lookup. // Production leaves this nil; tests set it via `defer ... = nil`. -var tenantLLMLookupForTest func(tenantID, modelName, factory string) (apiKey, baseURL string, err error) +var browserLLMLookupForTest func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (providerName, modelName, apiKey, baseURL string, err error) func init() { Register(componentNameBrowser, NewBrowserComponent) diff --git a/internal/agent/component/browser_test.go b/internal/agent/component/browser_test.go index e51f96373a..3e6322b7d6 100644 --- a/internal/agent/component/browser_test.go +++ b/internal/agent/component/browser_test.go @@ -26,6 +26,8 @@ import ( "ragflow/internal/agent/canvas" "ragflow/internal/agent/runtime" "ragflow/internal/entity" + + "gorm.io/gorm" ) // mockStagehandInvoker captures RunExtract requests and returns a @@ -187,11 +189,11 @@ func TestBrowser_DispatchesToRuntime(t *testing.T) { mock := &mockStagehandInvoker{rawJSON: `"agent result text"`} withMockRuntime(t, mock) - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) { - return "", "", errors.New("fake: tenant LLM not found") + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) { + return "", "", "", "", errors.New("fake: tenant LLM not found") } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) c, _ := NewBrowserComponent(map[string]any{ "llm_id": "deepseek-v4-pro@DeepSeek", @@ -245,9 +247,9 @@ func TestResolveBrowserLLM_ResolvesTenantModelID(t *testing.T) { t.Fatalf("create model: %v", err) } - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = nil - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = nil + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) ctx := t.Context() provider, model, apiKey, baseURL, err := resolveBrowserLLM(ctx, db, "tenant-1", "tenant-model-1") @@ -302,11 +304,11 @@ func TestBrowser_PropagatesRuntimeError(t *testing.T) { // Override the tenant LLM lookup so the test doesn't need a // real DB. - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) { - return "sk-test", "https://api.openai.com/v1", nil + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) { + return "OpenAI", "gpt-4o", "sk-test", "https://api.openai.com/v1", nil } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) c, _ := NewBrowserComponent(map[string]any{ "llm_id": "gpt-4o@OpenAI", @@ -420,11 +422,11 @@ func TestBrowser_RunExtractRequestShape(t *testing.T) { mock := &mockStagehandInvoker{rawJSON: `"ok"`} withMockRuntime(t, mock) - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) { - return "sk-test", "https://api.openai.com/v1", nil + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) { + return "OpenAI", "gpt-4o", "sk-test", "https://api.openai.com/v1", nil } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) c, _ := NewBrowserComponent(map[string]any{ "llm_id": "gpt-4o@OpenAI", @@ -462,11 +464,11 @@ func TestBrowser_HeadlessPropagates(t *testing.T) { mock := &mockStagehandInvoker{rawJSON: `"ok"`} withMockRuntime(t, mock) - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) { - return "sk-test", "", nil + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) { + return "OpenAI", "gpt-4o", "sk-test", "", nil } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) c, _ := NewBrowserComponent(map[string]any{ "llm_id": "gpt-4o@OpenAI", @@ -494,11 +496,11 @@ func TestBrowser_OutputsShape(t *testing.T) { mock := &mockStagehandInvoker{rawJSON: `"the agent's final message"`} withMockRuntime(t, mock) - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(tenantID, modelName, factory string) (string, string, error) { - return "sk-test", "", nil + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(ctx context.Context, db *gorm.DB, tenantID, llmID string) (string, string, string, string, error) { + return "OpenAI", "gpt-4o", "sk-test", "", nil } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) c, _ := NewBrowserComponent(map[string]any{ "llm_id": "gpt-4o@OpenAI", diff --git a/internal/agent/component/stagehand_runtime_integration_test.go b/internal/agent/component/stagehand_runtime_integration_test.go index f9e18ba2fb..73a44b878f 100644 --- a/internal/agent/component/stagehand_runtime_integration_test.go +++ b/internal/agent/component/stagehand_runtime_integration_test.go @@ -54,6 +54,8 @@ import ( "ragflow/internal/agent/canvas" "ragflow/internal/agent/runtime" + + "gorm.io/gorm" ) // TestStagehandRuntime_Extract is the single happy-path integration @@ -206,11 +208,11 @@ func TestBrowser_E2E_Extract(t *testing.T) { t.Cleanup(srv.Close) // Override tenant LLM lookup so the test doesn't need a real DB. - prevLookup := tenantLLMLookupForTest - tenantLLMLookupForTest = func(_, _, _ string) (string, string, error) { - return apiKey, baseURL, nil + prevLookup := browserLLMLookupForTest + browserLLMLookupForTest = func(_ context.Context, _ *gorm.DB, _, _ string) (string, string, string, string, error) { + return "OpenAI", model, apiKey, baseURL, nil } - t.Cleanup(func() { tenantLLMLookupForTest = prevLookup }) + t.Cleanup(func() { browserLLMLookupForTest = prevLookup }) // --- use production stagehand runtime --- r := newStagehandRuntimeFromEnv() diff --git a/web/src/stories/calendar.stories.tsx b/web/src/stories/calendar.stories.tsx index cfa5ebc049..9926947281 100644 --- a/web/src/stories/calendar.stories.tsx +++ b/web/src/stories/calendar.stories.tsx @@ -28,7 +28,7 @@ import { useState } from 'react'; function MyComponent() { const [date, setDate] = useState(new Date()); - + return ( - @@ -193,8 +193,8 @@ Shows the basic modal with default size and standard header/footer. const [open, setOpen] = useState(false); - @@ -257,8 +257,8 @@ Shows a small-sized modal, ideal for confirmations or brief messages. const [open, setOpen] = useState(false); - setOpen(true)}>Open Large Modal - setOpen(true)}>Open Modal with Custom Footer - setOpen(true)}>Open Modal without Footer - setOpen(true)}>Open Full Screen Modal - { }; - - {