ae7cd02cd6735ffaf925ffec6fe3205cc6f92c9d
340 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ae7cd02cd6 |
fix(rbac): polish tenant member UI; add /leave endpoint
Self-review of PR 3's TenantMembers.vue surfaced a handful of real bugs
and a few rough edges. This commit fixes them and adds the self-leave
endpoint that PR 3's design called out as a follow-up.
Frontend bug fixes
- Read err.status (not err.response.status). The axios interceptor in
utils/request.ts already flattens errors into { status, message,
... }, so the original branch-by-status code never matched and every
4xx fell through to the generic toast. Fixes 404/409 surfacing as
"userNotFound" / "alreadyMember" / "lastOwner" instead of "Something
went wrong".
- t-dialog @confirm replaces :on-confirm so the dialog stays open on
validation failure and on API error. Users can fix and retry without
retyping the email.
- Drop the t-select revert hack; with :model-value the rendered value
is already source-of-truth-on-row.role, so no setTimeout dance is
needed.
- Don't toast "no active tenant" on cold mount; watch the activeTenantId
ref and load once the auth store hydrates.
Frontend UX additions
- Empty state via t-empty (also covers the search-no-results case).
- Compact role-permissions matrix above the table, mirroring
OrganizationSettingsModal so Owners can see what each role grants
before promoting/inviting.
- Search box filtering by username/email (client-side, sufficient for
expected per-tenant member counts).
- "Leave Tenant" button — visible to any active member except the
last-Owner case (server still enforces).
Backend
- New POST /tenants/:id/leave route. Calls TenantMemberService.RemoveMember
with the caller's own user id so non-Owners can quit on their own.
Gated Viewer+ in the matrix; the existing last-Owner invariant in the
service still rejects last-Owner self-leave with 409.
- 3 new handler tests cover the happy path, the last-Owner 409 path,
and the missing-caller-id 401 path (defensive — auth middleware
always attaches a user, but the test pins the contract).
i18n
- Add tenantMember.permissions / search / leave / empty strings in all
four locales (en/zh/ko/ru).
|
||
|
|
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. |
||
|
|
0f0f7e4339 |
test(rbac): cover review gaps from PR 2 self-review
- Add TestRequireRole_CrossTenantSuperuserBypass: pin the CanAccessAllTenants -> Owner-gate bypass behaviour for RequireRole, matching the existing coverage on RequireOwnershipOrRole. Catches reorderings of the fast paths that would re-lock org-level superusers out of foreign-tenant Owner-only routes. - Add TestAgentCreatorLookup_CrossTenantIsHiddenAsNotFound: lock down the defence-in-depth tenant re-check inside AgentCreatorLookup so a future refactor that loosens the service-layer scope (or wires a cross-tenant variant by mistake) cannot leak access via the ownership shortcut. - Reword RequireOwnershipOrRole step 7-8 doc comment so "role check already failed" is unambiguous (it refers to step 1 above). - Annotate the synthetic-system-user construction in middleware/auth.go with a back-pointer to types.IsSyntheticUserID so anyone changing the prefix or numeric suffix knows to update the helper too. |
||
|
|
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. |
||
|
|
9e3c92ca95 |
feat(rbac): enforce tenant role matrix on routes (PR 2 of 3, #1303)
Build on PR 1's tenant_members schema by enforcing the role matrix on
every authenticated route and at agent run time. Enforcement is gated
behind cfg.Tenant.EnableRBAC so the change ships dormant — the same
code paths log the would-be rejection during the rollout window and
start blocking once operators flip the flag.
Middleware (internal/middleware/rbac.go):
- RequireRole(min, cfg): role-only gate using TenantRole.HasPermission.
- RequireOwnershipOrRole(min, lookup, cfg): allows the original creator
through even when their role is below min, otherwise falls back to
the role check. Lookup errors fail closed; an empty creator_id is
treated as tenant-owned and requires the role.
- Both honour cfg.Tenant.EnableRBAC for the rollout window.
Creator lookups (internal/handler/rbac_lookups.go):
- KBCreatorLookup resolves :id -> KnowledgeBase.CreatorID.
- AgentCreatorLookup resolves :id -> CustomAgent.CreatedBy; built-in
agents (IsBuiltin=true) return ("", nil) so they remain Admin+ only.
- Compile-time guards keep the methods aligned with CreatorLookup.
Route guards (internal/router/rbac.go + router.go):
- rbacGuards bundles the matrix into Viewer/Contributor/Admin/Owner
helpers plus OwnedKBOrAdmin / OwnedAgentOrAdmin. Each Register*
function takes *rbacGuards so route lines remain readable.
- KB / Agent / Knowledge / Chunk / Tenant / Model / MCPService /
VectorStore / DataSource / IMChannel routes annotated per the PR 1
design doc matrix. Tenant-wide infra (models, vector stores, MCP,
data sources, IM channels) reads as Viewer+ and writes as Admin+;
per-resource mutations on KB / Agent route through the
ownership-or-Admin guard.
Resource ownership (services):
- CreateKnowledgeBase / CopyKnowledgeBase populate CreatorID from the
authenticated user, so the OwnedKBOrAdmin guard has a creator to
match.
- CreateAgent / CopyAgent populate CreatedBy similarly. Built-in
agents stay tenant-owned.
Agent run-time gate (internal/handler/session/qa.go):
- AgentQA refuses to launch the agent engine for Viewers when the
target agent has RunnableByViewer=false. Same EnableRBAC gating as
the middleware so it's a no-op until enforcement is turned on.
Tests:
- 12 unit tests cover RequireRole / RequireOwnershipOrRole, including
the fail-open and fail-closed branches and the lookup-error path.
|
||
|
|
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. |
||
|
|
ce8969ab14 |
fix(initialization): surface upstream and SSRF errors verbatim in test-connection responses
Test-connection's "连接失败" was nearly useless for debugging because two layers conspired to swallow the actual upstream error: Backend swallow --------------- checkChatModelConnection and CheckASRModel pattern-matched on the upstream error string (401 / 404 / timeout / ...) and replaced it with a fixed Chinese hint. The original SDK message (request URL, response body, provider's error code description) was discarded. Refactor: extract classifyConnectionError(errMsg) — returns just the short Chinese hint. Callers combine "<hint>:<raw err>" so the hint indicates "where to start looking" and the raw error shows "what actually happened". The status-400 special-case (endpoint reachable, parameter mismatch only) still returns success. Frontend swallow ---------------- ModelEditorDialog.testConnection ignored result.message on success paths (fine), but also overwrote it with a generic i18n string on failure paths and 4xx rejections — even though axios's interceptor already lifts the backend's error.message to error.message. Failure branch now uses error?.message || result.message as the toast content; i18n fallback only when neither is present. SSRF allowlist hint ------------------- SSRF validation failures previously returned "Base URL 未通过安全校验: <reason>" with no mention of how to recover (the allowlist is server-side, not configurable in the UI). Operators hit it and were stuck. New utils.FormatSSRFError(label, rawURL, err) wraps the error with guidance to add the host to SSRF_WHITELIST_EXTRA (the operator-facing append-only escape hatch — SSRF_WHITELIST itself is the project's baseline). The hint echoes the failed host so it can be copy-pasted straight into the env var: Base URL 未通过安全校验:DNS resolution failed for hostname test.local.com ... 如该地址确实可信,请联系运维在服务端环境变量 SSRF_WHITELIST_EXTRA 中加入该主机(支持精确域名 / *.example.com 通配 / IP / CIDR),示例:SSRF_WHITELIST_EXTRA=test.local.com, *.example.com,10.0.0.0/8 All SSRF call sites switch to the helper: initialization (CheckRemoteModel / TestEmbedding / CheckRerank / CheckASR / TestMultimodal / batch validation), model CRUD, mcp_service CRUD, knowledge URL ingest, system ReconnectDocReader. |
||
|
|
a9d37970ae |
fix(initialization): fall back to stored apiKey when test-connecting an existing model
Direct regression from the previous credential subresource refactor: the edit-mode ModelEditorDialog stopped binding apiKey into formData (the field now lives in <CredentialResource>), so "Test connection" sent apiKey="" and every upstream call (DeepSeek / lkeap / OpenAI etc.) 401'd. Fix in the backend so the frontend stays simple: - ModelTestRequest gains an optional ModelID field. - New fillSecretsFromStoredModel helper: when ModelID is set AND a secret field on the request is empty, copy it from the stored (decrypted) model. Non-empty request values still win, so users typing a new key to verify it before saving behave the same as before. - The four test entrypoints (chat / embedding / rerank / asr) call the helper right after ShouldBindJSON. Frontend: ModelEditorDialog.testConnection forwards modelId when isEdit is true. The four API client signatures accept the optional field. WebSearch and MCP are unaffected: WebSearch already has a TestProviderByID ID-based path, MCP has no standalone raw test endpoint. |
||
|
|
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. |
||
|
|
4fb089d4d7 |
fix(kb): map ErrKnowledgeBaseNotFound to 404 across handler helpers
Five handler helpers (validateAndGetKnowledgeBase in knowledgebase.go,
validateKnowledgeBaseAccessWithKBID in knowledge.go, the kbService
guards in faq.go and tag.go, and getKnowledgeBaseForInitialization +
the per-kb config getter in initialization.go) wrapped every
GetKnowledgeBaseByID error — including the well-known
repository.ErrKnowledgeBaseNotFound sentinel — as
NewInternalServerError. The result was that every probe of a stale or
cross-tenant kb id surfaced as a 500 instead of the 404 it should have
been, both confusing clients ("real 5xx vs. wrong URL") and burning
ops attention on monitoring alerts.
The mapping pattern is the same as PR #1336 for sessions: detect the
sentinel via stderrors.Is and emit NewNotFoundError; everything else
still surfaces as a 500 so genuine DB / repo failures keep firing the
alerts that matter. Caught during the RBAC e2e smoke run on
feat/rbac, where a deliberate cross-tenant kb-id probe produced
HTTP 500 + body "knowledge base not found" — the smoking gun.
Tests: new internal/handler/knowledgebase_not_found_test.go covers
three cases — bare sentinel, fmt.Errorf("%w") wrapped sentinel
(regression guard against a future revert to `==`), and a non-sentinel
infrastructure error that must still 500. All three pass.
The full handler test package is green.
|
||
|
|
d7b5835519 |
fix(session): map ErrSessionNotFound to 404 across all handlers
PR #1309 plumbed user-scope into the session/message service layer so non-owner / wrong-tenant lookups now surface as ErrSessionNotFound at every entry point that goes through sessionRepo.Get. The handler-side mapping, however, only existed in 4 of the 8 affected sites: - handler/session/handler.go:GetSession ✓ already mapped (with `==`) - handler/session/handler.go:UpdateSession ✓ already mapped (with `==`) - handler/session/handler.go:DeleteSession ✓ already mapped (with `==`) - handler/session/handler.go:BatchDeleteSessions ✓ already mapped (with `==`) - handler/session/handler.go:ClearSessionMessages ✗ always 500 - handler/session/stream.go:ContinueStream session lookup ✓ (with `==`) - handler/session/stream.go:ContinueStream message lookup ✗ always 500 - handler/message.go:LoadMessages (recent + before-time) ✗ always 500 - handler/message.go:DeleteMessage ✗ always 500 Result: a Contributor in tenant A asking for a Contributor B's session messages got a 500 instead of 404 — the wire response leaked "something is broken" rather than "you can't see this URL", which breaks SDK / frontend axios interceptor error handling that keys on HTTP status. Two changes: 1. Add ErrSessionNotFound → 404 mapping to the 4 sites that lacked it (ClearSessionMessages, ContinueStream's GetMessage, LoadMessages's two paths, DeleteMessage). 2. Replace `err == errors.ErrSessionNotFound` with `stderrors.Is(err, errors.ErrSessionNotFound)` everywhere. Sentinel error comparison must use errors.Is so wrapped errors (`fmt.Errorf("...: %w", ErrSessionNotFound)`) still match. Today no service-layer caller wraps, but a future refactor that does would silently turn 404s into 500s with no test catching it. Tests: - internal/handler/message_session_not_found_test.go covers the four new mapping sites in handler/message.go (LoadMessages-recent, LoadMessages-before-time, DeleteMessage) and pins the wrapped-error behaviour with a dedicated regression test that fmt.Errorf("%w") a sentinel must still be detected as ErrSessionNotFound. The bare handler is hit through gin.Engine + middleware.ErrorHandler so the response shape matches production exactly. - PR #1309's existing service-level tests (session_user_scope_test, session_test on the repository) still pass — this PR is a strictly handler-layer follow-up. |
||
|
|
6f95c75ed2 |
feat(system-info): surface DB migration errors with troubleshooting links
When a startup database migration fails (e.g. issue #1319: pg_trgm not available so 000041 cannot build its trigram index), the application intentionally keeps booting so operators can reach the UI to diagnose. However, before this change the system info page silently dropped the "DB Version" row because the value was empty: - migration.go only cached the version after a successful m.Up(); the error path returned early and left migrationVersionSet=false. - system.go used CachedMigrationVersion's ok=false to skip emitting db_version, and the JSON tag was already omitempty. - SystemInfo.vue gated the entire row on v-if="systemInfo?.db_version". The end result was the most useful diagnostic surface vanishing in the exact failure mode that needs it most — Wiki ingest and KG features would silently produce nothing with no UI hint. Changes: - migration.go: replace sync.Once-based setter with an RWMutex-guarded state struct holding {version, dirty, err}. Every failure path now calls captureMigrationFailure(m, err), which best-effort reads m.Version() so the cached value still reflects the partial state. - system.go (handler): always emit db_version (falling back to "unknown" when no version could be read), append " (failed)" when an error is recorded, and add db_migration_error to the response. - swagger / client SDK: keep the API contract in sync with the new response field. - SystemInfo.vue: render the DB version row whenever either field is present, show a "Migration failed" danger tag, and add a full-width alert below the row carrying the error message plus two links: 1. View troubleshooting guide -> new docs/migration-troubleshooting.md 2. Report an issue -> github.com/Tencent/WeKnora/issues/new, prefilled with the captured error and environment metadata. - docs/migration-troubleshooting.md: new self-service guide covering the common failure modes (missing extension, dirty state, privileges, out of disk, schema drift) with concrete psql / make commands. - i18n: add the new keys to zh-CN, en-US, ko-KR, ru-RU. Refs #1319. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8f4e5a459f | fix(session): scope session access by user | ||
|
|
55caeefc3d |
feat(observability): log end-to-end TTFB on both ends of chat stream
To diagnose where latency lives between "user hits send" and "first token appears" we need a single number that can be matched across the browser console and the server log. Add correlated TTFB markers keyed by X-Request-ID: Frontend (streame.ts) * Generate the X-Request-ID once and reuse it for logging. * Log request:start when fetchEventSource is invoked, response:headers when onopen fires, and response:first_answer the first time an SSE payload with response_type === 'answer' arrives. Filtering by event type avoids treating session_title / references / tool_call as the "first token". Backend (session handler) * parseQARequest records the wall-clock entry time and logs TTFB:start with the same X-Request-ID. * qaRequestContext carries receivedAt through to the stream handler. * AgentStreamHandler emits a one-shot TTFB:first_answer_chunk log the first time it observes an AgentFinalAnswerData chunk, so the delta against TTFB:start is the server's request-in → first-token-out budget. The frontend delta minus the backend delta is then attributable to network + gin middleware, which were previously invisible. |
||
|
|
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. |
||
|
|
d199628701 |
fix(agent): exclude wiki-only KBs from quick-answer (RAG) mode
Quick-answer agent mode retrieves purely through vector/keyword chunk search and ships with no `allowed_tools`, so the existing capability filter (which only reads from `allowed_tools`) let wiki-only KBs through in every entry point. End result: users could @-mention, select, and receive suggested questions from wiki-only KBs in quick-answer mode, but the underlying retrieval always returned empty. Treat "RAG-only" as an implicit property of `agent_mode = quick-answer` and union it with the tool-derived filter. The same predicate is now used everywhere the user can pick or be steered toward a KB: Backend - `tools.DeriveKBFilterForAgent` / `KBSatisfiesAgentRequirements` layer the implicit quick-answer requirement on top of tool derivation. - `ListKnowledgeBases`, `SearchKnowledge` (shared-agent `@file`), `resolveKnowledgeBasesFromAgent` (chat runtime), `/search` IM command, and `GetSuggestedQuestions` now all use the agent-mode-aware variant. - `GetSuggestedQuestions` also skips the wiki-page fallback for quick-answer agents to cover the `selected` / explicit-kb-ids paths where a wiki-only KB could still slip through. Frontend - `deriveKbFilterForAgent` / `kbSatisfiesAgentRequirements` mirror the Go helpers. - `@` mention dropdown (`Input-field.vue`) uses the new helper. - Agent editor's "specified KB" picker (`AgentEditorModal.vue`) grays out wiki-only KBs for quick-answer agents with a tooltip, and the pre-save warning fires for quick-answer mode too. - i18n: add `agentEditor.agentType.kbMismatch.quickAnswer` across all four locales. |
||
|
|
c753c67608 |
docs(api): add and fix swag annotations on handlers
将 swag 注解覆盖率从 ~88% 提升到 100%,并修复审计中发现的若干既有
注解 bug。
新增注解(24 个):
- im.go: 4 (UpdateIMChannel/DeleteIMChannel/ToggleIMChannel/IMCallback)
其中 IMCallback 同时注册 GET 和 POST,两个方法都加了 @Router 声明。
- web_search.go: 1 (GetProviders)
- web_search_provider.go: 6
- wechat_qrcode.go: 2
- weknoracloud.go: 2
- knowledge.go: 2 (MoveKnowledge/GetKnowledgeMoveProgress)
- knowledgebase.go: 1 (ListMoveTargets)
- organization.go: 3 (ListSharedAgents/ListOrgAgentShares/RemoveAgentShare)
- mcp_service.go: 2 (SetMCPToolApproval/ResolveToolApproval)
- chunker_debug.go: 1 (PreviewChunking)
修复既有注解 bug(12 处):
- initialization.go: 10 个 @Router 路径与真实路由不符
(e.g. /initialization/kb/{kbId}/config → /initialization/config/{kbId}
/initialization/fabri/tag GET → /initialization/extract/fabri-tag POST)
- session/stream.go: ContinueStream 路径
/sessions/{session_id}/continue → /sessions/continue-stream/{session_id}
- mcp_service.go: ResolveToolApproval 请求体 {approve: bool}
→ {decision: "approve"|"reject", reason?, modified_args?}
收敛响应 schema 到已有命名 struct(之前用 map[string]interface{}):
- knowledge.go MoveKnowledge → handler.MoveKnowledgeResponse
- knowledge.go GetKnowledgeMoveProgress → types.KnowledgeMoveProgress
- web_search_provider.go GetProvider/UpdateProvider → types.WebSearchProviderEntity
- initialization.go InitializeByKB → handler.InitializationRequest
- initialization.go 三个 model-test endpoint → handler.ModelTestRequest
修正 organization.go 中 errors.AppError 应使用 apperrors 别名(该文件
stdlib errors 未别名化)。
不补 /files 和 /api/v1/files/presigned 内联闭包路由(文件服务,非业务 REST)。
Refs #890 #1049 #1168
|
||
|
|
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. |
||
|
|
7c0964bb95 |
fix(agent): enhance user authorization and concurrency handling in tool approval
- Introduced a new test to ensure that empty user IDs are rejected when a waiter has a user, preventing unauthorized approvals. - Added RequestNonce to resolve messages to uniquely identify Resolve calls, ensuring concurrent requests do not interfere with each other. - Updated waiter structure to use atomic operations for the resolved state, improving thread safety. - Enhanced error handling in the Resolve method to properly manage user mismatches and ensure accurate acknowledgment delivery across instances. - Updated MCP service handler to reject unauthenticated requests early, providing clearer feedback on authorization requirements. |
||
|
|
6d0f09d75f |
fix(agent): 加固 MCP 工具人审批的跨实例与并发语义
- 跨实例 Resolve 通过 Redis reply channel 接收 owning 实例的 ack (ok / not_found / tenant_mismatch / user_mismatch / already_resolved), HTTP 不再静默 200,超时退化为 NotFound。 - PendingRequest 增加 UserID,Resolve 校验会话所有者,避免同租户 其他用户越权批准/篡改参数。 - 新增 ErrAlreadyResolved,timer/ctx 命中后再次 Resolve 返回 400 而 不是静默成功;waiter.deliver 暴露胜出语义。 - NeedsApproval 默认 fail-close(DB 故障要求审批),可经 WEKNORA_AGENT_TOOL_APPROVAL_FAIL_OPEN=true 回退。 - Pub/Sub 增加 OriginID 自过滤、退避重连、namespace 后缀,避免自回 环噪声与多部署串扰。 - 工具执行超时常量统一通过 ToolExecContext.ExecTimeout 注入, mcp_tool.go 不再硬编码 60s。 - handler 修正 path 二次解码、错误用 errors.Is、modified_args=null 的 静默清空、ListMCPToolApprovals 的 404/500 误判。 - repository 用 clause.OnConflict 做原子 upsert,消除并发 500。 - 增加 NotFound / TenantMismatch / UserMismatch / AlreadyResolved / Race 等用例。 |
||
|
|
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). |
||
|
|
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. |
||
|
|
8388a50c23 |
fix(chunker): post-review fixes for preview endpoint robustness
Resolves issues from the review of be326aa..119f5e4. Each fix has a
regression test attached and overclaimed findings (parserEngineRules
defensive copy, runTier dead-code branch, init-vars architecture)
were intentionally not touched after re-evaluation.
Goroutine leak mitigation
- previewMaxChars dropped from 256k to 64k runes. The splitter does
not accept a context.Context, so when previewTimeout fires the
worker keeps running. Bounding input size keeps worst-case CPU
per request well under a second on commodity hardware. Sized so
10 concurrent timeouts don't pile up faster than they finish.
- Frontend MAX_CHARS lowered to match.
- Comment in handler explains the trade-off and points at the
follow-up: real cancellation needs the splitter to take a ctx.
Performance
- ApproxTokenCountFromRuneLen variant lets the preview handler
reuse a single rune-count per chunk for stats + size + token
estimation. Eliminates the previous triple []rune allocation per
chunk in the response loop.
- computeChunkSizeStats now takes []int (pre-computed rune lens)
instead of []chunker.Chunk; sumSq computed in float64 to avoid
the int*int overflow at l > ~46k.
Correctness / UX
- Preview panel sends strategy / token_limit / languages
unconditionally, mirroring the buildSubmitData convention so the
preview faithfully reflects what would happen on save.
- Empty-text returns a friendly 400 ("paste a sample…") instead of
gin's cryptic 'Field validation failed on the required tag'.
Tests
- TestSplit_DelegatesToSplitWithDiagnostics renamed to
TestSplit_AndDiagnostics_AgreeOnChunks (the post-audit refactor
made the original name a misnomer; the test still asserts the
right invariant under the new name).
- New TestSplitWithDiagnostics_ProfileSetForAuto and
TestSplitWithDiagnostics_ProfileNilForExplicit lock in the
profile-reuse contract that the preview endpoint depends on.
- New chunker_debug_test.go covers computeChunkSizeStats edge
cases (empty / single / varying / no-variance underflow) plus
PreviewChunking httptest scenarios (auto path, legacy strategy,
empty-text rejection, oversize rejection, chunk truncation with
full-set stats).
Doc cleanup
- runTier comment updated; the "stubbed in this scaffold" line was
obsolete since the heading and heuristic splitters shipped.
https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
|
||
|
|
beb7d005e9 |
fix(chunker): QA audit — pointer DTOs, profile reuse, stats correctness
Resolves the issues surfaced by the post-Phase-3 self-review. High severity - Strategy / TokenLimit / Languages can now be reset. The KB-config handler uses pointer DTOs so the absence of a field in the payload means "no change" while an explicit empty string / 0 / [] clears the value. The frontend now sends the fields unconditionally so the user can revert to defaults via the clear icon — previously these fields were write-once. - DocProfile is no longer computed twice in the preview endpoint. Diagnostics carries the profile that drove tier selection (when auto-strategy ran); the handler only re-profiles for explicit strategies that bypass the profiler. - Chunk-size stats (avg / min / max / stddev) are now computed over the FULL chunk set before the response is truncated to previewMaxChunks. Avoids systematically misleading metrics for large documents. - SplitWithDiagnostics defaults SelectedTier to TierLegacy so an empty-text request never leaves the diag with a blank tier string. Medium severity - Split() reverts to its own loop instead of delegating to SplitWithDiagnostics. Diagnostics struct allocation is reserved for callers that actually want the trace (preview endpoint). Matters in SplitParentChild where Split runs once per parent. - Preview endpoint runs the splitter on a goroutine wrapped in a 5s context.WithTimeout. CJK input at the 256k-rune ceiling could otherwise tie up the worker for several seconds. Low severity - KBChunkingSettings emits arrays via spread copies so parent-side mutation cannot drift the form's reactive state. - KBChunkingDebug font-family includes a monospace fallback so the chunk content stays monospaced even when the TDesign CSS variable is missing. Documentation - DocProfile JSON shape is now annotated as part of the public preview API; field renames would be breaking. https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
a867b1b29d |
feat(handler): add chunker preview endpoint for debugging
Adds POST /api/v1/chunker/preview — a read-only endpoint that runs the adaptive chunker on supplied text and returns the chunks plus diagnostic info (which tier won, which were rejected and why, the DocProfile that the selector saw). Used by the upcoming KB editor debug panel so users can experiment with chunking parameters without re-uploading documents. Backend pieces: - chunker.SplitWithDiagnostics(text, cfg) returns chunks plus a Diagnostics struct (SelectedTier, TierChain, Rejected). Split() now delegates here. - chunker.DocProfile gets JSON tags so it serializes cleanly. - handler.PreviewChunking caps input at 256k chars and chunks at 500 per response, computes a size-distribution summary, and never touches DB or embedding APIs. Auth: lives under the v1 group so the standard JWT middleware applies. https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k |
||
|
|
82b0e504b0 |
feat(handler): expose chunking strategy / token-limit / languages in API
Wires the new ChunkingConfig fields (Strategy, TokenLimit, Languages) through the documentSplitting DTO of the KB-config update endpoint and reflects them back in the GET-config response. Existing payloads stay fully backwards-compatible — only set the fields you want to change. 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. |
||
|
|
e029d31f86 |
feat(im): tenant-wide IM channels overview under the user menu
Until now the IM channels a tenant has connected were only visible from
inside each agent's editor (AgentEditorModal → IMChannelPanel). Finding
out which bots are live across all agents meant clicking through every
one of them. Add a tenant-scoped overview so the set of connected IMs is
always one hop away from the avatar.
Backend
- im.Service.ListChannelsByTenant(tenantID) returns all non-deleted
channels in the tenant, LEFT JOIN'd with custom_agents.name so
built-in agents (which don't have rows in custom_agents) still show
up with an empty agent_name; the frontend substitutes a localized
"built-in agent" label.
- Credentials are intentionally stripped from this response — the list
view is read-only and the editor route (GET /agents/:id/im-channels)
remains the only source for secrets.
- New ChannelWithAgent DTO + IMHandler.ListAllIMChannels wired at
GET /api/v1/im-channels under the existing RegisterIMChannelRoutes
group, so auth middleware is inherited.
Frontend
- IMChannelsOverviewPanel.vue: floating submenu pane, stacked rows
(agent on top, channel below) with identical 20px avatars and 12px
labels so neither identity dominates. Each row is the whole click
target (jumps to the agent editor); the switch uses @click.stop to
toggle in place without triggering navigation.
- UserMenu.vue: new hover-driven submenu entry ("已接入的 IM" + link
icon). The pane is teleported to <body> because the sidebar container
has overflow:hidden that would otherwise clip right-flying content.
Position is computed from the menu item's rect and clamps/flips
against the viewport edges.
- "Live" indicator: a pulsing green dot on the menu item when at least
one channel is enabled. UserMenu prefetches the list once on mount;
the panel re-emits channels-changed after every load/toggle so the
dot stays in sync. Respects prefers-reduced-motion.
- AgentList.vue: supplement the existing onMounted-only
checkAndOpenEditModal with a watch(route.query.edit) so
router.push({ path: '/platform/agents', query: { edit, section } })
from an already-mounted AgentList (the common case when navigating
from the overview) opens the editor immediately instead of only
after a hard refresh.
Assets
- Seven platform SVGs under assets/img/im/, pulled from iconify
(simple-icons / logos / tdesign / icon-park / remix icon) and baked
with brand colors so monochrome sources (wecom/wechat/dingtalk/
feishu) don't render black.
i18n
- New imOverview namespace in zh-CN / en-US / ko-KR / ru-RU covering
the menu label, panel title/subtitle, column headers, builtin-agent
fallback, and the live-indicator tooltip.
Tested
- go build ./...
- go test ./internal/im/...
- vue-tsc --noEmit
- npm run build
|
||
|
|
a5f80b747c | fix(web-search): normalize default tenant web search config at runtime | ||
|
|
b48adde3e6 | fix:attachment_document_failed | ||
|
|
7fcd92994b |
feat(tenant): expose API Key reset from the API Info page
Add a danger-themed reset button next to the existing copy/eye buttons on the API Info settings page. Confirmation dialog warns that the old key is revoked immediately; on success the new plaintext key replaces the displayed value and is auto-revealed so the user can copy it. Backend: new POST /api/v1/tenants/:id/api-key handler that wraps the existing TenantService.UpdateAPIKey; access is gated by the same authorizeTenantAccess check as other tenant endpoints. The handler returns the freshly generated plaintext key, while the encrypted form is persisted to the database. Frontend: new resetTenantApiKey API client, reset button + confirm dialog wiring in ApiInfo.vue, plus matching i18n entries for zh-CN, en-US, ko-KR, and ru-RU. |
||
|
|
20be935fe0 | Merge branch 'main' into storage-allow-list | ||
|
|
6601a483be | feat: add STORAGE_ALLOW_LIST env var | ||
|
|
da90867dd4 | feat(ui): refactor IM channels list layout and form\n\n- Change IM channel form from dialog to drawer for better UX\n- Convert channel list from vertical layout to responsive grid cards\n- Move edit/delete actions into a dropdown menu for cleaner card design\n- Replace platform radio group with a select dropdown in the add channel form | ||
|
|
48a764b866 |
refactor(knowledge-base): address review feedback
Backend
- BatchDeleteKnowledge handler now enqueues an asynq
TypeKnowledgeListDelete task instead of calling DeleteKnowledgeList
synchronously, matching the pattern used by ClearKnowledgeBaseContents.
Avoids long HTTP timeouts on large batches and shares the existing
async cleanup pipeline.
- Extract a small enqueueKnowledgeListDelete helper on KnowledgeHandler
so BatchDeleteKnowledge and ClearKnowledgeBaseContents share the
payload/marshal/enqueue boilerplate.
Frontend (DocumentListView)
- softer outer border and lighter row separators
- header background switched to the page tone with placeholder-color
text, reducing institutional gray
- selected row uses an inset left accent bar and a quieter brand tint
instead of a saturated background; hover stays neutral
- consolidate the .row-more-btn { opacity: 1 } rule that was duplicated
across the hover and selected blocks
|
||
|
|
be8e53bf08 |
feat(knowledge-base): add document batch delete and list view
Add a list-view alongside the existing card grid (issue #957) and multi-select batch delete (issue #1045) for documents inside a knowledge base. The two views are toggled from the toolbar and the preference is persisted in localStorage. Selection state is shared between views with shift-range support and a sticky batch-action bar. Backend - POST /api/v1/knowledge/batch-delete: validates KB scope and editor permission, then delegates to the existing DeleteKnowledgeList service which already cascades vector/file/graph cleanup. Caps batch size at 200 and uses a single GetKnowledgeBatch call for membership validation. Frontend - DocumentListView: table layout with sticky header, checkbox column (with indeterminate select-all), file-type icons, status badges, per-row action menu. - DocumentBatchBar: floating pill bar with selected count and delete action; appears when items are selected. - KnowledgeBase.vue: view-mode toggle, selection state, batch-delete confirmation dialog, hover/active checkbox overlay on existing cards. - Extracted formatFileSize and getFileIcon to utils/files.ts to share with the new list view and avoid further drift from the existing inline copies. - i18n keys added for zh-CN, en-US, ko-KR, ru-RU. Closes #1045 Closes #957 |
||
|
|
a4fe86f828 | fix(agent): improve tenant context handling and error reporting | ||
|
|
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). |
||
|
|
b8c45b173e |
fix(agent-share): don't require rerank model for wiki-only agents
The share-time validation forced every agent bound to a knowledge base to have a rerank model configured, which blocked Wiki-type agents (`wiki_search` / `wiki_read_page` / …) from being shared even though they never invoke the reranker at runtime. Align the check with the runtime logic in `session_agent_qa.go`: only require a rerank model when the agent's allowed_tools include `knowledge_search` (or rely on the default tool set, which contains it). Wiki-only agents can now be shared without a rerank model; hybrid / RAG agents still enforce the requirement. Also update the handler error message to reflect the new rule. |
||
|
|
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. |
||
|
|
4c906e0431 |
feat: Enhance agent tool compatibility and knowledge base filtering
- Introduced a unified capability requirements system for agent tools, ensuring consistent filtering of knowledge bases based on their capabilities. - Implemented derived `kb_filter` logic to streamline compatibility checks between agent tools and knowledge bases, reducing redundancy in configuration. - Updated frontend components to reflect new filtering logic, including improved handling of empty states and user feedback when no compatible knowledge bases are available. - Enhanced internationalization support by adding relevant messages for tool compatibility issues across multiple languages. |
||
|
|
b3484592e7 |
feat: Remove RebuildKnowledgeBaseIndex and associated handler from knowledge service
- Deleted the RebuildKnowledgeBaseIndex function from the knowledge service, which was responsible for reprocessing all documents in a knowledge base. - Removed the RebuildIndex handler from the KnowledgeBaseHandler, eliminating the endpoint that triggered the rebuild process. - Updated the router to remove the route for rebuilding the index, streamlining the knowledge base management functionality. |