mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-21 14:39:34 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 577c8e2869 | |||
| 694a25822f | |||
| 46818e9571 | |||
| b125565597 |
+236
@@ -2,6 +2,242 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
### [Version 2.2.0](https://github.com/lobehub/lobe-chat/compare/v2.1.59-canary.27...v2.2.0)
|
||||
|
||||
<sup>Released on **2026-05-18**</sup>
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **pricing**: restore DeepSeek models to official pricing.
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **conversation**: animate only the last markdown block + drop clearMessages hotkey.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### Styles
|
||||
|
||||
- **pricing**: restore DeepSeek models to official pricing, closes [#14911](https://github.com/lobehub/lobe-chat/issues/14911) ([e566688](https://github.com/lobehub/lobe-chat/commit/e566688))
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **conversation**: animate only the last markdown block + drop clearMessages hotkey, closes [#14906](https://github.com/lobehub/lobe-chat/issues/14906) ([469a8e6](https://github.com/lobehub/lobe-chat/commit/469a8e6))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 2.1.58](https://github.com/lobehub/lobe-chat/compare/v2.1.57...v2.1.58)
|
||||
|
||||
<sup>Released on **2026-05-13**</sup>
|
||||
|
||||
#### ✨ Features
|
||||
|
||||
- **agent-runtime**: persist agent operations to `agent_operations` table.
|
||||
- **misc**: support slack mpim and fix discord dm problem.
|
||||
- **database**: add `agent_operations` table.
|
||||
- **markdown**: user_feedback card + task card polish + Run now context menu.
|
||||
- **documents**: add optimistic create/delete and inline rename for document tree.
|
||||
- **devtools**: add dev-only feature flag override panel.
|
||||
- **misc**: add service model assignments settings.
|
||||
- **misc**: inline skill auth in recommended task templates.
|
||||
- **activator**: require activation reason.
|
||||
- **agent-signal,server,prompts**: consolidate in self-review implemented.
|
||||
- **hetero-agent**: support AskUserQuestion tools for claude code.
|
||||
- **bot**: gate device tools by sender identity.
|
||||
- **misc**: add user activity business hook.
|
||||
- **misc**: add Gemini 3.1 Flash-Lite provider cards.
|
||||
- **misc**: home daily brief with linkable welcome + paired input hint.
|
||||
- **agent-signal,prompts,database**: self-review now proposal actions to briefs, and automatically execute actions.
|
||||
- **misc**: add signOperationJwt with 4h expiry for hetero-agent operations.
|
||||
- **misc**: migrate Notion to LobeHub Market.
|
||||
- **misc**: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context.
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
|
||||
- **hetero-agent**: wire AskUserBridge response events to renderer.
|
||||
- **home**: blank user bubble when sending the placeholder hint.
|
||||
- **conversation**: prevent synthetic scroll from shrinking spacer.
|
||||
- **task-card**: localize task card date independent of dayjs global locale.
|
||||
- **web-crawler**: cap response body size to prevent serverless OOM.
|
||||
- **desktop**: focus onboarding auth success state.
|
||||
- **misc**: Docs image.
|
||||
- **desktop**: detect Windows npm .cmd shims for CLI agents (claude/codex/…).
|
||||
- **misc**: update Task page placeholder copy.
|
||||
- **builtin-tool-task**: expose `lobe-task` and add `setTaskSchedule`.
|
||||
- **desktop**: reset pendingLoginMethod on auth failure/cancel paths.
|
||||
- **utils**: cap image binary at 3.75MB so base64 payload stays under Anthropic 5MB limit.
|
||||
- **tasks**: scheduler, hotkey, comment & TodoList polish.
|
||||
- **cli**: remove stale cron entry from generated man page.
|
||||
- **misc**: sidebar add agent.
|
||||
- **misc**: replace ScrollShadow with ScrollArea to fix React #185 infinite render loop.
|
||||
- **heteroFinish**: trigger task lifecycle on cloud sandbox agent completion.
|
||||
- **hotkey**: remove redundant onClear to prevent double updateHotkey calls.
|
||||
- **misc**: reject inactive OIDC access.
|
||||
- **misc**: drop unreachable aihubmix empty-apiKey test.
|
||||
- **aihubmix**: use full models endpoint to return complete model list.
|
||||
- **onboarding**: skip marketplace on early exit, drop CJK in prompts.
|
||||
- **model-runtime**: enrich stream parse errors with provider/model context.
|
||||
- **home**: strip markdown links from daily-brief input placeholder.
|
||||
- **misc**: consume visual content parts in server runtime.
|
||||
- **misc**: store onboarding interests as keys.
|
||||
- **hetero-agent**: sync new-step assistant across replicas.
|
||||
- **misc**: remove the old cron job from lobehub.
|
||||
- **misc**: refresh content baseline from DB on every ingest call.
|
||||
- **hetero-agent**: disable Claude Code AskUserQuestion to avoid auto-decline.
|
||||
- **local-system**: guard readFile against binary blobs and oversized output.
|
||||
- **database,utils,userMemories**: should perfer to use `paradedb.match(...)` instead of hardcoded normalizer.
|
||||
- **database**: attach error listeners to Neon/Node pools to prevent Lambda crash.
|
||||
- **misc**: gateway client-tool pluginState + drop redundant `Exit code: 0` tail.
|
||||
- **gemini**: handle zero cachedContentTokenCount in usage conversion.
|
||||
- **misc**: first inject the cloudecc runtime session should use the existingStatus.
|
||||
- **misc**: slack connect error & slash commands.
|
||||
- **misc**: polish task agent manager.
|
||||
- **agent-runtime**: recover malformed tool_call names instead of finishing silently.
|
||||
- **misc**: remove signin captcha flow.
|
||||
- **misc**: add temporary email auth error locale.
|
||||
- **misc**: add bot callback service.
|
||||
- **misc**: sanitize sensitive comments and examples from production JS bundle.
|
||||
- **misc**: multiple account link.
|
||||
|
||||
#### 💄 Styles
|
||||
|
||||
- **misc**: use @lobehub/ui built-in HtmlPreview instead of custom component.
|
||||
- **misc**: polish desktop header icons, sidebar density, and task menus.
|
||||
- **review-panel**: hover revert button to discard per-file working-tree changes.
|
||||
- **misc**: standardize header action icon sizes.
|
||||
- **tool**: add word wrap toggle to tool arguments display.
|
||||
- **nav**: unify ActionIcon sizing and improve TodoList encapsulation.
|
||||
- **web-onboarding**: add Render for saveUserQuestion & showAgentMarketplace.
|
||||
- **misc**: add `reasoning_effort` support for Grok 4.3.
|
||||
- **misc**: increase chat topic title length.
|
||||
- **hetero-agent**: read-only SubAgent threads with breadcrumb header and thread switcher.
|
||||
- **chat-input**: show skeleton in action bar while config is loading.
|
||||
- **home**: add Recommendations module with hetero agent action library.
|
||||
- **copyable-label**: wrap long tool-call params instead of truncating.
|
||||
- **misc**: format tool execution time as Xmin Ys instead of X.Y min.
|
||||
- **misc**: Add new DeepSeek-V4 models.
|
||||
- **topic**: add copy session ID to topic dropdown menu.
|
||||
- **misc**: use visible divider between queued messages.
|
||||
- **intervention**: polish confirmation bar layout.
|
||||
- **settings**: remove image avatar from lab input markdown rendering item.
|
||||
- **task**: activity card stop run + register /tasks in SPA proxy.
|
||||
- **misc**: update auth captcha retry copy.
|
||||
|
||||
<br/>
|
||||
|
||||
<details>
|
||||
<summary><kbd>Improvements and Fixes</kbd></summary>
|
||||
|
||||
#### What's improved
|
||||
|
||||
- **agent-runtime**: persist agent operations to `agent_operations` table, closes [#14736](https://github.com/lobehub/lobe-chat/issues/14736) ([a772341](https://github.com/lobehub/lobe-chat/commit/a772341))
|
||||
- **misc**: support slack mpim and fix discord dm problem, closes [#14733](https://github.com/lobehub/lobe-chat/issues/14733) ([729265a](https://github.com/lobehub/lobe-chat/commit/729265a))
|
||||
- **database**: add `agent_operations` table, closes [#14416](https://github.com/lobehub/lobe-chat/issues/14416) ([cb8b616](https://github.com/lobehub/lobe-chat/commit/cb8b616))
|
||||
- **markdown**: user_feedback card + task card polish + Run now context menu, closes [#14727](https://github.com/lobehub/lobe-chat/issues/14727) ([79152fa](https://github.com/lobehub/lobe-chat/commit/79152fa))
|
||||
- **documents**: add optimistic create/delete and inline rename for document tree, closes [#14714](https://github.com/lobehub/lobe-chat/issues/14714) ([0007984](https://github.com/lobehub/lobe-chat/commit/0007984))
|
||||
- **devtools**: add dev-only feature flag override panel, closes [#14565](https://github.com/lobehub/lobe-chat/issues/14565) ([18b1c25](https://github.com/lobehub/lobe-chat/commit/18b1c25))
|
||||
- **misc**: add service model assignments settings, closes [#14712](https://github.com/lobehub/lobe-chat/issues/14712) ([eb924ec](https://github.com/lobehub/lobe-chat/commit/eb924ec))
|
||||
- **misc**: inline skill auth in recommended task templates, closes [#14676](https://github.com/lobehub/lobe-chat/issues/14676) ([4490e3e](https://github.com/lobehub/lobe-chat/commit/4490e3e))
|
||||
- **activator**: require activation reason, closes [#14597](https://github.com/lobehub/lobe-chat/issues/14597) ([5f14b7e](https://github.com/lobehub/lobe-chat/commit/5f14b7e))
|
||||
- **agent-signal,server,prompts**: consolidate in self-review implemented, closes [#14657](https://github.com/lobehub/lobe-chat/issues/14657) ([1374fd2](https://github.com/lobehub/lobe-chat/commit/1374fd2))
|
||||
- **hetero-agent**: support AskUserQuestion tools for claude code, closes [#14639](https://github.com/lobehub/lobe-chat/issues/14639) ([49c3d7e](https://github.com/lobehub/lobe-chat/commit/49c3d7e))
|
||||
- **bot**: gate device tools by sender identity, closes [#14634](https://github.com/lobehub/lobe-chat/issues/14634) ([3c81011](https://github.com/lobehub/lobe-chat/commit/3c81011))
|
||||
- **misc**: add user activity business hook, closes [#14601](https://github.com/lobehub/lobe-chat/issues/14601) ([521566b](https://github.com/lobehub/lobe-chat/commit/521566b))
|
||||
- **misc**: add Gemini 3.1 Flash-Lite provider cards, closes [#14604](https://github.com/lobehub/lobe-chat/issues/14604) ([9b032f0](https://github.com/lobehub/lobe-chat/commit/9b032f0))
|
||||
- **misc**: home daily brief with linkable welcome + paired input hint, closes [#14589](https://github.com/lobehub/lobe-chat/issues/14589) ([12e37f1](https://github.com/lobehub/lobe-chat/commit/12e37f1))
|
||||
- **agent-signal,prompts,database**: self-review now proposal actions to briefs, and automatically execute actions, closes [#14583](https://github.com/lobehub/lobe-chat/issues/14583) ([b7a5020](https://github.com/lobehub/lobe-chat/commit/b7a5020))
|
||||
- **misc**: add signOperationJwt with 4h expiry for hetero-agent operations, closes [#14586](https://github.com/lobehub/lobe-chat/issues/14586) ([d2c379c](https://github.com/lobehub/lobe-chat/commit/d2c379c))
|
||||
- **misc**: migrate Notion to LobeHub Market, closes [#14578](https://github.com/lobehub/lobe-chat/issues/14578) ([f1f2e58](https://github.com/lobehub/lobe-chat/commit/f1f2e58))
|
||||
- **misc**: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context, closes [#14568](https://github.com/lobehub/lobe-chat/issues/14568) ([7792f63](https://github.com/lobehub/lobe-chat/commit/7792f63))
|
||||
|
||||
#### What's fixed
|
||||
|
||||
- **hetero-agent**: wire AskUserBridge response events to renderer, closes [#14732](https://github.com/lobehub/lobe-chat/issues/14732) ([5174c13](https://github.com/lobehub/lobe-chat/commit/5174c13))
|
||||
- **home**: blank user bubble when sending the placeholder hint, closes [#14678](https://github.com/lobehub/lobe-chat/issues/14678) ([fc275ca](https://github.com/lobehub/lobe-chat/commit/fc275ca))
|
||||
- **conversation**: prevent synthetic scroll from shrinking spacer, closes [#14584](https://github.com/lobehub/lobe-chat/issues/14584) ([217afcf](https://github.com/lobehub/lobe-chat/commit/217afcf))
|
||||
- **task-card**: localize task card date independent of dayjs global locale, closes [#14730](https://github.com/lobehub/lobe-chat/issues/14730) ([df0e635](https://github.com/lobehub/lobe-chat/commit/df0e635))
|
||||
- **web-crawler**: cap response body size to prevent serverless OOM, closes [#14660](https://github.com/lobehub/lobe-chat/issues/14660) ([2202189](https://github.com/lobehub/lobe-chat/commit/2202189))
|
||||
- **desktop**: focus onboarding auth success state, closes [#14694](https://github.com/lobehub/lobe-chat/issues/14694) ([4e4294f](https://github.com/lobehub/lobe-chat/commit/4e4294f))
|
||||
- **misc**: Docs image, closes [#14726](https://github.com/lobehub/lobe-chat/issues/14726) ([3a4bd4a](https://github.com/lobehub/lobe-chat/commit/3a4bd4a))
|
||||
- **desktop**: detect Windows npm .cmd shims for CLI agents (claude/codex/…), closes [#14720](https://github.com/lobehub/lobe-chat/issues/14720) ([a40fe91](https://github.com/lobehub/lobe-chat/commit/a40fe91))
|
||||
- **misc**: update Task page placeholder copy, closes [#14704](https://github.com/lobehub/lobe-chat/issues/14704) ([eea742f](https://github.com/lobehub/lobe-chat/commit/eea742f))
|
||||
- **builtin-tool-task**: expose `lobe-task` and add `setTaskSchedule`, closes [#14713](https://github.com/lobehub/lobe-chat/issues/14713) ([5ff4590](https://github.com/lobehub/lobe-chat/commit/5ff4590))
|
||||
- **desktop**: reset pendingLoginMethod on auth failure/cancel paths, closes [#14695](https://github.com/lobehub/lobe-chat/issues/14695) ([51cefe0](https://github.com/lobehub/lobe-chat/commit/51cefe0))
|
||||
- **utils**: cap image binary at 3.75MB so base64 payload stays under Anthropic 5MB limit, closes [#14711](https://github.com/lobehub/lobe-chat/issues/14711) ([948e48b](https://github.com/lobehub/lobe-chat/commit/948e48b))
|
||||
- **tasks**: scheduler, hotkey, comment & TodoList polish, closes [#14707](https://github.com/lobehub/lobe-chat/issues/14707) ([1ae774d](https://github.com/lobehub/lobe-chat/commit/1ae774d))
|
||||
- **cli**: remove stale cron entry from generated man page, closes [#14709](https://github.com/lobehub/lobe-chat/issues/14709) ([94e4ea6](https://github.com/lobehub/lobe-chat/commit/94e4ea6))
|
||||
- **misc**: sidebar add agent, closes [#14693](https://github.com/lobehub/lobe-chat/issues/14693) ([fdedc96](https://github.com/lobehub/lobe-chat/commit/fdedc96))
|
||||
- **misc**: replace ScrollShadow with ScrollArea to fix React #185 infinite render loop, closes [#185](https://github.com/lobehub/lobe-chat/issues/185), closes [#14689](https://github.com/lobehub/lobe-chat/issues/14689) ([7349ad0](https://github.com/lobehub/lobe-chat/commit/7349ad0))
|
||||
- **heteroFinish**: trigger task lifecycle on cloud sandbox agent completion, closes [#14681](https://github.com/lobehub/lobe-chat/issues/14681) ([744059c](https://github.com/lobehub/lobe-chat/commit/744059c))
|
||||
- **hotkey**: remove redundant onClear to prevent double updateHotkey calls, closes [#14663](https://github.com/lobehub/lobe-chat/issues/14663) ([dfe1932](https://github.com/lobehub/lobe-chat/commit/dfe1932))
|
||||
- **misc**: reject inactive OIDC access, closes [#14674](https://github.com/lobehub/lobe-chat/issues/14674) ([b79c5d8](https://github.com/lobehub/lobe-chat/commit/b79c5d8))
|
||||
- **misc**: drop unreachable aihubmix empty-apiKey test, closes [#14669](https://github.com/lobehub/lobe-chat/issues/14669) ([b0ee35d](https://github.com/lobehub/lobe-chat/commit/b0ee35d))
|
||||
- **aihubmix**: use full models endpoint to return complete model list, closes [#14511](https://github.com/lobehub/lobe-chat/issues/14511) ([f4de472](https://github.com/lobehub/lobe-chat/commit/f4de472))
|
||||
- **onboarding**: skip marketplace on early exit, drop CJK in prompts, closes [#14598](https://github.com/lobehub/lobe-chat/issues/14598) ([a9eb904](https://github.com/lobehub/lobe-chat/commit/a9eb904))
|
||||
- **model-runtime**: enrich stream parse errors with provider/model context, closes [#14636](https://github.com/lobehub/lobe-chat/issues/14636) ([7daed90](https://github.com/lobehub/lobe-chat/commit/7daed90))
|
||||
- **home**: strip markdown links from daily-brief input placeholder, closes [#14635](https://github.com/lobehub/lobe-chat/issues/14635) ([0babdcf](https://github.com/lobehub/lobe-chat/commit/0babdcf))
|
||||
- **misc**: consume visual content parts in server runtime, closes [#14637](https://github.com/lobehub/lobe-chat/issues/14637) ([d445a89](https://github.com/lobehub/lobe-chat/commit/d445a89))
|
||||
- **misc**: store onboarding interests as keys, closes [#14624](https://github.com/lobehub/lobe-chat/issues/14624) ([9982de3](https://github.com/lobehub/lobe-chat/commit/9982de3))
|
||||
- **hetero-agent**: sync new-step assistant across replicas, closes [#14631](https://github.com/lobehub/lobe-chat/issues/14631) ([7675bd9](https://github.com/lobehub/lobe-chat/commit/7675bd9))
|
||||
- **misc**: remove the old cron job from lobehub, closes [#14630](https://github.com/lobehub/lobe-chat/issues/14630) ([457d112](https://github.com/lobehub/lobe-chat/commit/457d112))
|
||||
- **misc**: refresh content baseline from DB on every ingest call, closes [#14603](https://github.com/lobehub/lobe-chat/issues/14603) ([6595961](https://github.com/lobehub/lobe-chat/commit/6595961))
|
||||
- **hetero-agent**: disable Claude Code AskUserQuestion to avoid auto-decline, closes [#14629](https://github.com/lobehub/lobe-chat/issues/14629) ([ae8f9cf](https://github.com/lobehub/lobe-chat/commit/ae8f9cf))
|
||||
- **local-system**: guard readFile against binary blobs and oversized output, closes [#14602](https://github.com/lobehub/lobe-chat/issues/14602) ([96165e4](https://github.com/lobehub/lobe-chat/commit/96165e4))
|
||||
- **database,utils,userMemories**: should perfer to use `paradedb.match(...)` instead of hardcoded normalizer, closes [#14590](https://github.com/lobehub/lobe-chat/issues/14590) ([38b793f](https://github.com/lobehub/lobe-chat/commit/38b793f))
|
||||
- **database**: attach error listeners to Neon/Node pools to prevent Lambda crash, closes [#14606](https://github.com/lobehub/lobe-chat/issues/14606) ([11ec59b](https://github.com/lobehub/lobe-chat/commit/11ec59b))
|
||||
- **misc**: gateway client-tool pluginState + drop redundant `Exit code: 0` tail, closes [#14596](https://github.com/lobehub/lobe-chat/issues/14596) ([4bfd434](https://github.com/lobehub/lobe-chat/commit/4bfd434))
|
||||
- **gemini**: handle zero cachedContentTokenCount in usage conversion, closes [#14567](https://github.com/lobehub/lobe-chat/issues/14567) ([307cd8e](https://github.com/lobehub/lobe-chat/commit/307cd8e))
|
||||
- **misc**: first inject the cloudecc runtime session should use the existingStatus, closes [#14592](https://github.com/lobehub/lobe-chat/issues/14592) ([09c66ff](https://github.com/lobehub/lobe-chat/commit/09c66ff))
|
||||
- **misc**: slack connect error & slash commands, closes [#14591](https://github.com/lobehub/lobe-chat/issues/14591) ([8274be0](https://github.com/lobehub/lobe-chat/commit/8274be0))
|
||||
- **misc**: polish task agent manager, closes [#14569](https://github.com/lobehub/lobe-chat/issues/14569) ([a02ecbc](https://github.com/lobehub/lobe-chat/commit/a02ecbc))
|
||||
- **agent-runtime**: recover malformed tool_call names instead of finishing silently, closes [#14577](https://github.com/lobehub/lobe-chat/issues/14577) ([5f8ec8b](https://github.com/lobehub/lobe-chat/commit/5f8ec8b))
|
||||
- **misc**: remove signin captcha flow, closes [#14573](https://github.com/lobehub/lobe-chat/issues/14573) ([181b7eb](https://github.com/lobehub/lobe-chat/commit/181b7eb))
|
||||
- **misc**: add temporary email auth error locale, closes [#14564](https://github.com/lobehub/lobe-chat/issues/14564) ([2bdd901](https://github.com/lobehub/lobe-chat/commit/2bdd901))
|
||||
- **misc**: add bot callback service, closes [#14570](https://github.com/lobehub/lobe-chat/issues/14570) ([e4b5e52](https://github.com/lobehub/lobe-chat/commit/e4b5e52))
|
||||
- **misc**: sanitize sensitive comments and examples from production JS bundle, closes [#14557](https://github.com/lobehub/lobe-chat/issues/14557) ([1a6e07b](https://github.com/lobehub/lobe-chat/commit/1a6e07b))
|
||||
- **misc**: multiple account link, closes [#14562](https://github.com/lobehub/lobe-chat/issues/14562) ([760a342](https://github.com/lobehub/lobe-chat/commit/760a342))
|
||||
|
||||
#### Styles
|
||||
|
||||
- **misc**: use @lobehub/ui built-in HtmlPreview instead of custom component, closes [#14703](https://github.com/lobehub/lobe-chat/issues/14703) ([266d102](https://github.com/lobehub/lobe-chat/commit/266d102))
|
||||
- **misc**: polish desktop header icons, sidebar density, and task menus, closes [#14724](https://github.com/lobehub/lobe-chat/issues/14724) ([e56edab](https://github.com/lobehub/lobe-chat/commit/e56edab))
|
||||
- **review-panel**: hover revert button to discard per-file working-tree changes, closes [#14716](https://github.com/lobehub/lobe-chat/issues/14716) ([846e648](https://github.com/lobehub/lobe-chat/commit/846e648))
|
||||
- **misc**: standardize header action icon sizes, closes [#14717](https://github.com/lobehub/lobe-chat/issues/14717) ([ca9a781](https://github.com/lobehub/lobe-chat/commit/ca9a781))
|
||||
- **tool**: add word wrap toggle to tool arguments display, closes [#14706](https://github.com/lobehub/lobe-chat/issues/14706) ([bfa2850](https://github.com/lobehub/lobe-chat/commit/bfa2850))
|
||||
- **nav**: unify ActionIcon sizing and improve TodoList encapsulation, closes [#14692](https://github.com/lobehub/lobe-chat/issues/14692) ([877052f](https://github.com/lobehub/lobe-chat/commit/877052f))
|
||||
- **web-onboarding**: add Render for saveUserQuestion & showAgentMarketplace, closes [#14667](https://github.com/lobehub/lobe-chat/issues/14667) ([f591f7a](https://github.com/lobehub/lobe-chat/commit/f591f7a))
|
||||
- **misc**: add `reasoning_effort` support for Grok 4.3, closes [#14642](https://github.com/lobehub/lobe-chat/issues/14642) ([a1fac45](https://github.com/lobehub/lobe-chat/commit/a1fac45))
|
||||
- **misc**: increase chat topic title length, closes [#14659](https://github.com/lobehub/lobe-chat/issues/14659) ([e0ead0c](https://github.com/lobehub/lobe-chat/commit/e0ead0c))
|
||||
- **hetero-agent**: read-only SubAgent threads with breadcrumb header and thread switcher, closes [#14658](https://github.com/lobehub/lobe-chat/issues/14658) ([31e9130](https://github.com/lobehub/lobe-chat/commit/31e9130))
|
||||
- **chat-input**: show skeleton in action bar while config is loading, closes [#14656](https://github.com/lobehub/lobe-chat/issues/14656) ([84b802c](https://github.com/lobehub/lobe-chat/commit/84b802c))
|
||||
- **home**: add Recommendations module with hetero agent action library, closes [#14645](https://github.com/lobehub/lobe-chat/issues/14645) ([e261a6f](https://github.com/lobehub/lobe-chat/commit/e261a6f))
|
||||
- **copyable-label**: wrap long tool-call params instead of truncating, closes [#14640](https://github.com/lobehub/lobe-chat/issues/14640) ([60a127b](https://github.com/lobehub/lobe-chat/commit/60a127b))
|
||||
- **misc**: format tool execution time as Xmin Ys instead of X.Y min, closes [#14641](https://github.com/lobehub/lobe-chat/issues/14641) ([b85a1ad](https://github.com/lobehub/lobe-chat/commit/b85a1ad))
|
||||
- **misc**: Add new DeepSeek-V4 models, closes [#14110](https://github.com/lobehub/lobe-chat/issues/14110) ([867e22a](https://github.com/lobehub/lobe-chat/commit/867e22a))
|
||||
- **topic**: add copy session ID to topic dropdown menu, closes [#14595](https://github.com/lobehub/lobe-chat/issues/14595) ([a275009](https://github.com/lobehub/lobe-chat/commit/a275009))
|
||||
- **misc**: use visible divider between queued messages, closes [#14593](https://github.com/lobehub/lobe-chat/issues/14593) ([909b1ec](https://github.com/lobehub/lobe-chat/commit/909b1ec))
|
||||
- **intervention**: polish confirmation bar layout, closes [#14587](https://github.com/lobehub/lobe-chat/issues/14587) ([5c11130](https://github.com/lobehub/lobe-chat/commit/5c11130))
|
||||
- **settings**: remove image avatar from lab input markdown rendering item, closes [#14582](https://github.com/lobehub/lobe-chat/issues/14582) ([d73de25](https://github.com/lobehub/lobe-chat/commit/d73de25))
|
||||
- **task**: activity card stop run + register /tasks in SPA proxy, closes [#14559](https://github.com/lobehub/lobe-chat/issues/14559) ([a7cc553](https://github.com/lobehub/lobe-chat/commit/a7cc553))
|
||||
- **misc**: update auth captcha retry copy, closes [#14561](https://github.com/lobehub/lobe-chat/issues/14561) ([c208723](https://github.com/lobehub/lobe-chat/commit/c208723))
|
||||
|
||||
</details>
|
||||
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## [Version 2.1.57](https://github.com/lobehub/lobe-chat/compare/v2.1.57-canary.33...v2.1.57)
|
||||
|
||||
<sup>Released on **2026-05-09**</sup>
|
||||
|
||||
@@ -1,4 +1,58 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-05-18",
|
||||
"version": "2.2.0"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"features": [
|
||||
"support slack mpim and fix discord dm problem.",
|
||||
"add service model assignments settings.",
|
||||
"inline skill auth in recommended task templates.",
|
||||
"add user activity business hook.",
|
||||
"add Gemini 3.1 Flash-Lite provider cards.",
|
||||
"home daily brief with linkable welcome + paired input hint.",
|
||||
"add signOperationJwt with 4h expiry for hetero-agent operations.",
|
||||
"migrate Notion to LobeHub Market.",
|
||||
"Cloud Claude Code V3 — repo picker, GitHub token, sandbox context."
|
||||
],
|
||||
"fixes": [
|
||||
"Docs image.",
|
||||
"update Task page placeholder copy.",
|
||||
"sidebar add agent.",
|
||||
"replace ScrollShadow with ScrollArea to fix React #185 infinite render loop.",
|
||||
"reject inactive OIDC access.",
|
||||
"drop unreachable aihubmix empty-apiKey test.",
|
||||
"consume visual content parts in server runtime.",
|
||||
"store onboarding interests as keys.",
|
||||
"remove the old cron job from lobehub.",
|
||||
"refresh content baseline from DB on every ingest call.",
|
||||
"gateway client-tool pluginState + drop redundant Exit code: 0 tail.",
|
||||
"first inject the cloudecc runtime session should use the existingStatus.",
|
||||
"slack connect error & slash commands.",
|
||||
"polish task agent manager.",
|
||||
"remove signin captcha flow.",
|
||||
"add temporary email auth error locale.",
|
||||
"add bot callback service.",
|
||||
"sanitize sensitive comments and examples from production JS bundle.",
|
||||
"multiple account link."
|
||||
],
|
||||
"improvements": [
|
||||
"use @lobehub/ui built-in HtmlPreview instead of custom component.",
|
||||
"polish desktop header icons, sidebar density, and task menus.",
|
||||
"standardize header action icon sizes.",
|
||||
"add reasoning_effort support for Grok 4.3.",
|
||||
"increase chat topic title length.",
|
||||
"format tool execution time as Xmin Ys instead of X.Y min.",
|
||||
"Add new DeepSeek-V4 models.",
|
||||
"use visible divider between queued messages.",
|
||||
"update auth captcha retry copy."
|
||||
]
|
||||
},
|
||||
"date": "2026-05-13",
|
||||
"version": "2.1.58"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["hide runtime-only model aliases."],
|
||||
|
||||
@@ -1212,6 +1212,22 @@ table oidc_sessions {
|
||||
}
|
||||
}
|
||||
|
||||
table page_shares {
|
||||
id text [pk, not null]
|
||||
document_id text [not null]
|
||||
user_id text [not null]
|
||||
visibility text [not null, default: 'private']
|
||||
page_view_count integer [not null, default: 0]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
updated_at "timestamp with time zone" [not null, default: `now()`]
|
||||
|
||||
indexes {
|
||||
document_id [name: 'page_shares_document_id_unique', unique]
|
||||
user_id [name: 'page_shares_user_id_idx']
|
||||
}
|
||||
}
|
||||
|
||||
table chunks {
|
||||
id uuid [pk, not null, default: `gen_random_uuid()`]
|
||||
text text
|
||||
@@ -2167,6 +2183,10 @@ ref: threads.source_message_id - messages.id
|
||||
|
||||
ref: messages.message_group_id > message_groups.id
|
||||
|
||||
ref: page_shares.document_id > documents.id
|
||||
|
||||
ref: page_shares.user_id - users.id
|
||||
|
||||
ref: sessions.group_id - session_groups.id
|
||||
|
||||
ref: topic_documents.document_id > documents.id
|
||||
|
||||
@@ -284,15 +284,6 @@
|
||||
"header.groupDesc": "管理群组与对话偏好",
|
||||
"header.session": "会话设置",
|
||||
"header.sessionDesc": "助理档案与会话偏好",
|
||||
"googleDataProtection.cannotConnectGoogle.content": "您正在使用非 Google 模型。连接 Google 工具(Gmail、Drive、Docs)可能会将您的数据暴露给第三方服务商。请先切换到 Google 模型后再启用这些工具。",
|
||||
"googleDataProtection.cannotConnectGoogle.title": "无法连接 Google 工具",
|
||||
"googleDataProtection.cannotSendMessage.content": "您已启用 Google 工具({{tools}}),但正在使用非 Google 模型。发送消息可能会将您的 Google 数据暴露给第三方服务商。请禁用 Google 工具或切换到 Google 模型。",
|
||||
"googleDataProtection.cannotSendMessage.title": "无法发送消息",
|
||||
"googleDataProtection.cannotSendWithHistory.content": "您的对话历史中包含 Google 工具的使用记录({{tools}}),但您当前正在使用非 Google 模型。发送消息可能会将您的 Google 数据暴露给第三方服务商。请切换到 Google 模型。",
|
||||
"googleDataProtection.cannotSendWithHistory.title": "无法发送消息",
|
||||
"googleDataProtection.cannotSwitchModel.content": "您已启用 Google 工具({{tools}})。切换到非 Google 模型可能会将您的 Google 数据暴露给第三方服务商。请先禁用 Google 工具后再切换模型。",
|
||||
"googleDataProtection.cannotSwitchModel.title": "无法切换模型",
|
||||
"googleDataProtection.understood": "我已了解",
|
||||
"header.sessionWithName": "会话设置 · {{name}}",
|
||||
"header.title": "设置",
|
||||
"heterogeneousStatus.account.label": "账号",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/lobehub",
|
||||
"version": "2.1.57",
|
||||
"version": "2.2.0",
|
||||
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
||||
"keywords": [
|
||||
"framework",
|
||||
|
||||
@@ -33,9 +33,9 @@ export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
|
||||
description: 'Gmail is a free email service provided by Google',
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/gmail.svg',
|
||||
identifier: 'gmail',
|
||||
label: 'Gmail',
|
||||
readme:
|
||||
'Bring the power of Gmail directly into your AI assistant. Read, compose, and send emails, search your inbox, manage labels, and organize your communications—all through natural conversation.',
|
||||
label: 'Gmail',
|
||||
serverName: Klavis.McpServerName.Gmail,
|
||||
},
|
||||
{
|
||||
@@ -102,9 +102,9 @@ export const KLAVIS_SERVER_TYPES: KlavisServerType[] = [
|
||||
description: 'Google Drive is a cloud storage service',
|
||||
icon: 'https://hub-apac-1.lobeobjects.space/assets/logos/googledrive.svg',
|
||||
identifier: 'google-drive',
|
||||
label: 'Google Drive',
|
||||
readme:
|
||||
'Connect to Google Drive to access, organize, and manage your files. Search documents, upload files, share content, and navigate your cloud storage efficiently through AI assistance.',
|
||||
label: 'Google Drive',
|
||||
serverName: Klavis.McpServerName.GoogleDrive,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS "page_shares" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"document_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"visibility" text DEFAULT 'private' NOT NULL,
|
||||
"page_view_count" integer DEFAULT 0 NOT NULL,
|
||||
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "page_shares" DROP CONSTRAINT IF EXISTS "page_shares_document_id_documents_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "page_shares" ADD CONSTRAINT "page_shares_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "page_shares" DROP CONSTRAINT IF EXISTS "page_shares_user_id_users_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "page_shares" ADD CONSTRAINT "page_shares_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "page_shares_document_id_unique" ON "page_shares" USING btree ("document_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "page_shares_user_id_idx" ON "page_shares" USING btree ("user_id");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -721,6 +721,13 @@
|
||||
"when": 1778602304603,
|
||||
"tag": "0102_add_agent_operations_table",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 103,
|
||||
"version": "7",
|
||||
"when": 1779263513902,
|
||||
"tag": "0103_add_page_shares",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
"version": "6"
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './messengerInstallation';
|
||||
export * from './nextauth';
|
||||
export * from './notification';
|
||||
export * from './oidc';
|
||||
export * from './pageShare';
|
||||
export * from './rag';
|
||||
export * from './ragEvals';
|
||||
export * from './rbac';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { index, integer, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
import { createNanoId } from '../utils/idGenerator';
|
||||
import { timestamps } from './_helpers';
|
||||
import { documents } from './file';
|
||||
import { users } from './user';
|
||||
|
||||
/**
|
||||
* Page sharing table - Manages public sharing links for documents/pages.
|
||||
*/
|
||||
export const pageShares = pgTable(
|
||||
'page_shares',
|
||||
{
|
||||
id: text('id')
|
||||
.$defaultFn(() => createNanoId(8)())
|
||||
.primaryKey(),
|
||||
|
||||
documentId: text('document_id')
|
||||
.notNull()
|
||||
.references(() => documents.id, { onDelete: 'cascade' }),
|
||||
|
||||
userId: text('user_id')
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
|
||||
visibility: text('visibility').default('private').notNull(), // 'private' | 'link'
|
||||
|
||||
pageViewCount: integer('page_view_count').default(0).notNull(),
|
||||
|
||||
...timestamps,
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('page_shares_document_id_unique').on(t.documentId),
|
||||
index('page_shares_user_id_idx').on(t.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export type NewPageShare = typeof pageShares.$inferInsert;
|
||||
export type PageShareItem = typeof pageShares.$inferSelect;
|
||||
@@ -16,6 +16,7 @@ import { documentHistories } from './documentHistory';
|
||||
import { documents, files, knowledgeBases } from './file';
|
||||
import { generationBatches, generations, generationTopics } from './generation';
|
||||
import { messageGroups, messages, messagesFiles, messageTranslates } from './message';
|
||||
import { pageShares } from './pageShare';
|
||||
import { chunks, documentChunks, unstructuredChunks } from './rag';
|
||||
import { sessionGroups, sessions } from './session';
|
||||
import { threads, topicDocuments, topics } from './topic';
|
||||
@@ -246,10 +247,22 @@ export const documentsRelations = relations(documents, ({ one, many }) => ({
|
||||
relationName: 'fileDocuments',
|
||||
}),
|
||||
topics: many(topicDocuments),
|
||||
shares: many(pageShares),
|
||||
chunks: many(documentChunks),
|
||||
histories: many(documentHistories),
|
||||
}));
|
||||
|
||||
export const pageSharesRelations = relations(pageShares, ({ one }) => ({
|
||||
document: one(documents, {
|
||||
fields: [pageShares.documentId],
|
||||
references: [documents.id],
|
||||
}),
|
||||
user: one(users, {
|
||||
fields: [pageShares.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const documentHistoriesRelations = relations(documentHistories, ({ one }) => ({
|
||||
document: one(documents, {
|
||||
fields: [documentHistories.documentId],
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
export const GOOGLE_RESTRICTED_PROVIDER_IDS = ['xai', 'deepseek'] as const;
|
||||
export type GoogleRestrictedProvider = (typeof GOOGLE_RESTRICTED_PROVIDER_IDS)[number];
|
||||
|
||||
// Model prefixes that are restricted from using Google tools
|
||||
export const GOOGLE_RESTRICTED_MODEL_PREFIXES = ['grok', 'deepseek'] as const;
|
||||
|
||||
export const GOOGLE_TOOL_IDENTIFIERS = [
|
||||
'gmail',
|
||||
'google-calendar',
|
||||
'google-drive',
|
||||
'google-sheets',
|
||||
'google-docs',
|
||||
] as const;
|
||||
export type GoogleToolIdentifier = (typeof GOOGLE_TOOL_IDENTIFIERS)[number];
|
||||
|
||||
/**
|
||||
* Check if a provider/model combination is restricted from using Google tools
|
||||
* Checks both provider ID and model ID (model name may contain restricted prefixes)
|
||||
*/
|
||||
export const isGoogleRestrictedProvider = (providerId: string, modelId?: string): boolean => {
|
||||
// Check provider ID
|
||||
if (GOOGLE_RESTRICTED_PROVIDER_IDS.includes(providerId as GoogleRestrictedProvider)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check model ID (model name may start with grok or deepseek)
|
||||
if (modelId) {
|
||||
const lowerModelId = modelId.toLowerCase();
|
||||
if (GOOGLE_RESTRICTED_MODEL_PREFIXES.some((prefix) => lowerModelId.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isGoogleTool = (identifier: string): boolean =>
|
||||
GOOGLE_TOOL_IDENTIFIERS.includes(identifier as GoogleToolIdentifier);
|
||||
@@ -3,7 +3,6 @@ import { Loader2, SquareArrowOutUpRight } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useGoogleDataProtection } from '@/hooks/useGoogleDataProtection';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
@@ -41,8 +40,6 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
const [isWaitingAuth, setIsWaitingAuth] = useState(false);
|
||||
|
||||
const { checkGoogleToolConnect } = useGoogleDataProtection();
|
||||
|
||||
const oauthWindowRef = useRef<Window | null>(null);
|
||||
const windowCheckIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
@@ -203,12 +200,6 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a Google tool and we're using a restricted provider
|
||||
const isBlocked = await checkGoogleToolConnect(identifier);
|
||||
if (isBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
const newServer = await createKlavisServer({
|
||||
@@ -239,15 +230,6 @@ const KlavisServerItem = memo<KlavisServerItemProps>(
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!server) return;
|
||||
|
||||
// If enabling (not currently checked), check for Google tool restrictions
|
||||
if (!checked) {
|
||||
const isBlocked = await checkGoogleToolConnect(identifier);
|
||||
if (isBlocked) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsToggling(true);
|
||||
await togglePlugin(pluginId);
|
||||
setIsToggling(false);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -263,37 +262,4 @@ describe('AgentSignalReceiptList', () => {
|
||||
|
||||
expect(mocks.navigate).toHaveBeenCalledWith('/memory');
|
||||
});
|
||||
|
||||
it('opens memory receipts on their layer detail route when target metadata is available', () => {
|
||||
render(
|
||||
<AgentSignalReceiptList
|
||||
receipts={[
|
||||
{
|
||||
agentId: 'agent-1',
|
||||
createdAt: 1,
|
||||
detail: 'Saved this for future replies',
|
||||
id: 'receipt-1',
|
||||
kind: 'memory',
|
||||
sourceId: 'source-1',
|
||||
sourceType: 'client.gateway.runtime_end',
|
||||
status: 'applied',
|
||||
target: {
|
||||
id: 'preference-1',
|
||||
memoryId: 'memory-1',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
title: 'Decision-first PR review preference',
|
||||
type: 'memory',
|
||||
},
|
||||
title: 'Memory saved',
|
||||
topicId: 'topic-1',
|
||||
userId: 'user-1',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Open'));
|
||||
|
||||
expect(mocks.navigate).toHaveBeenCalledWith('/memory/preferences?preferenceId=preference-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
@@ -15,13 +14,6 @@ import { useChatStore } from '@/store/chat';
|
||||
import type { AgentSignalReceiptView } from '../hooks/useAgentSignalReceipts';
|
||||
|
||||
const PAGE_ROUTE_PATTERN = /^\/agent\/([^/]+)\/([^/]+)\/page(?:\/[^/?#]+)?/;
|
||||
const MEMORY_ROUTE_BY_LAYER = {
|
||||
[LayersEnum.Activity]: { idParam: 'activityId', path: '/memory/activities' },
|
||||
[LayersEnum.Context]: { idParam: 'contextId', path: '/memory/contexts' },
|
||||
[LayersEnum.Experience]: { idParam: 'experienceId', path: '/memory/experiences' },
|
||||
[LayersEnum.Identity]: { idParam: 'identityId', path: '/memory/identities' },
|
||||
[LayersEnum.Preference]: { idParam: 'preferenceId', path: '/memory/preferences' },
|
||||
} satisfies Record<LayersEnum, { idParam: string; path: string }>;
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
agentSignalDescription: css`
|
||||
@@ -66,17 +58,6 @@ interface AgentSignalReceiptItemProps {
|
||||
receipt: AgentSignalReceiptView;
|
||||
}
|
||||
|
||||
const getMemoryRoute = (target?: AgentSignalReceiptView['target']) => {
|
||||
if (target?.type !== 'memory') return;
|
||||
|
||||
if (!target.memoryLayer) return '/memory';
|
||||
|
||||
const route = MEMORY_ROUTE_BY_LAYER[target.memoryLayer];
|
||||
if (!route) return '/memory';
|
||||
|
||||
return target.id ? `${route.path}?${route.idParam}=${encodeURIComponent(target.id)}` : route.path;
|
||||
};
|
||||
|
||||
const AgentSignalReceiptItem = memo<AgentSignalReceiptItemProps>(({ receipt }) => {
|
||||
const { t } = useTranslation(['chat', 'common']);
|
||||
const navigate = useStableNavigate();
|
||||
@@ -103,11 +84,10 @@ const AgentSignalReceiptItem = memo<AgentSignalReceiptItemProps>(({ receipt }) =
|
||||
);
|
||||
const target = receipt.target;
|
||||
const documentId = target?.type === 'skill' ? (target.documentId ?? target.id) : undefined;
|
||||
const memoryRoute = getMemoryRoute(target);
|
||||
const canOpen = Boolean(memoryRoute) || Boolean(documentId);
|
||||
const canOpen = target?.type === 'memory' || Boolean(documentId);
|
||||
const handleOpen = useCallback(() => {
|
||||
if (memoryRoute) {
|
||||
navigate(memoryRoute);
|
||||
if (target?.type === 'memory') {
|
||||
navigate('/memory');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +103,7 @@ const AgentSignalReceiptItem = memo<AgentSignalReceiptItemProps>(({ receipt }) =
|
||||
}
|
||||
|
||||
openDocument(documentId);
|
||||
}, [documentId, memoryRoute, navigate, openDocument, target]);
|
||||
}, [documentId, navigate, openDocument, target]);
|
||||
|
||||
return (
|
||||
<PortalResourceCard
|
||||
@@ -132,6 +112,7 @@ const AgentSignalReceiptItem = memo<AgentSignalReceiptItemProps>(({ receipt }) =
|
||||
openLabel={canOpen ? t('common:cmdk.toOpen', 'Open') : undefined}
|
||||
title={title}
|
||||
tooltip={tooltip}
|
||||
// TODO: Replace memory fallback with category/id-aware routes when Agent Signal receipts expose them.
|
||||
onOpen={canOpen ? handleOpen : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useGoogleDataProtection } from '@/hooks/useGoogleDataProtection';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
|
||||
interface UsePanelHandlersProps {
|
||||
@@ -13,16 +12,9 @@ export const usePanelHandlers = ({
|
||||
onOpenChange,
|
||||
}: UsePanelHandlersProps) => {
|
||||
const updateAgentConfig = useAgentStore((s) => s.updateAgentConfig);
|
||||
const { checkModelSwitch } = useGoogleDataProtection();
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
async (modelId: string, providerId: string) => {
|
||||
// Check if switching to a restricted provider with Google tools enabled
|
||||
const isBlocked = await checkModelSwitch(providerId, modelId);
|
||||
if (isBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
(modelId: string, providerId: string) => {
|
||||
// Defer store update so the panel close animation completes
|
||||
// before React re-renders with new data (prevents detail panel flash).
|
||||
setTimeout(() => {
|
||||
@@ -34,7 +26,7 @@ export const usePanelHandlers = ({
|
||||
}
|
||||
}, 150);
|
||||
},
|
||||
[onModelChangeProp, updateAgentConfig, checkModelSwitch],
|
||||
[onModelChangeProp, updateAgentConfig],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { UIChatMessage } from '@lobechat/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type ConversationHooks } from '@/features/Conversation';
|
||||
|
||||
import { useGoogleDataProtection } from './useGoogleDataProtection';
|
||||
|
||||
interface UseAgentGoogleProtectionHooksParams {
|
||||
messages?: UIChatMessage[];
|
||||
}
|
||||
|
||||
export function useAgentGoogleProtectionHooks(
|
||||
params?: UseAgentGoogleProtectionHooksParams,
|
||||
): ConversationHooks {
|
||||
const { checkMessageHistoryForGoogleTools, checkMessageSend } = useGoogleDataProtection();
|
||||
|
||||
return useMemo(
|
||||
(): ConversationHooks => ({
|
||||
onBeforeSendMessage: async () => {
|
||||
// Check 1: Are Google tools currently enabled with restricted provider?
|
||||
const canSendWithEnabledTools = await checkMessageSend();
|
||||
if (!canSendWithEnabledTools) return false;
|
||||
|
||||
// Check 2: Does message history contain Google tool usage with restricted provider?
|
||||
const canSendWithHistory = await checkMessageHistoryForGoogleTools(params?.messages);
|
||||
if (!canSendWithHistory) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
[checkMessageSend, checkMessageHistoryForGoogleTools, params?.messages],
|
||||
);
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { UIChatMessage } from '@lobechat/types';
|
||||
import { App } from 'antd';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
GOOGLE_TOOL_IDENTIFIERS,
|
||||
isGoogleRestrictedProvider,
|
||||
isGoogleTool,
|
||||
} from '@/const/googleDataProtection';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { klavisStoreSelectors } from '@/store/tool/selectors';
|
||||
|
||||
/**
|
||||
* Check if messages contain any Google tool usage
|
||||
*/
|
||||
export const findGoogleToolsInMessages = (messages: UIChatMessage[] | undefined): string[] => {
|
||||
if (!messages || messages.length === 0) return [];
|
||||
|
||||
const googleToolIds = new Set<string>();
|
||||
|
||||
for (const message of messages) {
|
||||
// Check plugin field
|
||||
if (message.plugin?.identifier && isGoogleTool(message.plugin.identifier)) {
|
||||
googleToolIds.add(message.plugin.identifier);
|
||||
}
|
||||
|
||||
// Check tools field
|
||||
if (message.tools) {
|
||||
for (const tool of message.tools) {
|
||||
if (isGoogleTool(tool.identifier)) {
|
||||
googleToolIds.add(tool.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check children (grouped tool messages - AssistantContentBlock has tools field)
|
||||
if (message.children) {
|
||||
for (const child of message.children) {
|
||||
if (child.tools) {
|
||||
for (const tool of child.tools) {
|
||||
if (isGoogleTool(tool.identifier)) {
|
||||
googleToolIds.add(tool.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(googleToolIds);
|
||||
};
|
||||
|
||||
export interface GoogleDataProtectionState {
|
||||
enabledGoogleToolIds: string[];
|
||||
hasConflict: boolean;
|
||||
hasEnabledGoogleTools: boolean;
|
||||
isUsingRestrictedProvider: boolean;
|
||||
}
|
||||
|
||||
export const useGoogleDataProtection = () => {
|
||||
const { t } = useTranslation('setting');
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const plugins = useAgentStore(agentSelectors.currentAgentPlugins);
|
||||
const currentProvider = useAgentStore(agentSelectors.currentAgentModelProvider);
|
||||
const currentModel = useAgentStore(agentSelectors.currentAgentModel);
|
||||
const connectedServers = useToolStore(klavisStoreSelectors.getConnectedServers);
|
||||
|
||||
const state = useMemo((): GoogleDataProtectionState => {
|
||||
const connectedGoogleServerIds = connectedServers
|
||||
.filter((server) => isGoogleTool(server.identifier))
|
||||
.map((server) => server.identifier);
|
||||
|
||||
const enabledGoogleToolIds = plugins.filter((pluginId) =>
|
||||
connectedGoogleServerIds.includes(pluginId),
|
||||
);
|
||||
|
||||
const hasEnabledGoogleTools = enabledGoogleToolIds.length > 0;
|
||||
const isUsingRestrictedProvider = isGoogleRestrictedProvider(currentProvider, currentModel);
|
||||
const hasConflict = hasEnabledGoogleTools && isUsingRestrictedProvider;
|
||||
|
||||
return {
|
||||
enabledGoogleToolIds,
|
||||
hasConflict,
|
||||
hasEnabledGoogleTools,
|
||||
isUsingRestrictedProvider,
|
||||
};
|
||||
}, [plugins, currentProvider, currentModel, connectedServers]);
|
||||
|
||||
const checkGoogleToolConnect = useCallback(
|
||||
async (toolIdentifier: string): Promise<boolean> => {
|
||||
if (!isGoogleTool(toolIdentifier)) return false;
|
||||
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return false;
|
||||
|
||||
modal.warning({
|
||||
centered: true,
|
||||
content: t('googleDataProtection.cannotConnectGoogle.content'),
|
||||
okText: t('googleDataProtection.understood'),
|
||||
title: t('googleDataProtection.cannotConnectGoogle.title'),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
[currentProvider, currentModel, modal, t],
|
||||
);
|
||||
|
||||
const checkModelSwitch = useCallback(
|
||||
async (newProviderId: string, newModelId?: string): Promise<boolean> => {
|
||||
if (!isGoogleRestrictedProvider(newProviderId, newModelId)) return false;
|
||||
if (!state.hasEnabledGoogleTools) return false;
|
||||
|
||||
const toolNames = state.enabledGoogleToolIds
|
||||
.map((id) => {
|
||||
const toolInfo = GOOGLE_TOOL_IDENTIFIERS.find((t) => t === id);
|
||||
return toolInfo || id;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
modal.warning({
|
||||
centered: true,
|
||||
content: t('googleDataProtection.cannotSwitchModel.content', { tools: toolNames }),
|
||||
okText: t('googleDataProtection.understood'),
|
||||
title: t('googleDataProtection.cannotSwitchModel.title'),
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
[state.hasEnabledGoogleTools, state.enabledGoogleToolIds, modal, t],
|
||||
);
|
||||
|
||||
const checkMessageSend = useCallback(async (): Promise<boolean> => {
|
||||
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return true;
|
||||
if (!state.hasEnabledGoogleTools) return true;
|
||||
|
||||
const toolNames = state.enabledGoogleToolIds
|
||||
.map((id) => {
|
||||
const toolInfo = GOOGLE_TOOL_IDENTIFIERS.find((t) => t === id);
|
||||
return toolInfo || id;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
modal.warning({
|
||||
centered: true,
|
||||
content: t('googleDataProtection.cannotSendMessage.content', { tools: toolNames }),
|
||||
okText: t('googleDataProtection.understood'),
|
||||
title: t('googleDataProtection.cannotSendMessage.title'),
|
||||
});
|
||||
|
||||
return false;
|
||||
}, [
|
||||
currentProvider,
|
||||
currentModel,
|
||||
state.hasEnabledGoogleTools,
|
||||
state.enabledGoogleToolIds,
|
||||
modal,
|
||||
t,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Scenario 4: Check if message history contains Google tool usage
|
||||
* Block sending if using restricted provider with Google tools in history
|
||||
* Returns false to block, true to allow
|
||||
*/
|
||||
const checkMessageHistoryForGoogleTools = useCallback(
|
||||
async (messages: UIChatMessage[] | undefined): Promise<boolean> => {
|
||||
if (!isGoogleRestrictedProvider(currentProvider, currentModel)) return true;
|
||||
|
||||
const googleToolsInHistory = findGoogleToolsInMessages(messages);
|
||||
if (googleToolsInHistory.length === 0) return true;
|
||||
|
||||
const toolNames = googleToolsInHistory.join(', ');
|
||||
|
||||
modal.warning({
|
||||
centered: true,
|
||||
content: t('googleDataProtection.cannotSendWithHistory.content', { tools: toolNames }),
|
||||
okText: t('googleDataProtection.understood'),
|
||||
title: t('googleDataProtection.cannotSendWithHistory.title'),
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
[currentProvider, currentModel, modal, t],
|
||||
);
|
||||
|
||||
return {
|
||||
checkGoogleToolConnect,
|
||||
checkMessageHistoryForGoogleTools,
|
||||
checkMessageSend,
|
||||
checkModelSwitch,
|
||||
state,
|
||||
};
|
||||
};
|
||||
@@ -206,21 +206,7 @@ export default {
|
||||
'analytics.telemetry.title': 'Send Anonymous Usage Data',
|
||||
'analytics.title': 'Analytics',
|
||||
|
||||
'googleDataProtection.cannotConnectGoogle.content':
|
||||
'You are using a non-Google model. Connecting Google tools (Gmail, Drive, Docs) may expose your data to third-party providers. Please switch to a Google model before enabling these tools.',
|
||||
'googleDataProtection.cannotConnectGoogle.title': 'Cannot Connect Google Tool',
|
||||
'googleDataProtection.cannotSendMessage.content':
|
||||
'You have Google tools enabled ({{tools}}) but are using a non-Google model. Sending messages may expose your Google data to third-party providers. Please disable Google tools or switch to a Google model.',
|
||||
'googleDataProtection.cannotSendMessage.title': 'Cannot Send Message',
|
||||
'googleDataProtection.cannotSendWithHistory.content':
|
||||
'Your conversation history contains Google tool usage ({{tools}}), but you are now using a non-Google model. Sending messages may expose your Google data to third-party providers. Please switch to a Google model.',
|
||||
'googleDataProtection.cannotSendWithHistory.title': 'Cannot Send Message',
|
||||
'googleDataProtection.cannotSwitchModel.content':
|
||||
'You have Google tools enabled ({{tools}}). Switching to a non-Google model may expose your Google data to third-party providers. Please disable Google tools before switching models.',
|
||||
'googleDataProtection.cannotSwitchModel.title': 'Cannot Switch Model',
|
||||
'googleDataProtection.understood': 'Understood',
|
||||
|
||||
// Heterogeneous agent CLI status (shown on agent profile page in integration mode)
|
||||
// Heterogeneous agent CLI status (shown on agent profile page in integration mode)
|
||||
'heterogeneousStatus.account.label': 'Account',
|
||||
'heterogeneousStatus.auth.api': 'API',
|
||||
'heterogeneousStatus.auth.label': 'Auth Method',
|
||||
|
||||
@@ -10,7 +10,6 @@ import AgentHome from '@/features/AgentHome';
|
||||
import ChatMiniMap from '@/features/ChatMiniMap';
|
||||
import { ChatList, ConversationProvider } from '@/features/Conversation';
|
||||
import ZenModeToast from '@/features/ZenModeToast';
|
||||
import { useAgentGoogleProtectionHooks } from '@/hooks/useAgentGoogleProtectionHooks';
|
||||
import { useGatewayReconnect } from '@/hooks/useGatewayReconnect';
|
||||
import { useOperationState } from '@/hooks/useOperationState';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
@@ -55,9 +54,6 @@ const Conversation = memo(() => {
|
||||
// Get actionsBar config with branching support from ChatStore
|
||||
const actionsBarConfig = useActionsBarConfig();
|
||||
|
||||
// Get Google data protection hooks
|
||||
const googleProtectionHooks = useAgentGoogleProtectionHooks({ messages });
|
||||
|
||||
// Heterogeneous agents (Claude Code, etc.) use a simplified input — their
|
||||
// toolchain/memory/model are managed by the external runtime, so LobeHub's
|
||||
// model/tools/memory/KB/MCP/runtime-mode pickers don't apply.
|
||||
@@ -80,7 +76,6 @@ const Conversation = memo(() => {
|
||||
actionsBar={actionsBarConfig}
|
||||
context={context}
|
||||
hasInitMessages={!!messages}
|
||||
hooks={googleProtectionHooks}
|
||||
messages={messages}
|
||||
operationState={operationState}
|
||||
onMessagesChange={(messages, ctx) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const klavisRouter = router({
|
||||
|
||||
// Get the tool list for this server
|
||||
const toolsResponse = await ctx.klavisClient.mcpServer.getTools(serverName as any);
|
||||
const tools = (toolsResponse.tools || []) as { name: string }[];
|
||||
const tools = toolsResponse.tools || [];
|
||||
|
||||
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
|
||||
const manifest: ToolManifest = {
|
||||
|
||||
@@ -73,7 +73,7 @@ export const klavisRouter = router({
|
||||
const response = await klavisClient.mcpServer.getTools(input.serverName as any);
|
||||
|
||||
return {
|
||||
tools: (response.tools || []) as { name: string }[],
|
||||
tools: response.tools,
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
+1
-358
@@ -1,9 +1,8 @@
|
||||
// @vitest-environment node
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { RuntimeProcessorContext } from '../../../../runtime/context';
|
||||
import { defineUserMemoryActionHandler, resolveMemoryActionTargetFromState } from '../userMemory';
|
||||
import { defineUserMemoryActionHandler } from '../userMemory';
|
||||
|
||||
const memoryActionRunner = vi.fn();
|
||||
|
||||
@@ -74,64 +73,6 @@ describe('defineUserMemoryActionHandler', () => {
|
||||
expect(context.runtimeState.touchGuardState).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the applied memory target from the memory agent runner', async () => {
|
||||
memoryActionRunner.mockResolvedValue({
|
||||
detail: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
status: 'applied',
|
||||
target: {
|
||||
id: 'mem_td3XirTeX4f7',
|
||||
memoryId: 'mem_8gISOK6BhxGP',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
title: '偏好更详细、更长的回答段落',
|
||||
type: 'memory',
|
||||
},
|
||||
});
|
||||
|
||||
const handler = defineUserMemoryActionHandler({
|
||||
db: {} as never,
|
||||
memoryActionRunner,
|
||||
userId: 'user_1',
|
||||
});
|
||||
|
||||
const result = await handler.handle(
|
||||
{
|
||||
actionId: 'act_memory_target',
|
||||
actionType: 'action.user-memory.handle',
|
||||
chain: { chainId: 'chain_1', rootSourceId: 'source_1' },
|
||||
payload: {
|
||||
agentId: 'agent_1',
|
||||
idempotencyKey: 'source_1:memory:msg_1',
|
||||
message:
|
||||
'<speaker id="833816919" username="nivra2000" nickname="Aa T" />\n每一块都有点太短了?能否长一点呢',
|
||||
topicId: 'topic_1',
|
||||
},
|
||||
signal: {
|
||||
signalId: 'sig_1',
|
||||
signalType: 'signal.feedback.domain.memory',
|
||||
},
|
||||
source: { sourceId: 'source_1', sourceType: 'agent.user.message' },
|
||||
timestamp: 1,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
detail: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
output: {
|
||||
target: {
|
||||
id: 'mem_td3XirTeX4f7',
|
||||
memoryId: 'mem_8gISOK6BhxGP',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
title: '偏好更详细、更长的回答段落',
|
||||
type: 'memory',
|
||||
},
|
||||
},
|
||||
status: 'applied',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips memory actions when the feedback message is missing', async () => {
|
||||
const handler = defineUserMemoryActionHandler({
|
||||
db: {} as never,
|
||||
@@ -300,301 +241,3 @@ describe('defineUserMemoryActionHandler', () => {
|
||||
expect(memoryActionRunner).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveMemoryActionTargetFromState', () => {
|
||||
it('extracts the successful memory title from runtime tool calls', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_bad',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: '{,',
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_bad',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: 'The tool call arguments string is not valid JSON.',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_bad',
|
||||
},
|
||||
{
|
||||
id: 'msg_good',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
details: '用户反馈当前回复的每个模块都太短,希望后续展开得更充分。',
|
||||
summary: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
title: '偏好更详细、更长的回答段落',
|
||||
withPreference: {
|
||||
conclusionDirectives: '回答时展开每个段落和模块。',
|
||||
},
|
||||
}),
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_good',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Preference memory "偏好更详细、更长的回答段落" saved with memoryId: "mem_8gISOK6BhxGP" and preferenceId: "mem_td3XirTeX4f7"',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_good',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toEqual({
|
||||
id: 'mem_td3XirTeX4f7',
|
||||
memoryId: 'mem_8gISOK6BhxGP',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Arvin Xu 希望助手在输出时每个段落/模块都写得更长、更展开。',
|
||||
title: '偏好更详细、更长的回答段落',
|
||||
type: 'memory',
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves update identity targets from nested set arguments', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_update_identity',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
id: 'identity-existing',
|
||||
mergeStrategy: 'replace',
|
||||
set: {
|
||||
details: 'The user clarified that they maintain LobeHub Agent Signal code.',
|
||||
summary: 'The user maintains Agent Signal memory receipt behavior.',
|
||||
title: 'Maintains Agent Signal receipts',
|
||||
},
|
||||
}),
|
||||
name: 'lobe-user-memory____updateIdentityMemory',
|
||||
},
|
||||
id: 'call_update_identity',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: 'Identity memory updated: identity-existing',
|
||||
pluginState: { identityId: 'identity-existing' },
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_update_identity',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toEqual({
|
||||
id: 'identity-existing',
|
||||
memoryLayer: LayersEnum.Identity,
|
||||
summary: 'The user maintains Agent Signal memory receipt behavior.',
|
||||
title: 'Maintains Agent Signal receipts',
|
||||
type: 'memory',
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves receipt targets from persisted tool snapshots', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_persisted_tool',
|
||||
role: 'assistant',
|
||||
tools: [
|
||||
null,
|
||||
{
|
||||
apiName: 'addPreferenceMemory',
|
||||
arguments: {
|
||||
title: 'Persisted preference title',
|
||||
withPreference: {
|
||||
conclusionDirectives: 'Use persisted tool metadata for receipt targets.',
|
||||
},
|
||||
},
|
||||
id: 'call_persisted',
|
||||
identifier: 'lobe-user-memory',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: 'Preference memory saved',
|
||||
plugin: { id: 'call_persisted' },
|
||||
pluginState: { memoryId: 'mem_persisted', preferenceId: 'pref_persisted' },
|
||||
role: 'tool',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toEqual({
|
||||
id: 'pref_persisted',
|
||||
memoryId: 'mem_persisted',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Use persisted tool metadata for receipt targets.',
|
||||
title: 'Persisted preference title',
|
||||
type: 'memory',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips confirmed memory tool calls with invalid arguments', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_confirmed',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
details: 'Fallback details for a valid confirmed target.',
|
||||
title: 'Valid confirmed preference',
|
||||
}),
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_confirmed',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Preference memory "Valid confirmed preference" saved with memoryId: "mem_confirmed" and preferenceId: "pref_confirmed"',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_confirmed',
|
||||
},
|
||||
{
|
||||
id: 'msg_invalid',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: '{,',
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_invalid',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Preference memory "Invalid latest preference" saved with memoryId: "mem_invalid" and preferenceId: "pref_invalid"',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_invalid',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toEqual({
|
||||
id: 'pref_confirmed',
|
||||
memoryId: 'mem_confirmed',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Fallback details for a valid confirmed target.',
|
||||
title: 'Valid confirmed preference',
|
||||
type: 'memory',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores unconfirmed memory write tool calls when resolving receipt targets', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_confirmed',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
summary: 'The user prefers longer, more developed answers.',
|
||||
title: 'Confirmed preference title',
|
||||
}),
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_confirmed',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content:
|
||||
'Preference memory "Confirmed preference title" saved with memoryId: "mem_confirmed" and preferenceId: "pref_confirmed"',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_confirmed',
|
||||
},
|
||||
{
|
||||
id: 'msg_unconfirmed',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
summary: 'This write was not confirmed by a successful tool result.',
|
||||
title: 'Unconfirmed preference title',
|
||||
}),
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_unconfirmed',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: 'addPreferenceMemory with error detail: database timeout',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_unconfirmed',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toEqual({
|
||||
id: 'pref_confirmed',
|
||||
memoryId: 'mem_confirmed',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'The user prefers longer, more developed answers.',
|
||||
title: 'Confirmed preference title',
|
||||
type: 'memory',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not resolve a target when no memory write has a successful tool result', () => {
|
||||
const target = resolveMemoryActionTargetFromState({
|
||||
messages: [
|
||||
{
|
||||
id: 'msg_unconfirmed',
|
||||
role: 'assistant',
|
||||
tool_calls: [
|
||||
{
|
||||
function: {
|
||||
arguments: JSON.stringify({
|
||||
summary: 'This write was not confirmed by a successful tool result.',
|
||||
title: 'Unconfirmed preference title',
|
||||
}),
|
||||
name: 'lobe-user-memory____addPreferenceMemory',
|
||||
},
|
||||
id: 'call_unconfirmed',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
content: 'addPreferenceMemory with error detail: database timeout',
|
||||
role: 'tool',
|
||||
tool_call_id: 'call_unconfirmed',
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(target).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
createAgentSignalMemoryWriterPrompt,
|
||||
createAgentSignalMemoryWriterSystemRole,
|
||||
} from '@lobechat/prompts';
|
||||
import { LayersEnum, RequestTrigger } from '@lobechat/types';
|
||||
import { RequestTrigger } from '@lobechat/types';
|
||||
import { nanoid } from '@lobechat/utils';
|
||||
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
@@ -44,45 +44,21 @@ import { AGENT_SIGNAL_POLICY_ACTION_TYPES } from '../../types';
|
||||
|
||||
const MEMORY_AGENT_MAX_STEPS = 8;
|
||||
|
||||
const MEMORY_WRITE_API_NAMES = [
|
||||
MemoryApiName.addActivityMemory,
|
||||
MemoryApiName.addContextMemory,
|
||||
MemoryApiName.addExperienceMemory,
|
||||
MemoryApiName.addIdentityMemory,
|
||||
MemoryApiName.addPreferenceMemory,
|
||||
MemoryApiName.removeIdentityMemory,
|
||||
MemoryApiName.updateIdentityMemory,
|
||||
] as const;
|
||||
|
||||
const MEMORY_WRITE_TOOL_NAMES = new Set(
|
||||
MEMORY_WRITE_API_NAMES.map((apiName) => `${MemoryIdentifier}/${apiName}`),
|
||||
[
|
||||
MemoryApiName.addActivityMemory,
|
||||
MemoryApiName.addContextMemory,
|
||||
MemoryApiName.addExperienceMemory,
|
||||
MemoryApiName.addIdentityMemory,
|
||||
MemoryApiName.addPreferenceMemory,
|
||||
MemoryApiName.removeIdentityMemory,
|
||||
MemoryApiName.updateIdentityMemory,
|
||||
].map((apiName) => `${MemoryIdentifier}/${apiName}`),
|
||||
);
|
||||
|
||||
const MEMORY_WRITE_API_NAME_SET = new Set<string>(MEMORY_WRITE_API_NAMES);
|
||||
const MEMORY_WRITE_TARGET_BY_API_NAME: Record<string, { idKey: string; layer: LayersEnum }> = {
|
||||
[MemoryApiName.addActivityMemory]: { idKey: 'activityId', layer: LayersEnum.Activity },
|
||||
[MemoryApiName.addContextMemory]: { idKey: 'contextId', layer: LayersEnum.Context },
|
||||
[MemoryApiName.addExperienceMemory]: { idKey: 'experienceId', layer: LayersEnum.Experience },
|
||||
[MemoryApiName.addIdentityMemory]: { idKey: 'identityId', layer: LayersEnum.Identity },
|
||||
[MemoryApiName.addPreferenceMemory]: { idKey: 'preferenceId', layer: LayersEnum.Preference },
|
||||
[MemoryApiName.removeIdentityMemory]: { idKey: 'identityId', layer: LayersEnum.Identity },
|
||||
[MemoryApiName.updateIdentityMemory]: { idKey: 'identityId', layer: LayersEnum.Identity },
|
||||
};
|
||||
const TOOL_NAME_SEPARATOR = '____';
|
||||
|
||||
export interface MemoryActionTarget {
|
||||
id?: string;
|
||||
memoryId?: string;
|
||||
memoryLayer?: LayersEnum;
|
||||
summary?: string;
|
||||
title: string;
|
||||
type: 'memory';
|
||||
}
|
||||
|
||||
export interface MemoryAgentActionResult {
|
||||
detail?: string;
|
||||
status: 'applied' | 'failed' | 'skipped';
|
||||
target?: MemoryActionTarget;
|
||||
}
|
||||
|
||||
export interface UserMemoryActionHandlerOptions {
|
||||
@@ -177,196 +153,6 @@ const hasFailedMemoryWrite = (state: AgentState) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const getString = (value: unknown) => {
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||
};
|
||||
|
||||
const parseToolArguments = (value: unknown): Record<string, unknown> | undefined => {
|
||||
if (isRecord(value)) return value;
|
||||
|
||||
if (typeof value !== 'string') return;
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
|
||||
return isRecord(parsed) ? parsed : undefined;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
interface MemoryToolCallSnapshot {
|
||||
apiName?: string;
|
||||
arguments?: unknown;
|
||||
id?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
const getToolCallsFromMessage = (message: unknown): MemoryToolCallSnapshot[] => {
|
||||
if (!isRecord(message)) return [];
|
||||
|
||||
const toolCalls: MemoryToolCallSnapshot[] = [];
|
||||
const persistedTools = Array.isArray(message.tools) ? message.tools : [];
|
||||
|
||||
for (const tool of persistedTools) {
|
||||
if (!isRecord(tool)) continue;
|
||||
|
||||
toolCalls.push({
|
||||
apiName: getString(tool.apiName),
|
||||
arguments: tool.arguments,
|
||||
id: getString(tool.id),
|
||||
identifier: getString(tool.identifier),
|
||||
});
|
||||
}
|
||||
|
||||
const rawToolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
||||
|
||||
for (const toolCall of rawToolCalls) {
|
||||
if (!isRecord(toolCall)) continue;
|
||||
|
||||
const fn = isRecord(toolCall.function) ? toolCall.function : undefined;
|
||||
const name = getString(fn?.name);
|
||||
if (!name) continue;
|
||||
|
||||
const [identifier, apiName] = name.split(TOOL_NAME_SEPARATOR);
|
||||
|
||||
toolCalls.push({
|
||||
apiName: apiName || name,
|
||||
arguments: fn?.arguments,
|
||||
id: getString(toolCall.id),
|
||||
identifier: apiName ? identifier : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return toolCalls;
|
||||
};
|
||||
|
||||
const isMemoryWriteToolCall = (
|
||||
toolCall: MemoryToolCallSnapshot,
|
||||
): toolCall is MemoryToolCallSnapshot & { apiName: string } => {
|
||||
if (!toolCall.apiName || !MEMORY_WRITE_API_NAME_SET.has(toolCall.apiName)) return false;
|
||||
|
||||
return !toolCall.identifier || toolCall.identifier === MemoryIdentifier;
|
||||
};
|
||||
|
||||
const getToolMessageCallId = (message: unknown) => {
|
||||
if (!isRecord(message)) return;
|
||||
|
||||
const plugin = isRecord(message.plugin) ? message.plugin : undefined;
|
||||
|
||||
return getString(message.tool_call_id) ?? getString(plugin?.id);
|
||||
};
|
||||
|
||||
const getMemoryIdsFromToolMessage = (message: unknown) => {
|
||||
if (!isRecord(message)) return;
|
||||
|
||||
const ids: Record<string, string> = {};
|
||||
const addId = (key: string, value: unknown) => {
|
||||
if (!key.endsWith('Id')) return;
|
||||
|
||||
const id = getString(value);
|
||||
if (id) ids[key] = id;
|
||||
};
|
||||
|
||||
const pluginState = isRecord(message.pluginState) ? message.pluginState : undefined;
|
||||
if (pluginState) {
|
||||
for (const [key, value] of Object.entries(pluginState)) {
|
||||
addId(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const content = getString(message.content);
|
||||
if (content) {
|
||||
for (const match of content.matchAll(/([A-Za-z]\w*Id):\s*"([^"]+)"/g)) {
|
||||
addId(match[1], match[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(ids).length > 0 ? ids : undefined;
|
||||
};
|
||||
|
||||
const getMemoryToolResultIds = (state: AgentState) => {
|
||||
const resultIds = new Map<string, Record<string, string>>();
|
||||
|
||||
for (const message of state.messages ?? []) {
|
||||
const callId = getToolMessageCallId(message);
|
||||
const ids = getMemoryIdsFromToolMessage(message);
|
||||
|
||||
if (callId && ids) resultIds.set(callId, ids);
|
||||
}
|
||||
|
||||
return resultIds;
|
||||
};
|
||||
|
||||
const getNestedString = (payload: Record<string, unknown>, keys: string[]) => {
|
||||
let current: unknown = payload;
|
||||
|
||||
for (const key of keys) {
|
||||
if (!isRecord(current)) return;
|
||||
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return getString(current);
|
||||
};
|
||||
|
||||
const getToolArgumentString = (args: Record<string, unknown>, key: string) => {
|
||||
return getString(args[key]) ?? getNestedString(args, ['set', key]);
|
||||
};
|
||||
|
||||
const createTargetFromToolArguments = (
|
||||
args: Record<string, unknown>,
|
||||
toolCall: MemoryToolCallSnapshot & { apiName: string },
|
||||
resultIds?: Record<string, string>,
|
||||
): MemoryActionTarget | undefined => {
|
||||
const title = getToolArgumentString(args, 'title');
|
||||
if (!title) return;
|
||||
|
||||
const targetConfig = MEMORY_WRITE_TARGET_BY_API_NAME[toolCall.apiName];
|
||||
const id = targetConfig ? resultIds?.[targetConfig.idKey] : undefined;
|
||||
const memoryId = resultIds?.memoryId;
|
||||
const summary =
|
||||
getToolArgumentString(args, 'summary') ??
|
||||
getToolArgumentString(args, 'details') ??
|
||||
getNestedString(args, ['withPreference', 'conclusionDirectives']);
|
||||
|
||||
return {
|
||||
...((id ?? memoryId) ? { id: id ?? memoryId } : {}),
|
||||
...(memoryId ? { memoryId } : {}),
|
||||
...(targetConfig ? { memoryLayer: targetConfig.layer } : {}),
|
||||
...(summary ? { summary } : {}),
|
||||
title,
|
||||
type: 'memory',
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveMemoryActionTargetFromState = (
|
||||
state: AgentState,
|
||||
): MemoryActionTarget | undefined => {
|
||||
const resultIds = getMemoryToolResultIds(state);
|
||||
|
||||
for (const message of [...(state.messages ?? [])].reverse()) {
|
||||
const toolCalls = getToolCallsFromMessage(message).reverse();
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!isMemoryWriteToolCall(toolCall)) continue;
|
||||
if (!toolCall.id) continue;
|
||||
|
||||
const confirmedResultIds = resultIds.get(toolCall.id);
|
||||
if (!confirmedResultIds) continue;
|
||||
|
||||
const args = parseToolArguments(toolCall.arguments);
|
||||
if (!args) continue;
|
||||
|
||||
const target = createTargetFromToolArguments(args, toolCall, confirmedResultIds);
|
||||
if (target) return target;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const runMemoryActionAgent = async (
|
||||
input: {
|
||||
agentId?: string;
|
||||
@@ -503,13 +289,7 @@ export const runMemoryActionAgent = async (
|
||||
}
|
||||
|
||||
if (hasSuccessfulMemoryWrite(finalState)) {
|
||||
const target = resolveMemoryActionTargetFromState(finalState);
|
||||
|
||||
return {
|
||||
...(target?.summary ? { detail: target.summary } : {}),
|
||||
status: 'applied',
|
||||
...(target ? { target } : {}),
|
||||
};
|
||||
return { status: 'applied' };
|
||||
}
|
||||
|
||||
if (hasFailedMemoryWrite(finalState)) {
|
||||
@@ -592,15 +372,13 @@ export const handleUserMemoryAction = async (
|
||||
topicId: typeof action.payload.topicId === 'string' ? action.payload.topicId : undefined,
|
||||
};
|
||||
const runner = options.memoryActionRunner ?? ((input) => runMemoryActionAgent(input, options));
|
||||
let memoryActionResult: MemoryAgentActionResult | undefined;
|
||||
const memoryService = createMemoryService({
|
||||
writeMemory: async () => {
|
||||
const result = await runner(runnerInput);
|
||||
memoryActionResult = result;
|
||||
|
||||
if (result.status === 'applied') {
|
||||
return {
|
||||
memoryId: result.target?.id ?? idempotencyKey ?? action.actionId,
|
||||
memoryId: idempotencyKey ?? action.actionId,
|
||||
summary: result.detail,
|
||||
};
|
||||
}
|
||||
@@ -624,7 +402,6 @@ export const handleUserMemoryAction = async (
|
||||
.then<MemoryAgentActionResult>((writeResult) => ({
|
||||
detail: writeResult.summary,
|
||||
status: 'applied',
|
||||
...(memoryActionResult?.target ? { target: memoryActionResult.target } : {}),
|
||||
}))
|
||||
.catch((error: unknown): MemoryAgentActionResult => {
|
||||
if (error instanceof MemoryActionError) {
|
||||
@@ -644,7 +421,6 @@ export const handleUserMemoryAction = async (
|
||||
actionId: action.actionId,
|
||||
attempt: finalizeAttempt(startedAt, 'succeeded'),
|
||||
detail: result.detail,
|
||||
...(result.target ? { output: { target: result.target } } : {}),
|
||||
status: 'applied',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @vitest-environment node
|
||||
import type { BaseAction, ExecutorResult } from '@lobechat/agent-signal';
|
||||
import { createSource } from '@lobechat/agent-signal';
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AGENT_SIGNAL_POLICY_ACTION_TYPES } from '../../policies/types';
|
||||
@@ -53,7 +52,7 @@ const result = (input: {
|
||||
});
|
||||
|
||||
describe('projectAgentSignalReceipts', () => {
|
||||
it('projects applied memory action results without unstructured feedback as target', () => {
|
||||
it('projects applied memory action results', () => {
|
||||
expect(
|
||||
projectAgentSignalReceipts({
|
||||
actions: [
|
||||
@@ -79,6 +78,10 @@ describe('projectAgentSignalReceipts', () => {
|
||||
sourceId: 'source-1',
|
||||
sourceType: 'client.gateway.runtime_end',
|
||||
status: 'applied',
|
||||
target: {
|
||||
title: 'Remember that future PR reviews should be decision-first.',
|
||||
type: 'memory',
|
||||
},
|
||||
title: 'Memory saved',
|
||||
topicId: 'topic-1',
|
||||
userId: 'user-1',
|
||||
@@ -86,53 +89,6 @@ describe('projectAgentSignalReceipts', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers the memory target title from action output over the feedback message', () => {
|
||||
expect(
|
||||
projectAgentSignalReceipts({
|
||||
actions: [
|
||||
action({
|
||||
actionId: 'action-memory-1',
|
||||
actionType: AGENT_SIGNAL_POLICY_ACTION_TYPES.userMemoryHandle,
|
||||
payload: {
|
||||
message:
|
||||
'<speaker id="833816919" username="nivra2000" nickname="Aa T" />\nEvery section is too short. Can it be longer?',
|
||||
},
|
||||
}),
|
||||
],
|
||||
results: [
|
||||
result({
|
||||
actionId: 'action-memory-1',
|
||||
output: {
|
||||
target: {
|
||||
id: 'preference_1',
|
||||
memoryId: 'mem_1',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'The user prefers longer, more developed answer sections.',
|
||||
title: 'Preference for detailed answer sections',
|
||||
type: 'memory',
|
||||
},
|
||||
},
|
||||
status: 'applied',
|
||||
}),
|
||||
],
|
||||
source,
|
||||
userId: 'user-1',
|
||||
}),
|
||||
).toMatchObject([
|
||||
{
|
||||
kind: 'memory',
|
||||
target: {
|
||||
id: 'preference_1',
|
||||
memoryId: 'mem_1',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'The user prefers longer, more developed answer sections.',
|
||||
title: 'Preference for detailed answer sections',
|
||||
type: 'memory',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('projects applied skill-management results as updated skill receipts', () => {
|
||||
expect(
|
||||
projectAgentSignalReceipts({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AgentSignalSource, BaseAction, ExecutorResult } from '@lobechat/agent-signal';
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
|
||||
import { AGENT_SIGNAL_DEFAULTS } from '../constants';
|
||||
import { AGENT_SIGNAL_POLICY_ACTION_TYPES } from '../policies/types';
|
||||
@@ -94,10 +93,6 @@ export interface AgentSignalReceipt {
|
||||
documentId?: string;
|
||||
/** Backing resource id for future navigation when still available. Skill ids use `documents.id`. */
|
||||
id?: string;
|
||||
/** User memory base id for audit and fallback lookup. */
|
||||
memoryId?: string;
|
||||
/** User memory layer used to route memory receipts to their detail page. */
|
||||
memoryLayer?: LayersEnum;
|
||||
/** Short summary captured at write time. */
|
||||
summary?: string;
|
||||
/** Human-readable resource title captured at write time. */
|
||||
@@ -241,10 +236,6 @@ const getPayloadString = (payload: Record<string, unknown>, key: string) => {
|
||||
const getClampedString = (value: string, maxLength = 96) =>
|
||||
value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value;
|
||||
|
||||
const isMemoryLayer = (value: unknown): value is LayersEnum => {
|
||||
return Object.values(LayersEnum).includes(value as LayersEnum);
|
||||
};
|
||||
|
||||
const getReceiptTarget = (
|
||||
action: BaseAction,
|
||||
result: ExecutorResult,
|
||||
@@ -271,12 +262,6 @@ const getReceiptTarget = (
|
||||
? { documentId: payload.documentId }
|
||||
: {}),
|
||||
...(typeof payload.id === 'string' && payload.id.length > 0 ? { id: payload.id } : {}),
|
||||
...(type === 'memory' && typeof payload.memoryId === 'string' && payload.memoryId.length > 0
|
||||
? { memoryId: payload.memoryId }
|
||||
: {}),
|
||||
...(type === 'memory' && isMemoryLayer(payload.memoryLayer)
|
||||
? { memoryLayer: payload.memoryLayer }
|
||||
: {}),
|
||||
...(typeof payload.summary === 'string' && payload.summary.length > 0
|
||||
? { summary: payload.summary }
|
||||
: {}),
|
||||
@@ -287,6 +272,14 @@ const getReceiptTarget = (
|
||||
}
|
||||
|
||||
if (kind !== 'memory') return;
|
||||
|
||||
const message = getPayloadString(action.payload, 'message')?.trim();
|
||||
if (!message) return;
|
||||
|
||||
return {
|
||||
title: getClampedString(message),
|
||||
type: 'memory',
|
||||
};
|
||||
};
|
||||
|
||||
const toReceiptKind = (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @vitest-environment node
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -28,9 +27,6 @@ const receipt = {
|
||||
sourceType: 'client.gateway.runtime_end',
|
||||
status: 'applied' as const,
|
||||
target: {
|
||||
id: 'preference-1',
|
||||
memoryId: 'memory-1',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Use short answers in future chats',
|
||||
title: 'Short answer preference',
|
||||
type: 'memory' as const,
|
||||
@@ -65,9 +61,6 @@ describe('redis receipt store', () => {
|
||||
sourceType: 'client.gateway.runtime_end',
|
||||
status: 'applied',
|
||||
target: JSON.stringify({
|
||||
id: 'preference-1',
|
||||
memoryId: 'memory-1',
|
||||
memoryLayer: LayersEnum.Preference,
|
||||
summary: 'Use short answers in future chats',
|
||||
title: 'Short answer preference',
|
||||
type: 'memory',
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { LayersEnum } from '@lobechat/types';
|
||||
|
||||
import { AGENT_SIGNAL_KEYS } from '../../../constants';
|
||||
import type { AgentSignalReceipt } from '../../../services/receiptService';
|
||||
import type { AgentSignalReceiptStore } from '../../types';
|
||||
@@ -23,9 +21,6 @@ const toReceiptHash = (receipt: AgentSignalReceipt): Record<string, string> => (
|
||||
userId: receipt.userId,
|
||||
});
|
||||
|
||||
const isMemoryLayer = (value: unknown): value is LayersEnum =>
|
||||
Object.values(LayersEnum).includes(value as LayersEnum);
|
||||
|
||||
const parseReceiptTarget = (value?: string): AgentSignalReceipt['target'] | undefined => {
|
||||
if (!value) return;
|
||||
|
||||
@@ -43,14 +38,6 @@ const parseReceiptTarget = (value?: string): AgentSignalReceipt['target'] | unde
|
||||
? { documentId: target.documentId }
|
||||
: {}),
|
||||
...(typeof target.id === 'string' && target.id.length > 0 ? { id: target.id } : {}),
|
||||
...(target.type === 'memory' &&
|
||||
typeof target.memoryId === 'string' &&
|
||||
target.memoryId.length > 0
|
||||
? { memoryId: target.memoryId }
|
||||
: {}),
|
||||
...(target.type === 'memory' && isMemoryLayer(target.memoryLayer)
|
||||
? { memoryLayer: target.memoryLayer }
|
||||
: {}),
|
||||
...(typeof target.summary === 'string' && target.summary.length > 0
|
||||
? { summary: target.summary }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user