Commit Graph

191 Commits

Author SHA1 Message Date
matevip
8ead7b4254 feat(channels): card description fallback + tighter card layout 2026-05-02 07:39:33 +08:00
matevip
f0fbca803c feat(skill): wizard multi-file bundle + secret store + starter library 2026-05-01 22:39:23 +08:00
matevip
28c3b0e62f feat(model3d): Tencent Hunyuan 3D provider — Pro/Rapid action routing + <model-viewer> preview 2026-05-01 22:39:13 +08:00
matevip
e3ab06d57c feat(generative): unified async pipeline + live SSE delivery for music/video/image 2026-05-01 20:15:20 +08:00
matevip
0ffbfee4fb i18n(acp): translate ACP Endpoint(s) into Chinese across zh-CN 2026-05-01 20:15:07 +08:00
matevip
e5dff7acb1 fix(acp): detect keychain OAuth hijack on claude-code 'Request not allowed' 2026-05-01 20:15:01 +08:00
matevip
888200e1cd feat(acp): visual env editor with per-endpoint key suggestions 2026-05-01 20:14:49 +08:00
matevip
5e6764ecf1 fix(skill): tool-gate whitelist + markdown link host normalization
When an agent had any skill bound, the runtime tool gate was silently
hiding @Tool beans that aren't declared in any skill manifest, even
though the global system prompts (SOUL.md / "Web Search Capability" /
"File Reading Guidelines") explicitly tell the LLM these tools are
available. Result: the model would call search / renderDocx / read_file
/ etc., hit "Tool not found", then either give up or fall back to
unhelpful behaviour (e.g. dumping markdown text instead of producing a
.docx download).

This commit:

- Adds universally-promised, agent-wide tools to SYSTEM_LEVEL_TOOLS so
  they bypass the manifest restriction: document/media generation
  (renderDocx*, image_generate, music_generate, video_generate),
  global capability tools the system prompt mentions (search,
  browser_use, read_file / write_file / edit_file /
  execute_shell_command, detect_file_type, extract_*_text,
  readMateClawDoc), skill discovery siblings (listSkillFiles,
  listAvailableSkills), and the delegate triplet (delegateToAgent,
  delegateParallel, listAvailableAgents).

- Fixes 5 entries in the prior whitelist whose names did not match
  any real @Tool bean and were therefore silently dead:
    read_workspace_file   -> read_workspace_memory_file
    write_workspace_file  -> write_workspace_memory_file
    list_workspace_files  -> list_workspace_memory_files
    delegate_agent        -> delegateToAgent
    datetime              -> getCurrentDate / getCurrentDateTime / getCurrentTime
  Also adds the missing edit_workspace_memory_file.

- In the chat markdown renderer, strips any hallucinated
  https?://<host> prefix from /api/v1/files/generated/<id> download
  links before building the <a href>. Multiple LLMs have been
  observed prepending bogus hosts when echoing tool-returned download
  URLs back to the user, breaking the click. One-line defensive
  normalization independent of which model is in use.

Verified end-to-end on a previously-broken agent: search / browser_use
/ execute_shell_command / renderDocx all dispatch correctly now and
the final markdown link is a clean same-origin path. 36 whitelist
entries cross-checked against real @Tool method names.
AgentBindingServiceTest green.
2026-05-01 12:24:25 +08:00
matevip
f8ec223bcb refactor(ui): migrate remaining ElMessageBox.confirm calls to mcConfirm 2026-05-01 10:58:21 +08:00
matevip
8ce7e7d26d feat(ui): agent integration of SkillIcon + SkillIconPicker 2026-05-01 10:58:15 +08:00
matevip
efb91796fb feat(skill): pixelarticons icon library + picker component 2026-05-01 09:52:12 +08:00
matevip
9c6b728704 feat(ui): MateClaw skill detail drawer + shared McPagination / McConfirm components 2026-05-01 09:52:04 +08:00
matevip
9b9d0c8006 refactor(skill): trim Skills page — slim card + single status pill + drawer-owned Security 2026-05-01 09:51:52 +08:00
matevip
9ff15bc9a7 fix(activity): mobile adaptation + fix blank detail drawer 2026-05-01 09:51:39 +08:00
matevip
895b96a840 fix(activity): pagination blank — coerce total to Number + drop redundant hide-on-single-page 2026-05-01 09:51:33 +08:00
matevip
71e75d0e83 fix(activity): blank pagination items — drop overzealous pagination style overrides 2026-05-01 09:51:26 +08:00
matevip
191eebca95 fix(activity): align initial pageSize with page-sizes options (30→20) 2026-05-01 09:51:20 +08:00
matevip
a3f25d9b03 fix(activity): hide pagination when result fits in one page 2026-05-01 09:51:14 +08:00
matevip
7dd70ecd2f feat(activity): real pagination + frosted-glass styling 2026-05-01 09:51:07 +08:00
matevip
c46beaea9c fix(activity): style cleanup 2026-05-01 09:51:00 +08:00
matevip
b759eb8719 feat(skill): MCP-Skill bridge + system tool dedup + Lessons i18n 2026-05-01 09:50:45 +08:00
matevip
18c5c00797 feat(activity): redesign Activity page (taste pass) 2026-05-01 09:50:39 +08:00
matevip
f8524bf122 fix(skill): blank Tools / Features / Memory detail tabs 2026-05-01 09:50:32 +08:00
matevip
d76a5b994f feat(skill): Memory tab + auto-preflight after install 2026-05-01 09:50:25 +08:00
matevip
b8ce36f6cd feat(activity): three-source merged feed + detail drawer 2026-05-01 09:50:11 +08:00
matevip
170cb1f2f2 feat(skill): pre-flight install dialog + [Set Up] action 2026-05-01 09:50:04 +08:00
matevip
e74f273ae2 feat(skill): card surface — Source / Used-by / Lessons count 2026-05-01 09:49:51 +08:00
matevip
020a87ee7e feat(skill): ACP integration for external coding agents 2026-05-01 09:49:22 +08:00
matevip
f0991f543f feat(skill): skill template gallery + author wizard MVP 2026-05-01 09:49:14 +08:00
matevip
359600c77f feat(skill): per-skill LESSONS.md + self-evolution v1 2026-05-01 09:49:07 +08:00
matevip
d927521d51 feat(skill): install/uninstall split + Requirements API + provider router 2026-05-01 09:48:59 +08:00
matevip
91e231e7a5 feat(activity): promote Activity to top-level navigation 2026-05-01 09:48:53 +08:00
matevip
169a09506b feat(skill): detail drawer + Agent Tool Advanced fold 2026-05-01 09:48:46 +08:00
matevip
95144ef175 feat(skill): demote Tools to Settings + rename MCP Connections 2026-05-01 09:39:52 +08:00
matevip
46ac58500c fix(skill): correct pagination total and sort order on SkillMarket (issue #48) 2026-04-30 17:45:02 +08:00
matevip
977e181949 feat(cron): unify output, add reminder task type, in-flight progress UI
Three layers landed together because they share the same routing /
lifecycle plumbing:

1. Cron output unification
   - New CronConversationResolver routes web-origin jobs to the per-workspace
     tasks_<wsId> conversation; IM-bound jobs go to the channel session
     conversation when one exists (matched by senderId then targetId);
     legacy cron_<id> remains as the fallback.
   - CronJobLifecycleService inserts a system-role header divider when a
     run starts so users browsing the unified tasks_<wsId> view can tell
     which job started a run. BaseAgent.sanitizeForLlm filters these
     headers so they never reach the model.
   - WorkspaceService seeds tasks_<wsId> on workspace creation; V65
     migration backfills existing workspaces.
   - DeliveryConfig gains a userId field so IM session lookup can match
     by senderId (replyToken-based targetId is not stable across runs).
   - ConversationVO recognizes tasks_/cron_ underscore prefix as cron
     source. MessageList renders the system header as a labeled divider.
   - ChatConsole pins tasks_* conversations and tracks per-conversation
     read state so new cron output gets a visible unread dot.

2. Reminder task type
   - New task_type='reminder' in CronJobEntity + service validation.
   - CronJobRunner short-circuits 'reminder' jobs: hands trigger_message
     to finishRunAndPublish verbatim, no LLM call. Fixes a regression
     where reminders were rephrased into echoed wrappers.
   - New create_reminder tool alongside create_cron_job, with descriptions
     tightened so the model picks the right one (verbatim push vs LLM
     query that needs computation).
   - CronJobs.vue gets a third radio option + dedicated reminder field.

3. In-flight progress placeholder
   - Cron uses non-streaming chat()/execute(); tool-heavy ReAct loops
     can run 1-5 minutes between start and finish with no visible
     state, looking hung.
   - New GET /api/v1/cron-jobs/active-runs returns runs in status=running
     for a conversation. ChatConsole polls it on the existing 4s tick
     (and on conversation switch) and shows a spinner bar with elapsed
     time. When run count drops to zero, it refetches messages so the
     assistant bubble appears within ~1s of finish.
2026-04-30 15:01:24 +08:00
matevip
0c5c5bc2c2 fix(channels): hard-delete placeholder seeds; localize wizard verify+identity strings 2026-04-30 07:00:06 +08:00
matevip
3db4230142 feat(channels): redesign list page — show only configured channels, add hero empty state 2026-04-30 06:59:47 +08:00
matevip
a11f0586ba feat(channels): three-step onboarding wizard with live credential verify 2026-04-30 00:26:38 +08:00
matevip
2332792e8b chore: bump version to 1.2.0-SNAPSHOT 2026-04-29 23:20:56 +08:00
matevip
1f342acc95 release: v1.1.137 2026-04-29 16:44:08 +08:00
matevip
7cfb511948 fix(cron): rename conversation id prefix from 'cron:' to 'cron_' (issue #36) 2026-04-29 15:58:43 +08:00
matevip
8ad0fca4ef fix(memory): load structured/*.md by encoding path segments 2026-04-29 15:03:01 +08:00
matevip
822a77eef5 sync: tile catalog-row provider icons in dark mode (Settings → Models) 2026-04-29 11:36:55 +08:00
matevip
e759ad4a1b sync: settings UI polish, channel reliability fixes, DeepSeek cross-turn fix
- Settings → Models: inline API key, frosted drawer, dark-mode polish, provider icons, i18n sweep
- WeChat Work channel: rebuild HttpClient on reconnect, dedup failure signals, route auth_succeed errcode!=0 through failure handler
- Channel framework: per-adapter error isolation, QR auth SPI, health indicators
- Agent: patch cross-turn assistants for DeepSeek thinking-mode
- GitHub: bilingual issue templates with required fields
2026-04-29 11:22:47 +08:00
matevip
b4697f2806 fix(cron): post-deploy bug bundle — flakiness, scheduler, channel UI
User-reported field issues + a deeper code audit revealed multiple
overlapping bugs in the prior cron-channel delivery change. This fixes
all six.

#1 — Concurrency race on ToolExecutionExecutor (root cause of 'sometimes
   succeeds, sometimes fails' tool calls). The volatile instance fields
   currentRequesterId / currentWorkspaceBasePath / currentChatOrigin
   were shared by every conversation routed through the same per-agent
   executor; one user mid-build-loop while another's execute()
   overwrote the field would cross-contaminate the captured values into
   PreparedToolCall. Fix: kill the instance fields, thread
   origin/requester/workspace as method params straight into
   PreparedToolCall snapshot. Comment pins the rule so it cannot regress.

#2 — CHAT_ORIGIN missing from KeyStrategyFactory (latent timebomb,
   masked by spring-ai-alibaba-graph-core's non-filtering builder path).
   Without an addStrategy registration, multi-node state merges in long
   ReAct / Plan-Execute loops drop the key, ActionNode reads
   ChatOrigin.EMPTY, and the cron persists with channel_id=NULL. Also
   caught 4 more keys that were latently unregistered:
   WORKSPACE_BASE_PATH, STOP_REQUESTED, RETURN_DIRECT_TRIGGERED,
   DIRECT_TOOL_OUTPUTS. All five now registered in both ReAct and
   Plan-Execute factories.

#3 — CronJobs UI didn't surface channel binding. CronJobDTO carried
   channelId / deliveryConfig but the list page never rendered them.
   Added: (a) 'channel' column on list page, (b) channel + targetId
   rows in the detail modal, (c) backend batch-loads channel names via
   ChannelMapper.selectBatchIds so the column shows the human-readable
   name, (d) i18n keys (zh + en), (e) channelName field on TS CronJob
   type.

#4a — DingTalk targetId expiry. ChannelChatOriginFactory.resolveTargetId
   used to prefer ChannelMessage.replyToken which for DingTalk encodes
   a sessionWebhook URL that expires ~90 minutes after the inbound
   message. Cron persisted with that webhook then dies with 401/403 and
   marks NOT_DELIVERED forever. Fix: prefer the stable chatId, fall
   back to senderId — both work indefinitely via DingTalk's Robot API.

#4b — Scheduler pool exhaustion under long LLM. CronJobService's
   ThreadPoolTaskScheduler ran with poolSize=4 AND the LLM call lived
   on the scheduler thread. Four concurrent crons saturated the pool
   and the 5th silently missed its tick. Fix: keep scheduler tiny (it
   just fires triggers) and offload runAgent to a dedicated
   virtual-thread executor (cron-execute-* threads). LLM workload is
   I/O-bound — virtual threads scale to thousands at trivial cost.

#5 — Minor latent bugs:
   - AbstractCronResultDelivery.claimRun used .in(... 'NONE','PENDING',null),
     but SQL IN never matches NULL. Rewrote as IS NULL OR IN
     (NONE,PENDING) so legacy pre-V57 rows can still claim.
   - CronDeliveryListener.onCompletedRaw was an empty @EventListener
     with a wrong-headed comment about test fallbackExecution. Removed.
   - CronJobTool.resolveAgentId silently returned 1L when origin
     lacked an agentId — would silently bind to whatever agent #1
     happens to be. Replaced with explicit error so wiring bugs surface
     immediately instead of producing scheduled-but-never-runs crons.

State-key registration guard. New StateKeyRegistrationCoverageTest
scans MateClawStateKeys via reflection and parses
AgentGraphBuilder.java to extract every
.addStrategy(MateClawStateKeys.X, ...). Asserts every non-_NODE
constant appears in at least one factory. Caught the 4 unregistered
keys above on first run; will catch any future 'forgot to register'
regression.

Tests: 33 unit/arch tests + 27 regression in touched areas — all green.
Vue typecheck clean.

Refs: #25, #16
2026-04-28 21:45:07 +08:00
matevip
4011050ceb feat(cron): channel delivery via ChatOrigin + Spring AI ToolContext
Replaces the prior ThreadLocal context plumbing with explicit Spring AI
ToolContext threading carried by an immutable ChatOrigin value object,
so a cron created from inside WeChat (or any IM channel) delivers its
results back to the originating channel.

Architecture
- ChatOrigin / ChannelTarget value objects + per-entry-point factories
  (ChannelChatOriginFactory in vip.mate.channel, CronChatOriginFactory
  in vip.mate.cron — symmetric, no cyclic deps).
- LocaleAwareToolCallback now forwards call(String, ToolContext) and
  getToolMetadata so the decorator chain cannot silently drop the origin.
- AgentService 6-method overhaul + ChatOriginHolder bridge into
  StateGraph buildInitialState which writes CHAT_ORIGIN; ActionNode +
  StepExecutionNode forward it to ToolExecutionExecutor.
- ToolExecutionExecutor builds ToolContext per call; 8/8 tools migrated
  (CronJobTool, WorkspacePathGuard, Video/Image/Browser/ReadFile/Music,
  DelegateAgentTool with parent-origin inheritance).
- CronJobRunner + CronJobLifecycleService 3-segment REQUIRES_NEW model
  (T1 startRun / no-tx runAgent / T2 finishRunAndPublish); ArchUnit
  pins CronJobRunner as @Transactional-free.
- CronResultDelivery Strategy + AbstractCronResultDelivery Template
  with SQL CAS idempotency on mate_cron_job_run.delivery_status —
  replaces the prior process-local Caffeine TTL, cluster-safe.
- CronJobCompletedEvent + @Async @TransactionalEventListener(AFTER_COMMIT);
  cronDeliveryExecutor (core=2, max=4, queue=1000, AbortPolicy + audit).
- CronRunStaleCleanup @Scheduled(5min) sweeps PENDING-15min and
  status='running'-30min in one query each.
- CronJobRunner.wrapWithDeliveryGuard prepends a system note for
  channel-bound crons to suppress hallucinated 'install CLI to send
  WeChat' suggestions.
- ApprovalWorkflowService Memento: persist ChatOrigin snapshot on
  create, restore on replay so cross-restart approvals keep channel
  binding; ChannelMessageRouter + ChatController web-replay both prefer
  the Memento and fall back to fresh-build.
- ChannelManager.sendToChannel 4-arg DeliveryOptions overload;
  ChannelAdapter#proactiveSend default 4-arg pass-through; Slack
  overrides for thread_ts and Telegram overrides for message_thread_id.
- CronJobs UI: read-only 'last delivery' badge driven by
  CronJobMapper.selectListWithDeliveryStatus subquery.

Schema migrations V57/V58/V59 (V56 was already taken by an unrelated
provider migration — Flyway processes versions in order regardless of
gaps):
- V57: mate_cron_job_run delivery_status / target / error + composite
       index (delivery_status, started_at) covering the cleanup sweep.
- V58: mate_cron_job channel_id (indexed) + delivery_config TEXT (JSON
       via MyBatis Plus JacksonTypeHandler).
- V59: mate_tool_approval chat_origin TEXT (Memento).
All idempotent in both H2 (IF NOT EXISTS) and MySQL (INFORMATION_SCHEMA
guard + PREPARE).

ArchUnit guards (test scope, archunit-junit5 1.3.0):
- every concrete vip.mate.* ToolCallback must override
  call(String, ToolContext) — pins the decorator-forward fix.
- CronJobRunner must NOT carry @Transactional on the class or any
  method — pins the 3-segment lifecycle rule.

Tests: 32 new unit tests + 21 regression tests in touched areas, all
53 green:
- ChatOriginTest (6) — value-object invariants + JSON round-trip.
- LocaleAwareToolCallbackToolContextTest (2) — decorator forward.
- DeliveryConfigTest (4) — Jackson round-trip + forward-compat.
- ToolCallbackToolContextForwardArchTest (2) — both ArchUnit guards.
- CronJobRunnerDeliveryGuardTest (3) — channel-cron prefix injection.
- AbstractCronResultDeliveryTest (4) — claim CAS + concurrent CAS.
- ChannelCronResultDeliveryTest (6) — supports / doDeliver / errors.
- ApprovalReplayContinuityTest (5) — Memento round-trip + corrupt
  payload fallback + unknown-field tolerance.

Refs: #25, #16
2026-04-28 21:43:58 +08:00
matevip
6a3df2a6e0 feat(llm): enabled column for providers + Add Provider drawer
Adds explicit user-intent gating to the provider catalog. Fresh installs
get an empty dropdown by default — built-in cloud + local providers
(OpenAI, Anthropic, Ollama, LM Studio, MLX, llama.cpp, etc.) live in a
new 'Add Provider' drawer until the user opts them in. Existing installs
upgrade conservatively: V55 promotes any provider with evidence of use
(real api_key, OAuth token, recent chat usage, or current default model).

Backend
- V55 migration (H2 + MySQL): adds enabled BOOLEAN DEFAULT FALSE on
  mate_model_provider, plus 4 promote-to-true UPDATE rules. Also
  CREATE INDEX idx_message_runtime_provider_time so the 30-day usage
  lookup doesn't full-scan mate_message on heavy users.
- ModelProviderEntity, ProviderInfoDTO: enabled field.
- ModelProviderService:
    * listProviders() now filters WHERE enabled = TRUE — chat path,
      ModelSelector, Settings/Models main grid see only opted-in rows.
    * listCatalog() new — full catalog (enabled + disabled) for the drawer.
    * setEnabled(id, enabled) flips the flag, publishes
      ModelConfigChangedEvent (re-probe via the existing listener), and
      on disable auto-promotes a replacement default model when the
      disabled provider owned the current default. Returns EnableResult
      so the frontend can fire a toast.
    * createCustomProvider sets enabled=true (user just made the row).
- ProviderInitProbe.listConfiguredProviders also filters enabled=true —
  no point probing rows the user can't see.
- ModelConfigController: GET /catalog, POST /{id}/enable, POST /{id}/disable.
- Plugin-registered ChatModels are unaffected — they live in
  pluginChatModels (in-memory map), don't go through DB listProviders,
  so the enabled filter doesn't strand them.

Frontend
- New types: ProviderInfo.enabled, EnableResult.
- New API: catalog / enableProvider / disableProvider.
- New composable useProviderEnablement: catalog ref, drawerOpen,
  togglingId, loadCatalog, openDrawer / closeDrawer, enableProvider,
  disableProvider (fires defaultSwitchedToast on auto-switch).
- AddProviderDrawer.vue: lazy-loaded, reuses DoctorDrawer's Teleport +
  overlay + slide-in panel pattern. Two groups (cloud / local),
  unenabled rows surface to the top of each group, enabled rows show
  an 'Enabled' badge instead of a button. Mobile: full-screen sheet
  that slides up from below.
- ProviderCard: new 'Disable' button with soft-danger styling on
  enabled providers — soft-hide that keeps the config; user can
  re-enable from the drawer.
- Settings/Models index.vue:
    * Two top CTAs: 'Enable Provider' (drawer) and 'Custom' (existing
      custom-create modal) — distinct workflows, both surfaced.
    * Empty state with prominent 'Enable Provider' CTA when zero
      enabled providers — paired with onMounted auto-open of the
      drawer (sessionStorage guard so closing it doesn't bring it
      back on the next route visit in the same session).
    * Deep-link: ?addProvider=1 query forces the drawer open and
      strips itself after, so a back/forward doesn't re-fire the open.
- ModelSelector: when groups.length === 0 and not searching, show
  'No providers configured -> Configure' CTA linking to
  /settings/models?addProvider=1 — the natural flow when a fresh
  user opens chat before configuring anything.
- i18n: 13 new keys per locale (zh-CN + en-US) plus common.close.

Migration safety
- Conservative default policy: only rows with concrete evidence of
  use are auto-enabled; everything else stays hidden. Upgrade users
  may notice unused built-ins disappearing from their dropdown —
  that's the intended cleanup.
- mate_message index added so the 30-day usage rule doesn't full-scan
  on large installations; FlywayRepairConfig handles redeploy idempotency.

Tests
- ModelProviderServiceEnableTest covers all 7 enable/disable branches:
  flag flip + event publish, no-op on already-{enabled,disabled},
  default-switch when disabled provider owned current default,
  no-switch when default belongs elsewhere, no-replacement returns
  unchanged, getDefaultModel exception path, candidates with no
  models are skipped.
- ProviderInitProbeTest: helper provider() now sets enabled=true so
  the new probe filter doesn't strand existing fixtures.
- vip.mate.llm.** suite: 125 tests green. vue-tsc 0 errors. Browser
  page renders with both new buttons + drawer.
2026-04-28 15:03:40 +08:00
matevip
a168f91215 refactor(ui): split useProviders into 5 single-responsibility composables
Reshapes the Settings/Models frontend to match the channel-module split
convention (commit 22894ac4 'perf(channels): split Channels.vue...'),
zero behavior change. Paves the way for a follow-up that adds an enabled
column + AddProviderDrawer without bloating useProviders back to monolith.

Frontend split
- useProviders.ts goes from 615-line monolith to a 48-line facade that
  composes five single-responsibility slices:
    * useProviderList — providers / activeModels / currentProvider,
      loaders, status pill, icons
    * useProviderForm — create/edit modal + form, save/delete
    * useProviderDiscovery — manage-models modal, discovery, connection
      and per-model tests
    * useProviderOAuth — openai-chatgpt + claude-code OAuth flows
    * useProviderPool — manual reprobe (most pool surface inlined to
      ProviderInfo.liveness in the prior liveness change)
  Cross-composable refs flow via dep-injection arguments — no module-
  level state, no circular deps. Each composable stays independently
  testable.
- Pure helpers extracted to src/utils:
    * safeJson.ts — strict JSON-object parser
    * modelProtocol.ts — protocol <-> ChatModel class translation
- Modals (ProviderConfigModal, ManageModelsModal) loaded via
  defineAsyncComponent so the route's first paint doesn't drag along
  ~30KB of form/auth UI.
- el-skeleton placeholder during the initial Promise.all so the page
  paints something instead of blank-then-pop.

Layout regression fix
- MainLayout's <keep-alive> slot used :key='workspaceRouteKey' (a
  workspace-scoped string), shared between two <component v-if> blocks.
  Adding a second keepAlive route would have caused two components to
  mount side by side, because Vue saw identical keys and patched in
  place across the v-if boundary. Switched the key to
  ${workspaceRouteKey}:${route.path} so different routes get distinct
  vnode identities while workspace switching still busts the cache.
  Discovered while implementing the split — the multi-line HTML
  comment also had to live OUTSIDE <keep-alive>, since KeepAlive
  treats comments as children and rejects 'more than one'.

Embedding section title fix (drive-by)
- EmbeddingModelsSection.vue's scoped style didn't redeclare
  .group-title's flex layout, so the icon stacked above the title
  text instead of sitting inline. Added the missing flex rules
  locally — now matches the local-models / cloud-models group headers.

Verification
- vue-tsc 0 errors.
- Browser end-to-end: 27 cards render correctly, modals open via lazy
  load, /channels <-> /settings/models switch four times in a row with
  exactly one page title visible at each step (no stacking).
2026-04-28 15:01:14 +08:00
matevip
c0c642380a feat(llm): provider liveness model + honor requireApiKey on chat path
Phase 1 of the model-module refactor: combine pool / cooldown / probe-
completion signals into a single Liveness state surfaced through the
provider DTO, so the dropdown stops listing providers that are provably
unreachable. Zero schema change; one PR backend + frontend.

Backend
- Liveness enum with five mutually-exclusive states: LIVE, COOLDOWN,
  REMOVED, UNPROBED, UNCONFIGURED. Computed in ModelProviderService
  from AvailableProviderPool / ProviderHealthTracker / ProviderInitProbe
  snapshots batched once per listProviders() call.
- ProviderInitProbe.hasBeenProbed exposes a monotonic Set so the UI
  can distinguish 'still booting' from 'probed and removed' — without
  it the startup window flashes false REMOVED states.
- ProviderInfoDTO gains liveness + unavailableReason +
  cooldownRemainingMs + lastProbedAtMs. The legacy 'available' boolean
  stays but is now derived from liveness == LIVE so the chat fallback
  walker and the dropdown agree about what's usable.
- ProviderInitProbe injected into ModelProviderService via
  ObjectProvider to break the startup cycle (probe already depends on
  the service).

Frontend
- ProviderInfo type extended with liveness + the three detail fields.
- ModelSelector filters UNCONFIGURED + REMOVED out of the dropdown,
  shows COOLDOWN / UNPROBED with a status dot and dimmed rows that the
  user can still click to override.
- ProviderCard renders a five-state badge driven by liveness instead
  of the old configured + pool-entry combo. Reprobe button now keys
  off liveness in {REMOVED, COOLDOWN}.
- useProviders drops loadProviderPool / providerPool — pool data ships
  inline on each ProviderInfo, saves a round trip per page load and
  keeps a single source of truth.
- i18n: 8 new keys across zh-CN and en-US for liveness labels and the
  cooldown countdown tooltips.

Bonus fix (discovered during verification): AgentGraphBuilder.buildOpenAiApi
hard-required a usable API key on every OpenAI-compat provider, ignoring
the per-provider requireApiKey flag. That bug stranded keyless local
runtimes (LM Studio / MLX / llama.cpp) the moment a user actually
launched them; Ollama only worked by accident because its seed row
carries a placeholder string in api_key. keyRequired now honors
requireApiKey, and Spring AI's NoopApiKey is used when no key is needed
so the Authorization header is omitted entirely.

Test
- ModelProviderServiceLivenessTest covers all five Liveness states +
  the probe-bean-absent fallback branch.
- vip.mate.llm.** suite (118 tests) green; vue-tsc clean.
- End-to-end browser sanity: 27 raw providers reduce to 6 LIVE groups
  in the chat dropdown; LM Studio / MLX / llama.cpp render REMOVED red
  badges with reprobe buttons; cloud providers without keys show
  UNCONFIGURED.
2026-04-28 14:59:11 +08:00