Closes#168
The native DashScope provider exposes both chat and embedding models, but
DASHSCOPE_NATIVE_ALLOW_PREFIXES only listed chat families
(qwen-/qwen2-/qwen3-/deepseek-/baichuan/yi-/llama). When a user manually
added text-embedding-v1/v2/v3/v4 to the dashscope provider,
assertModelIdAcceptable() rejected the id because no allow prefix matched.
Add 'text-embedding-' to the allow-list and broaden the doc comment from
"native chat protocol" to "native protocol (chat or embedding)" so the
intent is clear.
Discovery probing is chat-based and will still mark embedding entries
probeOk=false; surfacing them as discoverable embedding suggestions is a
separate follow-up.
Closes#167
EmbeddingModelFactory.buildOpenAi() hard-failed on any provider whose API
key was empty or unusable, so keyless providers like Ollama and OpenCode
(declared with requireApiKey=false) could pass the chat connectivity test
but bounce when the same provider's embedding model was tested.
Mirror the chat path in OpenAiCompatibleChatModelBuilder.buildOpenAiApi:
- If requireApiKey is not explicitly false, an unusable key still throws.
- If requireApiKey == false, the key check is skipped and an empty string
is passed to OpenAiApi.builder() so no Authorization: Bearer header is
attached to the outgoing request.
Closes#166
EmbeddingModelFactory used EmbeddingProtocol.fromProviderId() to pick the
embedding protocol, which substring-matches 'dashscope' / 'qwen' / 'aliyun'
in the providerId. The dashscope-compat provider carries 'dashscope' in its
id but runs in OpenAI compatible mode (chatModel='OpenAIChatModel',
baseUrl='https://dashscope.aliyuncs.com/compatible-mode/v1'). Routing it to
DASHSCOPE_EMBEDDING made DashScopeApi build its native path against the
compat base, producing 404s on every embedding call.
Switch to the chatModel column instead — the same signal ModelProtocol
.fromChatModel() uses for the chat path. chatModel='DashScopeChatModel'
takes the native protocol; everything else (including dashscope-compat)
takes OpenAI-compatible.
EmbeddingProtocol.fromProviderId() is retained for reference but is no
longer called; future callers should follow the chatModel pattern.
Closes#162
require_mention=true previously degraded to a no-op when botPrefix was unset:
shouldProcess() returned true for all messages and checkAccess() fell through
unconditionally, so any group message would be answered — including ones where
the @mention targeted another user.
FeishuChannelAdapter now consults the Feishu SDK's mentions field directly:
- WebSocket: read EventMessage.getMentions(); webhook: read mentions[] from the
JSON payload. In both paths each mention's id.open_id is compared against the
bot's own open_id.
- Bot open_id is fetched lazily via /open-apis/bot/v3/info and cached on the
adapter instance. If the call fails the message is allowed through, matching
the previous behaviour.
- The require_mention gate is applied at the top of handleFeishuMessage so 1:1
chats are unaffected.
Tests: 15 unit cases covering null/empty inputs, bot mentioned, only-other
mentioned, bot among multiple mentions, and malformed payloads.
Register a no-op handler for the bot-added-to-chat event on the Feishu WebSocket EventDispatcher. Without it, adding the bot to a group chat raises HandlerNotFoundException and drops the long connection. Mirrors the existing reaction-event handlers. Fixes#153.
The Feishu SDK EventDispatcher had no handler registered for im.message.reaction.created_v1 / deleted_v1, so adding or removing an emoji reaction raised HandlerNotFoundException and logged an ERROR stack trace. Register no-op handlers to silently ignore these events.
The PRIVATE_ITEMS list contained the bare 'test' entry, which rsync
interprets as 'any directory named test at any depth' — so it caught
the root-level /test/ scratch directory (intended) AND every src/test/
under each module (not intended).
Pattern is already anchored to /test (root-only). This commit rsyncs
the accumulated src/test/ tree forward so opensource has the unit tests
that have been written / updated against existing src/main/ code since
the pattern regression. Going forward each per-commit sync will carry
src/test/ files along with the main change.
OAuth token save now promotes the first available chat model when no usable default exists. Default-model resolution and provider availability checks require Provider.enabled=true alongside credentials, so disabled Providers no longer return stale defaults. /models/enabled drops the single-Provider hard-code so OpenAI OAuth and other enabled chat models surface in selectors. Docker exposes the 1455 PKCE callback via MATECLAW_OAUTH_OPENAI_CALLBACK_BIND_HOST (default 127.0.0.1) and a port mapping; deployment mode stays Host-driven.
Add a parallel effectiveAllowedToolsDisplay field on the runtime status payload so the SkillMarket detail drawer can render mcp_<server>_<slug>_<hash> with the raw tool name appended in parentheses, while leaving the original prefixed list unchanged for any caller that needs the machine name. McpSkillBridge#decorateToolNameForDisplay reverses a prefixed name via the per-server cached tool list; the frontend prefers the new display field and falls back to effectiveAllowedTools when the field is absent.
Group-chat reply slot fallback:
- The platform blocks proactive sends in group chats; outbound paths
(cron summaries, async-task completions, generated image/music/3D
delivery, TTS audio) silently failed because they fell through to
the proactive-send command. New bounded LRU maps each group chat
to its most recent inbound frame id; the dispatcher prefers that
reply slot and falls through to proactive only for single chats.
- Centralised text and media dispatch through a single helper so the
group rule never has to be re-implemented per outbound path.
Upload size pre-check + auto-downgrade:
- Without client-side limits, oversized uploads streamed for ~1 minute
before the server rejected at the finish step — users saw nothing
arrive in their chat. The new decision layer mirrors the platform's
hard limits and produces three outcomes: rejected with a friendly
reason, downgraded to a generic file delivery with an inline note,
or pass-through unchanged.
- Files over 20MB reject. Images / videos over their 10MB limit
downgrade to file. Voice content that isn't AMR or exceeds 2MB
downgrades. AMR voice within 2MB stays native.
appmsg inbound parsing:
- Forwarded complex messages (document transfers, article links,
miniprogram cards) used to fall into the inbound switch's default
branch and silently drop. The new branch flattens four sub-types
into a text marker the agent reads plus any media that needs to
reach downstream tools — document forwards reuse the same magic-
byte sniff and per-conversation upload layout as native file
inbound, so extension recovery and chat-uploads serving work
identically.
- Article links produce "[链接] title\ndescription\nurl" so the agent
can summarize without round-tripping. Miniprograms surface their
title. Unknown sub-types still emit a generic marker so the agent
is never blind.
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.