Add an async export feature on the Dashboard page -- global admins can
generate and download a multi-sheet operational data report (.xlsx
packaged as .zip). The export covers 9 sheets:
1. Overview - interval KPIs, system snapshot, 7-day trend, period comparison,
model details (configured providers only), agent activity ranking top 10
2. Token Usage - daily breakdown by runtime_provider with avg tokens/msg
3. Skill Stats - skill list with usage count, last-call time, bound agents
4. User Stats - per-(workspace, user) aggregated tokens, duration, last active
5. User Conversations - detail rows pairing user-asst messages
6. Security and Audit - unified view across 6 sources (guard rules, audit logs,
approvals, grants, config, business audit events)
7. Channel Stats - per-channel conversation count, tokens, unique users
8. Model Config - enabled plus API-key-configured models with parameters
9. Cron Jobs - execution records with duration and token usage
Backend highlights:
- generate/progress/download endpoints guarded by PreAuthorize hasRole ADMIN
- single AtomicBoolean lock (409 when busy), 90-day frontend cap, 5-min deadline
- metadata-based tool-call counting, deleted=0 filtering everywhere
- value label mapping (chat to dialogue, TRUE to enabled, etc.)
- one-time downloadToken, file auto-cleanup after 24h or download
Frontend highlights:
- SVG ring progress bar with smooth dashoffset transition plus slow rotation
- visibility gated by workspaceStore.isGlobalAdmin (v-if on button)
- 1-second polling driving progress state machine (idle/generating/done)
- Element Plus date-picker (30-day default, 90-day max)
- Remove mateclaw-ui/package-lock.json and yarn.lock. This is a pnpm
monorepo where pnpm-lock.yaml is the only lockfile; the npm/yarn locks
were stray duplicates. Add a .gitignore rule so they are not committed
again by mistake.
- SourceEvidenceLedger: reference Pattern/Matcher by their imported
simple names instead of inline fully-qualified names.
Backend (SourceEvidenceLedger):
- appendWikiSourceTable now normalizes existing source lines in-place to
canonical "[N] Title - section - page N" format instead of skipping them
- Added replaceSourceLine helper that matches a full source line by regex
and replaces it with the canonical form
- When source lines exist without a "来源:" header, automatically insert
one so the frontend preprocessor can locate the source table
Frontend (useMarkdownRenderer):
- Added data-citation-index / data-citation-title to DOMPurify whitelist
- Added preprocessWikiCitations preprocessor: parses the canonical source
table to build an index-to-title map, replaces [n] markers in the answer
body with clickable <a> links, and wraps entire source-table rows so the
full line is clickable
- Integrated into the render pipeline after wikilink substitution and
before Marked parsing
Frontend (useGlobalWikilinkClick):
- Extended the click delegation selector to match both .wiki-link and
.wiki-citation elements
- Title extraction falls back: data-citation-title || data-wiki-title
Tests: added three test cases for source-line normalization, idempotency,
and automatic header insertion
Live lifecycle board (grid<->board toggle) plus an assignee-swimlane plan
board that groups follow-up re-runs of one goal into a single xN card.
Custom right-side detail/goal panels with markdown output. Fixes plans
being persisted under the per-run trace id so the board actually populates.
Closes#385
Issue #304. Operators who want an agent with NO knowledge base had no
way to express it: leaving the KB picker empty fell through to "inherit
workspace-wide" (every KB visible), so the agent ended up ingesting
every KB's context. This adds the same opt-out toggle that
skills_disabled (V126) / tools_disabled already provide.
Backend:
- V154 migration (h2 + mysql + kingbase): mate_agent.wiki_disabled
BOOLEAN/TINYINT/SMALLINT NOT NULL DEFAULT FALSE. Legacy agents stay
bit-identical.
- AgentEntity.wikiDisabled: Boolean field, @TableField("wiki_disabled").
- AgentBindingService.getBoundKbIds: short-circuit at the top —
wiki_disabled=true returns Set.of() regardless of binding rows. Mirrors
the precedence contract of getBoundSkillIds vs skills_disabled.
- AgentBindingService.setKbBindings: a non-empty save auto-clears a
stale wiki_disabled flag (same contract as setSkillBindings /
setToolBindings on their respective flags). Empty saves leave the flag
untouched — the UI toggle owns the bit, not the binding writer.
- AgentBindingServiceWikiDisabledTest: 5 cases covering all three
return states + the stale-flag auto-clear + empty-save no-op.
Frontend:
- Agents.vue KB picker: add the "此智能体不使用任何知识库" /
"This agent uses no knowledge bases" toggle, mirroring the skills /
tools picker layout. Tab badge shows "Off" when the toggle is on.
- types/index.ts: add Agent.wikiDisabled?: boolean.
- Save logic: when wikiDisabled is on, send an empty KB list (the
setKbs contract then leaves the flag alone server-side, exactly as
setSkills / setTools behave for their opt-out flags).
- i18n (zh + en): new strings for toggle label, hint, badge, and the
scope description shown when the toggle is on.
Stacked on top of #382 (which introduced AgentBindingResolver
.getBoundKbIds). No agent-runtime changes — wiki tools already degrade
cleanly when getBoundKbIds returns Set.of().
- per-KB entity-type whitelist (config UI + persistence; empty = built-in defaults)
- entity graph: legend grouped by type with click-to-filter; nodes colored by type
- always show entity names on graph nodes (not only on hover)
- earthy categorical palette aligned to the app theme, shared by entity & page graphs
- theme-aware graph label color (resolve CSS var for canvas, light/dark correct)
- manual extract = full rebuild: idempotent force re-extraction + orphan pruning,
guarded against data loss on a fully-failed run
- regression test for force re-extraction; zh/en i18n
When a webchat visitorId + sessionId pair exceeds the conversation_id
column width, WebChatController#deriveConversationId folds the variable
part into a SHA-256 hash prefixed with `#`:
webchat:<key8>:#<sha256[0..40]>
That `#` is the URL fragment delimiter. Every URL the admin console
builds by interpolating the conversationId into a path — message list,
status, rename, pin, model, delete, goals/by-conversation, chat/stop,
chat/pending-approvals — gets truncated at the `#` before reaching the
server. Symptom: opening one of these conversations in the console
surfaces as 405 (GET landing on @DeleteMapping("/{conversationId}"))
and 403 (owner check on the truncated id).
Add an `encId` helper (encodeURIComponent) and apply it to every
conversationId path segment. The server's @PathVariable decoder already
handles the percent-encoded form transparently, so this is purely a
client-side fix that recovers every existing hashed-id row in addition
to any future ones.
Issue: #372
The Web/API (webchat) onboarding wizard marked api_key as required while
it is also readOnly and platform-generated on save. The readOnly field
could never be filled during creation, so canSubmitConfig never passed
and "Continue" stayed disabled.
Exclude readOnly fields from the wizard's required/optional field sets so
they neither gate "Continue" nor render as fillable inputs. The api_key
still appears as required + readOnly in the edit modal once a value
exists.
Refs matevip/mateclaw#338
Turn a single natural-language requirement into a ready-to-review
employee: the model proposes name, persona, runtime type and a
validated set of skills/tools/knowledge base, which the user confirms
or tweaks before the agent is created.
- backend: POST /api/v1/agents/generate builds a draft from the
workspace's real capability catalog; every suggested tool/skill/KB is
re-validated against the catalog so nothing hallucinated is offered
- frontend: 3-step wizard at /agents/create reusing the existing
create + binding endpoints; reusable capability picker shows selected
items as compact chips with an on-demand searchable catalog
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
Always-on memory (structured user/feedback blocks, PROFILE.md, MEMORY.md) is injected into every system prompt but only ever grew, inflating per-turn context over time. This adds deterministic size control across all always-on sources:
- Injection budget: cap the always-on structured block by total chars and per-type entry count, keeping the most-recently-updated entries (LRU by Updated date) and disclosing how many were omitted
- Nightly consolidation: a dedicated scheduled pass merges duplicate/stale user & feedback entries via the LLM, preserving each entry's original Updated date; runs per owner bucket (shared + personal) with a per-run cap and a never-grow safety guard
- File ceilings: deterministic backstop truncates PROFILE.md / MEMORY.md at a section boundary when a rewrite overruns its budget
- Manual trigger endpoint for the consolidation maintenance task
All knobs under mate.memory.*; covered by unit tests.
- goal: continue (not skip) on max-iterations and evidence-insufficient turns.
A max-iterations turn grants a fresh iteration budget ("hard continuation"),
bounded per run and sized into the graph recursion ceiling, so a task too big
for one budget keeps going instead of stalling until the next user message.
- plan-execute: re-plan the remaining work on a step exception, and on a
signature-based stall (repeated failures / identical results / no usable
result) instead of advancing dependent steps with junk; bounded by a per-run
re-plan cap, with a graduated change-strategy nudge before the hard stop.
- plan-execute: auto-derive a goal from a genuine multi-step plan, seeding the
acceptance criteria from the plan steps, so the goal subsystem engages without
the model calling setGoal; broadcast goal_created so the UI hydrates.
- react: refund the iteration for setup-only rounds (load_skill / enable_tool)
so a tight budget is not eaten by the load-then-use two-step.
- ui: re-fetch the active goal when a turn finishes so a goal created or mutated
mid-conversation surfaces without depending on an SSE event.
- streaming: make retry backoff / total-time budget instance fields with a
test-only seam; clarify that the wall-clock budget (not max-retries) bounds a
sustained SERVER_ERROR loop to ~8 attempts, fixing the slow/flaky retry test.
- rescan: keep the KB id as a string end to end so the 19-digit snowflake id
isn't truncated past Number.MAX_SAFE_INTEGER (rescan no longer 404s)
- lint: resolve [[...]] targets against page slugs AND titles like the viewer,
so a title reference to an existing page is no longer reported broken
- ingest: derive the slug deterministically from the title (no inconsistent
romanization), auto-recompute broken links once a KB finishes importing,
and reconcile dangling [[concept]] links — redirect to the covering page via
declared aliases, or demote to plain text when uncovered
- add the page aliases column migration for h2 / mysql / kingbase
- add useStreamingMarkdown: cap mid-stream markdown re-render to ~140ms,
full-fidelity render once the segment completes
- skip code-block language auto-detection while streaming (escaped plain
text), restore full highlighting on the final render
- defer echarts/mermaid blocks to a lightweight loading placeholder while
streaming so their parsers never run on truncated source
- bypass the render cache for streaming-mode output
- wire into ContentSegment and MessageBubble (content + thinking)
Surface the connected database product as a subtle chip in the Dashboard
header. SystemHealthService now reports a database label on /system/health
(reused by the front-end — no extra request), derived from a new
DatabaseBootstrapRunner.getDatabaseLabel() that reads the JDBC product name
once and normalizes it to a canonical label (MySQL / MariaDB / PostgreSQL /
H2, and 人大金仓 for the KingbaseES family), collapsing driver version noise.
The raw-materials surface was renamed to "Sources" when upload, paste,
directory scan and per-KB auto-sync were unified into one tab. The read-only
viewers' reading-toggle segment still carried the old "Raw materials" label,
so managers saw "Sources" while read-only viewers saw "Raw materials" for the
same panel. Point the segment at the same i18n key for a consistent name.
* 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.
The KB workspace split into a reading view (pages + graph) and a manage view
(raw materials, config, transformations, advanced, recent-activity snapshot)
gated behind manage:wiki. That moved the raw-materials and recent-activity
surfaces — which read-only viewers (view:wiki without manage:wiki) could
previously browse — entirely behind the management gate, silently dropping
their access.
Re-surface both in the reading-view segmented toggle for viewers who lack a
management view. Managers keep the focused pages/graph toggle and still reach
these surfaces through the management view, so nothing is duplicated for them.
The content panels already render by activeTab, so this only widens the
reading toggle and the activeTab/readingTab types.
- Fail closed to a global fallback sandbox root when a conversation has no
per-workspace base path, instead of leaving file/shell tools unconstrained
- Refuse shell commands that delete the workspace root directory itself
- Block workspace-boundary escapes at the policy layer before the approval
prompt, not only at execution time
- Approval bar now shows the actual command / target path being approved
The pageType profile could only be edited as a raw JSON string. Add a
structured form editor (default) that builds the profile without writing JSON:
profile-level settings (fallbackType / allowAdditionalFields), an ordered page
type list (add via a small wizard, remove, reorder), and a per-type form for
label / description / layer / field schema, with stage prompts (route/create/
merge) and the markdown template folded into an "advanced" section that carries
inline descriptions and examples. A form/JSON toggle keeps the JSON view as the
final review surface; serialization preserves unknown keys for forward-compat,
and save/validate/reset reuse the existing endpoints (no backend change).
Closesmatevip/mateclaw#310
The KB workspace previously stacked all seven surfaces in one tab strip.
Split them by intent: a gear on each library card opens the management view
(raw materials, config, transformations, advanced, hot cache), while clicking
the card body opens the reading view (pages + graph). The reading view drives
page/graph via a header segmented control with the page tree shown only for
the page viewer; the two views share loaded data and toggle without refetch.
Closesmatevip/mateclaw#308
Closes#289 — after an MCP server (re)connects, chat queries kept replying
"from memory" instead of calling MCP tools.
Root cause: agents snapshot their tool set at build time and are cached in
AgentService.agentInstances, but MCP server lifecycle changes never
invalidated that cache (unlike model-config / tool-guard changes which do).
A stale, tool-less agent graph survived until process restart.
Changes:
- Add McpServerChangedEvent; McpServerService publishes it on connect /
disconnect / reconnect / delete / (re)connect-failure / batch refresh /
startup init. AgentService listens and calls refreshAllAgents(), so the
next turn rebuilds against the live MCP tool set. Also closes the boot
race where the web server accepts requests before the @Order(200) MCP
init runner finishes.
- Make create/update/toggle connect asynchronously on a dedicated pool
("mcp-connect") so a slow/unreachable server can no longer freeze the
admin request; status returns immediately as "connecting".
- UI: render the new "connecting" status (pulsing amber dot), show a
friendly "connecting in background" toast, and poll until the status
settles (window widened to ~40s to outlast the default connect timeout).
- UI: MCP config modal no longer closes on outside/backdrop click — only
the × and Cancel buttons close it, so an accidental click can't discard
unsaved config.
Verified E2E: ckjia-shopping (参考价) MCP server connected at runtime with
no backend restart; the cached 通用助手 agent immediately enabled and called
ckjia_shopping_recommend, returning real product cards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vision/video sidecar dropdown filtered candidates through the built-in
capability heuristics, so provider-compatible models whose custom names match
no known prefix (and carry no declared modalities) were hidden and could not be
selected as a sidecar — even when they natively support the modality.
- listByType now returns every enabled chat model, annotating each row with a
transient modalityCapable flag and sorting known-capable rows first, instead
of hard-filtering recognized models only.
- MultimodalRouter honours an explicitly configured sidecar model instead of
dropping it when the heuristics don't recognize it; a wrong pick degrades
gracefully through the caption path rather than silently disabling routing.
- ModelPicker gains an optional capability badge; the sidecar UI tags
recognized vision models while keeping every enabled model selectable.
Rename the webchat channel from "embed widget" to "Web / API access" (key
unchanged, docs/i18n only) and extend the backend SSE endpoint for pure
backend integration:
- WebChatRequest gains optional agentId (route one Key to multiple agents;
rejected unless the agent shares the channel's workspace) and sessionId
(one visitor, multiple isolated threads).
- sessionId is validated ([A-Za-z0-9_-]{1,64}) and only composed into the
server-derived conversationId — raw conversationIds are never accepted, so
the key+visitor namespace still bounds every thread.
- A `meta` SSE event echoes the effective sessionId/conversationId at stream
start so callers can persist and re-address a thread.
- Memory stays attributed per visitor (api:<visitorId>), shared across that
visitor's sessions.
All new fields are optional; omitting them reproduces the prior behaviour
byte-for-byte. Refs matevip/mateclaw#295.
- WikiPageTypeProfile: normalise pageType keys to lowercase on set, so a
user-authored profile with an uppercase key still matches the
case-insensitive hasPageType/get lookups.
- WikiDirectoryScanService: normalise the symlink-resolved glob base to
forward slashes so directory-scan globs work on Windows paths.
- Agents roster tag filter: keep selected tags that no longer exist on any
agent visible and deselectable (and show the filter bar when only such
orphan selections remain) instead of silently filtering with no way to clear.
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
Move the AGENTS.md section editing into a click-to-open modal so the Basic
tab no longer expands the full document inline. Present the entry as a
settings-style row (title + description on the left, a "manage" button on the
right) instead of stacked label/hint/button.
Add optional move-up/move-down controls to MemorySection (off by default, so
MemoryBrowser is unaffected) and wire reordering in AgentGuideEditor: adjacent
sections swap and the whole file is re-saved, with a synthetic preamble pinned
to the top.
Add a friendly AGENTS.md section editor inside the edit-agent modal's
"高级" (Advanced) collapsible, alongside the existing additional-instructions
field. New AgentGuideEditor reuses MemorySection cards, parses the file into
## sections, and saves whole-file via the workspace API (no backend change).
Empty files offer a "create" scaffold flow.
Reword the collapsible to a generic "高级" and add a role-clarifying note that
fixes the boundary: factory identity goes in the form fields, evolving
memory/rules go in the context files. Also fix the additional-instructions
textarea so it fills the form width.
Closesmatevip/mateclaw#290
Adds a tag filter bar to the agent roster, orthogonal to the existing
type/status tabs. Multi-select narrows by intersection (an agent must
carry every selected tag). Tags also render as clickable chips on each
agent card. The bar is hidden when no agent has tags.
Tags are de-duped per agent and ordered by usage frequency. When a
workspace has more than 12 distinct tags, only the top-N show inline and
a search box filters the rest; selected tags stay visible regardless.
Allow knowledge base source paths to be configured as a newline-separated
list of absolute paths or glob patterns rather than a single directory.
- Each non-blank, non-# line is treated as one path or glob pattern
- Plain paths (no wildcards) retain the existing recursive-scan behaviour
filtered by SUPPORTED_EXTENSIONS
- Glob patterns (e.g. /data/ocr/**/*.txt) walk from the fixed-prefix base
and apply Java's PathMatcher against each candidate's absolute path
- Patterns whose filename segment explicitly specifies an extension
(*.txt, *.{xlsx,csv}) skip the SUPPORTED_EXTENSIONS secondary filter,
respecting the user's explicit choice (key for OCR pipelines that
produce .txt output and should ignore the original PDF scans)
- Candidates collected across multiple patterns are deduplicated by
resolved absolute path so overlapping patterns don't double-count
- Symlink-escape check uses each pattern's own validated scan root
- parseSourcePatterns / extractBasePath moved into WikiSourcePathValidator
to eliminate a static circular reference between the two services
- Frontend watcher panel: single-line <input> replaced with <textarea>
supporting multiline editing; i18n updated with example patterns
- No DB schema change; fully backward-compatible with existing single-path
configs stored in sourceDirectory
Closes #(pending issue)