mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-14 18:36:39 +08:00
- Host API scope "datasources": a plugin lists the workspace's data sources of its own connectors and starts incremental syncs (skipped while one is running). Go Host.DataSources / SyncDataSource, Python data_sources / sync_data_source. The Jira example's issues webhook uses it to sync the affected projects at once, checking Jira's X-Hub-Signature when a secret is set. - Events: deleting a whole knowledge base now publishes knowledge.deleted per document; documents the wiki drain completes and stalled documents housekeeping fails publish ingested / failed. - Instance lists (data sources, web search, models, IM channels) tag instances whose plugin is switched off in the workspace, and name plugin types from the contribution listing when the type list no longer has them. - Plugin cards show icons: the package icon as manifest.iconData (a data URI filled when the package opens), builtin model vendors' logos, and the app's brand logos for other builtins. - Instance editors: connectors and webSearch contributions may name an editor page shown below the generated form; it reads the instance and sets values through the new form.set bridge call and "values" event. Plugin connector settings now stay editable while stored credentials are kept. - WEKNORA_PLUGIN_NETNS=1 (Linux) runs host plugins in their own user and network namespace through the plugin-sandbox helper, which relays the egress proxy and the Host API over unix sockets; plugins cannot bypass the proxy. Starting fails, clearly, where user namespaces are refused.
104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
package hostapi
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Tencent/WeKnora/internal/types"
|
|
"github.com/Tencent/WeKnora/pluginsdk/pluginapi"
|
|
)
|
|
|
|
// ScopeDataSources lets a plugin see the workspace's data sources of its
|
|
// own connectors and start their syncs, e.g. when a webhook says the
|
|
// source changed.
|
|
const ScopeDataSources = "datasources"
|
|
|
|
// runningSyncWindow is how long a running sync keeps new requests from
|
|
// queueing another; older runs are presumed stuck.
|
|
const runningSyncWindow = 30 * time.Minute
|
|
|
|
// DataSourceStore finds data sources by type.
|
|
type DataSourceStore interface {
|
|
// FindByTenantAndTypePrefix lists a tenant's data sources whose type
|
|
// starts with prefix.
|
|
FindByTenantAndTypePrefix(ctx context.Context, tenantID uint64, prefix string) ([]*types.DataSource, error)
|
|
}
|
|
|
|
// DataSourceSyncer starts syncs and reads their history.
|
|
type DataSourceSyncer interface {
|
|
ManualSync(ctx context.Context, dsID string) (*types.SyncLog, error)
|
|
GetSyncLogs(ctx context.Context, dsID string, limit, offset int) ([]*types.SyncLog, error)
|
|
}
|
|
|
|
// DataSources serves the datasources scope. A plugin only ever sees the
|
|
// data sources of its own connectors ("<plugin>/<connector>") in the
|
|
// token's tenant.
|
|
type DataSources struct {
|
|
store DataSourceStore
|
|
syncer DataSourceSyncer
|
|
now func() time.Time
|
|
}
|
|
|
|
// NewDataSources creates the scope.
|
|
func NewDataSources(store DataSourceStore, syncer DataSourceSyncer) *DataSources {
|
|
return &DataSources{store: store, syncer: syncer, now: time.Now}
|
|
}
|
|
|
|
func (d *DataSources) own(ctx context.Context, c *Claims) ([]*types.DataSource, error) {
|
|
return d.store.FindByTenantAndTypePrefix(ctx, c.TenantID, c.PluginID+"/")
|
|
}
|
|
|
|
// List lists the plugin's data sources in the tenant.
|
|
func (d *DataSources) List(ctx context.Context, c *Claims) (*pluginapi.DataSourceList, error) {
|
|
list, err := d.own(ctx, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := &pluginapi.DataSourceList{DataSources: make([]pluginapi.DataSourceInfo, 0, len(list))}
|
|
for _, ds := range list {
|
|
info := pluginapi.DataSourceInfo{
|
|
ID: ds.ID, Name: ds.Name, KnowledgeBaseID: ds.KnowledgeBaseID,
|
|
Connector: strings.TrimPrefix(ds.Type, c.PluginID+"/"), Status: ds.Status, LastSyncAt: ds.LastSyncAt,
|
|
ResourceIDs: []string{},
|
|
}
|
|
if cfg, err := ds.ParseConfig(); err == nil && cfg != nil && cfg.ResourceIDs != nil {
|
|
info.ResourceIDs = cfg.ResourceIDs
|
|
}
|
|
out.DataSources = append(out.DataSources, info)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Sync starts an incremental sync of one of the plugin's data sources,
|
|
// unless one started recently and is still running: bursts of webhooks
|
|
// then cost one sync, and the next scheduled or triggered sync picks up
|
|
// what that one missed.
|
|
func (d *DataSources) Sync(ctx context.Context, c *Claims, id string) (*pluginapi.SyncStarted, error) {
|
|
list, err := d.own(ctx, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var ds *types.DataSource
|
|
for _, candidate := range list {
|
|
if candidate.ID == id {
|
|
ds = candidate
|
|
}
|
|
}
|
|
if ds == nil {
|
|
return nil, pluginapi.Errorf(pluginapi.CodeNotFound, "no data source %q of this plugin", id)
|
|
}
|
|
ctx = context.WithValue(ctx, types.TenantIDContextKey, c.TenantID)
|
|
if logs, err := d.syncer.GetSyncLogs(ctx, id, 1, 0); err == nil && len(logs) > 0 {
|
|
last := logs[0]
|
|
if last.Status == types.SyncLogStatusRunning && d.now().Sub(last.StartedAt) < runningSyncWindow {
|
|
return &pluginapi.SyncStarted{Status: pluginapi.SyncRunning, SyncLogID: last.ID}, nil
|
|
}
|
|
}
|
|
started, err := d.syncer.ManualSync(ctx, id)
|
|
if err != nil {
|
|
return nil, pluginapi.Errorf(pluginapi.CodeUnavailable, "start sync: %v", err)
|
|
}
|
|
return &pluginapi.SyncStarted{Status: pluginapi.SyncQueued, SyncLogID: started.ID}, nil
|
|
}
|