- Apply the session-list additions to the SQLite init migration (Lite build)
and make QueryPaged dialect-aware: LOWER(..) LIKE on SQLite, ILIKE on
Postgres, drop NULLS LAST on SQLite. Escape LIKE wildcards in the keyword
via the existing escapeLikeKeyword helper.
- SetPinned now returns rowsAffected so the handler can respond 404 on
unknown / unauthorized session IDs instead of a misleading 200.
- GET /sessions always returns the enriched shape (pin state + IM origin
fields) so the frontend never needs a second roundtrip; the dual-path
legacy branch in the handler is gone.
- Register pin routes with the wildcard name that matches each verb's
existing radix tree (POST :session_id, DELETE :id) and accept either
param name in the handler; avoids gin's "wildcard conflicts" panic.
- Drop the redundant [platform] prefix from IM session titles now that
the list renders a platform icon alongside the title; add unit tests
for shortID / buildUserSessionTitle / buildThreadSessionTitle.
- Frontend: remove the submenu search input and its i18n/keyword wiring
(search lives elsewhere in the app); pin icon uses the TDesign `pin`
glyph and inherits color so the active session turns green; optimistic
pin moves the item to the top of the list so it shows up at the top
of the Pinned group; IM text badge replaced by the platform SVG icons
from assets/img/im, desaturated by default and full-color on hover
or when the session is active.
Sessions today are a flat list ordered by updated_at. Two gaps showed up in
practice:
- Users cannot find specific chats as the list grows beyond a screen.
- IM-created sessions (WeCom/Feishu/Slack/...) are indistinguishable:
every title was "IM-<platform>" or "IM-<platform>-<username>" and the
list API hid the underlying im_channel_sessions mapping, so admins had
no way to tell which Feishu group a session came from.
Backend
- Migration 000039 adds sessions.user_id (owner), is_pinned, pinned_at
plus a composite index for the list query. Existing rows keep user_id
NULL and stay visible at the tenant level for backward compatibility.
- CreateSession now writes the caller's user_id from auth context.
- GET /sessions accepts keyword / source / agent_id. When any filter is
set, the response switches to enriched items that LEFT JOIN
im_channel_sessions and expose im_platform / im_chat_id / im_thread_id
/ im_user_id / im_agent_id / im_channel_id. No filters => legacy shape,
existing clients unaffected.
- Ordering: is_pinned DESC, pinned_at DESC NULLS LAST, updated_at DESC.
- POST/DELETE /sessions/:id/pin for user-scoped pin/unpin.
- IM session titles: "[platform] <user|chat|thread>" with short ID
suffixes so group/DM/thread sessions are visually distinct without
needing a round-trip to fetch a display name from the IM adapter.
Frontend
- Search input debounced at 300ms drives the keyword filter.
- Pinned chats render in a dedicated group above the time-based groups,
with a pin icon and a pin/unpin entry in the per-chat dropdown.
- IM chats get a short [platform] badge in the list.
- Pin toggle is optimistic and guards against double-clicks.
- zh/en/ko/ru i18n keys added for the new strings.
Until now the IM channels a tenant has connected were only visible from
inside each agent's editor (AgentEditorModal → IMChannelPanel). Finding
out which bots are live across all agents meant clicking through every
one of them. Add a tenant-scoped overview so the set of connected IMs is
always one hop away from the avatar.
Backend
- im.Service.ListChannelsByTenant(tenantID) returns all non-deleted
channels in the tenant, LEFT JOIN'd with custom_agents.name so
built-in agents (which don't have rows in custom_agents) still show
up with an empty agent_name; the frontend substitutes a localized
"built-in agent" label.
- Credentials are intentionally stripped from this response — the list
view is read-only and the editor route (GET /agents/:id/im-channels)
remains the only source for secrets.
- New ChannelWithAgent DTO + IMHandler.ListAllIMChannels wired at
GET /api/v1/im-channels under the existing RegisterIMChannelRoutes
group, so auth middleware is inherited.
Frontend
- IMChannelsOverviewPanel.vue: floating submenu pane, stacked rows
(agent on top, channel below) with identical 20px avatars and 12px
labels so neither identity dominates. Each row is the whole click
target (jumps to the agent editor); the switch uses @click.stop to
toggle in place without triggering navigation.
- UserMenu.vue: new hover-driven submenu entry ("已接入的 IM" + link
icon). The pane is teleported to <body> because the sidebar container
has overflow:hidden that would otherwise clip right-flying content.
Position is computed from the menu item's rect and clamps/flips
against the viewport edges.
- "Live" indicator: a pulsing green dot on the menu item when at least
one channel is enabled. UserMenu prefetches the list once on mount;
the panel re-emits channels-changed after every load/toggle so the
dot stays in sync. Respects prefers-reduced-motion.
- AgentList.vue: supplement the existing onMounted-only
checkAndOpenEditModal with a watch(route.query.edit) so
router.push({ path: '/platform/agents', query: { edit, section } })
from an already-mounted AgentList (the common case when navigating
from the overview) opens the editor immediately instead of only
after a hard refresh.
Assets
- Seven platform SVGs under assets/img/im/, pulled from iconify
(simple-icons / logos / tdesign / icon-park / remix icon) and baked
with brand colors so monochrome sources (wecom/wechat/dingtalk/
feishu) don't render black.
i18n
- New imOverview namespace in zh-CN / en-US / ko-KR / ru-RU covering
the menu label, panel title/subtitle, column headers, builtin-agent
fallback, and the live-indicator tooltip.
Tested
- go build ./...
- go test ./internal/im/...
- vue-tsc --noEmit
- npm run build
GORM Scan / AfterFind hooks for every AES-encrypted column followed
the same lenient pattern:
if decrypted, err := utils.DecryptAESGCM(c.APIKey, key); err == nil {
c.APIKey = decrypted
}
When SYSTEM_AES_KEY was missing, rotated, or the wrong length the
decryption error was swallowed and the in-memory struct kept the raw
"enc:v1:..." ciphertext. The application then happily forwarded the
ciphertext upstream as the actual API key / password, surfacing as
401/403/SignatureDoesNotMatch from third-party vendors. Worse, a
ciphertext snippet of a customer credential was leaking into the
external provider's request logs.
Introduce utils.DecryptStoredSecret that:
- returns "" / legacy plaintext untouched (no false positives for
pre-encryption rows);
- returns ErrEncryptedDataMissingKey when the value carries the
enc:v1: prefix but no AES key is configured;
- propagates any GCM auth-tag failure from a rotated key.
Wire it into the five Scan / AfterFind sites that currently swallow
the error:
- Tenant.AfterFind (tenants.api_key)
- CredentialsConfig.Scan (tenants.we_knora_cloud.app_secret)
- ModelParameters.Scan (models.parameters.api_key, .app_secret)
- ConnectionConfig.Scan (vector_store_connections.password, .api_key)
- WebSearchProviderParameters.Scan (web_search_providers.api_key)
The error is wrapped with the originating column so the failure is
diagnosable in the logs. Operators must restore the previous
SYSTEM_AES_KEY (or rotate the affected secrets) instead of receiving
silent vendor 401s.
Add table-driven tests covering empty input, legacy plaintext,
round-trip, missing key, wrong-length key, and rotated key. The
strict path explicitly asserts that ciphertext does NOT leak in the
returned plaintext on any error path.
Reverts the tenant-ID-from-context change in da5fcd3. The storage path
encodes the resource owner's tenant, and the presigned-URL verifier
uses that ID to look up the owning tenant's StorageEngineConfig. Using
the caller's tenant ID from context would break cross-tenant shared
resources — e.g. when tenant Y reads an image from a KB shared by
tenant X, signing with Y would cause the verifier to open Y's storage
backend and 404 on X's file.
Keep:
- presignDefaultTTL shortened from 24h to 2h (independent improvement).
- godoc note on ParseTenantIDFromStoragePath flagging the ambiguity
for cloud paths with numeric bucket/region names.
- Unit tests for path-based tenant extraction and the no-external-URL
backward-compat path.
Addresses review feedback on the IM storage URL rewrite:
- localFileService.GetFileURL now reads tenant ID from request context
first, falling back to ParseTenantIDFromStoragePath only when context
is absent. Fixes ambiguity for cloud providers whose paths embed
numeric bucket/region names before the tenant segment, which could
mint presigned URLs bound to the wrong tenant ID.
- Shorten presigned URL default TTL from 24h to 2h. A leaked HMAC key
authorizes cross-tenant file reads, so URLs should expire quickly;
IM clients fetch referenced images within seconds anyway.
- Document ParseTenantIDFromStoragePath as a best-effort fallback.
- Add unit tests covering context-first, path-fallback, and the
no-external-URL backward-compat path.
IM platforms (Feishu, Slack, Telegram, DingTalk, Mattermost, WeCom) cannot
render provider:// URLs (local://, minio://, s3://, etc.) that appear in
LLM answers containing knowledge base images. The web frontend handles
these via the authenticated /files endpoint, but IM clients need publicly
resolvable HTTP URLs.
Changes:
- Add HMAC-SHA256 presigned URL utility (internal/utils/presign.go) for
generating time-limited, signature-verified file access URLs
- Add GET /api/v1/files/presigned endpoint that serves files without
session auth, verified by HMAC signature and expiry
- Update localFileService.GetFileURL() to return presigned HTTP URLs
when APP_EXTERNAL_URL is configured (cloud backends already return
presigned URLs via their SDKs)
- Add IM content rewriting pipeline: strip <image> XML tags, strip
citation tags, rewrite storage URLs to HTTP — applied at all IM
output points (streaming flush, non-streaming reply, fallback)
- Add holdback buffer in streaming flush to prevent URL/tag splitting
across chunk boundaries
Closes#1058
Card grid showed a faded checkbox at all times after multi-select was
added, which felt visually noisy and misaligned the title column. Hide
the checkbox column by default and let it expand on hover / when any
card is selected / when this card itself is selected. Drop
:focus-within so a cleared selection doesn't get stuck visible due to
the retained input focus.
Aligns the three list-style settings pages (ModelSettings, WebSearchSettings,
McpSettings) under a consistent visual language, and replaces the modal editors
with right-side drawers for a calmer editing flow.
- Extract shared building blocks under frontend/src/components/settings/:
SettingCard (list-item card), SettingDrawer (500px right drawer with a
pinned footer), and useConfirmDelete (a single DialogPlugin.confirm path
that replaces the mix of window.confirm / t-popconfirm / ad-hoc dialogs).
- Swap the old bespoke overlay / t-dialog shells in ModelEditorDialog and
McpServiceDialog for SettingDrawer. Form logic is untouched.
- Model settings now use a single t-tabs filter (All/Chat/Embedding/ReRank/
Vision/Speech with per-type counts) and decouple "Add model" from the tab
state via a dropdown in the header. Type tags get a 5-color palette so
categories read at a glance. The list is a two-column grid.
- Web search and MCP lists are likewise rendered as two-column grids, with
richer cards: transport type / provider, on-off and default state, plus a
compact meta row showing base URL, proxy or service URL.
- Introduce modelSettings.typeShort.{chat,embedding,rerank,vllm,asr} in all
four locales for the shortened category labels.
registerIMAdapterFactories in internal/container/container.go had grown
to ~290 lines of platform-specific config parsing and runtime start/stop
logic across 7 IM platforms, plus three private credential helpers. The
container package is meant to wire dependencies, not own each platform's
credentials schema and websocket lifecycle.
Extract each platform's factory into its own subpackage file and expose
the credential helpers on the im package:
- New im.ParseCredentials / im.GetString / im.GetBool (formerly the
unexported parseCredentials / getString / credentialBool in container).
- New im.ResolveMode helper collapses the six identical "default to
websocket when Mode is empty" blocks.
- New {wecom,feishu,slack,telegram,dingtalk,mattermost,wechat}.NewFactory
returns an im.AdapterFactory; container.go now calls each one in a
single line.
Behavior is preserved: adapter construction, goroutine start/stop,
log messages, default modes (mattermost still defaults to "webhook";
others to "websocket"), and error strings are unchanged. The WeCom
corp_agent_id float64/int switch and the WeChat long-poll-only path
are preserved as-is.
Net: container.go drops ~320 lines and no longer needs to be touched
when adding or tuning an IM platform.
- Detect sticky state via IntersectionObserver and flatten the table
header's top corners when stuck so the rounded cut-outs no longer
reveal the list container's background while scrolling.
- Lift the batch-action toolbar out of flex flow and float it 12px above
the scroll container bottom so selecting rows no longer shrinks the
visible list; strengthen its shadow to match the floating treatment.
Add a danger-themed reset button next to the existing copy/eye buttons
on the API Info settings page. Confirmation dialog warns that the old
key is revoked immediately; on success the new plaintext key replaces
the displayed value and is auto-revealed so the user can copy it.
Backend: new POST /api/v1/tenants/:id/api-key handler that wraps the
existing TenantService.UpdateAPIKey; access is gated by the same
authorizeTenantAccess check as other tenant endpoints. The handler
returns the freshly generated plaintext key, while the encrypted form
is persisted to the database.
Frontend: new resetTenantApiKey API client, reset button + confirm
dialog wiring in ApiInfo.vue, plus matching i18n entries for zh-CN,
en-US, ko-KR, and ru-RU.
The Secret template defaulted both keys to randAlphaNum 32, which Helm
re-rolls on every template render. As a result, any `helm upgrade`
without explicit secrets.systemAesKey / secrets.tenantAesKey rotated
the keys, breaking decryption of every previously encrypted field
(tenants.api_key, model API keys, vector store credentials, web
search provider keys, WeKnoraCloud.AppSecret) and surfacing
"enc:v1:..." ciphertext in the UI.
Use Helm's `lookup` to reuse the values stored in the existing Secret
when one is already present, falling back to randAlphaNum only on
first install. Also document the recovery caveat in values.yaml so
operators understand the risk of relying on the auto-generated value.
CreateTenant and UpdateAPIKey both overwrote tenant.APIKey with the
AES-encrypted ciphertext before returning, causing the UI to display
"enc:v1:..." right after generating or resetting an API key. Save the
plaintext in a local variable, encrypt only the value handed to
UpdateTenant, and return the plaintext to callers.
Subsequent reads via GetTenantByID continue to rely on the AfterFind
hook for transparent decryption, so this only affects the initial
write-back path.
The chat layout is a flex column (.chat) inside .platform-route-outlet,
with .chat_scroll_box {flex:1; overflow-y:auto} above .input-container.
Flex items default to min-height:auto, so when messages outgrew the
viewport the scroll box was stretched by its content instead of
scrolling internally. That pushed the input container out of view and
broke page-level scrolling because the parent clips overflow.
- .chat: add min-height:0 so its flex:1 child can shrink
- .chat_scroll_box: add min-height:0 to enable overflow-y scrolling
- .input-container: add flex-shrink:0 so a tall scroll box cannot
squeeze it to zero height
The previous list-view tone refinement set the selected-row hover
background to var(--td-brand-color-light), which is aliased to
var(--td-brand-color-1) — the same value used as the static selected
background. Hovering a selected row produced no visible change, leaving
users without feedback for what was clickable.
Use color-mix() to blend brand-color-1 with a touch of brand-color so
the hover delta is visible in both light and dark themes, without
reverting to the saturated brand-color-2 the original code used.
Backend
- BatchDeleteKnowledge handler now enqueues an asynq
TypeKnowledgeListDelete task instead of calling DeleteKnowledgeList
synchronously, matching the pattern used by ClearKnowledgeBaseContents.
Avoids long HTTP timeouts on large batches and shares the existing
async cleanup pipeline.
- Extract a small enqueueKnowledgeListDelete helper on KnowledgeHandler
so BatchDeleteKnowledge and ClearKnowledgeBaseContents share the
payload/marshal/enqueue boilerplate.
Frontend (DocumentListView)
- softer outer border and lighter row separators
- header background switched to the page tone with placeholder-color
text, reducing institutional gray
- selected row uses an inset left accent bar and a quieter brand tint
instead of a saturated background; hover stays neutral
- consolidate the .row-more-btn { opacity: 1 } rule that was duplicated
across the hover and selected blocks
Add a list-view alongside the existing card grid (issue #957) and
multi-select batch delete (issue #1045) for documents inside a knowledge
base. The two views are toggled from the toolbar and the preference is
persisted in localStorage. Selection state is shared between views with
shift-range support and a sticky batch-action bar.
Backend
- POST /api/v1/knowledge/batch-delete: validates KB scope and editor
permission, then delegates to the existing DeleteKnowledgeList service
which already cascades vector/file/graph cleanup. Caps batch size at
200 and uses a single GetKnowledgeBatch call for membership validation.
Frontend
- DocumentListView: table layout with sticky header, checkbox column
(with indeterminate select-all), file-type icons, status badges,
per-row action menu.
- DocumentBatchBar: floating pill bar with selected count and delete
action; appears when items are selected.
- KnowledgeBase.vue: view-mode toggle, selection state, batch-delete
confirmation dialog, hover/active checkbox overlay on existing cards.
- Extracted formatFileSize and getFileIcon to utils/files.ts to share
with the new list view and avoid further drift from the existing
inline copies.
- i18n keys added for zh-CN, en-US, ko-KR, ru-RU.
Closes#1045Closes#957
When the uploaded file is itself an image, the image reference now carries
an IsOriginal flag so ResolveAndStore skips the small-icon size filter.
Otherwise small standalone images (e.g. avatars below 64x64) were silently
dropped before reaching multimodal OCR/caption processing.
When users ask broad queries like "请整理知识库中的数据" in RAG mode,
vector/keyword search returns nothing because the query has no specific
content to match. The user needs to see what documents exist, not search
results.
- Add buildKBDocumentListing() to fetch document titles/filenames from
the knowledge base and inject them into the fallback prompt via
{{kb_documents}} placeholder
- Update fallback prompt templates (model_fallback, default_fallback_prompt)
to include document listing context so the LLM can guide users
- Improve rewrite prompt: tighten summarize vs kb_search classification,
expand kb_search to cover browse/organize/list operations, add examples
Closes#959
The default DOCREADER_DOCX_MAX_PAGES=100 silently truncates large
documents, causing users to see at most ~1000 chunks regardless of
document length. Change the default to 0 (no limit) so all pages are
processed. Operators who need a cap can still set the env var.
Fixes#719
The IM service applied a hard 120s deadline (`qaTimeout`) to the entire
QA pipeline context. Multi-round ReAct agents easily exceed this — when
the deadline fires mid-stream the LLM provider returns "context deadline
exceeded" which leaks into the user-visible answer (fixes#1000).
Changes:
- Replace `context.WithTimeout(ctx, qaTimeout)` with `context.WithCancel(ctx)`
in both `handleMessageStream` and `runQA`, matching the web handler's
approach. Each agent round still has its own per-call LLMCallTimeout
(default 120s) which is sufficient.
- Filter `ResponseTypeError` stream chunks in `streamLLMToEventBus` so
error messages (e.g. "context deadline exceeded") are never appended
to the LLM content. Instead they are captured in `StreamError` and
surfaced as a Go error when no usable content was produced.
- Bump Redis inflight mapping TTL from qaTimeout+30s to 10 minutes to
accommodate longer agent runs.
- Remove the now-unused `qaTimeout` constant.
Three bugs fixed:
1. Session cross-contamination (#1066): resolveUserSession and
resolveThreadSession looked up ChannelSession by (platform, user_id,
chat_id, tenant_id) without agent_id. When the same user talked to
two bots bound to different agents under the same tenant, they shared
a session — mixing knowledge base context. Add agent_id to all four
WHERE clauses and update the DB unique indexes to match.
2. Swapped parameters in resolveThreadSession: the SQL expected
(chat_id, thread_id) but Go args passed (threadID, msg.ChatID).
3. Orphaned ChannelSession crash (#1046): deleting a session from the
WeKnora UI soft-deletes it (GORM), but the ChannelSession row
survives because soft-delete doesn't trigger SQL ON DELETE CASCADE.
Subsequent IM messages hit "record not found" on GetSession and the
bot becomes permanently unresponsive. Now detect this case, recycle
the stale mapping, and transparently create a fresh session.
The outer if/else in Execute claimed to prefer chatModel (LLM-based
reranking), but both branches called rerankResults which internally
prefers rerankModel first. This made the outer branching dead code
with a misleading comment.
Consolidate into a single branch that delegates entirely to
rerankResults, whose actual priority is: rerankModel → chatModel → none.
Replace token-by-token rendering (marked.lexer + v-for + marked.parser)
with single-pass marked.parse() in botmsg.vue and AgentStreamDisplay.vue.
This prevents Vue's v-for keyed diffing from destroying KaTeX DOM nodes
on each streaming update.
Also adds a dev-only markdown test page (/platform/dev/markdown) with
LaTeX, code, tables, Mermaid, and streaming simulation for visual
regression testing.
- Add per-KB sync.Map lock in Lite mode to mirror Redis SetNX concurrency.
- Requeue failed wiki ops in Lite by enqueueing asynq tasks with delay/tracing.
- Teach SyncTaskExecutor to honor ProcessIn delay and MaxRetry like Redis asynq.
Sync key structure of non-Chinese locale files with zh-CN.ts as the
source of truth: add missing keys (translated into the target language)
and remove keys that no longer exist in zh-CN.
- en-US: +1 (knowledgeEditor.wikiBrowser.aliases)
- ko-KR: +110 / -9 (drop confluence/github/web_crawler entries that
don't exist in zh-CN; add chat.wiki*, auth.oidc*, agent.editor.web*,
knowledgeEditor.indexing.*, agentEditor.{im.wechat*,desc.web*,
llmCallTimeout.*}, datasource.* prereq/scheduleHuman/resourceType/
relative-time keys, etc.)
- ru-RU: +108 / -9 (same shape as ko-KR, minus auth.oidc which already
existed)
- Update Scan method to handle both legacy bare-array format (e.g. [{...}, {...}]) and current object-wrapped format (e.g. {"engines": [{...}, {...}]}).
- Improve error handling for unmarshalling failures, providing clearer feedback on format issues.
This change ensures backward compatibility while allowing for the new data structure.
- Introduced a mechanism to track and requeue failed operations during the wiki ingest process.
- Added a new `requeueFailedOps` function to append failed operations back to the Redis pending list for retry in subsequent batches.
- Updated the `ProcessWikiIngest` method to collect failed operations and ensure they are retried after trimming the pending list.
This change improves the robustness of the ingest process by preventing data loss from transient failures.
Two critical fixes to the peekPendingList function:
1. Add error logging for JSON unmarshal failures (line 303)
- Previously: Malformed JSON items were silently skipped with no indication
- Now: Each failure is logged with the error and raw item content (first 100 chars)
- This makes data loss visible and debuggable
2. Fix trim count mismatch (line 325)
- Previously: Returned len(result) - the count of raw Redis items peeked
- Now: Returns len(ops) - the count of successfully parsed items
- Impact: Malformed items now stay in Redis for retry instead of being discarded
- This prevents silent data loss when JSON encoding issues occur
Root cause of the reported issue:
- When 8 files are uploaded in a batch, if one has encoding issues that cause
JSON.Unmarshal to fail, it was silently dropped from processing
- The file was trimmed from Redis without being processed
- Result: Only 7 of 8 files appeared in the Wiki
Fix approach:
- Make unmarshal failures visible in logs
- Retry malformed items in the next batch instead of discarding them
- Admin can see the errors and take corrective action
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add llm_call_timeout field to CustomAgentConfig TypeScript interface
- Add thinking field to CustomAgentConfig (for extended thinking support)
- Add UI control (number input) in AgentEditorModal for timeout configuration
- Range: 0-600 seconds (0 = use global default)
- Supports 60-600 seconds recommended range
- Add i18n translations in Chinese (zh-CN) and English (en-US)
- Label: "LLM 调用超时" / "LLM Call Timeout"
- Description with explanation of timeout behavior
- Hint about default and unlimited wait implications
- Placeholder with recommended range
- Initialize llm_call_timeout: 0 in formData (uses global default)
This completes the frontend support for per-agent LLM timeout configuration that was previously missing. The setting integrates with the existing three-tier configuration hierarchy:
1. Per-agent config (llm_call_timeout in custom_agents table)
2. Global config (agent.llm_call_timeout in config.yaml)
3. Hardcoded default (120 seconds)
- Promote Interface Showcase to right after Latest Updates / before
Architecture so users see the product UI early.
- Rebalance grid: hero Q&A (full width) -> Wiki Browser + Wiki Graph
(50/50) -> Agent Mode (full width) -> KB Management + Settings
(50/50). Forces equal-width columns via width=50% so screenshots
no longer look uneven.
- Add small emoji prefixes to subsection labels for visual rhythm.