06feee0fee1904ee5b4e738d98015ccd65caa2d3
1659 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
06feee0fee | feat: modify the skills registration directory | ||
|
|
69fbbfc252 |
chore(cli): gofmt sweep
`gofmt -w cli/` — trailing newlines, minor whitespace alignment. Caught during PR-readiness review (`gofmt -l .` had been non-clean since the em-dash → ASCII bulk replace earlier in the v0.4 series). |
||
|
|
f2e8e3f56c |
refactor(cli): drop aiclient package; align AGENTS.md with mainstream
Survey of 10 mainstream CLIs (gh, lark, stripe, vercel, supabase, aws, azure, gcloud, openai/codex, github-copilot-cli) showed env-gated per-command --help blurbs are a Stripe-only pattern; gh uses env detect for telemetry only, and lark relies on installed agent Skills + MCP. Our cmd/mcp/serve already covers the dominant 2025/26 path, so internal/aiclient/ (136 LOC + 38 callsites) is net maintenance burden without precedent. - Drop internal/aiclient/ entirely (annotations + detect + tests) - Remove 38 SetAgentHelp callsites + agentAwareHelpFunc / SetHelpFunc wiring in cmd/root.go - Migrate 4 command-level rules to standard Long help (visible to all, not env-gated): doc upload mode mutex, kb edit at-least-one, kb pin idempotent, search chunks channel mutex - Rewrite AGENTS.md as a developer guide (gh-style 6 H2 / 167 lines): audience preamble + Build / Architecture / Command Structure / Testing / Code Style / Error Handling. Drops sections absent in surveyed projects (Commit & PR Conventions, Who Uses This CLI) - Clean 14 internal doc refs (ADR-N, spec §X, v0.X) in source comments and docs that pointed at docs/superpowers/ — that directory is local-only / uncommitted, so refs are dead for outside readers - Drop forward-looking "once v0.2 ships" from README |
||
|
|
a0dd989c81 |
refactor(cli): auth security audit — gh CLI parity hardening
Compared the auth subtree (login/logout/list/status/refresh/token)
against gh CLI's auth implementation. Three gaps closed:
1. `auth login --with-token` validates the API key against `/auth/me`
before persisting (mirrors gh's pre-persist GetCurrentLogin probe).
A typo'd / expired / wrong-host key fails fast with
`auth.bad_credential` (exit 3) and nothing is written to the
keyring. Side benefit: api-key contexts now carry the resolved
`user` + `tenant_id` at rest, so `auth list` reflects who owns
the key — previously these columns were blank for `--with-token`
contexts because we never queried the server.
2. `auth login` prints a stderr advisory when the secrets store falls
back to the 0600 plaintext file (keychain unavailable — typical on
headless CI, WSL without DBus, agent containers). `weknora doctor`
carried the same info in its credential_storage check, but users
who go straight to `auth login` could miss it. gh has the same
silent-fallback gap; we're stricter here.
3. AGENTS.md adds an "Auth security contract" section documenting:
- Credential storage (keychain primary, 0600 file fallback)
- `--with-token` reads stdin (not flag value), pre-validated
- No env-var token bypass — by design, to avoid the
`/proc/<pid>/environ` / `ps -E` leak surface that
`GH_TOKEN`-style env vars expose
- `auth status` / `auth list` never emit token values
- `auth refresh --json` returns only `{context}` (never the
new tokens)
- `auth token` stdout has no trailing newline + TTY stderr hint
- `auth logout` is local-only (no server-side revocation)
Verified against gh CLI behavior (cli.github.com manual + cli/cli
trunk source):
| dimension | gh | weknora v0.4 |
|---------------------------------|----------------|--------------|
| pre-persist token validation | ✓ | ✓ (new) |
| OS keychain primary | go-keyring | go-keyring |
| stderr warning on file fallback | ✗ silent | ✓ (new) |
| `auth status` default token | masked prefix | not shown |
| `auth token` TTY warning | ✗ | ✓ |
| env-var token bypass | ✓ (GH_TOKEN) | ✗ by design |
| process-args / `ps` leak surface| ✗ stdin only | ✗ stdin only |
|
||
|
|
e623e8208f |
refactor(cli): delete envelope infrastructure, errors to stderr
Removes the entire envelope machinery now that every success path
emits bare JSON:
- cli/internal/format/envelope.go (Envelope, Success, Failure,
SuccessWithRisk, WriteEnvelope, Meta, Notice, UpdateNotice,
VersionSkewNotice, Risk, RiskLevel, ErrorBody) + tests.
- cli/internal/format/filter.go envelope-specific helpers
(WriteEnvelopeFiltered, marshalEnvelope, applyFieldFilter,
filterDataPayload, filterObjectData); the reusable
filterArrayItems / filterObjectKeys / writeJQ stay for bare.go.
- cli/internal/cmdutil/exporter.go + tests (envelope-only).
- cli/internal/cmdutil/PrintErrorEnvelope + ToErrorBody +
operationRiskOf + Error.OperationRisk field + OperationRisk struct.
Error path: all errors now go to stderr via cmdutil.PrintError in
`code: message\nhint: ...` form, regardless of --json. Stdout stays
empty (or holds the partial-success the command already wrote) so
downstream `--json | jq` pipelines never have to filter error shapes
out of the success stream. Typed exit codes (3 auth.* / 4
resource.not_found / 5 input.* / 6 server.rate_limited / 7 server.*
+ network.* / 10 input.confirmation_required) carry the failure
class for agents that branch on it.
Acceptance contract:
- envelope_test.go → wire_test.go (TestEnvelopeGolden → TestWireGolden).
- testdata/envelopes/ → testdata/wire/.
- Error-path cases assert the typed code substring on stderr.
- Orphan whoami.*.json goldens deleted.
AGENTS.md + README.md rewritten for the bare-data contract:
- Drop envelope schema section + dry-run rule.
- Document bare JSON on stdout + `code: msg\nhint: …` on stderr.
- ADR-3 reframed around bare data and why error separation matters
for `--json | jq` pipelines.
WriteJSONFiltered short-circuits to WriteJSON when both filters are
empty (skip the marshal-buffer round-trip for the common case).
Final review pass:
- Fix wire-contract bug: `--json id,name` (space form) is broken by
pflag's NoOptDefVal; AGENTS.md / README.md / SetAgentHelp + the
field-discovery help text all switched to `--json=id,name`.
- Fix `weknora api --jq` silently ignored: api.go now routes through
WriteJSONFiltered with jopts.JQ.
- AGENTS.md: drop the false claim that `auth logout` honors `-y`
(logout is local-only with no ConfirmDestructive guard); list the
actual destructive commands instead.
- Rewrite cli/acceptance/e2e/e2e_test.go for the bare-data wire shape
(was still parsing `out["data"]` / `env["ok"]`).
- Add `JSONOptions.Emit(w, v)` helper; collapse ~33 repeated
`format.WriteJSONFiltered(iostreams.IO.Out, X, jopts.Fields,
jopts.JQ)` sites to `jopts.Emit(iostreams.IO.Out, X)` — drops the
format import from 22 cmd/* files.
- Delete single-caller `cmdutil.MustRequireFlag`; inline as
`_ = cmd.MarkFlagRequired(...)` everywhere.
- Add `_ = cmd.MarkFlagRequired("name")` to `kb create`; it was the
only write command relying on runtime --name validation while
`context add` already used the cobra-level mark.
- `context use`: register `--json` / `--jq` (was always emitting JSON
unconditionally with no human path and no flag — diverged from
every other write command); human mode now prints
`✓ Switched context to X (was Y)`.
- Replace per-package `confirmPrompter` / `scriptedConfirm` /
`errPrompter` test doubles with `testutil.ConfirmPrompter`.
- Rename `chatService` → `ChatService` (export to match siblings
`ListService` / `ViewService`); rename `printUploadSuccess` →
`renderUploadSuccess` (siblings use `render*`).
- `defaultHint(CodeResourceNotFound)`: drop the hardcoded
"list available with `weknora kb list`" — misleading on agent /
doc / session 404. Replaced with "verify the resource ID and try
again".
- Strip stale `v0.2/v0.3` / "envelope" / "v0.0/v0.1 supports only"
historical tags from production comments and a few test
descriptions.
|
||
|
|
cc8254f862 |
refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):
1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
(gh, aws, stripe, lark): none expose --dry-run. The mainstream that
does (kubectl/git/helm/ansible) operates on declarative manifests
or local state where the preview is materially different from the
executed action. WeKnora's CLI just echoed the same parameters
that would have gone on the wire — the preview added no real
signal over `--help` + reading the call site. Removes:
- root --dry-run persistent flag + cmdutil/dryrun.go
- DryRun fields + EmitDryRun calls in 12 write commands
- format.Envelope.DryRun field
- 8 corresponding *_test.go cases
- --dry-run mention from README.md and CHANGELOG.md
- "dry_run":false from 16 golden envelopes
2. Migrate every --json output to bare data:
- New format.WriteJSON / WriteJSONFiltered helpers
(cli/internal/format/bare.go) share filterArrayItems /
filterObjectKeys / writeJQ with the (still-live for now) envelope
filter helpers.
- Read commands (kb/doc/session list+view, search chunks/docs/
sessions/kb, auth list/status, agent list/view, context list,
doctor) emit bare arrays / objects on stdout.
- Write commands (kb create/edit/delete/pin/empty, doc upload/
upload_recursive/delete, session delete, auth login/logout/
refresh/token, link/unlink, context add/use/remove, agent
invoke, chat, api, version) emit bare result objects. Risk
classification dropped — the resource + exit code already
convey the action.
Per-command shape changes:
list / search → []T (was {ok, data:{items:[…]}})
view → T (was {ok, data:T, _meta:…})
create / edit → T
delete / pin / etc. → {id, …action result…}
doctor → {summary, checks}
api → {status, headers, body}
_meta dropped on the read path:
pagination (page/page_size/total/has_more) — agents iterate with
--all-pages or accept --limit (gh CLI parity);
kb_id / context echo — caller already knows what it asked for.
Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
|
||
|
|
bdc589e1c0 |
refactor(cli): --limit/--all-pages, Go 1.26, internal/agent → aiclient
Cross-cutting cleanup that lands alongside the new feature surface: - `--limit / -L` and `--all-pages` on every list command. Default --limit 30 (gh-parity); --all-pages drains every server page client-side, capped by --limit. Closes the audit finding that the old "1000 max per call" implicit cap was undiscoverable. - `auth token` emits a TTY-only stderr advisory when stdout is a terminal (the credential just got displayed in scrollback) plus an api-key-mode rotation hint. - Comment + doc discipline pass: drop external project name references from in-code comments (we reference them in design notes, not inline). - Bump `go` directive to 1.26.0 and CI matrix to 1.26.x to align with the main module's go.mod. - Rename `cli/internal/agent` → `cli/internal/aiclient` to disambiguate from the new `cli/cmd/agent` resource subtree. The package handles AI coding-agent env detection + per-command --help annotations; the new name reflects that more precisely. |
||
|
|
9bb83b47fd |
feat(cli): mcp serve curated stdio MCP server
`weknora mcp serve` — long-lived stdio MCP (Model Context Protocol) transport that exposes a fixed, curated tool surface to MCP-aware agents (Claude Desktop, Claude Code, custom MCP clients). Curated tool set (readonly by default): - whoami — active context + tenant - search (hybrid retrieval against a KB) - kb list / view - doc list / view - agent list / view / invoke - session list / view The list is intentionally narrow to the read + agent-invoke surface; destructive verbs (`delete` / `empty` / `upload`) are gated behind `--write`. Schema is built from each leaf cobra command's flags so adding a new tool is a single registry entry plus a Service interface. Includes the simplify post-review polish + a second simplify pass to fold the resulting feedback (typed schemas, agent_help wording, unify chat / agent invoke option names). |
||
|
|
493fc41e98 |
feat(cli): agent subtree (list/view/invoke)
Manages WeKnora's first-class Custom Agent resources — server-side records (system prompt + model + allowed tools + KB scope) that the user authored in the web UI. Commands: - `weknora agent list` — tenant-visible agents (built-in + custom), sorted updated_at desc; `--limit`/`-L` caps the slice client-side. - `weknora agent view <id>` — full sdk.Agent including nested AgentConfig (mode / model / allowed_tools / KB scope). Human mode prints a compact KV layout + Config: block. - `weknora agent invoke <agent-id> "<text>"` — streams the agent's configured workflow against a query over SSE. Auto-creates a fresh session unless `--session` is passed. Streaming defaults to TTY + no-stream/no-json; agent-friendly buffered single-object output with `--json` (or `--no-stream`). Decoupled from the existing `chat` subtree: agents bring their own system prompt / tool surface / KB selection, so the chat / agent split matches the server-side resource boundary. |
||
|
|
3b67986863 |
feat(cli): per-resource filter flags on list commands
Adds the filter flags users were reaching for via `--jq` post-filter: - `kb list --pinned` — client-side filter to KBs with `IsPinned`. - `doc list --status <pending|processing|completed|failed>` — server-side query-param filter; `failed` surfaces ingestion errors immediately for triage. - `session list --since <duration>` — client-side filter to sessions updated within the past duration. Accepts time.ParseDuration forms (24h, 1h30m, 30m) plus a `<N>d` suffix for whole days (7d, 0.5d). Server-side filters are forwarded as query params (where the API supports them) to avoid pulling the full list into memory; client-side filters apply after the fetch so they compose with `--limit`. |
||
|
|
1b20b06f5e |
feat(cli): --json field-select, --jq, auth token, doc --from-url
Output ergonomics:
- `--json` accepts a comma-separated field list (gh-parity); selects
named keys from the per-command payload. Bare `--json` keeps the
full shape.
- `--jq <expr>` evaluates a gojq expression over the JSON; pairs with
`--json field-list` so projection runs before jq.
- `--version` is a global cobra flag in addition to the `version`
subcommand; both render the same line.
- Per-command `--help` now renders the available JSON field list under
"JSON fields available via `--json id,name,...`" (field-discovery
parity with gh / kubectl `-o jsonpath`).
New commands:
- `auth token` — print the active context's credential to stdout for
shell command substitution (`WEKNORA_TOKEN=$(weknora auth token)`).
Default: raw secret, no trailing newline. `--json` emits
`{token, mode, context}`.
- `doc upload --from-url <URL>` — ingest a remote URL via the SDK
`CreateKnowledgeFromURL`. `--name` forwarded as `FileName` so the
server's known-extension heuristic upgrades crawl-mode to
file-download-mode where appropriate.
Includes the simplify post-review polish pass (field-filter unit
tests, --json/--jq compose check, agent_help copy fixes).
|
||
|
|
4fb089d4d7 |
fix(kb): map ErrKnowledgeBaseNotFound to 404 across handler helpers
Five handler helpers (validateAndGetKnowledgeBase in knowledgebase.go,
validateKnowledgeBaseAccessWithKBID in knowledge.go, the kbService
guards in faq.go and tag.go, and getKnowledgeBaseForInitialization +
the per-kb config getter in initialization.go) wrapped every
GetKnowledgeBaseByID error — including the well-known
repository.ErrKnowledgeBaseNotFound sentinel — as
NewInternalServerError. The result was that every probe of a stale or
cross-tenant kb id surfaced as a 500 instead of the 404 it should have
been, both confusing clients ("real 5xx vs. wrong URL") and burning
ops attention on monitoring alerts.
The mapping pattern is the same as PR #1336 for sessions: detect the
sentinel via stderrors.Is and emit NewNotFoundError; everything else
still surfaces as a 500 so genuine DB / repo failures keep firing the
alerts that matter. Caught during the RBAC e2e smoke run on
feat/rbac, where a deliberate cross-tenant kb-id probe produced
HTTP 500 + body "knowledge base not found" — the smoking gun.
Tests: new internal/handler/knowledgebase_not_found_test.go covers
three cases — bare sentinel, fmt.Errorf("%w") wrapped sentinel
(regression guard against a future revert to `==`), and a non-sentinel
infrastructure error that must still 500. All three pass.
The full handler test package is green.
|
||
|
|
3a0a0cff9c |
fix(migration): ensure pg_trgm is created before trigram index in 000041
Migration 000002 creates the pg_trgm extension only inside the conditional embeddings block guarded by app.skip_embedding. Deployments that use a non-postgres retrieve driver (Qdrant, Elasticsearch, Milvus, etc.) skip that block entirely, so pg_trgm is never installed. When migration 000041 then runs CREATE INDEX ... USING GIN (lower(title) gin_trgm_ops) the statement fails and the whole migration is rolled back, leaving task_pending_ops and task_dead_letters uncreated. The application keeps starting (migrations are best-effort), but wiki ingest enqueue silently fails and wiki pages are never produced. Re-issue CREATE EXTENSION IF NOT EXISTS pg_trgm at the top of 000041 so the extension is guaranteed present at the moment the trigram index is created. The statement is idempotent on environments where 000002 already installed it, and surfaces a clear, early error on environments where the extension genuinely cannot be loaded. Fixes #1319. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
d7b5835519 |
fix(session): map ErrSessionNotFound to 404 across all handlers
PR #1309 plumbed user-scope into the session/message service layer so non-owner / wrong-tenant lookups now surface as ErrSessionNotFound at every entry point that goes through sessionRepo.Get. The handler-side mapping, however, only existed in 4 of the 8 affected sites: - handler/session/handler.go:GetSession ✓ already mapped (with `==`) - handler/session/handler.go:UpdateSession ✓ already mapped (with `==`) - handler/session/handler.go:DeleteSession ✓ already mapped (with `==`) - handler/session/handler.go:BatchDeleteSessions ✓ already mapped (with `==`) - handler/session/handler.go:ClearSessionMessages ✗ always 500 - handler/session/stream.go:ContinueStream session lookup ✓ (with `==`) - handler/session/stream.go:ContinueStream message lookup ✗ always 500 - handler/message.go:LoadMessages (recent + before-time) ✗ always 500 - handler/message.go:DeleteMessage ✗ always 500 Result: a Contributor in tenant A asking for a Contributor B's session messages got a 500 instead of 404 — the wire response leaked "something is broken" rather than "you can't see this URL", which breaks SDK / frontend axios interceptor error handling that keys on HTTP status. Two changes: 1. Add ErrSessionNotFound → 404 mapping to the 4 sites that lacked it (ClearSessionMessages, ContinueStream's GetMessage, LoadMessages's two paths, DeleteMessage). 2. Replace `err == errors.ErrSessionNotFound` with `stderrors.Is(err, errors.ErrSessionNotFound)` everywhere. Sentinel error comparison must use errors.Is so wrapped errors (`fmt.Errorf("...: %w", ErrSessionNotFound)`) still match. Today no service-layer caller wraps, but a future refactor that does would silently turn 404s into 500s with no test catching it. Tests: - internal/handler/message_session_not_found_test.go covers the four new mapping sites in handler/message.go (LoadMessages-recent, LoadMessages-before-time, DeleteMessage) and pins the wrapped-error behaviour with a dedicated regression test that fmt.Errorf("%w") a sentinel must still be detected as ErrSessionNotFound. The bare handler is hit through gin.Engine + middleware.ErrorHandler so the response shape matches production exactly. - PR #1309's existing service-level tests (session_user_scope_test, session_test on the repository) still pass — this PR is a strictly handler-layer follow-up. |
||
|
|
6f95c75ed2 |
feat(system-info): surface DB migration errors with troubleshooting links
When a startup database migration fails (e.g. issue #1319: pg_trgm not available so 000041 cannot build its trigram index), the application intentionally keeps booting so operators can reach the UI to diagnose. However, before this change the system info page silently dropped the "DB Version" row because the value was empty: - migration.go only cached the version after a successful m.Up(); the error path returned early and left migrationVersionSet=false. - system.go used CachedMigrationVersion's ok=false to skip emitting db_version, and the JSON tag was already omitempty. - SystemInfo.vue gated the entire row on v-if="systemInfo?.db_version". The end result was the most useful diagnostic surface vanishing in the exact failure mode that needs it most — Wiki ingest and KG features would silently produce nothing with no UI hint. Changes: - migration.go: replace sync.Once-based setter with an RWMutex-guarded state struct holding {version, dirty, err}. Every failure path now calls captureMigrationFailure(m, err), which best-effort reads m.Version() so the cached value still reflects the partial state. - system.go (handler): always emit db_version (falling back to "unknown" when no version could be read), append " (failed)" when an error is recorded, and add db_migration_error to the response. - swagger / client SDK: keep the API contract in sync with the new response field. - SystemInfo.vue: render the DB version row whenever either field is present, show a "Migration failed" danger tag, and add a full-width alert below the row carrying the error message plus two links: 1. View troubleshooting guide -> new docs/migration-troubleshooting.md 2. Report an issue -> github.com/Tencent/WeKnora/issues/new, prefilled with the captured error and environment metadata. - docs/migration-troubleshooting.md: new self-service guide covering the common failure modes (missing extension, dirty state, privileges, out of disk, schema drift) with concrete psql / make commands. - i18n: add the new keys to zh-CN, en-US, ko-KR, ru-RU. Refs #1319. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
8f4e5a459f | fix(session): scope session access by user | ||
|
|
7478e3cddb |
refactor(retriever): introduce factory for KB-scoped retrieve engine resolution
Extracts the 25 repeated NewCompositeRetrieveEngine call sites across seven services into two factory functions with tenant-ownership verification: CreateRetrieveEngineForKB (synchronous) and CreateRetrieveEngineFromPayload (async task handlers). Promotes GetByStoreID from concrete-only to interfaces.RetrieveEngineRegistry. Extends KBDeletePayload / IndexDeletePayload with an omitempty VectorStoreID snapshot. Design highlights - Factory verifies tenant ownership of the resolved store (defense-in-depth) so a gap in PR3 validation or a tampered Asynq payload cannot cross tenants. Cross-tenant attempts return ErrVectorStoreForbidden with a structured log entry. - Sentinel errors (ErrTenantInfoMissing, ErrVectorStoreNotFound, ErrVectorStoreForbidden) let async handlers classify non-retryable failures as asynq.SkipRetry. - nil and empty-string pointers for vectorStoreID normalize to "unbound" so callers never send an empty UUID to GetByStoreID. - resolveBoundEngine constructs engineInfos directly (with slices.Clone of Support()) instead of going through NewCompositeRetrieveEngine. The latter only reads from the byEngineType map and cannot reach DB stores in byStoreID -- multiple stores can share the same engine type. Unbound fallbacks still reuse NewCompositeRetrieveEngine so the tenant effective-engines path is byte-for-byte unchanged. - DeleteKnowledgeBase now reads the KB before soft-delete so the enqueued KBDeletePayload carries a VectorStoreID snapshot; once the row is soft-deleted GORM's default scope hides it. Cost: +1 SELECT per KB-delete request (a rare admin operation). - enqueueIndexDeleteTask gains a vectorStoreID *string parameter that the single caller (tag.go) populates from the owning KB. The worker (ProcessIndexDelete) validates ownership via the factory. - cleanupKnowledgeResources loads the KB so bound-store cleanup routes correctly. If the load fails it warns and falls back to tenant effective engines; orphan vectors become observable in logs instead of disappearing silently. - DeleteKnowledges (batch) stays on the tenant effective-engines path -- batches can span stores and multi-store fan-out is PR4 scope. Noted inline. Merge safety All knowledge bases have VectorStoreID = NULL at merge time (introduced nullable in PR1, Tencent/WeKnora#994). Every factory call falls back to NewCompositeRetrieveEngine(registry, tenantInfo.GetEffectiveEngines()), which is the existing code path for these KBs. Async payloads from before this change decode to VectorStoreID = nil (omitempty) and take the same fallback. Testing - go build ./... - go vet ./... - go test -race -count=1 ./internal/application/service/retriever/... covers unbound (nil/empty), bound, cross-tenant, unregistered store, ownership infra error, legacy payload shapes (missing field/null/ empty string/nil), tampered payload, and parallel invocation. - grep -r NewCompositeRetrieveEngine internal/application/service returns zero results outside factory.go/composite.go. Refs: Part of Tencent/WeKnora#993. Depends on Tencent/WeKnora#994. |
||
|
|
35c79281c8 |
feat(cli): doc view + unlink (fill v0.3 design-gap audit)
Final design-pass audit on the v0.3 surface flagged two real gaps. (A) doc view <id> was missing. Every other resource subtree exposes a view verb (kb view, session view) for inspecting a single record, but doc — which has the richest metadata of the three (title, file name, type, size, parse_status, embedding_model, processed_at, error_message) — had no single-doc surface. Users wanting one doc's metadata had to `doc list | grep`. Implementation mirrors kb view: narrow ViewService(GetKnowledge) interface, --json envelope path, human KEY: VALUE renderer. Optional fields are omitted rather than rendered as "-" so the panel is dense. Tested: human renderer, title fallback when FileName empty, omit-empty contract, JSON envelope shape, 404 classification. (B) link had no counterpart. Once .weknora/project.yaml is written, the only way to clear it was `rm` by hand. Both vercel and netlify ship `unlink` as a top-level verb; not having one was a discoverability gap. Top-level rather than `link --clear` follows the verb-noun convention of the rest of the surface — the verb stands alone and the operation isn't parameterised. unlink walks up from cwd via projectlink.Discover (the same parent-chain logic Factory.ResolveKB uses on the read side), so a user in a subdirectory of a linked project can unlink without cd-ing up. Errors with input.invalid_argument when no link is found anywhere in the chain. Idempotent under racy concurrent removal: os.ErrNotExist on os.Remove falls through to a Success envelope since the post-condition holds either way. projectlink package gained Remove() alongside Save / Load / Discover so unlink doesn't reimplement the idempotent-remove pattern inline. Top-level registration in cmd/root.go, alongside link. cli/AGENTS.md verb canon line adds unlink to the locally-introduced list. cli/CHANGELOG.md gains an Added entry for each. 5 unit tests for view + 4 for unlink (cwd / walk-up / no-link error / JSON envelope). Full suite green. Intentionally deferred: - session edit (rename a session): sessions auto-name from the first prompt; polish rather than a gap. - link --clear as an alternative to unlink: top-level unlink is the documented form; aliases would just multiply the surface. |
||
|
|
4a5449233d |
fix(cli): plug v0.3 final review findings (json + auth + path + bounds + kb)
Seven bugs surfaced via two audit rounds — parallel reviewer agents
plus a real-server end-to-end demo. Each fix arrives with a
regression test.
1. doc upload --recursive --json corrupted the envelope stream.
Per-file FAIL/OK plain lines printed unconditionally to stdout,
then a Success envelope, then on partial failure a typed error
that the root handler turned into a SECOND Failure envelope —
three outputs where one was expected. Fix: gate the plain lines
behind !opts.JSONOut, and add cmdutil.Error.Silent so the JSON-
path partial-failure preserves its typed exit code without
triggering PrintErrorEnvelope's default Failure-envelope write.
2. auth refresh / AuthRetryTransport misclassified HTTP failures as
network.error. RefreshAndPersist wrapped every refresher error
with CodeNetworkError, but the SDK emits "HTTP error 401: ..."
for a rejected refresh token — which should surface as
auth.token_expired. Switched to WrapHTTP for proper status-
derived classification. Affects both `auth refresh` and the
transport's refresh closure.
3. doc download accepted ".." as a server-suggested filename. The
rejection list covered "" / "." / filepath.Separator but not
bare ".." — filepath.Base("..") is "..", which slipped through
to os.Create and produced a confusing local.file_io wrap. Added
to the rejection set.
4. search chunks / docs / kb / sessions had no lower bound on
--limit. `-L 0` / `-L -1` was forwarded to the server with
undefined behavior. Added a 1..1000 bound at the RunE boundary
across all four (matching doc list / session list page-size
bounds). Internal callers in tests can still pass Limit==0 for
the "no client-side cap" runChunks path — the bound only applies
at the user-input layer.
5. cli/AGENTS.md ADR-3 verb-canon summary listed only v0.2 verbs as
"gh-canonical" and missed v0.3 additions (edit, pin, unpin,
download — all gh-canonical) plus locally-introduced ones
(empty, refresh, add, remove, link). Rewritten as an explicit
gh-canonical / locally-introduced split.
6. kb pin returned 404. Server registers /knowledge-bases/{id}/pin
as PUT (router.go:292); SDK was using POST. gin's router silently
404s on method-mismatch (treats it as path-not-found, not 405),
so the CLI classified the response as resource.not_found and
masked the real failure mode. Switched the SDK to http.MethodPut.
The asymmetry that hid this past round 1: kb unpin on a freshly-
created KB hits the no-op branch in cmd/kb/pin.go that skips the
SDK call entirely, so unpin "worked" without ever exercising the
broken path. Only the real-server demo, where kb pin actually
fires, surfaced it.
7. kb edit clobbered current Name when only --description was
passed. EditOptions used *string to distinguish "unset" from
"set to empty", but sdk.UpdateKnowledgeBaseRequest declares both
fields as plain string (no omitempty), so the JSON body always
carried `"name": ""`. Server requires Name → 400. Fix: runEdit
does fetch-then-update — GetKnowledgeBase first, build the PUT
body with current values, then overlay user-set fields. Same
TOCTOU window as kb pin / unpin.
Audit-flagged items intentionally NOT changed:
- kb pin / unpin check-then-toggle TOCTOU: documented; the clean
fix would be a server-side setter and belongs in a separate API
change.
- AuthRetryTransport singleflight test gap for one concurrency
scenario; v0.4 polish.
- cli/README.md:50 "once v0.2 ships" and CHANGELOG.md:8
"10 top-level commands": v0.2-PR artifacts, not v0.3-introduced.
- kb edit / kb pin are v0.3-new commands, so neither bug needs a
cli/CHANGELOG.md Fixed entry — the v0.3 release ships them
working as the Added bullets advertise.
|
||
|
|
13cce78332 |
fix(cli): drop link --context flag (shadowed global --context)
The `link` subcommand declared a local `--context` StringVar that
shadowed the root-level persistent `--context` flag at the cobra layer.
Two different semantics under one name:
- root global `--context <name>`: "override the active context for
THIS invocation only, no disk write" (single-shot connection
override, applied via Factory.ContextOverride).
- link local `--context <name>`: "the context name to record in
.weknora/project.yaml" (persisted state, written to disk).
The shadow meant `weknora --context staging link` (intent: link runs
against staging) silently did NOT propagate the override into link's
runtime; instead link's local "" beat the global. `weknora link
--context staging` (intent: record staging in the file) did work, but
shared a name with the unrelated global behavior, which is a usability
trap.
Resolution: drop the local flag entirely. The active context at link
time is what gets recorded; users who want to bind under a different
context use the global `--context X link --kb my-kb` form, which now
propagates correctly (no local shadow). This matches the bind-command
patterns surveyed across mainstream CLIs:
- lark-cli `config bind` — uses domain-specific flags (--source /
--app-id / --identity); the global --profile is named distinctly.
- gh `repo set-default` — uses a positional for the bind target;
the global -R/--repo is the only flag-form path.
- netlify `link` — uses --id/--name for the bind target; no
--site global flag at all (env var only).
- vercel `link` — reuses --project for both global and link, but
only works because vercel ships a custom parser that merges
flag/env/file precedence; cobra's persistent-flag shadowing is
silent-override, not graceful merge.
The lark-cli / gh / netlify pattern of "bind command's target flag
must not share a name with the global override flag" is the cobra-
friendly choice; dropping the flag is the simplest form of that.
No behavior change for the common path (`weknora link --kb my-kb`
without --context still records the active context). The "record
under a specific non-current context" use case is now expressed via
the global flag, which is what it was designed for.
link_test.go untouched (no test referenced the dropped flag).
|
||
|
|
c9b837dfce |
docs(cli): sync README + AGENTS.md, add cli/CHANGELOG.md, clear stale e2e refs
v0.3 feature commits didn't update the docs alongside; this commit
syncs them and introduces a CLI-local changelog so v0.3+ release
notes stop crowding the project root file.
cli/CHANGELOG.md (new):
- Subsystem-local pattern, mirroring mcp-server/CHANGELOG.md. CLI
versions independently from server / frontend cadence; reduces
merge-conflict surface on the shared root file.
- Scope: Added + SDK additions only. v0.3-internal dev churn
(--top-k → --limit, kb clear-contents → kb empty, link --context
introduce-then-drop, internal Go type-name leaks) never reached a
shipped release so it doesn't belong in Changed / Fixed sections.
mcp-server's v1.0.0 changelog is Added-only for the same reason.
- v0.0–v0.2 history stays in the project root CHANGELOG.md;
cross-referenced from the top of cli/CHANGELOG.md.
Stale --help / quickstart examples fixed in cli/cmd/root.go,
cli/README.md, and cli/AGENTS.md — all three showed the dropped bare
`weknora search "<q>" --kb=...` form; updated to `search chunks ...`.
AGENTS.md updates:
- Verb canon table gained edit / empty / download / pin / unpin /
add / remove.
- `auth` subtree description gained `refresh` and the transparent
401-retry transport (replacing the now-inverted "deferred to v0.3"
sentence).
- `search` and `session` subtree paragraphs added; top-level
verb list gained `context` and `session`.
cli/README.md top-level command list gained `session`; `search`
short retitled to the parent description ("Search across chunks,
knowledge bases, documents, or sessions") since search is now a
pure dispatcher.
Pre-existing stale e2e refs swept up while syncing:
- cli/acceptance/doc.go listed e2e/ under "Future v0.2+:" — moved
into the present-tense Sub-packages block.
- envelope_test.go preamble "Deferred to v0.2 e2e" rephrased to
"Deferred to the e2e harness" so it isn't pinned to a past version.
Not changed (out of scope, flagged for future PRs):
- envelope_test.go "Implemented count: 16" vs the actual 14 named
entries — could be a different counting rule; verify with PR-8
author before editing.
- envelope_test.go context_use deferred-cases narrative is loose
(context_use.success IS golden-pinned today) but rewriting needs
careful re-derivation of which error scenarios are still deferred.
- cli/README.md:50 "once v0.2 ships" — v0.2-PR-original wording;
not load-bearing once a release tag exists.
No project-root CHANGELOG.md change in this commit.
|
||
|
|
5adcedf170 |
refactor(cli): v0.3 cross-cutting cleanup
Cross-cutting findings surfaced by the branch-completion review. Perf bug: - Factory.Client closure was not memoized. Factory.ResolveKB internally calls f.Client() to resolve --kb name → id, then the command's RunE calls f.Client() again. Two SDK clients, two keyring reads, two AuthRetryTransport allocations per name-resolved invocation, with *independent* token state (a refresh in one was invisible to the other). Switched to sync.Once like Secrets already does. Silent bug bait: - cmdutil.NormalizeHost docstring claimed CodeInputMissingFlag for the empty case; code returned CodeInputInvalidArgument. Aligned doc to code (present-but-empty is a bad value, not a missing flag). Agent contract gaps: - Five user-facing subcommands lacked SetAgentHelp: auth login / logout / list / status and chat. Added concise strings with error- code call-outs so agents can branch without parsing human strings. Helper extraction (≥3 callers): - text.KnowledgeDisplayName(fileName, title, id) — byte-identical formatter that was in both cmd/doc/list.go and cmd/search/docs.go. Takes fields directly so internal/text stays SDK-free. - cmdutil.WrapHTTP(cause, fmt, args...) *Error — replaces the `Wrapf(ClassifyHTTPError(err), err, ...)` pattern across 24 SDK call sites. Sed-driven migration; off-pattern shapes in chat.go (used streamErr) and cmdutil/kb.go (in-package) hand-edited. Contract test gains a comment update: post-migration the dominant pattern is WrapHTTP which the AST scanner skips entirely (only NewError/Wrapf selectors inspected); ClassifyHTTPErrorOutputs() bridge still covers the dynamic codes those paths can yield. UX consistency: - cmd/doc/list.go --page-size help now reads "Items per page (1..1000)" matching cmd/session/list.go. The bounds validation already enforced 1..1000; the help text was the last drift. Comment-discipline sweep: - Deleted the WHAT-only "*Options captures `weknora ...` flag state" docstring across 23 files (context, kb, auth, doc, session, search, chat, doctor, link). Where the line carried a real WHY clause (kb/delete, doc/delete, session/delete, kb/edit), kept the WHY and dropped only the leading WHAT phrase. - Stripped third-party project-name attribution from inline comments and one user-visible flag-help string across ~40 files in cli/cmd and cli/internal (plus 4 test-file comments). Removed phrases like "Mirrors `gh X`", "borrowed from lark-cli", "kubectl-style", "gcloud `--project`", "Stripe pattern", and the embedded GitHub URLs pointing at those projects. Behavioral descriptions and the WHY behind each comment are preserved; only the upstream-name attribution is gone. Inspiration / north-star references belong in cli/AGENTS.md (the design doc) and commit messages, not scattered through every file. Triggered by an audit round that surfaced several false / fragile parity claims (e.g. "Mirrors `gh repo edit`" — gh repo edit has no --name flag; "matches gcloud `--project` id-or-name" — gcloud's --project accepts ID only). Rather than fix them one by one, the whole category of in-comment external-project references was stripped uniformly. |
||
|
|
73a88b4f0a |
feat(cli): api --input + completion smoke
api (3-11):
- `--input <file>` reads the request body from disk; `--input -` reads
from stdin. Matches gh CLI canonical naming verified against the gh
manual ("The file to use as body for the HTTP request — use \"-\" to
read from standard input"). `--data` / `--input` are mutually
exclusive.
- Options.StdinReader (defaults to iostreams.IO.In) for test injection.
completion (3-13 smoke only — release-artifact ship deferred to release
milestone):
- Smoke test asserts cobra's auto-registered bash/zsh/fish/powershell
scripts produce non-trivially-sized output with the per-shell
signature (#compdef / complete -c weknora / etc.). Guards against
cobra bumps silently breaking completion for one shell.
3-14 doctor --no-cache: already implemented (factory.go:297) with
TestDoctor_NoCache_BypassesCache covering it — verified, no change
needed.
Roadmap: 3-11, 3-13 (smoke), 3-14 (verified).
|
||
|
|
d54a7a5834 |
feat(cli): search verb-noun subtree (chunks/kb/docs/sessions)
Roadmap 3-1. Verb-noun shape borrowed from gh search (gh search repos / code / commits / issues / prs verified against the gh manual). Subcommands: - `search chunks "<q>" --kb X` — hybrid retrieval (RAG search). - `search kb "<q>"` — case-insensitive substring match across KB names and descriptions; sorted by name length (shortest hits first). - `search docs "<q>" --kb X` — pages through ListKnowledge filtering by title / file_name; stops once --limit matches are found. - `search sessions "<q>"` — pages through GetSessionsByTenant filtering by title / description. kb / docs / sessions are client-side filters because the server has no fuzzy search endpoint for any of them. ListKnowledgeBases returns the full tenant catalog in one call; the doc/session walkers chunk at 200 per request and stop early on limit. The parent `search` command is a pure dispatcher — there is no bare- positional form (no `weknora search "<q>"`). Cleanups surfaced by the post-commit reviewer round: - UX consistency: search docs's displayDocName ordered Title → FileName → "-", while doc list's displayName uses FileName → Title → ID. Same Knowledge rendered differently across commands. Aligned search docs on doc list's existing FileName-first convention. - cmdutil.ResolveKBFlag(ctx, lister, raw) — extracted the `IsKBID ? raw : ResolveKBNameToID` block duplicated across chunks and docs. - text.ContainsFold(needle, fields...) — replaces inline `strings.Contains(strings.ToLower(field), needle)` patterns. 37 unit tests across chunks/kb/docs/sessions plus the parent registration smoke-test. Roadmap: 3-1. |
||
|
|
78f3994112 |
feat(cli): doc download + upload --recursive
Roadmap items 3-9 (download) and 3-10 (recursive upload).
SDK addition (additive, non-breaking):
- OpenKnowledgeFile(ctx, id) (filename, body io.ReadCloser, err) —
the new primitive that returns the body as a stream plus the
server-suggested Content-Disposition filename. The existing path-
form DownloadKnowledgeFile is now a thin wrapper (also gained
partial-file-on-error cleanup, a pre-existing bug exposed by the
reshape).
doc download <id>:
Borrows shape from `gh release download` (positional id, output flag,
`-` sentinel for stdout). Flag names match gh canon verified against
the gh manual: `-O, --output <file>` for destination; `--clobber` for
overwrite control.
- Default: writes to cwd under the server-suggested filename. If the
server didn't send one, errors with input.missing_flag.
- --output FILE / -O FILE: writes to FILE. Refuses overwrite without
--clobber.
- --output -: stream to stdout (binary-safe).
- Partial writes on error are cleaned up.
doc upload --recursive <dir> --glob '*.pdf':
NOTE on upstream parity: `gh release upload` does NOT support
--recursive (verified — it takes individual file args only). `aws s3
cp --recursive` does, but uses `--include`/`--exclude` glob pattern
pairs rather than a single `--glob`. weknora's single positive `--glob`
is a deliberate simplification, not a direct mirror of either tool.
- Walks the tree, filters by base-name glob, uploads each match
sequentially. Per-file line output: OK / FAIL with the underlying
error. Exit 0 only on full success; on partial failure returns the
first failure's typed code so callers can branch. Rejects --name
with --recursive.
- --dry-run lists matches without uploading.
- --json emits {kb_id, uploaded[], failed[]} envelope at completion.
Bugs caught in the post-commit reviewer round:
- SECURITY: server-supplied filename was used in os.Rename without
sanitization. A malicious / buggy server returning
"../../etc/shadow" could escape cwd. Now filepath.Base'd; "." / "/"
/ "" rejected. Regression test added.
- Wasted-bytes path eliminated via the SDK reshape: the CLI now
inspects filename and applies refuseIfExists BEFORE streaming.
Two-phase temp+rename gone.
- refuseIfExists(path, clobber) helper extracted.
- --json honored in --recursive (uploadOutcome was JSON-tagged but
the envelope was never emitted).
7 + 7 unit tests for download (+ path-traversal regression) and
recursive upload (+ JSON envelope regression).
Roadmap: 3-9, 3-10.
|
||
|
|
2f8681b48e |
feat(cli): session subtree + kb edit / pin / empty
Roadmap items 3-5 (session) and 3-6/7/8 (kb manage).
cli/cmd/session/ (new package; sessioncmd to avoid shadowing stdlib):
- session list: paginated table (ID/TITLE/UPDATED). --page / --page-size
with 1..1000 validation. _meta.has_more from page*size < total.
- session view <id>: prints metadata; non-empty fields only. Server
timestamps arrive as strings; parsed best-effort as RFC3339.
- session delete <id>: high-risk-write; exit-10 confirmation in non-
TTY/--json paths; --dry-run emits envelope.risk + dry_run:true.
cli/cmd/kb (extended):
- kb edit <id> [--name N] [--description D]: at least one flag required;
*string options so unset fields stay unset in the PUT body. SDK
UpdateKnowledgeBaseRequest has no embedding_model field, so the
roadmap's --embedding-model dropped.
- kb pin <id> / kb unpin <id>: direct parity with gh issue pin /
gh issue unpin (verified against gh manual). Idempotent: GetKnowledgeBase
reads IsPinned, TogglePinKnowledgeBase fires only on state change.
SDK KnowledgeBase struct gained the IsPinned field (server already
returned it; SDK just hadn't modeled it — non-breaking additive).
- kb empty <id>: high-risk-write; exit-10 confirmation;
--dry-run. Returns deleted_count from the async clear response.
weknora-specific operation; no mainstream parallel.
Golden envelopes for kb_list and kb_view updated to include the new
is_pinned field — strict-additive change.
Cleanups surfaced by the post-commit reviewer round:
- ConfirmPrompter promoted to cli/internal/testutil/ (4-copy threshold
reached: context/remove, kb/delete, kb/empty, session/delete).
kb/delete_test.go's pre-existing local copy left untouched per the
upstream-respect convention.
- kb pin/unpin idempotent no-op path no longer emits a write-class
envelope. Added _meta.warnings "already {un}pinned — no server
call made" and dropped the risk classification on the no-op branch.
- doc list --page-size was unbounded while session list enforces
1..1000. Same validation added to doc list.
18 + 18 unit tests; e2e exit codes verified.
Roadmap: 3-5, 3-6, 3-7, 3-8.
|
||
|
|
4c26bc9ecc |
feat(cli): auth refresh + transparent 401 retry transport
Two halves of v0.3 roadmap item 3-2. (1) `weknora auth refresh` — explicit token renewal: Reads the stored refresh_token, spends it via POST /api/v1/auth/refresh (OAuth refresh-token grant), and persists both new tokens. API-key contexts rejected with input.invalid_argument (no refresh semantic). NOTE: gh CLI has `gh auth refresh` but with different semantics — gh's variant is an OAuth scope expansion / re-prompt via the browser (verified against the gh manual). The two share a name but solve different problems; there's no direct gh parallel for refresh-token grant because gh's PAT/OAuth-app model doesn't expose a short-lived access_token + refresh_token pair to clients. Error mapping: - no current context → auth.unauthenticated - --name unknown → local.context_not_found - missing refresh in keyring → auth.token_expired (hint: re-login) - server Success=false → auth.token_expired - network → network.error Envelope omits the token values (would leak into agent transcripts). (2) AuthRetryTransport — transparent retry: Wraps the SDK http.Client. On a 401 from a non-/auth/* endpoint: - JWT context: read refresh token, hit /auth/refresh, persist new pair, replay original request with new bearer. - API-key context: pass through (no refresh semantic). - Non-replayable body (req.GetBody == nil): pass through. - /auth/login or /auth/refresh: pass through (no recursion). Concurrent 401s are singleflight-coalesced via sync.Mutex — 5 parallel calls trigger exactly 1 refresh. SDK additions (additive, non-breaking): - WithTransport(rt http.RoundTripper) ClientOption. - PathAuthLogin / PathAuthRefresh constants (cli/internal/cmdutil/authretry imports them so the CLI and SDK can't drift on path strings). Refactor surfaced by the post-commit reviewer round: - cmdutil.RefreshAndPersist(ctx, store, refresher, ctxName) — the load-refresh → call-SDK → persist-pair sequence was duplicated between the standalone `auth refresh` and the transport's refresh closure; collapsed to one canonical implementation. - refreshFn signature takes context.Context so Ctrl+C during a transparent refresh cancels. - AuthRetryTransport.CurrentToken() removed — never called. 8 + 8 + 8 unit tests cover happy path / refresh-fail / auth-endpoint skip / api-key passthrough / singleflight under concurrency / non- replayable-body fallback. Roadmap: 3-2. |
||
|
|
41a98b5743 |
feat(cli): context CRUD
New v0.3 P0 entry 3-4: kubectl-style context-management subtree using gh's `<noun> <verb>` surface convention consistent with the rest of this CLI. - context list: tabwriter rendering + --json envelope; reads config.yaml only. - context add <name> --host <url> [--user]: validates http(s) URL, first context auto-becomes current, rejects duplicates with did-you-mean. - context remove <name>: best-effort keyring cleanup like `auth logout`. Removing the current context triggers exit-10 confirmation (lark-cli skill protocol) — subsequent commands would lose their default --context. (`context use` predates v0.3; the subtree was previously use-only.) Bugs caught and fixed inline by the post-commit reviewer round: - auth login was accepting `http://` (empty host portion) because the old validateHost only checked the scheme. New cmdutil.NormalizeHost (shared by both login and context add) requires u.Host != "". - context add's validateName claimed `..` was rejected but only denied / \\ space. Switched to positive allowlist [A-Za-z0-9._-] plus explicit ./../path-separator rejection. Helper consolidation: - cli/internal/cmdutil/host.go: NormalizeHost (trim, scheme, host non-empty) — both auth login and context add share it. - cli/internal/format/dash.go: DashIfEmpty — promoted from copies in cmd/auth/list.go and cmd/context/list.go. - recordingStore test stub dropped in favor of secrets.NewMemStore; contextKeyList test helper replaced by the existing contextKeys. 14 unit tests; 13 e2e branches verified. Roadmap: 3-4. |
||
|
|
a7d30f73ca |
fix(frontend): make rerank model optional in agent editor save
The runtime chat pipeline (internal/application/service/chat_pipeline/rerank.go:56) already skips rerank when rerank_model_id is empty, so it should not be required at save time. The editor incorrectly required rerank_model_id whenever a RAG KB was selected, even though the field is optional. This fix removes the validation so users can save agents without configuring a rerank model. Fixes #1252 |
||
|
|
6e6d61d945 |
fix(org): searchable join bypasses invite code expiry
JoinByOrganizationID no longer delegates to JoinByInviteCode for direct join, matching the product copy that searchable spaces need no invite code. Extract joinAsViewerWithChecks for shared member-limit logic. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f0b1851281 |
docs(issues): require concrete app/UI versions instead of "latest"
Update the bug report and question issue templates to direct users to the Settings → System Info page and ask for the concrete "App Version" and "UI Version" values shown there, explicitly disallowing "latest", "main" or "master" as answers. This pairs with the new UI Version row on the System Info page so version-mismatch bugs (e.g. SearXNG base_url field missing on stale UI builds) can be triaged at a glance. |
||
|
|
6cb493eda2 |
feat(frontend): show UI build version on system info page
Inject the frontend package.json version at build time via Vite and render it as a dedicated "UI Version" row in System Info, alongside the existing app (backend) version returned by /system/info. When the two versions differ a warning tag is shown so users can tell at a glance that their weknora-ui image is out of sync with weknora-app (e.g. missing config fields for newer providers like SearXNG). Rename the existing version row label from "System Version" to "App Version" to distinguish it from the new UI version row in all four locales (zh-CN, en-US, ko-KR, ru-RU). |
||
|
|
588394a8e6 |
fix(compose): stop publishing docreader gRPC port to the host
The docreader gRPC service has no authentication or TLS, but the default `ports: ["50051:50051"]` mapping in docker-compose.yml binds to 0.0.0.0, exposing an unauthenticated document parser (with URL fetch capability) on every host interface. The app container reaches docreader through the internal Docker network via `docreader:50051` (the default `DOCREADER_ADDR`), so the host port mapping is not required for normal operation. Replace the `ports` entry with `expose: ["50051"]` so the port is only reachable inside the WeKnora-network. Operators who need to call docreader from the host (for debugging, etc.) can re-add a `ports:` entry in a local override, preferably bound to 127.0.0.1. |
||
|
|
80bd268862 |
chore: release v0.5.2
Bump version to v0.5.2 across VERSION, Helm chart, frontend package files, and cloud-image script comment. Update CHANGELOG and all four language READMEs with v0.5.2 highlights (Wiki Mode at scale, MCP human-in-the-loop approval, new LLM/vector/storage/search backends, adaptive 3-tier chunking, global command palette, CLI preview, etc.). |
||
|
|
ec2b44494f |
fix(frontend): show reasoning_content in historical agent steps
Tool-calling rounds emit reasoning into the OpenAI-protocol reasoning_content field rather than visible content, so AgentStep.Thought is often empty in DB. The history reconstructor only read step.thought, which made the historical step card silent for those rounds even though the user saw the reasoning live. Fall back to step.reasoning_content when step.thought is empty so reload mirrors the live experience. |
||
|
|
b00bc84f35 |
fix(agent): pass reasoning_content back to providers that require it
MiMo and DeepSeek V3.2/V4 reject multi-turn requests in thinking mode when the prior assistant message lacks reasoning_content with HTTP 400: "The reasoning_content in the thinking mode must be passed back to the API." Agent's ReAct loop is the worst-case scenario — every round produces tool calls, exactly the case DeepSeek's docs specify reasoning_content MUST participate in subsequent context. Plumb reasoning_content through the full assistant-message round-trip: - chat.Message / types.ChatResponse / types.AgentStep gain a reasoning_content field (AgentStep persists via the existing Message.AgentSteps jsonb column, no migration needed). - streamLLMToEventBus accumulates reasoning chunks into result.ReasoningContent and surfaces it on the round's ChatResponse. - engine.runReActIteration writes it onto AgentStep so cross-turn replay preserves it. - observe.appendToolResults attaches it to the same-turn assistant message; agent_history.buildAssistantHistoryMessages does the same on cross-turn replay. - RemoteAPIChat.ConvertMessages forwards it on assistant turns to openai.ChatCompletionMessage.ReasoningContent (already supported by go-openai); providers that don't recognize the field ignore it. Tests cover the three boundaries: ConvertMessages serializes it for upstream, appendToolResults preserves it within the same turn, and buildAssistantHistoryMessages replays it across turns. Scope is intentionally limited to Agent mode — KnowledgeQA's chat pipeline and Anthropic's signed thinking_blocks are separate fixes that require schema changes (rendered_content / thinking_blocks columns). Fixes #1302 |
||
|
|
1ae06fb857 |
docs(frontend): clarify Wiki page-link graph vs entity-relation knowledge graph
The Wiki browser exposes a page-link graph (references between Wiki pages) while KB Settings → Knowledge Graph configures an LLM-extracted entity-relationship graph. Both are labeled "图谱"/"Graph", which is easy to confuse. - Expand graphSettings.description to call out the distinction - Add tabGraphTip tooltip on the Wiki "图谱" breadcrumb tab - Extend wikiBrowser.graphNoData empty state with the same clarification - Update zh-CN, en-US, ko-KR, ru-RU consistently |
||
|
|
f58139a945 |
fix(frontend): persist graph extract toggle state on KB save (#1297)
The "启用实体关系提取" switch in GraphSettings was silently dropped on save: buildSubmitData only attached extract_config to the request when both indexingStrategy.graphEnabled AND nodeExtractConfig.enabled were true. When the graph indexing strategy was off (the default), the toggle change never reached the backend and reloading the KB always showed the switch as off. Always emit extract_config based on the user's actual toggle state so the value round-trips correctly through updateKBConfig regardless of the indexing strategy selection. |
||
|
|
9e08ab7302 |
fix(chat): map MaxTokens to MaxCompletionTokens for GPT-5/o-series
OpenAI's GPT-5 series and o-series reasoning models (o1/o3/o4-mini) no longer accept `max_tokens` and reject non-default sampling params (`temperature`, `top_p`, `frequency_penalty`, `presence_penalty`). Azure OpenAI propagates the same constraint, returning HTTP 400 with "this model is not supported MaxTokens, please use MaxCompletionTokens". Lower the compatibility shim into `RemoteAPIChat.BuildChatCompletionRequest` (the single OpenAI-protocol egress) so every internal caller keeps using `MaxTokens` uniformly: - Add `provider.IsOpenAIReasoningOrGPT5Model` with precise prefix matching (covers `gpt-5*`, `o1`/`o1-*`, `o3`/`o3-*`, `o4`/`o4-*`; rejects `olympus-1`, `openai-*`, `o3xtra`, etc.). - When the provider is `openai`/`azure_openai` and the model matches, map `MaxTokens` to `MaxCompletionTokens` (explicit `MaxCompletionTokens` wins) and skip the unsupported sampling fields; `omitempty` keeps them off the wire. - Behavior is unchanged for gpt-4o, gpt-4, and all other providers. Add unit tests for the matcher and for the request-building path (Azure gpt-5.2, OpenAI gpt-5/o1-mini/o3/o4-mini, plus negative cases). Fixes #1283 |
||
|
|
4d2b8707ff | fix: support Anthropic gateway streaming | ||
|
|
55caeefc3d |
feat(observability): log end-to-end TTFB on both ends of chat stream
To diagnose where latency lives between "user hits send" and "first token appears" we need a single number that can be matched across the browser console and the server log. Add correlated TTFB markers keyed by X-Request-ID: Frontend (streame.ts) * Generate the X-Request-ID once and reuse it for logging. * Log request:start when fetchEventSource is invoked, response:headers when onopen fires, and response:first_answer the first time an SSE payload with response_type === 'answer' arrives. Filtering by event type avoids treating session_title / references / tool_call as the "first token". Backend (session handler) * parseQARequest records the wall-clock entry time and logs TTFB:start with the same X-Request-ID. * qaRequestContext carries receivedAt through to the stream handler. * AgentStreamHandler emits a one-shot TTFB:first_answer_chunk log the first time it observes an AgentFinalAnswerData chunk, so the delta against TTFB:start is the server's request-in → first-token-out budget. The frontend delta minus the backend delta is then attributable to network + gin middleware, which were previously invisible. |
||
|
|
f740e7cecf |
feat(observability): expand Langfuse spans across chat pipeline
Previously the Langfuse timeline for a knowledge-chat request only showed generations (chat.completion, embedding.embed, rerank), so the work happening between them — query setup, vector/keyword search, result merging, prompt assembly — appeared as unexplained gaps. Add spans around the pieces that fill those gaps: * qa.setup wraps request-time KB / model / search-target resolution before the pipeline event loop starts, accounting for the visible delay before the first chat.completion. * pipeline.<event> wraps each pipeline stage's eventManager.Trigger call so generations inside a stage nest under it. CHAT_COMPLETION_ STREAM is intentionally skipped because its OnEvent returns as soon as the streaming goroutine starts; a stage span would always finish before the chat.completion.stream generation it nominally parents. * retrieve wraps the actual vector + keyword retrieve call inside HybridSearch, exposing the DB round-trip that previously sat invisibly between embedding generations and rerank. * web_search wraps the external web-search HTTP call when enabled. |
||
|
|
64b20a2d87 |
feat(agent): support dedicated model for query understanding step
Quick-answer (RAG) agents can now configure a separate chat model for the query-understanding stage (rewrite + intent classification), decoupling it from the main conversation model so users can route the lightweight rewrite call to a cheaper / faster model. - Add CustomAgentConfig.QueryUnderstandModelID and plumb it through PipelineRequest and ChatManage.Clone. - query_understand plugin prefers QueryUnderstandModelID on the text-only path; falls back to ChatModelID if the configured model cannot be resolved (with a warn log). Multimodal path is unchanged to keep vision-capable model selection intact. - AgentEditorModal exposes a ModelSelector under the existing Query Rewrite block; empty means reuse the main chat model. - Add i18n strings (queryUnderstandModel / placeholder / desc) for zh-CN, en-US, ko-KR, ru-RU. - Extend Go SDK AgentConfig with the new field. |
||
|
|
1f60d19f1b |
fix(agent-editor): move data-analysis toggle to retrieval section
The data-analysis pipeline stage is a retrieval-strategy concern (it only runs after chunk search/rerank), so the toggle belongs alongside the other retrieval knobs rather than under knowledge base settings. Refs: https://github.com/Tencent/WeKnora/issues/1244 |
||
|
|
3f9b09e306 |
fix(pipeline): make data-analysis stage opt-in per agent (#1244)
The legacy in-pipeline DuckDB SQL data-analysis stage used to run on every quick-answer RAG request whose retrieved chunks included a CSV/Excel file, adding one extra LLM round-trip (~3s) to generate a SQL query that most plain Q&A users never wanted. There was no way to disable it. Introduce a per-agent DataAnalysisEnabled flag (default off), wire it through PipelineRequest, and gate the DATA_ANALYSIS stage on it. Surface the toggle in the agent editor for quick-answer agents with at least one knowledge base attached. Refs: https://github.com/Tencent/WeKnora/issues/1244 |
||
|
|
86b05d923e |
fix(middleware): mask camelCase secret fields in request logs
The request logger's sanitizeBody only matched lowercase / snake_case field names (api_key, apikey, access_token, ...), so values for the camelCase JSON fields actually used by the API (apiKey, secretKey, refreshToken, accessToken, ...) were written to logs in clear text. Replace the per-field patterns with a single case-insensitive regex that tolerates optional `_`/`-` separators, covering snake_case, camelCase and PascalCase variants, and extend coverage to id_token, client_secret, private_key, auth_token, api_secret and passwd. The field name is preserved; only its value is replaced with "***". Add unit tests for sanitizeBody covering the previously-leaking camelCase fields and common variants. Fixes #1287 |
||
|
|
cacca049d9 |
feat(knowledge-base): document list filters and explicit batch-management UX
Add three new optional filters to the document list under a knowledge base detail page — parse status, source/channel, and updated time range — and rework multi-select to no longer cause the card title to jitter on hover. Backend - Introduce types.KnowledgeListFilter to aggregate optional filter dimensions (tag, keyword, file_type, parse_status, source, updated_from/to) and switch ListPagedKnowledgeByKnowledgeBaseID (repository/service/interface) to accept it instead of a growing positional parameter list. - The ListKnowledge HTTP handler accepts new parse_status, source, start_time and end_time query params; time params accept RFC3339, "YYYY-MM-DD HH:MM:SS" and "YYYY-MM-DD". The repository routes source="manual"/"url" onto the type column to stay consistent with file_type semantics; other source values match the channel column. - Update the four other callers (agent_service, initialization) to pass an empty filter struct, preserving prior behavior. Frontend - Add three controls in the doc-filter-bar (status select, source select, date-range picker with future-date disabled) wired through getKnowled / listKnowledgeFiles into the new backend params. - Replace the hover-triggered card checkbox with an explicit "批量管理" mode (mirrors the session list UX): in card view the checkbox only renders while batch mode is on, entered via the per-card "..." menu; the list view keeps its leading checkbox column. Switching from list to grid auto-enables batch mode when something is already selected, so the selection stays visible. - DocumentBatchBar now stays open whenever batch mode or selection > 0, and its "取消选择" button both clears the selection and exits batch mode. API surface sync - Regenerate Swagger artifacts (docs/docs.go / swagger.json / swagger.yaml). - Update docs/api/knowledge.md with the new query parameters. - Add backward-compatible ListKnowledgeWithFilter + KnowledgeListFilter to the Go SDK; the existing ListKnowledge keeps its signature. i18n - New filter labels in zh-CN / en-US / ko-KR / ru-RU; reuse existing menu.batchManage / batchManage.cancel for the multi-select strings. |
||
|
|
f635eaf466 |
chore(github): translate templates to English and improve content
- Convert issue and PR templates to English only for broader reach - Bug report: add Steps to Reproduce, Actual Behavior, WeKnora Version, and Deployment Method as required fields; expand log guide to cover Lite / Desktop / source builds - Feature request: replace user-selected Priority with Impact to reduce severity inflation - PR template: slim down to 6 sections, add Conventional Commits hint in the title, and require `make fmt && make lint && make test` in the checklist |
||
|
|
b387f3637a | fix(feishu): tolerate partial wiki node listing failures | ||
|
|
8b2a36d759 |
fix(multimodal): unblock processing when provider:// image read fails
When image bytes for a multimodal task cannot be read via FileService (e.g. tenant.StorageEngineConfig.MinIO is empty while the image was saved using the global MINIO_* env vars), the previous code fell back to the HTTP downloader, which rejected the provider:// URL with "unsupported URL scheme". The asynq handler then returned an error, asynq retried until exhaustion, and the per-knowledge "pending images" counter was never decremented — leaving the document stuck in "processing" indefinitely (issue #1282). Changes: - ImageMultimodalService now holds a default FileService and resolveFileServiceForPayload falls back to it when the tenant-scoped storage config cannot produce a usable service, mirroring the write-side fallback in knowledgeService.resolveFileService. - Extract readImageBytes: provider:// URLs are read exclusively via FileService and never handed to the HTTP downloader. - On unrecoverable read failure for a single image, log and skip that image but still call checkAndFinalizeAllImages so the parent knowledge can progress to post-processing. Fixes #1282 |