Commit Graph

893 Commits

Author SHA1 Message Date
weitong a27f12a6cb 验证完毕 2026-05-15 20:47:01 +08:00
hjz 06feee0fee feat: modify the skills registration directory 2026-05-15 17:33:15 +08:00
wizardchen 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.
2026-05-14 20:12:41 +08:00
wizardchen 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.
2026-05-14 16:47:36 +08:00
wizardchen 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>
2026-05-14 16:34:50 +08:00
wolfkill 8f4e5a459f fix(session): scope session access by user 2026-05-14 15:56:43 +08:00
ochan.kwon 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.
2026-05-14 15:53:58 +08:00
Andy Yang 6e6d61d945 fix(org): searchable join bypasses invite code expiry
JoinByOrganizationID no longer delegates to JoinByInviteCode for direct
join, matching the product copy that searchable spaces need no invite
code. Extract joinAsViewerWithChecks for shared member-limit logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 10:46:26 +08:00
wizardchen 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
2026-05-13 12:29:10 +08:00
wizardchen 9e08ab7302 fix(chat): map MaxTokens to MaxCompletionTokens for GPT-5/o-series
OpenAI's GPT-5 series and o-series reasoning models (o1/o3/o4-mini) no
longer accept `max_tokens` and reject non-default sampling params
(`temperature`, `top_p`, `frequency_penalty`, `presence_penalty`).
Azure OpenAI propagates the same constraint, returning HTTP 400 with
"this model is not supported MaxTokens, please use MaxCompletionTokens".

Lower the compatibility shim into `RemoteAPIChat.BuildChatCompletionRequest`
(the single OpenAI-protocol egress) so every internal caller keeps using
`MaxTokens` uniformly:

- Add `provider.IsOpenAIReasoningOrGPT5Model` with precise prefix matching
  (covers `gpt-5*`, `o1`/`o1-*`, `o3`/`o3-*`, `o4`/`o4-*`; rejects
  `olympus-1`, `openai-*`, `o3xtra`, etc.).
- When the provider is `openai`/`azure_openai` and the model matches, map
  `MaxTokens` to `MaxCompletionTokens` (explicit `MaxCompletionTokens`
  wins) and skip the unsupported sampling fields; `omitempty` keeps them
  off the wire.
- Behavior is unchanged for gpt-4o, gpt-4, and all other providers.

Add unit tests for the matcher and for the request-building path
(Azure gpt-5.2, OpenAI gpt-5/o1-mini/o3/o4-mini, plus negative cases).

Fixes #1283
2026-05-13 00:40:35 +08:00
langcaiye 4d2b8707ff fix: support Anthropic gateway streaming 2026-05-13 00:14:23 +08:00
wizardchen 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.
2026-05-12 21:16:37 +08:00
wizardchen f740e7cecf feat(observability): expand Langfuse spans across chat pipeline
Previously the Langfuse timeline for a knowledge-chat request only
showed generations (chat.completion, embedding.embed, rerank), so the
work happening between them — query setup, vector/keyword search,
result merging, prompt assembly — appeared as unexplained gaps.

Add spans around the pieces that fill those gaps:

* qa.setup wraps request-time KB / model / search-target resolution
  before the pipeline event loop starts, accounting for the visible
  delay before the first chat.completion.
* pipeline.<event> wraps each pipeline stage's eventManager.Trigger
  call so generations inside a stage nest under it. CHAT_COMPLETION_
  STREAM is intentionally skipped because its OnEvent returns as soon
  as the streaming goroutine starts; a stage span would always finish
  before the chat.completion.stream generation it nominally parents.
* retrieve wraps the actual vector + keyword retrieve call inside
  HybridSearch, exposing the DB round-trip that previously sat invisibly
  between embedding generations and rerank.
* web_search wraps the external web-search HTTP call when enabled.
2026-05-12 21:16:37 +08:00
wizardchen 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.
2026-05-12 20:27:20 +08:00
wizardchen 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
2026-05-12 19:39:35 +08:00
wizardchen 86b05d923e fix(middleware): mask camelCase secret fields in request logs
The request logger's sanitizeBody only matched lowercase / snake_case
field names (api_key, apikey, access_token, ...), so values for the
camelCase JSON fields actually used by the API (apiKey, secretKey,
refreshToken, accessToken, ...) were written to logs in clear text.

Replace the per-field patterns with a single case-insensitive regex
that tolerates optional `_`/`-` separators, covering snake_case,
camelCase and PascalCase variants, and extend coverage to id_token,
client_secret, private_key, auth_token, api_secret and passwd. The
field name is preserved; only its value is replaced with "***".

Add unit tests for sanitizeBody covering the previously-leaking
camelCase fields and common variants.

Fixes #1287
2026-05-12 19:27:29 +08:00
wizardchen 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.
2026-05-12 18:29:47 +08:00
wolfkill b387f3637a fix(feishu): tolerate partial wiki node listing failures 2026-05-12 17:40:59 +08:00
wizardchen 8b2a36d759 fix(multimodal): unblock processing when provider:// image read fails
When image bytes for a multimodal task cannot be read via FileService
(e.g. tenant.StorageEngineConfig.MinIO is empty while the image was
saved using the global MINIO_* env vars), the previous code fell back
to the HTTP downloader, which rejected the provider:// URL with
"unsupported URL scheme". The asynq handler then returned an error,
asynq retried until exhaustion, and the per-knowledge "pending images"
counter was never decremented — leaving the document stuck in
"processing" indefinitely (issue #1282).

Changes:
- ImageMultimodalService now holds a default FileService and
  resolveFileServiceForPayload falls back to it when the tenant-scoped
  storage config cannot produce a usable service, mirroring the
  write-side fallback in knowledgeService.resolveFileService.
- Extract readImageBytes: provider:// URLs are read exclusively via
  FileService and never handed to the HTTP downloader.
- On unrecoverable read failure for a single image, log and skip that
  image but still call checkAndFinalizeAllImages so the parent
  knowledge can progress to post-processing.

Fixes #1282
2026-05-12 17:40:04 +08:00
langcaiye 42d98261e7 feat: support Anthropic chat provider 2026-05-12 17:33:16 +08:00
wizardchen 5949be739f fix(agent): relax rerank model requirement for custom agents
Previously the custom agent editor hid the rerank model field when no
RAG-type knowledge base existed in the configured scope, but the
not-ready check and runtime hard-failed whenever knowledge_search was
in allowed_tools. Users with wiki-only or empty scopes saw "Rerank
Model required" warnings they could not resolve, and "All knowledge
bases" agents broke later if a RAG-type KB was added.

- Backend: when knowledge_search is enabled, fall back to the tenant
  default rerank model (ConversationConfig.RerankModelID) before
  erroring out, matching the built-in agent behaviour.
- Editor: always show the rerank field once a KB scope is selected;
  only mark it required (red *) when the scope contains a RAG KB, with
  a hint explaining the tenant-default fallback.
- Editor: only render rerank top_k / threshold sliders when a rerank
  model is actually selected.
- Input field: drop the eager "missing rerank" not-ready reason; the
  backend is now the single source of truth for rerank availability.
- i18n: add agent.editor.rerankModelOptionalHint across all locales.
2026-05-12 16:57:24 +08:00
nullkey 4ed7b90eac fix(faq): correct PostgreSQL cast precedence in FAQ search
`metadata->'similar_questions'::text` parses as
`metadata -> ('similar_questions'::text)` because `::` binds tighter
than `->`, so the expression yields jsonb instead of text. ILIKE on
jsonb then fails with "operator does not exist: jsonb ~~* unknown",
returning 1007 Internal server error for `search_field=similar_questions`
and `search_field=answers`. The `standard_question` branch was unaffected
because it uses `->>` (text) directly.

Wrap the json access in parens so the cast applies to the extracted
value: `(metadata->'similar_questions')::text ILIKE ?`. The MySQL
branch uses JSON_EXTRACT and is unchanged.

Fixes #1264
2026-05-12 16:42:21 +08:00
wizardchen 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.
2026-05-12 16:27:28 +08:00
wizardchen a082c04d28 chore(deps): update dependencies in /docreader and adjust dependabot configuration
- Updated `pydantic` from 2.12.3 to 2.13.4 and `pypdfium2` from 5.0.0 to 5.8.0 in the `docreader` requirements.
- Modified the dependabot configuration to set `open-pull-requests-limit` to 0 and added an `ignore` rule for version updates across all ecosystems, allowing only security updates.
- Adjusted settings for `server-security`, `client-security`, `frontend-security`, and `miniprogram-security` groups to streamline security update handling.

This change aims to enhance dependency management and maintain security while reducing noise from version update PRs.
2026-05-12 14:58:52 +08:00
wizardchen 9ad8e7ca78 fix(agent): replay attachments in multi-turn history
Agent mode does not persist `rendered_content` for user messages, so
when the next turn's history was rebuilt from DB, attachments uploaded
in prior turns disappeared — the model only saw the raw query plus the
prior assistant reply, breaking follow-up questions that referenced
the file (e.g. "what is in there?").

Reconstruct the attachment prompt from the stored `Attachments` column
when `RenderedContent` is empty, mirroring how image captions are
already replayed. KnowledgeQA turns (which do persist
`RenderedContent`) are unaffected and won't get attachments injected
twice.

Refs #1237
2026-05-12 14:46:57 +08:00
nullkey 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
2026-05-12 13:16:58 +08:00
wizardchen 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.
2026-05-12 12:26:24 +08:00
wizardchen 5fb7e692e5 refactor(web-search): drop dead api_key path in SearXNG provider
The SearXNG provider forwarded `parameters.api_key` as
`Authorization: Bearer <key>` for reverse-proxy auth, but the frontend
never rendered the API key input for self-hosted providers
(`requires_api_key=false`), so the path was unreachable from the UI.

Rather than expose another UI knob for a niche reverse-proxy setup that
the project does not currently support, remove the Authorization header
branch and the `apiKey` field on `SearxngProvider`. Tenants who put
SearXNG behind an authenticating reverse proxy can still front it with
network-level auth (mTLS, IP allowlist) without changes here.

`WebSearchProviderParameters.APIKey` is generic and used by other
providers, so it stays.
2026-05-11 16:53:47 +08:00
wizardchen c2d60933c8 refactor(security_test): standardize struct field formatting in SSRF whitelist tests
- Adjusted the formatting of struct fields in the SSRF whitelist test cases for improved readability and consistency.
- No functional changes were made; this is purely a code style improvement to enhance maintainability.
2026-05-11 16:53:47 +08:00
wizardchen 0f5dc41f4e feat(searxng): enhance SearXNG configuration and validation
- Updated .env.example to clarify SEARXNG_SECRET generation and added SSRF_WHITELIST_EXTRA for improved security.
- Modified docker-compose files to bind SearXNG to localhost by default and introduced a one-time initialization service to set up settings.yml correctly.
- Enhanced SearxngProvider with stricter URL validation, ensuring no query or fragment is present in the base URL.
- Added unit tests for SearXNG validation and date parsing to ensure robustness.
- Updated frontend WebSearchSettings to reflect changes in SearXNG instance URL handling.

This commit improves the security and usability of the SearXNG integration, addressing potential misconfigurations and enhancing the developer experience.
2026-05-11 16:53:47 +08:00
wizardchen d2a1006beb fix(web-search): address review fixups for SearXNG provider
- utils: 合并 SSRF_WHITELIST 与 SSRF_WHITELIST_EXTRA,避免部署侧默认值
  (如 docker-compose 注入的 searxng 主机名)被用户的 SSRF_WHITELIST
  自定义值覆盖。
- docker-compose.yml: 把 searxng 默认值挪到 SSRF_WHITELIST_EXTRA。
- searxng: 抽出导出函数 ValidateSearxngBaseURL,让 service 层保存校验
  和 provider 构造校验完全一致;service 改为调用同一函数。
- searxng: language 由非法的 "auto" 改为 "all";移除强制 safesearch=1
  让实例 settings.yml 决定。
- searxng: publishedDate 增加多格式 fallback(RFC3339Nano/无时区/
  日期-only/RFC1123 等),无法解析时 debug 日志记录。
- searxng: 解析 unresponsive_engines;结果为空时打 warn 日志,便于排查
  "配置正确却搜不到结果"的情况。
- frontend: WebSearchSettings 的 Instance URL 标签/占位符走 i18n,
  zh-CN / en-US / ru-RU / ko-KR 四个 locale 补齐 baseUrlLabel /
  baseUrlPlaceholder。
2026-05-11 16:53:47 +08:00
wizardchen 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
2026-05-11 16:53:47 +08:00
wizardchen 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.
2026-05-10 22:57:12 +08:00
wizardchen 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 等用例。
2026-05-10 22:57:12 +08:00
wizardchen 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).
2026-05-10 22:57:12 +08:00
wizardchen 24a010ff59 fix(chat): reduce default LLM timeout values to 300s for chat and 600s for stream 2026-05-10 00:16:12 +08:00
wizardchen e83378ecd1 fix(chat): 调大 LLM 兜底超时默认值(chat 600s / stream 1800s) 2026-05-10 00:16:12 +08:00
wizardchen 386df92582 fix(chat): LLM 兜底超时仅在上层无 deadline 时生效,并修复 ChatStream raw HTTP 路径的 cancel 泄漏
PR #1238 引入的 context.WithTimeout 会把上层 ctx 的 deadline 强制截断到
默认 120s/300s,导致调用方无法设置更长的合理超时(例如长推理模型场景)。
同时 ChatStream 在走 chatStreamWithRawHTTP 早返回路径时丢弃了 cancel
函数,timeoutCtx 要等 deadline 到期才释放,go vet/lostcancel 也会告警。

本次修复:
- 新增 withLLMTimeout 辅助函数:仅在上层 ctx 没有 deadline 时附加默认
  超时;上层若已显式设置 deadline(无论比默认更短还是更长),都原样
  尊重,把超时的最终决定权交还给调用方。
- 默认值通过环境变量可覆盖:
  - WEKNORA_LLM_CHAT_TIMEOUT_SECONDS    (默认 120s)
  - WEKNORA_LLM_STREAM_TIMEOUT_SECONDS  (默认 300s)
- 修复 ChatStream raw HTTP 早返回路径未调用 cancel 的泄漏:新增
  wrapStreamCancel,在底层 channel 关闭后统一执行 cancel。
- 补充单测覆盖三种 deadline 场景与环境变量解析。
2026-05-10 00:16:12 +08:00
杨明康 6b8dbaa25b fix(chat): 为 LLM 非流式/流式调用添加超时保护,防止 worker 被 hung 请求永久阻塞
IM 渠道(企微等)的消息处理走 KnowledgeQA 管道,该管道的 LLM 调用此前
未设置任何超时,依赖 context cancellation 控制。然而 QA 管道传入的 context
本身无 deadline,一旦上游 LLM API(如 mimo-v2.5-pro)的网络请求 hang 在
TCP 层面(连接未断但无数据返回),worker goroutine 会永久阻塞在
CreateChatCompletion / CreateChatCompletionStream 上。

随着时间推移,多个 worker 陆续卡死,队列中所有 worker 耗尽后机器人对任何
消息都不再回复。

本提交在 RemoteAPIChat.Chat 和 RemoteAPIChat.ChatStream 两个入口方法中
统一添加 context.WithTimeout:
- 非流式调用(Chat):120s 超时
- 流式调用(ChatStream):300s 超时

如果上层调用方已设置更短的 deadline,WithTimeout 自动遵循更短的那个,
不会覆盖已有的合理超时设置。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:03:53 +08:00
langcaiye 02be05c095 feat: support Tencent VectorDB keyword retrieval 2026-05-10 00:02:05 +08:00
wizardchen 82947c726e fix(wiki-ingest,asynqdl): review fixups for PR #1241
- repo: drop r.db.Debug() from FindSimilarPages — it was dumping every
  trigram probe's SQL+args (per-alias, per-item) into production logs.
- wiki_ingest dedup: fix Printf format string ("selected for %d new
  items" had two args), and harden validMerge against un-prefixed
  slugs whose strings.Index returned -1 and silently passed the type
  check.
- wiki_ingest_batch: drop the duplicated loggedBatchSize/MapPar/
  ReducePar assignments.
- asynqdl: record the real attempt count (retried + 1) on the dead
  letter row instead of a hard-coded 0; tighten payloadProbe to the
  set of field names with consistent semantics across payloads
  (drop source_id/target_id/target_kb_id which differ by task type).
- asynqdl tests: update for the trimmed probe and assert FailCount=0
  outside an asynq worker ctx so the semantics stay pinned.
2026-05-10 00:01:06 +08:00
wizardchen 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.
2026-05-10 00:01:06 +08:00
wizardchen ff950ef249 fix(ollama): adapt to ToolCallFunctionArguments struct API
The ollama Go SDK changed ToolCallFunctionArguments from
map[string]any to a struct backed by an ordered map. Use ToMap() for
read access and NewToolCallFunctionArguments()+UnmarshalJSON for
construction so the build matches the upgraded SDK.
2026-05-09 13:58:55 +08:00
langcaiye 74b1342440 feat: add Tencent VectorDB retriever backend 2026-05-09 13:14:01 +08:00
wizardchen b7388f10e1 fix(wiki-ingest): reconcile summary links and log feed when reduce LLM fails
When a doc's entity/concept page generation hit a transient LLM error in
the reduce phase, the page never got written, but the doc's summary page
(generated in parallel with the slug list baked into its content) was
already persisted with [[entity/foo|name]] links pointing at the missing
page. The wiki log feed also surfaced the failed slugs as clickable
entries that 404'd. cleanDeadLinks only ran for retract batches, so the
debris persisted indefinitely.

reduceSlugUpdates now reports addition-path failures back to the batch
driver, which collects them and:

  - rewrites this batch's summary pages, replacing dead [[slug|display]]
    refs with plain display text (or a humanized slug tail);
  - filters dead slugs out of docResults[].Pages before the wiki log
    entries are flushed;
  - broadens the cleanDeadLinks trigger to fire whenever any page was
    touched or any slug failed, as a long-tail safety net.

Pure text replacement; no extra LLM calls. Failed slugs are picked up
naturally on the next ingest of the same document via the existing slug-
continuity rules, so the system self-heals without manual reingest.
2026-05-09 02:10:15 +08:00
wizardchen fc6f160eff fix(retriever/doris): code review cleanup
针对 4cce6f2e(接入 Apache Doris)的 code review 修复,主要修正若干阻断性
问题与可读性问题,并剔除不应进入主仓的本地工作流文件。

阻断性修复:
- docker-compose: Doris 镜像由 2.1.0 升至 4.1.0。原 2.1.0 不支持 HNSW
  ANN、cosine_distance_approximate 与 Stream Load partial_columns,
  按当前 DDL 一启动就会失败。
- DSN 字面量拼接改用 mysql.Config.FormatDSN()。原 fmt.Sprintf 在用户名/
  密码包含 `@`/`:`/`/` 等字符时会跑偏。覆盖 health check 与 engine
  factory 两处。

健壮性修复:
- 新增 validateEmbedding,写入与查询前拒绝 NaN/±Inf;strconv.FormatFloat
  对非有限值会输出 "NaN"/"+Inf" 拼成无效 SQL。
- waitANNReady 改为后台 goroutine + 独立 context,避免新维度首次写入路径
  阻塞最长 30s;ANN 未就绪时 Doris 会自动退化为 brute-force。

清理:
- annIndexReady 移除最终两个分支都 return true 的冗余写法。
- Save 移除冗余的双重 toDorisVectorEmbedding。
- testDorisConnection 把 "5.7.99 Doris-4.1.0" 解析为裸 "4.1.0",与
  Postgres/ES 的版本格式对齐。

剔除(不应合入主仓):
- docs/wiki/集成扩展/Doris改动与上游同步.md:纯 fork 维护工作流文档。
- scripts/e2e-doris.sh:作者本地 E2E 验证清单。

测试:
- repository_test 用 require.Eventually 适配 ANN 异步轮询。
- 现有 doris 单测全部通过。
2026-05-09 00:31:03 +08:00
issunion 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 往返、健康检查错误路径均有单测覆盖。
2026-05-08 21:59:35 +08:00
wizardchen e66c42343b fix(wiki-ingest): harden conflict retry and right-size retry budget
Avoid treating Redis pending-length read failures as empty queues, reset stale fail-count keys on fresh ingest/drop paths, and reduce wiki ingest MaxRetry to a moderate shared constant to limit queue churn.
2026-05-08 21:58:51 +08:00
wizardchen 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>
2026-05-08 21:48:52 +08:00
toy0116 bf12868923 fix(wiki): cap requeueFailedOps retry count to prevent queue pile-up
Root cause: requeueFailedOps unconditionally RPush'd every failed op back to
wiki:pending, causing unbounded growth when a document consistently triggers
LLM timeouts. Observed: 553 entries in wiki:pending:08134644 (one KB), mostly
duplicates for ~5 unique documents that each timed out on every batch cycle.

Fix:
- Add wikiFailCountKeyPrefix ("wiki:failcount:") and wikiMaxFailRetries (5)
  constants in wiki_ingest.go
- requeueFailedOps now atomically Incr(wiki:failcount:{kbID}:{knowledgeID})
  before each RPush; skips re-queue (drops op permanently) if count > 5
- Expire the fail-count key at wikiPendingTTL (24h) on every update
- wiki_ingest_batch.go: Del the fail-count key at ingestSucceeded++ so
  transient errors don't permanently burn through a document's retry budget

Evidence of problem severity: 5 persistently-failing docs × ~110 requeue
cycles each = 553 pending entries; had to use Lua atomic dedup to recover.

Behaviour after this fix:
- First 5 failures → normal retry (re-queued, follow-up batch scheduled)
- 6th failure → logged as WARN "dropping op … after 6 failures (limit 5)",
  not re-queued; prevents the feedback loop
- If document is fixed upstream and re-ingested via EnqueueWikiIngest, the
  op is a fresh entry with no existing fail-count key → full 5-retry budget

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:46:36 +08:00