feat(admin): 面板补全 — 插件安装表单+已装列表 / LLM组maxTokens展示编辑 / POST admin/llm 部分编辑合并
- 前端: api.ts(adminPluginInstall带config/uninstall/listInstalledPlugins) + types + plugins/InstalledPlugins.tsx(已装列表+安装表单, Miuix) + llm/LlmProviderForm.tsx(maxTokens输入) ; Plugins.tsx去假安装按钮 - 后端 admin_write_llm_channels.go: llmProviderRequest字段改指针, mergeLlmProvider部分合并(省略保留原值), runtimeconfig/models.go Find - 新增测试 admin_write_test TestAdminAddLlmProviderPartialEditPreservesFields - 前端 build + 后端 build/vet/test 全绿; Docs/api-client v0.29 - (面板无法实机登录, 用 react-dom/server 冒烟渲染 LlmModule/PluginsModule)
This commit is contained in:
1 parent
70c9fba529
commit
18b2040c95
10 files changed
+812
-220
No files matched your search
@@ -301,6 +301,50 @@ func TestAdminAddLlmProviderSwitchProvider(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminAddLlmProviderPartialEditPreservesFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
modelsPath := filepath.Join(t.TempDir(), "models.json")
|
||||
env := config.LLMConfig{Provider: "openai", BaseURL: "https://api.openai.com/v1", APIKey: "sk-env", Model: "gpt-4o"}
|
||||
runtime := newLlmRuntimeForGateway(t, modelsPath, env)
|
||||
_, ts, jwt := buildEnv(t, cfgDefault(), Deps{LLM: runtime})
|
||||
tok := signAny(t, jwt, "acct", auth.RoleOwner)
|
||||
|
||||
// Seed a fully-specified group.
|
||||
resp, body := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/llm",
|
||||
map[string]any{"id": "deepseek", "provider": "openai", "baseUrl": "https://api.deepseek.com/v1", "apiKey": "sk-ds", "model": "deepseek-chat", "multimodal": true, "fallback": "deepseek-lite", "maxTokens": 1234}, tok)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("seed status = %d, want 201 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Partial edit: only the model is given. Every omitted field must keep its
|
||||
// stored value — in particular maxTokens (a full replace reset it to 0) and
|
||||
// apiKey (a full replace wiped it).
|
||||
resp2, body2 := doReq(t, http.MethodPost, ts.URL+"/api/v1/admin/llm",
|
||||
map[string]any{"id": "deepseek", "model": "deepseek-chat-v2"}, tok)
|
||||
if resp2.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("edit status = %d, want 201 (body=%s)", resp2.StatusCode, body2)
|
||||
}
|
||||
var res struct {
|
||||
Provider AdminLlmProvider `json:"provider"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body2)["data"], &res); err != nil {
|
||||
t.Fatalf("decode edit: %v", err)
|
||||
}
|
||||
p := res.Provider
|
||||
if p.Model != "deepseek-chat-v2" || p.MaxTokens != 1234 || p.Fallback != "deepseek-lite" ||
|
||||
!p.Multimodal || p.Provider != "openai" || p.BaseURL != "https://api.deepseek.com/v1" {
|
||||
t.Fatalf("edit provider = %+v, want model updated and every omitted field preserved", p)
|
||||
}
|
||||
|
||||
groups, warn := runtimeconfig.LoadModels(modelsPath, env)
|
||||
if warn != nil {
|
||||
t.Fatalf("load models warn = %v", warn)
|
||||
}
|
||||
if len(groups) == 0 || groups[0].APIKey != "sk-ds" || groups[0].MaxTokens != 1234 || groups[0].Model != "deepseek-chat-v2" {
|
||||
t.Fatalf("file groups = %+v, want apiKey/maxTokens preserved and model updated", groups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminAddLlmProviderRejectsBadInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
modelsPath := filepath.Join(t.TempDir(), "models.json")
|
||||
|
||||
Reference in new issue
Block a user