refactor(weknoracloud): update credential handling and configuration

- Replaced references to Docreader credentials with WeKnoraCloud credentials across multiple services and handlers.
- Introduced a new CredentialsConfig structure to manage third-party provider credentials, specifically for WeKnoraCloud.
- Updated database schema to include a credentials column for storing WeKnoraCloud AppID and AppSecret.
- Enhanced methods for retrieving and utilizing WeKnoraCloud credentials, ensuring proper encryption and decryption during storage and retrieval.

This update improves the management of WeKnoraCloud credentials, streamlining the integration and enhancing security measures.
This commit is contained in:
wizardchen
2026-04-15 10:55:39 +08:00
committed by lyingbug
parent 3b23dccd5c
commit 3f8201cd49
13 changed files with 150 additions and 91 deletions

View File

@@ -8057,12 +8057,12 @@ func (s *knowledgeService) resolveDocReader(ctx context.Context, engine, fileTyp
case docparser.SimpleEngineName:
return &docparser.SimpleFormatReader{}
case docparser.WeKnoraCloudEngineName:
creds := s.tenantService.GetDocreaderCredentials(ctx)
creds := s.tenantService.GetWeKnoraCloudCredentials(ctx)
if creds == nil {
logger.Warnf(ctx, "[resolveDocReader] WeKnoraCloud: no tenant docreader credentials (fileType=%s)", fileType)
logger.Warnf(ctx, "[resolveDocReader] WeKnoraCloud: no tenant credentials (fileType=%s)", fileType)
return nil
}
reader, err := docparser.NewWeKnoraCloudSignedDocumentReader(creds.AppID, creds.APIKey)
reader, err := docparser.NewWeKnoraCloudSignedDocumentReader(creds.AppID, creds.AppSecret)
if err != nil {
logger.Errorf(ctx, "[resolveDocReader] WeKnoraCloud reader init failed: %v", err)
return nil

View File

@@ -71,7 +71,7 @@ func (s *modelService) resolveWeKnoraCloudCredentials(ctx context.Context, param
if s.tenantService == nil {
return
}
creds := s.tenantService.GetDocreaderCredentials(ctx)
creds := s.tenantService.GetWeKnoraCloudCredentials(ctx)
if creds == nil {
return
}
@@ -79,7 +79,7 @@ func (s *modelService) resolveWeKnoraCloudCredentials(ctx context.Context, param
appID = creds.AppID
}
if appSecret == "" {
appSecret = creds.APIKey
appSecret = creds.AppSecret
}
return
}

View File

@@ -343,42 +343,24 @@ func (s *tenantService) GetTenantByIDForUser(ctx context.Context, tenantID uint6
return tenant, nil
}
func (s *tenantService) GetDocreaderCredentials(ctx context.Context) *types.DocreaderCredentials {
// Try to get tenant info directly first
func (s *tenantService) GetWeKnoraCloudCredentials(ctx context.Context) *types.WeKnoraCloudCredentials {
// Try to get tenant info from context first (already loaded by middleware).
// CredentialsConfig.Scan handles decryption, so credentials are ready to use.
if tenant, ok := types.TenantInfoFromContext(ctx); ok {
if tenant.ParserEngineConfig != nil {
appID := strings.TrimSpace(tenant.ParserEngineConfig.DocreaderAppID)
if appID == "" || tenant.ParserEngineConfig.DocreaderAPIKey == "" {
return nil
}
if key := utils.GetAESKey(); key != nil {
if encrypted, err := utils.DecryptAESGCM(tenant.ParserEngineConfig.DocreaderAPIKey, key); err == nil {
return &types.DocreaderCredentials{AppID: appID, APIKey: encrypted}
}
}
return &types.DocreaderCredentials{AppID: appID, APIKey: tenant.ParserEngineConfig.DocreaderAPIKey}
if creds := tenant.Credentials.GetWeKnoraCloud(); creds != nil {
return creds
}
}
// If no tenant info in context, try to get tenant ID and load tenant
// Fallback: load tenant from repo by tenantID
tenantID, ok := types.TenantIDFromContext(ctx)
if !ok {
return nil
}
// Load tenant from repo if we only have tenantID
tenant, err := s.repo.GetTenantByID(ctx, tenantID)
if err == nil && tenant != nil && tenant.ParserEngineConfig != nil {
appID := strings.TrimSpace(tenant.ParserEngineConfig.DocreaderAppID)
if appID != "" && tenant.ParserEngineConfig.DocreaderAPIKey != "" {
if key := utils.GetAESKey(); key != nil {
if encrypted, err := utils.DecryptAESGCM(tenant.ParserEngineConfig.DocreaderAPIKey, key); err == nil {
return &types.DocreaderCredentials{AppID: appID, APIKey: encrypted}
}
}
return &types.DocreaderCredentials{AppID: appID, APIKey: tenant.ParserEngineConfig.DocreaderAPIKey}
}
if err != nil || tenant == nil {
return nil
}
return nil
return tenant.Credentials.GetWeKnoraCloud()
}

View File

@@ -46,15 +46,8 @@ func (s *weKnoraCloudService) SaveCredentials(ctx context.Context, appID, appSec
return fmt.Errorf("credential verification failed: %w", err)
}
encryptedSecret := appSecret
if key := utils.GetAESKey(); key != nil {
if encrypted, err := utils.EncryptAESGCM(appSecret, key); err == nil {
encryptedSecret = encrypted
}
}
tenantID := types.MustTenantIDFromContext(ctx)
return s.updateTenantCredentials(ctx, tenantID, appID, encryptedSecret)
return s.updateTenantCredentials(ctx, tenantID, appID, appSecret)
}
// verifyCredentials 向 WeKnoraCloud /api/v1/health 发送带签名头的 GET。
@@ -105,30 +98,26 @@ func (s *weKnoraCloudService) CheckStatus(ctx context.Context) (*types.WeKnoraCl
return &types.WeKnoraCloudStatusResult{HasModels: false, NeedsReinit: false}, nil
}
// Check if tenant has WeKnoraCloud credentials in parser config
if tenant.ParserEngineConfig == nil || tenant.ParserEngineConfig.DocreaderAppID == "" || tenant.ParserEngineConfig.DocreaderAPIKey == "" {
return &types.WeKnoraCloudStatusResult{
HasModels: false,
NeedsReinit: false,
}, nil
creds := tenant.Credentials.GetWeKnoraCloud()
if creds == nil {
return &types.WeKnoraCloudStatusResult{HasModels: false, NeedsReinit: false}, nil
}
// Try to decrypt the API key
if key := utils.GetAESKey(); key != nil {
if _, err := utils.DecryptAESGCM(tenant.ParserEngineConfig.DocreaderAPIKey, key); err != nil {
return &types.WeKnoraCloudStatusResult{
HasModels: true,
NeedsReinit: true,
Reason: "WeKnoraCloud 凭证解密失败(服务重启后加密密钥已变更),请重新填写 APPID 和 APPSECRET",
}, nil
}
// CredentialsConfig.Scan already attempts decryption.
// If the AES key has rotated, Scan silently keeps the enc:v1:... blob.
if strings.HasPrefix(creds.AppSecret, utils.EncPrefix) {
return &types.WeKnoraCloudStatusResult{
HasModels: true,
NeedsReinit: true,
Reason: "WeKnoraCloud 凭证解密失败(服务重启后加密密钥已变更),请重新填写 APPID 和 APPSECRET",
}, nil
}
return &types.WeKnoraCloudStatusResult{HasModels: true, NeedsReinit: false}, nil
}
// updateTenantCredentials 更新租户的 WeKnoraCloud 凭证和 DocReader 地址
func (s *weKnoraCloudService) updateTenantCredentials(ctx context.Context, tenantID uint64, appID, encryptedSecret string) error {
// updateTenantCredentials 更新租户的 WeKnoraCloud 凭证
func (s *weKnoraCloudService) updateTenantCredentials(ctx context.Context, tenantID uint64, appID, appSecret string) error {
if s.tenantRepo == nil {
return fmt.Errorf("tenant repository is required")
}
@@ -137,10 +126,12 @@ func (s *weKnoraCloudService) updateTenantCredentials(ctx context.Context, tenan
if err != nil {
return err
}
if tenant.ParserEngineConfig == nil {
tenant.ParserEngineConfig = &types.ParserEngineConfig{}
if tenant.Credentials == nil {
tenant.Credentials = &types.CredentialsConfig{}
}
tenant.Credentials.WeKnoraCloud = &types.WeKnoraCloudCredentials{
AppID: appID,
AppSecret: appSecret,
}
tenant.ParserEngineConfig.DocreaderAppID = appID
tenant.ParserEngineConfig.DocreaderAPIKey = encryptedSecret
return s.tenantRepo.UpdateTenant(ctx, tenant)
}

View File

@@ -1515,9 +1515,9 @@ func (h *InitializationHandler) CheckRemoteModel(c *gin.Context) {
return
}
var appID, appSecret string
if tenantInfo.ParserEngineConfig != nil {
appID = tenantInfo.ParserEngineConfig.DocreaderAppID
appSecret = tenantInfo.ParserEngineConfig.DocreaderAPIKey
if creds := tenantInfo.Credentials.GetWeKnoraCloud(); creds != nil {
appID = creds.AppID
appSecret = creds.AppSecret
}
// 创建模型配置进行测试
@@ -1612,9 +1612,9 @@ func (h *InitializationHandler) TestEmbeddingModel(c *gin.Context) {
return
}
var appID, appSecret string
if tenantInfo.ParserEngineConfig != nil {
appID = tenantInfo.ParserEngineConfig.DocreaderAppID
appSecret = tenantInfo.ParserEngineConfig.DocreaderAPIKey
if creds := tenantInfo.Credentials.GetWeKnoraCloud(); creds != nil {
appID = creds.AppID
appSecret = creds.AppSecret
}
// 构造 embedder 配置
@@ -1740,9 +1740,9 @@ func (h *InitializationHandler) checkRerankModelConnection(ctx context.Context,
return false, "租户信息未找到"
}
var appID, appSecret string
if tenantInfo.ParserEngineConfig != nil {
appID = tenantInfo.ParserEngineConfig.DocreaderAppID
appSecret = tenantInfo.ParserEngineConfig.DocreaderAPIKey
if creds := tenantInfo.Credentials.GetWeKnoraCloud(); creds != nil {
appID = creds.AppID
appSecret = creds.AppSecret
}
config.AppID = appID
config.AppSecret = appSecret

View File

@@ -142,8 +142,16 @@ func (h *SystemHandler) getDocReaderConnInfo() (addr, transport string) {
func (h *SystemHandler) ListParserEngines(c *gin.Context) {
var overrides map[string]string
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.ParserEngineConfig != nil {
overrides = tenant.ParserEngineConfig.ToOverridesMap()
if tenant, ok := v.(*types.Tenant); ok && tenant != nil {
if tenant.ParserEngineConfig != nil {
overrides = tenant.ParserEngineConfig.ToOverridesMap()
}
if creds := tenant.Credentials.GetWeKnoraCloud(); creds != nil {
if overrides == nil {
overrides = make(map[string]string)
}
overrides["weknoracloud_app_id"] = creds.AppID
}
}
}
@@ -196,8 +204,16 @@ func (h *SystemHandler) ReconnectDocReader(c *gin.Context) {
var overrides map[string]string
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.ParserEngineConfig != nil {
overrides = tenant.ParserEngineConfig.ToOverridesMap()
if tenant, ok := v.(*types.Tenant); ok && tenant != nil {
if tenant.ParserEngineConfig != nil {
overrides = tenant.ParserEngineConfig.ToOverridesMap()
}
if creds := tenant.Credentials.GetWeKnoraCloud(); creds != nil {
if overrides == nil {
overrides = make(map[string]string)
}
overrides["weknoracloud_app_id"] = creds.AppID
}
}
}
remoteEngines := h.fetchRemoteEngines(c.Request.Context(), h.documentReader, overrides)
@@ -223,6 +239,16 @@ func (h *SystemHandler) CheckParserEngines(c *gin.Context) {
return
}
overrides := body.ToOverridesMap()
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
if tenant, ok := v.(*types.Tenant); ok && tenant != nil {
if creds := tenant.Credentials.GetWeKnoraCloud(); creds != nil {
if overrides == nil {
overrides = make(map[string]string)
}
overrides["weknoracloud_app_id"] = creds.AppID
}
}
}
reader, docreaderAddr, docreaderTransport := h.resolveDocReader(c.Request.Context(), overrides)
connected := reader != nil && reader.IsConnected()
remoteEngines := h.fetchRemoteEngines(c.Request.Context(), reader, overrides)
@@ -1034,11 +1060,11 @@ func (h *SystemHandler) ResolveDocumentReader(ctx context.Context, addr string)
}
if service.IsWeKnoraCloudDocReaderAddr(addr) {
creds := h.tenantSvc.GetDocreaderCredentials(ctx)
creds := h.tenantSvc.GetWeKnoraCloudCredentials(ctx)
if creds == nil {
return nil
}
reader, err := docparser.NewWeKnoraCloudSignedDocumentReader(creds.AppID, creds.APIKey)
reader, err := docparser.NewWeKnoraCloudSignedDocumentReader(creds.AppID, creds.AppSecret)
if err != nil {
return nil
}

View File

@@ -89,7 +89,7 @@ func (e *weKnoraCloudEngine) FileTypes(_ bool) []string {
return []string{"docx", "doc", "pdf", "md", "markdown", "xlsx", "xls", "pptx", "ppt"}
}
func (e *weKnoraCloudEngine) CheckAvailable(docreaderConnected bool, overrides map[string]string) (bool, string) {
if overrides["docreader_app_id"] != "" {
if overrides["weknoracloud_app_id"] != "" {
return true, ""
}
return false, "WeKnora Cloud credentials not configured. Go to Settings → WeKnora Cloud to set up."

View File

@@ -28,8 +28,8 @@ type TenantService interface {
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)
// GetTenantByAPIKey gets a tenant by API key
GetDocreaderCredentials(ctx context.Context) *types.DocreaderCredentials
// GetWeKnoraCloudCredentials returns the decrypted WeKnoraCloud credentials for the current tenant.
GetWeKnoraCloudCredentials(ctx context.Context) *types.WeKnoraCloudCredentials
}
// TenantRepository defines the tenant repository interface

View File

@@ -101,6 +101,8 @@ type Tenant struct {
ConversationConfig *ConversationConfig `yaml:"conversation_config" json:"conversation_config" gorm:"type:jsonb"`
// Parser engine config overrides (MinerU endpoint, API key, etc.). Used when parsing documents; overrides env.
ParserEngineConfig *ParserEngineConfig `yaml:"parser_engine_config" json:"parser_engine_config" gorm:"type:jsonb"`
// Credentials config: third-party provider credentials (e.g. WeKnoraCloud AppID/AppSecret)
Credentials *CredentialsConfig `yaml:"credentials" json:"credentials" gorm:"type:jsonb"`
// Storage engine config: parameters for Local, MinIO, COS. Used for document/file storage and docreader.
StorageEngineConfig *StorageEngineConfig `yaml:"storage_engine_config" json:"storage_engine_config" gorm:"type:jsonb"`
// Chat history config: knowledge base configuration for indexing and searching chat messages via vector search
@@ -230,13 +232,72 @@ func (c *ConversationConfig) Scan(value interface{}) error {
return json.Unmarshal(b, c)
}
// CredentialsConfig holds third-party provider credentials at the tenant level.
// Stored as a single JSONB column; each provider is a nested object so new
// providers can be added without schema changes.
type CredentialsConfig struct {
WeKnoraCloud *WeKnoraCloudCredentials `json:"weknoracloud,omitempty"`
}
// WeKnoraCloudCredentials stores WeKnoraCloud AppID and AppSecret.
// AppSecret is AES-256 encrypted before persisting to database.
type WeKnoraCloudCredentials struct {
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
}
// GetWeKnoraCloud returns the WeKnoraCloud credentials, or nil if not configured.
func (c *CredentialsConfig) GetWeKnoraCloud() *WeKnoraCloudCredentials {
if c == nil || c.WeKnoraCloud == nil {
return nil
}
if c.WeKnoraCloud.AppID == "" || c.WeKnoraCloud.AppSecret == "" {
return nil
}
return c.WeKnoraCloud
}
// Value implements the driver.Valuer interface for CredentialsConfig
func (c *CredentialsConfig) Value() (driver.Value, error) {
if c == nil {
return nil, nil
}
cp := *c
if cp.WeKnoraCloud != nil && cp.WeKnoraCloud.AppSecret != "" {
if key := utils.GetAESKey(); key != nil {
if encrypted, err := utils.EncryptAESGCM(cp.WeKnoraCloud.AppSecret, key); err == nil {
cp.WeKnoraCloud = &WeKnoraCloudCredentials{AppID: cp.WeKnoraCloud.AppID, AppSecret: encrypted}
}
}
}
return json.Marshal(cp)
}
// Scan implements the sql.Scanner interface for CredentialsConfig
func (c *CredentialsConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return nil
}
if err := json.Unmarshal(b, c); err != nil {
return err
}
if c.WeKnoraCloud != nil && c.WeKnoraCloud.AppSecret != "" {
if key := utils.GetAESKey(); key != nil {
if decrypted, err := utils.DecryptAESGCM(c.WeKnoraCloud.AppSecret, key); err == nil {
c.WeKnoraCloud.AppSecret = decrypted
}
}
}
return nil
}
// ParserEngineConfig holds tenant-level overrides for document parser engines (e.g. MinerU endpoint, API key).
// These values take precedence over environment variables when parsing documents.
type ParserEngineConfig struct {
// docreader 凭证
DocreaderAppID string `json:"docreader_app_id,omitempty"`
DocreaderAPIKey string `json:"docreader_api_key,omitempty"`
MinerUEndpoint string `json:"mineru_endpoint"` // MinerU 自建服务端点
MinerUAPIKey string `json:"mineru_api_key"` // MinerU 云 API Key
@@ -262,9 +323,6 @@ func (c *ParserEngineConfig) ToOverridesMap() map[string]string {
return nil
}
m := make(map[string]string)
if c.DocreaderAppID != "" {
m["docreader_app_id"] = c.DocreaderAppID
}
if c.MinerUEndpoint != "" {
m["mineru_endpoint"] = c.MinerUEndpoint
}

View File

@@ -6,8 +6,3 @@ type WeKnoraCloudStatusResult struct {
NeedsReinit bool `json:"needs_reinit"` // 是否需要重新初始化(凭证损坏)
Reason string `json:"reason,omitempty"` // 需要重新初始化的原因
}
type DocreaderCredentials struct {
AppID string
APIKey string
}

View File

@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS tenants (
web_search_config TEXT DEFAULT NULL,
parser_engine_config TEXT DEFAULT NULL,
storage_engine_config TEXT DEFAULT NULL,
credentials TEXT DEFAULT NULL,
chat_history_config TEXT,
retrieval_config TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

View File

@@ -0,0 +1 @@
ALTER TABLE tenants DROP COLUMN IF EXISTS credentials;

View File

@@ -0,0 +1,5 @@
-- Description: Add credentials column to tenants for third-party provider credentials (e.g. WeKnoraCloud AppID/AppSecret).
DO $$ BEGIN RAISE NOTICE '[Migration 000035] Adding credentials column to tenants'; END $$;
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS credentials JSONB DEFAULT NULL;
COMMENT ON COLUMN tenants.credentials IS 'Third-party provider credentials (e.g. WeKnoraCloud AppID/AppSecret); encrypted at application level';