Add an opt-in named-entity extraction pass so the wiki knowledge graph
captures fine-grained entities (people, organizations, locations, ...)
and their relations, not just page-level link relations.
- new tables mate_wiki_entity / _mention / _relation (h2/mysql/kingbase)
- structured LLM extraction per chunk with entity resolution
(normalized-key dedup + embedding near-merge), mention/relation
persistence and page linking via chunk citations
- per-KB opt-in toggle (off by default); async dispatch after embedding
- read API: entity list, KB graph, entity ego-graph, manual extract
- UI: entity-layer toggle in the graph view + KB config toggle
- replace inline fully-qualified class names with imports in WikiProcessingService
Closes#336
* feat(wiki): unify raw materials & source watcher into a Sources tab with per-KB auto-sync
The raw-material directory scan and the Advanced "source watcher" sub-tab were
the same engine (same kb.sourceDirectory, same WikiDirectoryScanService) split
across two surfaces with two editable directory inputs. Merge them into one
"Sources" tab (upload / paste / directory manual scan + auto-sync toggle +
the raw-material list) and drop the watcher sub-tab from Advanced.
Auto-sync is now per-KB opt-in: a new watcher_enabled column (V146) gates the
periodic scan per knowledge base. The server-global mate.wiki.watcher-enabled
stays as an ops master switch — a KB is auto-scanned only when both are on
(AND). Manual scans are unaffected. Scan interval stays global for now
(tracked separately).
Closesmatevip/mateclaw#314
* docs(wiki): document source-watcher global switch env vars
Expose MATE_WIKI_WATCHER_ENABLED / MATE_WIKI_WATCHER_INTERVAL_MS as
explicit placeholders in application-mysql.yml, .env.example and
docker-compose.yml, mirroring MATE_WIKI_ALLOWED_SOURCE_ROOTS. Notes the
AND semantics (global ops gate + per-KB toggle) so operators know the
global switch alone is not sufficient.
A second POST to /reclassify on the same KB spawned an independent pass over
the same pages, doubling LLM spend and racing the first pass's page-type
writes. Add a per-KB in-flight guard that rejects a concurrent run with a
friendly message (409 via R.fail rather than a generic 500), released in a
finally once the async pass completes. Also count and broadcast per-page
failures so an all-failing run is visible instead of reporting changed=0, and
type the api modelId param as string|number per the snowflake ID convention.
Add a backfill path so pages created before a KB's pageType profile changed
can be migrated into newly-added types. A per-page classify-only LLM call
(title + summary in, single page_type out) is normalised through the profile
and written back via a partial update that never touches page content.
Exposed as POST /knowledge-bases/{id}/reclassify (admin) and a "re-classify
existing pages" action in the Wiki advanced panel.
Wiki page classification was only profile-aware in the main ingest pipeline.
Transformation outputs hard-coded "synthesis", agent-created pages were left
untyped, and the frontend hard-coded the built-in ten types for ordering,
colouring and labels — so custom/synthesis types sank to the bottom, rendered
grey and showed raw keys.
Backend:
- Add nullable target_page_type column to mate_wiki_transformation (V142,
mysql + h2) plus the entity field and CRUD normalization (blank = use
profile fallbackType; membership validated at save time, not edit time).
- Route transformation single-run + KB-aggregate page saves through
WikiPageTypeProfileService.normalizePageType so output joins the KB
classification; agent wiki_create_page now lands on the profile fallbackType
instead of an untyped page.
Frontend:
- Load + parse the KB pageType profile into the wiki store (order, labels,
fallbackType) on KB select / refresh.
- New useWikiPageType composable: profile-driven label (3-tier fallback) and
colour (built-in fixed + deterministic hash palette for custom types).
- Sidebar grouping order, graph colouring, node panel, graph filter and the
page header badge now follow the profile; transformation editor gains a
target-type dropdown sourced from the profile when output target is a page.
Refs #292
Add a single global-proxy switch that routes the backend's outbound traffic
through a configured HTTP/HTTPS/SOCKS proxy, for deployments that cannot reach
overseas APIs directly or must use a unified egress.
- ProxyManager installs the proxy via a default ProxySelector (honored by
java.net.http and HttpURLConnection), the proxy system properties, and a
--proxy-server arg for the browser tool; restores direct egress when
disabled. One switch covers LLM, web search, media generation, channels,
MCP and the browser.
- SOCKS applies to the HttpURLConnection-based egress only; the java.net.http
LLM/streaming path uses an HTTP proxy, and the UI states this.
- New Settings -> Network Proxy page: enable toggle, address, bypass list,
test-connection, and a coverage summary. Config stored as key/value in
mate_system_setting (no migration).
refs #109
Agents now have a per-agent primary wiki KB stored on
mate_agent.primary_kb_id. KBs remain workspace-shared — selecting one in
the agent editor only chooses the default wiki target for that agent, it
does not change the KB's ownership or visibility.
Backend
- AgentEntity: add primary_kb_id field (FieldStrategy.ALWAYS so the UI
can clear it back to "no primary")
- AgentController#update: switch body to Map<String, Object> so we can
tell "field missing" apart from "explicit null" via containsKey, then
convertValue back to AgentEntity
- WikiKnowledgeBaseService:
- new resolvePrimaryKb(agentId): prefers agent.primary_kb_id when it
points to a workspace-visible KB; falls back to legacy
kb.agent_id marker, then to most-recently-updated workspace KB
- listByAgentId now returns the full workspace set (KBs are
workspace-shared under the new model)
- update(id, name, description) no longer touches agent_id
- WikiController: new GET /knowledge-bases/bindable for the UI picker;
PUT /knowledge-bases/{id} no longer reads agentId
- WikiKnowledgeBaseEntity: add FieldStrategy.ALWAYS on embeddingModelId
and configContent so explicit nulls actually unbind/clear instead of
being silently skipped by MyBatis-Plus's NOT_NULL default
- Migrations V129 (H2 + MySQL): add primary_kb_id column + index, backfill
from legacy kb.agent_id, MySQL uses INFORMATION_SCHEMA guard +
PREPARE/EXECUTE for idempotency
- WikiKnowledgeBaseServiceTest: 13 cases, all passing
Frontend
- Agents.vue: new "Knowledge Base" tab, radio-select bindable KBs
- API: listBindableKBs() + Agent.primaryKbId typed string | number | null
- IDs handled as strings throughout (Snowflake-safe)
- i18n keys for the new tab in zh-CN and en-US
Closes#174
Model identifiers like 'Qwen/Qwen3-Embedding-8B' or
'Pro/deepseek-ai/DeepSeek-V3' carry forward slashes that Spring MVC
decodes from %2F before path matching, so even with the frontend's
encodeURIComponent the request never reaches the handler and 404s out.
The two affected endpoints take modelId as a request param instead:
DELETE /{providerId}/models/{modelId} -> DELETE /{providerId}/models?modelId=...
POST /{providerId}/models/{modelId}/test -> POST /{providerId}/models/test?modelId=...
modelApi.removeProviderModel / testModel in the UI follow suit, passing
the id via axios params so axios handles the URL encoding consistently.
providerId stays as a path variable — provider ids are kebab-case and
never contain slashes.
WeCom quoted-message context:
- Parse the body.quote field that arrives alongside any inbound message
(text / image / voice / file / mixed sub-types). When a user long-
presses a previous bot bubble and types a follow-up like "解释一下",
the agent now sees both the user's new text and the referenced
content as proper context — replies stay on topic instead of
guessing what was being explained.
- Quoted images / files are downloaded through the same pipeline as
inbound new media (magic-byte sniff, ZIP container peek for
DOCX / XLSX / PPTX recovery, chat-uploads layout) so the vision
sidecar and document tools can actually analyse what was quoted.
- Reading order in the assembled prompt: "[引用消息: ...]\n<user text>"
first, then quoted media parts, then the user's own current-message
media. Mixed quotes flatten into a space-joined summary.
Multimodal sidecar settings preservation:
- The bulk settings PUT used to unconditionally overwrite the vision /
video sidecar model ids — null in a partial payload became "" in
the DB, silently wiping the configured sidecar every time a user
saved an unrelated settings page (System / Music / Image / etc.).
Symptom: "I picked a vision model, saved a different settings tab,
now the bot can't see images anymore."
- Bulk save now guards both keys with non-null checks, matching the
pattern used for music / 3D / image / video / tts / stt blocks.
- A dedicated /settings/sidecar endpoint always writes both keys, so
the sidecar UI can still explicitly clear via null without leaking
the write-on-null semantics into every other settings save.
- Frontend sidecar card switches to the dedicated endpoint; other
settings pages keep their existing partial-payload behaviour.