ae7cd02cd6735ffaf925ffec6fe3205cc6f92c9d
321 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f7150cb5c |
feat(rbac): tenant member management + invite_only gate (PR 3 of 3, #1303)
Wire the human-facing surface for the tenant-RBAC system that PR 1 introduced and PR 2 enforced: REST endpoints + UI for managing tenant members, plus the long-pending invite_only registration gate. Backend - internal/handler/auth.go: Register now rejects with 403 when auth.registration_mode=invite_only. PR 1 already exposes the mode via /auth/config so the frontend hides the sign-up link; this is the server-side enforcement for direct API calls. - internal/handler/tenant_member.go: new TenantMemberHandler with ListMembers / AddMember / UpdateMemberRole / RemoveMember. Maps service-layer sentinels onto HTTP semantics (ErrLastOwner -> 409, ErrMembershipAlreadyExists -> 409, ErrUserNotFound on AddMember -> 404, ErrInvalidTenantRole -> 400). AddMember requires the invitee to already have an account; SMTP-driven email invites are deferred. - internal/types/tenant_member.go: new TenantMemberResponse projection carrying the fields the management UI renders (email, username, avatar, role, joined_at). - internal/router/router.go: four routes under /tenants/:id/members gated per the matrix (Viewer+ for list, Owner+ for every mutation). - internal/container/container.go: dig provider for the new handler. Tests - handler/tenant_member_test.go: 12 table-driven cases covering happy path, dangling-user tolerance, ErrLastOwner -> 409, ErrMembershipNotFound -> 404, ErrMembershipAlreadyExists -> 409, unknown email -> 404, invalid role rejected before user lookup, bad tenant id -> 400. - handler/auth_register_invite_only_test.go: invite_only -> 403, self_serve -> 201, nil Auth config falls through. Frontend - src/api/tenant/members.ts: typed client (TenantRole/TenantMember/...) for the four endpoints. - src/views/settings/TenantMembers.vue: members table with add dialog, inline role select, confirm-remove modal. All mutation widgets gated behind currentTenantRole === 'owner'; server still enforces. - src/views/settings/Settings.vue: register the Members tab; show only when currentTenantRole is loaded (avoids tab flash on cold nav). - i18n in en-US/zh-CN/ko-KR/ru-RU: new tenantMember namespace covering all UI strings and structured error messages (lastOwner / userNotFound / alreadyMember / invalidRole etc). This completes the #1303 RBAC rollout. With cfg.Tenant.EnableRBAC=true operators now have a full management surface; with EnableRBAC=false the routes still exist but mutations log-and-pass per PR 2 contract. |
||
|
|
8b71def60b |
fix(rbac): address PR 2 review feedback
Middleware - RequireOwnershipOrRole now skips the creator lookup entirely when EnableRBAC is off, eliminating a hidden per-request DB roundtrip in the rollout window. - New ErrResourceNotFound sentinel lets lookups signal "not visible to this tenant"; the middleware passes the request through so handlers can return a real 404 instead of a misleading 403. - Transient lookup failures now surface as 503 (was 403) so monitoring can tell denied-by-policy from cannot-verify-right-now. - Cross-tenant superusers (CanAccessAllTenants) bypass tenant role gates, restoring DELETE /tenants/:id and ResetAPIKey for org operators that resolve to Admin in foreign tenants. - Construction-time warning fires once when cfg is nil/incomplete, so a wiring bug that disables enforcement globally is visible at boot. Route coverage - Add role guards to FAQ, KnowledgeTag, WikiPage, Skill, WebSearch, WebSearchProvider, WeKnoraCloud, Session, Chat, and Message route groups so the matrix actually matches the PR description. Service layer - KB and agent creators are no longer recorded as the synthetic system-<tenantID> user used by the X-API-Key path; resources stay tenant-owned. New types.IsSyntheticUserID helper centralises the prefix check. - KBCreatorLookup and AgentCreatorLookup re-check tenant scope so a same-ID resource in another tenant cannot piggyback through the ownership shortcut. Docs - CustomAgent.RunnableByViewer comment now matches enforcement: it only blocks Viewer-driven agent-mode runs, not plain RAG/Wiki QA. Tests - New cases for the 503/404 paths, lookup-skip behaviour, cross-tenant superuser bypass, the lookup helpers (not-found, cross-tenant, built-in agent, missing tenant context), and IsSyntheticUserID. |
||
|
|
af23483433 |
fix(auth): honour JWT tenant_id claim in middleware
POST /auth/switch-tenant mints an access token whose `tenant_id` claim
points to the target tenant, but the middleware previously discarded
that claim entirely:
user, err := userService.ValidateToken(ctx, token) // returned only *User
targetTenantID := user.TenantID // always the home tenant
The session therefore stayed scoped to the user's home tenant despite
the supposedly fresh token, making the new endpoint a no-op once
the request hit any tenant-scoped query.
This change widens the contract:
- UserService.ValidateToken now returns (user, activeTenantID, error)
where activeTenantID is the JWT's `tenant_id` claim, falling back to
user.TenantID when the claim is absent (backward compatibility with
pre-RBAC tokens still in the wild).
- middleware/auth.go uses activeTenantID as the default target before
applying any X-Tenant-ID header override, and recomputes
crossTenantSwitch from `activeTenantID != user.TenantID` so the
resolveTenantRole branches stay correct for switch-tenant sessions.
- The non-middleware /auth/validate handler in handler/auth.go absorbs
the new return value (it only cares about user identity).
Tests:
- internal/application/service/user_jwt_test.go pins the JWT->tenant-id
projection (`tenantIDFromClaims`) — claim present beats fallback,
zero/negative/string claims fall back, int64/uint64 are accepted for
test-built tokens.
Required for PR 2 multi-tenant integration tests, where a single user
needs to switch between Owner-in-A and Viewer-in-B sessions and observe
distinct authority on each side.
|
||
|
|
f3d1210577 |
refactor(rbac): address PR 1 review feedback
Tighten correctness, security and observability of the tenant RBAC foundation introduced in 3c4cdd70 in response to internal review. Bugfixes - custom_agents.runnable_by_viewer: drop the GORM `default:true` tag. GORM treats a plain bool's zero value as "unspecified" and silently rewrites it to the column default on insert, which would prevent admins from ever creating an agent with viewer-runtime disabled. The column-level DEFAULT TRUE on the database side still backstops rows inserted by raw SQL / migrations. - TenantMemberRepository.HasAnyMembers: replace `Limit(1).Count(&n)` with `Take(&probe)`. GORM's Count ignores Limit and scans the whole table, which matters because this runs on the auth middleware's hot path for users without a cached membership. - service/user.LoginWithOIDC: include User, Tenant and Memberships in the OIDCCallbackResponse so the OIDC flow returns the same shape as password login (previously User and friends were left nil). Security - middleware/auth.resolveTenantRole: restrict orphan-tenant auto-promotion to the caller's home tenant. Cross-tenant superuser visits and JWTs minted for foreign tenants no longer trigger silent Owner grants. Two regression tests pin the new behaviour. - service/user.synthFallbackMembership: default the synthesised role to Viewer instead of Owner. The login response only feeds UI; the backend re-derives the real role from tenant_members on every request, so showing a Viewer UI is preferable to a misleading Owner UI that would surface admin controls the backend will then 403. - migrations/versioned/000043_tenant_rbac.down.sql: gate the destructive drop behind a `weknora.allow_destructive_migration` GUC so an accidental `migrate down` cannot wipe role data in production. - frontend/src/stores/auth.ts: comment currentTenantRole as rendering-only so future PRs cannot mistake the localStorage-backed value for an authorisation signal. API contract - Memberships in LoginResponse and OIDCCallbackResponse now serialises without omitempty, and buildMembershipsForUser / synthFallbackMembership always return a non-nil slice — frontend can rely on JSON `[]`. - handler/auth.GetCurrentUser includes memberships so /auth/me refreshes recover currentTenantRole after a page reload. Performance - Add TenantService.GetTenantsByIDs (with the matching repo method) and use it inside buildMembershipsForUser to batch tenant-name lookups instead of issuing one query per membership. Observability - Replace the remaining `log.Printf` calls in middleware/auth with contextual logger.Warnf / logger.Infof so role resolution events ride the standard request-id / Langfuse trace plumbing. The successful auto-promote line is tagged `[audit]` for log mining. Tests - internal/application/service/tenant_member_test.go: hand-rolled fakeTenantMemberRepo plus 11 cases covering AddMember validation, EnsureOwner idempotence, "cannot demote/remove the last Owner", same-role no-op, ErrInvalidTenantRole / ErrMembershipNotFound, and the TenantRole.HasPermission lattice. - internal/middleware/auth_role_test.go: fakeMemberService plus 8 cases pinning every branch of resolveTenantRole, including the two H1 regressions (cross-tenant superuser must not auto-promote; orphan auto-promote requires the home tenant). |
||
|
|
7f7a48880f |
feat(rbac): add tenant_members schema and auth wiring (PR 1 of 3, #1303)
Lay the foundation for tenant-level RBAC without enforcing it yet. Schema, types, middleware lookup, and login-flow shape changes ship now; RequireRole/route annotations and member-management endpoints follow in PR 2 and PR 3. Highlights: - New tenant_members table (Postgres + SQLite) with (user_id, tenant_id) unique key and (owner|admin|contributor|viewer) role values. Migration 000043 backfills one row per existing user, marking the earliest-created active user in each tenant as Owner and the rest as Contributor. - KnowledgeBase.creator_id and CustomAgent.runnable_by_viewer added so PR 2 can wire ownership-based and Viewer-runtime checks. - Auth middleware loads the active TenantMember row, sets a new TenantRoleContextKey, and falls back via three escape hatches: cross-tenant superusers (CanAccessAllTenants) get Admin in the target tenant; orphan tenants (zero members) auto-promote the first authenticating human to Owner; and when tenant.enable_rbac is false (the v1 default) unmatched lookups fail open with Admin to preserve current behaviour. - Register / OIDC provisioning now insert an Owner membership for the registrant via TenantMemberService.EnsureOwner. - LoginResponse renamed tenant -> active_tenant and gained a memberships array. POST /auth/switch-tenant issues a fresh token pair scoped to a target membership; GET /auth/config exposes auth.registration_mode for the frontend register-tab gating. - New config keys: auth.registration_mode (self_serve | invite_only, defaults to self_serve) and tenant.enable_rbac (defaults to false). Both have WEKNORA_* env overrides. - Frontend store persists memberships and exposes currentTenantRole; Login.vue calls /auth/config and hides the Register tab in invite_only mode. Go SDK types updated to match the new LoginResponse shape. |
||
|
|
0e8de6192c |
feat(knowledge-base): validate vector store bindings on create, copy, and delete
Wires KnowledgeBase.VectorStoreID and the ownership-aware retrieve factory into the user-facing knowledge-base lifecycle: - POST /knowledge-bases validates the requested vector_store_id against the caller's tenant scope and the engine registry. New error codes ErrVectorStoreBindingInvalid (2200) and ErrVectorStoreUnavailable (2201) distinguish the typed branches without echoing UUIDs to the client. - GET / POST / PUT / PUT-pin responses embed the bound store's display metadata (name, source, engine_type, status) without exposing any connection credentials. Cross-tenant shared KBs receive a suppressed payload (vector_store_id stripped, source="shared") so operator-chosen store names cannot be enumerated across tenants. - POST /knowledge-bases/copy synchronously rejects clones whose target has a different embedding model or vector store, before the async clone task is enqueued. The async clone worker re-applies the same checks for defense in depth. - DELETE /vector-stores/:id refuses to remove a store with bound KBs, inside a transaction that row-locks the store on PostgreSQL and serializes via WAL on SQLite. unregister-from-registry is wrapped in defer/recover so a panic surfaces as a structured warning instead of silently leaking a stale engine. - vector_store_id is immutable after creation. The GORM <-:create tag blocks every ORM update path; the service-layer DTO omits the field entirely; a reflection-based regression test catches any future maintainer who adds it back to either layer. - Empty-string vector_store_id is normalized to nil at both the create path and inside SharesStoreWith, so rows persisted by callers that did not run Normalize first cannot trip false same-store comparisons. Part of #993. Depends on #994 and #1310. |
||
|
|
dc634e863a |
feat(crypto): AES-GCM at-rest for MCP and DataSource credentials with graceful key-rotation handling
Two encryption gaps and one fail-loud regression, fixed together because
they are inseparable in practice.
At-rest encryption gaps
-----------------------
Before this change, only Tenant.APIKey, Tenant.Credentials.WeKnoraCloud.AppSecret,
Model.Parameters.APIKey/AppSecret, and WebSearchProviderParameters.APIKey
were encrypted with SYSTEM_AES_KEY. MCP and DataSource were missed:
- MCPAuthConfig.Value/Scan went through bare json.Marshal/Unmarshal, so
api_key and token were written to mcp_services.auth_config (json) in
plaintext regardless of whether SYSTEM_AES_KEY was set.
- DataSource.Config (types.JSON) is a byte passthrough, so connector
secrets (Notion token, GitHub PAT, Feishu app_secret, ...) inside the
Credentials map were stored in plaintext too.
Now both follow the ModelParameters / WebSearchProviderParameters pattern.
Encryption operates on a local copy to avoid mutating the caller's
in-memory struct (otherwise the next read of the same *MCPAuthConfig
would surface ciphertext as if it were the API key). For DataSource the
ToJSON ↔ ParseConfig pair is the only application boundary that
materializes credentials, so encrypting there is sufficient and avoids
having to fork the generic types.JSON.
Backward compatibility
----------------------
DecryptStoredSecret returns rows without the enc:v1: prefix verbatim, so
historical plaintext records keep working. Next write re-saves them
encrypted. No migration step.
Graceful Scan-time degradation
------------------------------
The strict "fail loud on key rotation" behaviour of DecryptStoredSecret
turned into a footgun once it ran from GORM Scan: a single broken row
caused ListModels / ListMCPServices / ListDataSources to return zero
results, hiding all other rows and giving the operator no way to find
which credential needed re-entering.
New utils.DecryptStoredSecretLenient: same logic, but on failure returns
("", false). The strict variant is kept for hot paths that are about to
actually USE the credential (so a rotated key surfaces loudly there
rather than silently degrading to "missing api key").
All Scan paths switch to lenient + per-failure log line:
ModelParameters.Scan, WebSearchProviderParameters.Scan,
MCPAuthConfig.Scan, DataSource.ParseConfig, CredentialsConfig.Scan.
On lenient failure the field blanks out (never ciphertext), the row
still loads, the DTO reports configured=false so the UI shows "not
configured", and any upstream call attempted with the empty value fails
with a clear "missing api key" rather than sending ciphertext as the key.
Tenant.AfterFind (Tenant.APIKey) intentionally stays strict — it is the
primary HTTP auth credential and a silent empty value would change auth
semantics ambiguously.
|
||
|
|
7643dd457e |
refactor(credentials): introduce /credentials subresource pattern
Replace the legacy "redacted placeholder + Clear* boolean" pattern with
dedicated per-resource credential subresources across MCP services,
Models, Web search providers, and Data sources.
Why
---
The previous design had three problems:
1. Main PUT body carried secret fields. The frontend echoed back a
redacted "***" placeholder, and a fragile MergeUpdate / IsRedactedOrEmpty
defense in the service layer tried to detect "user did not change this"
vs "user wants to clear this". A regression in that defense (or a new
frontend forgetting it) silently overwrites the stored secret with the
placeholder.
2. The "remove this credential" UX was a red checkbox under a pre-filled
password input. Three intents (preserve / replace / clear) collapsed
onto one field, and credential changes were bundled with unrelated
config edits in the same submit. Users wiped working keys by mistake.
3. Secret presence was inferred from "did the response come back with a
'***' placeholder", which couples the contract to a magic string.
Design
------
Each of the four resources now exposes:
PUT /{resource}/{id}/credentials # write one or more fields
DELETE /{resource}/{id}/credentials/{field} # clear a single field
"Is this configured?" metadata travels on the main resource response as
a typed map (dto.MCPServiceResponse.Credentials etc.) — no separate GET
endpoint. The frontend reads the configured boolean from the main GET
and never sees secret values at all.
Main PUT endpoints now ignore any secret fields in the request body and
log a deprecation warning if they appear, so legacy clients fail loudly
rather than overwriting silently.
Frontend
--------
- New reusable <CredentialResource> component renders a three-state card
(unconfigured / configured / editing) and drives the dedicated
endpoints. Used by MCP, Model, Web search; DataSource has a bespoke
card with the same behaviour because its credentials are a single
atomic per-connector map.
- Cancel from edit mode now restores state synchronously from the meta
prop. The previous async refresh() was a no-op while state was still
'editing', leaving the input frozen open.
- Remove is single-click + toast. The danger-themed button is the
deterrent; a modal confirm adds friction without adding safety (the
plaintext is irrecoverable client-side either way — recovery means
re-typing).
DTOs (internal/handler/dto/) are deliberately separate from the GORM
models so "no secret in response" is a compile-time invariant: a future
contributor cannot leak a secret without explicitly adding the field to
the DTO, which is review-able in a single diff.
Storage is unchanged — credentials still live in the existing jsonb /
parameters columns. No schema migration.
Cleanup
-------
- types.MCPService / types.Model / types.WebSearchProviderEntity /
types.DataSourceConfig: drop ClearAPIKey / ClearToken / ClearAppSecret
/ ClearCredentials boolean fields, MergeUpdate(), RedactSensitiveData().
- utils/types/secret.go: drop PreserveIfRedacted / IsRedactedOrEmpty.
RedactedSecretPlaceholder constant is retained because VectorStore
still uses the old pattern and is out of scope here.
- Frontend hasExistingApiKey / clearApiKey / convertToLegacyFormat
redaction handling removed; i18n keys renamed secret -> credential.
|
||
|
|
d869cc1ff6 |
fix: redact sensitive fields and prevent credential loss on edit
Closes #988 Unify the "write-only secrets" pattern across MCP services, Models, WebSearch providers, and DataSources. Fix two related defects: 1. GET responses leaked plaintext credentials — GetMCPServiceByID while List masked, all Model / WebSearchProvider / DataSource GET responses unconditionally. 2. When the frontend echoed a masked placeholder back on save, the update path overwrote the stored secret with the mask — silently wiping the real credential. MCP surfaced this most visibly: ListMCPServices masked Token to "first4****last4" and McpServiceDialog round-tripped that masked string back into the DB. - internal/types/secret.go introduces the shared sentinel RedactedSecretPlaceholder = "***" and two helpers: - IsRedactedOrEmpty(s): true for "" or the sentinel - PreserveIfRedacted(incoming, existing): merge predicate - Each resource adds a RedactSensitiveData (mutates-in-place) or Redacted (returns-a-copy) method and a MergeUpdate contract: empty or placeholder preserves, any other value replaces, and a write-only Clear* bool flag (gorm:"-", json omitempty) wipes with precedence over preserve / replace. Clear* never persists. - Every GET / List / Create / Update response path now redacts. VectorStore's pre-existing "***" literal is refactored onto the shared constant for consistency. All four dialogs — McpServiceDialog, ModelEditorDialog, WebSearchSettings, DataSourceEditorDialog — never pre-fill secret inputs. The stored-state signal is collapsed into the input placeholder: "•••••••• (Enter new value to replace)" when the server reports a stored value, falling back to the connector-specific placeholder otherwise. The input itself carries the "something is there" cue, matching how password dots render. An opt-in "Remove this credential" checkbox appears in edit mode when a value is stored; checking it disables the input and triggers a danger-themed confirmation dialog before submit. DataSource rolls the clear up to a connector-level "Remove all credentials" flag since its credentials are per-connector atomic bundles (e.g. Feishu app_id + app_secret). i18n updates across en-US, zh-CN, ko-KR, ru-RU: secret.storedPlaceholder / clearHint / confirmClearTitle / confirmClearBody. - MCP update handler's manual map-to-struct projection was silently dropping clear_api_key / clear_token / custom_headers on partial updates. Pull them off the map explicitly. - MCP configChanged detection now snapshots URL / StdioConfig / TransportType before the merge so connection recycling still triggers on real transport changes (not just whenever AuthConfig is present). - MCP CustomHeaders: nil preserves existing, non-nil replaces. Was always overwriting, silently wiping headers on any auth-bearing update. - GetMCPServiceByID now redacts (was plaintext while List masked). - WebSearchProvider update preserves Name / Description when empty instead of clobbering them. - WebSearchProvider TestProviderRaw rejects the "***" placeholder up front with an actionable message rather than forwarding it to the upstream provider API as a literal key. - Model builtin hideSensitiveInfo now preserves non-secret metadata (SupportsVision, Provider, ExtraConfig, InterfaceType, ParameterSize, EmbeddingParameters) that were previously zeroed out for builtin model cards. - Replace local equalStringSlices / equalStringMaps helpers with stdlib slices.Equal / maps.Equal (Go 1.21+). - internal/types/{secret,mcp,model,web_search_provider,datasource}_test.go: table-driven coverage of merge, redact, clear, and write-only flag invariants for every resource. - internal/application/service/mcp_service_test.go: full service layer with a fake repo; covers ListMCPServices / GetMCPServiceByID redaction, UpdateMCPService partial-update honoring of Clear*, CustomHeaders preserve-on-nil / replace-on-non-nil, and clear-flag precedence over submitted values. Additive only. No changes to request or response JSON shape beyond the write-only Clear* flags (omitted by default via omitempty). No DB schema changes. No SDK struct field removals. Existing clients that don't know about the new flags continue to function unchanged. - DataSource.Config jsonb at-rest encryption. Pre-existing residual risk; tracked as a follow-up. - Renaming VectorStore.MaskSensitiveFields to match the new RedactSensitiveData / Redacted naming convention. Left alone to minimize diff; a follow-up rename PR can unify. |
||
|
|
8f4e5a459f | fix(session): scope session access by user | ||
|
|
7478e3cddb |
refactor(retriever): introduce factory for KB-scoped retrieve engine resolution
Extracts the 25 repeated NewCompositeRetrieveEngine call sites across seven services into two factory functions with tenant-ownership verification: CreateRetrieveEngineForKB (synchronous) and CreateRetrieveEngineFromPayload (async task handlers). Promotes GetByStoreID from concrete-only to interfaces.RetrieveEngineRegistry. Extends KBDeletePayload / IndexDeletePayload with an omitempty VectorStoreID snapshot. Design highlights - Factory verifies tenant ownership of the resolved store (defense-in-depth) so a gap in PR3 validation or a tampered Asynq payload cannot cross tenants. Cross-tenant attempts return ErrVectorStoreForbidden with a structured log entry. - Sentinel errors (ErrTenantInfoMissing, ErrVectorStoreNotFound, ErrVectorStoreForbidden) let async handlers classify non-retryable failures as asynq.SkipRetry. - nil and empty-string pointers for vectorStoreID normalize to "unbound" so callers never send an empty UUID to GetByStoreID. - resolveBoundEngine constructs engineInfos directly (with slices.Clone of Support()) instead of going through NewCompositeRetrieveEngine. The latter only reads from the byEngineType map and cannot reach DB stores in byStoreID -- multiple stores can share the same engine type. Unbound fallbacks still reuse NewCompositeRetrieveEngine so the tenant effective-engines path is byte-for-byte unchanged. - DeleteKnowledgeBase now reads the KB before soft-delete so the enqueued KBDeletePayload carries a VectorStoreID snapshot; once the row is soft-deleted GORM's default scope hides it. Cost: +1 SELECT per KB-delete request (a rare admin operation). - enqueueIndexDeleteTask gains a vectorStoreID *string parameter that the single caller (tag.go) populates from the owning KB. The worker (ProcessIndexDelete) validates ownership via the factory. - cleanupKnowledgeResources loads the KB so bound-store cleanup routes correctly. If the load fails it warns and falls back to tenant effective engines; orphan vectors become observable in logs instead of disappearing silently. - DeleteKnowledges (batch) stays on the tenant effective-engines path -- batches can span stores and multi-store fan-out is PR4 scope. Noted inline. Merge safety All knowledge bases have VectorStoreID = NULL at merge time (introduced nullable in PR1, Tencent/WeKnora#994). Every factory call falls back to NewCompositeRetrieveEngine(registry, tenantInfo.GetEffectiveEngines()), which is the existing code path for these KBs. Async payloads from before this change decode to VectorStoreID = nil (omitempty) and take the same fallback. Testing - go build ./... - go vet ./... - go test -race -count=1 ./internal/application/service/retriever/... covers unbound (nil/empty), bound, cross-tenant, unregistered store, ownership infra error, legacy payload shapes (missing field/null/ empty string/nil), tampered payload, and parallel invocation. - grep -r NewCompositeRetrieveEngine internal/application/service returns zero results outside factory.go/composite.go. Refs: Part of Tencent/WeKnora#993. Depends on Tencent/WeKnora#994. |
||
|
|
b00bc84f35 |
fix(agent): pass reasoning_content back to providers that require it
MiMo and DeepSeek V3.2/V4 reject multi-turn requests in thinking mode when the prior assistant message lacks reasoning_content with HTTP 400: "The reasoning_content in the thinking mode must be passed back to the API." Agent's ReAct loop is the worst-case scenario — every round produces tool calls, exactly the case DeepSeek's docs specify reasoning_content MUST participate in subsequent context. Plumb reasoning_content through the full assistant-message round-trip: - chat.Message / types.ChatResponse / types.AgentStep gain a reasoning_content field (AgentStep persists via the existing Message.AgentSteps jsonb column, no migration needed). - streamLLMToEventBus accumulates reasoning chunks into result.ReasoningContent and surfaces it on the round's ChatResponse. - engine.runReActIteration writes it onto AgentStep so cross-turn replay preserves it. - observe.appendToolResults attaches it to the same-turn assistant message; agent_history.buildAssistantHistoryMessages does the same on cross-turn replay. - RemoteAPIChat.ConvertMessages forwards it on assistant turns to openai.ChatCompletionMessage.ReasoningContent (already supported by go-openai); providers that don't recognize the field ignore it. Tests cover the three boundaries: ConvertMessages serializes it for upstream, appendToolResults preserves it within the same turn, and buildAssistantHistoryMessages replays it across turns. Scope is intentionally limited to Agent mode — KnowledgeQA's chat pipeline and Anthropic's signed thinking_blocks are separate fixes that require schema changes (rendered_content / thinking_blocks columns). Fixes #1302 |
||
|
|
64b20a2d87 |
feat(agent): support dedicated model for query understanding step
Quick-answer (RAG) agents can now configure a separate chat model for the query-understanding stage (rewrite + intent classification), decoupling it from the main conversation model so users can route the lightweight rewrite call to a cheaper / faster model. - Add CustomAgentConfig.QueryUnderstandModelID and plumb it through PipelineRequest and ChatManage.Clone. - query_understand plugin prefers QueryUnderstandModelID on the text-only path; falls back to ChatModelID if the configured model cannot be resolved (with a warn log). Multimodal path is unchanged to keep vision-capable model selection intact. - AgentEditorModal exposes a ModelSelector under the existing Query Rewrite block; empty means reuse the main chat model. - Add i18n strings (queryUnderstandModel / placeholder / desc) for zh-CN, en-US, ko-KR, ru-RU. - Extend Go SDK AgentConfig with the new field. |
||
|
|
3f9b09e306 |
fix(pipeline): make data-analysis stage opt-in per agent (#1244)
The legacy in-pipeline DuckDB SQL data-analysis stage used to run on every quick-answer RAG request whose retrieved chunks included a CSV/Excel file, adding one extra LLM round-trip (~3s) to generate a SQL query that most plain Q&A users never wanted. There was no way to disable it. Introduce a per-agent DataAnalysisEnabled flag (default off), wire it through PipelineRequest, and gate the DATA_ANALYSIS stage on it. Surface the toggle in the agent editor for quick-answer agents with at least one knowledge base attached. Refs: https://github.com/Tencent/WeKnora/issues/1244 |
||
|
|
cacca049d9 |
feat(knowledge-base): document list filters and explicit batch-management UX
Add three new optional filters to the document list under a knowledge base detail page — parse status, source/channel, and updated time range — and rework multi-select to no longer cause the card title to jitter on hover. Backend - Introduce types.KnowledgeListFilter to aggregate optional filter dimensions (tag, keyword, file_type, parse_status, source, updated_from/to) and switch ListPagedKnowledgeByKnowledgeBaseID (repository/service/interface) to accept it instead of a growing positional parameter list. - The ListKnowledge HTTP handler accepts new parse_status, source, start_time and end_time query params; time params accept RFC3339, "YYYY-MM-DD HH:MM:SS" and "YYYY-MM-DD". The repository routes source="manual"/"url" onto the type column to stay consistent with file_type semantics; other source values match the channel column. - Update the four other callers (agent_service, initialization) to pass an empty filter struct, preserving prior behavior. Frontend - Add three controls in the doc-filter-bar (status select, source select, date-range picker with future-date disabled) wired through getKnowled / listKnowledgeFiles into the new backend params. - Replace the hover-triggered card checkbox with an explicit "批量管理" mode (mirrors the session list UX): in card view the checkbox only renders while batch mode is on, entered via the per-card "..." menu; the list view keeps its leading checkbox column. Switching from list to grid auto-enables batch mode when something is already selected, so the selection stays visible. - DocumentBatchBar now stays open whenever batch mode or selection > 0, and its "取消选择" button both clears the selection and exits batch mode. API surface sync - Regenerate Swagger artifacts (docs/docs.go / swagger.json / swagger.yaml). - Update docs/api/knowledge.md with the new query parameters. - Add backward-compatible ListKnowledgeWithFilter + KnowledgeListFilter to the Go SDK; the existing ListKnowledge keeps its signature. i18n - New filter labels in zh-CN / en-US / ko-KR / ru-RU; reuse existing menu.batchManage / batchManage.cancel for the multi-select strings. |
||
|
|
1f5970b67e |
refactor(agent): rebuild multi-turn history from DB, drop llmcontext layer
The Agent ran a parallel context cache (Redis/in-memory) on top of the messages table to feed multi-turn history into the LLM. That dual-write caused subtle drift (e.g. compression diverging from DB, system-prompt swaps lost on restart) and required a separate ClearContext path on session/IM clear. Make the messages table the single source of truth: - Add service.LoadAgentHistory: rebuilds chat.Message history per turn from the persisted messages, expanding AgentSteps into proper OpenAI assistant_with_tool_calls + tool messages and replaying the canonical final answer (with <think> blocks stripped). final_answer tool calls are filtered to avoid duplicating the trailing answer. - Make AgentEngine stateless across turns: drop ContextManager / sessionID cache plumbing from the engine, agent service, and CreateAgentEngine signature. The engine only uses sessionID for logging/event emission. - Wire AgentQA to load history from DB on demand using HistoryTurns (default 5) when MultiTurnEnabled, otherwise run with empty history. - Delete the llmcontext package (ContextManager interface, Redis/memory storage, factory) and the SessionService.ClearContext API path; IM /clear and session message clear no longer need to invalidate cache. Behavior preserved: KnowledgeQA-mode replay is unchanged (turns with empty AgentSteps just produce the single canonical assistant message), and Agent-mode turns now consistently see prior tool calls and results. |
||
|
|
1110615300 |
feat(web-search): add SearXNG provider (#1166)
支持对接自建/公共 SearXNG 实例作为网络搜索引擎,缓解免费搜索引擎在国内 网络环境下访问受限的问题。 - types: 新增 WebSearchProviderTypeSearxng 与 BaseURL 参数字段; 类型元数据新增 RequiresBaseURL,前端可动态渲染 Instance URL 表单。 - infrastructure/web_search/searxng.go: 调用 /search?format=json,强制 utils.ValidateURLForSSRF 校验 base_url,可选 api_key 透传给反代鉴权。 - service: isValidProviderType 与参数校验接入 searxng。 - container: 注册 NewSearxngProvider 工厂。 - frontend: WebSearchSettings 表单根据 requires_base_url 渲染 Instance URL 输入框;编辑回填、free 判定同步更新。 - docker: 新增可选 searxng 服务(profile=searxng/full),附带最小化 settings.yml(启用 JSON 格式、关闭 limiter、关闭遥测), docker-compose 默认 SSRF_WHITELIST 包含 searxng 容器名。 - .env.example: 补充 SEARXNG_PORT / SEARXNG_SECRET 说明。 Closes #1166 |
||
|
|
5510ea8f5a |
feat(agent): human-in-the-loop approval for MCP tool calls (#1173)
Add an opt-in human approval gate so Agent runs pause before executing MCP tools that operators flag as dangerous, surface an approval card in the chat UI, and only resume after the user approves (optionally with edited args) or rejects. Backend - New mcp_tool_approvals table + repo/service to mark per-tool approval required (PG migration 000042 + sqlite init). - approval.Gate coordinates RequestAndWait / Resolve with sync.Once delivery, configurable timeout, and Redis Pub/Sub fan-out so multi- replica deployments work without sticky sessions. - MCPTool.Execute integrates the gate; uses a round-level ApprovalCtx (without the per-tool 60s timeout) for the wait, and re-derives a fresh 60s exec ctx after approval so CallTool keeps a full window. - New SSE response types (tool_approval_required / _resolved) and EventBus events plumb approval state to AgentStreamDisplay. - REST: list/set per-tool approval flag, resolve pending approval. - Configurable via agent.tool_approval_timeout_seconds (yaml) or WEKNORA_AGENT_TOOL_APPROVAL_TIMEOUT env (accepts seconds or Go duration). Frontend - MCP settings: per-tool "require approval" switch on the test panel. - Chat: ToolApprovalCard renders the pause point with editable JSON args, validation feedback, mm:ss countdown that turns warning/danger near deadline, and a resolved state that retains context. - i18n strings added for zh-CN / en-US / ko-KR / ru-RU. Docs - docs/zh/mcp-approval.md covering behavior, config, API, deployment considerations (Redis cross-instance, restart limitations). |
||
|
|
02be05c095 | feat: support Tencent VectorDB keyword retrieval | ||
|
|
04e8105728 |
perf(wiki-ingest): scale to 4w-doc KBs + generic task queue / dead-letter
The wiki ingest post-process pipeline OOM'd and ran for hours on KBs
with ~40k documents. The dominant tail was a per-batch ListAllPages
that pulled every page (multi-MB content blobs) into Go memory, plus a
24h-TTL Redis pending list whose data could be evicted before a long
serialized backlog drained. dedup did O(P × N) Jaccard scoring in Go
on every batch. Lint loaded the full graph. The index page kept the
entire wiki directory in its content column and rewrote the TOAST
chunk on every ingest. None of these survive at 4w docs.
This change reworks the write path end to end and pulls the durable-
queue + dead-letter primitives out of wiki and into shared
infrastructure that every asynq task type now benefits from.
- migration 000041: task_pending_ops + task_dead_letters tables, plus
three GIN indexes on wiki_pages (source_refs jsonb_path_ops,
source_refs text fallback, lower(title) trgm).
- TaskPendingOpsRepository + TaskDeadLetterRepository in
internal/application/repository/task_queue.go: cursor-paginated
list, atomic IncrFailCount via UPDATE…RETURNING, dedup-key scoped
delete that refuses empty keys so a buggy caller can't wipe a KB's
queue.
- internal/middleware/asynqdl: writes a task_dead_letters row when an
asynq task exhausts its retry budget. Payload-agnostic — a small
probe struct extracts TenantID + scope hints across every existing
payload type, so summary:generation / image:multimodal /
faq_import / kb:clone / etc. all dead-letter without per-handler
code. Best-effort: an insert failure never masks the underlying
task error. Installed in router/task.go before the langfuse mw so
it sees raw errors.
- Pending queue moved Redis → PG (no TTL, restart-durable). Redis
keeps just the active-batch lock and the delete tombstone.
- In-batch retry budget (wikiMaxFailRetries=5) tracked via
pendingRepo.IncrFailCount; over the cap the row is moved to
task_dead_letters with task_type=wiki:ingest, related_id=knowledge
id. Asynq retries (10) are handled by the new middleware.
- Removed the per-batch ListAllPages. WikiBatchContext now carries
lazy fetcher closures (SlugTitleMany, SummaryByKnowledgeID) with
mutex-protected caches; reduce reaches for titles / summaries on
demand instead of pre-loading the whole KB.
- getExistingPageSlugsForKnowledge now uses ListSlugsBySourceRef,
which the new GIN index on source_refs serves as a Bitmap Index
Scan instead of a sequential text LIKE.
- Dedup pre-filter uses pg_trgm via FindSimilarPages
(idx_wiki_pages_title_trgm). For each new entity/concept (and each
of its aliases) we ask the DB for the top-K trigram-similar
existing pages and union the results — bounded prompt size, no Go-
side O(P × M) loop. Small KBs (≤25 entities) bypass the pre-filter.
- cleanDeadLinks / injectCrossLinks are scoped to the batch's
affected slugs (a few dozen) instead of every page in the KB. Both
use the new lite ListBySlugs / ExistsSlugs repo methods so they
pull only slug + title + outlinks, not full content.
- Slug fuzzy resolve (slug_fuzzy.go): when the LLM emits
[[bad-slug|display]] the cleanup path now tries display-text
reverse lookup → hyphen/case normalized equality → char-bigram
Jaccard ≥ 0.8 before stripping. Recovers the common pinyin-word-
break drift case ("shang-hai-tower" vs "shanghai-tower") in place
instead of replacing the link with plain text.
- rebuildIndexPage uses ListByTypeRecent(200) for the first-time
intro and drops the full DocumentSummaries blob from the
incremental update prompt, so its context stays bounded regardless
of KB size.
- Concurrency tunables surfaced in WikiConfig: IngestBatchSize /
IngestMapParallel / IngestReduceParallel, with sensible defaults
via OrDefault helpers. scheduleFollowUp drops to ProcessIn(0) so
follow-ups don't waste asynq retry slots bouncing on the active
lock.
- RunLint walks pages via the new ListPagesCursor in 200-page
windows and computes the live-slug set with a one-column
ListAllSlugs Pluck instead of a Limit:0 GetGraph that materialized
every node + edge. Memory is now bounded; 40k pages walks in
constant ~4MB.
- 14 GORM tests for both repos against an in-memory SQLite mirror of
the production DDL (task_queue_test.go).
- 6 tests for the dead-letter middleware covering retry budget
detection, payload-agnostic scope inference, error truncation, and
repo-failure isolation.
- 7 tests for the slug fuzzy resolve helper covering the three
resolution stages, display-text priority over normalized equality,
bigram fallback acceptance, and rejection of unrelated slugs.
- Existing wiki_ingest / wiki_lint / wiki_page tests updated to the
new fetcher / cursor APIs.
|
||
|
|
74b1342440 | feat: add Tencent VectorDB retriever backend | ||
|
|
4cce6f2e99 |
feat(retriever): 接入 Apache Doris 4.1 作为向量数据库
为 RetrieveEngine 体系新增 Doris 后端,与现有 Qdrant/Milvus/Weaviate
等保持完整能力对齐:向量检索、关键词检索、健康检查、环境变量与多实例
DB 配置、前端类型注册、单元测试、Docker Compose 模板。
实现要点:
- 协议分工:主链路用 MySQL 协议(database/sql + go-sql-driver/mysql)
做 DDL / 查询 / 删除;批量更新走 Stream Load HTTP API,并启用
partial_update=true、merge_type=APPEND,自动按 1MiB 切分批次并处理
307 重定向。
- 表结构:UNIQUE KEY(id) + enable_unique_key_merge_on_write=true 以
支持 upsert/部分列更新;按维度分表(<base>_<dim>),每张表上建
HNSW ANN 索引(metric_type=cosine_distance)和 INVERTED 索引
(parser=chinese)。
- 分数语义:使用 cosine_distance_approximate,再以 1 - dist 转换为
"越大越相似",与现有 KVHybridRetrieveEngine 约定一致。
- 异步索引:ANN 索引为后台构建,ensureTable 通过轮询 SHOW INDEX 等
待索引就绪后再放行写入,避免首次检索召回为空。
- ARRAY<FLOAT> 序列化:go-sql-driver/mysql 不支持数组占位符,
embeddingLiteral 将 []float32 转成 SQL 字面量字符串再拼接。
新增文件:
- internal/application/repository/retriever/doris/{structs,schema,
query,repository,streamload,repository_test}.go
- scripts/e2e-doris.sh:E2E 验证清单
- docs/wiki/集成扩展/Doris改动与上游同步.md:fork-and-rebase 工作流
与改动清单
修改文件(接线 + 文档):
- internal/types/{retriever,tenant,vectorstore}.go:新增
DorisRetrieverEngineType、env 解析、表单 schema 与索引参数校验
- internal/container/{container,engine_factory}.go:环境变量驱动
与 VectorStore 配置驱动两条路径都支持 Doris
- internal/application/service/vectorstore{,_healthcheck}.go:连接
校验 + Ping/Version 健康检查
- docker-compose.yml:新增 doris-fe / doris-be 服务(profile=doris)
- .env.example:DORIS_* 环境变量与示例
- docs/{使用其他向量数据库,wiki/集成扩展/集成向量数据库}.md:
使用说明与索引/分数行为说明
依赖:go.mod/go.sum 新增 github.com/go-sql-driver/mysql(运行时)和
github.com/DATA-DOG/go-sqlmock(测试)。
测试:repository 层 SQL 形状、Stream Load HTTP 行为、whereBuilder
逻辑、embeddingLiteral 往返、健康检查错误路径均有单测覆盖。
|
||
|
|
d0144f3586 |
perf(wiki): move ingest log to event table and index to on-demand API
Fixes O(n²) write amplification during wiki ingest on large KBs. Previously
every ingest/retract op re-wrote the single `slug='log'` row end-to-end and
every batch re-wrote the entire `slug='index'` directory markdown. On a 40k-
doc KB the log row grew to tens of MB and the index row to several MB, so
each batch triggered giant TOAST updates that dominated ingest wall time.
Log: new `wiki_log_entries` event table (`id DESC` indexed per KB) replaces
the single TEXT row. Batch ingest now collects entries and flushes them
once per batch via `AppendBatch`. Each entry stores `pages_affected` as
JSONB `[{slug,title}]` so the UI can render real titles; custom Scan falls
back to legacy `[]string` so older rows still deserialize.
Index: `wiki_pages[slug=index].content` keeps only the LLM-generated intro
(a few KB). The directory is now served by a structured paginated API
(`GetIndexView`) that reads `slug/title/summary` per type with cursor
pagination, so the agent and the frontend only pull the slice they need.
`RebuildIndexPage` degrades to a no-op; agent `wiki_read_page('index')`
synthesizes a small top-K overview and points callers at `wiki_search`.
Ingest resilience: LLM calls wrap with 3-attempt exponential backoff on
transient errors (5xx/408/429, transport resets/timeouts). Summary/extract
failures now bubble up so the batch's failed-op requeue path runs instead
of silently dropping the doc.
Frontend: sidebar Index/Log entries switch to dedicated views. Index view
streams intro → Summary → Entity → Concept → Synthesis → Comparison via
IntersectionObserver (with a nextTick re-check so small KBs still load
every section). Log view uses cursor pagination; pages_affected renders
titles with slug tooltip.
- new migration: migrations/versioned/000040_wiki_log_entries.{up,down}.sql
- tests: log repo pagination + legacy Scan, ListByTypeLight windowing,
renderIndexOverviewForAgent output, isTransientLLMError classifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df1e9a1bfd |
perf(wiki): subgraph API and interactive exploration for large graphs
Wikis with tens of thousands of pages used to crash the browser trying to render the entire link graph at once (30MB+ JSON, 100k+ SVG elements). This change moves the graph viewer from "fetch everything, render everything" to "fetch a slice, expand on demand" with several matching UX improvements. Backend * GET /wiki/graph now accepts `mode` (overview | ego), `center`, `depth`, `types`, `limit` query params. Default is overview top-500 by link_count, capped at 2000. Response includes a Meta object so the frontend can render a truncation hint and drive the UI. * Pure helper `computeGraphSubset` extracted for testability; six unit tests cover overview truncation / type filter / ego BFS / missing center error. * WikiLintService passes Limit=0 (uncapped) so link integrity checks still walk every page. * Repo `Search` adds CASE-based relevance ranking (title 4, slug 3, summary 2, content 1) so full-text results put the most obvious matches first instead of whatever was updated most recently. Frontend * Graph viewer fetches overview on entry; ego pivot on double-click, search, wiki-link click, URL ?slug=, global issues jump. * Shift+click or hover ⊕ button blooms neighbors onto the current canvas additively. Bloom tracks generations and LRU-evicts oldest when total exceeds 1500 nodes; ego center / selected node / latest anchor are always protected. * "Grow frontier (N)" legend action expands every dashed-ring node in one click (6-way concurrency) while skipping Index/Log super-nodes that would otherwise dump the whole wiki onto the canvas. * Node dashed expansion ring + drawer "X/Y neighbors shown" hint tell users which nodes still have neighbors to load. * Search dropdown uses remote full-text search (debounced, sequence- numbered to drop stale responses) and falls back to the overview top-500 snapshot when the input is empty. * Type filters now round-trip to the server so top-N is always computed from the user's active type set rather than hiding nodes client-side and shrinking the view. * Status card replaces the cramped "centered on X · N hops · M nodes" line with a structured focus/overview summary, resolving slugs to page titles. * Help popover in the legend lists every canvas shortcut. i18n entries added for zh-CN / en-US / ko-KR / ru-RU. |
||
|
|
7b9a831db3 |
fix(chunker): trim surrounding whitespace before embedding
Heuristic and recursive splitters preserve original byte positions in Chunk.Content (the End-Start == RuneCountInString invariant is required by document-reconstruction code paths), which means a chunk sliced at a boundary often carries leading/trailing newlines from the boundary itself. Feeding that whitespace into the embedding model dilutes the vector and wastes tokens for no benefit. TrimSpace the body inside EmbeddingContent for both chunker.Chunk and the two types.Chunk / types.ParsedChunk mirrors. Inner whitespace is preserved; positions/Content are unchanged. Also clean up appendChunk: the previous code computed a trimmed string for the empty-check then stored a separate untrimmed copy, which read as if it might be intentionally divergent. Replace with an explicit 'raw text, skip if pure whitespace' shape and document the invariant. |
||
|
|
2feab191bb |
test(types): regression test that ContextHeader is hidden from JSON
Locks in two invariants exposed during the integration audit: - ContextHeader on types.Chunk must never leak into the JSON representation served to API clients (json:"-" + gorm:"-"). - types.ParsedChunk.EmbeddingContent prepends ContextHeader with a blank line separator and falls back to plain Content when the header is unset. https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
bc44c31cec |
fix(chunker): QA audit — position invariant, recursive split, perf
Resolves the issues surfaced by a self-review of the adaptive chunking work. Tests covering each regression were added first to prove the fix. Critical - Heading splitter no longer prepends breadcrumb to Content. The breadcrumb now lives on Chunk.ContextHeader; EmbeddingContent() combines header + content for the embedder. This restores the End-Start == len(Content) invariant relied on by knowledge.go's document-reconstruction path (summary generation, UI highlight) and also eliminates the duplicate-heading regression where the section heading appeared twice in a chunk's body. - splitBySeparators is now genuinely recursive: when one separator leaves a piece > ChunkSize, the next-priority separator is applied inside that piece. Mirrors the Python reference. Without this, a document with one paragraph break followed by a long run of newline-separated lines could emit a single ~1950-rune chunk for ChunkSize=300. Medium - SplitParentChild forces children onto the recursive tier, skipping the per-parent profile pass (was N extra O(N) scans). - Heuristic splitter snaps overlap start to the nearest semantic boundary or newline instead of slicing mid-line / mid-word. - Strategy.Split returns the legacy tier's output directly when the whole chain rejects rather than running SplitText a second time. Low - ensureDefaults caps ChunkOverlap at ChunkSize/2 so pathological configs (TokenLimit clamping ChunkSize tiny while Overlap stays high) cannot produce ~97% overlap chunks. - DefaultChunkOverlap docstring spells out the migration story for KBs that stored ChunkOverlap=0. ContextHeader plumbing - chunker.Chunk and types.ParsedChunk gain ContextHeader plus an EmbeddingContent() helper. - types.Chunk gets a transient (gorm:"-") ContextHeader field so the knowledge service can build embedding inputs as titlePrefix + chunk.EmbeddingContent() without a DB schema change. https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
2850ac78ce |
feat(retrieval): configurable RRF parameters per tenant
Reciprocal Rank Fusion previously hard-coded k=60 and weights 0.7/0.3. Both are now sourced from RetrievalConfig (RRFK, RRFVectorWeight, RRFKeywordWeight) with the same numbers as defaults via three new GetEffective accessors. fuseOrDeduplicate / fuseWithRRF accept the config; HybridSearch reads it from tenantInfo and forwards. Lets corpus owners tune fusion behavior — large corpora benefit from larger k, keyword-heavy domains from a higher keyword weight, etc. — without changing application code. https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
1585a6f12c |
feat(chunker): recursive priority splitting + token-aware budget
The Go splitter previously combined all separators into one regex, losing the priority-based recursive behavior of the Python reference. This pass walks the separator list in order: the first separator that yields >1 piece wins, mirroring docreader/splitter/splitter.py. Larger semantic boundaries (\n\n) now win over smaller ones (\n). Additional cross-cutting changes: - ChunkingConfig gains Strategy / TokenLimit / Languages fields so the per-KB config can pick a tier and pin embedding token budgets. - buildSplitterConfig in the knowledge service uses the new chunker.DefaultChunkSize / DefaultChunkOverlap constants (overlap consolidated to 80 ≈ 15%) instead of the previous hardcoded 50. - ensureDefaults clamps ChunkSize to a per-language character budget derived from TokenLimit so chunks stay safe under embedding model token caps. Chinese (~1.7 chars/tok) gets a tighter clamp than English (~4 chars/tok). https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
46cab52fb0 |
fix(storage): merge tos/s3/oss/ks3 tenant configs in buildStorageConfig (#1117)
knowledgeService.buildStorageConfig had hardcoded switches in two places that
only handled local/minio/cos. Knowledge bases configured for tos, s3, oss, or
ks3 reached the docreader with a DocParserStorageConfig containing only the
Provider tag — bucket, endpoint, and credentials were silently dropped, and
extraction stalled or fetched from the wrong location.
- Tenant-merge switch: add tos/s3/oss/ks3 cases mapping
StorageEngineConfig.{TOS,S3,OSS,KS3} fields onto DocParserStorageConfig.
Same shape as the existing minio/cos branches; AccessKey -> AccessKeyID,
SecretKey -> SecretAccessKey, no AppID.
- Legacy compat switch: list tos/s3/oss/ks3 alongside local with hasKBFull=false
so fall-through to the tenant-merge path is intentional rather than an
unrecognised provider sliding past the switch. The legacy `cos_config` column
predates these providers and they should never resolve via that path.
- Doc drift: types/custom_agent.go ImageStorageProvider comment and
handler/system.go StorageEngineStatusItem.Name comment now list all 7
providers (matched against types.StorageEngineConfig and ParseProviderScheme).
Tests (knowledge_storage_config_test.go, +268 LOC, 17 cases):
- TestBuildStorageConfig_TenantMergeAllProviders pins per-provider field
population for all 7 providers; the tos/s3/oss/ks3 sub-tests are the
regression guard for #1117.
- TestBuildStorageConfig_LegacyPathOnlyForCOSAndMinIO pins that legacy
StorageConfig data is only consumed for cos/minio and skipped for the
others (intentional fall-through).
- TestBuildStorageConfig_NoTenantFallsThroughToEmpty pins the no-tenant case.
Out of scope (called out in the linked issue):
- The systemic refactor (single source-of-truth provider registry) — left as
a follow-up; this PR keeps changes local to the buggy code path.
- initialization.go validateStorageEngineConfig multimodal validation gap —
the InitializationRequest struct does not carry tos/s3/oss/ks3 fields, so
fixing it requires API surface additions; can ship separately.
Closes #1117 (the buildStorageConfig switches; remaining items deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
42348eb40d |
feat(ks3): add KS3 object storage support
Add Kingsoft Cloud KS3 as a supported storage provider, including backend service implementation, storage config UI, and provider scheme recognition throughout the codebase. |
||
|
|
fe0d24ae87 |
fix(sessions): review fixes for keyword search / pinning / IM titles
- Apply the session-list additions to the SQLite init migration (Lite build) and make QueryPaged dialect-aware: LOWER(..) LIKE on SQLite, ILIKE on Postgres, drop NULLS LAST on SQLite. Escape LIKE wildcards in the keyword via the existing escapeLikeKeyword helper. - SetPinned now returns rowsAffected so the handler can respond 404 on unknown / unauthorized session IDs instead of a misleading 200. - GET /sessions always returns the enriched shape (pin state + IM origin fields) so the frontend never needs a second roundtrip; the dual-path legacy branch in the handler is gone. - Register pin routes with the wildcard name that matches each verb's existing radix tree (POST :session_id, DELETE :id) and accept either param name in the handler; avoids gin's "wildcard conflicts" panic. - Drop the redundant [platform] prefix from IM session titles now that the list renders a platform icon alongside the title; add unit tests for shortID / buildUserSessionTitle / buildThreadSessionTitle. - Frontend: remove the submenu search input and its i18n/keyword wiring (search lives elsewhere in the app); pin icon uses the TDesign `pin` glyph and inherits color so the active session turns green; optimistic pin moves the item to the top of the list so it shows up at the top of the Pinned group; IM text badge replaced by the platform SVG icons from assets/img/im, desaturated by default and full-color on hover or when the session is active. |
||
|
|
dbd804d6e3 |
feat(sessions): add keyword search, user-scoped pinning, and IM source visibility
Sessions today are a flat list ordered by updated_at. Two gaps showed up in practice: - Users cannot find specific chats as the list grows beyond a screen. - IM-created sessions (WeCom/Feishu/Slack/...) are indistinguishable: every title was "IM-<platform>" or "IM-<platform>-<username>" and the list API hid the underlying im_channel_sessions mapping, so admins had no way to tell which Feishu group a session came from. Backend - Migration 000039 adds sessions.user_id (owner), is_pinned, pinned_at plus a composite index for the list query. Existing rows keep user_id NULL and stay visible at the tenant level for backward compatibility. - CreateSession now writes the caller's user_id from auth context. - GET /sessions accepts keyword / source / agent_id. When any filter is set, the response switches to enriched items that LEFT JOIN im_channel_sessions and expose im_platform / im_chat_id / im_thread_id / im_user_id / im_agent_id / im_channel_id. No filters => legacy shape, existing clients unaffected. - Ordering: is_pinned DESC, pinned_at DESC NULLS LAST, updated_at DESC. - POST/DELETE /sessions/:id/pin for user-scoped pin/unpin. - IM session titles: "[platform] <user|chat|thread>" with short ID suffixes so group/DM/thread sessions are visually distinct without needing a round-trip to fetch a display name from the IM adapter. Frontend - Search input debounced at 300ms drives the keyword filter. - Pinned chats render in a dedicated group above the time-based groups, with a pin icon and a pin/unpin entry in the per-chat dropdown. - IM chats get a short [platform] badge in the list. - Pin toggle is optimistic and guards against double-clicks. - zh/en/ko/ru i18n keys added for the new strings. |
||
|
|
a5f80b747c | fix(web-search): normalize default tenant web search config at runtime | ||
|
|
3b23713c54 |
fix(crypto): fail loudly when encrypted DB fields cannot be decrypted
GORM Scan / AfterFind hooks for every AES-encrypted column followed
the same lenient pattern:
if decrypted, err := utils.DecryptAESGCM(c.APIKey, key); err == nil {
c.APIKey = decrypted
}
When SYSTEM_AES_KEY was missing, rotated, or the wrong length the
decryption error was swallowed and the in-memory struct kept the raw
"enc:v1:..." ciphertext. The application then happily forwarded the
ciphertext upstream as the actual API key / password, surfacing as
401/403/SignatureDoesNotMatch from third-party vendors. Worse, a
ciphertext snippet of a customer credential was leaking into the
external provider's request logs.
Introduce utils.DecryptStoredSecret that:
- returns "" / legacy plaintext untouched (no false positives for
pre-encryption rows);
- returns ErrEncryptedDataMissingKey when the value carries the
enc:v1: prefix but no AES key is configured;
- propagates any GCM auth-tag failure from a rotated key.
Wire it into the five Scan / AfterFind sites that currently swallow
the error:
- Tenant.AfterFind (tenants.api_key)
- CredentialsConfig.Scan (tenants.we_knora_cloud.app_secret)
- ModelParameters.Scan (models.parameters.api_key, .app_secret)
- ConnectionConfig.Scan (vector_store_connections.password, .api_key)
- WebSearchProviderParameters.Scan (web_search_providers.api_key)
The error is wrapped with the originating column so the failure is
diagnosable in the logs. Operators must restore the previous
SYSTEM_AES_KEY (or rotate the affected secrets) instead of receiving
silent vendor 401s.
Add table-driven tests covering empty input, legacy plaintext,
round-trip, missing key, wrong-length key, and rotated key. The
strict path explicitly asserts that ciphertext does NOT leak in the
returned plaintext on any error path.
|
||
|
|
d55b52652c |
fix(docparser): preserve standalone image uploads from icon filter
When the uploaded file is itself an image, the image reference now carries an IsOriginal flag so ResolveAndStore skips the small-icon size filter. Otherwise small standalone images (e.g. avatars below 64x64) were silently dropped before reaching multimodal OCR/caption processing. |
||
|
|
dd524ffb1c |
feat: enhance Scan method in RetrieverEngines to support legacy and current formats
- Update Scan method to handle both legacy bare-array format (e.g. [{...}, {...}]) and current object-wrapped format (e.g. {"engines": [{...}, {...}]}).
- Improve error handling for unmarshalling failures, providing clearer feedback on format issues.
This change ensures backward compatibility while allowing for the new data structure.
|
||
|
|
beca2b89a3 |
feat(observability): extend Langfuse tracing across asynq pipeline
Previously the Langfuse integration only traced in-process HTTP requests (chat / search / eval), so file uploads and every downstream asynq task (document parse, chunk embedding, OCR/VLM, summary / question gen, wiki ingest, datasource sync, etc.) produced either disconnected shallow traces or no observation at all. This change threads one trace end-to-end: - tracer: add SPAN observation type and StartSpan; add ResumeTrace so a worker can attach to an upstream trace without emitting a duplicate trace-create; StartGeneration now auto-picks parentObservationId from ctx so nested trace -> span -> generation trees render correctly. - types.TracingContext + LangfuseTracingCarrier: embed on all 17 asynq payloads so trace_id / parent_obs_id / user_id / session_id serialise into every job. - langfuse.InjectTracing: injected at 28 enqueue sites before json.Marshal so the HTTP-layer trace survives the Redis hop. - langfuse.AsynqMiddleware: mux.Use hook that peeks the payload, either resumes the upstream trace or opens a standalone asynq.<type> trace for scheduled jobs, and wraps the handler in a SPAN with task metadata (id / queue / retry / payload_bytes) plus ERROR level on failure. - GinMiddleware.shouldTrace: whitelist ingestion / knowledge-mutation / FAQ / wiki / datasource endpoints so the root trace actually starts. - Tests: tracer_test.go covers span nesting, error status, and ResumeTrace no-trace-create guarantee; asynq_test.go covers InjectTracing round-trip, middleware resume path, and standalone trace fallback. - Docs: docs/Langfuse\u96c6\u6210.md now lists the covered task types and documents the cross-process propagation model. No behavioural change when Langfuse is disabled (all new code paths are no-ops and carriers serialise to empty strings with omitempty). |
||
|
|
492e92580b |
feat(observability): integrate Langfuse for LLM token tracking and tracing
Closes #620 #497. Add opt-in Langfuse observability covering all five model types (chat, embedding, rerank, VLM, ASR) with HTTP-request-scoped traces and Docker Compose support (both cloud and self-hosted). Core package internal/tracing/langfuse: - HTTP client with batched async ingestion (non-blocking in request path) - Sampling, environment / release tagging, and graceful fallback when LANGFUSE_* env vars are absent (wrappers become no-ops) - Gin middleware opens one trace per traced request and finishes it after the handler chain returns, attaching method / path / user / session - Trace context is stored under a typed key exported from internal/types so logger.CloneContext can preserve it across handler / goroutine boundaries (otherwise each LLM call auto-created an orphan trace, fragmenting one request into many) Per-model generation wrappers (opt-in via NewChat/NewEmbedder/...): - chat: captures prompt, streaming output, token usage + TTFT - embedding: approximates tokens when the provider omits usage - rerank: previews query/docs, summarizes results to keep payload small - vlm: records image count and total bytes, never uploads raw pixels - asr: records file size and audio duration, never uploads audio bytes Async title generation (GenerateTitleAsync) now forwards the trace key into the goroutine so title calls appear under the parent chat trace. Docker Compose: - LANGFUSE_* env passthrough on the `app` service for cloud deployments - Optional `langfuse` profile spins up a self-hosted Langfuse stack that reuses WeKnora's existing PostgreSQL (separate database via an idempotent init container that fixes ICU collation drift) and Redis (separate DB number), adding only ClickHouse, MinIO, web and worker containers - web/worker entrypoints URL-encode DB_PASSWORD / REDIS_PASSWORD at start to avoid Prisma P1013 when passwords contain @ / # / etc. Docs: docs/Langfuse集成.md covers cloud vs self-hosted, per-model usage strategy, code map, and resource footprint. |
||
|
|
fd3e2992b1 |
feat(models): support custom HTTP headers across all remote model calls
Introduce per-model `custom_headers` config (similar to OpenAI Python SDK's `extra_headers`) so users can inject gateway auth tokens, trace IDs, etc. into every outbound API request for chat / embedding / rerank / VLLM / ASR models. Reserved headers like Authorization / Content-Type are always preserved to avoid breaking auth or signing flows. Along the way, unify production and "test connection" paths onto a single `ConfigFromModel(*types.Model, appID, appSecret)` constructor in each model package. Both `service.modelService.GetXxxModel` and the four `handler.initialization.Check*Model` / `TestEmbeddingModel` endpoints now go through the same field mapping, so new parameters only need to be added in one place. Backend: - Add `CustomHeaders map[string]string` to `types.ModelParameters` with JSON/YAML `omitempty` tags for forward compatibility. - New `internal/utils/extraheaders.go` providing `ApplyCustomHeaders` (for hand-rolled HTTP paths) and `WrapHTTPClientWithHeaders` / `CustomHeadersRoundTripper` (for SDK paths like go-openai). Reserved headers (Authorization, api-key, Content-Type, Accept, Host, Content-Length, User-Agent) are filtered out. - Inject headers in every remote model path: chat (SDK + raw HTTP), embedding (openai/aliyun/jina/volcengine/nvidia/azure_openai), rerank (remote_api/aliyun/jina/nvidia/zhipu — via shared `customHeaderSetter` interface), VLM and ASR (via http.Client wrapping). - Add `ConfigFromModel` to each of chat/embedding/rerank/vlm/asr, consolidating field mapping (ExtraConfig, CustomHeaders, InterfaceType defaulting, WeKnoraCloud credentials, etc.) and cover it with dedicated unit tests per package. - Refactor `service/model.go`: replace ~100 lines of hand-written Config literals with one-line `ConfigFromModel` calls; drop the now-unused `stringMapToAnyMap` helper. - Refactor `handler/initialization.go` test-connection endpoints: merge four ad-hoc request structs into one `ModelTestRequest`, extract `buildTestModel` and `resolveTenantWeKnoraCloudCreds` helpers, and route all four endpoints through `ConfigFromModel` + `NewXxx` so the test path is now behaviorally identical to production. Frontend: - Add `custom_headers` to the `ModelConfig` API type. - `ModelEditorDialog.vue`: new Key-Value header editor (add/remove rows), auto-converts map <-> array when loading/saving, and passes the current header set to the `Test Connection` button so the preview reflects exactly what production will send. - `ModelSettings.vue`: serialize the header array back into a map for the backend (drops empty rows). - i18n: add custom-header labels / descriptions / placeholders to zh-CN, en-US, ru-RU, ko-KR. Tests: - `internal/utils/extraheaders_test.go` covers reserved-header filtering and round-tripper wrapping (including nil-client fallback). - New `config_from_model_test.go` in chat / embedding / rerank / vlm / asr verifies all fields (CustomHeaders, ExtraConfig, InterfaceType defaulting, AppID/AppSecret) are propagated end-to-end. |
||
|
|
c5494f612b |
feat: Implement auto-linked content updates in wiki page management
- Introduced UpdateAutoLinkedContent method to persist changes from machine-only link decorators without incrementing the version number, ensuring user-facing revisions remain consistent. - Updated existing methods to utilize the new auto-linked content update logic, enhancing the handling of cross-link injection and dead-link cleanup. - Enhanced the KnowledgeBase and WikiBrowser components to improve user experience during wiki operations, including status tracking and indexing indicators. - Added scheduling for wiki status probes to provide timely feedback on indexing processes after user-triggered actions. |
||
|
|
ffc4eeaf57 |
feat: Refactor wiki page update logic for improved version control
- Enhanced the UpdatePage and UpdateMeta methods to differentiate between user-visible content changes and bookkeeping updates, ensuring the version number is only incremented for actual content modifications. - Updated the WikiPage struct and associated interfaces to reflect the new versioning policy, improving clarity on when the version is modified. - Improved documentation for methods to clarify their intended use and behavior regarding versioning and metadata updates. |
||
|
|
a8a3d9e694 |
feat: Enhance wiki extraction and configuration options
- Introduced extraction granularity settings in the wiki configuration, allowing users to control the level of detail in entity and concept extraction (focused, standard, exhaustive). - Updated the UI to reflect these new options, including tooltips for better user guidance. - Refactored the knowledge base editor to support the new extraction granularity feature, ensuring a seamless user experience. - Improved the backend logic for wiki ingest tasks to utilize the new granularity settings, enhancing the accuracy of extracted content. - Added tests to validate the new extraction granularity functionality and ensure robust performance across different configurations. |
||
|
|
da8ba98622 |
feat: Refactor knowledge base service and router for improved wiki handling
- Updated the knowledge base service to ensure WikiConfig is created when wiki indexing is enabled, enhancing backward compatibility. - Removed legacy syncing of WikiConfig.Enabled to IndexingStrategy.WikiEnabled, streamlining the logic for wiki enablement. - Introduced a custom retry delay function for asynq tasks to handle wiki ingest lock conflicts more effectively, improving task management. - Cleaned up the container file by removing unnecessary whitespace, contributing to better code readability. |
||
|
|
afa58a3610 |
feat: Update built-in agent display order and clarify internal agent usage
- Added comments to clarify the purpose of the `builtinAgentIDsOrdered` list, specifically noting the exclusion of `BuiltinWikiFixerID` to prevent clutter in the user-facing agent list. - This change enhances the understanding of the built-in agent configuration and maintains a cleaner user interface for agent selection. |
||
|
|
4022f134bd |
feat: Introduce agent type presets for smart-reasoning mode
- Added a new configuration file `agent_type_presets.yaml` to define various agent type presets, including RAG Q&A, Wiki Q&A, Hybrid (Wiki + RAG), and Data Analysis. - Updated the `builtin_agents.yaml` to associate built-in agents with the new agent type presets, enhancing their configuration options. - Enhanced the agent system prompt templates to reflect the new agent types and their functionalities. - Implemented API endpoints to retrieve agent type presets, allowing for dynamic loading in the frontend. - Updated the frontend to support agent type selection in the agent editor modal, improving user experience by auto-filling relevant configurations based on selected presets. These changes significantly enhance the flexibility and usability of the agent system, allowing users to easily configure agents based on predefined types. |
||
|
|
0531700518 |
feat: Enhance suggested questions functionality with wiki integration
- Updated the SuggestedQuestion interface to include 'wiki' as a new source type, allowing for more diverse question origins. - Implemented ListRecentForSuggestions method in the wikiPageRepository to retrieve recent wiki pages for fallback suggestions, improving the agent's ability to generate relevant questions when other sources are unavailable. - Enhanced the customAgentService to utilize the new wiki page suggestions, ensuring a broader range of questions can be generated from wiki content. These changes significantly improve the agent's suggestion capabilities by incorporating wiki content, enhancing user interaction and information retrieval. |
||
|
|
fdccb68ee4 |
feat: Improve wiki cleanup and ingest handling for deleted knowledge
- Enhanced the `cleanupWikiOnKnowledgeDelete` function to pass the full knowledge object, allowing for more accurate title and summary retrieval during wiki cleanup. - Introduced a tombstone mechanism in Redis to mark recently deleted knowledge, enabling in-flight wiki ingest tasks to skip processing for deleted documents. - Updated the `isKnowledgeGone` function to check for deleted knowledge using the tombstone, improving efficiency in handling concurrent delete and ingest operations. - Added logic in the `wikiIngestService` to filter out updates for deleted knowledge, ensuring that no ghost references are created during the ingest process. - Implemented lint checks for stale source references in the `WikiLintService`, allowing for automatic cleanup of pages referencing deleted knowledge. These changes significantly enhance the robustness of the wiki system, ensuring consistency and accuracy in handling knowledge deletions and related wiki pages. |
||
|
|
28deb8282f |
feat: Add comprehensive documentation for WeKnora knowledge base and wiki features
- Introduced multiple new documentation files, including a complete documentation index, quick reference guides, and comprehensive analyses of the knowledge base and wiki settings. - The new files provide detailed insights into the architecture, data models, API endpoints, and implementation strategies for the knowledge base and wiki functionalities. - Enhanced navigation and usability for developers and team members by summarizing key components, configurations, and data flows related to the knowledge base system. These additions significantly improve the clarity and accessibility of documentation, supporting better onboarding and reference for current and future team members. |
||
|
|
bd096454e1 |
feat: Introduce Wiki Fixer agent and related functionalities
- Added a new built-in agent, "Wiki Fixer," designed to repair and optimize Wiki pages based on linter issues, with multilingual support for enhanced accessibility. - Implemented a corresponding system prompt detailing the agent's role, mission, and workflow for effective issue resolution. - Introduced new API functions for listing and updating wiki issues, allowing for better management of content conflicts and errors. - Enhanced the frontend with new UI elements to display pending issues and facilitate auto-fixing, improving user interaction with the Wiki content. These changes significantly enhance the agent's capabilities for maintaining the accuracy and quality of Wiki pages, fostering a more reliable knowledge base. |