mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-17 04:55:51 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a37972483 | |||
| 376976849b | |||
| a52104552a | |||
| 57781850ce | |||
| a101957715 | |||
| 4e309e6f26 | |||
| 57e3940bc6 |
@@ -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">
|
||||
|
||||
[](#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>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2026-04-20",
|
||||
"version": "2.1.52"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"fixes": ["fix minify cli.", "recent delete."]
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "视频生成完成"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user