Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,14 @@ SYSTEM_AES_KEY=weknora-system-aes-key-32bytes!!

# 自定义 Skills 目录(挂载后指定,免重建镜像)。
# WEKNORA_SKILLS_DIR=
# 插件组合:profile YAML(默认 config/plugin_profile.yaml)、额外 overlay、逗号分隔启用项。
# WEKNORA_PLUGIN_PROFILE=config/plugin_profile.yaml
# WEKNORA_PLUGIN_PATCH=
# WEKNORA_PLUGINS=websearch.echo
# 运行时插件目录(默认 plugins.d,: 或系统分隔符可写多个;none 关闭扫描)。
# 目录里每个子文件夹放 plugin.yaml;语言插件用 runtime: stdio(stdin/stdout JSON-RPC),
# 轻脚本用 runtime: js。不要为了写插件去起 HTTP sidecar。
# WEKNORA_PLUGIN_DIR=plugins.d
# 智能体大模型调用默认超时(秒,默认 120;复杂推理调大如 300/600)。
# 注:全局默认;单个智能体在数据库配独立 llm_call_timeout 时以其为准。
# WEKNORA_AGENT_LLM_TIMEOUT=300
Expand Down
22 changes: 22 additions & 0 deletions config/plugin_profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# WeKnora plugin composition (Cordis-style profile).
#
# Layers applied onto an empty entry list:
# 1) each named bundle, in order (code-defined; "base" = in-tree web search)
# 2) this file's patch
# 3) WEKNORA_PLUGIN_PATCH overlay (optional extra YAML)
# 4) WEKNORA_PLUGIN_DIR (scan plugins.d/*/plugin.yaml → bundle "external")
# 5) WEKNORA_PLUGINS=id1,id2 (insert-if-missing convenience)
#
# A patch targets a row by id and replaces listed fields, or inserts a new
# row when insert: true. See docs/dev/plugin-architecture.md.
name: standard
bundles:
- base
# patch:
# - id: websearch.exa
# disabled: true
# - id: websearch.echo
# plugin: websearch.echo
# insert: true
# config:
# title: echo
2 changes: 2 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
- [x] 支持与企微、飞书等 IM 系统集成,在 IM 内使用 WeKnora 能力

## 组件与扩展
- [x] 引入 Cordis 风格插件核(`internal/plugin`),联网搜索改为 profile/bundle 组合,不再写进 `container.go`
- [ ] 将其余扩展缝(数据源 / IM / 存储 / 模型 / 检索 / 工具 / 分块 / 解析)迁到同一 Host
- [ ] 鼓励社区维护各厂商的模型服务、网络搜索服务等组件
- [ ] 鼓励社区提供更多与知识库相关的 Skills

Expand Down
285 changes: 285 additions & 0 deletions docs/dev/plugin-architecture.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions docs/wiki/集成扩展/添加网络搜索引擎.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ source: 添加新的网络搜索引擎.md
```
internal/types/web_search_provider.go # 实体定义 + Provider 类型元数据
internal/infrastructure/web_search/ # Provider 实现(bing/google/duckduckgo/tavily)
internal/container/container.go # DI 注册
internal/plugin/websearch/plugins.go # 插件 Host 注册
internal/types/interfaces/web_search.go # WebSearchProvider 接口
```

Expand Down Expand Up @@ -58,9 +58,9 @@ WebSearchProviderTypeBrave WebSearchProviderType = "brave"

在 `isValidProviderType()` 中添加新类型。

### 5. DI 注册
### 5. 插件注册

在 `registerWebSearchProviders` 中注册
在 `internal/plugin/websearch/plugins.go` 的 `builtins()` 中加一行。不要改 `container.go`。树外插件见 `docs/dev/plugin-architecture.md`

### 6. 验证

Expand All @@ -84,7 +84,7 @@ curl http://localhost:8080/api/v1/web-search-providers/types
| `internal/types/web_search_provider.go` | 添加常量 + 类型元数据 |
| `internal/infrastructure/web_search/brave.go` | **新建** Provider 实现 |
| `internal/application/service/web_search_provider.go` | `isValidProviderType` 加新类型 |
| `internal/container/container.go` | `registerWebSearchProviders` 注册 |
| `internal/plugin/websearch/plugins.go` | `builtins()` 注册 |

## 相关主题

Expand Down
2 changes: 2 additions & 0 deletions docs/wiki/项目概述/版本路线图.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ source: ROADMAP.md

## 组件与扩展

- [x] 引入 Cordis 风格插件核(`internal/plugin`),联网搜索改为 profile/bundle 组合,不再写进 `container.go`
- [ ] 将其余扩展缝(数据源 / IM / 存储 / 模型 / 检索 / 工具 / 分块 / 解析)迁到同一 Host
- [ ] 鼓励社区维护各厂商的模型服务、网络搜索服务等组件
- [ ] 鼓励社区提供更多与知识库相关的 Skills

Expand Down
22 changes: 12 additions & 10 deletions docs/添加新的网络搜索引擎.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ internal/
│ ├── google.go # Google 实现
│ ├── duckduckgo.go # DuckDuckGo 实现
│ └── tavily.go # Tavily 实现
├── container/
│ └── container.go # DI 注册(registerWebSearchProviders
├── plugin/
│ └── websearch/plugins.go # 进程级注册(builtins + plugin.Host
└── types/interfaces/
└── web_search.go # WebSearchProvider 接口
```
Expand Down Expand Up @@ -187,19 +187,21 @@ func isValidProviderType(provider types.WebSearchProviderType) bool {
}
```

### 5. 在 DI 容器中注册
### 5. 在插件 bundle 中注册

编辑 `internal/container/container.go` 的 `registerWebSearchProviders` 函数
编辑 `internal/plugin/websearch/plugins.go` 的 `builtins()`(不要改 `container.go`)

```go
func registerWebSearchProviders(registry *infra_web_search.Registry) {
// ... 已有注册 ...

// Register Brave provider type
registry.Register(infra_web_search.BraveProviderTypeInfo(), infra_web_search.NewBraveProvider)
func builtins() []spec {
return []spec{
// ... 已有引擎 ...
{"brave", web_search.NewBraveProvider},
}
}
```

树外插件用 `plugin.Register` + profile / `WEKNORA_PLUGINS`,见 `docs/dev/plugin-architecture.md`。

### 6. 验证

```bash
Expand Down Expand Up @@ -264,4 +266,4 @@ type WebSearchProviderParameters struct {
| `internal/types/web_search_provider.go` | 添加常量 + 类型元数据 |
| `internal/infrastructure/web_search/brave.go` | **新建** Provider 实现 |
| `internal/application/service/web_search_provider.go` | `isValidProviderType` 加新类型 |
| `internal/container/container.go` | `registerWebSearchProviders` 注册 |
| `internal/plugin/websearch/plugins.go` | `builtins()` 注册 |
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ require (
gorm.io/gorm v1.31.1
)

require (
github.com/dlclark/regexp2/v2 v2.5.2 // indirect
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
)

require (
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1602,13 +1602,17 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.5.2 h1:HAsucWRhsqcDzl6Ua9aR8JwYOTzrZyPrF0/FNxJVAI0=
github.com/dlclark/regexp2/v2 v2.5.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6 h1:Oh2rRG1un7tLlC3/NJDzKppZ4CeZGkVFJCUOTRwLpfw=
github.com/dop251/goja v0.0.0-20260806115107-493f22071ef6/go.mod h1:LiIEzozrcvNXorsG/3+ypGqdTUAqZryhzSsqi0oU/Qg=
github.com/duckdb/duckdb-go-bindings v0.10502.0 h1:Uhg/dfvPLQv4cH35lMD48hqUcdOh2Z7bcuykjr4qnOA=
github.com/duckdb/duckdb-go-bindings v0.10502.0/go.mod h1:8KF3oEKrmYdSbZnQ1BPTdxAZDHRaM1LEv+oBvL2nSLk=
github.com/duckdb/duckdb-go-bindings/lib/darwin-amd64 v0.10502.0 h1:1GxSHSI1ef3sCdDVrJ9l8s6aTd7P1K788os9lHrs43g=
Expand Down Expand Up @@ -1819,6 +1823,8 @@ github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy0
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w=
github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c/go.mod h1:oVDCh3qjJMLVUSILBRwrm+Bc6RNXGZYtoh9xdvf1ffM=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
Expand Down Expand Up @@ -1973,6 +1979,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM=
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
Expand Down
22 changes: 6 additions & 16 deletions internal/application/service/web_search_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,7 @@ func (s *webSearchProviderService) DeleteProvider(ctx context.Context, tenantID

// isValidProviderType checks if the given provider type is supported
func isValidProviderType(provider types.WebSearchProviderType) bool {
switch provider {
case types.WebSearchProviderTypeBing,
types.WebSearchProviderTypeGoogle,
types.WebSearchProviderTypeDuckDuckGo,
types.WebSearchProviderTypeTavily,
types.WebSearchProviderTypeOllama,
types.WebSearchProviderTypeBaidu,
types.WebSearchProviderTypeSearxng,
types.WebSearchProviderTypeKeenable,
types.WebSearchProviderTypeMetaso,
types.WebSearchProviderTypeZhipu,
types.WebSearchProviderTypeExa:
return true
default:
return false
}
return types.IsKnownWebSearchProviderType(string(provider))
}

// validateProviderParameters validates required parameters for each provider type
Expand Down Expand Up @@ -192,6 +177,11 @@ func validateProviderParameters(provider types.WebSearchProviderType, params typ
if err := infra_web_search.ValidateSearxngBaseURL(params.BaseURL); err != nil {
return err
}
default:
if info, ok := types.LookupWebSearchProviderType(string(provider)); ok &&
info.RequiresAPIKey && params.APIKey == "" {
return fmt.Errorf("API key is required for %s provider", provider)
}
}
if err := validateOptionalProxyURL(params.ProxyURL); err != nil {
return err
Expand Down
21 changes: 3 additions & 18 deletions internal/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import (
"github.com/Tencent/WeKnora/internal/models/embedding"
"github.com/Tencent/WeKnora/internal/models/limiter"
"github.com/Tencent/WeKnora/internal/models/utils/ollama"
pluginboot "github.com/Tencent/WeKnora/internal/plugin/boot"
"github.com/Tencent/WeKnora/internal/router"
"github.com/Tencent/WeKnora/internal/storageallowlist"
"github.com/Tencent/WeKnora/internal/stream"
Expand Down Expand Up @@ -249,7 +250,8 @@ func BuildContainer(container *dig.Container) *dig.Container {
// Web search service (needed by AgentService)
logger.Debugf(ctx, "[Container] Registering web search registry and providers...")
must(container.Provide(infra_web_search.NewRegistry))
must(container.Invoke(registerWebSearchProviders))
must(container.Provide(pluginboot.NewHost))
must(container.Invoke(pluginboot.Start))
must(container.Provide(repository.NewWebSearchProviderRepository))
must(container.Provide(repository.NewVectorStoreRepository))
must(container.Provide(repository.NewStorageBackendRepository))
Expand Down Expand Up @@ -1590,23 +1592,6 @@ func NewDuckDB() (*sql.DB, error) {
return sqlDB, nil
}

// registerWebSearchProviders registers all web search provider types to the registry.
// Each provider type is registered with its factory function that accepts parameters.
// Provider instances are created on-demand when tenants configure them.
func registerWebSearchProviders(registry *infra_web_search.Registry) {
registry.Register("duckduckgo", infra_web_search.NewDuckDuckGoProvider)
registry.Register("google", infra_web_search.NewGoogleProvider)
registry.Register("bing", infra_web_search.NewBingProvider)
registry.Register("tavily", infra_web_search.NewTavilyProvider)
registry.Register("ollama", infra_web_search.NewOllamaProvider)
registry.Register("baidu", infra_web_search.NewBaiduProvider)
registry.Register("searxng", infra_web_search.NewSearxngProvider)
registry.Register("keenable", infra_web_search.NewKeenableProvider)
registry.Register("zhipu", infra_web_search.NewZhipuProvider)
registry.Register("exa", infra_web_search.NewExaProvider)
registry.Register("metaso", infra_web_search.NewMetasoProvider)
}

// registerIMService registers adapter factories, loads enabled channels, and
// wires the process-lifetime shutdown hook. Each platform's factory lives in
// its own subpackage to keep this file focused on wiring.
Expand Down
27 changes: 27 additions & 0 deletions internal/infrastructure/web_search/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ func (r *Registry) Register(id string, factory ProviderFactory) {
r.factories[id] = factory
}

// Unregister removes a provider type factory. It is the reverse of Register
// and is used when a plugin isolate unloads.
func (r *Registry) Unregister(id string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.factories, id)
}

// Has reports whether a provider type is registered.
func (r *Registry) Has(id string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.factories[id]
return ok
}

// List returns registered provider type IDs in unspecified order.
func (r *Registry) List() []string {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, 0, len(r.factories))
for id := range r.factories {
out = append(out, id)
}
return out
}

// CreateProvider creates a provider instance by type with the given parameters.
func (r *Registry) CreateProvider(providerType string, params types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) {
r.mu.RLock()
Expand Down
40 changes: 40 additions & 0 deletions internal/infrastructure/web_search/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package web_search

import (
"context"
"testing"

"github.com/Tencent/WeKnora/internal/types"
"github.com/Tencent/WeKnora/internal/types/interfaces"
)

type stubProvider struct{ name string }

func (s stubProvider) Name() string { return s.name }
func (s stubProvider) Search(context.Context, string, int, bool) ([]*types.WebSearchResult, error) {
return nil, nil
}

func TestRegistryRegisterHasListUnregister(t *testing.T) {
r := NewRegistry()
r.Register("echo", func(types.WebSearchProviderParameters) (interfaces.WebSearchProvider, error) {
return stubProvider{name: "echo"}, nil
})
if !r.Has("echo") {
t.Fatal("expected echo to be registered")
}
if got := r.List(); len(got) != 1 || got[0] != "echo" {
t.Fatalf("list = %v", got)
}
p, err := r.CreateProvider("echo", types.WebSearchProviderParameters{})
if err != nil || p.Name() != "echo" {
t.Fatalf("create = %v, %v", p, err)
}
r.Unregister("echo")
if r.Has("echo") {
t.Fatal("echo should be gone")
}
if _, err := r.CreateProvider("echo", types.WebSearchProviderParameters{}); err == nil {
t.Fatal("expected missing provider error")
}
}
Loading
Loading