WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s

This commit is contained in:
yawan
2026-09-22 16:19:06 +08:00
commit f7fc992f57
3929 changed files with 1006607 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/event"
"github.com/Tencent/WeKnora/internal/models/chat"
"github.com/Tencent/WeKnora/internal/models/rerank"
"github.com/Tencent/WeKnora/internal/types"
)
// AgentStreamEvent represents a streaming event from the agent
type AgentStreamEvent struct {
Type string `json:"type"` // "thought", "tool_call", "tool_result", "final_answer", "error", "references"
Content string `json:"content"` // Incremental content
Data map[string]interface{} `json:"data"` // Additional structured data
Done bool `json:"done"` // Whether this is the last event
Iteration int `json:"iteration"` // Current iteration number
}
// AgentEngine defines the interface for agent execution engine
type AgentEngine interface {
// Execute executes the agent with conversation history and returns a stream of events
// imageURLs is optional - when provided, images are passed to the LLM as multimodal content
Execute(
ctx context.Context,
sessionID, messageID, query string,
llmContext []chat.Message,
imageURLs ...[]string,
) (*types.AgentState, error)
// SetMemoryPrompt supplies the long-term memory envelope for this run.
// It must be called before Execute; an empty string is a no-op.
SetMemoryPrompt(prompt string)
}
// AgentService defines the interface for agent-related operations
type AgentService interface {
// CreateAgentEngine creates an agent engine with the given configuration and EventBus.
// Conversation history is loaded by the caller (see service.LoadAgentHistory) and
// passed into AgentEngine.Execute; the engine itself is stateless across turns.
CreateAgentEngine(
ctx context.Context,
config *types.AgentConfig,
chatModel chat.Chat,
rerankModel rerank.Reranker,
eventBus *event.EventBus,
sessionID, assistantMessageID string,
) (AgentEngine, error)
// ValidateConfig validates an agent configuration
ValidateConfig(config *types.AgentConfig) error
}
+77
View File
@@ -0,0 +1,77 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/gin-gonic/gin"
)
// AuditLogQuery is the cursor + filter set for listing audit log
// entries. AfterID is the last id from the previous page (rows with
// id < AfterID are returned, newest first); 0 means "from the top".
// Limit is capped at 100 inside the repository regardless of caller
// input — keeps unbounded scans off the table.
type AuditLogQuery struct {
AfterID uint64
Limit int
Action types.AuditAction
Outcome types.AuditOutcome
ActorUserID string
ScopeType string
ScopeID string
UnscopedOnly bool
}
// AuditLogRepository is the storage primitive for the audit table.
// All writes are inserts (immutable rows); the only "update" surface
// is none — once written, an entry is permanent.
type AuditLogRepository interface {
Create(ctx context.Context, entry *types.AuditLog) error
List(ctx context.Context, tenantID uint64, q *AuditLogQuery) ([]*types.AuditLog, error)
// CountSinceForDedup is the rate-limit primitive for LogDenied —
// returns the count of matching rows in the trailing window so the
// service can skip writing duplicates. Filter is
// (tenant_id, actor_user_id, action, request_path, created_at >= since).
CountSinceForDedup(
ctx context.Context,
tenantID uint64,
actorUserID string,
action types.AuditAction,
requestPath string,
since time.Time,
) (int64, error)
// DeleteOlderThan removes audit rows whose created_at is strictly
// before cutoff and returns the affected row count. It is the
// retention primitive driven by the daily background sweep.
// Implementations should delete in a single statement (no per-row
// fetch) so the long-tail cost stays at "one DELETE per sweep".
DeleteOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
}
// AuditLogService is the high-level audit API the rest of the codebase
// uses. It owns timestamp defaulting (Log) and rate-limit dedup
// (LogDenied) so callers don't have to think about either.
type AuditLogService interface {
// Log writes a single audit entry. Callers fill TenantID + Action +
// any per-event fields; the service fills CreatedAt if zero.
Log(ctx context.Context, entry *types.AuditLog) error
// LogDenied records a middleware-level reject decision. Subject to
// 1-minute sliding-window dedup keyed by
// (tenant_id, actor_user_id, action, request_path) so a probing
// client cannot flood the table.
LogDenied(
ctx context.Context,
c *gin.Context,
tenantID uint64,
actorUserID, actorRole string,
requiredRole types.TenantRole,
) error
List(ctx context.Context, tenantID uint64, q *AuditLogQuery) ([]*types.AuditLog, error)
// Purge deletes rows whose created_at is strictly older than the
// retention horizon. retentionDays <= 0 makes the call a no-op,
// which keeps the daily sweep cheap when retention is disabled.
// Returns rows deleted; transient repo errors propagate.
Purge(ctx context.Context, retentionDays int) (int64, error)
}
+178
View File
@@ -0,0 +1,178 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// ChunkImageInfo holds (knowledge_id, image_info) pairs for image cleanup before chunk deletion.
type ChunkImageInfo struct {
KnowledgeID string `gorm:"column:knowledge_id"`
ImageInfo string `gorm:"column:image_info"`
}
// ChunkRepository defines the interface for chunk repository operations
type ChunkRepository interface {
// CreateChunks creates chunks
CreateChunks(ctx context.Context, chunks []*types.Chunk) error
// GetChunkByID gets a chunk by id
GetChunkByID(ctx context.Context, tenantID uint64, id string) (*types.Chunk, error)
// GetChunkByIDOnly gets a chunk by id without tenant filter (for permission resolution)
GetChunkByIDOnly(ctx context.Context, id string) (*types.Chunk, error)
// GetChunkBySeqID gets a chunk by seq_id
GetChunkBySeqID(ctx context.Context, tenantID uint64, seqID int64) (*types.Chunk, error)
// ListChunksByID lists chunks by ids
ListChunksByID(ctx context.Context, tenantID uint64, ids []string) ([]*types.Chunk, error)
// ListChunksByIDOnly lists chunks by ids without tenant filter (for shared KB resolution).
ListChunksByIDOnly(ctx context.Context, ids []string) ([]*types.Chunk, error)
// ListChunksBySeqID lists chunks by seq_ids
ListChunksBySeqID(ctx context.Context, tenantID uint64, seqIDs []int64) ([]*types.Chunk, error)
// ListChunksByKnowledgeID lists the knowledge's text chunks. Despite the name
// it filters to chunk_type = 'text'; reach for ListChunksByKnowledgeIDAndTypes
// when summary, parent_text or image chunks are needed too.
ListChunksByKnowledgeID(ctx context.Context, tenantID uint64, knowledgeID string) ([]*types.Chunk, error)
// ListChunksByKnowledgeIDAndTypes lists the knowledge's chunks restricted to
// the given chunk types, ordered by chunk_index.
ListChunksByKnowledgeIDAndTypes(
ctx context.Context, tenantID uint64, knowledgeID string, chunkTypes []types.ChunkType,
) ([]*types.Chunk, error)
// ListPagedChunksByKnowledgeID lists paged chunks by knowledge id.
// When tagIDs is non-empty, results are filtered by tag_id (OR semantics).
// knowledgeType: "faq" or "manual" - determines sort order and search behavior
// - FAQ: sorts by updated_at, searchField can be "standard_question", "similar_questions", "answers", or "" for all
// - Document (manual): sorts by chunk_index, keyword searches content only
// sortOrder: "asc" for ascending, default is descending
// searchField: specifies which field to search in (only applicable for FAQ type)
// isEnabled: when non-nil, filters chunks by their enabled state. Agent/model
// consumers must pass true so disabled content never enters model context.
ListPagedChunksByKnowledgeID(
ctx context.Context,
tenantID uint64,
knowledgeID string,
page *types.Pagination,
chunkType []types.ChunkType,
tagIDs []string,
keyword string,
searchField string,
sortOrder string,
knowledgeType string,
isEnabled *bool,
) ([]*types.Chunk, int64, error)
ListChunkByParentID(ctx context.Context, tenantID uint64, parentID string) ([]*types.Chunk, error)
// ListChunksByParentIDs lists chunks whose parent_chunk_id is in the given list
ListChunksByParentIDs(ctx context.Context, tenantID uint64, parentIDs []string) ([]*types.Chunk, error)
// UpdateChunk updates a chunk
UpdateChunk(ctx context.Context, chunk *types.Chunk) error
// CreateChunkRevision stores an immutable snapshot of a superseded revision.
CreateChunkRevision(ctx context.Context, revision *types.ChunkRevision) error
// SaveChunkRevision atomically snapshots the old row and applies the new
// row only when its revision still matches expectedRevision.
SaveChunkRevision(ctx context.Context, chunk *types.Chunk, revision *types.ChunkRevision, expectedRevision int) error
// ListChunkRevisions returns snapshots ordered newest first.
ListChunkRevisions(ctx context.Context, tenantID uint64, chunkID string) ([]*types.ChunkRevision, error)
// GetChunkRevision returns one historical snapshot.
GetChunkRevision(ctx context.Context, tenantID uint64, chunkID string, revision int) (*types.ChunkRevision, error)
// UpdateChunks updates chunks in batch
UpdateChunks(ctx context.Context, chunks []*types.Chunk) error
// SaveChunks persists full chunk objects in a single transaction using GORM Save (UPDATE).
SaveChunks(ctx context.Context, chunks []*types.Chunk) error
// DeleteChunk deletes a chunk
DeleteChunk(ctx context.Context, tenantID uint64, id string) error
// DeleteChunks deletes chunks by IDs in batch
DeleteChunks(ctx context.Context, tenantID uint64, ids []string) error
// DeleteChunksByKnowledgeID deletes chunks by knowledge id
DeleteChunksByKnowledgeID(ctx context.Context, tenantID uint64, knowledgeID string) error
// DeleteByKnowledgeList deletes all chunks for a knowledge list
DeleteByKnowledgeList(ctx context.Context, tenantID uint64, knowledgeIDs []string) error
// ListImageInfoByKnowledgeIDs returns non-empty (knowledge_id, image_info) pairs for image cleanup.
ListImageInfoByKnowledgeIDs(ctx context.Context, tenantID uint64, knowledgeIDs []string) ([]ChunkImageInfo, error)
// MoveChunksByKnowledgeID updates knowledge_base_id for all chunks of a knowledge item
MoveChunksByKnowledgeID(ctx context.Context, tenantID uint64, knowledgeID string, targetKBID string) error
// DeleteChunksByTagID deletes all chunks with the specified tag ID
// Returns the IDs of deleted chunks for index cleanup
DeleteChunksByTagID(ctx context.Context, tenantID uint64, kbID string, tagID string, excludeIDs []string) ([]string, error)
// CountChunksByKnowledgeBaseID counts the number of chunks in a knowledge base.
CountChunksByKnowledgeBaseID(ctx context.Context, tenantID uint64, kbID string) (int64, error)
// DeleteUnindexedChunks deletes unindexed chunks by knowledge id and chunk index range
DeleteUnindexedChunks(ctx context.Context, tenantID uint64, knowledgeID string) ([]*types.Chunk, error)
// ListAllFAQChunksByKnowledgeID lists all FAQ chunks for a knowledge ID
// only ID and ContentHash fields for efficiency
ListAllFAQChunksByKnowledgeID(ctx context.Context, tenantID uint64, knowledgeID string) ([]*types.Chunk, error)
// ListAllFAQChunksWithMetadataByKnowledgeBaseID lists all FAQ chunks for a knowledge base ID
// returns ID and Metadata fields for duplicate question checking
ListAllFAQChunksWithMetadataByKnowledgeBaseID(ctx context.Context, tenantID uint64, kbID string) ([]*types.Chunk, error)
// FindFAQChunkWithDuplicateQuestion finds a single FAQ chunk whose standard_question or
// similar_questions overlap with the given question list. Returns nil if no duplicate found.
FindFAQChunkWithDuplicateQuestion(ctx context.Context, tenantID uint64, kbID string, excludeChunkID string, questions []string) (*types.Chunk, error)
// ListAllFAQChunksForExport lists all FAQ chunks for export with full metadata, tag_id, is_enabled, and flags
ListAllFAQChunksForExport(ctx context.Context, tenantID uint64, knowledgeID string) ([]*types.Chunk, error)
// UpdateChunkFlagsBatch updates flags for multiple chunks in batch using a single SQL statement.
// setFlags: map of chunk ID to flags to set (OR operation)
// clearFlags: map of chunk ID to flags to clear (AND NOT operation)
UpdateChunkFlagsBatch(ctx context.Context, tenantID uint64, kbID string, setFlags map[string]types.ChunkFlags, clearFlags map[string]types.ChunkFlags) error
// UpdateChunkFieldsByTagID updates fields for all chunks with the specified tag ID.
// Supports updating is_enabled, flags, and tag_id fields.
// newTagID: if not nil, updates tag_id to this value (empty string means uncategorized)
UpdateChunkFieldsByTagID(ctx context.Context, tenantID uint64, kbID string, tagID string, isEnabled *bool, setFlags types.ChunkFlags, clearFlags types.ChunkFlags, newTagID *string, excludeIDs []string) ([]string, error)
// FAQChunkDiff compares FAQ chunks between two knowledge bases and returns the differences.
FAQChunkDiff(ctx context.Context, srcTenantID uint64, srcKBID string, dstTenantID uint64, dstKBID string) (*types.FAQChunkDiffResult, error)
// ListFAQChunkStatusByIDs loads status fields for FAQ clone sync.
ListFAQChunkStatusByIDs(ctx context.Context, tenantID uint64, ids []string) (map[string]*types.FAQChunkStatus, error)
// ListRecommendedFAQChunks lists FAQ chunks with the recommended flag set.
// Filter by explicitly selected kbIDs, knowledgeIDs, and/or FAQ tagIDs.
// Returns up to `limit` chunks sorted by updated_at descending.
ListRecommendedFAQChunks(ctx context.Context, tenantID uint64, kbIDs []string, knowledgeIDs []string, tagIDs []string, limit int) ([]*types.Chunk, error)
// ListRecentDocumentChunksWithQuestions lists recent document chunks that have generated questions.
// Filter by kbIDs and/or knowledgeIDs. At least one of them must be non-empty.
// Returns up to `limit` chunks sorted by updated_at descending.
ListRecentDocumentChunksWithQuestions(ctx context.Context, tenantID uint64, kbIDs []string, knowledgeIDs []string, limit int) ([]*types.Chunk, error)
}
// ChunkService defines the interface for chunk service operations
type ChunkService interface {
// CreateChunks creates chunks
CreateChunks(ctx context.Context, chunks []*types.Chunk) error
// GetChunkByID gets a chunk by id (uses tenant from context)
GetChunkByID(ctx context.Context, id string) (*types.Chunk, error)
// GetChunkByIDOnly gets a chunk by id without tenant filter (for permission resolution)
GetChunkByIDOnly(ctx context.Context, id string) (*types.Chunk, error)
// ListChunksByKnowledgeID lists chunks by knowledge id
ListChunksByKnowledgeID(ctx context.Context, knowledgeID string) ([]*types.Chunk, error)
// ListPagedChunksByKnowledgeID lists paged chunks by knowledge id
ListPagedChunksByKnowledgeID(
ctx context.Context,
knowledgeID string,
page *types.Pagination,
chunkType []types.ChunkType,
) (*types.PageResult, error)
// UpdateChunk updates a chunk
UpdateChunk(ctx context.Context, chunk *types.Chunk) error
// UpdateChunks updates chunks in batch
UpdateChunks(ctx context.Context, chunks []*types.Chunk) error
// DeleteChunk deletes a chunk
DeleteChunk(ctx context.Context, id string) error
// DeleteChunks deletes chunks by IDs in batch
DeleteChunks(ctx context.Context, ids []string) error
// DeleteChunksByKnowledgeID deletes chunks by knowledge id
DeleteChunksByKnowledgeID(ctx context.Context, knowledgeID string) error
// DeleteByKnowledgeList deletes all chunks for a knowledge list
DeleteByKnowledgeList(ctx context.Context, ids []string) error
// ListChunkByParentID lists chunks by parent id
ListChunkByParentID(ctx context.Context, tenantID uint64, parentID string) ([]*types.Chunk, error)
// GetRepository gets the chunk repository
GetRepository() ChunkRepository
// DeleteGeneratedQuestion deletes a single generated question from a chunk by question ID
// This updates the chunk metadata and removes the corresponding vector index
DeleteGeneratedQuestion(ctx context.Context, chunkID string, questionID string) error
// UpdateDocumentChunk applies a revision-checked edit and synchronizes retrieval indices.
UpdateDocumentChunk(ctx context.Context, chunkID string, content *string, isEnabled *bool, expectedRevision *int) (*types.Chunk, error)
// ListChunkRevisions lists immutable snapshots for a chunk.
ListChunkRevisions(ctx context.Context, chunkID string) ([]*types.ChunkRevision, error)
// RevertDocumentChunk restores a historical revision as a new current revision.
RevertDocumentChunk(ctx context.Context, chunkID string, revision int, expectedRevision *int) (*types.Chunk, error)
// UpsertGeneratedQuestion creates or updates a generated retrieval question.
UpsertGeneratedQuestion(ctx context.Context, chunkID string, questionID string, question string) (*types.GeneratedQuestion, error)
}
+159
View File
@@ -0,0 +1,159 @@
// Package interfaces defines the interface contracts for custom agent management
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// CustomAgentService defines the custom agent service interface
// Provides high-level operations for agent creation, querying, updating, and deletion
type CustomAgentService interface {
// CreateAgent creates a new custom agent
// Parameters:
// - ctx: Context information, carrying request tracking, user identity, etc.
// - agent: Agent object containing basic information and configuration
// Returns:
// - Created agent object (including automatically generated ID)
// - Possible errors such as insufficient permissions, validation errors, etc.
CreateAgent(ctx context.Context, agent *types.CustomAgent) (*types.CustomAgent, error)
// GetAgentByID retrieves agent information by ID (uses tenant from context)
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the agent
// Returns:
// - Agent object, if found (including built-in agents)
// - Possible errors such as not existing, insufficient permissions, etc.
GetAgentByID(ctx context.Context, id string) (*types.CustomAgent, error)
// GetAgentByIDAndTenant reads the current tenant without builtin resolution.
// Cross-tenant organization shares must use AgentShareService.
GetAgentByIDAndTenant(ctx context.Context, id string, tenantID uint64) (*types.CustomAgent, error)
// ListAgents lists all agents under the current tenant (including built-in agents)
// Parameters:
// - ctx: Context information, containing tenant information
// Returns:
// - List of agent objects (built-in agents first, then custom agents sorted by creation time)
// - Possible errors such as insufficient permissions, etc.
ListAgents(ctx context.Context) ([]*types.CustomAgent, error)
// UpdateAgent updates agent information
// Parameters:
// - ctx: Context information
// - agent: Agent object containing update information
// Returns:
// - Updated agent object
// - Possible errors such as not existing, insufficient permissions, cannot modify built-in, etc.
UpdateAgent(ctx context.Context, agent *types.CustomAgent) (*types.CustomAgent, error)
// DeleteAgent deletes an agent
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the agent
// Returns:
// - Possible errors such as not existing, insufficient permissions, cannot delete built-in, etc.
DeleteAgent(ctx context.Context, id string) error
// CopyAgent creates a copy of an existing agent
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the agent to copy
// Returns:
// - The newly created agent copy
// - Possible errors such as not existing, insufficient permissions, etc.
CopyAgent(ctx context.Context, id string) (*types.CustomAgent, error)
// GetSuggestedQuestions returns suggested questions for the agent based on its
// associated knowledge bases. When kbIDs or knowledgeIDs are provided, they override
// the agent's default knowledge base selection.
// Parameters:
// - ctx: Context information
// - agentID: Agent ID
// - kbIDs: Optional knowledge base IDs to override agent config
// - knowledgeIDs: Optional knowledge item IDs to further filter
// - tagScopes: Optional KB-scoped knowledge tags (OR semantics within each KB)
// - limit: Maximum number of questions to return
// Returns:
// - List of suggested questions
// - Possible errors
GetSuggestedQuestions(ctx context.Context, agentID string, kbIDs []string, knowledgeIDs []string, tagScopes []types.TagScope, limit int) ([]types.SuggestedQuestion, error)
// GetKnowledgeSuggestedQuestions returns only knowledge-derived candidates.
// It is independent of whether starter suggestions are enabled and is used
// as a source/fallback for contextual follow-up generation.
GetKnowledgeSuggestedQuestions(ctx context.Context, agentID string, kbIDs []string, knowledgeIDs []string, tagScopes []types.TagScope, limit int) ([]types.SuggestedQuestion, error)
}
// CustomAgentRepository defines the custom agent repository interface
// Responsible for agent data persistence and retrieval
type CustomAgentRepository interface {
// CreateAgent creates an agent record
// Parameters:
// - ctx: Context information
// - agent: Agent object
// Returns:
// - Possible errors such as database connection failure, unique constraint conflicts, etc.
CreateAgent(ctx context.Context, agent *types.CustomAgent) error
// GetAgentByID queries an agent by ID and tenant
// Parameters:
// - ctx: Context information
// - id: Agent ID
// - tenantID: Tenant ID for isolation
// Returns:
// - Agent object, if found
// - Possible errors such as record not existing, database errors, etc.
GetAgentByID(ctx context.Context, id string, tenantID uint64) (*types.CustomAgent, error)
// ListAgentsByTenantID lists all agents for a specific tenant
// Parameters:
// - ctx: Context information
// - tenantID: Tenant ID
// Returns:
// - List of agent objects
// - Possible errors such as database errors, etc.
ListAgentsByTenantID(ctx context.Context, tenantID uint64) ([]*types.CustomAgent, error)
// UpdateAgent updates an agent record
// Parameters:
// - ctx: Context information
// - agent: Agent object containing update information
// Returns:
// - Possible errors such as record not existing, database errors, etc.
UpdateAgent(ctx context.Context, agent *types.CustomAgent) error
// DeleteAgent deletes an agent record
// Parameters:
// - ctx: Context information
// - id: Agent ID
// - tenantID: Tenant ID for isolation (required for composite primary key)
// Returns:
// - Possible errors such as record not existing, database errors, etc.
DeleteAgent(ctx context.Context, id string, tenantID uint64) error
// CountByModelID counts active agents in the tenant whose config references
// the given model ID (chat, rerank, VLM, ASR, query-understand, etc.).
CountByModelID(ctx context.Context, tenantID uint64, modelID string) (int64, error)
// ListModelUsages returns the minimal active agent projections that
// reference the model, with every matching binding merged per object.
// Implementations must cap the result at types.ModelUsageListLimit;
// callers that need the untruncated size should use CountByModelID.
ListModelUsages(ctx context.Context, tenantID uint64, modelID string) ([]types.ModelUsageResource, error)
// CountBySandboxConfigID counts agents pointing at a sandbox config.
//
// Used only to warn the admin which agents reference a config; never use it
// to refuse operations. Agent references are permanent state, so blocking on
// them would make credential rotation impossible.
CountBySandboxConfigID(ctx context.Context, tenantID uint64, configID string) (int64, error)
// ListNamesBySandboxConfigID returns agent names pointing at a sandbox config.
//
// Used only to warn the admin which agents reference a config; never use it
// to refuse operations. Agent references are permanent state, so blocking on
// them would make credential rotation impossible.
ListNamesBySandboxConfigID(ctx context.Context, tenantID uint64, configID string) ([]string, error)
}
+128
View File
@@ -0,0 +1,128 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
)
// DataSourceService defines the interface for data source management operations
type DataSourceService interface {
// CreateDataSource creates a new data source configuration
CreateDataSource(ctx context.Context, ds *types.DataSource) (*types.DataSource, error)
// GetDataSource retrieves a data source by ID
GetDataSource(ctx context.Context, id string) (*types.DataSource, error)
// ListDataSources lists all data sources for a knowledge base
ListDataSources(ctx context.Context, kbID string) ([]*types.DataSource, error)
// UpdateDataSource updates an existing data source
UpdateDataSource(ctx context.Context, ds *types.DataSource) (*types.DataSource, error)
// DeleteDataSource deletes a data source (soft delete)
DeleteDataSource(ctx context.Context, id string) error
// UpdateDataSourceCredentials replaces the connector credential map.
// DataSource credentials are per-connector atomic — there is no
// individual-field PUT, the whole map gets replaced. Returns the updated
// data source so the caller can re-fetch the redacted shape.
UpdateDataSourceCredentials(
ctx context.Context, id string, credentials map[string]interface{},
) (*types.DataSource, error)
// ClearDataSourceCredentials wipes the connector credential map.
// Idempotent on already-empty credentials.
ClearDataSourceCredentials(ctx context.Context, id string) error
// ValidateConnection tests the connection to an external data source
ValidateConnection(ctx context.Context, dsID string) error
// ValidateCredentials tests connectivity using raw credentials without persisting anything.
// This is used by the frontend "Test Connection" button before creating a data source.
ValidateCredentials(ctx context.Context, connectorType string, credentials map[string]interface{}) error
// ListAvailableResources lists resources available for sync in the external system.
// parentID enables lazy loading: "" lists the top level, a resource ExternalID lists its children.
ListAvailableResources(ctx context.Context, dsID string, parentID string) ([]types.Resource, error)
// ResolveResourceAncestors returns the deduplicated ExternalIDs of every
// ancestor that must be expanded to reveal the given (possibly deeply nested)
// resources in a lazily-loaded picker. Used to restore an existing selection
// when editing a data source.
ResolveResourceAncestors(ctx context.Context, dsID string, resourceIDs []string) ([]string, error)
// ManualSync triggers an immediate sync for a data source
ManualSync(ctx context.Context, dsID string) (*types.SyncLog, error)
// PauseDataSource pauses a data source's scheduled syncs
PauseDataSource(ctx context.Context, id string) error
// ResumeDataSource resumes a paused data source
ResumeDataSource(ctx context.Context, id string) error
// GetSyncLogs retrieves sync history for a data source
GetSyncLogs(ctx context.Context, dsID string, limit int, offset int) ([]*types.SyncLog, error)
// GetSyncLog retrieves a specific sync log entry
GetSyncLog(ctx context.Context, syncLogID string) (*types.SyncLog, error)
// ProcessSync handles the actual sync operation (called by asynq task)
ProcessSync(ctx context.Context, task *asynq.Task) error
}
// DataSourceRepository defines database access patterns for data sources
type DataSourceRepository interface {
// Create inserts a new data source record
Create(ctx context.Context, ds *types.DataSource) error
// FindByID retrieves a data source by ID
FindByID(ctx context.Context, id string) (*types.DataSource, error)
// FindByKnowledgeBase lists all data sources for a knowledge base
FindByKnowledgeBase(ctx context.Context, kbID string) ([]*types.DataSource, error)
// Update updates an existing data source
Update(ctx context.Context, ds *types.DataSource) error
// UpdateSyncState updates only fields produced by a sync run.
UpdateSyncState(ctx context.Context, ds *types.DataSource) error
// Delete performs a soft delete
Delete(ctx context.Context, id string) error
// FindActive retrieves all active data sources (used for scheduling)
FindActive(ctx context.Context) ([]*types.DataSource, error)
}
// SyncLogRepository defines database access patterns for sync logs
type SyncLogRepository interface {
// Create inserts a new sync log entry
Create(ctx context.Context, log *types.SyncLog) error
// FindByID retrieves a sync log by ID
FindByID(ctx context.Context, id string) (*types.SyncLog, error)
// FindByDataSource lists sync logs for a data source with pagination
FindByDataSource(ctx context.Context, dsID string, limit int, offset int) ([]*types.SyncLog, error)
// FindLatest retrieves the most recent sync log for a data source
FindLatest(ctx context.Context, dsID string) (*types.SyncLog, error)
// HasRunningSync checks if a data source has any sync currently in "running" status.
// Used to prevent overlapping sync executions.
HasRunningSync(ctx context.Context, dsID string) (bool, error)
// Update updates an existing sync log entry
Update(ctx context.Context, log *types.SyncLog) error
// UpdateResult updates only fields produced by a sync run.
UpdateResult(ctx context.Context, log *types.SyncLog) error
// CancelPendingByDataSource marks all non-terminal sync logs for a data source as canceled.
CancelPendingByDataSource(ctx context.Context, dsID string) error
// CleanupOldLogs deletes sync logs older than the retention period
CleanupOldLogs(ctx context.Context, retentionDays int) error
}
@@ -0,0 +1,25 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// DocReader is the core interface for reading documents into markdown.
type DocReader interface {
Read(ctx context.Context, req *types.ReadRequest) (*types.ReadResult, error)
}
// DocumentReader extends DocReader with transport lifecycle management
// and remote engine discovery. Used by gRPC/HTTP clients that talk to
// the Python docreader service.
type DocumentReader interface {
DocReader
Reconnect(addr string) error
IsConnected() bool
// ListEngines queries the remote docreader for its registered parser engines.
// Returns engines the remote service supports, allowing auto-discovery of
// newly added engines without Go code changes.
ListEngines(ctx context.Context, overrides map[string]string) ([]types.ParserEngineInfo, error)
}
@@ -0,0 +1,38 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// EmbedChannelRepository persists web embed channel rows.
type EmbedChannelRepository interface {
Create(ctx context.Context, ch *types.EmbedChannel) error
GetByID(ctx context.Context, id string) (*types.EmbedChannel, error)
GetByPublishToken(ctx context.Context, token string) (*types.EmbedChannel, error)
ListByAgent(ctx context.Context, tenantID uint64, agentID string) ([]*types.EmbedChannel, error)
ListByTenant(ctx context.Context, tenantID uint64) ([]*types.EmbedChannel, error)
Update(ctx context.Context, ch *types.EmbedChannel) error
Delete(ctx context.Context, tenantID uint64, id string) error
}
// EmbedChannelService manages web embed channel lifecycle.
type EmbedChannelService interface {
Create(ctx context.Context, tenantID uint64, agentID string, req *types.EmbedChannel) (*types.EmbedChannel, string, error)
ListByAgent(ctx context.Context, tenantID uint64, agentID string) ([]*types.EmbedChannel, error)
ListByTenant(ctx context.Context, tenantID uint64) ([]*types.EmbedChannel, error)
Update(ctx context.Context, tenantID uint64, id string, req *types.EmbedChannel, enabled *bool, showSuggested *bool, allowWebSearch *bool, allowFileUpload *bool, defaultLocale *string, webhookURL *string, webhookSecret *string) (*types.EmbedChannel, error)
Delete(ctx context.Context, tenantID uint64, id string) error
RotateToken(ctx context.Context, tenantID uint64, id string) (*types.EmbedChannel, string, error)
LookupForEmbed(ctx context.Context, channelID, token string) (*types.EmbedChannel, error)
LookupEnabledChannel(ctx context.Context, channelID string) (*types.EmbedChannel, error)
IssueSessionToken(ctx context.Context, channelID string) (sessionToken string, expiresIn int, err error)
IssuePreviewSession(ctx context.Context, tenantID uint64, channelID string) (sessionToken string, expiresIn int, err error)
ResolveSessionToken(ctx context.Context, token string) (channelID string, err error)
PublicConfig(ctx context.Context, ch *types.EmbedChannel) types.EmbedChannelPublicConfig
SuggestedQuestions(ctx context.Context, ch *types.EmbedChannel, limit int) ([]types.SuggestedQuestion, error)
EmbedChunk(ctx context.Context, ch *types.EmbedChannel, chunkID string) (*types.Chunk, error)
EmbedDisplayTitle(ctx context.Context, ch *types.EmbedChannel) string
GetOwnedChannel(ctx context.Context, tenantID uint64, id string) (*types.EmbedChannel, error)
}
+35
View File
@@ -0,0 +1,35 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// EvaluationService defines operations for evaluation tasks
type EvaluationService interface {
// Evaluation starts a new evaluation task
Evaluation(ctx context.Context, datasetID string, knowledgeBaseID string,
chatModelID string, rerankModelID string,
) (*types.EvaluationDetail, error)
// EvaluationResult retrieves evaluation result by task ID
EvaluationResult(ctx context.Context, taskID string) (*types.EvaluationDetail, error)
}
// Metrics defines interface for computing evaluation metrics
type Metrics interface {
// Compute calculates metric score based on input data
Compute(metricInput *types.MetricInput) float64
}
// EvalHook defines interface for evaluation process hooks
type EvalHook interface {
// Handle processes evaluation state change
Handle(ctx context.Context, state types.EvalState, index int, data interface{}) error
}
// DatasetService defines operations for dataset management
type DatasetService interface {
// GetDatasetByID retrieves QA pairs from dataset by ID
GetDatasetByID(ctx context.Context, datasetID string) ([]*types.QAPair, error)
}
+31
View File
@@ -0,0 +1,31 @@
package interfaces
import (
"context"
"io"
"mime/multipart"
)
// FileService is the interface for file services.
// FileService provides methods to save, retrieve, and delete files.
type FileService interface {
// CheckConnectivity verifies that the storage backend is reachable and
// properly configured (e.g. bucket exists, credentials valid).
CheckConnectivity(ctx context.Context) error
// SaveFile saves a file.
SaveFile(ctx context.Context, file *multipart.FileHeader, tenantID uint64, knowledgeID string) (string, error)
// SaveBytes saves bytes data to a file and returns the file path.
// If temp is true, the file will be saved to a temporary storage that may auto-expire.
SaveBytes(ctx context.Context, data []byte, tenantID uint64, fileName string, temp bool) (string, error)
// GetFile retrieves a file.
GetFile(ctx context.Context, filePath string) (io.ReadCloser, error)
// GetFileURL returns a download URL for the file (if supported by the storage backend).
GetFileURL(ctx context.Context, filePath string) (string, error)
// DeleteFile deletes a file.
DeleteFile(ctx context.Context, filePath string) error
// CopyFile copies an existing stored object to a NEW object owned by
// (tenantID, knowledgeID), returning the new provider:// path. The copy is
// independent: deleting the source never affects it. Returns ErrCrossBackendCopy
// when srcPath belongs to a different storage provider than this service.
CopyFile(ctx context.Context, srcPath string, tenantID uint64, knowledgeID string) (string, error)
}
+349
View File
@@ -0,0 +1,349 @@
package interfaces
import (
"context"
"io"
"mime/multipart"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
)
// KnowledgeService defines the interface for knowledge services.
type KnowledgeService interface {
// CreateKnowledgeFromFile creates knowledge from a file.
// channel identifies the ingestion channel (e.g. "web", "api", "wechat"); empty defaults to "web".
CreateKnowledgeFromFile(
ctx context.Context,
kbID string,
file *multipart.FileHeader,
metadata map[string]string,
enableMultimodel *bool,
customFileName string,
tagIDs []string,
channel string,
processOverrides *types.KnowledgeProcessOverrides,
) (*types.Knowledge, error)
// CreateKnowledgeFromURL creates knowledge from a URL.
// When fileName or fileType is provided (or the URL path has a known file extension),
// the URL is treated as a direct file download instead of a web page crawl.
// channel identifies the ingestion channel; empty defaults to "web".
CreateKnowledgeFromURL(
ctx context.Context,
kbID string,
url string,
fileName string,
fileType string,
enableMultimodel *bool,
title string,
tagIDs []string,
channel string,
processOverrides *types.KnowledgeProcessOverrides,
) (*types.Knowledge, error)
// CreateKnowledgeFromPassage creates knowledge from text passages.
// channel identifies the ingestion channel; empty defaults to "web".
CreateKnowledgeFromPassage(ctx context.Context, kbID string, passage []string, channel string) (*types.Knowledge, error)
// CreateKnowledgeFromPassageSync creates knowledge from text passages and waits until chunks are indexed.
CreateKnowledgeFromPassageSync(ctx context.Context, kbID string, passage []string, channel string) (*types.Knowledge, error)
// CreateKnowledgeFromManual creates or saves manual Markdown knowledge content.
// channel identifies the ingestion channel; empty defaults to "web".
CreateKnowledgeFromManual(
ctx context.Context,
kbID string,
payload *types.ManualKnowledgePayload,
channel string,
) (*types.Knowledge, error)
// GetKnowledgeByID retrieves knowledge by ID (uses tenant from context).
GetKnowledgeByID(ctx context.Context, id string) (*types.Knowledge, error)
// GetKnowledgeByIDOnly retrieves knowledge by ID without tenant filter (for permission resolution).
GetKnowledgeByIDOnly(ctx context.Context, id string) (*types.Knowledge, error)
// GetOwningKBCreatorID resolves a knowledge ID to the CreatorID of its
// owning KnowledgeBase, scoped to the caller's tenant. Used by the
// per-KB ownership lookups in handler/rbac_lookups.go (PR 5, #1303) so
// chunk and knowledge sub-resource routes can inherit the same
// "creator-of-the-KB OR Admin+" gate that KB-level routes already use.
// Returns the underlying repository sentinel errors unchanged so
// callers can map them to middleware.ErrResourceNotFound.
GetOwningKBCreatorID(ctx context.Context, knowledgeID string) (string, error)
// GetKnowledgeBatch retrieves a batch of knowledge by IDs.
GetKnowledgeBatch(ctx context.Context, tenantID uint64, ids []string) ([]*types.Knowledge, error)
// GetKnowledgeBatchWithSharedAccess retrieves knowledge by IDs including items from shared KBs the user has access to.
GetKnowledgeBatchWithSharedAccess(ctx context.Context, tenantID uint64, ids []string) ([]*types.Knowledge, error)
// ListKnowledgeIDsByTagIDs returns document knowledge IDs carrying any of
// the specified KB-local tags.
ListKnowledgeIDsByTagIDs(ctx context.Context, tenantID uint64, kbID string, tagIDs []string) ([]string, error)
// ListKnowledgeByKnowledgeBaseID lists all knowledge under a knowledge base.
ListKnowledgeByKnowledgeBaseID(ctx context.Context, kbID string) ([]*types.Knowledge, error)
// ListPagedKnowledgeByKnowledgeBaseID lists all knowledge under a knowledge base
// with pagination. The filter struct controls optional dimensions (tag, keyword,
// file type, parse status, source channel, updated time range); pass a zero
// struct to disable all filters.
ListPagedKnowledgeByKnowledgeBaseID(
ctx context.Context,
kbID string,
page *types.Pagination,
filter types.KnowledgeListFilter,
) (*types.PageResult, error)
// ListKnowledgeFolderTree returns the folder hierarchy derived from the
// folder_path of every knowledge entry in a knowledge base, with per-folder
// document counts. It powers the document sidebar tree.
ListKnowledgeFolderTree(ctx context.Context, kbID string) (*types.KnowledgeFolderTree, error)
// MoveKnowledgeToFolder re-files knowledge entries under the given folder
// path (empty means the knowledge base top level). Folders are derived from
// the stored paths, so a path that does not exist yet is created implicitly.
MoveKnowledgeToFolder(
ctx context.Context,
kbID string,
ids []string,
folderPath string,
) (int64, error)
// RenameKnowledgeFolder moves a folder and everything below it to a new path.
RenameKnowledgeFolder(ctx context.Context, kbID string, from string, to string) (int64, error)
// DeleteKnowledge deletes knowledge by ID.
DeleteKnowledge(ctx context.Context, id string) error
// DeleteKnowledgeList deletes multiple knowledge entries by IDs.
DeleteKnowledgeList(ctx context.Context, ids []string) error
// GetKnowledgeFile retrieves the file associated with the knowledge.
GetKnowledgeFile(ctx context.Context, id string) (io.ReadCloser, string, error)
// UpdateKnowledge updates knowledge information.
UpdateKnowledge(ctx context.Context, knowledge *types.Knowledge) error
// RegenerateKnowledgeSummary refreshes the document description and summary retrieval chunk.
RegenerateKnowledgeSummary(ctx context.Context, knowledgeID string) (*types.Knowledge, error)
// RequestKnowledgeSummaryRefresh enqueues an async summary refresh.
RequestKnowledgeSummaryRefresh(ctx context.Context, knowledgeID string) error
// RegenerateChunkQuestions rebuilds the auxiliary questions for one current chunk revision.
RegenerateChunkQuestions(ctx context.Context, chunkID string) ([]types.GeneratedQuestion, error)
// UpdateManualKnowledge updates manual Markdown knowledge content.
UpdateManualKnowledge(
ctx context.Context,
knowledgeID string,
payload *types.ManualKnowledgePayload,
) (*types.Knowledge, error)
// ReparseKnowledge deletes existing document content and re-parses the knowledge asynchronously.
// When processOverrides is non-nil, it is validated and persisted to the knowledge metadata
// before re-parsing, letting callers adjust parse config on reparse; nil keeps stored overrides.
ReparseKnowledge(
ctx context.Context,
knowledgeID string,
processOverrides *types.KnowledgeProcessOverrides,
) (*types.Knowledge, error)
// CancelKnowledgeParse marks an in-progress parse as cancelled by the
// user. The knowledge row and any partially written chunks/index are
// kept; downstream queued tasks for the same knowledge are best-effort
// dequeued and active workers are signalled to stop at their next
// checkpoint. Idempotent — returns the existing row when the knowledge
// is already cancelled. Returns an error when the knowledge is in a
// terminal state (completed / failed) or being deleted.
CancelKnowledgeParse(ctx context.Context, knowledgeID string) (*types.Knowledge, error)
// CloneKnowledgeBase clones knowledge to another knowledge base.
CloneKnowledgeBase(ctx context.Context, srcID, dstID string) error
// UpdateImageInfo updates image information for a knowledge chunk.
UpdateImageInfo(ctx context.Context, knowledgeID string, chunkID string, imageInfo string) error
// ListFAQEntries lists FAQ entries under a FAQ knowledge base.
// When tagUUIDs is non-empty, results are filtered by tag UUID on FAQ chunks (OR semantics).
// searchField: specifies which field to search in ("standard_question", "similar_questions", "answers", "" for all)
// sortOrder: "asc" for time ascending (updated_at ASC), default is time descending (updated_at DESC)
ListFAQEntries(
ctx context.Context,
kbID string,
page *types.Pagination,
tagUUIDs []string,
legacyTagSeqID int64,
keyword string,
searchField string,
sortOrder string,
) (*types.PageResult, error)
// UpsertFAQEntries imports or appends FAQ entries asynchronously.
// When DryRun is true, only validates entries without actually importing.
// Returns task ID (Knowledge ID) for tracking import progress.
UpsertFAQEntries(ctx context.Context, kbID string, payload *types.FAQBatchUpsertPayload) (string, error)
// CreateFAQEntry creates a single FAQ entry synchronously.
CreateFAQEntry(ctx context.Context, kbID string, payload *types.FAQEntryPayload) (*types.FAQEntry, error)
// GetFAQEntry retrieves a single FAQ entry by seq_id.
GetFAQEntry(ctx context.Context, kbID string, entrySeqID int64) (*types.FAQEntry, error)
// UpdateFAQEntry updates a single FAQ entry.
UpdateFAQEntry(ctx context.Context, kbID string, entrySeqID int64, payload *types.FAQEntryPayload) (*types.FAQEntry, error)
// AddSimilarQuestions adds similar questions to a FAQ entry.
AddSimilarQuestions(ctx context.Context, kbID string, entrySeqID int64, questions []string) (*types.FAQEntry, error)
// UpdateFAQEntryFieldsBatch updates multiple fields for FAQ entries in batch.
// Supports updating is_enabled, is_recommended, tag_id, and other fields in a single call.
UpdateFAQEntryFieldsBatch(ctx context.Context, kbID string, req *types.FAQEntryFieldsBatchUpdate) error
// DeleteFAQEntries deletes FAQ entries in batch by seq_id.
DeleteFAQEntries(ctx context.Context, kbID string, entrySeqIDs []int64) error
// SearchFAQEntries searches FAQ entries using hybrid search.
SearchFAQEntries(ctx context.Context, kbID string, req *types.FAQSearchRequest) ([]*types.FAQEntry, error)
// ExportFAQEntries exports all FAQ entries for a knowledge base as CSV data.
ExportFAQEntries(ctx context.Context, kbID string) ([]byte, error)
// ExportFAQEntriesJSON exports all FAQ entries as a JSON array compatible with FAQEntryPayload.
ExportFAQEntriesJSON(ctx context.Context, kbID string) ([]byte, error)
// UpdateKnowledgeTagBatch updates tag for document knowledge items in batch.
// authorizedKBID restricts all updates to knowledge items belonging to this KB;
// pass empty string to skip (caller must ensure authorization by other means).
UpdateKnowledgeTagBatch(ctx context.Context, authorizedKBID string, updates map[string][]string) error
// SetKnowledgeTags replaces all tags for a single knowledge entry.
SetKnowledgeTags(ctx context.Context, knowledgeID string, tagIDs []string) error
// GetKnowledgeTags returns tags for multiple knowledge IDs.
GetKnowledgeTags(ctx context.Context, knowledgeIDs []string) (map[string][]*types.KnowledgeTag, error)
// UpdateFAQEntryTagBatch updates tag for FAQ entries in batch.
// Key: entry seq_id, Value: tag seq_id (nil to remove tag)
UpdateFAQEntryTagBatch(ctx context.Context, kbID string, updates map[int64]*int64) error
// GetRepository gets the knowledge repository
GetRepository() KnowledgeRepository
// ProcessManualUpdate handles Asynq manual knowledge update tasks (cleanup + re-indexing)
ProcessManualUpdate(ctx context.Context, t *asynq.Task) error
// ProcessDocument handles Asynq document processing tasks
ProcessDocument(ctx context.Context, t *asynq.Task) error
// ProcessFAQImport handles Asynq FAQ import tasks
ProcessFAQImport(ctx context.Context, t *asynq.Task) error
// ProcessQuestionGeneration handles Asynq question generation tasks
ProcessQuestionGeneration(ctx context.Context, t *asynq.Task) error
// ProcessSummaryGeneration handles Asynq summary generation tasks
ProcessSummaryGeneration(ctx context.Context, t *asynq.Task) error
// ProcessKBClone handles Asynq knowledge base clone tasks
ProcessKBClone(ctx context.Context, t *asynq.Task) error
// ProcessKnowledgeMove handles Asynq knowledge move tasks
ProcessKnowledgeMove(ctx context.Context, t *asynq.Task) error
// ProcessKnowledgeListDelete handles Asynq knowledge list delete tasks
ProcessKnowledgeListDelete(ctx context.Context, t *asynq.Task) error
// ProcessKnowledgeListReparse handles Asynq knowledge list reparse tasks
ProcessKnowledgeListReparse(ctx context.Context, t *asynq.Task) error
// GetKBCloneProgress retrieves the progress of a knowledge base clone task
GetKBCloneProgress(ctx context.Context, taskID string) (*types.KBCloneProgress, error)
// SaveKBCloneProgress saves the progress of a knowledge base clone task
SaveKBCloneProgress(ctx context.Context, progress *types.KBCloneProgress) error
// GetKnowledgeMoveProgress retrieves the progress of a knowledge move task
GetKnowledgeMoveProgress(ctx context.Context, taskID string) (*types.KnowledgeMoveProgress, error)
// SaveKnowledgeMoveProgress saves the progress of a knowledge move task
SaveKnowledgeMoveProgress(ctx context.Context, progress *types.KnowledgeMoveProgress) error
// GetFAQImportProgress retrieves the progress of an FAQ import task
GetFAQImportProgress(ctx context.Context, taskID string) (*types.FAQImportProgress, error)
// UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result
UpdateLastFAQImportResultDisplayStatus(ctx context.Context, kbID string, displayStatus string) error
// SearchKnowledge searches knowledge items by keyword across the tenant.
// fileTypes: optional list of file extensions to filter by (e.g., ["csv", "xlsx"])
SearchKnowledge(ctx context.Context, keyword string, offset, limit int, fileTypes []string) ([]*types.Knowledge, bool, int64, error)
// SearchKnowledgeForScopes searches knowledge within the given (tenant_id, kb_id) scopes (e.g. for shared agent context).
SearchKnowledgeForScopes(ctx context.Context, scopes []types.KnowledgeSearchScope, keyword string, offset, limit int, fileTypes []string) ([]*types.Knowledge, bool, int64, error)
}
// KnowledgeRepository defines the interface for knowledge repositories.
type KnowledgeRepository interface {
CreateKnowledge(ctx context.Context, knowledge *types.Knowledge) error
GetKnowledgeByID(ctx context.Context, tenantID uint64, id string) (*types.Knowledge, error)
// GetKnowledgeByIDOnly returns knowledge by ID without tenant filter (for permission resolution).
GetKnowledgeByIDOnly(ctx context.Context, id string) (*types.Knowledge, error)
ListKnowledgeByKnowledgeBaseID(ctx context.Context, tenantID uint64, kbID string) ([]*types.Knowledge, error)
// ListPagedKnowledgeByKnowledgeBaseID lists all knowledge in a knowledge base
// with pagination. The filter struct controls optional dimensions (tag, keyword,
// file type, parse status, source channel, updated time range); pass a zero
// struct to disable all filters.
ListPagedKnowledgeByKnowledgeBaseID(ctx context.Context,
tenantID uint64, kbID string, page *types.Pagination, filter types.KnowledgeListFilter,
) ([]*types.Knowledge, int64, error)
UpdateKnowledge(ctx context.Context, knowledge *types.Knowledge) error
// UpdateKnowledgeBatch updates knowledge items in batch
UpdateKnowledgeBatch(ctx context.Context, knowledgeList []*types.Knowledge) error
DeleteKnowledge(ctx context.Context, tenantID uint64, id string) error
DeleteKnowledgeList(ctx context.Context, tenantID uint64, ids []string) error
GetKnowledgeBatch(ctx context.Context, tenantID uint64, ids []string) ([]*types.Knowledge, error)
// CheckKnowledgeExists checks if knowledge already exists.
// For file types, check by fileHash or (fileName+fileSize).
// For URL types, check by URL.
// Returns whether it exists, the existing knowledge object (if any), and possible error.
CheckKnowledgeExists(
ctx context.Context,
tenantID uint64,
kbID string,
params *types.KnowledgeCheckParams,
) (bool, *types.Knowledge, error)
// ListKnowledgeFolderCounts aggregates the number of knowledge entries
// stored directly in each folder_path of a knowledge base.
ListKnowledgeFolderCounts(
ctx context.Context,
tenantID uint64,
kbID string,
) ([]*types.KnowledgeFolderCount, error)
// UpdateKnowledgeFolderPath files the given entries under folderPath.
UpdateKnowledgeFolderPath(
ctx context.Context,
tenantID uint64,
kbID string,
ids []string,
folderPath string,
) (int64, error)
// RenameKnowledgeFolderPath rewrites folder_path for a folder and all of its
// descendants. Renaming onto an existing path merges the folders.
RenameKnowledgeFolderPath(
ctx context.Context,
tenantID uint64,
kbID string,
from string,
to string,
) (int64, error)
// AminusB returns the IDs of knowledge in A that have no counterpart in B,
// comparing file_hash as a multiset (so duplicate-count differences and
// NULL/empty hashes are handled correctly, letting a clone converge).
AminusB(ctx context.Context, Atenant uint64, A string, Btenant uint64, B string) ([]string, error)
UpdateKnowledgeColumn(ctx context.Context, id string, column string, value interface{}) error
// UpdateKnowledgeColumns updates multiple columns of a knowledge row in a single
// statement so callers that flip several related fields (e.g. parse_status +
// error_message) cannot leave the row in a half-updated state.
UpdateKnowledgeColumns(ctx context.Context, id string, values map[string]interface{}) error
// UpdateActiveDeletingKnowledgeColumns updates an active, non-deleted knowledge row
// only when it is still in the transient deleting state.
UpdateActiveDeletingKnowledgeColumns(ctx context.Context, id string, values map[string]interface{}) (bool, error)
// FinalizeSubtask atomically decrements pending_subtasks_count for the
// given knowledge and promotes parse_status from "finalizing" to
// "completed" when the count reaches zero. Returns the post-decrement
// count, whether this caller's UPDATE was the one that promoted the
// row, and any error.
FinalizeSubtask(ctx context.Context, id string) (int, bool, error)
// SetFinalizing atomically transitions a row from "processing" to
// "finalizing" and writes the initial pending_subtasks_count. Returns
// whether the transition took place (false when the row's parse_status
// was no longer "processing", e.g. user cancelled / deleted in flight).
SetFinalizing(ctx context.Context, id string, expectedSubtasks int) (bool, error)
// CountKnowledgeByKnowledgeBaseID counts the number of knowledge items in a knowledge base.
CountKnowledgeByKnowledgeBaseID(ctx context.Context, tenantID uint64, kbID string) (int64, error)
// CountKnowledgeByStatus counts the number of knowledge items with the specified parse status.
CountKnowledgeByStatus(ctx context.Context, tenantID uint64, kbID string, parseStatuses []string) (int64, error)
// SearchKnowledge searches knowledge items by keyword across the tenant.
// fileTypes: optional list of file extensions to filter by (e.g., ["csv", "xlsx"])
SearchKnowledge(ctx context.Context, tenantID uint64, keyword string, offset, limit int, fileTypes []string) ([]*types.Knowledge, bool, error)
// FindByMetadataKey finds a knowledge item by a key-value pair in the metadata JSON column.
// Used by data source sync to locate existing items by external_id.
FindByMetadataKey(ctx context.Context, tenantID uint64, kbID string, key string, value string) (*types.Knowledge, error)
// FindByMetadataKeyPrefix finds knowledge items whose metadata[key] starts
// with the given prefix. Used to sweep an external node's attachment sub-items.
FindByMetadataKeyPrefix(ctx context.Context, tenantID uint64, kbID string, key string, prefix string) ([]*types.Knowledge, error)
// FindByDataSourceExternalID finds a knowledge item owned by one data source
// and identified by the source's external item ID.
FindByDataSourceExternalID(
ctx context.Context, tenantID uint64, kbID, dataSourceID, externalID string,
) (*types.Knowledge, error)
// HardDeleteKnowledge physically removes a row after DeleteKnowledge's soft-delete
// cascade. Sync-internal deletions use this so rows never become tombstones.
HardDeleteKnowledge(ctx context.Context, tenantID uint64, id string) error
// HardDeleteKnowledgeList is the batch counterpart of HardDeleteKnowledge.
HardDeleteKnowledgeList(ctx context.Context, tenantID uint64, ids []string) error
// SearchKnowledgeInScopes searches knowledge items by keyword within the given (tenant_id, kb_id) scopes (own + shared).
SearchKnowledgeInScopes(ctx context.Context, scopes []types.KnowledgeSearchScope, keyword string, offset, limit int, fileTypes []string) ([]*types.Knowledge, bool, int64, error)
// ListIDsByTagIDs returns all knowledge IDs that have any of the specified tag IDs (OR semantics).
ListIDsByTagIDs(ctx context.Context, tenantID uint64, kbID string, tagIDs []string) ([]string, error)
// SetKnowledgeTags replaces all tags for a single knowledge entry (deletes old, inserts new).
SetKnowledgeTags(ctx context.Context, knowledgeID string, tagIDs []string) error
// AddKnowledgeTagRelations attaches tags without removing existing ones,
// after validating that the knowledge and every tag belong to the given
// tenant and knowledge base. Duplicate deliveries are idempotent.
AddKnowledgeTagRelations(
ctx context.Context,
tenantID uint64,
kbID, knowledgeID string,
tagIDs []string,
) error
// GetKnowledgeTags returns tags for multiple knowledge IDs.
GetKnowledgeTags(ctx context.Context, knowledgeIDs []string) (map[string][]*types.KnowledgeTag, error)
// DeleteKnowledgeTagRelations deletes all tag relations for a knowledge entry.
DeleteKnowledgeTagRelations(ctx context.Context, knowledgeID string) error
}
+252
View File
@@ -0,0 +1,252 @@
// Package interfaces defines the interface contracts between different system components
// Through interface definitions, business logic can be decoupled from specific implementations,
// improving code testability and maintainability
// Knowledge base related interfaces are used to manage knowledge base resources and their contents
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
"gorm.io/gorm"
)
// KnowledgeBaseService defines the knowledge base service interface
// Provides high-level operations for knowledge base creation, querying, updating, deletion, and content searching
type KnowledgeBaseService interface {
// CreateKnowledgeBase creates a new knowledge base
// Parameters:
// - ctx: Context information, carrying request tracking, user identity, etc.
// - kb: Knowledge base object containing basic information
// Returns:
// - Created knowledge base object (including automatically generated ID)
// - Possible errors such as insufficient permissions, duplicate names, etc.
CreateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) (*types.KnowledgeBase, error)
// GetKnowledgeBaseByID retrieves knowledge base information by ID
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the knowledge base
// Returns:
// - Knowledge base object, if found
// - Possible errors such as not existing, insufficient permissions, etc.
GetKnowledgeBaseByID(ctx context.Context, id string) (*types.KnowledgeBase, error)
// GetKnowledgeBaseByIDOnly retrieves knowledge base by ID without tenant filter
// Used for cross-tenant shared KB access where permission is checked elsewhere
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the knowledge base
// Returns:
// - Knowledge base object, if found
// - Possible errors such as not existing, etc.
GetKnowledgeBaseByIDOnly(ctx context.Context, id string) (*types.KnowledgeBase, error)
// GetKnowledgeBasesByIDsOnly retrieves knowledge bases by IDs without tenant filter (batch).
GetKnowledgeBasesByIDsOnly(ctx context.Context, ids []string) ([]*types.KnowledgeBase, error)
// FillKnowledgeBaseCounts fills KnowledgeCount, ChunkCount, IsProcessing, ProcessingCount for the given KB (uses kb.TenantID).
FillKnowledgeBaseCounts(ctx context.Context, kb *types.KnowledgeBase) error
// ListKnowledgeBases lists all knowledge bases under the current tenant
// Parameters:
// - ctx: Context information, containing tenant information
// Returns:
// - List of knowledge base objects
// - Possible errors such as insufficient permissions, etc.
ListKnowledgeBases(ctx context.Context) ([]*types.KnowledgeBase, error)
// ListKnowledgeBasesByTenantID lists all knowledge bases for a specific tenant (e.g. for shared agent context).
ListKnowledgeBasesByTenantID(ctx context.Context, tenantID uint64) ([]*types.KnowledgeBase, error)
// UpdateKnowledgeBase updates knowledge base information
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the knowledge base
// - name: New knowledge base name
// - description: New knowledge base description
// - config: Knowledge base configuration, including chunking strategy, vectorization settings, etc.
// Returns:
// - Updated knowledge base object
// - Possible errors such as not existing, insufficient permissions, etc.
UpdateKnowledgeBase(ctx context.Context,
id string, name string, description string, config *types.KnowledgeBaseConfig,
) (*types.KnowledgeBase, error)
// DeleteKnowledgeBase deletes a knowledge base
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the knowledge base
// Returns:
// - Possible errors such as not existing, insufficient permissions, etc.
DeleteKnowledgeBase(ctx context.Context, id string) error
// TogglePinKnowledgeBase toggles the pin status of a knowledge base
TogglePinKnowledgeBase(ctx context.Context, id string) (*types.KnowledgeBase, error)
// HybridSearch performs hybrid search (vector + keywords) in the knowledge base
// Parameters:
// - ctx: Context information
// - id: Unique identifier of the knowledge base
// - params: Search parameters, including query text, thresholds, etc.
// Returns:
// - List of search results, sorted by relevance
// - Possible errors such as not existing, insufficient permissions, search engine errors, etc.
HybridSearch(ctx context.Context, id string, params types.SearchParams) ([]*types.SearchResult, error)
// GetQueryEmbedding computes the query embedding using the embedding model
// associated with the given knowledge base. This allows callers to pre-compute
// and reuse embeddings across multiple KBs that share the same model.
GetQueryEmbedding(ctx context.Context, kbID string, queryText string) ([]float32, error)
// ResolveEmbeddingModelKeys resolves embedding model IDs to their actual
// model identity key (name + endpoint). KBs using the same underlying model
// across different tenants will share the same key, enabling optimal grouping.
// Returns a map from KB ID to model identity key string.
ResolveEmbeddingModelKeys(ctx context.Context, kbs []*types.KnowledgeBase) map[string]string
// CopyKnowledgeBase copies a knowledge base
// Parameters:
// - ctx: Context information
// - sourceID: Source knowledge base ID
// - targetID: Target knowledge base ID
// Returns:
// - Copied knowledge base object
// - Possible errors such as not existing, insufficient permissions, etc.
CopyKnowledgeBase(ctx context.Context, src string, dst string) (*types.KnowledgeBase, *types.KnowledgeBase, error)
// DuplicateKnowledgeBase creates a new settings-only knowledge base duplicate.
// It does not copy knowledge entries, chunks, FAQ rows, wiki pages, indexes,
// data sources, shares, pins, or task state. A new UUID is always generated.
DuplicateKnowledgeBase(ctx context.Context, src string) (*types.KnowledgeBase, error)
// GetRepository gets the knowledge base repository
// Parameters:
// - ctx: Context with authentication and request information
//
// Returns:
// - interfaces.KnowledgeBaseRepository: Knowledge base repository
GetRepository() KnowledgeBaseRepository
// ProcessKBDelete handles async knowledge base deletion task
// Parameters:
// - ctx: Context information
// - t: Asynq task containing KBDeletePayload
// Returns:
// - Possible errors during deletion
ProcessKBDelete(ctx context.Context, t *asynq.Task) error
}
// KnowledgeBaseRepository defines the knowledge base repository interface
// Responsible for knowledge base data persistence and retrieval,
// serving as a bridge between the service layer and data storage
type KnowledgeBaseRepository interface {
// CreateKnowledgeBase creates a knowledge base record
// Parameters:
// - ctx: Context information
// - kb: Knowledge base object
// Returns:
// - Possible errors such as database connection failure, unique constraint conflicts, etc.
CreateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) error
// GetKnowledgeBaseByID queries a knowledge base by ID
// Parameters:
// - ctx: Context information
// - id: Knowledge base ID
// Returns:
// - Knowledge base object, if found
// - Possible errors such as record not existing, database errors, etc.
GetKnowledgeBaseByID(ctx context.Context, id string) (*types.KnowledgeBase, error)
// GetKnowledgeBaseByIDAndTenant queries a knowledge base by ID scoped to a tenant.
// Returns ErrKnowledgeBaseNotFound if the KB does not exist or does not belong to the tenant.
// Parameters:
// - ctx: Context information
// - id: Knowledge base ID
// - tenantID: Tenant ID (enforces tenant isolation)
// Returns:
// - Knowledge base object, if found and owned by tenant
// - Possible errors such as record not existing or wrong tenant, database errors, etc.
GetKnowledgeBaseByIDAndTenant(ctx context.Context, id string, tenantID uint64) (*types.KnowledgeBase, error)
// GetKnowledgeBaseByIDs queries knowledge bases by multiple IDs
// Parameters:
// - ctx: Context information
// - ids: List of knowledge base IDs
// Returns:
// - List of knowledge base objects
// - Possible errors such as database errors, etc.
GetKnowledgeBaseByIDs(ctx context.Context, ids []string) ([]*types.KnowledgeBase, error)
// ListKnowledgeBases lists all knowledge bases in the system
// Parameters:
// - ctx: Context information
// Returns:
// - List of knowledge base objects
// - Possible errors such as database errors, etc.
ListKnowledgeBases(ctx context.Context) ([]*types.KnowledgeBase, error)
// ListKnowledgeBasesByTenantID lists all knowledge bases for a specific tenant
// Parameters:
// - ctx: Context information
// - tenantID: Tenant ID
// Returns:
// - List of knowledge base objects
// - Possible errors such as database errors, etc.
ListKnowledgeBasesByTenantID(ctx context.Context, tenantID uint64) ([]*types.KnowledgeBase, error)
// UpdateKnowledgeBase updates a knowledge base record
// Parameters:
// - ctx: Context information
// - kb: Knowledge base object containing update information
// Returns:
// - Possible errors such as record not existing, database errors, etc.
UpdateKnowledgeBase(ctx context.Context, kb *types.KnowledgeBase) error
// DeleteKnowledgeBase deletes a knowledge base record
// Parameters:
// - ctx: Context information
// - id: Knowledge base ID
// Returns:
// - Possible errors such as record not existing, database errors, etc.
DeleteKnowledgeBase(ctx context.Context, id string) error
// CleanupDeletedKnowledgeBaseCategories retries category cleanup after soft deletion.
CleanupDeletedKnowledgeBaseCategories(ctx context.Context, id string) error
// CountByVectorStoreID counts active KBs bound to the given vector store
// within a tenant scope. Accepts a *gorm.DB handle so callers can share a
// transaction (e.g., the VectorStore delete guard's row-lock context) or
// run standalone (pass nil → uses the repository's default db).
//
// The soft-delete filter is applied automatically by the gorm.DeletedAt
// scope on KnowledgeBase; implementations MUST NOT add an explicit
// `deleted_at IS NULL` predicate (avoids divergence with the auto-scope).
CountByVectorStoreID(ctx context.Context, db *gorm.DB, tenantID uint64, storeID string) (int64, error)
// CountByModelID counts active KBs in the tenant that reference the given
// model ID in any model-binding field (embedding, summary, VLM, ASR, etc.).
CountByModelID(ctx context.Context, tenantID uint64, modelID string) (int64, error)
// ListModelUsages returns the minimal active KB projections that reference
// the model, with every matching binding merged per object. Implementations
// must cap the result at types.ModelUsageListLimit; callers that need the
// untruncated size should use CountByModelID.
ListModelUsages(ctx context.Context, tenantID uint64, modelID string) ([]types.ModelUsageResource, error)
// SetUserKBPin inserts or removes a row in user_kb_pins for the given
// (tenant, user, kb) triple. Returns the resulting pinned_at (nil when
// pinned=false) and an error. The tenant_id is captured to support
// efficient "wipe a tenant" cleanups even though (user_id, kb_id)
// alone would be unique in practice.
SetUserKBPin(
ctx context.Context, tenantID uint64, userID string, kbID string, pinned bool,
) (pinnedAt *time.Time, err error)
// ListUserKBPinIDs returns the kb_id → pinned_at map of every KB the
// given user has personally pinned in this tenant. Used by the list
// path to stamp KnowledgeBase.IsPinned / PinnedAt without a per-row
// roundtrip.
ListUserKBPinIDs(
ctx context.Context, tenantID uint64, userID string,
) (map[string]time.Time, error)
}
+64
View File
@@ -0,0 +1,64 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// MCPOAuthRepository persists OAuth clients (per service) and tokens
// (per principal + service) for the MCP OAuth2 authorization-code flow.
type MCPOAuthRepository interface {
// GetClient returns the registered OAuth client for a service, or
// (nil, nil) when none has been registered yet.
GetClient(ctx context.Context, tenantID uint64, serviceID string) (*types.MCPOAuthClient, error)
// SaveClient creates or updates the registered OAuth client for a service.
SaveClient(ctx context.Context, client *types.MCPOAuthClient) error
// DeleteClient removes the registered OAuth client for a service.
DeleteClient(ctx context.Context, tenantID uint64, serviceID string) error
// GetToken returns the stored token for (tenant, user, service), or
// (nil, nil) when the user has not authorized yet.
GetToken(ctx context.Context, tenantID uint64, userID, serviceID string) (*types.MCPOAuthToken, error)
// GetTokenForPrincipal returns the stored token for (tenant, principal, service),
// or (nil, nil) when the principal has not authorized yet.
GetTokenForPrincipal(
ctx context.Context, tenantID uint64, principal types.Principal, serviceID string,
) (*types.MCPOAuthToken, error)
// SaveToken creates or updates the per-user token for a service.
SaveToken(ctx context.Context, token *types.MCPOAuthToken) error
// SaveTokenForPrincipal creates or updates the per-principal token for a service.
SaveTokenForPrincipal(ctx context.Context, token *types.MCPOAuthToken) error
// DeleteToken removes the per-user token for a service (revoke /
// re-authorize).
DeleteToken(ctx context.Context, tenantID uint64, userID, serviceID string) error
// DeleteTokenForPrincipal removes the per-principal token for a service.
DeleteTokenForPrincipal(ctx context.Context, tenantID uint64, principal types.Principal, serviceID string) error
// TryAcquireTokenRefreshLease atomically claims the right to use a refresh
// token. This prevents concurrent instances from presenting the same
// rotating refresh token to the authorization server.
TryAcquireTokenRefreshLease(
ctx context.Context,
tenantID uint64,
principal types.Principal,
serviceID, leaseID string,
leaseUntil time.Time,
) (bool, error)
// ReleaseTokenRefreshLease releases a lease only when leaseID still owns it.
ReleaseTokenRefreshLease(
ctx context.Context,
tenantID uint64,
principal types.Principal,
serviceID, leaseID string,
) error
}
+85
View File
@@ -0,0 +1,85 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// MCPServiceRepository defines the interface for MCP service data access
type MCPServiceRepository interface {
// Create creates a new MCP service
Create(ctx context.Context, service *types.MCPService) error
// GetByID retrieves an MCP service by ID and tenant ID
GetByID(ctx context.Context, tenantID uint64, id string) (*types.MCPService, error)
// List retrieves all MCP services for a tenant
List(ctx context.Context, tenantID uint64) ([]*types.MCPService, error)
// ListEnabled retrieves all enabled MCP services for a tenant
ListEnabled(ctx context.Context, tenantID uint64) ([]*types.MCPService, error)
// ListByIDs retrieves MCP services by multiple IDs for a tenant
ListByIDs(ctx context.Context, tenantID uint64, ids []string) ([]*types.MCPService, error)
// Update updates an MCP service
Update(ctx context.Context, service *types.MCPService) error
// Delete deletes an MCP service (soft delete)
Delete(ctx context.Context, tenantID uint64, id string) error
}
// MCPServiceService defines the interface for MCP service business logic
type MCPServiceService interface {
// CreateMCPService creates a new MCP service
CreateMCPService(ctx context.Context, service *types.MCPService) error
// GetMCPServiceByID retrieves an MCP service by ID
GetMCPServiceByID(ctx context.Context, tenantID uint64, id string) (*types.MCPService, error)
// ListMCPServices lists all MCP services for a tenant
ListMCPServices(ctx context.Context, tenantID uint64) ([]*types.MCPService, error)
// ListMCPServicesByIDs retrieves multiple MCP services by IDs
ListMCPServicesByIDs(ctx context.Context, tenantID uint64, ids []string) ([]*types.MCPService, error)
// UpdateMCPService updates an MCP service. updateFields records presence for
// scalar fields whose zero values cannot represent omission. Supported keys
// are "name", "description", and "enabled"; a nil map means none of those
// scalar fields were provided.
UpdateMCPService(
ctx context.Context,
service *types.MCPService,
updateFields map[string]bool,
) error
// DeleteMCPService deletes an MCP service
DeleteMCPService(ctx context.Context, tenantID uint64, id string) error
// TestMCPService tests the connection to an MCP service and returns available tools/resources
TestMCPService(ctx context.Context, tenantID uint64, id string) (*types.MCPTestResult, error)
// GetMCPServiceTools retrieves the list of tools from an MCP service
GetMCPServiceTools(ctx context.Context, tenantID uint64, id string) ([]*types.MCPTool, error)
// GetMCPServiceResources retrieves the list of resources from an MCP service
GetMCPServiceResources(ctx context.Context, tenantID uint64, id string) ([]*types.MCPResource, error)
// UpdateMCPCredentials writes one or more credential fields on the auth
// config. Nil pointer means "do not touch this field". Returns the updated
// service (with current AuthConfig) so the handler can derive the
// configured/not-configured metadata for the response.
//
// Implementations MUST close any active MCP client connection for this
// service so the next upstream call reconnects with the new credential.
UpdateMCPCredentials(
ctx context.Context, tenantID uint64, id string, apiKey *string, token *string,
) (*types.MCPService, error)
// ClearMCPCredential removes a single credential field. field must be
// "api_key" or "token"; other values must be rejected by the caller.
// Implementations MUST close any active MCP client connection for this
// service. Clearing a field that is already empty is a no-op (no error).
ClearMCPCredential(ctx context.Context, tenantID uint64, id, field string) error
}
@@ -0,0 +1,21 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// MCPToolApprovalRepository persists per-tool approval requirements.
type MCPToolApprovalRepository interface {
ListByService(ctx context.Context, tenantID uint64, serviceID string) ([]*types.MCPToolApproval, error)
IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
Upsert(ctx context.Context, row *types.MCPToolApproval) error
}
// MCPToolApprovalService is the business layer for MCP tool approval flags.
type MCPToolApprovalService interface {
ListByService(ctx context.Context, tenantID uint64, serviceID string) ([]*types.MCPToolApproval, error)
SetRequireApproval(ctx context.Context, tenantID uint64, serviceID, toolName string, require bool) error
IsRequired(ctx context.Context, tenantID uint64, serviceID, toolName string) (bool, error)
}
+325
View File
@@ -0,0 +1,325 @@
package interfaces
import (
"context"
"errors"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
)
// MemoryScope is the resolved (workspace, principal) pair a memory operation
// runs under. It is always derived from the request context so no caller can
// address another user's memory by passing an id.
type MemoryScope struct {
TenantID uint64
SubjectID string
}
func (s MemoryScope) Valid() bool {
return s.TenantID > 0 && s.SubjectID != ""
}
// ErrMemoryItemNotPending rejects stale review actions without reviving history.
var ErrMemoryItemNotPending = errors.New("memory: item is not awaiting confirmation")
// MemoryRepository is the storage contract for long-term memory. Every method
// takes an explicit scope rather than reading it from ctx, so a background
// worker cannot accidentally operate on whatever the ambient context happens
// to hold.
type MemoryRepository interface {
// GetSubject returns the memory space, or (nil, nil) when it does not exist.
GetSubject(ctx context.Context, scope MemoryScope) (*types.MemorySubject, error)
// EnsureSubject returns the memory space, creating it on first use.
EnsureSubject(ctx context.Context, scope MemoryScope) (*types.MemorySubject, error)
// UpdateSubjectEnabled flips the per-user opt out.
UpdateSubjectEnabled(ctx context.Context, scope MemoryScope, enabled bool) error
// UpdateSubjectBlock stores the rendered resident block and item count.
UpdateSubjectBlock(ctx context.Context, scope MemoryScope, block string, itemCount int) error
// EnqueuePendingSession records that a session has turns past the cursor
// and claims the in-flight slot when no run is already scheduled. It
// returns the subject as it was before the update plus whether this call
// is the one responsible for enqueuing the task, so a burst of turns
// produces exactly one task and never a dropped turn.
EnqueuePendingSession(
ctx context.Context, scope MemoryScope, sessionID string, inFlightTimeout time.Duration, turnStart ...time.Time,
) (*types.MemorySubject, bool, error)
// ClaimPendingSessions snapshots the durable queue and current cursor.
// Entries remain pending until successful completion acknowledges them.
ClaimPendingSessions(ctx context.Context, scope MemoryScope) ([]string, types.MemorySessionCursors, error)
// FinishExtraction advances the watermark and releases the in-flight slot.
// A zero cursor leaves the watermark untouched.
FinishExtraction(ctx context.Context, scope MemoryScope, cursors types.MemorySessionCursors, completed ...string) error
// ReleaseExtractionSlot clears the in-flight marker without advancing the
// watermark, used when a run ends early.
ReleaseExtractionSlot(ctx context.Context, scope MemoryScope) error
ReserveExtraction(ctx context.Context, scope MemoryScope, payload types.MemoryExtractPayload, delay, lease time.Duration) (*types.MemoryExtractPayload, error)
StartExtraction(ctx context.Context, scope MemoryScope, attemptID string, lease time.Duration) (bool, error)
FailExtraction(ctx context.Context, scope MemoryScope, attemptID string) error
ListDueExtractions(ctx context.Context, limit int) ([]*types.MemorySubject, error)
DeferExtraction(ctx context.Context, scope MemoryScope, delay time.Duration) error
PauseExtraction(ctx context.Context, scope MemoryScope, attemptID string, delay time.Duration) error
ResetExhaustedExtraction(ctx context.Context, scope MemoryScope) (bool, error)
// CreateItem inserts one memory item.
CreateItem(ctx context.Context, item *types.MemoryItem) error
// GetItem returns one item inside the scope, or (nil, nil) when absent.
GetItem(ctx context.Context, scope MemoryScope, id string) (*types.MemoryItem, error)
// ListActiveByKinds returns active items of the given kinds, newest first.
ListActiveByKinds(ctx context.Context, scope MemoryScope, kinds []string, limit int) ([]*types.MemoryItem, error)
// ListActiveResident returns the items that belong in the always-injected
// block: stable traits, plus anything the user explicitly asked to keep.
ListActiveResident(ctx context.Context, scope MemoryScope, limit int) ([]*types.MemoryItem, error)
// ListItems returns items for the memory manager, filtered by status.
ListItems(ctx context.Context, scope MemoryScope, status string, limit, offset int) ([]*types.MemoryItem, int64, error)
// FindActiveByKey returns the active item occupying a topic key, if any.
FindActiveByKey(ctx context.Context, scope MemoryScope, normalizedKey string) (*types.MemoryItem, error)
// UpdateItemContent rewrites an item edited in the memory manager.
UpdateItemContent(ctx context.Context, scope MemoryScope, id, content, normalizedKey string, importance int) error
// SupersedeItem marks an outdated item as replaced by another one.
SupersedeItem(ctx context.Context, scope MemoryScope, id, supersededBy string) error
// DeleteItem physically removes one item. Forgetting means forgetting.
DeleteItem(ctx context.Context, scope MemoryScope, id string) error
// DeleteAll removes every item in the scope and returns how many were removed.
DeleteAll(ctx context.Context, scope MemoryScope) (int64, error)
// TouchUsed records that items were injected into a turn.
TouchUsed(ctx context.Context, scope MemoryScope, ids []string) error
// AddTombstone records that a statement was deliberately forgotten. Only
// the topic, a fingerprint and the originating message are stored, never
// the statement itself.
AddTombstone(ctx context.Context, scope MemoryScope, topic, fingerprint, sourceMessageID string) error
// HasTombstoneForMessage reports whether a memory derived from this message
// was rejected within the given window. The window matters: this rule
// exists to stop the re-derivation that happens one debounce later, not to
// ban a message for good.
HasTombstoneForMessage(
ctx context.Context, scope MemoryScope, sourceMessageID string, within time.Duration,
) (bool, error)
// ListTombstones returns the most recent rejections for this subject.
ListTombstones(ctx context.Context, scope MemoryScope, limit int) ([]*types.MemoryTombstone, error)
// HasTombstone reports whether this exact statement was already forgotten.
HasTombstone(ctx context.Context, scope MemoryScope, fingerprint string) (bool, error)
// ExpireOverdue archives items whose expires_at has passed and returns how
// many were archived.
ExpireOverdue(ctx context.Context, scope MemoryScope) (int64, error)
// MarkConsolidated records that this subject's whole store was just
// reviewed, so the next review waits out the interval.
MarkConsolidated(ctx context.Context, scope MemoryScope) error
// MarkForcedConsolidated records that this subject asked for a review
// themselves. Kept apart from MarkConsolidated so the daily pass and the
// button rate limit each other independently.
MarkForcedConsolidated(ctx context.Context, scope MemoryScope) error
// UpsertItemEmbedding stores or replaces the vector for one memory.
UpsertItemEmbedding(ctx context.Context, scope MemoryScope, embedding *types.MemoryItemEmbedding) error
// DeleteItemEmbedding drops one memory's vector. What a memory embeds to
// can change after it is written — an interest embeds the other wordings
// of its subject — and dropping the vector is how the existing backfill is
// asked to rebuild it.
DeleteItemEmbedding(ctx context.Context, scope MemoryScope, itemID string) error
// ItemEmbeddings loads the vectors for the given items, keyed by item id.
// Only vectors produced by modelID are returned: vectors from a different
// model are not comparable, so mixing them would score nonsense.
ItemEmbeddings(ctx context.Context, scope MemoryScope, itemIDs []string, modelID string) (map[string][]float32, error)
// ItemsMissingEmbeddings returns active items that have no vector yet, so a
// background pass can fill them in.
ItemsMissingEmbeddings(ctx context.Context, scope MemoryScope, modelID string, limit int) ([]*types.MemoryItem, error)
// ListLive returns items of one kind that the user can see: in use plus
// proposed and awaiting a decision.
ListLive(ctx context.Context, scope MemoryScope, kind string, limit int) ([]*types.MemoryItem, error)
// ConfirmPendingItem atomically activates a proposal and supersedes its
// conflicting active memories. A missing item returns (nil, nil).
ConfirmPendingItem(ctx context.Context, scope MemoryScope, id string) (*types.MemoryItem, error)
// RejectPendingItem atomically forgets a proposal and records its tombstone.
// Both review methods reject non-pending items with ErrMemoryItemNotPending.
RejectPendingItem(ctx context.Context, scope MemoryScope, id string) (*types.MemoryItem, error)
// BumpTopic records one more sighting of a topic and returns the running
// total, so a caller can decide whether it has recurred enough to promote.
BumpTopic(
ctx context.Context, scope MemoryScope, topic, normalizedKey, alias string,
) (*types.MemoryTopicStat, error)
// RenameTopic gives a subject a better canonical label, keeping the old one
// as an alias. Reports false when another row already holds the new key.
RenameTopic(ctx context.Context, scope MemoryScope, oldKey, newKey, newLabel string) (bool, error)
// MarkTopicPromoted stops a topic from being promoted again.
MarkTopicPromoted(ctx context.Context, scope MemoryScope, normalizedKey string) error
// TopTopics returns the most-asked topics, newest activity first.
TopTopics(ctx context.Context, scope MemoryScope, limit int) ([]*types.MemoryTopicStat, error)
// TopicByKey returns one subject's statistics, or nil when it is not
// tracked. It exists so an interest can be embedded together with the
// other wordings this person has used for the same subject.
TopicByKey(ctx context.Context, scope MemoryScope, normalizedKey string) (*types.MemoryTopicStat, error)
// TopicByID returns one topic row inside the scope, or (nil, nil) when it
// is absent. Used by the memory manager so a user can promote or dismiss
// a subject that has not yet become a memory.
TopicByID(ctx context.Context, scope MemoryScope, id string) (*types.MemoryTopicStat, error)
// ListUnpromotedTopics returns subjects that have been counted but have
// not yet become an interest, closest to the threshold first.
ListUnpromotedTopics(ctx context.Context, scope MemoryScope, limit, offset int) ([]*types.MemoryTopicStat, int64, error)
// DeleteTopic removes one topic row. Counting starts over if the subject
// is asked about again, unless a tombstone blocks promotion.
DeleteTopic(ctx context.Context, scope MemoryScope, id string) error
// DeleteAllTopics drops every topic counter in the scope. Clearing memory
// has to include these, or a subject sitting at N-1 would promote on the
// next question after the user asked to forget everything.
DeleteAllTopics(ctx context.Context, scope MemoryScope) error
// BumpDocAffinity records that an answer for this person drew on a document.
BumpDocAffinity(ctx context.Context, scope MemoryScope, docs []types.MemoryDocAffinity) error
// DocAffinity returns this person's affinity for the given documents.
DocAffinity(ctx context.Context, scope MemoryScope, knowledgeIDs []string) (map[string]int, error)
// TopDocAffinity returns the documents this person relies on most.
TopDocAffinity(ctx context.Context, scope MemoryScope, limit int) ([]*types.MemoryDocAffinity, error)
// DocAffinityByID returns one affinity row inside the scope, or (nil, nil)
// when it is absent.
DocAffinityByID(ctx context.Context, scope MemoryScope, id string) (*types.MemoryDocAffinity, error)
// ListFamiliarDocs returns documents cited often enough to count as a
// habit, most-used first.
ListFamiliarDocs(ctx context.Context, scope MemoryScope, minHits, limit, offset int) ([]*types.MemoryDocAffinity, int64, error)
// DeleteDocAffinity removes one document counter.
DeleteDocAffinity(ctx context.Context, scope MemoryScope, id string) error
// DeleteAllDocAffinity drops every document counter in the scope.
DeleteAllDocAffinity(ctx context.Context, scope MemoryScope) error
// ArchiveLowestRanked archives active items beyond the capacity cap and
// returns how many were archived.
ArchiveLowestRanked(ctx context.Context, scope MemoryScope, keep int) (int64, error)
// CountActive returns the number of active items in the scope.
CountActive(ctx context.Context, scope MemoryScope) (int64, error)
}
// MemoryRecall is what one turn pulls in: the resident block plus any
// situational items matched against the query.
type MemoryRecall struct {
// Prompt is the ready-to-append envelope, empty when nothing was recalled.
Prompt string
// Items are the situational items plus the resident items that produced
// Prompt, in the order they appear. Surfaced to the chat UI.
Items []*types.MemoryItem
}
// MemorySearchResult is what an on-demand lookup into the memory store
// returns.
//
// It is a separate type from MemoryRecall because the two have different
// consumers. Recall produces a prompt envelope for a turn; a search produces
// items for a tool, and that tool has to tell the model *why* it got nothing.
// "This user has memory switched off" and "nothing stored matches" call for
// different answers, and collapsing both into an empty slice would have the
// agent report a blank memory store to someone who simply disabled it.
type MemorySearchResult struct {
// Available is false when memory is off at any level — workspace, user or
// the agent handling this request.
Available bool
// Items are the matches, most relevant first.
Items []*types.MemoryItem
}
// RetrievalContext is what memory contributes to retrieval rather than to the
// answer prompt: who this person is, what they keep asking about, and which
// documents they rely on.
//
// This is the part of memory that earns its keep in a knowledge-base product.
// The same question means different things to different people — "how do I tune
// the segmentation" from someone working on medical imaging should not retrieve
// the same passages as it would from someone working on autonomous driving —
// and that difference has to be applied before retrieval, not after.
type RetrievalContext struct {
// Background is a compact description of the person, for query rewriting.
Background string
// Interests are the topics they keep returning to.
Interests []string
// Documents are titles they rely on, as vocabulary for the rewriter.
Documents []string
// Items are the memories behind the above, so the UI can show what
// influenced retrieval instead of it happening invisibly.
Items []*types.MemoryItem
}
func (c RetrievalContext) Empty() bool {
return c.Background == "" && len(c.Interests) == 0 && len(c.Documents) == 0
}
// MemoryService is the read/write API for long-term memory.
type MemoryService interface {
// Recall assembles the memory to inject for one turn. It performs no LLM
// calls and returns an empty recall (never an error) whenever memory is
// disabled at any level, so callers can use it unconditionally.
Recall(ctx context.Context, query string) MemoryRecall
// SearchMemory ranks the user's stored memories against an arbitrary
// query, for callers that need to reach past what Recall's per-turn budget
// admitted. Like Recall it performs no LLM call and never errors.
SearchMemory(ctx context.Context, query string, limit int) MemorySearchResult
// MemoryAvailable reports whether this request may read memory at all:
// the workspace switch, the user's own opt out and the agent's preference
// combined.
//
// Read paths do not need this — Recall and SearchMemory already degrade on
// their own. It exists for callers that must decide whether to *offer* a
// memory-backed feature, where the difference between "off" and "empty"
// has to be settled before anything is built rather than after it is
// called.
MemoryAvailable(ctx context.Context) bool
// RetrievalContextFor returns what memory contributes to retrieval. Like
// Recall it makes no model call and degrades to an empty value.
RetrievalContextFor(ctx context.Context) RetrievalContext
// DocumentAffinity scores the given documents by how much this person has
// relied on them before. Absent documents simply have no entry.
DocumentAffinity(ctx context.Context, knowledgeIDs []string) map[string]int
// RecordAnswerSources notes which documents an answer drew on, which is the
// only per-person retrieval signal available without asking anything.
RecordAnswerSources(ctx context.Context, refs []types.MemoryDocAffinity)
// ObserveQuestionTopics counts what a person asked about so a recurring
// subject can become an interest. Returns any newly promoted interests.
ObserveQuestionTopics(ctx context.Context, topics []string) []string
// Remember stores a statement the user explicitly asked to keep.
Remember(ctx context.Context, item types.MemoryItem) (*types.MemoryItem, error)
// ScheduleExtraction debounces and enqueues background distillation for a
// finished turn. Best effort: failures are logged, never returned.
ScheduleExtraction(ctx context.Context, sessionID, messageID, chatModelID string)
// Handle runs the background distillation task.
Handle(ctx context.Context, task *asynq.Task) error
// ListItems backs the memory manager list.
ListItems(ctx context.Context, status string, limit, offset int) ([]*types.MemoryItem, int64, error)
// ListTopics returns subjects that have been counted but not yet promoted
// into an interest, so the memory manager can show what is still forming.
ListTopics(ctx context.Context, limit, offset int) ([]*types.MemoryTopicView, int64, error)
// PromoteTopic turns a counted subject into an interest immediately,
// without waiting for the remaining hits.
PromoteTopic(ctx context.Context, id string) (*types.MemoryItem, error)
// DeleteTopic stops tracking a subject. The refusal is remembered so the
// same topic is not promoted automatically later.
DeleteTopic(ctx context.Context, id string) error
// ListDocuments returns documents this person keeps drawing answers from.
ListDocuments(ctx context.Context, limit, offset int) ([]*types.MemoryDocView, int64, error)
// DeleteDocument stops using one document as a personal retrieval signal.
DeleteDocument(ctx context.Context, id string) error
// FamiliarKnowledgeIDs returns the knowledge ids that have been cited
// often enough to count as a habit. Used to highlight Wiki pages whose
// source documents this person actually works from. Empty on any failure
// so callers can use it unconditionally.
FamiliarKnowledgeIDs(ctx context.Context) []string
// CreateItem adds a memory typed by the user in the memory manager.
CreateItem(ctx context.Context, kind, content string, importance int) (*types.MemoryItem, error)
// ConfirmItem accepts a memory the system inferred, so it starts being used.
ConfirmItem(ctx context.Context, id string) (*types.MemoryItem, error)
// RejectItem declines an inference and remembers the refusal, so the same
// guess is not proposed again next week.
RejectItem(ctx context.Context, id string) error
// UpdateItem edits one item's content and importance.
UpdateItem(ctx context.Context, id, content string, importance int) (*types.MemoryItem, error)
// DeleteItem forgets one item.
DeleteItem(ctx context.Context, id string) error
// Clear forgets everything in the caller's memory space.
Clear(ctx context.Context) (int64, error)
// ConsolidateNow reviews the caller's store for near-duplicates and stale
// tasks immediately, without waiting for the daily distillation pass.
ConsolidateNow(ctx context.Context) (*types.MemoryConsolidationResult, error)
// GetSettings returns the effective per-user memory settings.
GetSettings(ctx context.Context) (*types.MemorySettings, error)
// SetEnabled flips the per-user opt out.
SetEnabled(ctx context.Context, enabled bool) error
// RetryExtraction renews only the caller's exhausted automatic retry budget.
RetryExtraction(ctx context.Context) error
}
+131
View File
@@ -0,0 +1,131 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// MessageService defines the message service interface
type MessageService interface {
// CreateMessage creates a message
CreateMessage(ctx context.Context, message *types.Message) (*types.Message, error)
// GetMessage gets a message
GetMessage(ctx context.Context, sessionID string, id string) (*types.Message, error)
// GetMessagesBySession gets all messages of a session
GetMessagesBySession(ctx context.Context, sessionID string, page int, pageSize int) ([]*types.Message, error)
// GetRecentMessagesBySession gets recent messages of a session
GetRecentMessagesBySession(ctx context.Context, sessionID string, limit int) ([]*types.Message, error)
// GetMessagesBySessionBeforeTime gets messages before a specific time of a session
GetMessagesBySessionBeforeTime(
ctx context.Context, sessionID string, beforeTime time.Time, limit int,
) ([]*types.Message, error)
// UpdateMessage updates a message
UpdateMessage(ctx context.Context, message *types.Message) error
// UpdateMessageImages updates only the images JSONB column for a message.
UpdateMessageImages(ctx context.Context, sessionID, messageID string, images types.MessageImages) error
// UpdateMessageRenderedContent updates the rendered_content column for a user message.
UpdateMessageRenderedContent(ctx context.Context, sessionID, messageID string, renderedContent string) error
// DeleteMessage deletes a message
DeleteMessage(ctx context.Context, sessionID string, id string) error
// ClearSessionMessages deletes all messages in a session, along with their chat history KB entries
ClearSessionMessages(ctx context.Context, sessionID string) error
// SearchMessages searches messages by keyword and/or vector similarity across the caller's own sessions.
// Uses the chat history knowledge base for vector search instead of in-memory computation.
SearchMessages(ctx context.Context, params *types.MessageSearchParams) (*types.MessageSearchResult, error)
// IndexMessageToKB indexes a message (Q&A pair) into the chat history knowledge base asynchronously.
// Called after assistant message is created to enable future vector search.
IndexMessageToKB(ctx context.Context, userQuery string, assistantAnswer string, messageID string, sessionID string)
// DeleteMessageKnowledge deletes the Knowledge entry associated with a message from the chat history KB.
DeleteMessageKnowledge(ctx context.Context, knowledgeID string)
// DeleteSessionKnowledge deletes all Knowledge entries for messages in a session from the chat history KB.
DeleteSessionKnowledge(ctx context.Context, sessionID string)
// GetChatHistoryKBStats returns statistics about the chat history knowledge base (indexed message count, etc.)
GetChatHistoryKBStats(ctx context.Context) (*types.ChatHistoryKBStats, error)
// GetSessionArtifacts returns every skill-produced MessageArtifact
// recorded against any assistant message of the session. Used to power
// the frontend "download files generated in this session" drawer and
// to clean up storage blobs on session deletion.
GetSessionArtifacts(ctx context.Context, sessionID string) (types.MessageArtifacts, error)
}
// MessageRepository defines the message repository interface
type MessageRepository interface {
// CreateMessage creates a message
CreateMessage(ctx context.Context, message *types.Message) (*types.Message, error)
// GetMessage gets a message
GetMessage(ctx context.Context, sessionID string, id string) (*types.Message, error)
// GetMessagesBySession gets all messages of a session
GetMessagesBySession(ctx context.Context, sessionID string, page int, pageSize int) ([]*types.Message, error)
// GetRecentMessagesBySession gets recent messages of a session
GetRecentMessagesBySession(ctx context.Context, sessionID string, limit int) ([]*types.Message, error)
// GetMessagesBySessionBeforeTime gets messages before a specific time of a session
GetMessagesBySessionBeforeTime(
ctx context.Context, sessionID string, beforeTime time.Time, limit int,
) ([]*types.Message, error)
// ListMessagesBySessionAfterTime returns messages created strictly after
// afterTime, oldest first. Long-term memory distillation walks forward from
// a watermark, so it needs the oldest unprocessed messages rather than the
// newest ones: paging from the newest end would skip everything in between
// once a session outruns the page size.
ListMessagesBySessionAfterTime(
ctx context.Context, sessionID string, afterTime time.Time, limit int,
) ([]*types.Message, error)
// MemoryOwnedSessionIDs enforces exact tenant/principal ownership. Legacy
// unowned and shared group-thread sessions cannot populate personal memory.
MemoryOwnedSessionIDs(ctx context.Context, scope MemoryScope, sessionIDs []string) (map[string]bool, error)
// MemoryTurnStart returns the user input time for a completed turn, only
// inside that principal's session. A missing or foreign turn is an error.
MemoryTurnStart(ctx context.Context, scope MemoryScope, sessionID, messageID string) (time.Time, error)
// MemoryAllowedMessageIDs rechecks the Agent attached to each completed turn.
MemoryAllowedMessageIDs(ctx context.Context, scope MemoryScope, sessionID string, messages []*types.Message) (map[string]bool, error)
// UpdateMessage updates a message
UpdateMessage(ctx context.Context, message *types.Message) error
// UpdateMessageImages updates only the images JSONB column for a message
UpdateMessageImages(ctx context.Context, sessionID, messageID string, images types.MessageImages) error
// UpdateMessageRenderedContent updates the rendered_content column for a user message
UpdateMessageRenderedContent(ctx context.Context, sessionID, messageID string, renderedContent string) error
// DeleteMessage deletes a message
DeleteMessage(ctx context.Context, sessionID string, id string) error
// DeleteMessagesBySessionID deletes all messages belonging to a session
DeleteMessagesBySessionID(ctx context.Context, sessionID string) error
// GetFirstMessageOfUser gets the first message of a user
GetFirstMessageOfUser(ctx context.Context, sessionID string) (*types.Message, error)
// SearchMessagesByKeyword searches messages by keyword (ILIKE) across sessions for a tenant
// OwnedSessionIDs narrows a set of session ids to the ones this person owns.
OwnedSessionIDs(ctx context.Context, tenantID uint64, ownerID string, sessionIDs []string) (map[string]bool, error)
SearchMessagesByKeyword(ctx context.Context, tenantID uint64, ownerID, keyword string, sessionIDs []string, limit int) ([]*types.MessageWithSession, error)
// GetMessagesByKnowledgeIDs retrieves messages by their associated Knowledge IDs
GetMessagesByKnowledgeIDs(ctx context.Context, knowledgeIDs []string) ([]*types.MessageWithSession, error)
// GetMessagesByRequestIDs retrieves messages by their request IDs (used to fetch Q&A pair partners)
GetMessagesByRequestIDs(ctx context.Context, requestIDs []string) ([]*types.MessageWithSession, error)
// GetKnowledgeIDsBySessionID retrieves all knowledge IDs for messages in a session
GetKnowledgeIDsBySessionID(ctx context.Context, sessionID string) ([]string, error)
// UpdateMessageKnowledgeID updates the knowledge_id field for a message
UpdateMessageKnowledgeID(ctx context.Context, messageID string, knowledgeID string) error
// GetSessionArtifacts returns every skill-produced MessageArtifact recorded
// against any assistant message of the session, in creation order. The
// implementation only projects the artifacts JSONB column, so it stays
// cheap even for long conversations. Empty slice + nil error means the
// session has no artifacts yet (never an error).
GetSessionArtifacts(ctx context.Context, sessionID string) (types.MessageArtifacts, error)
// GetSessionAttachments returns every user-uploaded attachment recorded in
// the session. Implementations should project only the attachments column.
GetSessionAttachments(ctx context.Context, sessionID string) (types.MessageAttachments, error)
}
@@ -0,0 +1,53 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// MessageSuggestionRepository persists generated suggestions and their
// product-analytics events. AcquireGeneration serializes duplicate requests
// from reconnecting clients with an expiring lease.
type MessageSuggestionRepository interface {
GetByCacheKey(
ctx context.Context,
tenantID uint64,
assistantMessageID string,
placement string,
configHash string,
locale string,
) (*types.MessageSuggestionSet, error)
GetByID(ctx context.Context, tenantID uint64, sessionID string, id string) (*types.MessageSuggestionSet, error)
AcquireGeneration(
ctx context.Context,
set *types.MessageSuggestionSet,
regenerate bool,
) (*types.MessageSuggestionSet, bool, error)
Save(ctx context.Context, set *types.MessageSuggestionSet) error
CreateEvent(ctx context.Context, event *types.MessageSuggestionEvent) error
DeleteByMessageID(ctx context.Context, tenantID uint64, sessionID string, messageID string) error
DeleteBySessionID(ctx context.Context, tenantID uint64, sessionID string) error
}
type MessageSuggestionService interface {
EnsureFollowUps(
ctx context.Context,
sessionID string,
assistantMessageID string,
regenerate bool,
) (*types.MessageSuggestionSet, error)
GetFollowUps(
ctx context.Context,
sessionID string,
assistantMessageID string,
) (*types.MessageSuggestionSet, error)
RecordEvent(
ctx context.Context,
sessionID string,
setID string,
questionID string,
eventType string,
) error
ValidateAttribution(ctx context.Context, sessionID string, query string, attribution *types.SuggestionAttribution) error
}
+69
View File
@@ -0,0 +1,69 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/models/asr"
"github.com/Tencent/WeKnora/internal/models/chat"
"github.com/Tencent/WeKnora/internal/models/embedding"
"github.com/Tencent/WeKnora/internal/models/rerank"
"github.com/Tencent/WeKnora/internal/models/vlm"
"github.com/Tencent/WeKnora/internal/types"
)
// ModelService defines the model service interface
type ModelService interface {
// CreateModel creates a model
CreateModel(ctx context.Context, model *types.Model) error
// GetModelByID gets a model by ID
GetModelByID(ctx context.Context, id string) (*types.Model, error)
// ListModels lists all models
ListModels(ctx context.Context) ([]*types.Model, error)
// UpdateModel updates a model
UpdateModel(ctx context.Context, model *types.Model) error
// DeleteModel deletes a model
DeleteModel(ctx context.Context, id string) error
// UpdateModelCredentials writes one or more credential fields on the
// model's Parameters. Nil pointer means "do not touch this field";
// empty string is treated as no-op (use ClearModelCredential to remove).
// Returns the updated model.
UpdateModelCredentials(ctx context.Context, id string, apiKey, appSecret *string) (*types.Model, error)
// ClearModelCredential removes a single credential field. field must be
// "api_key" or "app_secret". Clearing an already-empty field is a no-op.
ClearModelCredential(ctx context.Context, id, field string) error
// GetEmbeddingModel gets an embedding model
GetEmbeddingModel(ctx context.Context, modelId string) (embedding.Embedder, error)
// GetEmbeddingModelForTenant gets an embedding model for a specific tenant (for cross-tenant sharing)
GetEmbeddingModelForTenant(ctx context.Context, modelId string, tenantID uint64) (embedding.Embedder, error)
// GetRerankModel gets a rerank model
GetRerankModel(ctx context.Context, modelId string) (rerank.Reranker, error)
// GetChatModel gets a chat model
GetChatModel(ctx context.Context, modelId string) (chat.Chat, error)
// GetVLMModel gets a vision language model
GetVLMModel(ctx context.Context, modelId string) (vlm.VLM, error)
// GetASRModel gets an automatic speech recognition model
GetASRModel(ctx context.Context, modelId string) (asr.ASR, error)
}
// ModelRepository defines the model repository interface
type ModelRepository interface {
// Create creates a model
Create(ctx context.Context, model *types.Model) error
// GetByID gets a model by ID
GetByID(ctx context.Context, tenantID uint64, id string) (*types.Model, error)
// List lists all models
List(
ctx context.Context,
tenantID uint64,
modelType types.ModelType,
source types.ModelSource,
) ([]*types.Model, error)
// Update updates a model
Update(ctx context.Context, model *types.Model) error
// Delete deletes a model
Delete(ctx context.Context, tenantID uint64, id string) error
// ClearDefaultByType clears the default flag for all models of a specific type
// optionally excluding a specific model ID.
ClearDefaultByType(ctx context.Context, tenantID uint, modelType types.ModelType, excludeID string) error
}
+221
View File
@@ -0,0 +1,221 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// OrganizationService defines the organization service interface.
//
// Plan 3 of #1303: organization membership is per-tenant, not per-user.
// Adding/removing/updating "members" therefore takes a tenant_id, and
// permission checks key on (org, tenant). RepresentativeUserID is
// informational and not load-bearing.
type OrganizationService interface {
// Organization CRUD
CreateOrganization(ctx context.Context, userID string, tenantID uint64, req *types.CreateOrganizationRequest) (*types.Organization, error)
GetOrganization(ctx context.Context, id string) (*types.Organization, error)
GetOrganizationByInviteCode(ctx context.Context, inviteCode string) (*types.Organization, error)
ListTenantOrganizations(ctx context.Context, tenantID uint64) ([]*types.Organization, error)
UpdateOrganization(ctx context.Context, id string, userID string, tenantID uint64, req *types.UpdateOrganizationRequest) (*types.Organization, error)
DeleteOrganization(ctx context.Context, id string, userID string, tenantID uint64) error
// Member Management (Plan 3: members are tenants)
// AddTenantMember adds a tenant to the org with the given role; representativeUserID
// is only used for display labelling.
AddTenantMember(ctx context.Context, orgID string, tenantID uint64, representativeUserID string, role types.OrgMemberRole) error
RemoveTenantMember(ctx context.Context, orgID string, memberTenantID uint64, operatorUserID string, operatorTenantID uint64) error
UpdateTenantMemberRole(ctx context.Context, orgID string, memberTenantID uint64, role types.OrgMemberRole, operatorUserID string, operatorTenantID uint64) error
ListTenantMembers(ctx context.Context, orgID string) ([]*types.OrganizationTenantMember, error)
GetTenantMember(ctx context.Context, orgID string, tenantID uint64) (*types.OrganizationTenantMember, error)
// Invite Code
GenerateInviteCode(ctx context.Context, orgID string, userID string, tenantID uint64) (string, error)
JoinByInviteCode(ctx context.Context, inviteCode string, userID string, tenantID uint64) (*types.Organization, error)
// Searchable organizations (discovery)
SearchSearchableOrganizations(ctx context.Context, tenantID uint64, query string, limit int) (*types.ListSearchableOrganizationsResponse, error)
JoinByOrganizationID(ctx context.Context, orgID string, userID string, tenantID uint64, message string, requestedRole types.OrgMemberRole) (*types.Organization, error)
// Join Requests (for organizations that require approval)
SubmitJoinRequest(ctx context.Context, orgID string, userID string, tenantID uint64, message string, requestedRole types.OrgMemberRole) (*types.OrganizationJoinRequest, error)
ListJoinRequests(ctx context.Context, orgID string) ([]*types.OrganizationJoinRequest, error)
CountPendingJoinRequests(ctx context.Context, orgID string) (int64, error)
ReviewJoinRequest(ctx context.Context, orgID string, requestID string, approved bool, reviewerID string, reviewerTenantID uint64, message string, assignRole *types.OrgMemberRole) error
// Role Upgrade Requests (for existing tenants to request higher permissions)
RequestRoleUpgrade(ctx context.Context, orgID string, userID string, tenantID uint64, requestedRole types.OrgMemberRole, message string) (*types.OrganizationJoinRequest, error)
GetPendingUpgradeRequest(ctx context.Context, orgID string, tenantID uint64) (*types.OrganizationJoinRequest, error)
// Permission Check (drive purely off the requesting tenant)
IsTenantOrgAdmin(ctx context.Context, orgID string, tenantID uint64) (bool, error)
GetTenantRoleInOrg(ctx context.Context, orgID string, tenantID uint64) (types.OrgMemberRole, error)
}
// OrganizationRepository defines the organization repository interface.
//
// Members are stored per-tenant in `organization_tenant_members`.
type OrganizationRepository interface {
// Organization CRUD
Create(ctx context.Context, org *types.Organization) error
GetByID(ctx context.Context, id string) (*types.Organization, error)
GetByInviteCode(ctx context.Context, inviteCode string) (*types.Organization, error)
ListByTenantID(ctx context.Context, tenantID uint64) ([]*types.Organization, error)
ListSearchable(ctx context.Context, query string, limit int) ([]*types.Organization, error)
Update(ctx context.Context, org *types.Organization) error
Delete(ctx context.Context, id string) error
// Tenant member operations
AddTenantMember(ctx context.Context, member *types.OrganizationTenantMember) error
RemoveTenantMember(ctx context.Context, orgID string, tenantID uint64) error
UpdateTenantMemberRole(ctx context.Context, orgID string, tenantID uint64, role types.OrgMemberRole) error
ListTenantMembers(ctx context.Context, orgID string) ([]*types.OrganizationTenantMember, error)
GetTenantMember(ctx context.Context, orgID string, tenantID uint64) (*types.OrganizationTenantMember, error)
ListTenantMembersByTenantForOrgs(ctx context.Context, tenantID uint64, orgIDs []string) (map[string]*types.OrganizationTenantMember, error)
CountTenantMembers(ctx context.Context, orgID string) (int64, error)
// Invite code
UpdateInviteCode(ctx context.Context, orgID string, inviteCode string, expiresAt *time.Time) error
// Join requests (still keyed on the *requesting* user, but a single
// pending request per (org, tenant) since the approve action grants
// the whole tenant access).
CreateJoinRequest(ctx context.Context, request *types.OrganizationJoinRequest) error
GetJoinRequestByID(ctx context.Context, id string) (*types.OrganizationJoinRequest, error)
GetPendingJoinRequestByTenant(ctx context.Context, orgID string, tenantID uint64) (*types.OrganizationJoinRequest, error)
GetPendingRequestByTenantAndType(ctx context.Context, orgID string, tenantID uint64, requestType types.JoinRequestType) (*types.OrganizationJoinRequest, error)
ListJoinRequests(ctx context.Context, orgID string, status types.JoinRequestStatus) ([]*types.OrganizationJoinRequest, error)
CountJoinRequests(ctx context.Context, orgID string, status types.JoinRequestStatus) (int64, error)
UpdateJoinRequestStatus(ctx context.Context, id string, status types.JoinRequestStatus, reviewedBy string, reviewMessage string) error
}
// KBShareService defines the knowledge base sharing service interface.
//
// Plan 3 of #1303: permission resolution keys on the caller's tenant
// (not user). The 3-dimension cap is applied inside CheckTenantKBPermission
// and is the canonical permission gate for shared KBs:
//
// effective = min(share.Permission, tenant_org_role, tenant_role_cap)
//
// where tenant_role_cap pins tenant Viewers to OrgRoleViewer regardless
// of the org-level grant — Viewer in your own tenant must always be
// read-only on shared resources.
type KBShareService interface {
// Share Management
ShareKnowledgeBase(ctx context.Context, kbID string, orgID string, userID string, tenantID uint64, permission types.OrgMemberRole) (*types.KnowledgeBaseShare, error)
UpdateSharePermission(ctx context.Context, shareID string, permission types.OrgMemberRole, userID string, tenantID uint64) error
RemoveShare(ctx context.Context, shareID string, userID string, tenantID uint64) error
// Query
// ListSharesByKnowledgeBase lists shares for a KB; tenantID must own the KB (authz check).
ListSharesByKnowledgeBase(ctx context.Context, kbID string, tenantID uint64) ([]*types.KnowledgeBaseShare, error)
ListSharesByOrganization(ctx context.Context, orgID string) ([]*types.KnowledgeBaseShare, error)
ListSharedKnowledgeBases(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole) ([]*types.SharedKnowledgeBaseInfo, error)
ListSharedKnowledgeBasesInOrganization(ctx context.Context, orgID string, tenantID uint64, callerTenantRole types.TenantRole) ([]*types.OrganizationSharedKnowledgeBaseItem, error)
// ListSharedKnowledgeBaseIDsByOrganizations returns per-org direct shared KB IDs (batch, for sidebar count).
ListSharedKnowledgeBaseIDsByOrganizations(ctx context.Context, orgIDs []string, tenantID uint64) (map[string][]string, error)
GetShare(ctx context.Context, shareID string) (*types.KnowledgeBaseShare, error)
GetShareByKBAndOrg(ctx context.Context, kbID string, orgID string) (*types.KnowledgeBaseShare, error)
// Permission Check (Plan 3: tenant-keyed). Returns the effective
// OrgMemberRole the caller can use against the shared KB after the
// 3-D cap, plus a bool indicating whether *any* share matches the
// caller's tenant. callerTenantRole is the caller's tenant-RBAC
// role and is used to apply the Viewer cap.
CheckTenantKBPermission(ctx context.Context, kbID string, callerTenantID uint64, callerTenantRole types.TenantRole) (types.OrgMemberRole, bool, error)
// HasTenantKBPermission is a thin "do I have at least N" wrapper over
// CheckTenantKBPermission.
HasTenantKBPermission(ctx context.Context, kbID string, callerTenantID uint64, callerTenantRole types.TenantRole, requiredRole types.OrgMemberRole) (bool, error)
// Get source tenant for cross-tenant embedding
GetKBSourceTenant(ctx context.Context, kbID string) (uint64, error)
// Count shares for knowledge bases
CountSharesByKnowledgeBaseIDs(ctx context.Context, kbIDs []string) (map[string]int64, error)
// CountByOrganizations returns share counts per organization (for sidebar); excludes deleted KBs
CountByOrganizations(ctx context.Context, orgIDs []string) (map[string]int64, error)
}
// KBShareRepository defines the knowledge base sharing repository interface
type KBShareRepository interface {
// CRUD
Create(ctx context.Context, share *types.KnowledgeBaseShare) error
GetByID(ctx context.Context, id string) (*types.KnowledgeBaseShare, error)
GetByKBAndOrg(ctx context.Context, kbID string, orgID string) (*types.KnowledgeBaseShare, error)
Update(ctx context.Context, share *types.KnowledgeBaseShare) error
Delete(ctx context.Context, id string) error
// DeleteByKnowledgeBaseID soft-deletes all shares for a knowledge base (e.g. when KB is deleted)
DeleteByKnowledgeBaseID(ctx context.Context, kbID string) error
// DeleteByOrganizationID soft-deletes all shares for an organization (e.g. when the org is deleted)
DeleteByOrganizationID(ctx context.Context, orgID string) error
// List
ListByKnowledgeBase(ctx context.Context, kbID string) ([]*types.KnowledgeBaseShare, error)
ListByOrganization(ctx context.Context, orgID string) ([]*types.KnowledgeBaseShare, error)
ListByOrganizations(ctx context.Context, orgIDs []string) ([]*types.KnowledgeBaseShare, error)
CountByOrganizations(ctx context.Context, orgIDs []string) (map[string]int64, error)
// Query for tenant's accessible shared knowledge bases
ListSharedKBsForTenant(ctx context.Context, tenantID uint64) ([]*types.KnowledgeBaseShare, error)
// Count shares
CountSharesByKnowledgeBaseID(ctx context.Context, kbID string) (int64, error)
CountSharesByKnowledgeBaseIDs(ctx context.Context, kbIDs []string) (map[string]int64, error)
}
// AgentShareService defines the agent sharing service interface.
//
// Plan 3 of #1303: visibility and access checks key on the caller's
// tenant. callerTenantRole flows through so the 3-D cap (tenant Viewer
// → at most OrgRoleViewer) can be applied consistently.
type AgentShareService interface {
ShareAgent(ctx context.Context, agentID string, orgID string, userID string, tenantID uint64, permission types.OrgMemberRole) (*types.AgentShare, error)
RemoveShare(ctx context.Context, shareID string, userID string, tenantID uint64) error
ListSharesByAgent(ctx context.Context, agentID string, tenantID uint64) ([]*types.AgentShare, error)
ListSharesByOrganization(ctx context.Context, orgID string) ([]*types.AgentShare, error)
ListSharedAgents(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole) ([]*types.SharedAgentInfo, error)
ListSharedAgentsInOrganization(ctx context.Context, orgID string, tenantID uint64, callerTenantRole types.TenantRole) ([]*types.OrganizationSharedAgentItem, error)
// ListSharedAgentsInOrganizations returns per-org agent list (batch, for sidebar count merge).
ListSharedAgentsInOrganizations(ctx context.Context, orgIDs []string, tenantID uint64, callerTenantRole types.TenantRole) (map[string][]*types.OrganizationSharedAgentItem, error)
// SetSharedAgentDisabledByMe sets whether the current tenant has "disabled" this shared agent for their conversation dropdown (per-tenant preference; will be revisited in a follow-up PR).
SetSharedAgentDisabledByMe(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64, disabled bool) error
// GetSharedAgentForTenant returns the shared agent by agentID if the caller's tenant has access; used to resolve KB scope for @ mention.
GetSharedAgentForTenant(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, agentID string, sourceTenantID ...uint64) (*types.CustomAgent, error)
// TenantCanAccessKBViaSomeSharedAgent returns true if the caller's tenant has at least one shared agent that can access the given KB (for opening KB detail from "通过智能体可见" list without passing agent_id).
TenantCanAccessKBViaSomeSharedAgent(ctx context.Context, tenantID uint64, callerTenantRole types.TenantRole, kb *types.KnowledgeBase) (bool, error)
GetShare(ctx context.Context, shareID string) (*types.AgentShare, error)
GetShareByAgentAndOrg(ctx context.Context, agentID string, orgID string) (*types.AgentShare, error)
// GetShareByAgentIDForTenant returns one share for the given agentID that the tenant can access, excluding source_tenant_id == excludeTenantID (e.g. caller's own tenant to get shared-from-other only).
GetShareByAgentIDForTenant(ctx context.Context, tenantID uint64, agentID string, excludeTenantID uint64) (*types.AgentShare, error)
// CountByOrganizations returns share counts per organization (for sidebar); excludes deleted agents
CountByOrganizations(ctx context.Context, orgIDs []string) (map[string]int64, error)
}
// AgentShareRepository defines the agent sharing repository interface
type AgentShareRepository interface {
Create(ctx context.Context, share *types.AgentShare) error
GetByID(ctx context.Context, id string) (*types.AgentShare, error)
GetByAgentAndOrg(ctx context.Context, agentID string, orgID string) (*types.AgentShare, error)
Update(ctx context.Context, share *types.AgentShare) error
Delete(ctx context.Context, id string) error
DeleteByAgentIDAndSourceTenant(ctx context.Context, agentID string, sourceTenantID uint64) error
DeleteByOrganizationID(ctx context.Context, orgID string) error
ListByAgent(ctx context.Context, agentID string) ([]*types.AgentShare, error)
ListByOrganization(ctx context.Context, orgID string) ([]*types.AgentShare, error)
ListByOrganizations(ctx context.Context, orgIDs []string) ([]*types.AgentShare, error)
ListSharedAgentsForTenant(ctx context.Context, tenantID uint64) ([]*types.AgentShare, error)
GetShareByAgentIDAndSourceForTenant(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64) (*types.AgentShare, error)
CountByOrganizations(ctx context.Context, orgIDs []string) (map[string]int64, error)
// GetShareByAgentIDForTenant returns one share for the given agentID that the tenant can access (tenant in org), excluding source_tenant_id == excludeTenantID.
GetShareByAgentIDForTenant(ctx context.Context, tenantID uint64, agentID string, excludeTenantID uint64) (*types.AgentShare, error)
}
// TenantDisabledSharedAgentRepository stores per-tenant "disabled" agents (hidden from conversation dropdown; own and shared)
type TenantDisabledSharedAgentRepository interface {
ListByTenantID(ctx context.Context, tenantID uint64) ([]*types.TenantDisabledSharedAgent, error)
// ListDisabledOwnAgentIDs returns agent IDs that this tenant has disabled for their own agents (source_tenant_id = tenant_id)
ListDisabledOwnAgentIDs(ctx context.Context, tenantID uint64) ([]string, error)
Add(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64) error
Remove(ctx context.Context, tenantID uint64, agentID string, sourceTenantID uint64) error
}
+78
View File
@@ -0,0 +1,78 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// ResourceCleaner owns process-lifetime cleanup callbacks (database clients,
// worker pools, etc.). It is unrelated to persisted StoredResource records.
type ResourceCleaner interface {
Register(cleanup types.CleanupFunc)
RegisterWithName(name string, cleanup types.CleanupFunc)
Cleanup(ctx context.Context) []error
}
// ResourceRepository persists stable resource identities, ownership bindings,
// and short-lived access grants.
type ResourceRepository interface {
Create(ctx context.Context, resource *types.StoredResource) error
GetByID(ctx context.Context, id string) (*types.StoredResource, error)
GetByHandle(ctx context.Context, handle string) (*types.StoredResource, error)
GetByTenantLocation(ctx context.Context, tenantID uint64, locationHash string) (*types.StoredResource, error)
MarkDeleted(ctx context.Context, id string) error
CreateBinding(ctx context.Context, binding *types.ResourceBinding) error
DeleteBinding(ctx context.Context, resourceID, ownerType, ownerID string) error
CountBindings(ctx context.Context, resourceID string) (int64, error)
CreateGrant(ctx context.Context, grant *types.ResourceAccessGrant) error
GetValidGrant(ctx context.Context, tokenHash string, now time.Time) (*types.ResourceAccessGrant, error)
DeleteExpiredGrants(ctx context.Context, before time.Time) error
}
// ResourceRegistration describes one physical object at registration time.
type ResourceRegistration struct {
Kind string
MimeType string
OriginalName string
Size int64
ContentHash string
Temporary bool
}
// ResourceFileScope identifies the domain object authorizing a file proxy.
// TenantID is the file owner's tenant; the caller remains in ctx.
type ResourceFileScope struct {
TenantID uint64
KnowledgeBaseID string
MessageID string
SessionID string
EmbedChannelID string
}
type ResourceFileAuthorizer interface {
AuthorizeFileRead(ctx context.Context, value string, scope ResourceFileScope) error
}
// ResourceCatalog maps public resource references to internal storage
// locations and manages their access capabilities.
type ResourceCatalog interface {
Register(ctx context.Context, tenantID uint64, physicalPath string, meta ResourceRegistration) (string, error)
Resolve(ctx context.Context, reference string) (*types.StoredResource, error)
ResolvePath(ctx context.Context, value string) (string, *types.StoredResource, error)
Bind(ctx context.Context, reference, ownerType, ownerID, relation string) error
// Release drops one owner's claim on a resource and reports how many
// claims remain. Callers use the count to decide whether the bytes may be
// deleted: a resource still referenced by another owner — the assistant
// message an answer was saved from, a second knowledge entry — must
// survive the owner being removed.
//
// remaining is -1 when the answer is unknown (the reference is not a
// catalog handle, or the count could not be read), which callers should
// treat as "delete as before" rather than as "keep forever".
Release(ctx context.Context, reference, ownerType, ownerID string) (remaining int64, err error)
MarkDeleted(ctx context.Context, reference string) error
CreateAccessGrant(ctx context.Context, reference string, ttl time.Duration) (string, error)
ResolveAccessGrant(ctx context.Context, token string) (*types.StoredResource, error)
}
+156
View File
@@ -0,0 +1,156 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/models/embedding"
"github.com/Tencent/WeKnora/internal/types"
)
// RetrieveEngine defines the retrieve engine interface
type RetrieveEngine interface {
// EngineType gets the retrieve engine type
EngineType() types.RetrieverEngineType
// Retrieve executes the retrieve
Retrieve(ctx context.Context, params types.RetrieveParams) ([]*types.RetrieveResult, error)
// Support gets the supported retrieve types
Support() []types.RetrieverType
}
// RetrieveEngineRepository defines the retrieve engine repository interface
type RetrieveEngineRepository interface {
// Save saves the index info
Save(ctx context.Context, indexInfo *types.IndexInfo, params map[string]any) error
// BatchSave saves the index info list
BatchSave(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) error
// EstimateStorageSize estimates the storage size
EstimateStorageSize(ctx context.Context, indexInfoList []*types.IndexInfo, params map[string]any) int64
// DeleteByChunkIDList deletes the index info by chunk id list
DeleteByChunkIDList(ctx context.Context, indexIDList []string, dimension int, knowledgeType string) error
// DeleteBySourceIDList deletes the index info by source id list
DeleteBySourceIDList(ctx context.Context, sourceIDList []string, dimension int, knowledgeType string) error
// 复制索引数据
// sourceKnowledgeBaseID: 源知识库ID
// sourceToTargetChunkIDMap: 源分块ID到目标分块ID的映射关系
// targetKnowledgeBaseID: 目标知识库ID
// params: 额外参数,如向量表示等
CopyIndices(
ctx context.Context,
sourceKnowledgeBaseID string,
sourceToTargetKBIDMap map[string]string,
sourceToTargetChunkIDMap map[string]string,
targetKnowledgeBaseID string,
dimension int,
knowledgeType string,
) error
// DeleteByKnowledgeIDList deletes the index info by knowledge id list
DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int, knowledgeType string) error
// BatchUpdateChunkEnabledStatus updates the enabled status of chunks in batch
// chunkStatusMap: map of chunk ID to enabled status (true = enabled, false = disabled)
BatchUpdateChunkEnabledStatus(ctx context.Context, chunkStatusMap map[string]bool) error
// BatchUpdateChunkTagID updates the tag ID of chunks in batch
// chunkTagMap: map of chunk ID to tag ID (empty string means no tag)
BatchUpdateChunkTagID(ctx context.Context, chunkTagMap map[string]string) error
// RetrieveEngine retrieves the engine
RetrieveEngine
}
// RetrieveEngineRegistry defines the retrieve engine registry interface
type RetrieveEngineRegistry interface {
// Register registers the retrieve engine service
Register(indexService RetrieveEngineService) error
// GetRetrieveEngineService gets the retrieve engine service
GetRetrieveEngineService(engineType types.RetrieverEngineType) (RetrieveEngineService, error)
// GetAllRetrieveEngineServices gets all retrieve engine services
GetAllRetrieveEngineServices() []RetrieveEngineService
// GetByStoreID returns the engine service registered for a specific DB store ID.
//
// IMPORTANT: This method does NOT verify tenant ownership of the returned
// store. Callers MUST use the CreateRetrieveEngineForKB /
// CreateRetrieveEngineFromPayload factory functions in the retriever package
// rather than calling this directly. The factories wrap GetByStoreID with
// tenant ownership verification (defense-in-depth against cross-tenant IDOR).
GetByStoreID(storeID string) (RetrieveEngineService, error)
// GetOrLoadByStoreID returns the engine for storeID, rebuilding it from the
// database when this process has no entry for it. The registry is per-process:
// an engine registered on one instance is missing on every other until that
// instance restarts, and an engine whose creation failed during startup stays
// missing even across restarts. Rebuilding on demand lets both cases recover
// without an operator-driven rollout.
//
// Unlike GetByStoreID, this method scopes its database lookup to tenantID, so
// it cannot hydrate a store belonging to another tenant. Callers should still
// verify ownership first: the tenant scope is defense-in-depth, not a
// replacement for the ownership check.
GetOrLoadByStoreID(
ctx context.Context, tenantID uint64, storeID string,
) (RetrieveEngineService, error)
}
// RetrieveEngineService defines the retrieve engine service interface
type RetrieveEngineService interface {
// Index indexes the index info
Index(ctx context.Context,
embedder embedding.Embedder,
indexInfo *types.IndexInfo,
retrieverTypes []types.RetrieverType,
) error
// BatchIndex indexes the index info list
BatchIndex(ctx context.Context,
embedder embedding.Embedder,
indexInfoList []*types.IndexInfo,
retrieverTypes []types.RetrieverType,
) error
// EstimateStorageSize estimates the storage size
EstimateStorageSize(ctx context.Context,
embedder embedding.Embedder,
indexInfoList []*types.IndexInfo,
retrieverTypes []types.RetrieverType,
) int64
// CopyIndices 从源知识库复制索引到目标知识库,免去重新计算嵌入向量的开销
// sourceKnowledgeBaseID: 源知识库ID
// sourceToTargetChunkIDMap: 源分块ID到目标分块ID的映射关系,key为源分块ID,value为目标分块ID
// targetKnowledgeBaseID: 目标知识库ID
CopyIndices(
ctx context.Context,
sourceKnowledgeBaseID string,
sourceToTargetKBIDMap map[string]string,
sourceToTargetChunkIDMap map[string]string,
targetKnowledgeBaseID string,
dimension int,
knowledgeType string,
) error
// DeleteByChunkIDList deletes the index info by chunk id list
DeleteByChunkIDList(ctx context.Context, indexIDList []string, dimension int, knowledgeType string) error
// DeleteBySourceIDList deletes the index info by source id list
DeleteBySourceIDList(ctx context.Context, sourceIDList []string, dimension int, knowledgeType string) error
// DeleteByKnowledgeIDList deletes the index info by knowledge id list
DeleteByKnowledgeIDList(ctx context.Context, knowledgeIDList []string, dimension int, knowledgeType string) error
// BatchUpdateChunkEnabledStatus updates the enabled status of chunks in batch
// chunkStatusMap: map of chunk ID to enabled status (true = enabled, false = disabled)
BatchUpdateChunkEnabledStatus(ctx context.Context, chunkStatusMap map[string]bool) error
// BatchUpdateChunkTagID updates the tag ID of chunks in batch
// chunkTagMap: map of chunk ID to tag ID (empty string means no tag)
BatchUpdateChunkTagID(ctx context.Context, chunkTagMap map[string]string) error
// RetrieveEngine retrieves the engine
RetrieveEngine
}
@@ -0,0 +1,17 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// RetrieveGraphRepository is a repository for retrieving graphs
type RetrieveGraphRepository interface {
// AddGraph adds a graph to the repository
AddGraph(ctx context.Context, namespace types.NameSpace, graphs []*types.GraphData) error
// DelGraph deletes a graph from the repository
DelGraph(ctx context.Context, namespace []types.NameSpace) error
// SearchNode searches for nodes in the repository
SearchNode(ctx context.Context, namespace types.NameSpace, nodes []string) (*types.GraphData, error)
}
+109
View File
@@ -0,0 +1,109 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/event"
"github.com/Tencent/WeKnora/internal/types"
)
// SessionService defines the session service interface
type SessionService interface {
// CreateSession creates a session
CreateSession(ctx context.Context, session *types.Session) (*types.Session, error)
// GetSession gets a session, honoring the caller's per-user scope with an
// Admin+ read fallback for tenant channel sessions (API / IM / embed).
// Use only for read paths.
GetSession(ctx context.Context, id string) (*types.Session, error)
// GetOwnedSession gets a session strictly within the caller's owner scope
// (no Admin+ API-key fallback). Write/mutation endpoints must use this so a
// tenant admin cannot alter API-key sessions they can only read.
GetOwnedSession(ctx context.Context, id string) (*types.Session, error)
// GetSessionByID loads a session by tenant and id without user scoping.
GetSessionByID(ctx context.Context, tenantID uint64, id string) (*types.Session, error)
// SetSessionOwnerID assigns sessions.user_id for the given session row.
SetSessionOwnerID(ctx context.Context, tenantID uint64, sessionID, ownerID string) error
// GetSessionsByTenant gets all sessions of a tenant
GetSessionsByTenant(ctx context.Context) ([]*types.Session, error)
// GetPagedSessionsByTenant gets paged sessions of a tenant
GetPagedSessionsByTenant(ctx context.Context, page *types.Pagination) (*types.PageResult, error)
// UpdateSession updates a session
UpdateSession(ctx context.Context, session *types.Session) error
// UpdateSessionLastRequestState records the input-bar state used for the
// most recent QA request on this session. Best-effort: callers should log
// but not surface failures to the user.
UpdateSessionLastRequestState(ctx context.Context, sessionID string, state *types.SessionLastRequestState) error
// DeleteSession deletes a session
DeleteSession(ctx context.Context, id string) error
// BatchDeleteSessions deletes multiple sessions by IDs
BatchDeleteSessions(ctx context.Context, ids []string) error
// DeleteAllSessions deletes all sessions for the current tenant
DeleteAllSessions(ctx context.Context) error
// ListSessions returns a page of sessions for the current tenant/user with
// search/source filters and pin-aware ordering. User scope is pulled from ctx.
ListSessions(ctx context.Context, query *types.SessionListQuery) (*types.PageResult, error)
// CountSessionsBySource returns the total for a source filter without the
// Admin+ gate applied by ListSessions (for aggregate stats endpoints).
CountSessionsBySource(ctx context.Context, query *types.SessionListQuery) (int64, error)
// SetSessionPinned pins or unpins the session for the current user scope.
// Returns the number of rows affected; 0 signals "not found" to the handler.
SetSessionPinned(ctx context.Context, sessionID string, pinned bool) (int64, error)
// GenerateTitle generates a title for the current conversation
// modelID: optional model ID to use for title generation (if empty, uses first available KnowledgeQA model)
GenerateTitle(ctx context.Context, session *types.Session, messages []types.Message, modelID string) (string, error)
// GenerateTitleAsync generates a title for the session asynchronously
// It emits an event when the title is generated
// modelID: optional model ID to use for title generation (if empty, uses first available KnowledgeQA model)
GenerateTitleAsync(ctx context.Context, session *types.Session, userQuery string, modelID string, eventBus *event.EventBus)
// KnowledgeQA performs knowledge-based question answering.
// Events are emitted through eventBus (references, answer chunks, completion).
KnowledgeQA(ctx context.Context, req *types.QARequest, eventBus *event.EventBus) error
// KnowledgeQAByEvent performs knowledge-based question answering by event
KnowledgeQAByEvent(ctx context.Context, chatManage *types.ChatManage, eventList []types.EventType) error
// SearchKnowledge performs knowledge-based search, without summarization
// knowledgeBaseIDs: list of knowledge base IDs to search (supports multi-KB)
// knowledgeIDs: list of specific knowledge (file) IDs to search
SearchKnowledge(ctx context.Context, knowledgeBaseIDs []string, knowledgeIDs []string, tagScopes []types.TagScope, query string) ([]*types.SearchResult, error)
// AgentQA performs agent-based question answering with conversation history and streaming support.
AgentQA(ctx context.Context, req *types.QARequest, eventBus *event.EventBus) error
}
// SessionRepository defines the session repository interface
type SessionRepository interface {
// Create creates a session
Create(ctx context.Context, session *types.Session) (*types.Session, error)
// Get gets a session visible to the tenant/user scope.
Get(ctx context.Context, tenantID uint64, userID string, id string) (*types.Session, error)
// GetByID loads a session by tenant and id without user scoping. Callers
// must enforce access (e.g. embed channel + session signature).
GetByID(ctx context.Context, tenantID uint64, id string) (*types.Session, error)
// GetIMPlatform returns the IM platform bound to a session via
// im_channel_sessions, or "" when the session has no IM mapping. Used to
// classify a session's origin folder on read without a full list query.
GetIMPlatform(ctx context.Context, tenantID uint64, sessionID string) (string, error)
// GetByTenantID gets all sessions visible to the tenant/user scope.
GetByTenantID(ctx context.Context, tenantID uint64, userID string) ([]*types.Session, error)
// GetPagedByTenantID gets paged sessions visible to the tenant/user scope.
GetPagedByTenantID(ctx context.Context, tenantID uint64, userID string, page *types.Pagination) ([]*types.Session, int64, error)
// QueryPaged lists sessions with filters, user-scoped ownership and pin-aware ordering.
QueryPaged(ctx context.Context, q *types.SessionListQuery) ([]*types.SessionListItem, int64, error)
// Update updates a session visible to the tenant/user scope.
Update(ctx context.Context, session *types.Session, userID string) (int64, error)
// SetOwnerID assigns sessions.user_id for a tenant-scoped row.
SetOwnerID(ctx context.Context, tenantID uint64, id, ownerID string) (int64, error)
// UpdateLastRequestState persists the most recent input-bar state for a
// session (agent, model, KB scope, etc.) so the chat UI can restore it
// when the session is reopened. Scope rules match Update.
UpdateLastRequestState(ctx context.Context, tenantID uint64, userID string, sessionID string, state *types.SessionLastRequestState) (int64, error)
// SetPinned pins or unpins a session row scoped by tenant.
// userID, when non-empty, is enforced so users cannot pin sessions they don't own.
// Returns the number of rows affected; 0 means the session doesn't exist or is
// not visible to this caller.
SetPinned(ctx context.Context, tenantID uint64, userID string, id string, pinned bool) (int64, error)
// Delete deletes a session visible to the tenant/user scope.
Delete(ctx context.Context, tenantID uint64, userID string, id string) (int64, error)
// BatchDelete deletes multiple sessions visible to the tenant/user scope.
BatchDelete(ctx context.Context, tenantID uint64, userID string, ids []string) (int64, error)
// DeleteAllByTenantID deletes all sessions visible to the tenant/user scope.
DeleteAllByTenantID(ctx context.Context, tenantID uint64, userID string) (int64, error)
}
+16
View File
@@ -0,0 +1,16 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/agent/skills"
)
// SkillService defines the interface for skill business logic
type SkillService interface {
// ListPreloadedSkills returns metadata for all preloaded skills
ListPreloadedSkills(ctx context.Context) ([]*skills.SkillMetadata, error)
// GetSkillByName retrieves a skill by its name
GetSkillByName(ctx context.Context, name string) (*skills.Skill, error)
}
@@ -0,0 +1,31 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
type StorageBackendRepository interface {
Create(ctx context.Context, backend *types.StorageBackend) error
GetByID(ctx context.Context, tenantID uint64, id string) (*types.StorageBackend, error)
List(ctx context.Context, tenantID uint64) ([]*types.StorageBackend, error)
Update(ctx context.Context, backend *types.StorageBackend) error
Delete(ctx context.Context, tenantID uint64, id string) error
FindLegacyAlias(ctx context.Context, tenantID uint64, provider string) (*types.StorageBackend, error)
}
type StorageBackendService interface {
Create(ctx context.Context, backend *types.StorageBackend) error
Update(ctx context.Context, backend *types.StorageBackend) error
Delete(ctx context.Context, tenantID uint64, id string) error
SetDefault(ctx context.Context, tenantID uint64, id string) error
Test(ctx context.Context, backend *types.StorageBackend) error
}
// StorageBackendResolver is the single runtime entry point for resolving one
// concrete storage instance. backendID wins; provider is a legacy fallback.
type StorageBackendResolver interface {
ResolveFileService(ctx context.Context, tenant *types.Tenant, backendID, provider, localBaseDir string) (FileService, string, error)
ResolveBackend(ctx context.Context, tenant *types.Tenant, backendID, provider string) (*types.StorageBackend, error)
}
@@ -0,0 +1,33 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// StreamEvent represents a single event in the stream
type StreamEvent struct {
ID string `json:"id"` // Unique event ID
Type types.ResponseType `json:"type"` // Event type (thinking, tool_call, complete, etc.)
Content string `json:"content"` // Event content (chunk for streaming events)
Done bool `json:"done"` // Whether this event is done
Timestamp time.Time `json:"timestamp"` // When this event occurred
Data map[string]interface{} `json:"data,omitempty"` // Additional event data (references, metadata, etc.)
Usage *types.TokenUsage `json:"usage,omitempty"` // LLM token usage aggregated over the turn (complete events)
}
// StreamManager stream manager interface - minimal append-only design
// All stream state is managed through events: metadata, references, completion, etc.
type StreamManager interface {
// AppendEvent appends a single event to the stream
// Uses Redis RPush for O(1) append performance
// All event types (thinking, tool_call, references, complete) use this method
AppendEvent(ctx context.Context, sessionID, messageID string, event StreamEvent) error
// GetEvents gets events starting from offset
// Uses Redis LRange for incremental reads
// Returns: events slice, next offset for subsequent reads, error
GetEvents(ctx context.Context, sessionID, messageID string, fromOffset int) ([]StreamEvent, int, error)
}
@@ -0,0 +1,94 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// SystemSettingRepository is the storage layer for the platform-wide
// system_settings table. All methods are system-scoped — there is no
// tenant_id; rows are global to the deployment.
type SystemSettingRepository interface {
// Get fetches a row by key. Returns (nil, nil) when the key is not
// present — callers fall back to ENV / default at the service layer.
Get(ctx context.Context, key string) (*types.SystemSetting, error)
// List returns every row. Used by the management UI to render the
// settings page; no pagination yet (the registry is small — single
// digits in P1, expected to stay double-digits long-term).
List(ctx context.Context) ([]*types.SystemSetting, error)
// Upsert writes a row keyed by Key. Insert if missing, update if
// present. Used by SystemSettingService.Update on every save.
Upsert(ctx context.Context, s *types.SystemSetting) error
// Delete removes the row by key. Returns (true, nil) if a row was
// deleted, (false, nil) if no row matched (idempotent — service
// layer treats this as a no-op for audit purposes). Real DB errors
// surface as (_, err).
Delete(ctx context.Context, key string) (bool, error)
}
// SystemSettingService exposes both the 3-tier resolver (used by
// production code paths that consume settings) and the management CRUD
// (used by the SystemAdmin UI).
//
// 3-tier resolver priority: DB > ENV > built-in default. The service
// owns the registry of legal keys; reading or writing an unknown key
// returns an error from Update / falls through to default for Get.
//
// P1 ships with no in-memory cache: every GetXxx hits the DB. The
// callers (file-upload size check, etc.) are not on a hot path so the
// extra ~1ms is negligible. P2 may add Redis pubsub + a TTL cache;
// the SubscribeRedis hook below is a placeholder for that.
type SystemSettingService interface {
// GetInt returns the resolved int64 value for `key`.
//
// envName is the legacy environment-variable name to consult when
// the DB row is absent ("" means the key has no ENV fallback).
// def is the built-in default used when both DB and ENV miss.
//
// Errors at the DB layer degrade gracefully: the function logs a
// warning and falls through to ENV / default rather than returning
// an error to upstream business code (which would have to bubble
// it through every caller — we'd rather mis-serve a request with
// the default than 500 a file upload).
GetInt(ctx context.Context, key string, envName string, def int64) int64
GetString(ctx context.Context, key string, envName string, def string) string
GetBool(ctx context.Context, key string, envName string, def bool) bool
// GetStringList resolves a comma-separated list of strings. envName
// is treated as a comma-separated string at the ENV level (mirrors
// the legacy SSRF_WHITELIST format). The slice returned is always
// non-nil so callers can iterate without a nil check.
GetStringList(ctx context.Context, key string, envName string, def []string) []string
// List, Get, Update are the management-CRUD surface called by the
// SystemAdmin handlers (gated to user.is_system_admin = true at the
// router layer). Update emits an audit log on success.
List(ctx context.Context) ([]*types.SystemSetting, error)
Get(ctx context.Context, key string) (*types.SystemSetting, error)
// Update writes a new value for `key`, validating that:
// 1. key is in the in-code registry (rejects 400 otherwise — UI
// cannot inject arbitrary keys),
// 2. rawValue's Go type matches the registry's expected type
// (e.g. int64 / float64 for "int", string for "string", bool
// for "bool"),
// 3. the actor is captured from ctx (UserIDFromContext) and
// written to last_modified_by + the audit log.
// Returns the persisted row on success.
Update(ctx context.Context, key string, rawValue any) (*types.SystemSetting, error)
// Reset removes the DB override for `key` so the resolver falls
// back to ENV / built-in default. Idempotent: deleting a key that
// was never persisted returns nil. Emits an audit row on actual
// deletions, invalidates the local cache, and publishes to peers.
// Returns an error only when the DB write itself fails; callers
// must NOT translate "no row" into a 404 — that's the success
// path for an idempotent reset.
Reset(ctx context.Context, key string) error
// SubscribeRedis is a P2 hook: when implemented, it will subscribe
// to a "weknora:system_settings:changed" channel and invalidate any
// in-memory cache on receipt. P1 implementations may return a no-op
// because there is no cache yet. Keeping the method in the interface
// now means P2 can drop in a real implementation without changing
// the container wiring.
SubscribeRedis(ctx context.Context) error
}
+65
View File
@@ -0,0 +1,65 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
)
// KnowledgeTagService defines operations on knowledge base scoped tags.
type KnowledgeTagService interface {
// ListTags lists all tags under a knowledge base with associated statistics.
ListTags(ctx context.Context, kbID string, page *types.Pagination, keyword string) (*types.PageResult, error)
// CreateTag creates a new tag under a knowledge base.
CreateTag(ctx context.Context, kbID string, name string, color string, sortOrder int) (*types.KnowledgeTag, error)
// UpdateTag updates tag basic information.
UpdateTag(ctx context.Context, id string, name *string, color *string, sortOrder *int) (*types.KnowledgeTag, error)
// DeleteTag deletes a tag.
// When contentOnly=true, only deletes the content under the tag but keeps the tag itself.
// excludeIDs: IDs of chunks to exclude from deletion (only valid when deleting chunks)
DeleteTag(ctx context.Context, id string, force bool, contentOnly bool, excludeIDs []string) error
// FindOrCreateTagByName finds a tag by name or creates it if not exists.
FindOrCreateTagByName(ctx context.Context, kbID string, name string) (*types.KnowledgeTag, error)
// ProcessIndexDelete handles async index deletion task
ProcessIndexDelete(ctx context.Context, t *asynq.Task) error
}
// KnowledgeTagRepository defines persistence operations for tags.
type KnowledgeTagRepository interface {
Create(ctx context.Context, tag *types.KnowledgeTag) error
Update(ctx context.Context, tag *types.KnowledgeTag) error
GetByID(ctx context.Context, tenantID uint64, id string) (*types.KnowledgeTag, error)
// GetBySeqID retrieves a tag by its seq_id.
GetBySeqID(ctx context.Context, tenantID uint64, seqID int64) (*types.KnowledgeTag, error)
// GetByIDs retrieves multiple tags by their IDs in a single query.
GetByIDs(ctx context.Context, tenantID uint64, ids []string) ([]*types.KnowledgeTag, error)
// GetBySeqIDs retrieves multiple tags by their seq_ids in a single query.
GetBySeqIDs(ctx context.Context, tenantID uint64, seqIDs []int64) ([]*types.KnowledgeTag, error)
GetByName(ctx context.Context, tenantID uint64, kbID string, name string) (*types.KnowledgeTag, error)
ListByKB(
ctx context.Context,
tenantID uint64,
kbID string,
page *types.Pagination,
keyword string,
) ([]*types.KnowledgeTag, int64, error)
Delete(ctx context.Context, tenantID uint64, id string) error
// CountReferences returns number of knowledges and chunks that reference the tag.
CountReferences(
ctx context.Context,
tenantID uint64,
kbID string,
tagID string,
) (knowledgeCount int64, chunkCount int64, err error)
// BatchCountReferences returns number of knowledges and chunks for multiple tags in a single query.
// Returns a map of tagID -> {knowledgeCount, chunkCount}
BatchCountReferences(
ctx context.Context,
tenantID uint64,
kbID string,
tagIDs []string,
) (map[string]types.TagReferenceCounts, error)
// DeleteUnusedTags deletes tags that are not referenced by any knowledge or chunk.
DeleteUnusedTags(ctx context.Context, tenantID uint64, kbID string) (int64, error)
}
@@ -0,0 +1,9 @@
package interfaces
import "github.com/hibiken/asynq"
// TaskEnqueuer abstracts task enqueueing. *asynq.Client satisfies this interface.
// For Lite mode (no Redis), a synchronous implementation dispatches tasks inline.
type TaskEnqueuer interface {
Enqueue(task *asynq.Task, opts ...asynq.Option) (*asynq.TaskInfo, error)
}
+13
View File
@@ -0,0 +1,13 @@
package interfaces
import (
"context"
"github.com/hibiken/asynq"
)
// TaskHandler is a interface for handling asynchronous tasks
type TaskHandler interface {
// Handle handles the task
Handle(ctx context.Context, t *asynq.Task) error
}
+112
View File
@@ -0,0 +1,112 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// TaskInspector abstracts queue inspection / cancellation against the
// task backend. It is best-effort: implementations may scan a finite
// number of tasks per call and return whatever count they could
// affect. Lite mode (no Redis) ships a no-op implementation because
// SyncTaskExecutor dispatches inline goroutines that cannot be
// dequeued before they start.
//
// Use cases today: user-initiated cancel of an in-progress knowledge
// parse, which must remove downstream multimodal / post-process /
// question / summary tasks already enqueued against the same
// knowledge_id, plus signal active workers to stop at their next
// checkpoint.
type TaskInspector interface {
// CancelTasksForKnowledge removes pending/scheduled/retry tasks
// whose payload references the given knowledge ID, and signals
// active workers running such tasks to stop. Returns rough
// counts of (deletedFromQueue, activeCancelled) for observability.
// Errors are returned but callers should treat the operation as
// best-effort: the row-level abort flag remains the source of
// truth, this just prevents wasted work.
CancelTasksForKnowledge(ctx context.Context, knowledgeID string) (deleted int, cancelled int, err error)
// HasQueuedTasksForKnowledge reports whether any pending / scheduled
// / retry / active task referencing the given knowledge ID still
// lives in the queue backend. It is the read-only counterpart of
// CancelTasksForKnowledge: the housekeeping sweep calls it before
// flipping a long-idle "processing"/"finalizing" row to "failed" so
// it can tell a genuinely orphaned row (no task anywhere) from one
// whose enrichment subtasks are merely backlogged behind a busy
// queue (no span heartbeat yet because no worker has picked them up).
//
// Best-effort and short-circuiting: it returns true as soon as the
// first match is seen. On backend error it returns (false, err);
// callers decide the fail-safe direction. Lite mode (no Redis)
// always returns false — inline executors never queue, so the
// span/updated_at checks remain authoritative there.
HasQueuedTasksForKnowledge(ctx context.Context, knowledgeID string) (bool, error)
// QueueStats returns a read-only depth snapshot for every queue this
// application enqueues into, for the System Admin runtime dashboard.
//
// The `supported` bool reports whether queue inspection is available
// on the current backend: true in asynq/Redis mode, false in Lite
// mode (no Redis) where tasks run inline and no queue exists. Callers
// use it to render an "unavailable in this deployment" state rather
// than an empty table.
//
// Best-effort: a per-queue backend error is logged and that queue is
// surfaced as a zeroed row (so the full lane set stays visible) rather
// than failing the whole call.
QueueStats(ctx context.Context) (stats []types.QueueStat, supported bool, err error)
// WorkerServerStats returns live asynq server heartbeats across all
// replicas. The runtime dashboard uses them to aggregate actual cluster
// capacity and busy workers for each configured pool.
WorkerServerStats(ctx context.Context) (stats []types.WorkerServerStat, supported bool, err error)
}
// KnowledgeBaseTaskCanceller is the optional queue-cleanup capability used
// when a knowledge base is deleted. It is separate from TaskInspector so
// lightweight test doubles and queue backends that cannot inspect tasks do
// not need to implement knowledge-base-wide scanning.
type KnowledgeBaseTaskCanceller interface {
// CancelTasksForKnowledgeBase removes pending/scheduled/retry tasks
// associated with kbID and signals matching active workers to stop.
// knowledgeIDs covers batch tasks whose payload references documents but
// does not carry the parent knowledge-base ID. dataSourceIDs covers sync
// tasks whose payload only identifies a data source.
CancelTasksForKnowledgeBase(
ctx context.Context,
kbID string,
knowledgeIDs []string,
dataSourceIDs []string,
) (deleted int, cancelled int, err error)
}
// RuntimeTaskInspector is the optional operator surface implemented by queue
// backends that retain inspectable task state. It is separate from
// TaskInspector so Lite mode and light-weight tests do not need to implement
// queue mutations.
type RuntimeTaskInspector interface {
ListRuntimeTasks(
ctx context.Context,
queue string,
state types.RuntimeTaskState,
cursor string,
pageSize int,
) (page types.RuntimeTaskPage, supported bool, err error)
GetRuntimeTask(ctx context.Context, queue, taskID string) (task *types.RuntimeTaskInfo, supported bool, err error)
RunRuntimeTask(ctx context.Context, queue, taskID string) (supported bool, err error)
DeleteRuntimeTask(ctx context.Context, queue, taskID string) (supported bool, err error)
// ForceDeleteRuntimeTask removes a queue record without checking the
// operator-facing AllowedActions. Used when the business row is already
// gone but a retry/pending task survived (orphan cleanup).
ForceDeleteRuntimeTask(ctx context.Context, queue, taskID string) (supported bool, err error)
// PurgeArchivedRuntimeTasks removes every archived (finally-failed) task
// from one queue in a single call and returns how many records were
// deleted. It only ever touches the archived (dead-letter) set, so live
// pending/active/scheduled/retry tasks are never affected. Business state
// is intentionally left untouched: archived tasks have already had their
// document status flipped to "failed" on their last retry, mirroring the
// semantics of deleting archived records one by one.
PurgeArchivedRuntimeTasks(ctx context.Context, queue string) (deleted int, supported bool, err error)
}
+143
View File
@@ -0,0 +1,143 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// TaskPendingOpsRepository persists rows for the generic task pending
// queue (`task_pending_ops`). The queue is the durable replacement for
// the Redis-list-backed wiki:pending:<kbID> queue. It is intentionally
// stateless about consumer semantics: the (TaskType, Scope, ScopeID)
// tuple is the only routing primitive the repository understands;
// deduplication, batching, and retry policy live in the consumer.
//
// Concurrency model: two consumption primitives coexist.
// - PeekBatch does NOT take row locks; consumers enforce per-(scope_id)
// serialization out-of-band (e.g. an external Redis lock). This is the
// original primitive and is still used by single-process (Lite) mode.
// - ClaimBatch DOES take row locks (SELECT ... FOR UPDATE SKIP LOCKED on
// Postgres) and marks rows with claimed_at, so multiple concurrent
// consumers of the same tuple pull DISJOINT rows without an external
// lock. This is what lets the wiki pipeline drop its exclusive per-KB
// batch lock and spread one KB's backlog across the whole worker pool.
type TaskPendingOpsRepository interface {
// Enqueue inserts a single op. The caller fills in TenantID, TaskType,
// Scope, ScopeID, Op, DedupKey, Payload; ID, FailCount, EnqueuedAt
// are server-side defaults.
Enqueue(ctx context.Context, op *types.TaskPendingOp) error
// PeekBatch returns up to `limit` rows for the given queue tuple,
// ordered by id ASC (FIFO within the queue). Rows are NOT removed —
// callers must DeleteByIDs once the ops have been processed (or
// IncrFailCount and leave them for the next pass).
PeekBatch(ctx context.Context, taskType, scope, scopeID string, limit int) ([]*types.TaskPendingOp, error)
// ClaimBatch atomically claims eligible rows for the tuple, grouped by
// dedup_key, and returns them with claimed_at set to now. `limit`
// counts DISTINCT dedup_keys (documents), NOT rows: ALL eligible rows
// sharing a chosen dedup_key are claimed together so a document with
// several queued ops is never split across two concurrent batches.
// A row is eligible when it is unclaimed (claimed_at IS NULL) or its
// claim is stale (claimed_at < staleBefore) — the latter recovers rows
// abandoned by a crashed worker. On Postgres the per-key anchor row is
// locked with FOR UPDATE SKIP LOCKED so concurrent claimers take
// disjoint key sets without blocking or double-claiming.
//
// Claimed rows are NOT removed: the consumer must DeleteByIDs on
// success, or ReleaseByIDs to hand a still-retryable row back to the
// pool (otherwise it stays claimed until staleBefore elapses).
ClaimBatch(ctx context.Context, taskType, scope, scopeID string, limit int, staleBefore time.Time) ([]*types.TaskPendingOp, error)
// ReleaseByIDs clears claimed_at (back to NULL) for the given rows so
// a claimed-but-not-consumed row becomes immediately eligible for the
// next ClaimBatch. No-op for empty input. Used by the wiki retry path
// to re-queue a transiently-failed op without waiting for its claim to
// go stale. Harmless on rows that were never claimed (Lite mode).
ReleaseByIDs(ctx context.Context, ids []int64) error
// DeleteByIDs removes the given rows. No-op for empty input. Used
// to consume a successfully-processed batch, and to drop ops that
// have been moved to task_dead_letters.
DeleteByIDs(ctx context.Context, ids []int64) error
// IncrFailCount increments fail_count for one row and returns the
// new value. Returns (0, nil) if the row does not exist (race with
// DeleteByIDs is benign).
IncrFailCount(ctx context.Context, id int64) (int, error)
// PendingCount returns the number of rows currently queued for the
// given tuple. Cheap (covered by idx_task_pending_ops_scope) and
// used by the wiki ingest follow-up scheduler.
PendingCount(ctx context.Context, taskType, scope, scopeID string) (int64, error)
// DeleteByDedupKey removes rows for the tuple whose DedupKey
// matches. If `op` is non-empty, only rows with that exact op are
// removed (this lets wiki ingest scrub queued "ingest" ops while
// preserving "retract" ops for the same knowledge — retract is
// still needed to clean up wiki pages after the source doc is
// deleted). If `op` is empty, every matching row is removed
// regardless of op.
DeleteByDedupKey(ctx context.Context, taskType, scope, scopeID, dedupKey, op string) error
}
// TaskPendingOpsScopeCleaner is an optional extension for callers that need
// to discard every durable pending operation owned by a deleted scope. It is
// intentionally separate from TaskPendingOpsRepository so alternate queue
// implementations and existing test doubles are not forced to implement a
// lifecycle-only operation.
type TaskPendingOpsScopeCleaner interface {
DeleteByScope(ctx context.Context, scope, scopeID string) error
}
// TaskPendingOpsKnowledgeBaseGuard atomically persists a KB-scoped operation
// only while its knowledge base is still active. Implementations must
// serialize the active-KB check with soft deletion so a detached worker cannot
// recreate durable work after the deletion scrub has finished.
type TaskPendingOpsKnowledgeBaseGuard interface {
EnqueueIfKnowledgeBaseActive(ctx context.Context, op *types.TaskPendingOp) (accepted bool, err error)
}
// TaskPendingOpsFinalizingSeeder atomically hands a processing knowledge row
// to the asynchronous finalizing pipeline while persisting the durable
// pending operation that owns one of its subtask slots.
type TaskPendingOpsFinalizingSeeder interface {
SeedKnowledgeFinalizingWithPendingOp(
ctx context.Context,
knowledgeID string,
expectedSubtasks int,
op *types.TaskPendingOp,
) (promoted bool, err error)
}
// TaskDeadLetterRepository persists rows for the generic task dead-letter
// archive (`task_dead_letters`). Two writers exist: the asynq
// dead-letter middleware (one row per archived asynq task), and the
// service-level retry handlers (one row per in-batch op that exhausted
// its consumer-defined retry budget — wiki ingest is the current case).
//
// Reads are operator-driven: list by scope to triage a single KB,
// list by task_type for cross-KB symptom hunting. No TTL.
type TaskDeadLetterRepository interface {
// Insert records one dead letter. Best-effort caller: the asynq
// middleware ignores the error so a failed insert never masks the
// underlying task error.
Insert(ctx context.Context, dl *types.TaskDeadLetter) error
// ListByScope returns dead letters for the given scope tuple,
// newest-first, paginated by failed-id cursor. `cursor` is the
// stringified id of the oldest entry from the previous page; "" =
// from the newest. Empty nextCursor = end of stream. `limit` is
// clamped to [1, 200].
ListByScope(ctx context.Context, scope, scopeID, cursor string, limit int) ([]*types.TaskDeadLetter, string, error)
// ListByTaskType returns dead letters for the given task_type,
// newest-first, with the same cursor semantics as ListByScope.
ListByTaskType(ctx context.Context, taskType, cursor string, limit int) ([]*types.TaskDeadLetter, string, error)
// DeleteByID drops a single dead letter (e.g. after operators have
// requeued the task manually).
DeleteByID(ctx context.Context, id int64) error
}
@@ -0,0 +1,33 @@
package interfaces
import (
"context"
"io"
"time"
"github.com/Tencent/WeKnora/internal/types"
"github.com/hibiken/asynq"
)
type TemporaryDocumentRepository interface {
Create(ctx context.Context, document *types.TemporaryDocument) error
GetByID(ctx context.Context, tenantID uint64, documentID string) (*types.TemporaryDocument, error)
GetScoped(ctx context.Context, tenantID uint64, sessionID, documentID string) (*types.TemporaryDocument, error)
ListScoped(ctx context.Context, tenantID uint64, sessionID string) ([]*types.TemporaryDocument, error)
MarkProcessing(ctx context.Context, tenantID uint64, documentID string, startedAt time.Time) error
MarkReady(ctx context.Context, tenantID uint64, documentID, content string, chunks, imageRefs, metadata types.JSON, tokenCount, chunkCount int, readyAt time.Time) error
MarkFailed(ctx context.Context, tenantID uint64, documentID, message string) error
DeleteScoped(ctx context.Context, tenantID uint64, sessionID, documentID string) error
ListExpired(ctx context.Context, before time.Time, limit int) ([]*types.TemporaryDocument, error)
}
type TemporaryDocumentService interface {
Create(ctx context.Context, tenantID uint64, sessionID, fileName, mimeType string, fileSize int64, reader io.Reader, options types.TemporaryDocumentCreateOptions) (*types.TemporaryDocument, error)
Get(ctx context.Context, tenantID uint64, sessionID, documentID string) (*types.TemporaryDocument, error)
OpenFile(ctx context.Context, tenantID uint64, sessionID, documentID string) (io.ReadCloser, string, error)
List(ctx context.Context, tenantID uint64, sessionID string) ([]*types.TemporaryDocument, error)
Delete(ctx context.Context, tenantID uint64, sessionID, documentID string) error
ResolveForPrompt(ctx context.Context, tenantID uint64, sessionID string, documentIDs []string, query string) (*types.TemporaryDocumentPromptResult, error)
Process(ctx context.Context, task *asynq.Task) error
CleanupExpired(ctx context.Context) error
}
+123
View File
@@ -0,0 +1,123 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// TenantService defines the tenant service interface
type TenantService interface {
// CreateTenant creates a tenant
CreateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error)
// GetTenantByID gets a tenant by ID
GetTenantByID(ctx context.Context, id uint64) (*types.Tenant, error)
// GetTenantsByIDs batches GetTenantByID for multiple IDs in a single
// query. Returns a map keyed by tenant ID for O(1) lookup at the
// call site; missing tenants are simply absent from the map.
GetTenantsByIDs(ctx context.Context, ids []uint64) (map[uint64]*types.Tenant, error)
// ListTenants lists all tenants
ListTenants(ctx context.Context) ([]*types.Tenant, error)
// UpdateTenant updates a tenant
UpdateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error)
// DeleteTenant deletes a tenant
DeleteTenant(ctx context.Context, id uint64) error
// ListAllTenants lists all tenants (for users with cross-tenant access permission)
ListAllTenants(ctx context.Context) ([]*types.Tenant, error)
// BulkSetStorageQuota overwrites every tenant's storage_quota with
// quotaBytes. Returns how many rows were affected. Used by the
// SystemAdmin "apply default to all tenants" action; bypasses the
// per-tenant whitelist on PUT /tenants/:id (which intentionally
// forbids storage_quota edits for Owners). quotaBytes must be > 0;
// callers are responsible for resolving GB→bytes.
BulkSetStorageQuota(ctx context.Context, quotaBytes int64) (int64, error)
// SearchTenants searches tenants with pagination and filters
SearchTenants(ctx context.Context, keyword string, tenantID uint64, page, pageSize int) ([]*types.Tenant, int64, error)
// GetTenantByIDForUser gets a tenant by ID with permission check
GetTenantByIDForUser(ctx context.Context, tenantID uint64, userID string) (*types.Tenant, error)
// GetWeKnoraCloudCredentials returns the decrypted WeKnoraCloud credentials for the current tenant.
GetWeKnoraCloudCredentials(ctx context.Context) *types.WeKnoraCloudCredentials
}
// TenantRepository defines the tenant repository interface
type TenantRepository interface {
// CreateTenant creates a tenant
CreateTenant(ctx context.Context, tenant *types.Tenant) error
// GetTenantByID gets a tenant by ID
GetTenantByID(ctx context.Context, id uint64) (*types.Tenant, error)
// GetTenantsByIDs batches GetTenantByID; see TenantService.GetTenantsByIDs.
GetTenantsByIDs(ctx context.Context, ids []uint64) (map[uint64]*types.Tenant, error)
// ListTenants lists all tenants
ListTenants(ctx context.Context) ([]*types.Tenant, error)
// SearchTenants searches tenants with pagination and filters
SearchTenants(ctx context.Context, keyword string, tenantID uint64, page, pageSize int) ([]*types.Tenant, int64, error)
// UpdateTenant updates a tenant
UpdateTenant(ctx context.Context, tenant *types.Tenant) error
// DeleteTenant deletes a tenant
DeleteTenant(ctx context.Context, id uint64) error
// AdjustStorageUsed adjusts the storage used for a tenant
AdjustStorageUsed(ctx context.Context, tenantID uint64, delta int64) error
// BulkSetStorageQuota — see TenantService.BulkSetStorageQuota.
BulkSetStorageQuota(ctx context.Context, quotaBytes int64) (int64, error)
}
type TenantAPIKeyCreateRequest struct {
TenantID uint64
ScopeType types.APIKeyScopeType
Name string
FullAccess bool
KnowledgeBaseIDs []string
Capabilities []string
ExpiresAt *time.Time
}
type TenantAPIKeyCreateResult struct {
APIKey *types.TenantAPIKey
Token string
}
// TenantAPIKeyUpdateRequest 修改已创建租户 API Key 的可配置属性。
// 配置语义与创建接口一致:FullAccess 为 true 时忽略细粒度能力和知识库范围。
type TenantAPIKeyUpdateRequest struct {
TenantID uint64
APIKeyID uint64
Name string
FullAccess bool
KnowledgeBaseIDs []string
Capabilities []string
ExpiresAt *time.Time
}
type TenantAPIKeyRepository interface {
CreateAPIKey(ctx context.Context, key *types.TenantAPIKey) error
GetAPIKeyByHash(ctx context.Context, hash string) (*types.TenantAPIKey, error)
ListAPIKeys(ctx context.Context, tenantID uint64) ([]*types.TenantAPIKey, error)
ListPlatformAPIKeys(ctx context.Context) ([]*types.TenantAPIKey, error)
UpdateAPIKey(ctx context.Context, tenantID uint64, id uint64, key *types.TenantAPIKey) (*types.TenantAPIKey, error)
RevokeAPIKey(ctx context.Context, tenantID uint64, id uint64) error
RevokePlatformAPIKey(ctx context.Context, id uint64) error
UpdateAPIKeyHash(ctx context.Context, id uint64, hash string) error
UpdateAPIKeyLastUsed(ctx context.Context, id uint64, at time.Time) error
// ListKeysWithPlaceholderHash returns keys whose key_hash is still the
// migration placeholder (never authenticated since the 000065 upgrade),
// so the real SHA-256 hash can be computed and backfilled.
ListKeysWithPlaceholderHash(ctx context.Context) ([]*types.TenantAPIKey, error)
// HasKeysWithPlaceholderHash is a cheap existence probe used at startup
// to skip loading/decrypting api_key rows once backfill is complete.
HasKeysWithPlaceholderHash(ctx context.Context) (bool, error)
}
type TenantAPIKeyService interface {
CreateAPIKey(ctx context.Context, req TenantAPIKeyCreateRequest) (*TenantAPIKeyCreateResult, error)
AuthenticateAPIKey(ctx context.Context, token string) (*types.TenantAPIKey, error)
ListAPIKeys(ctx context.Context, tenantID uint64) ([]*types.TenantAPIKey, error)
ListPlatformAPIKeys(ctx context.Context) ([]*types.TenantAPIKey, error)
UpdateAPIKey(ctx context.Context, req TenantAPIKeyUpdateRequest) (*types.TenantAPIKey, error)
RevokeAPIKey(ctx context.Context, tenantID uint64, id uint64) error
RevokePlatformAPIKey(ctx context.Context, id uint64) error
// BackfillMissingKeyHashes computes and persists the SHA-256 key_hash
// for legacy keys still carrying the migration placeholder.
// Returns the number of keys backfilled.
BackfillMissingKeyHashes(ctx context.Context) (int, error)
}
@@ -0,0 +1,88 @@
package interfaces
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/types"
)
// TenantInvitationRepository persists tenant_invitations rows that
// capture the "pending invite" intent before it becomes a real
// tenant_members row. All read methods skip GORM soft-deleted rows.
type TenantInvitationRepository interface {
// Create inserts a new pending invitation. Returns the conflict
// error sentinel (ErrPendingInvitationExists) if the (tenant_id,
// invitee) partial unique index rejects the insert.
Create(ctx context.Context, inv *types.TenantInvitation) error
// GetByID fetches an invitation by surrogate id, or (nil, nil) if
// missing. Used by Accept/Decline/Revoke handlers.
GetByID(ctx context.Context, id uint64) (*types.TenantInvitation, error)
// GetPendingByPair returns the in-flight pending invitation for
// (tenant, invitee) or (nil, nil) when none exists. Used by the
// service layer to short-circuit duplicate Create calls before
// they hit the unique index.
GetPendingByPair(ctx context.Context, tenantID uint64, inviteeUserID string) (*types.TenantInvitation, error)
// GetActiveByToken looks up the share-link row matching the
// supplied plaintext token. Returns (nil, nil) if no row matches
// or the row is no longer pending. "Active" rather than "pending
// for a single user" because share-link rows are multi-use:
// AcceptByToken does NOT consume them, only Revoke or expiry
// removes them from circulation.
GetActiveByToken(ctx context.Context, token string) (*types.TenantInvitation, error)
// ListByTenant returns invitations for the tenant ordered by
// id DESC (newest first). includeTerminal controls whether
// non-pending rows (accepted/declined/revoked/expired) are
// returned — the management UI shows pending by default, with
// a "history" toggle for the rest.
ListByTenant(ctx context.Context, tenantID uint64, includeTerminal bool) ([]*types.TenantInvitation, error)
// CountByTenantList counts invitation rows matching the same filter as
// ListByTenant (tenant_id + optional pending-only vs include-terminal).
CountByTenantList(ctx context.Context, tenantID uint64, includeTerminal bool) (int64, error)
// ListByTenantPage returns invitations for the tenant with id DESC
// paging, same filtering semantics as ListByTenant.
ListByTenantPage(ctx context.Context, tenantID uint64, includeTerminal bool, offset, limit int) ([]*types.TenantInvitation, error)
// ListByInvitee returns invitations addressed to the user across
// all tenants. Ordered by id DESC. Used both by the inbox page
// (includeTerminal=false) and by /me/invitations/history if/when
// we expose history (includeTerminal=true).
ListByInvitee(ctx context.Context, inviteeUserID string, includeTerminal bool) ([]*types.TenantInvitation, error)
// CountPendingByInvitee returns the number of pending invitations
// for the user. Used by the avatar-row badge so the UI doesn't
// have to fetch the full list just to render a count.
CountPendingByInvitee(ctx context.Context, inviteeUserID string) (int64, error)
// MarkStatusIfPending atomically transitions a row from pending
// to the supplied status. respondedAt is recorded as the
// transition time. Returns gorm.ErrRecordNotFound when the row
// does not exist or is already in a non-pending state, which the
// service layer maps to ErrInvitationNotPending — that error is
// the "stale state machine" signal callers care about.
MarkStatusIfPending(
ctx context.Context,
id uint64,
status types.TenantInvitationStatus,
respondedAt time.Time,
) error
// SweepExpired transitions all pending rows whose expires_at is
// before `now` into the expired state. Returns the affected row
// count so callers can decide whether to emit per-row audit
// events (we don't — the volume could be high and the audit-log
// table already records the original invitation_sent rows).
SweepExpired(ctx context.Context, now time.Time) (int64, error)
// IncrementAcceptedCount atomically bumps accepted_count by 1.
// Used by AcceptByToken so the management UI can show "N 人已通过
// 此链接加入" for share-link rows. Per-user invitations also call
// this on accept; the count just caps at 1 there.
IncrementAcceptedCount(ctx context.Context, id uint64) error
}
@@ -0,0 +1,104 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// TenantInvitationService is the business-logic layer over
// TenantInvitationRepository. It enforces the lifecycle invariants
// described in TenantInvitationStatus, runs the lazy expiry sweep, and
// brokers the accept-time hand-off into TenantMemberService so that
// "accept" produces both the invitation transition AND the
// tenant_members row in a single audit story.
type TenantInvitationService interface {
// Create issues a new pending invitation for (tenantID, inviteeUserID)
// with the given role. Caller passes the inviter user id (may be
// nil for service-internal flows). Returns the freshly created row
// or a sentinel error (ErrPendingInvitationExists / ErrAlreadyMember /
// ErrInvalidTenantRole) for the common conflicts.
Create(
ctx context.Context,
tenantID uint64,
inviteeUserID string,
role types.TenantRole,
invitedBy *string,
message string,
) (*types.TenantInvitation, error)
// Accept transitions the pending row into accepted AND creates the
// active tenant_members row in the same transaction. The callerUserID
// MUST equal inv.InviteeUserID — the service rejects anyone else.
// Returns the freshly created membership for the response shape.
Accept(ctx context.Context, invID uint64, callerUserID string) (*types.TenantMember, error)
// Decline transitions the pending row into declined. Same caller
// rule as Accept: only the invitee themselves.
Decline(ctx context.Context, invID uint64, callerUserID string) error
// Revoke transitions the pending row into revoked. Callable by an
// Owner of the tenant the invitation belongs to (the handler
// applies the Owner gate via g.Owner() route middleware, so this
// method does not re-check role).
Revoke(ctx context.Context, invID uint64) error
// GetByID returns an invitation or (nil, nil). Used by handlers to
// authorise per-row operations (e.g. is the caller the invitee?).
GetByID(ctx context.Context, invID uint64) (*types.TenantInvitation, error)
// ListByTenant returns invitations for a tenant. Always runs the
// lazy expiry sweep before reading so stale-pending rows surface
// as expired in the UI.
ListByTenant(ctx context.Context, tenantID uint64, includeTerminal bool) ([]*types.TenantInvitation, error)
// ListTenantInvitationsPage paginates invitations for the tenant
// management UI after the lazy sweep (same filtering as ListByTenant).
ListTenantInvitationsPage(ctx context.Context, tenantID uint64, includeTerminal bool, page, pageSize int) ([]*types.TenantInvitation, int64, error)
// ListByInvitee returns invitations addressed to the user across
// all tenants. Always runs the lazy sweep first.
ListByInvitee(ctx context.Context, inviteeUserID string, includeTerminal bool) ([]*types.TenantInvitation, error)
// CountPendingByInvitee returns the user's pending invitation count
// after running the lazy sweep, so the inbox badge can poll a
// lightweight endpoint without paginating the full list.
CountPendingByInvitee(ctx context.Context, inviteeUserID string) (int64, error)
// CreateShareLink generates a multi-use share-link invitation for
// the tenant. The plaintext token is persisted on the row and also
// returned for the handler to compose the registration URL — the
// token stays available via list/get for as long as the row is
// pending so Owners can re-share without "copy now or lose it"
// pressure. Multiple share-link rows can coexist on the same
// tenant (different roles, or just multiple campaigns).
CreateShareLink(
ctx context.Context,
tenantID uint64,
role types.TenantRole,
invitedBy *string,
message string,
) (inv *types.TenantInvitation, plainToken string, err error)
// LookupByToken resolves a plaintext token to its active share-link
// row. Returns ErrInvitationTokenInvalid for unknown / expired /
// revoked tokens to avoid leaking which tokens used to exist. The
// row is NOT consumed; the same token can be looked up arbitrarily
// many times until it expires or the Owner revokes it.
LookupByToken(ctx context.Context, plainToken string) (*types.TenantInvitation, error)
// AcceptByToken creates a tenant_members row binding newUserID to
// the share-link's tenant + role. Unlike Accept, this does NOT flip
// the invitation row into a terminal state — share-link rows stay
// pending so subsequent invitees can also register through the
// same link. Idempotent: if the user already has membership, the
// existing row is returned. If the same user also has a pending direct
// invitation for the tenant, it is reconciled to accepted so the inbox
// cannot offer a second acceptance after membership already exists.
AcceptByToken(ctx context.Context, plainToken string, newUserID string) (*types.TenantMember, error)
// MarkPendingAcceptedIfExists transitions a per-user pending invitation
// into accepted when auto-accept joins the invitee without inbox
// confirmation. No-op when no pending row exists.
MarkPendingAcceptedIfExists(ctx context.Context, tenantID uint64, inviteeUserID string) error
}
@@ -0,0 +1,71 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// TenantMemberRepository persists (user, tenant) membership rows that
// carry the per-tenant TenantRole.
//
// All methods operate on active rows only (deleted_at IS NULL) unless the
// docstring explicitly says otherwise. Soft deletion is handled by GORM
// via the DeletedAt field on TenantMember.
type TenantMemberRepository interface {
// Create inserts a new active membership row. Caller is responsible
// for ensuring no other active row exists for the same (user, tenant)
// pair; the partial unique index will return a conflict error otherwise.
Create(ctx context.Context, member *types.TenantMember) error
// Get returns the active membership for the given (user, tenant) pair,
// or (nil, nil) if no such row exists.
Get(ctx context.Context, userID string, tenantID uint64) (*types.TenantMember, error)
// ListByUser returns every active membership owned by the given user,
// ordered by joined_at ascending.
ListByUser(ctx context.Context, userID string) ([]*types.TenantMember, error)
// ListByTenant returns every active membership inside the given tenant,
// ordered by joined_at ascending.
ListByTenant(ctx context.Context, tenantID uint64) ([]*types.TenantMember, error)
// CountFilteredByTenant counts active memberships in tenant. Optional
// search matches user email/username (join users table); empty search
// counts all memberships.
CountFilteredByTenant(ctx context.Context, tenantID uint64, search string) (int64, error)
// ListPagedByTenant returns active memberships sorted joined_at ASC, id ASC.
// search filters by user email/username (join users table); empty
// search lists all memberships in tenant.
ListPagedByTenant(ctx context.Context, tenantID uint64, search string, offset, limit int) ([]*types.TenantMember, error)
// UpdateRole changes the role of an existing active membership. Returns
// gorm.ErrRecordNotFound if no active row matches.
UpdateRole(ctx context.Context, userID string, tenantID uint64, role types.TenantRole) error
// SoftDelete marks the active membership as deleted. The user record
// itself is untouched.
SoftDelete(ctx context.Context, userID string, tenantID uint64) error
// CountActiveOwners reports how many active rows in the tenant carry
// the owner role. Used by service-layer invariant checks ("cannot
// remove the last owner").
CountActiveOwners(ctx context.Context, tenantID uint64) (int64, error)
// HasAnyMembers reports whether the tenant has at least one active
// membership. Used by the auth middleware to decide whether to
// auto-promote the first authenticating human in an API-key-only tenant.
HasAnyMembers(ctx context.Context, tenantID uint64) (bool, error)
// DemoteOwnerAtomically demotes an Owner to a non-Owner role inside
// a transaction that holds an UPDATE lock on the tenant's other
// active Owners, fixing the TOCTOU race where two concurrent
// demotions could leave the tenant ownerless. Returns the
// repo-level ErrLastOwner sentinel when no other Owner exists.
DemoteOwnerAtomically(ctx context.Context, userID string, tenantID uint64, newRole types.TenantRole) error
// RemoveOwnerAtomically soft-deletes an Owner row under the same
// lock as DemoteOwnerAtomically.
RemoveOwnerAtomically(ctx context.Context, userID string, tenantID uint64) error
}
@@ -0,0 +1,54 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// TenantMemberService is the business-logic layer over TenantMemberRepository.
// It enforces tenant-RBAC invariants such as "every tenant with members must
// keep at least one active Owner". HTTP handlers and other services call this
// interface rather than the repository directly so the invariants cannot be
// silently bypassed.
type TenantMemberService interface {
// AddMember inserts a new active membership row. Returns an error if
// (user, tenant) already has an active membership.
AddMember(ctx context.Context, userID string, tenantID uint64, role types.TenantRole, invitedBy *string) (*types.TenantMember, error)
// EnsureOwner is an idempotent helper used by the registration flow:
// if the user already has an active membership in the tenant, return
// it; otherwise create one with role=owner. This is the common path
// for self-service registration where the registrant becomes the
// Owner of the tenant their account just created.
EnsureOwner(ctx context.Context, userID string, tenantID uint64) (*types.TenantMember, error)
// GetMembership returns the active (user, tenant) membership, or
// (nil, nil) if no such row exists.
GetMembership(ctx context.Context, userID string, tenantID uint64) (*types.TenantMember, error)
// ListByUser returns every active membership owned by the user.
ListByUser(ctx context.Context, userID string) ([]*types.TenantMember, error)
// ListByTenant returns every active membership inside the tenant.
ListByTenant(ctx context.Context, tenantID uint64) ([]*types.TenantMember, error)
// ListMembersPage lists members with pagination. Query matches email or
// username case-insensitively; empty query returns full tenant list slice.
ListMembersPage(ctx context.Context, tenantID uint64, query string, page, pageSize int) ([]*types.TenantMember, int64, error)
// HasAnyMembers reports whether the tenant has at least one active
// member. The auth middleware uses this to recover orphan tenants
// (e.g. API-key-only tenants that never had a human member): the
// first human authenticating into such a tenant is auto-promoted
// to Owner.
HasAnyMembers(ctx context.Context, tenantID uint64) (bool, error)
// UpdateRole changes the role of an existing membership while
// enforcing the "cannot demote the last active Owner" invariant.
UpdateRole(ctx context.Context, userID string, tenantID uint64, newRole types.TenantRole) error
// RemoveMember soft-deletes the membership while enforcing the
// "cannot remove the last active Owner" invariant.
RemoveMember(ctx context.Context, userID string, tenantID uint64) error
}
+150
View File
@@ -0,0 +1,150 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// UserService defines the user service interface
type UserService interface {
// Register creates a new user account
Register(ctx context.Context, req *types.RegisterRequest) (*types.User, error)
// Login authenticates a user and returns tokens
Login(ctx context.Context, req *types.LoginRequest) (*types.LoginResponse, error)
// GetOIDCAuthorizationURL builds the third-party OIDC authorization URL
GetOIDCAuthorizationURL(ctx context.Context, redirectURI string) (*types.OIDCAuthURLResponse, error)
// LoginWithOIDC exchanges the callback code, auto-provisions users if needed, and completes login.
// provisioning is the default tenant mode for a newly auto-created user
// (resolved by the caller from auth.default_tenant_mode).
LoginWithOIDC(ctx context.Context, code, redirectURI string, provisioning types.TenantProvisioningMode) (*types.OIDCCallbackResponse, error)
// GetUserByID gets a user by ID
GetUserByID(ctx context.Context, id string) (*types.User, error)
// GetUsersByIDs batch-fetches users by id, returning a map keyed by
// user id. Missing ids are simply absent from the result; the call
// is not an error when some ids resolve to no row. Used on hot list
// endpoints (tenant members, audit logs) to avoid N+1 queries.
GetUsersByIDs(ctx context.Context, ids []string) (map[string]*types.User, error)
// GetUserByEmail gets a user by email
GetUserByEmail(ctx context.Context, email string) (*types.User, error)
// GetUserByUsername gets a user by username
GetUserByUsername(ctx context.Context, username string) (*types.User, error)
// GetUserByTenantID gets the first user (owner) of a tenant
GetUserByTenantID(ctx context.Context, tenantID uint64) (*types.User, error)
// UpdateUser updates user information
UpdateUser(ctx context.Context, user *types.User) error
// DeleteUser deletes a user
DeleteUser(ctx context.Context, id string) error
// ChangePassword changes user password
ChangePassword(ctx context.Context, userID string, oldPassword, newPassword string) error
// AdminResetPassword replaces a user's password without requiring the old
// password and revokes all of that user's existing sessions. Callers must
// enforce the system-admin and cannot-reset-self guards before invoking it.
AdminResetPassword(ctx context.Context, userID string, newPassword string) error
// ValidatePassword validates user password
ValidatePassword(ctx context.Context, userID string, password string) error
// GenerateTokens generates access and refresh tokens for user
GenerateTokens(ctx context.Context, user *types.User) (accessToken, refreshToken string, err error)
// BuildLoginMemberships projects the user's tenant memberships into
// the login-response shape. activeTenant is reused (without an extra
// lookup) for the matching row's TenantName. The slice is guaranteed
// non-nil so callers can serialise it as an empty JSON array when the
// membership table is unavailable.
BuildLoginMemberships(ctx context.Context, user *types.User, activeTenant *types.Tenant) []types.Membership
// SwitchTenant issues a new token pair scoped to targetTenantID and
// returns the corresponding LoginResponse. The caller's previous
// refresh token (passed in for revocation) is invalidated. Membership
// is verified via the TenantMember service before tokens are issued.
// On success the target is recorded as the user's last-active-tenant
// preference (next login and refresh land there). A preference-write
// failure fails the switch so no token pair is issued.
SwitchTenant(ctx context.Context, user *types.User, targetTenantID uint64, currentRefreshToken string) (*types.LoginResponse, error)
// ValidateToken validates an access token. It returns the user
// referenced by the token plus the active tenant ID encoded in the
// JWT's `tenant_id` claim — the latter lets the auth middleware
// honour /auth/switch-tenant sessions that were minted with a
// non-home tenant. Falls back to user.TenantID when the claim is
// missing (old tokens issued before tenant-level RBAC).
ValidateToken(ctx context.Context, token string) (*types.User, uint64, error)
// RefreshToken refreshes access token using refresh token
RefreshToken(ctx context.Context, refreshToken string) (accessToken, newRefreshToken string, err error)
// RevokeToken revokes a token
RevokeToken(ctx context.Context, token string) error
// Logout revokes every outstanding access/refresh token for the user
// identified by the presented JWT.
Logout(ctx context.Context, token string) error
// GetCurrentUser gets current user from context
GetCurrentUser(ctx context.Context) (*types.User, error)
// SearchUsers searches users by username or email
SearchUsers(ctx context.Context, query string, limit int) ([]*types.User, error)
// ListSystemAdmins lists users with IsSystemAdmin=true.
// Returns the page of admins plus the total count (for pagination UI);
// callers pass offset/limit to page through results. Used by the
// /api/v1/system/admin/list endpoint, gated to SystemAdmin callers.
ListSystemAdmins(ctx context.Context, offset, limit int) ([]*types.User, int64, error)
// AdminCreateUser provisions a new local user on behalf of a
// SystemAdmin. When req.Password is nil, a random password is generated
// and returned exactly once as the second result. provisioning is
// resolved by the caller from the shared auth.default_tenant_mode policy.
AdminCreateUser(
ctx context.Context, req *types.AdminCreateUserRequest, provisioning types.TenantProvisioningMode,
) (*types.User, string, error)
// RevokeSystemAdmin removes system-admin privileges with the
// last-admin/self-revoke checks performed atomically.
RevokeSystemAdmin(ctx context.Context, userID, actorID string) (*types.User, error)
// UpdateUserPreferences partially updates the calling user's
// preferences blob (PATCH semantics: only keys present in `patch`
// overwrite existing values). Returns the updated, persisted prefs.
UpdateUserPreferences(ctx context.Context, userID string, patch types.UserPreferences) (types.UserPreferences, error)
}
// UserRepository defines the user repository interface
type UserRepository interface {
// CreateUser creates a user
CreateUser(ctx context.Context, user *types.User) error
// GetUserByID gets a user by ID
GetUserByID(ctx context.Context, id string) (*types.User, error)
// GetUsersByIDs batch-fetches users by id, returning a map keyed by
// user id. Missing ids are simply absent from the result.
GetUsersByIDs(ctx context.Context, ids []string) (map[string]*types.User, error)
// GetUserByEmail gets a user by email
GetUserByEmail(ctx context.Context, email string) (*types.User, error)
// GetUserByUsername gets a user by username
GetUserByUsername(ctx context.Context, username string) (*types.User, error)
// GetUserByTenantID gets the first user (owner) of a tenant
GetUserByTenantID(ctx context.Context, tenantID uint64) (*types.User, error)
// UpdateUser updates a user
UpdateUser(ctx context.Context, user *types.User) error
// DeleteUser deletes a user
DeleteUser(ctx context.Context, id string) error
// ListUsers lists users with pagination
ListUsers(ctx context.Context, offset, limit int) ([]*types.User, error)
// ListSystemAdmins lists users where is_system_admin = true.
// Walks the partial-friendly idx_users_is_system_admin index. Returns
// the slice plus the total count for pagination metadata. Used by
// the system-admin management endpoint.
ListSystemAdmins(ctx context.Context, offset, limit int) ([]*types.User, int64, error)
// RevokeSystemAdmin removes system-admin privileges with the
// last-admin/self-revoke checks performed atomically.
RevokeSystemAdmin(ctx context.Context, userID, actorID string) (*types.User, error)
// SearchUsers searches users by username or email
SearchUsers(ctx context.Context, query string, limit int) ([]*types.User, error)
}
// AuthTokenRepository defines the auth token repository interface
type AuthTokenRepository interface {
// CreateToken creates an auth token
CreateToken(ctx context.Context, token *types.AuthToken) error
// GetTokenByValue gets a token by its value
GetTokenByValue(ctx context.Context, tokenValue string) (*types.AuthToken, error)
// GetTokensByUserID gets all tokens for a user
GetTokensByUserID(ctx context.Context, userID string) ([]*types.AuthToken, error)
// UpdateToken updates a token
UpdateToken(ctx context.Context, token *types.AuthToken) error
// DeleteToken deletes a token
DeleteToken(ctx context.Context, id string) error
// DeleteExpiredTokens deletes all expired tokens
DeleteExpiredTokens(ctx context.Context) error
// RevokeTokensByUserID revokes all tokens for a user
RevokeTokensByUserID(ctx context.Context, userID string) error
}
@@ -0,0 +1,33 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// UserResourceFavoriteRepository is the storage contract for per-user
// favorited resources. Implementations are expected to be tenant-aware
// (queries always include tenant_id) so a user switching tenants sees
// only that tenant's favorites — matches the PK shape in migration 000047.
type UserResourceFavoriteRepository interface {
// List returns favorites for (userID, tenantID, resourceType), newest first.
List(ctx context.Context, userID string, tenantID uint64, resourceType string) ([]*types.UserResourceFavorite, error)
// Add upserts a favorite (no-op if already present). Returns whether
// a new row was actually inserted so the service layer can decide
// whether to emit an audit event.
Add(ctx context.Context, userID string, tenantID uint64, resourceType, resourceID string) (created bool, err error)
// Remove deletes a favorite; returns whether anything was deleted.
Remove(ctx context.Context, userID string, tenantID uint64, resourceType, resourceID string) (removed bool, err error)
// IsFavorite exists for the rare RPC where the caller wants a single
// boolean rather than the full list (e.g. a future GET-by-id endpoint).
IsFavorite(ctx context.Context, userID string, tenantID uint64, resourceType, resourceID string) (bool, error)
}
// UserResourceFavoriteService wraps the repository with input validation
// (resource type allowlist, non-empty id) so the handler stays thin.
type UserResourceFavoriteService interface {
List(ctx context.Context, userID string, tenantID uint64, resourceType string) ([]*types.UserResourceFavorite, error)
Add(ctx context.Context, userID string, tenantID uint64, resourceType, resourceID string) error
Remove(ctx context.Context, userID string, tenantID uint64, resourceType, resourceID string) error
}
+95
View File
@@ -0,0 +1,95 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// StoreRegistry provides VectorStore-based engine registration/lookup.
// Separated from RetrieveEngineRegistry to avoid changing the existing interface
// used by 6 services (17 call sites). Phase 2 may merge into RetrieveEngineRegistry.
type StoreRegistry interface {
// RegisterWithStoreID registers an engine service by VectorStore ID.
// Upsert semantics: existing entry is overwritten silently.
RegisterWithStoreID(storeID string, svc RetrieveEngineService)
// GetByStoreID retrieves an engine service by VectorStore ID.
GetByStoreID(storeID string) (RetrieveEngineService, error)
// UnregisterByStoreID removes an engine service by VectorStore ID (idempotent).
UnregisterByStoreID(storeID string)
}
// EngineFactory creates a RetrieveEngineService from a VectorStore's config.
// Defined as a function type to avoid circular imports between container and service packages.
type EngineFactory func(ctx context.Context, store types.VectorStore) (RetrieveEngineService, error)
// VectorStoreService defines the service interface for vector store management.
// Tenant isolation is enforced by the handler layer (getOwnedStore pattern).
type VectorStoreService interface {
// CreateStore validates and creates a new vector store.
CreateStore(ctx context.Context, store *types.VectorStore) error
// UpdateStore updates an existing vector store (name only).
UpdateStore(ctx context.Context, store *types.VectorStore) error
// DeleteStore deletes a vector store by tenant + id.
// Rejects deletion when any active knowledge base is bound to the store
// (binding guard); the caller must unbind or delete those KBs first.
DeleteStore(ctx context.Context, tenantID uint64, id string) error
// TestConnection tests connectivity to a vector database.
// Returns the detected server version on success (e.g., "7.10.1"), empty string if unknown.
//
// Validation-free: intended for trusted configs (env stores, stored
// configs already validated at create time). Handlers receiving raw
// user input MUST use TestRawConnection instead.
TestConnection(ctx context.Context, engineType types.RetrieverEngineType, config types.ConnectionConfig) (string, error)
// TestRawConnection validates raw user-supplied connection config
// (engine-type allowlist, required fields, SSRF policy) and then delegates
// to TestConnection. This is the entry point for unpersisted user input.
TestRawConnection(ctx context.Context, engineType types.RetrieverEngineType, config types.ConnectionConfig) (string, error)
// SaveDetectedVersion updates the connection_config.version for a stored vector store.
SaveDetectedVersion(ctx context.Context, store *types.VectorStore, version string) error
// ResolveStoreView returns the API-safe display projection of a single
// store ID, scoped to the given tenant. Tries DB stores first, then the
// cached env-store set. Returns types.UnavailableStoreDisplay() when the
// store cannot be resolved — callers should still succeed in such cases
// (the response carries a "unavailable" status signal for the UI).
//
// Never returns connection credentials in any form: the StoreDisplay
// payload carries only Name / Source / EngineType / Status.
ResolveStoreView(ctx context.Context, tenantID uint64, storeID string) (types.StoreDisplay, error)
// BatchResolveStoreView resolves multiple store IDs in a single DB read
// plus the cached env-store match. Returned map keys are the storeIDs
// originally requested; missing IDs map to types.UnavailableStoreDisplay().
//
// Intended for list endpoints that need store metadata for many KBs at
// once without incurring N+1 ResolveStoreView calls.
BatchResolveStoreView(ctx context.Context, tenantID uint64, storeIDs []string) (map[string]types.StoreDisplay, error)
// EnvDefaultStoreView returns the display payload for KBs that fall
// back to the env-configured store. Unlike DefaultStoreDisplay() in
// the types package, this method also fills the engine type
// (e.g. "postgres") so UIs can render the same badge shape for
// env-bound and user-bound KBs. Independent of ResolveStoreView so
// list paths can use it without violating the "no per-KB
// ResolveStoreView" invariant.
EnvDefaultStoreView(ctx context.Context) types.StoreDisplay
}
// VectorStoreRepository defines the repository interface for VectorStore CRUD.
type VectorStoreRepository interface {
// Create creates a new vector store
Create(ctx context.Context, store *types.VectorStore) error
// GetByID retrieves a vector store by ID within a tenant scope
GetByID(ctx context.Context, tenantID uint64, id string) (*types.VectorStore, error)
// List lists all vector stores for a tenant
List(ctx context.Context, tenantID uint64) ([]*types.VectorStore, error)
// Update updates a vector store (only mutable fields: name)
Update(ctx context.Context, store *types.VectorStore) error
// UpdateConnectionConfig updates only the connection_config column
UpdateConnectionConfig(ctx context.Context, store *types.VectorStore) error
// Delete soft-deletes a vector store
Delete(ctx context.Context, tenantID uint64, id string) error
// ExistsByEndpointAndIndex checks if a store with the same endpoint and index already exists
ExistsByEndpointAndIndex(ctx context.Context, tenantID uint64, engineType types.RetrieverEngineType, endpoint string, indexName string) (bool, error)
}
+29
View File
@@ -0,0 +1,29 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// WebSearchProvider defines the interface for web search providers
type WebSearchProvider interface {
// Name returns the name of the provider
Name() string
// Search performs a web search
Search(ctx context.Context, query string, maxResults int, includeDate bool) ([]*types.WebSearchResult, error)
}
// WebSearchService defines the interface for web search services
type WebSearchService interface {
// Search performs a web search using the provider entity identified by providerID.
// If providerID is empty, it falls back to the deprecated config.Provider field for backward compatibility.
Search(ctx context.Context, providerID string, config *types.WebSearchConfig, query string) ([]*types.WebSearchResult, error)
// CompressWithRAG performs RAG-based compression using a temporary, hidden knowledge base
// The temporary knowledge base is deleted after use. The UI will not list it due to repo filtering.
CompressWithRAG(ctx context.Context, sessionID string, tempKBID string, questions []string,
webSearchResults []*types.WebSearchResult, cfg *types.WebSearchConfig,
kbSvc KnowledgeBaseService, knowSvc KnowledgeService,
seenURLs map[string]bool, knowledgeIDs []string,
) (compressed []*types.WebSearchResult, kbID string, newSeen map[string]bool, newIDs []string, err error)
}
@@ -0,0 +1,49 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// WebSearchProviderRepository defines the repository interface for web search provider CRUD
type WebSearchProviderRepository interface {
// Create creates a new web search provider
Create(ctx context.Context, provider *types.WebSearchProviderEntity) error
// GetByID retrieves a web search provider by ID within a tenant scope
GetByID(ctx context.Context, tenantID uint64, id string) (*types.WebSearchProviderEntity, error)
// GetDefault retrieves the default provider (is_default=true) for a tenant, or nil if none.
GetDefault(ctx context.Context, tenantID uint64) (*types.WebSearchProviderEntity, error)
// List lists all web search providers for a tenant
List(ctx context.Context, tenantID uint64) ([]*types.WebSearchProviderEntity, error)
// Update updates a web search provider
Update(ctx context.Context, provider *types.WebSearchProviderEntity) error
// Delete deletes a web search provider (soft delete)
Delete(ctx context.Context, tenantID uint64, id string) error
// ClearDefault clears the default flag for all providers of a tenant, optionally excluding one
ClearDefault(ctx context.Context, tenantID uint64, excludeID string) error
}
// WebSearchProviderService defines the service interface for web search provider management.
// Tenant isolation is enforced by the handler layer (getOwned pattern).
// Service methods operate on entities whose TenantID is already verified.
type WebSearchProviderService interface {
// CreateProvider creates a new web search provider.
// provider.TenantID must be set by the caller (handler).
CreateProvider(ctx context.Context, provider *types.WebSearchProviderEntity) error
// UpdateProvider updates an existing provider.
// provider.TenantID must be set by the caller (handler) for the repository WHERE clause.
UpdateProvider(ctx context.Context, provider *types.WebSearchProviderEntity) error
// DeleteProvider deletes a provider by tenant + id.
DeleteProvider(ctx context.Context, tenantID uint64, id string) error
// UpdateProviderCredentials writes one or more credential fields.
// apiKey nil means "do not touch"; empty string is a no-op (clearing
// goes through ClearProviderCredential). Returns the updated entity.
UpdateProviderCredentials(
ctx context.Context, tenantID uint64, id string, apiKey *string,
) (*types.WebSearchProviderEntity, error)
// ClearProviderCredential removes a single credential field. Currently
// only "api_key" is recognized. Idempotent on already-empty fields.
ClearProviderCredential(ctx context.Context, tenantID uint64, id, field string) error
}
@@ -0,0 +1,26 @@
package interfaces
import (
"context"
)
// WebSearchStateService defines the service interface for managing web search temporary KB state
type WebSearchStateService interface {
// GetWebSearchTempKBState retrieves the temporary KB state for web search from Redis
GetWebSearchTempKBState(
ctx context.Context,
sessionID string,
) (tempKBID string, seenURLs map[string]bool, knowledgeIDs []string)
// SaveWebSearchTempKBState saves the temporary KB state for web search to Redis
SaveWebSearchTempKBState(
ctx context.Context,
sessionID string,
tempKBID string,
seenURLs map[string]bool,
knowledgeIDs []string,
)
// DeleteWebSearchTempKBState deletes the temporary KB state for web search from Redis
DeleteWebSearchTempKBState(ctx context.Context, sessionID string) error
}
+16
View File
@@ -0,0 +1,16 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// WeKnoraCloudService 处理 WeKnoraCloud 凭证管理
type WeKnoraCloudService interface {
// SaveCredentials 仅保存 APPID/APPSECRET 凭证到空间配置,不自动创建模型
SaveCredentials(ctx context.Context, appID, appSecret string) error
// CheckStatus 检查当前空间的 WeKnoraCloud 凭证是否可正常解密
// needsReinit=true 表示加密状态已损坏(salt 变更等),需要用户重新填写凭证
CheckStatus(ctx context.Context) (*types.WeKnoraCloudStatusResult, error)
}
+429
View File
@@ -0,0 +1,429 @@
package interfaces
import (
"context"
"github.com/Tencent/WeKnora/internal/types"
)
// WikiPageService defines the wiki page service interface.
// Provides high-level operations for wiki page CRUD, link management,
// and chunk synchronization.
type WikiPageService interface {
// CreatePage creates a new wiki page, parses outbound links, updates
// bidirectional link references, and syncs to chunks for retrieval.
CreatePage(ctx context.Context, page *types.WikiPage) (*types.WikiPage, error)
// UpdatePage updates an existing wiki page, re-parses links, and updates
// bidirectional references. The `version` field is incremented only when
// a user-visible content field (title, content, summary, page_type,
// status) actually differs from the stored value — bookkeeping-only
// writes (e.g. refreshing source_refs after a same-content re-ingest) are
// persisted without bumping the version.
UpdatePage(ctx context.Context, page *types.WikiPage) (*types.WikiPage, error)
// UpdatePageMeta updates only metadata (status, source_refs, etc.) without
// incrementing version or re-parsing links. Use for publish/archive/source
// ref changes driven by internal reconciliation.
UpdatePageMeta(ctx context.Context, page *types.WikiPage) error
// UpdateAutoLinkedContent persists content changes that come from
// machine-only link decoration — cross-link injection and dead-link
// cleanup — without bumping `version`. Out-links are re-parsed from the
// new body and bidirectional in-link references on target pages are
// refreshed. Conceptually the body still represents the same revision
// the user saw before, just with wiki-link markup added or stripped, so
// surfacing a version bump to end users is misleading.
UpdateAutoLinkedContent(ctx context.Context, page *types.WikiPage) error
// GetPageBySlug retrieves a wiki page by its slug within a knowledge base.
GetPageBySlug(ctx context.Context, kbID string, slug string) (*types.WikiPage, error)
// RepairContentLinks rewrites dead [[slug]] references in content to their
// most likely live target (rewrite-only — never strips). Used by the agent
// write path to auto-correct LLM-mangled slugs (especially UUID-based
// summary slugs) before persistence. Returns the possibly-updated content
// and whether any rewrite happened. Best-effort: callers may ignore errors.
RepairContentLinks(ctx context.Context, kbID, selfSlug, content string) (string, bool, error)
// GetPageByID retrieves a wiki page by its unique ID.
GetPageByID(ctx context.Context, id string) (*types.WikiPage, error)
// ListPages lists wiki pages with optional filtering and pagination.
ListPages(ctx context.Context, req *types.WikiPageListRequest) (*types.WikiPageListResponse, error)
// DeletePage soft-deletes a wiki page and removes its chunk sync.
DeletePage(ctx context.Context, kbID string, slug string) error
// GetIndex returns the index page for a knowledge base.
// Creates a default one if it doesn't exist.
//
// The index row now carries only the LLM-generated intro in its content
// column — the heavy directory listing was moved out of wiki_pages.
// Callers that want a renderable directory should use GetIndexView,
// which assembles {intro, groups} structured output on demand.
GetIndex(ctx context.Context, kbID string) (*types.WikiPage, error)
// GetIndexView returns the structured index response — intro (from the
// index row) plus a paginated window of directory entries per requested
// page_type. `types` restricts which page types to include (empty =
// all), `limit` bounds the per-type window size, `cursor` is an opaque
// offset string resumed from a previous response.
//
// Keeping the directory as JSON rather than a multi-MB markdown blob
// means a 40k-page KB no longer pays O(N) transport + rendering cost
// on every index open. See /wiki/index handler + WikiBrowser.vue for
// the consumer.
GetIndexView(ctx context.Context, kbID string, pageTypes []string, limit int, cursor string) (*types.WikiIndexResponse, error)
// GetGraph returns the link graph data for visualization. The caller
// supplies a WikiGraphRequest describing the desired slice of the graph
// (overview top-N or ego neighborhood around a center slug). Callers
// that need the full graph (e.g. wiki lint) can set Limit <= 0 to
// disable the node cap; the HTTP handler always clamps Limit to a
// safe range before invoking the service.
GetGraph(ctx context.Context, req *types.WikiGraphRequest) (*types.WikiGraphData, error)
// GetStats returns aggregate statistics about the wiki.
GetStats(ctx context.Context, kbID string) (*types.WikiStats, error)
// RebuildLinks re-parses all pages and rebuilds bidirectional link references.
RebuildLinks(ctx context.Context, kbID string) error
// InjectCrossLinks scans specified pages and injects [[wiki-links]] for mentions
// of other wiki page titles/aliases in the content.
InjectCrossLinks(ctx context.Context, kbID string, affectedSlugs []string)
// RebuildIndexPage regenerates the index page.
RebuildIndexPage(ctx context.Context, kbID string) error
// ListAllPages retrieves all non-archived wiki pages in a knowledge base without pagination.
// Used for index rebuild, graph generation, cross-link injection, etc.
ListAllPages(ctx context.Context, kbID string) ([]*types.WikiPage, error)
// ListByType returns every wiki page of the given type. Used by
// callers that need the full set for a specific type (e.g. intro
// regeneration needs summary pages). For directory rendering, use
// GetIndexView instead — it paginates via ListByTypeLight.
ListByType(ctx context.Context, kbID string, pageType string) ([]*types.WikiPage, error)
// ListPagesBySourceRef retrieves all wiki pages whose source_refs reference
// the given knowledge ID. Used by delete/ingest reconciliation paths that
// need to find pages touched by a specific document at read time (rather
// than relying on a caller-provided stale snapshot).
ListPagesBySourceRef(ctx context.Context, kbID string, knowledgeID string) ([]*types.WikiPage, error)
// ListSlugsBySourceRef returns just the slugs of pages that cite the
// given knowledge id. Cheaper than ListPagesBySourceRef when callers
// only need the slug set (e.g. wiki ingest's "before" snapshot).
ListSlugsBySourceRef(ctx context.Context, kbID string, knowledgeID string) ([]string, error)
// ListBySlugs is the lazy fetcher used by the wiki ingest batch
// context: returns lightweight projections (no content / source_refs)
// for the requested slugs in a single IN query. Used in place of
// the pre-batch ListAllPages dump.
ListBySlugs(ctx context.Context, kbID string, slugs []string) (map[string]*types.WikiPageLite, error)
// ListSummariesByKnowledgeIDs returns summary-page content keyed by
// the knowledge id that authored it. Used by the retract / reparse
// branches of reduceSlugUpdates for "what was this doc's
// contribution?" framing.
ListSummariesByKnowledgeIDs(ctx context.Context, kbID string, kids []string) (map[string]string, error)
// ExistsSlugs reports which of the given slugs are live (non-archived).
// Used by cleanDeadLinks to validate out-link targets cheaply.
ExistsSlugs(ctx context.Context, kbID string, slugs []string) (map[string]bool, error)
// ListAllSlugs returns every non-archived slug in the KB. Used by
// lint to compute the live-slug set for broken-link detection.
ListAllSlugs(ctx context.Context, kbID string) ([]string, error)
// ListPagesCursor walks the KB in id-asc order with an opaque
// cursor. Used by lint to stream the page set without holding it
// all in memory at once.
ListPagesCursor(ctx context.Context, kbID string, cursor string, limit int) ([]*types.WikiPage, string, error)
// ListByTypeRecent returns the most-recently-updated pages of the
// given type, projected to slug/title/summary, capped at `limit`.
// Used by rebuildIndexPage's first-time generation path.
ListByTypeRecent(ctx context.Context, kbID string, pageType string, limit int) ([]types.WikiIndexEntry, error)
// FindSimilarPages performs a pg_trgm similarity search over
// page titles. Used by the dedup pre-filter to surface candidate
// merge targets server-side.
FindSimilarPages(ctx context.Context, kbID string, query string, pageTypes []string, limit int) ([]*types.WikiPageLite, error)
// FindPagesByNormalizedTitle returns non-archived pages of pageType whose
// display title matches identity after the same whitespace/case fold used
// by wiki ingest identity claims. Used so exact same-title pages are found
// even when they miss the trigram top-K.
FindPagesByNormalizedTitle(ctx context.Context, kbID, pageType, identity string) ([]*types.WikiPageLite, error)
// FindPagesByNormalizedTitles is the batched form of
// FindPagesByNormalizedTitle. identities are already whitespace-stripped
// and lowercased; empty entries are ignored.
FindPagesByNormalizedTitles(ctx context.Context, kbID, pageType string, identities []string) ([]*types.WikiPageLite, error)
// ListDistinctCategoryPaths returns the existing wiki folder paths (split
// into segments), capped at maxPaths. Used by wiki ingest's taxonomy
// planner as the pool of folders to reuse.
ListDistinctCategoryPaths(ctx context.Context, kbID string, maxPaths int) ([][]string, error)
// --- Folder tree (wiki_folders) ---
// ListChildFolders returns the direct child folders of parentID ("" =
// root) relevant to the given page types, each enriched with a recursive
// page count (the folder's whole subtree, restricted to pageTypes) and
// whether it has relevant sub-folders. A folder is shown when its subtree
// holds at least one page of pageTypes, or when it is entirely empty (a
// user-created container with no pages of any type). When pageTypes is
// empty every type counts.
ListChildFolders(
ctx context.Context, kbID string, parentID string, pageTypes []string,
) ([]types.WikiFolderNode, error)
// GetFolder retrieves a single folder by id.
GetFolder(ctx context.Context, kbID string, id string) (*types.WikiFolder, error)
// CreateFolder creates a new (initially empty) folder under parentID and
// returns it. Fails if a sibling with the same name already exists.
CreateFolder(ctx context.Context, kbID string, tenantID uint64, parentID string, name string) (*types.WikiFolder, error)
// RenameOrMoveFolder renames and/or reparents a folder, recomputing the
// materialized path/depth of the whole subtree and the cached paths of
// every page underneath. Returns the updated folder.
RenameOrMoveFolder(ctx context.Context, kbID string, id string, newName string, newParentID string, moveParent bool) (*types.WikiFolder, error)
// DeleteFolder removes an empty folder. Fails if it still contains pages
// or child folders (the UI must move or delete contents first).
DeleteFolder(ctx context.Context, kbID string, id string) error
// PruneEmptyFolderChains deletes candidate folders that are still empty,
// walking upward through newly-empty ancestors. Candidates are supplied by
// document retract cleanup so unrelated user-created empty folders remain.
PruneEmptyFolderChains(ctx context.Context, kbID string, folderIDs []string) ([]string, error)
// FindOrCreateFolderPath resolves a category path (e.g. ["AI","RAG"]) to a
// folder id, creating any missing intermediate folders. Returns the leaf
// folder id and the canonical (cleaned) path. An empty/blank path resolves
// to the root ("").
FindOrCreateFolderPath(ctx context.Context, kbID string, tenantID uint64, path []string) (string, []string, error)
// MovePage relocates a page into folderID ("" = root), recomputing its
// cached category path. Returns the updated page.
MovePage(ctx context.Context, kbID string, slug string, folderID string) (*types.WikiPage, error)
// CountByType returns page counts grouped by type for a knowledge
// base. Re-exposed at the service layer so the index intro
// generation path can frame the LLM prompt with "showing N of M"
// metadata when the recent-N projection doesn't cover the full
// summary set.
CountByType(ctx context.Context, kbID string) (map[string]int64, error)
// SearchPages performs full-text search over wiki pages.
SearchPages(ctx context.Context, kbID string, query string, limit int) ([]*types.WikiPage, error)
// ListRevisions returns the stored historical snapshots for a page
// (newest first, content omitted) plus the total snapshot count and the
// page's current version. The current version itself has no revision
// row — it lives in wiki_pages.
ListRevisions(ctx context.Context, kbID string, slug string, limit int, offset int) (*types.WikiPageRevisionListResponse, error)
// GetRevision returns one historical snapshot (content included).
GetRevision(ctx context.Context, kbID string, slug string, version int) (*types.WikiPageRevision, error)
// RevertPageToVersion rolls the page back to the content of the given
// stored revision by applying it as a regular edit: the pre-revert state
// is snapshotted, version advances, and links are re-parsed. The edit is
// attributed to WikiEditSourceRevert.
RevertPageToVersion(ctx context.Context, kbID string, slug string, version int) (*types.WikiPage, error)
// CreateIssue logs a new issue for a wiki page.
CreateIssue(ctx context.Context, issue *types.WikiPageIssue) (*types.WikiPageIssue, error)
// ListIssues retrieves issues for a knowledge base, optionally filtered by slug and status.
ListIssues(ctx context.Context, kbID string, slug string, status string) ([]*types.WikiPageIssue, error)
// UpdateIssueStatus updates the status of an issue (e.g. pending -> resolved/ignored).
UpdateIssueStatus(ctx context.Context, issueID string, status string) error
}
// WikiPageRepository defines the wiki page data persistence interface.
type WikiPageRepository interface {
// Create inserts a new wiki page record.
Create(ctx context.Context, page *types.WikiPage) error
// Update rewrites a wiki page record with optimistic locking and
// unconditionally increments `version`. Callers are responsible for
// deciding whether the edit is user-visible — the service layer uses
// UpdateMeta for bookkeeping-only writes instead.
Update(ctx context.Context, page *types.WikiPage) error
// UpdateMeta updates bookkeeping / provenance fields (in/out links,
// status, source_refs, chunk_refs, page_metadata, updated_at) without
// touching `version`. Safe for link maintenance, re-ingest with an
// unchanged body, and status-only transitions.
UpdateMeta(ctx context.Context, page *types.WikiPage) error
// UpdateAutoLinkedContent rewrites `content`, `out_links` and
// `updated_at` in place while leaving `version` untouched. Intended for
// machine-only link markup changes (cross-link injection / dead-link
// cleanup) so the first-ingest page doesn't jump to v2 just because the
// post-processor added a `[[...]]` wrapper.
UpdateAutoLinkedContent(ctx context.Context, page *types.WikiPage) error
// GetByID retrieves a wiki page by its unique ID.
GetByID(ctx context.Context, id string) (*types.WikiPage, error)
// GetBySlug retrieves a wiki page by slug within a knowledge base.
GetBySlug(ctx context.Context, kbID string, slug string) (*types.WikiPage, error)
// List retrieves wiki pages with filtering and pagination.
List(ctx context.Context, req *types.WikiPageListRequest) ([]*types.WikiPage, int64, error)
// ListByType retrieves all wiki pages of a given type within a knowledge base.
ListByType(ctx context.Context, kbID string, pageType string) ([]*types.WikiPage, error)
// ListByTypeLight returns a paginated window of lightweight entries
// (slug/title/summary only) for the given page_type plus the total
// non-archived count. Used by the structured index API so reads do not
// have to materialize TEXT content for every wiki_pages row.
ListByTypeLight(ctx context.Context, kbID string, pageType string, limit int, offset int) ([]types.WikiIndexEntry, int64, error)
// ListBySourceRef retrieves all wiki pages that reference a given source knowledge ID.
ListBySourceRef(ctx context.Context, kbID string, sourceKnowledgeID string) ([]*types.WikiPage, error)
// ListSlugsBySourceRef returns just the slugs of pages that reference
// the given knowledge id. Same predicate as ListBySourceRef but
// projected to a single column for the wiki ingest pipeline's
// "before" snapshot path.
ListSlugsBySourceRef(ctx context.Context, kbID string, sourceKnowledgeID string) ([]string, error)
// ListBySlugs returns lightweight projections (slug/title/page_type/
// status/aliases/out_links) for the given slugs in one IN query.
// Used by the lazy fetcher path in wiki ingest's Map/Reduce phases.
ListBySlugs(ctx context.Context, kbID string, slugs []string) (map[string]*types.WikiPageLite, error)
// ListSummariesByKnowledgeIDs returns summary-page content keyed by
// the knowledge id that authored it. Used by the retract branch of
// reduceSlugUpdates for "what was this doc's contribution?" framing.
ListSummariesByKnowledgeIDs(ctx context.Context, kbID string, kids []string) (map[string]string, error)
// ExistsSlugs reports which of the given slugs are live (non-
// archived). Used by cleanDeadLinks to validate out-link targets
// without loading the referenced pages' content.
ExistsSlugs(ctx context.Context, kbID string, slugs []string) (map[string]bool, error)
// ListAllSlugs returns every non-archived slug in the KB. Used by
// lint to compute the live-slug set without paying for ListAll's
// full row materialization.
ListAllSlugs(ctx context.Context, kbID string) ([]string, error)
// ListPagesCursor returns up to `limit` pages ordered by id ASC,
// paginated by an opaque numeric cursor. Used by lint to stream
// the KB without loading every page at once.
ListPagesCursor(ctx context.Context, kbID string, cursor string, limit int) ([]*types.WikiPage, string, error)
// ListByTypeRecent returns the most-recently-updated pages of the
// given type, projected to slug/title/summary, capped at `limit`.
// Used by rebuildIndexPage's first-time generation path so the
// LLM prompt size is bounded on large KBs.
ListByTypeRecent(ctx context.Context, kbID string, pageType string, limit int) ([]types.WikiIndexEntry, error)
// FindSimilarPages returns the top-k pages whose lowercase title
// is most similar to the query under pg_trgm. `pageTypes` empty
// defaults to entity+concept. Used by the dedup pre-filter to
// surface candidate merge targets server-side.
FindSimilarPages(ctx context.Context, kbID string, query string, pageTypes []string, limit int) ([]*types.WikiPageLite, error)
// FindPagesByNormalizedTitle returns non-archived pages of pageType whose
// whitespace-stripped, lowercased title equals identity.
FindPagesByNormalizedTitle(ctx context.Context, kbID, pageType, identity string) ([]*types.WikiPageLite, error)
// FindPagesByNormalizedTitles is the batched form of
// FindPagesByNormalizedTitle.
FindPagesByNormalizedTitles(ctx context.Context, kbID, pageType string, identities []string) ([]*types.WikiPageLite, error)
// ListDistinctCategoryPaths returns the materialized paths of existing
// wiki folders (split into segments), capped at maxPaths. Used by the
// wiki ingest taxonomy planner as the pool of folders to reuse.
ListDistinctCategoryPaths(ctx context.Context, kbID string, maxPaths int) ([][]string, error)
// --- Folder tree (wiki_folders) ---
// CreateFolder inserts a new directory node.
CreateFolder(ctx context.Context, folder *types.WikiFolder) error
// GetFolderByID retrieves a folder by id within a knowledge base.
GetFolderByID(ctx context.Context, kbID string, id string) (*types.WikiFolder, error)
// GetChildFolderByName returns the live child folder of parentID with the
// given name, or ErrWikiFolderNotFound. Used by find-or-create.
GetChildFolderByName(ctx context.Context, kbID string, parentID string, name string) (*types.WikiFolder, error)
// ListChildFolders returns the direct child folders of parentID, ordered
// by sort_order then name.
ListChildFolders(ctx context.Context, kbID string, parentID string) ([]*types.WikiFolder, error)
// ListAllFolders returns every folder in the knowledge base.
ListAllFolders(ctx context.Context, kbID string) ([]*types.WikiFolder, error)
// UpdateFolder rewrites a folder's mutable fields (name, parent_id, path,
// depth, sort_order, updated_at).
UpdateFolder(ctx context.Context, folder *types.WikiFolder) error
// DeleteFolder soft-deletes a folder by id.
DeleteFolder(ctx context.Context, kbID string, id string) error
// CountPagesInFolder returns the number of live (non-archived) pages
// directly in the folder (folder_id = id).
CountPagesInFolder(ctx context.Context, kbID string, folderID string) (int64, error)
// CountPagesByFolder returns live (non-archived) page counts grouped by
// folder_id. When pageTypes is non-empty the count is restricted to those
// page types; otherwise all types are counted. Pages at the wiki root carry
// the empty-string key.
CountPagesByFolder(ctx context.Context, kbID string, pageTypes []string) (map[string]int64, error)
// ListPagesByFolderIDs returns all pages whose folder_id is in the set.
// Used to recompute cached paths when a folder subtree is moved/renamed.
ListPagesByFolderIDs(ctx context.Context, kbID string, folderIDs []string) ([]*types.WikiPage, error)
// ListAll retrieves all non-archived wiki pages in a knowledge base (for link rebuilding, graph generation).
ListAll(ctx context.Context, kbID string) ([]*types.WikiPage, error)
// ListRecentForSuggestions returns recent user-visible wiki pages under the given
// knowledge bases, used to produce fallback suggested questions for Wiki-only KBs
// that do not have AI-generated document questions or recommended FAQ entries.
// Excludes the index page and archived pages. Returns up to `limit` rows sorted
// by updated_at descending.
ListRecentForSuggestions(ctx context.Context, tenantID uint64, kbIDs []string, limit int) ([]*types.WikiPage, error)
// Delete soft-deletes a wiki page by knowledge base ID and slug.
Delete(ctx context.Context, kbID string, slug string) error
// DeleteByID soft-deletes a wiki page by ID.
DeleteByID(ctx context.Context, id string) error
// Search performs full-text search on wiki pages within a knowledge base.
Search(ctx context.Context, kbID string, query string, limit int) ([]*types.WikiPage, error)
// CountByType returns page counts grouped by type for a knowledge base.
CountByType(ctx context.Context, kbID string) (map[string]int64, error)
// CountOrphans returns the number of pages with no inbound links.
CountOrphans(ctx context.Context, kbID string) (int64, error)
// UpdateWithRevision snapshots the version being superseded and applies
// the page update in a single transaction, so history can never contain
// a snapshot of a version that is still current. Inserting an already-
// present (page_id, version) pair is a silent no-op.
UpdateWithRevision(ctx context.Context, page *types.WikiPage, rev *types.WikiPageRevision) error
// ListRevisions returns snapshots for a page newest-first (content
// column omitted) plus the total snapshot count.
ListRevisions(ctx context.Context, kbID string, pageID string, limit int, offset int) ([]*types.WikiPageRevision, int64, error)
// GetRevision returns one snapshot with content.
GetRevision(ctx context.Context, kbID string, pageID string, version int) (*types.WikiPageRevision, error)
// PruneRevisions bounds per-page history storage using the two-tier
// retention described by the request.
PruneRevisions(ctx context.Context, req types.WikiRevisionPruneRequest) error
// DeleteRevisionsByPage hard-deletes a page's entire snapshot history.
DeleteRevisionsByPage(ctx context.Context, pageID string) error
// CreateIssue inserts a new wiki page issue record.
CreateIssue(ctx context.Context, issue *types.WikiPageIssue) error
// ListIssues retrieves issues with optional filtering by slug and status.
ListIssues(ctx context.Context, kbID string, slug string, status string) ([]*types.WikiPageIssue, error)
// UpdateIssueStatus updates an issue's status.
UpdateIssueStatus(ctx context.Context, issueID string, status string) error
}