Compare commits

...

7 Commits

Author SHA1 Message Date
Neko Ayaka 6a37972483 👷 build(database): added parent id and filename columns for agent_documents 2026-04-27 19:08:19 +08:00
lobehubbot 376976849b 🔖 chore(release): release version v2.1.53 [skip ci] 2026-04-27 05:20:52 +00:00
Arvin Xu a52104552a 🚀 release: 20260427 (#14217)
# 🚀 LobeHub v2.1.53 (20260427)

**Release Date:** April 27, 2026
**Since v2.1.52:** 194 merged PRs · 17 contributors

> Introduce Heterogeneous Agent — Claude Code and Codex run as
first-class desktop runtimes, paired with a new Agent Signal package,
sharper desktop UX, and a wave of flagship model additions.

---

##  Highlights

- **Introduce Heterogeneous Agent** — Claude Code and Codex run as
first-class desktop agents: subagent rendering, partial-message
streaming, multi-turn resume, terminal error surfacing, rich tool
inspectors, and runtime polish. (#14162, #13754, #14067, #14001, #13970,
#13942)
- **Screen capture & Quick Chat tray** — New desktop screen capture
overlay (macOS permission-gated) with Quick Chat tray and upload
pipeline improvements; chat input auto-focuses on overlay mount.
(#13818, #14097, #14105)
- **Desktop topic & tab UX** — Dedicated topic popup window with
cross-window sync, Cmd+W/Cmd+T tab shortcuts, TabBar polish, recent
working directories expanded to 20, and human approval notifications.
(#13957, #13983, #13972, #14036, #14092)
- **Git workflow built-in** — One-click pull/push from the branch chip,
ahead/behind badge, and submodule/worktree repo detection. (#14041,
#13980, #13978)
- **Agent Signal package** — New `@lobechat/agent-signal` runtime for
dynamic memory feedback signals, with OTel metrics and self-iteration in
Lab. (#14157, #14170, #14159, #14169, #14187)
- **New models** — Claude Opus 4.7 with `xhigh` effort tier, GPT-5.5,
DeepSeek V4 Flash/Pro with reasoning slider, Kimi K2.6, MiMo-V2.5/Pro,
gpt-image-2, Qwen3.6 Flash/Plus, and Pixverse-c1. (#13903, #14147,
#14114, #14004, #14089, #14039, #13923)
- **New providers** — OpenCode Zen, OpenCode Go, and Azure OpenAI Router
runtime. (#13943, #14064, #13823)
- **Mobile settings overhaul** — Full settings menu and responsive
profile layout for mobile. (#14019)

---

## 🏗️ Heterogeneous Agent

- Claude Code runtime, working-directory awareness, and sidebar polish.
(#13970)
- CC subagent rendering with persistent streamed text; parallel-tool
orphan fix. (#14001, #13968, #14024)
- Per-step usage persisted to each step assistant message. (#13964)
- Per-phase workflow expand defaults; full-expand toggle with
three-level expansion. (#14171, #13906)
- Hetero-mode actions bar; tool inspector polish. (#13963, #14034,
#14030)
- Codex desktop integration with rich tool rendering and devtools
preview. (#14067, #14100)
- Codex terminal error surfacing and CLI output tracing. (#14166)
- Tighten `isCanUseVision` default and add aggregator fallback. (#14172)
- Persist `ccSessionId` in topic metadata for CC multi-turn resume.
(#13902)
- CC account card, topic filter, and integration polish. (#13955,
#13942, #13950)
- Token-level deltas streamed via `--include-partial-messages`. (#13929)

---

## 🧠 Agent Signal & Self-Iteration

- New `@lobechat/agent-signal` package with dynamic feedback signals.
(#14157)
- AgentSignalRuntime wired through agent-tracing and observability-otel
metrics. (#14170, #14159)
- Self-iteration feature flag added to Lab; front-side flag check.
(#14169, #14186)
- Signal policy for receiving memory feedback dynamically. (#14187)

---

## 💬 Conversation

- Queue follow-up sends during running CC turns. (#14179)
- Persist per-topic chat scroll position; pin user message + fold long
messages. (#14191, #14056)
- Inline resend when editing last user message. (#14080)
- Disable first-block markdown streaming to prevent flicker. (#14193,
#13904)
- Prevent Markdown stream replay when vlist remounts streaming items.
(#14086)
- Stop repinning after manual scroll; unify scroll-to-user + spacer
hooks. (#14099, #14132)

---

## 📱 Platforms & Integrations

### Desktop / Electron

- Screen capture overlay, Quick Chat tray, and upload pipeline
improvements. (#13818)
- macOS permission gate for screen capture; auto-focus chat panel input.
(#14097, #14105)
- Dedicated topic popup window with cross-window sync. (#13957)
- TabBar polish: `+` button for new topic, dark theme blend, close icon
by default. (#13972, #14203, #13973)
- Recent working directories expanded from 5 to 20; submodule/worktree
repo detection. (#14036, #13978)
- Cmd+W / Cmd+T tab shortcuts and global shortcut consolidation.
(#13983, #13880)
- Linux icon configuration; human approval desktop notifications.
(#14042, #14092)

### Git Workflow

- One-click pull/push from branch chip; ahead/behind badge with
refactored GitCtr. (#14041, #13980)

### Mobile

- Full settings menu and responsive profile layout. (#14019)
- Agent route added to mobile router; mobile agent topic route
registered. (#14103, #14158)
- Session list skeleton row layout corrected. (#14040)

### Bot / Messaging

- DM strategy support; bot emoji and markdown render optimization.
(#14201, #14091, #14140)
- Slack webhook fix; bot platform setup guide reference. (#14052,
#14121)

---

## 🤖 Models & Providers

### New models

- **Claude Opus 4.7** with `xhigh` effort tier; strip temperature/top_p.
(#13903, #13909)
- **GPT-5.5**. (#14147)
- **DeepSeek V4** Flash/Pro cards with reasoning slider; cache-hit and
Pro discount pricing. (#14114, #14209, #14196, #14131)
- **Kimi K2.6** model with LobeHub-hosted card. (#14004, #14006)
- **MiMo-V2.5 / V2.5-Pro**. (#14089)
- **gpt-image-2**, **Qwen3.6 Flash/Plus**, **Pixverse-c1**. (#14039,
#13923)

### New providers

- **OpenCode Zen** and **OpenCode Go** with env-var support. (#13943,
#14064)
- **Azure OpenAI Router** runtime support. (#13823)
- Model alias mapping for image and video runtimes. (#13896)
- Seedance video models migrated to Dreamina. (#14144)

### Runtime reliability

- Sanitize invalid tool_call arguments to unbreak strict providers.
(#14033)
- Tolerate null `function.name` in streaming tool_call deltas. (#14139)
- Preserve Gemini 3 `thoughtSignature` in `call_tools_batch`
normalization. (#14032)
- Downgrade `image_url` parts when target model lacks vision. (#14029)
- Preserve Cloudflare provider error context. (#14136)
- Use `safety_identifier` for OpenAI Responses API. (#14148)
- Unwrap underlying PG error in `formatErrorEventData`. (#14038)

---

## 🖥️ User Experience

- **Onboarding** — Preset agent naming suggestions, structured hunk ops
for `updateDocument`, persona analytics snapshot, footer promotion
pipeline, wrap-up button. (#13931, #13989, #13930, #13853, #13934)
- **Document workflow** — Agent documents promoted as primary workspace
panel; history management and compare workflow; web-crawl docs
associated with agent documents. (#13924, #13725, #13893)
- **cmdk** — Agent identity surfaced on topic search results;
topic/message search scoped to current agent. (#14204, #13960)
- **Floating chat panel** and workspace improvements. (#13887)
- **Topic completion status** with dropdown action and filter. (#14005)

---

## 🔧 Tooling

- Redis-backed feature flag provider for runtime config. (#14098)
- Vite upgraded to 8.0.0 with Rolldown strict execution order. (#12720,
#14058)
- `@lobechat/model-bank` automated npm release with provenance. (#14015,
#14017, #14018)
- Skill activation fallback when `activateTools` cannot find identifier.
(#14010)
- Cron tool: timezone and existing jobs injected into system prompt;
clarified `lobe-gtd` and `lobe-cron` descriptions. (#14012, #14013)

---

## 🔒 Security & Reliability

- **Security:** uuid bumped to v14 (advisory). (#14083)
- **Security:** validate avatar URL and scope old-avatar deletion to
owner. (#13982)
- **Security:** clear OIDC sessions on better-auth signout; return 401
(not 500) for expired OIDC JWT. (#13916, #14014)
- **Reliability:** scope pending-approval check to current assistant
turn. (#14182)
- **Reliability:** sanitize heterogeneous-agent attachment cache
filenames. (#13937)
- **Reliability:** reduce subagent task status error noise. (#14026)

---

## 👥 Contributors

Huge thanks to **17 contributors** who shipped **194 merged PRs** this
week.

@Hardy · @shaun0927 · @hezhijie0327 · @sxjeru · @arvinxx · @Innei ·
@tjx666 · @LiJian · @Neko · @Rdmclin2 · @AmAzing129 · @sudongyuer ·
@CanisMinor · @rivertwilight

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**:
https://github.com/lobehub/lobehub/compare/v2.1.52...v2.1.53
2026-04-27 13:18:26 +08:00
YuTengjing 57781850ce feat(notification): add i18n keys for scheduled task failure (#14088) 2026-04-27 10:26:55 +08:00
LiJian a101957715 fix(activator): add Klavis service triggers to lobe-creds activation rules (#14134)
When users mention Klavis-managed services (Notion, Slack, Google Drive,
Airtable, Jira, Figma, etc.), the activator now recognizes these as
credential/connection intents and activates lobe-creds automatically.
This enables the full Klavis OAuth flow to be triggered inline without
requiring the user to manually navigate to settings.

Related to #14090
2026-04-27 10:26:28 +08:00
YuTengjing 4e309e6f26 🐛 fix: update DeepSeek cache hit pricing (#14209) 2026-04-27 01:21:53 +08:00
lobehubbot 57e3940bc6 🔖 chore(release): release version v2.1.52 [skip ci] 2026-04-20 09:36:46 +00:00
23 changed files with 15095 additions and 47 deletions
+25
View File
@@ -2,6 +2,31 @@
# Changelog
### [Version 2.1.52](https://github.com/lobehub/lobe-chat/compare/v2.1.51...v2.1.52)
<sup>Released on **2026-04-20**</sup>
#### 👷 Build System
- **database**: add topic status and tasks automation mode.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Build System
- **database**: add topic status and tasks automation mode, closes [#13994](https://github.com/lobehub/lobe-chat/issues/13994) ([3bcd581](https://github.com/lobehub/lobe-chat/commit/3bcd581))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.1.51](https://github.com/lobehub/lobe-chat/compare/v0.0.0-nightly.pr13850.8503...v2.1.51)
<sup>Released on **2026-04-16**</sup>
+5
View File
@@ -1,4 +1,9 @@
[
{
"children": {},
"date": "2026-04-20",
"version": "2.1.52"
},
{
"children": {
"fixes": ["fix minify cli.", "recent delete."]
+3
View File
@@ -129,6 +129,8 @@ table agent_documents {
user_id text [not null]
agent_id text [not null]
document_id varchar(255) [not null]
parent_id uuid
filename text
template_id varchar(100)
access_self integer [not null, default: 31]
access_shared integer [not null, default: 0]
@@ -160,6 +162,7 @@ table agent_documents {
deleted_at [name: 'agent_documents_deleted_at_idx']
(agent_id, deleted_at, policy_load) [name: 'agent_documents_agent_autoload_deleted_idx']
document_id [name: 'agent_documents_document_id_idx']
parent_id [name: 'agent_documents_parent_id_idx']
(agent_id, document_id, user_id) [name: 'agent_documents_agent_document_user_unique', unique]
}
}
+9 -1
View File
@@ -1,10 +1,18 @@
{
"agent_cron_job_failed": "Your scheduled task \"{{jobName}}\" failed. Open the task to see the full error.",
"agent_cron_job_failed_insufficient_budget": "Your scheduled task \"{{jobName}}\" couldn't run because your account is out of credits. Top up or upgrade your plan to resume future runs.",
"agent_cron_job_failed_insufficient_budget_title": "Scheduled task paused: insufficient credits",
"agent_cron_job_failed_title": "Scheduled task failed",
"billboard.learnMore": "Learn more",
"billboard.menuLabel": "Announcements",
"image_generation_completed": "Your image \"{{prompt}}\" is ready.",
"image_generation_completed_title": "Image generation completed",
"inbox.archiveAll": "Archive all",
"inbox.empty": "No notifications yet",
"inbox.emptyUnread": "No unread notifications",
"inbox.filterUnread": "Show unread only",
"inbox.markAllRead": "Mark all as read",
"inbox.title": "Notifications"
"inbox.title": "Notifications",
"video_generation_completed": "Your video \"{{prompt}}\" is ready.",
"video_generation_completed_title": "Video generation completed"
}
+7
View File
@@ -447,11 +447,18 @@
"myAgents.status.published": "Published",
"myAgents.status.unpublished": "Unpublished",
"myAgents.title": "My Published Agents",
"notification.category.generation.desc": "Image and video completion notifications",
"notification.category.generation.title": "Generation",
"notification.category.schedule.desc": "Scheduled agent run failures and pauses",
"notification.category.schedule.title": "Scheduled tasks",
"notification.email.desc": "Receive email notifications when important events occur",
"notification.email.title": "Email Notifications",
"notification.enabled": "Enabled",
"notification.inbox.desc": "Show notifications in the in-app inbox",
"notification.inbox.title": "Inbox Notifications",
"notification.item.agent_cron_job_failed": "Scheduled task failures",
"notification.item.image_generation_completed": "Image generation completed",
"notification.item.video_generation_completed": "Video generation completed",
"notification.title": "Notification Channels",
"plugin.addMCPPlugin": "Add MCP",
"plugin.addTooltip": "Custom Skills",
+9 -1
View File
@@ -1,10 +1,18 @@
{
"agent_cron_job_failed": "定时任务「{{jobName}}」执行失败,点击查看详情。",
"agent_cron_job_failed_insufficient_budget": "定时任务「{{jobName}}」因账户额度不足未能执行。请充值或升级套餐以恢复后续运行。",
"agent_cron_job_failed_insufficient_budget_title": "定时任务已暂停:额度不足",
"agent_cron_job_failed_title": "定时任务执行失败",
"billboard.learnMore": "了解更多",
"billboard.menuLabel": "公告",
"image_generation_completed": "图片「{{prompt}}」已生成。",
"image_generation_completed_title": "图片生成完成",
"inbox.archiveAll": "全部归档",
"inbox.empty": "暂无通知",
"inbox.emptyUnread": "没有未读通知",
"inbox.filterUnread": "仅显示未读",
"inbox.markAllRead": "全部标为已读",
"inbox.title": "通知"
"inbox.title": "通知",
"video_generation_completed": "视频「{{prompt}}」已生成。",
"video_generation_completed_title": "视频生成完成"
}
+7
View File
@@ -454,11 +454,18 @@
"myAgents.status.published": "已上架",
"myAgents.status.unpublished": "未上架",
"myAgents.title": "我发布的助理",
"notification.category.generation.desc": "图片和视频生成完成通知",
"notification.category.generation.title": "生成",
"notification.category.schedule.desc": "定时助理运行失败和暂停通知",
"notification.category.schedule.title": "定时任务",
"notification.email.desc": "当重要事件发生时接收邮件通知",
"notification.email.title": "邮件通知",
"notification.enabled": "启用",
"notification.inbox.desc": "在应用内收件箱中显示通知",
"notification.inbox.title": "站内通知",
"notification.item.agent_cron_job_failed": "定时任务执行失败",
"notification.item.image_generation_completed": "图片生成完成",
"notification.item.video_generation_completed": "视频生成完成",
"notification.title": "通知渠道",
"plugin.addMCPPlugin": "添加 MCP",
"plugin.addTooltip": "自定义技能",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/lobehub",
"version": "2.1.51",
"version": "2.1.53",
"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",
@@ -59,6 +59,12 @@ export const systemPrompt = `You have access to a Tools Activator that allows yo
- User wants to store or manage sensitive information securely
- Sandbox code execution requires credentials/secrets to be injected
- User asks to connect to services like GitHub, Linear, Twitter, Microsoft, etc.
- User wants to use, open, connect, or interact with a third-party integration service
(e.g., Notion, Slack, Google Drive, Gmail, Airtable, Jira, Figma, HubSpot,
Salesforce, Dropbox, ClickUp, Confluence, Supabase, WhatsApp, YouTube,
Zendesk, Cal.com, OneDrive, Outlook Mail, Google Sheets, Google Docs)
- User says things like "help me use Notion", "connect my Slack", "open Google Drive",
"I want to use Jira", "set up Airtable" — these are Klavis-managed OAuth services
**Decision flow:**
1. **If ANY trigger condition above is met** → Immediately activate \`lobe-creds\`
@@ -66,6 +72,10 @@ export const systemPrompt = `You have access to a Tools Activator that allows yo
3. If credential exists → use \`getPlaintextCred\` or \`injectCredsToSandbox\` (for sandbox execution)
4. If credential doesn't exist:
- For OAuth services (GitHub, Linear, Microsoft, Twitter) → use \`initiateOAuthConnect\`
- For Klavis-managed services (Notion, Slack, Google Drive, Airtable, Jira, etc.)
→ use \`connectKlavisService\` after activating \`lobe-creds\`. The full list of
available Klavis services is shown in \`<klavis_integrations>\` inside the
lobe-creds system prompt.
- For API keys/tokens → guide user to save with \`saveCreds\`
5. For sandbox code that needs credentials → use \`injectCredsToSandbox\` to inject them as environment variables
@@ -0,0 +1,5 @@
ALTER TABLE "agent_documents" ADD COLUMN IF NOT EXISTS "parent_id" uuid;--> statement-breakpoint
ALTER TABLE "agent_documents" ADD COLUMN IF NOT EXISTS "filename" text;--> statement-breakpoint
ALTER TABLE "agent_documents" DROP CONSTRAINT IF EXISTS "agent_documents_parent_id_agent_documents_id_fk";--> statement-breakpoint
ALTER TABLE "agent_documents" ADD CONSTRAINT "agent_documents_parent_id_agent_documents_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."agent_documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "agent_documents_parent_id_idx" ON "agent_documents" USING btree ("parent_id");
File diff suppressed because it is too large Load Diff
@@ -700,6 +700,13 @@
"when": 1776674965365,
"tag": "0099_topic_status_tasks_automation_mode",
"breakpoints": true
},
{
"idx": 100,
"version": "7",
"when": 1777287925574,
"tag": "0100_add_parent_id_and_filename_for_agent_documents",
"breakpoints": true
}
],
"version": "6"
@@ -1,3 +1,4 @@
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
import {
index,
integer,
@@ -61,6 +62,22 @@ export const agentDocuments = pgTable(
documentId: varchar('document_id', { length: 255 })
.notNull()
.references(() => documents.id, { onDelete: 'cascade' }),
/**
* Parent VFS entry inside the same agent document tree.
*
* Null means the entry is at the ordinary VFS root. This references
* `agent_documents.id`, not `documents.id`.
*/
parentId: uuid('parent_id').references((): AnyPgColumn => agentDocuments.id, {
onDelete: 'cascade',
}),
/**
* Ordinary VFS segment name owned by this agent document entry.
*
* This starts nullable for the schema-expansion PR and becomes non-null in the
* backfill/constraint PR after existing rows are migrated.
*/
filename: text('filename'),
/**
* Template source label (e.g. 'claw', 'custom').
@@ -171,6 +188,7 @@ export const agentDocuments = pgTable(
table.policyLoad,
),
index('agent_documents_document_id_idx').on(table.documentId),
index('agent_documents_parent_id_idx').on(table.parentId),
uniqueIndex('agent_documents_agent_document_user_unique').on(
table.agentId,
table.documentId,
+9 -5
View File
@@ -17,8 +17,9 @@ const deepseekChatModels: AIChatModelCard[] = [
maxOutput: 384_000,
pricing: {
currency: 'CNY',
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.02, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 1, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -50,9 +51,10 @@ const deepseekChatModels: AIChatModelCard[] = [
maxOutput: 384_000,
pricing: {
currency: 'CNY',
// Official limited-time 75% off discount is valid until 2026-05-05 23:59 Beijing time.
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
// DeepSeek V4 Pro limited-time 75% off discount is valid until 2026-05-05 23:59 Beijing time.
units: [
{ name: 'textInput_cacheRead', rate: 0.25, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.025, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 6, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -86,8 +88,9 @@ const deepseekChatModels: AIChatModelCard[] = [
maxOutput: 384_000,
pricing: {
currency: 'CNY',
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.02, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 1, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -111,8 +114,9 @@ const deepseekChatModels: AIChatModelCard[] = [
maxOutput: 384_000,
pricing: {
currency: 'CNY',
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.02, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 1, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -14,8 +14,9 @@ export const deepseekChatModels: AIChatModelCard[] = [
id: 'deepseek-v4-flash',
maxOutput: 384_000,
pricing: {
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.0028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 0.14, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 0.28, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -45,9 +46,10 @@ export const deepseekChatModels: AIChatModelCard[] = [
id: 'deepseek-v4-pro',
maxOutput: 384_000,
pricing: {
// Official limited-time 75% off discount is valid until 2026-05-05 15:59 UTC.
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
// DeepSeek V4 Pro limited-time 75% off discount is valid until 2026-05-05 15:59 UTC.
units: [
{ name: 'textInput_cacheRead', rate: 0.03625, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.003625, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 0.435, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 0.87, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -79,8 +81,9 @@ export const deepseekChatModels: AIChatModelCard[] = [
legacy: true,
maxOutput: 384_000,
pricing: {
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.0028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 0.14, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 0.28, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -103,8 +106,9 @@ export const deepseekChatModels: AIChatModelCard[] = [
legacy: true,
maxOutput: 384_000,
pricing: {
// Official cache-hit input price is permanently reduced to 1/10 of the launch price.
units: [
{ name: 'textInput_cacheRead', rate: 0.028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput_cacheRead', rate: 0.0028, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 0.14, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 0.28, strategy: 'fixed', unit: 'millionTokens' },
],
@@ -19,8 +19,7 @@ import { type RuntimeVideoGenParams } from 'model-bank';
import { NextResponse } from 'next/server';
import { chargeAfterGenerate } from '@/business/server/video-generation/chargeAfterGenerate';
// TODO: temporarily disabled until notification UI is polished
// import { notifyVideoCompleted } from '@/business/server/video-generation/notifyVideoCompleted';
import { notifyVideoCompleted } from '@/business/server/video-generation/notifyVideoCompleted';
import { AsyncTaskModel } from '@/database/models/asyncTask';
import { GenerationModel } from '@/database/models/generation';
import { generationBatches } from '@/database/schemas';
@@ -217,14 +216,17 @@ export const POST = async (req: Request, { params }: { params: Promise<{ provide
status: AsyncTaskStatus.Success,
});
// TODO: temporarily disabled until notification UI is polished
// notifyVideoCompleted({
// generationBatchId: generation.generationBatchId!,
// model: requestedModel,
// prompt: batch?.prompt ?? '',
// topicId: batch?.generationTopicId,
// userId: asyncTask.userId,
// }).catch((err) => console.error('[video-webhook] notification failed:', err));
try {
await notifyVideoCompleted({
generationBatchId: generation.generationBatchId!,
model: requestedModel,
prompt: batch?.prompt ?? '',
topicId: batch?.generationTopicId,
userId: asyncTask.userId,
});
} catch (err) {
console.error('[video-webhook] notification failed:', err);
}
// Charge after successful video generation
try {
+10
View File
@@ -1,10 +1,20 @@
export default {
'agent_cron_job_failed':
'Your scheduled task "{{jobName}}" failed. Open the task to see the full error.',
'agent_cron_job_failed_insufficient_budget':
'Your scheduled task "{{jobName}}" couldn\'t run because your account is out of credits. Top up or upgrade your plan to resume future runs.',
'agent_cron_job_failed_insufficient_budget_title': 'Scheduled task paused: insufficient credits',
'agent_cron_job_failed_title': 'Scheduled task failed',
'billboard.learnMore': 'Learn more',
'billboard.menuLabel': 'Announcements',
'image_generation_completed': 'Your image "{{prompt}}" is ready.',
'image_generation_completed_title': 'Image generation completed',
'inbox.archiveAll': 'Archive all',
'inbox.empty': 'No notifications yet',
'inbox.emptyUnread': 'No unread notifications',
'inbox.filterUnread': 'Show unread only',
'inbox.markAllRead': 'Mark all as read',
'inbox.title': 'Notifications',
'video_generation_completed': 'Your video "{{prompt}}" is ready.',
'video_generation_completed_title': 'Video generation completed',
};
+7
View File
@@ -468,6 +468,13 @@ export default {
'notification.email.title': 'Email Notifications',
'notification.inbox.desc': 'Show notifications in the in-app inbox',
'notification.inbox.title': 'Inbox Notifications',
'notification.category.generation.desc': 'Image and video completion notifications',
'notification.category.generation.title': 'Generation',
'notification.category.schedule.desc': 'Scheduled agent run failures and pauses',
'notification.category.schedule.title': 'Scheduled tasks',
'notification.item.agent_cron_job_failed': 'Scheduled task failures',
'notification.item.image_generation_completed': 'Image generation completed',
'notification.item.video_generation_completed': 'Video generation completed',
'notification.title': 'Notification Channels',
'myAgents.actions.cancel': 'Cancel',
'myAgents.actions.confirmDeprecate': 'Confirm Deprecate',
@@ -2,7 +2,7 @@ import { isDesktop } from '@lobechat/const';
import { Avatar } from '@lobehub/ui';
import { SkillsIcon } from '@lobehub/ui/icons';
import {
// BellIcon,
BellIcon,
Brain,
BrainCircuit,
ChartColumnBigIcon,
@@ -101,12 +101,11 @@ export const useCategory = () => {
key: SettingsTabs.Hotkey,
label: t('tab.hotkey'),
},
// TODO: temporarily disabled until notification UI is polished
// enableBusinessFeatures && {
// icon: BellIcon,
// key: SettingsTabs.Notification,
// label: t('tab.notification'),
// },
enableBusinessFeatures && {
icon: BellIcon,
key: SettingsTabs.Notification,
label: t('tab.notification'),
},
].filter(Boolean) as CategoryItem[];
groups.push({
+13 -11
View File
@@ -17,8 +17,7 @@ import { type RuntimeImageGenParams } from 'model-bank';
import { z } from 'zod';
import { chargeAfterGenerate } from '@/business/server/image-generation/chargeAfterGenerate';
// TODO: temporarily disabled until notification UI is polished
// import { notifyImageCompleted } from '@/business/server/image-generation/notifyImageCompleted';
import { notifyImageCompleted } from '@/business/server/image-generation/notifyImageCompleted';
import { createImageBusinessMiddleware } from '@/business/server/trpc-middlewares/async';
import { AsyncTaskModel } from '@/database/models/asyncTask';
import { FileModel } from '@/database/models/file';
@@ -376,15 +375,18 @@ export const imageRouter = router({
status: AsyncTaskStatus.Success,
});
// TODO: temporarily disabled until notification UI is polished
// notifyImageCompleted({
// duration,
// generationBatchId,
// model,
// prompt: params.prompt,
// topicId: generationTopicId,
// userId: ctx.userId,
// }).catch((err) => console.error('[image-async] notification failed:', err));
try {
await notifyImageCompleted({
duration,
generationBatchId,
model,
prompt: params.prompt,
topicId: generationTopicId,
userId: ctx.userId,
});
} catch (err) {
console.error('[image-async] notification failed:', err);
}
if (ENABLE_BUSINESS_FEATURES) {
await chargeAfterGenerate({
@@ -72,6 +72,26 @@ async function fetchDeliver(url: string, payload: Record<string, unknown>): Prom
}
}
function buildWebhookPayload(
event: AnyHookEvent,
eventFields?: (keyof AgentHookEvent)[],
): Record<string, unknown> {
if (eventFields) {
const payload: Record<string, unknown> = {};
for (const field of eventFields) {
if (field === 'finalState') continue;
if (field in event) payload[field] = event[field as keyof AnyHookEvent];
}
return payload;
}
const payload = { ...event };
if ('finalState' in payload) {
delete (payload as { finalState?: unknown }).finalState;
}
return payload;
}
/**
* HookDispatcher — central hub for registering and dispatching agent lifecycle hooks
*
@@ -129,12 +149,7 @@ export class HookDispatcher {
hook.id,
hook.webhook.url,
);
// Strip finalState from webhook payload (too large, local-only)
// Webhook delivery only applies to step-level hooks (AgentHookEvent)
const webhookPayload = { ...event };
if ('finalState' in webhookPayload) {
delete (webhookPayload as { finalState?: unknown }).finalState;
}
const webhookPayload = buildWebhookPayload(event, hook.webhook.eventFields);
await deliverWebhook(hook.webhook, {
...webhookPayload,
hookId: hook.id,
@@ -189,6 +189,47 @@ describe('HookDispatcher', () => {
expect(body.hookId).toBe('custom-body-hook');
});
it('should only include selected event fields when eventFields is set', async () => {
dispatcher.register(operationId, [
{
handler: vi.fn(),
id: 'projected-hook',
type: 'onError',
webhook: {
body: { taskId: 'task_123' },
eventFields: ['errorMessage', 'reason', 'topicId'],
url: 'https://example.com/hook',
},
},
]);
const serialized = dispatcher.getSerializedHooks(operationId);
await dispatcher.dispatch(
operationId,
'onError',
makeEvent({
errorDetail: 'internal raw error',
errorMessage: 'Public error',
finalState: { status: 'error' },
lastAssistantContent: 'private assistant output',
reason: 'error',
topicId: 'topic_123',
}),
serialized,
);
const call = vi.mocked(global.fetch).mock.calls[0];
const body = JSON.parse(call[1]?.body as string);
expect(body).toEqual({
errorMessage: 'Public error',
hookId: 'projected-hook',
hookType: 'onError',
reason: 'error',
taskId: 'task_123',
topicId: 'topic_123',
});
});
it('should not call local handler in production mode', async () => {
const handler = vi.fn();
dispatcher.register(operationId, [
@@ -38,6 +38,9 @@ export interface AgentHookWebhook {
/** Delivery method: 'fetch' (plain HTTP) or 'qstash' (guaranteed delivery). Default: 'qstash' */
delivery?: 'fetch' | 'qstash';
/** Event fields to include in the webhook payload. Defaults to all serializable event fields. */
eventFields?: (keyof AgentHookEvent)[];
/** Webhook endpoint URL (relative or absolute) */
url: string;
}