Commit Graph

1457 Commits

Author SHA1 Message Date
Claude 55bc5dcbe2 fix(chunking): post-review polish — error surfacing + doc accuracy
Three small follow-ups from the QA review of 13f57ca:

1. KBChunkingDebug error surfacing
   Previously a 200 OK response with { success: false, error: "..." }
   would have been swallowed under a generic "unexpected response shape"
   message. The strict-shape check now distinguishes empty response,
   success=false (surfaces resp.error directly), and missing data — so
   any future backend-side validation message reaches the user.

2. Token approximation in docs/CHUNKING.md
   Child Chunk Size default 384 ≈ 95 EN tokens (was rounded to 80).
   384 / 4 chars-per-token = 96; "~95" is the honest figure.

3. API surface in docs/CHUNKING.md
   The example only documented PUT /initialization/config/:kbId
   (camelCase, documentSplitting envelope). Added explicit notes that
   POST/PUT /knowledge-bases use snake_case under chunking_config, and
   that POST /chunker/preview also uses the snake_case form plus a text
   field. Readers picking the wrong endpoint won't be surprised by the
   case mismatch anymore.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +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 2cc0322004 fix(frontend): chunking debug panel — visible loading + error states, prevent submit
User report: clicking "Run preview" made the debug section appear to
disappear. Three independent problems contributed; fix all of them.

1. t-button rendered without explicit type="button". TDesign defaults
   vary across versions; if the panel ever lives inside a parent
   <form> (the editor modal does not currently, but this is defense
   in depth), the click would submit the form and reload the page.
   Added type="button" plus @click.prevent.stop on the run button.

2. No loading visual. While the request was in flight, result was
   set back to null, so the result block disappeared with nothing
   in its place. The textarea was still on screen but the spacing
   shifted enough that users could read it as "section gone". Added
   a loading panel with t-loading spinner + status text.

3. Error display was too quiet. A tiny one-line error block could be
   missed entirely behind the modal's scrollable content. Made it
   visually prominent (bold prefix, full-width error background) so
   it can't be overlooked.

Bonus hardening:
- Stricter response-shape check (resp.success !== true || !resp.data
  catches malformed responses, not just non-200s).
- console.error with [KBChunkingDebug] prefix so the failure is
  debuggable from DevTools without enabling verbose logging.
- New i18n key chunking.debug.loading across all 4 locales.

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
Claude beb7d005e9 fix(chunker): QA audit — pointer DTOs, profile reuse, stats correctness
Resolves the issues surfaced by the post-Phase-3 self-review.

High severity
- Strategy / TokenLimit / Languages can now be reset. The KB-config
  handler uses pointer DTOs so the absence of a field in the payload
  means "no change" while an explicit empty string / 0 / [] clears
  the value. The frontend now sends the fields unconditionally so
  the user can revert to defaults via the clear icon — previously
  these fields were write-once.
- DocProfile is no longer computed twice in the preview endpoint.
  Diagnostics carries the profile that drove tier selection (when
  auto-strategy ran); the handler only re-profiles for explicit
  strategies that bypass the profiler.
- Chunk-size stats (avg / min / max / stddev) are now computed over
  the FULL chunk set before the response is truncated to
  previewMaxChunks. Avoids systematically misleading metrics for
  large documents.
- SplitWithDiagnostics defaults SelectedTier to TierLegacy so an
  empty-text request never leaves the diag with a blank tier string.

Medium severity
- Split() reverts to its own loop instead of delegating to
  SplitWithDiagnostics. Diagnostics struct allocation is reserved
  for callers that actually want the trace (preview endpoint).
  Matters in SplitParentChild where Split runs once per parent.
- Preview endpoint runs the splitter on a goroutine wrapped in a
  5s context.WithTimeout. CJK input at the 256k-rune ceiling
  could otherwise tie up the worker for several seconds.

Low severity
- KBChunkingSettings emits arrays via spread copies so parent-side
  mutation cannot drift the form's reactive state.
- KBChunkingDebug font-family includes a monospace fallback so the
  chunk content stays monospaced even when the TDesign CSS variable
  is missing.

Documentation
- DocProfile JSON shape is now annotated as part of the public
  preview API; field renames would be breaking.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 6864a9f9e2 feat(frontend): chunking debug panel with live preview
Adds a collapsible "Test with sample text" panel underneath the
chunking settings in the KB editor. Users paste a Markdown / plain-text
snippet, hit "Run preview", and see exactly how the current chunking
configuration would split it — without re-uploading any document.

The panel calls POST /api/v1/chunker/preview and renders:
- Selected tier (heading / heuristic / recursive / legacy) as a tag,
  with a warning when the chain fell through to legacy fallback
- Rejected tiers + reasons (so users see why "auto" picked tier X)
- DocProfile stats grid: lines, chars, headings, page-breaks,
  chapter markers, detected languages
- Chunk-size summary: count / avg / stddev / min / max
- Per-chunk cards with seq, size (chars + ~tokens), position range,
  context header (when set), content preview (expand/collapse)

The panel reads its config from the live form state so edits to
strategy / chunk-size / overlap / etc. are reflected on the next
preview run — no save needed.

Pieces:
- types/chunker.ts: TS types mirroring the backend response shape
- api/chunker/index.ts: previewChunking(body) wrapper
- views/knowledge/settings/KBChunkingDebug.vue: the panel component
- KBChunkingSettings.vue: embeds the panel + exposes a debugConfig
  computed prop
- i18n keys for en-US / zh-CN / ko-KR / ru-RU (8 keys each)

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude a867b1b29d feat(handler): add chunker preview endpoint for debugging
Adds POST /api/v1/chunker/preview — a read-only endpoint that runs the
adaptive chunker on supplied text and returns the chunks plus
diagnostic info (which tier won, which were rejected and why, the
DocProfile that the selector saw). Used by the upcoming KB editor
debug panel so users can experiment with chunking parameters without
re-uploading documents.

Backend pieces:
- chunker.SplitWithDiagnostics(text, cfg) returns chunks plus a
  Diagnostics struct (SelectedTier, TierChain, Rejected). Split() now
  delegates here.
- chunker.DocProfile gets JSON tags so it serializes cleanly.
- handler.PreviewChunking caps input at 256k chars and chunks at 500
  per response, computes a size-distribution summary, and never
  touches DB or embedding APIs.

Auth: lives under the v1 group so the standard JWT middleware applies.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 46d9c396c6 feat(frontend): expose chunking strategy in KB editor
Surfaces the adaptive 3-tier chunking controls in the existing
"Chunking" section of the KB editor modal. Users can now pick a
strategy from a dropdown without crafting JSON for the API:

- Automatic (recommended)  → backend profiler picks the tier
- Markdown-optimized       → forces heading-aware splitting
- Smart structure detection → forces heuristic boundary detection
- Classic                  → original recursive splitter (legacy)

The fifth backend value `recursive` is intentionally hidden from the
UI — it's functionally near `legacy` and would dilute the choice.

Advanced options (collapsible, disabled when strategy=legacy):
- TokenLimit (0–8192 approx tokens; 0 = char-based budget)
- Languages multi-select (DE/EN/ZH; empty = auto-detect)

Behaviour:
- New KBs default to strategy="auto"
- Existing KBs without the field render with empty strategy and keep
  classic behavior on save unless the user actively picks one
- Overlap warning when overlap >= chunkSize/2

Wired through:
- KBChunkingSettings.vue: strategy select + advanced collapsible +
  ChunkingConfig interface extended
- KnowledgeBaseEditorModal.vue: form-state defaults, kb-load mapping,
  buildSubmitData (chunking_config) + updateKBConfig (documentSplitting)
  pass strategy/token_limit/languages
- api/initialization/index.ts: types extended
- i18n: en-US / zh-CN / ko-KR / ru-RU all carry strategy + advanced
  keys (6 keys each)

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 2feab191bb test(types): regression test that ContextHeader is hidden from JSON
Locks in two invariants exposed during the integration audit:
- ContextHeader on types.Chunk must never leak into the JSON
  representation served to API clients (json:"-" + gorm:"-").
- types.ParsedChunk.EmbeddingContent prepends ContextHeader with a
  blank line separator and falls back to plain Content when the
  header is unset.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude bc44c31cec fix(chunker): QA audit — position invariant, recursive split, perf
Resolves the issues surfaced by a self-review of the adaptive chunking
work. Tests covering each regression were added first to prove the
fix.

Critical
- Heading splitter no longer prepends breadcrumb to Content. The
  breadcrumb now lives on Chunk.ContextHeader; EmbeddingContent()
  combines header + content for the embedder. This restores the
  End-Start == len(Content) invariant relied on by knowledge.go's
  document-reconstruction path (summary generation, UI highlight) and
  also eliminates the duplicate-heading regression where the section
  heading appeared twice in a chunk's body.
- splitBySeparators is now genuinely recursive: when one separator
  leaves a piece > ChunkSize, the next-priority separator is applied
  inside that piece. Mirrors the Python reference. Without this, a
  document with one paragraph break followed by a long run of
  newline-separated lines could emit a single ~1950-rune chunk for
  ChunkSize=300.

Medium
- SplitParentChild forces children onto the recursive tier, skipping
  the per-parent profile pass (was N extra O(N) scans).
- Heuristic splitter snaps overlap start to the nearest semantic
  boundary or newline instead of slicing mid-line / mid-word.
- Strategy.Split returns the legacy tier's output directly when the
  whole chain rejects rather than running SplitText a second time.

Low
- ensureDefaults caps ChunkOverlap at ChunkSize/2 so pathological
  configs (TokenLimit clamping ChunkSize tiny while Overlap stays
  high) cannot produce ~97% overlap chunks.
- DefaultChunkOverlap docstring spells out the migration story for
  KBs that stored ChunkOverlap=0.

ContextHeader plumbing
- chunker.Chunk and types.ParsedChunk gain ContextHeader plus an
  EmbeddingContent() helper.
- types.Chunk gets a transient (gorm:"-") ContextHeader field so the
  knowledge service can build embedding inputs as
  titlePrefix + chunk.EmbeddingContent() without a DB schema change.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude b0fbb8da9f chore(docreader): align Python splitter overlap default with Go
The Go production splitter now uses DefaultChunkOverlap = 80
(~15% of DefaultChunkSize). Bringing the Python sidecar splitter to
the same number prevents subtle behavior drift if the docreader path
ever falls back to it for chunking.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 2850ac78ce feat(retrieval): configurable RRF parameters per tenant
Reciprocal Rank Fusion previously hard-coded k=60 and weights 0.7/0.3.
Both are now sourced from RetrievalConfig (RRFK, RRFVectorWeight,
RRFKeywordWeight) with the same numbers as defaults via three new
GetEffective accessors. fuseOrDeduplicate / fuseWithRRF accept the
config; HybridSearch reads it from tenantInfo and forwards.

Lets corpus owners tune fusion behavior — large corpora benefit from
larger k, keyword-heavy domains from a higher keyword weight, etc. —
without changing application code.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 0dc32cb0c4 feat(retriever): exponential backoff + safety truncation for embeddings
batchEmbedWithBackoff replaces the previous fixed 100ms × 5 retry loop
with proper exponential backoff (200/400/800/1600/3200 ms) and respects
context cancellation between attempts. sanitizeForEmbedding caps any
single embedding input at 20k runes — well above realistic chunk
budgets, so it only triggers on pathologically large inputs and logs
a warning when it does.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 82b0e504b0 feat(handler): expose chunking strategy / token-limit / languages in API
Wires the new ChunkingConfig fields (Strategy, TokenLimit, Languages)
through the documentSplitting DTO of the KB-config update endpoint and
reflects them back in the GET-config response. Existing payloads stay
fully backwards-compatible — only set the fields you want to change.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude d31acf1063 feat(chunker): heuristic Tier 2 splitter
splitByHeuristicsImpl handles documents that lack proper Markdown
headings but contain other structural cues — form-feeds, numbered
sections, multilingual chapter markers (DE/EN/ZH), all-caps section
titles, visual separators (---/===), repeated page footers, and runs
of blank lines. Boundaries are sorted by position; greedy bin-packing
fills chunks up to cfg.ChunkSize and oversize blocks between two
boundaries fall back to the legacy splitter for inner segmentation.

Boundary detection skips fenced code blocks and de-duplicates
overlapping offsets keeping the highest-priority candidate.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude cf8789f84c feat(chunker): heading-aware Tier 1 splitter
splitByHeadingsImpl detects the dominant Markdown heading level via the
profiler and splits the document at headings of that level (or shallower).
Each resulting chunk gets a breadcrumb prefix like
'# Chapter 1\n## Section 1.2' so downstream embedding sees the section
context, not just the body. Sections larger than ChunkSize fall back to
the legacy splitter for inner segmentation, with the breadcrumb prepended
to every sub-chunk and ChunkSize reduced by the breadcrumb length so the
final chunk still fits the budget.

- DominantHeadingLevel: prefer the lowest level with ≥3 hits, else the
  deepest level with ≥1 hit (so small docs with H1+H2 still split at H2)
- HeadingHierarchy state is updated for sub-headings within a section so
  later primary sections see the right active stack
- Code-block contents are skipped during heading detection so a `# foo`
  inside ``` doesn't create a fake boundary
- strategy.go uses context.Background() for the fallback log entry
  (GetLogger panics on nil context)

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 592d3618dc feat(chunker): heading hierarchy tracker
Adds HeadingHierarchy, a level-stack that tracks Markdown heading
nesting (#, ##, ###, ...). When a level-N heading is observed, all
deeper entries are popped — so Breadcrumb() always reflects the active
context path. Used by the upcoming heading-aware splitter to prepend a
context breadcrumb (e.g. '# Chapter 1\n## Section 1.2') to every chunk
inside a section, lifting embedding quality for non-trivial documents.

The existing header_tracker (table headers) uses a binary start/end hook
model that does not fit headings — a heading does not have an explicit
end, it is replaced by the next heading of equal or shallower depth.
A separate level-stack is the right shape.

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude 1585a6f12c feat(chunker): recursive priority splitting + token-aware budget
The Go splitter previously combined all separators into one regex, losing
the priority-based recursive behavior of the Python reference. This pass
walks the separator list in order: the first separator that yields >1
piece wins, mirroring docreader/splitter/splitter.py. Larger semantic
boundaries (\n\n) now win over smaller ones (\n).

Additional cross-cutting changes:
- ChunkingConfig gains Strategy / TokenLimit / Languages fields so the
  per-KB config can pick a tier and pin embedding token budgets.
- buildSplitterConfig in the knowledge service uses the new
  chunker.DefaultChunkSize / DefaultChunkOverlap constants (overlap
  consolidated to 80 ≈ 15%) instead of the previous hardcoded 50.
- ensureDefaults clamps ChunkSize to a per-language character budget
  derived from TokenLimit so chunks stay safe under embedding model
  token caps. Chinese (~1.7 chars/tok) gets a tighter clamp than
  English (~4 chars/tok).

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
Claude ae6fde2310 feat(chunker): foundation for adaptive 3-tier chunking
Adds the building blocks for tiered chunking strategies — multilingual
patterns, token approximation, document profiling, validation, and a
strategy resolver that today still falls through to the legacy
SplitText. Default Strategy is empty/legacy so existing knowledge bases
continue to chunk identically.

- tokens.go: char/token approximation per language (en/de/zh/mixed)
  and a coarse DetectLanguage heuristic
- patterns.go: source-of-truth regex bibliothek for headings, numbered
  sections, multilingual chapter markers, visual separators, etc.
- profiler.go: single-pass document scan + SelectStrategy chain
- validator.go: rejects obviously broken chunk-sets to drive fallbacks
- strategy.go: public Split / SplitParentChild entry points; resolves
  StrategyAuto via the profiler, anything else maps to a fixed tier
- splitter.go: SplitterConfig gains Strategy / TokenLimit / Languages
  fields and DefaultChunkSize / DefaultChunkOverlap constants (overlap
  raised to 80 ≈ 15%)
- knowledge.go: switches both call sites to chunker.Split /
  SplitParentChild — behavior unchanged for empty Strategy

https://claude.ai/code/session_01XADhx6mtu2ZYW3DE9Lun6k
2026-05-06 17:17:07 +08:00
voidborne-d 46cab52fb0 fix(storage): merge tos/s3/oss/ks3 tenant configs in buildStorageConfig (#1117)
knowledgeService.buildStorageConfig had hardcoded switches in two places that
only handled local/minio/cos. Knowledge bases configured for tos, s3, oss, or
ks3 reached the docreader with a DocParserStorageConfig containing only the
Provider tag — bucket, endpoint, and credentials were silently dropped, and
extraction stalled or fetched from the wrong location.

- Tenant-merge switch: add tos/s3/oss/ks3 cases mapping
  StorageEngineConfig.{TOS,S3,OSS,KS3} fields onto DocParserStorageConfig.
  Same shape as the existing minio/cos branches; AccessKey -> AccessKeyID,
  SecretKey -> SecretAccessKey, no AppID.
- Legacy compat switch: list tos/s3/oss/ks3 alongside local with hasKBFull=false
  so fall-through to the tenant-merge path is intentional rather than an
  unrecognised provider sliding past the switch. The legacy `cos_config` column
  predates these providers and they should never resolve via that path.
- Doc drift: types/custom_agent.go ImageStorageProvider comment and
  handler/system.go StorageEngineStatusItem.Name comment now list all 7
  providers (matched against types.StorageEngineConfig and ParseProviderScheme).

Tests (knowledge_storage_config_test.go, +268 LOC, 17 cases):
- TestBuildStorageConfig_TenantMergeAllProviders pins per-provider field
  population for all 7 providers; the tos/s3/oss/ks3 sub-tests are the
  regression guard for #1117.
- TestBuildStorageConfig_LegacyPathOnlyForCOSAndMinIO pins that legacy
  StorageConfig data is only consumed for cos/minio and skipped for the
  others (intentional fall-through).
- TestBuildStorageConfig_NoTenantFallsThroughToEmpty pins the no-tenant case.

Out of scope (called out in the linked issue):
- The systemic refactor (single source-of-truth provider registry) — left as
  a follow-up; this PR keeps changes local to the buggy code path.
- initialization.go validateStorageEngineConfig multimodal validation gap —
  the InitializationRequest struct does not carry tos/s3/oss/ks3 fields, so
  fixing it requires API surface additions; can ship separately.

Closes #1117 (the buildStorageConfig switches; remaining items deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:09:42 +08:00
lyingbug 0d8540c93c Revert "refactor(knowledge): split 9.8k-line service file by responsibility"
This reverts commit f67d0b53ce.
2026-05-06 17:09:24 +08:00
wizardchen f67d0b53ce 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:06:05 +08:00
sqkstwj 0e19c978ae fix(files): fall back to global file service when tenant storage config is unavailable 2026-05-06 17:04:38 +08:00
wizardchen d46a51cf6e fix(security): forbid <style> in sanitized document HTML
DOMPurify was configured with USE_PROFILES.svg, which implicitly allows
<style> tags so that inline SVG styling works. User-authored markdown
rendered via v-html therefore let a pasted nginx error page's
<style>body { width: 35em; margin: 0 auto }</style> through the
sanitizer and apply as global CSS, squashing the entire app layout.

Add 'style' to FORBID_TAGS so top-level style elements from document
content are dropped regardless of the SVG profile.
2026-05-06 16:41:20 +08:00
wizardchen 6ac2f964ca feat(kb-list): replace type column with source and add description subtitle
Document list view relied on a redundant Type column (file extension is
already shown in the filename and icon) while hiding where a document
came from — external syncs from Feishu/Notion/Yuque, web URLs, manual
entries, and uploaded files were indistinguishable.

Swap Type for a Source column that resolves from `channel` first and
falls back to `type`, and surface `description` as a 12px subtitle under
the filename so the list view no longer requires hovering to read the
summary.
2026-05-06 16:41:20 +08:00
wizardchen 2d3f32de5c feat(frontend): replace standalone search page with global command palette
Reworks search from a siloed /platform/knowledge-search page into a
universal ⌘K palette plus natural, in-context entry points.

Why the old page felt out of place:
- "Search" was an action pinned next to entity routes (knowledge base,
  agent, organization), mixing noun- and verb-level navigation items.
- Its two tabs (knowledge vs. chat messages) forced users to pick a
  mental model before they knew what they wanted.
- It was also the only way to search at all — no global shortcut, no
  context-aware entry.

What this replaces it with:
- A new ⌘K/Ctrl+K command palette (GlobalCommandPalette.vue) mounted
  once in views/platform/index.vue. Results are grouped by type
  (files, messages, knowledge bases, agents, chat titles, commands)
  so users never need to pre-filter. Debounced, identity-keyed
  selection survives async arrivals (slow KB results no longer yank
  the highlight off the user's current row). ⌘1-9 jump shortcuts are
  rendered inline on each row for discoverability.
- Sidebar gets a visible "Search" entry at the top — clicking it
  opens the palette (no route change), with a platform-aware kbd hint
  (⌘K on Mac, Ctrl K elsewhere).
- KB detail page: opening ⌘K there auto-scopes the search to the
  current KB via a removable chip (Backspace in empty input also
  clears it); no separate embedded search bar needed.
- The legacy route /platform/knowledge-search now redirects to the KB
  list with ?cmdk= set, so existing bookmarks still work.
- KnowledgeBase.vue now handles both route-query changes and a
  window event (weknora:open-knowledge) for intra-KB navigation,
  fixing the silent-no-op when the palette jumps to a chunk in the
  already-open KB.

Dropped:
- frontend/src/views/knowledge/KnowledgeSearch.vue (1163 lines).
- The exploratory SessionSearchBar mode-switching widget in the chat
  submenu; ⌘K covers its use cases without a two-mode UI.

i18n: zh-CN / en-US / ko-KR / ru-RU updated together — new
commandPalette.*, kb.*, and menu.search keys; obsolete session.search.*
and menu.knowledgeSearch removed.
2026-05-06 16:26:22 +08:00
wizardchen 0be4572bd5 feat(kb-list): group pinned knowledge bases into a labeled section
On the KB list page, pinned items already sort to the top but look
identical to the rest, so at a glance you can't tell where the pinned
group ends. Add lightweight section headers ("置顶" / "其他") that
span the grid when the list transitions from pinned to unpinned, in
both the "全部" and "我的" tabs. Shared KBs never enter the pinned
group (backend doesn't pin them).

No divider line, no backend change — purely a visual grouping on the
existing sort order.
2026-05-06 12:33:04 +08:00
Claude c3e53a3fc4 test(ingest): cover insufficient-content gate; allow threshold override
Addresses two pre-merge review notes:

1. Extract the sentinel-returning threshold check from getSummary into
   a small package-private helper (checkSufficientSummaryContent) and
   add a unit test for it. The new test pins the behavior the caller
   in ProcessSummaryGeneration relies on: the sentinel is returned for
   empty / image-only / sub-threshold / scanned-PDF-without-OCR inputs,
   and nil is returned for legitimate short notes and scanned PDFs
   where VLM OCR succeeded.

   The downstream "errors.Is(err, errInsufficientSummaryContent) →
   SummaryStatusFailed + skip summary chunk" branch is mechanical (no
   branching beyond the errors.Is check); covering it end-to-end would
   require mocking KnowledgeRepository / ChunkRepository / chat.Chat,
   for which there is no precedent in this package.

2. Switch minTextContentRunes from const to var so tests can override
   the threshold and so future config plumbing can adjust it at
   runtime without a rebuild. The new test exercises the override.
2026-05-06 11:23:32 +08:00
Claude 7ca4b2f53c chore(comment): update slug source to knowledge ID
Drive-by comment fix: extractEntitiesAndConceptsNoUpsert still claimed
summary slugs are code-generated from the document title. Since the
previous commit they are derived from the knowledge ID instead.
Pure documentation change, no behavior impact.
2026-05-06 11:23:32 +08:00
Claude 6744e1cdd5 fix(ingest): QA follow-up — preserve OCR text and close remaining filename leaks
Self-review of the prior commit surfaced one critical regression and
several smaller issues. This change addresses them.

Critical: the old stripImageMarkup stripped the WHOLE <image>...</image>
block, but searchutil.EnrichContentWithImageInfo wraps successful VLM OCR
output in exactly that shape — so a scanned PDF where OCR worked was
being rejected by hasSufficientTextContent and routed to the same
"failed" path as the OCR-empty case. The fix splits stripping into:
  * <image_original>...</image_original> blocks removed entirely
    (they only carry the redundant Markdown ref already stripped above)
  * <image>, <images>, <image_caption>, <image_ocr> tags removed at
    the tag level, INNER TEXT PRESERVED, so OCR / caption text reaches
    the threshold check and the LLM call.

A regression test pins this down with realistic enriched-image XML that
the previous code failed silently on.

Other fixes:
  * Threshold lowered from 50 to 10 runes — short legitimate documents
    (brief memos, single-line notes) must still pass.
  * Summary chunk content no longer prefixes "# Document\n<filename>";
    only the LLM-generated summary is indexed, so filenames cannot
    leak back into RAG retrieval context.
  * Wiki summary slug is now derived from the knowledge ID rather than
    the upload filename, eliminating filename-based slugs like
    "summary/mx5280-pdf" that downstream wiki-edit prompts can read.
  * Citation sourceRef likewise drops the "|<docTitle>" suffix.
  * Dead docTitle parameter removed from extractCandidateSlugs,
    extractEntitiesAndConceptsNoUpsert, classifyChunkCitations.
  * The redundant stripImageMarkup pass in the warning log is gone;
    realTextRuneCount caches the count once and uses
    utf8.RuneCountInString to avoid the rune-slice allocation.

Wiki-page display titles still flow through DocTitle (filename) into
later wiki-edit / index-rebuild prompts. That is a larger refactor of
the wiki pipeline and is intentionally left for a separate PR.
2026-05-06 11:23:32 +08:00
Claude 15550d14d4 fix(ingest): stop hallucinating summaries from filenames on empty PDFs
Scanned PDFs without a successful OCR pass (e.g. when no VLM is configured
on the KB, or when the VLM returned a "no text content" reply that the
sanitizer discarded) reach the summary and wiki-ingest pipelines with
nothing but markdown image references for content. The downstream LLM
prompts used to receive the filename, file type, and title alongside that
empty content, and the generate_summary template explicitly forbade the
model from refusing — so the model would invent a topic from the filename
alone, producing e.g. a "Canon scanner manual" summary for an MX5280.pdf
that is actually a scanned legal letter.

Two changes shut this down:

1. Drop filename / title / file-type from every LLM prompt input. The
   wiki summary, knowledge extract, candidate slug, and chunk citation
   prompts now see only the document content. The knowledge summary
   path no longer prepends the "Document Type: X / File Name: Y" intro.

2. Add a content-sufficiency guard in front of the LLM call. A new
   helper strips markdown image references and HTML image tags and
   refuses to invoke the model when fewer than 50 real-text runes
   remain. getSummary returns a sentinel error in that case and the
   caller marks the knowledge as SummaryStatusFailed instead of
   surfacing the first chunk's bare image reference. Wiki ingest skips
   LLM extraction with a clear log message.

The generate_summary.yaml prompt is also updated: the "NEVER output
refusal phrases" clause is removed and replaced with an explicit
empty-content rule that returns a deterministic placeholder line.

Real OCR (Tesseract / PaddleOCR) and filename-pattern sanitisation are
intentionally left for separate PRs.
2026-05-06 11:23:32 +08:00
guangyang1206 d3d433574f fix(data-analysis): capture LOCAL_STORAGE_BASE_DIR at init time (#1040)
resolveFileServiceForKnowledge was reading os.Getenv("LOCAL_STORAGE_BASE_DIR")
on every request.  This causes a mismatch when the variable is not in the
process environment at request time (e.g. it was set programmatically before
app startup but not exported, or the DI-constructed fileService uses a
different configured path).

Fix: store the value in a new localBaseDir field at NewDataAnalysisTool
construction time — the same moment the DI container assembles the local
FileService — and use t.localBaseDir in resolveFileServiceForKnowledge
instead.  This guarantees the data-analysis tool always resolves files
under the same base directory that the local FileService was configured
with, eliminating the "path not found" errors when LOCAL storage is used
for a knowledge base while the global storage backend is minio/oss/cos.

Fixes #1040
2026-05-06 11:19:56 +08:00
c e938f1c6ae test(agent): expand graph query regression fixture 2026-05-06 11:04:55 +08:00
c b2ff29cc0f fix(agent): correct graph config reporting 2026-05-06 11:04:55 +08:00
c f08192ba26 fix: include shared knowledge in knowledge search 2026-05-05 01:04:49 +08:00
toy0116 05d35c904b feat(frontend): respect BASE_URL so the SPA works behind a path-prefix reverse proxy
When WeKnora is mounted under a sub-path (Kubernetes Ingress with a
path-prefix rule, an Nginx `location /weknora/` block, a Cloudflare
Tunnel route, or any other reverse-proxy gateway), the frontend's
`getApiBaseUrl()` currently returns `''` and axios issues `/api/v1/...`
requests at the origin root — bypassing the sub-path entirely and
404'ing every backend call.

This change makes `getApiBaseUrl()` honor Vite's
`import.meta.env.BASE_URL` so that, when the bundle was built with e.g.
`vite build --base /weknora/`, axios produces `/weknora/api/v1/...`.
Trailing-slash stripped to avoid double slashes (`/weknora//api/v1/...`).

For deployments at the origin root (the common case), `BASE_URL`
defaults to `'/'` which strips to `''` — behavior unchanged from
upstream. No new build flag required; no behavior change for existing
users.

Reproduce the original problem:

  1. cd frontend && vite build --base /weknora/
  2. Serve dist/ behind Nginx at `location /weknora/`, proxy
     /weknora/api/* → backend.
  3. Open https://example.com/weknora/ → 404 on every API request
     because axios calls /api/v1/auth/test instead of
     /weknora/api/v1/auth/test.

After the fix the same setup just works.
2026-05-05 00:58:19 +08:00
wizardchen e4fe9d7d9b fix(i18n): remove duplicate liveIndicator key in ko-KR locale
Vite raised `Duplicate key "liveIndicator" in object literal` at
ko-KR.ts:4107. Both entries mapped to the same Korean translation so
the second is simply redundant. Removing it silences the warning
without changing any user-visible string.

Verified no other duplicate keys remain in en-US / zh-CN / ko-KR /
ru-RU with a brace-scope-aware scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:54:43 +08:00
Claude 35a5e29a23 fix(frontend): drop infectious preference fallback, add migration latch
QA review of the previous hardening commit identified two cross-user
infection paths created by the smart-fallback read chain (CRIT-1/2),
plus a UI-desync hazard from validation guards in setters (HIGH-1).

- Extracted localStorage helpers and migration logic into
  composables/preferenceStorage.ts as a single source of truth.
- Reads now hit only the active user's namespace - no anon / legacy
  fallback in the hot path. Pre-existing values are migrated **once**
  per active user via migratePreferencesIntoUser, which adopts anon
  and legacy keys into the user namespace and then deletes the source
  keys, so a second user logging into the same browser cannot inherit
  the previous user's preferences.
- Migration latch tracks which user has already been migrated this
  session; auth store's reloadUserPreferences resets the latch on
  setUser/logout so the next active user gets a fresh migration.
- setSansFont / setMonoFont / setFontSize / setTheme now return a
  boolean; GeneralSettings.vue handlers roll the form back to the
  canonical state when validation rejects a value, preventing the
  UI from drifting out of sync with the source of truth.
- safeSetItem now console.warn on persistence failures so quota /
  private-mode issues are at least diagnosable.
- applyFont is diff-aware: only the CSS variables that actually
  changed are written to the root style, avoiding unnecessary style
  recalculation when only one knob is flipped.
- applyTheme caches the last effective mode to skip redundant
  attribute writes and Wails IPC calls.

https://claude.ai/code/session_01S1FVPf4jRfWXkTSnpMmMp7
2026-05-05 00:48:40 +08:00
Claude 2e0b423057 fix(frontend): harden font/theme preferences for production edge cases
Edge-case review identified several issues that would degrade UX or
silently break under unusual conditions:

- Anon settings chosen on the login screen were lost on first login
  because the storage namespace switched from "anon" to the user id;
  resolveStorageValue now falls through user -> anon -> legacy, so
  pre-login choices carry over and v1 unscoped keys are still picked up.
- localStorage.setItem can throw under quota / private mode; setters now
  swallow the error so the UI still updates for the session.
- applyFont reduced to String(undefined) when currentSize held an
  invalid value; runtime fallbacks to the default scale guard against
  manipulated storage.
- Added var(--app-font-scale, 1) defaults to all 32 calc() expressions
  in theme.css and the body font-size, so a missing variable cannot
  void the calc() value.
- Validation guards in setSansFont / setMonoFont / setFontSize / setTheme
  reject invalid keys instead of writing them to storage.
- GeneralSettings.vue now watches the composable refs so the form stays
  in sync if preferences change externally (e.g. user switch while on
  the page).
- Dropped the unverified t-radio-group variant prop in favour of the
  default style.

https://claude.ai/code/session_01S1FVPf4jRfWXkTSnpMmMp7
2026-05-05 00:48:40 +08:00
Claude bafcbd5e5f feat(frontend): add font size, namespace UI prefs per user
Build on the font picker with three additions per user feedback:

- Font size selector (Small / Normal / Large) wired through a new
  --app-font-scale CSS variable; the body font-size and every
  --td-font-size-* token are wrapped in calc(... * var(--app-font-scale))
  so TDesign components scale together with the body
- localStorage keys for theme, font family, mono font and font size are
  now namespaced as WeKnora_${userId}_${key}; falls back to "anon" before
  login. Each user keeps their own preferences in the same browser
- auth store calls reloadFontFromStorage / reloadThemeFromStorage on
  setUser (when the active user changes) and in logout, so a user switch
  swaps the applied preferences without a reload

https://claude.ai/code/session_01S1FVPf4jRfWXkTSnpMmMp7
2026-05-05 00:48:40 +08:00
Claude f140c12518 feat(frontend): unify font rendering and add font picker in settings
The UI used hardcoded "PingFang SC" in 50+ components, while the global
font stack in App.vue used a different fallback chain. Outside of mainland
China this caused visibly different fonts in the sidebar, breadcrumbs,
search and agent views compared to settings and document panes.

- Introduce --app-font-family / --app-font-family-mono CSS variables in
  theme.css and route TDesign's --td-font-family through them
- Add a useFont composable that persists the user's choice in localStorage
  and applies it via document.documentElement.style before mount
- Replace hardcoded font-family declarations across the app with the new
  variables (TencentSans logo and the SVG attribute list are preserved)
- Add UI font and monospace font selectors in General Settings with
  i18n strings for en-US, zh-CN, ko-KR and ru-RU

https://claude.ai/code/session_01S1FVPf4jRfWXkTSnpMmMp7
2026-05-05 00:48:40 +08:00
wizardchen 6c1442aaf9 fix(im): clamp reconnect backoff to avoid int64 overflow → busy loop
After ~34 failed reconnects, math.Pow(2, attempt-1) * baseDelay overflowed
int64 and produced a negative time.Duration, which bypassed the max-delay
cap. time.After(-1s) fires immediately, so a single bad credential (e.g.
WeCom bot_id/secret error 853000) turned into thousands of reconnects
per minute and massive log spam ("reconnecting in -1s (attempt 4601)").

Replace math.Pow with a bit-shift and cap the exponent at 30, which is
well above the shift needed to reach the 30s max delay. Same fix applied
to the WeChat long-poll client. Added a regression test that exercises
attempt=4601.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 00:36:18 +08:00
ChenRussell 42348eb40d feat(ks3): add KS3 object storage support
Add Kingsoft Cloud KS3 as a supported storage provider, including
  backend service implementation, storage config UI, and provider
  scheme recognition throughout the codebase.
2026-05-05 00:22:27 +08:00
Windfarer 6f3679716f fix oidc auth login 2026-05-05 00:21:41 +08:00
bingxiang.cheng db8abd3f9b feat(openmaic-classroom): 增加基于知识图谱概念的微课堂生成支持,更新需求构建模板 2026-05-04 23:54:15 +08:00
IFeynman 19cd17c0a6 fix(wiki): apply reviewer's detailed resolution for contradiction handling 2026-05-04 23:52:38 +08:00
IFeynman cf8e62fa9e fix(wiki): address PR review feedbacks regarding citation format, rule conflicts, and search loops 2026-05-04 23:52:38 +08:00
IFeynman b313da20ae feat(wiki): enforce strict citation tracing and ontology reuse in Wiki prompts 2026-05-04 23:52:38 +08:00
Andy Yang 3d94a476bf fix(memory): dedupe episode write on duplicate streaming Done
AgentFinalAnswer may carry Done:true more than once (e.g. finish chunk
plus EOF). Guard AddEpisode with sync.Once so memory persists once per
turn, consistent with qa.go completion handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 23:49:29 +08:00
c 48aeafcc3f fix(chat): include history in model fallback 2026-05-04 23:24:21 +08:00