Commit Graph
874 Commits
Author SHA1 Message Date
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
杨明康andClaude Opus 4.7 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
wizardchenandClaude Opus 4.7 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
toy0116andClaude Sonnet 4.6 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
toy0116andClaude Sonnet 4.6 edbcfa80ab fix(wiki-ingest): prevent retry exhaustion on concurrent lock conflict
Root cause: when multiple wiki:ingest tasks queue up for the same KB,
they all hit ErrWikiIngestConcurrent and retry every 15 s. With
MaxRetry=10 (150 s window), any batch taking longer than ~2.5 min
caused pending tasks to exhaust retries and land in archived.

Confirmed via Redis inspection: archived wiki:ingest task had
retry=10, retried=10, error="concurrent wiki task active".

Two-part fix:

1. Early-exit at lock conflict when pending list is empty.
   If another batch is active and the pending list is already empty,
   there is nothing left to process — the concurrent task will handle
   everything. Return nil (success) immediately instead of burning
   through retry slots on a guaranteed no-op. Only retry when the
   pending list still has items, i.e., we have real work to do.

2. Increase MaxRetry 10 → 25 across all four enqueue sites.
   25 × 15 s = ~6 min window; accommodates large KB batches and the
   60 s orphan-lock expiry with generous headroom. Combined with the
   early-exit above, redundant tasks now consume 0 retries instead of
   10, so the remaining budget is entirely available for genuine
   concurrency conflicts.

No API or schema changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:46:36 +08:00
wizardchen 576671e64a fix: propagate user attachments to agent query in AgentQA
Attachments uploaded from the UI were correctly parsed by the handler and
set on QARequest.Attachments, but the Agent path dropped them before
calling engine.Execute. Only the KnowledgeQA pipeline injected them via
Attachments.BuildPrompt(), so in "smart reasoning" (Agent) mode the model
never saw the uploaded file and answered as if no attachment existed.

Append Attachments.BuildPrompt() to agentQuery alongside QuotedContext,
mirroring chat_pipeline/into_chat_message.go. This keeps the engine
signature unchanged and matches the existing KnowledgeQA behavior.
2026-05-08 19:09:58 +08:00
wizardchen 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.
2026-05-08 13:32:54 +08:00
wizardchen 92e02c1d49 chore(llm): add fire-once OpenAI-layer stream diagnostic logs
Add per-stream once-only logs at the OpenAI-protocol layer so we can
triage streaming behavior (natural-stop vs tool-call, TTFC, ordering of
reasoning/content/tool_calls) without grepping through every delta.

streamState now tracks:
  - streamStartedAt    : baseline for elapsed_ms on each fire-once log
  - firstContentSeen   : delta.Content first appearance
  - firstReasoningSeen : reasoning_content first appearance
  - firstToolCallSeen / noToolCallStopLogged (existing flags retained)

Logs emitted at most once per stream:
  [LLM Stream] First reasoning_content at OpenAI layer (len, preview, elapsed_ms)
  [LLM Stream] First delta.Content at OpenAI layer (len, preview, tool_call_seen, thinking_seen, elapsed_ms)
  [LLM Stream] First tool_calls delta at OpenAI layer (count, first_id, first_name, first_content_seen, thinking_seen, elapsed_ms)
  [LLM Stream] Natural-stop at OpenAI layer (finish=stop, tool_calls field never observed, thinking_seen, first_content_seen, elapsed_ms)

Together with the existing agent-layer "Natural-stop candidate detected"
log this lets a single grep reconstruct the temporal layout of one stream
(reasoning -> content -> tool_call OR natural-stop) and tell apart:
  (A) tool_calls field truly absent (real natural-stop)
  (B) tool_calls field arrived but high-level marker not yet emitted

Pure logging change; no behavior change. Previews use the existing
truncateForDebug helper (rune-safe, capped at 80 chars).
2026-05-07 18:28:28 +08:00
wizardchen 1d3bd8bb13 chore(agent): log natural-stop candidate when no tool calls
Add explicit response logging for the branch where the model returns
finish=stop with zero tool calls. This makes it easier to identify the
natural-stop path early in logs and correlate rounds that are likely to
be treated as final-answer candidates by analyzeResponse.

- include `tool_calls=0` in the no-tool summary log
- emit a dedicated "Natural-stop candidate detected" info log when
  finish_reason is stop and no tool calls are present
2026-05-07 18:28:28 +08:00
bingxiang.cheng a232b12ff0 fix(agent): 修复WeKnora对话完前端未显示结束标记问题
fix(agent): 修复流式回答think内容与answer混淆问题
2026-05-07 16:34:20 +08:00
draix 7adf88766a fix(graph): recover JSON from malformed/truncated fences in extract_entity
Closes #1113

The graph extraction pipeline relies on a strict markdown fence regex to
pull JSON out of LLM responses. In production the regex misses ~84% of
real-world responses, in three ways:

  1. The LLM hits max_tokens mid-output and produces no closing fence.
  2. The opening fence is malformed or surrounded by prose, so the
     non-greedy regex fails to anchor a match.
  3. The model returns raw JSON with stray backticks but no real fence.

In every case extractContent fell through to returning the raw text,
which then failed json.Unmarshal with errors like
"invalid character '` + "`" + "' looking for beginning of value".

This change keeps the existing happy path untouched and adds a
conservative recovery step in the default branch of extractContent:

  - If an opening ``` is present, take everything after it, drop a
    likely language tag on the first line, cut at any trailing closing
    fence, and trim stray backticks/whitespace.
  - Otherwise, look for an outermost JSON object/array in the text using
    a small bracket-balanced scanner that respects string literals, so
    embedded {} or [] inside JSON strings don't confuse it.
  - Only fall back to the original raw-text behavior when neither
    strategy yields anything plausible.

The recovery helpers (stripFencesAndExtract, extractJSONLike,
isLikelyLanguageTag) are package-private and have table-driven tests in
extract_entity_test.go covering the three failure patterns described in
the issue, plus the previously-working fenced and bare-JSON shapes to
guard against regressions.
2026-05-07 15:40:19 +08:00
wizardchen fb9ae11aa8 fix: 为前端静态资源设置正确的 Cache-Control 响应头
修复升级前端版本后,用户必须退出重新登录才能看到新版页面的问题。

根因:nginx 和 Lite 版 Go 静态服务均未设置 Cache-Control,浏览器按
启发式策略长期缓存 index.html,导致刷新仍加载旧版入口 HTML,旧版
引用的 hash 文件名也就不会变更。

改动:
- frontend/nginx.conf: index.html 及 SPA fallback 设 no-cache,
  must-revalidate;/assets/* (Vite 产物带 hash 文件名)设一年
  immutable 长缓存。location 内重复声明原有安全头,避免 nginx
  add_header 不继承导致安全头丢失。
- internal/router/router.go: Lite 版 serveFrontendStatic 按同样
  策略设置 Cache-Control。
2026-05-07 12:08:02 +08:00
wizardchen d7478094a2 fix(chunker): recognise CN chapter titles and multi-level numeric headings
The heuristic splitter relied on two regexes that were too strict for
real-world Chinese technical documents:

- ChineseChapterPattern required 第 / numeral / unit to be adjacent, so
  the very common "第 1 章 引言" form never matched.
- NumberedSectionPattern required a trailing dot after the numeral, so
  multi-level numbering such as "1.1 文档目的" or "2.2.1 用户与权限"
  was missed.

As a result, documents like the CHAPTER_SAMPLE shipped with the chunking
debug drawer collected zero heuristic markers, and ProfileDocument fell
all the way through to the character-level Legacy tier, producing chunks
that ignored the document's explicit chapter structure.

- Loosen ChineseChapterPattern to tolerate spaces around 第 / 数字 / 单位.
- Allow the multi-level branch of NumberedSectionPattern to drop its
  trailing dot; keep the single-level / roman branch dot-required to
  avoid false positives on version strings.
- Extend patterns_test.go with positive and negative cases covering the
  CHAPTER_SAMPLE wording plus deep-nesting and lone-numeral regressions.
2026-05-06 22:14:16 +08:00
wizardchen d703a14745 feat(chunker): SplitParentChild children honour configured strategy
Children were forced to StrategyRecursive on the assumption that
re-profiling each parent would be too expensive. In practice profiling
each parent is bounded by O(sum(parent_size)) ≈ O(N) total, the same
order as the original parent profiling pass — and the gain is real:
when a parent (e.g. an H1 chapter) contains its own sub-headings, the
heading splitter on child input now picks them up and generates a
finer-grained breadcrumb instead of every child sharing the parent's
top-level breadcrumb.

Add mergeBreadcrumbs to combine the parent ContextHeader with the
child's freshly-derived one, dropping the duplicated seam line that
appears when the child's first heading equals the parent's last.

Tests cover (a) sub-headings now appearing in child breadcrumbs and
(b) the merge dedup against duplicated seam lines.
2026-05-06 21:16:48 +08:00
wizardchen 7b9a831db3 fix(chunker): trim surrounding whitespace before embedding
Heuristic and recursive splitters preserve original byte positions in
Chunk.Content (the End-Start == RuneCountInString invariant is required
by document-reconstruction code paths), which means a chunk sliced at a
boundary often carries leading/trailing newlines from the boundary
itself. Feeding that whitespace into the embedding model dilutes the
vector and wastes tokens for no benefit.

TrimSpace the body inside EmbeddingContent for both chunker.Chunk and
the two types.Chunk / types.ParsedChunk mirrors. Inner whitespace is
preserved; positions/Content are unchanged.

Also clean up appendChunk: the previous code computed a trimmed string
for the empty-check then stored a separate untrimmed copy, which read
as if it might be intentionally divergent. Replace with an explicit
'raw text, skip if pure whitespace' shape and document the invariant.
2026-05-06 21:16:48 +08:00
wizardchen ad69240ac1 fix(chunker): heuristic splitter drops boundaries inside protected spans
Heuristic boundary detection (numbered sections, all-caps headings,
\\n{3,} blank blocks, etc.) ran on the raw text and could land inside
atomic regions handled by protectedPatterns — most notably LaTeX
$$...$$ blocks, Markdown tables, fenced code, and image/link refs.
A boundary inside such a region would cause the bin-packer to slice
through protected content, defeating the protection.

Convert protectedSpans output to rune offsets once and filter the
boundary list before bin-packing. Boundaries on a span edge are kept
(they align with the span) — only strictly-interior ones are dropped.
2026-05-06 21:16:48 +08:00
wizardchen 151e999db1 perf(chunker): thread DocProfile through splitter dispatch
Strategy.Split / SplitWithDiagnostics already run ProfileDocument once
when the auto strategy resolves the chain. splitByHeadingsImpl was
rerunning the same O(N) pass on entry, so every auto-mode call paid
2x scan cost; SplitParentChild paid 2x per parent.

Pass the profile down through runTier and let splitters compute their
own only when called outside the auto path (where profile == nil).
2026-05-06 21:16:48 +08:00
wizardchen 5a79a817a1 fix(chunker): heuristic splitter overlap was always zero
applyOverlapAligned searched for the latest boundary in
[curEnd-2*overlap, curEnd], but curEnd itself is always one of the
boundaries (the bin-packer only flushes at boundary positions). The
loop therefore always returned curEnd, producing chunkStart == curEnd
and zero overlap regardless of cfg.ChunkOverlap.

Exclude curEnd from the search window so an earlier boundary can be
picked, restoring the intended overlap behaviour.
2026-05-06 21:16:48 +08:00
wizardchen 6efe781a50 refactor(chunker): drop redundant TierRecursive
TierRecursive and TierLegacy both invoked SplitText with identical
output, but only TierLegacy got the "always-return-on-validation-failure"
safety-net behavior. Auto chains thus ran SplitText twice on the
fallback path and produced two-line debug rejection traces with the same
reason.

Inline TierRecursive into TierLegacy: SelectStrategy and the explicit
StrategyRecursive entry point now emit single-legacy chains. The
StrategyRecursive public constant stays so existing ChunkingConfig rows
keep parsing — it's just an alias for legacy now.

No user-visible behavior change; one fewer SplitText call per failed
auto fallback.
2026-05-06 20:23:05 +08:00
wizardchen 2f86e5b13d feat(chunker): coalesce tiny adjacent chunks in heading splitter
Documents with many short headings (FAQ-style, quick refs) used to fail
the heading-tier validator with "too many tiny chunks" and silently fall
back to legacy splitting, defeating the purpose of heading-aware mode.

Merge physically adjacent sections whose combined size still fits within
ChunkSize, deriving a shared breadcrumb via commonHeadingPrefix so the
merged chunk's ContextHeader stays meaningful.

Tests cover the merge path, position-invariant preservation after merge,
ChunkSize ceiling, and the breadcrumb prefix helper. Existing tests that
relied on tiny fixtures were grown so each section stays distinct.
2026-05-06 20:23:05 +08:00
wizardchen d4c30126a9 refactor(knowledge): split 9.8k-line service file by responsibility
internal/application/service/knowledge.go had grown to 9883 lines /
149 functions spanning CRUD, document processing, summary/question
generation, clone/move, FAQ (CRUD+import+index+export), wiki cleanup,
multimodal image, and file utilities. Navigating it, or diffing
changes in it, was increasingly painful.

Purely mechanical split into 8 files in the same package. No
signatures, behaviors, or types were modified. knowledgeService
struct, NewKnowledgeService, and package-level errors remain in
knowledge.go unchanged. Per-file imports were trimmed by goimports.

  knowledge.go              497  struct + constructor + errs + CRUD reads + Search
  knowledge_create.go      1144  CreateFrom{File,URL,Passage,Manual} + helpers
  knowledge_delete.go       686  Delete + wiki cleanup + ProcessKnowledgeListDelete
  knowledge_process.go     2407  ProcessDocument/Summary/Question + Reparse + UpdateImageInfo
  knowledge_clone_move.go  1049  Clone* + Move* + progress persistence
  knowledge_faq.go         1932  FAQ CRUD + Search + Export
  knowledge_faq_import.go  1939  FAQ import/validate/index + runningFAQImportInfo
  knowledge_util.go         352  file-type/url/hash + VLM+Storage config + resolveFileService

Verification: go build, go vet, gofmt on the 8 files, and
golangci-lint all clean (22 issues post-split matches 22 pre-split
in knowledge.go — no new issues introduced). The pre-existing
unused warnings on getVLMConfig / buildStorageConfig were left as-is
rather than removing dead code beyond the scope of this task.
2026-05-06 17:40:10 +08:00
Claude 49adf0d73c docs(chunking): align overlap default + sharpen UI/source/repo docs
Three documentation passes around the adaptive chunking work:

UI
- Frontend ChunkOverlap default consolidated to 80 (was 100), matching
  chunker.DefaultChunkOverlap on the backend. Both DEFAULT_CHUNKING_PRESET
  and initFormData updated. The KB-load fallback also uses 80 when a
  loaded KB has no chunk_overlap stored.
- All four locales (en-US, zh-CN, ko-KR, ru-RU) get rewritten chunking
  setting descriptions: each now states the validated range, the default,
  and the situations where you'd deviate (FAQ vs narrative, embedder
  token limits, language-specific corpora).

Source code
- splitter.go: DefaultChunkSize / DefaultChunkOverlap constants get a
  longer block-comment explaining the per-language token math and the
  use-case sweet spots, plus the migration note on what the old
  inconsistent defaults were.
- KBChunkingSettings.vue: new comment block above ChunkingConfig
  documents the slider min/max for each setting, why those bounds
  exist, and the recommended TokenLimit values per embedding model.

Repo docs
- New docs/CHUNKING.md: end-to-end guide covering why chunking matters,
  the adaptive 3-tier architecture, per-setting reference with ranges
  and sweet spots, parent-child explanation, the token-limit table per
  embedder (OpenAI / Voyage / Cohere / BGE / MiniLM / Jina), 7 use-case
  presets, the debug panel workflow, the API surface, and known
  trade-offs (recursive strategy hidden from UI, no auto-reindex on
  strategy switch, OCR limitations).
- CHANGELOG.md gets a new [Unreleased] section consolidating all the
  adaptive-chunking work shipped on this branch: 5 features, 8
  improvements, 6 fixes, 1 docs entry. The entry references
  docs/CHUNKING.md for deeper explanation.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 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
2026-05-06 17:17:07 +08:00