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
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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user