Files
WeKnora/internal/plugin/hostapi/datasources.go
wizardchen 9c20afa259 feat(plugin): finish P2 - sync trigger, event gaps, off hints, icons, editors, netns
- 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.
2026-09-26 13:54:34 +08:00

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
}