mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🔨 chore: pre-merge group chat relative implement (#9432)
* pre-merge code * fix tests * fix circular * remove redirectUri * fix types * improve sql * fix docs * fix lint * update model runtime
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: Explain how group chat works in LobeHub (Multi-agent orchestratoin)
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
This rule explains how group chat (multi-agent orchestration) works. Not confused with session group, which is a organization method to manage session.
|
||||
|
||||
## Key points
|
||||
|
||||
- A supervisor will devide who and how will speak next
|
||||
- Each agent will speak just like in single chat (if was asked to speak)
|
||||
- Not coufused with session group
|
||||
|
||||
## Related Files
|
||||
|
||||
- src/store/chat/slices/message/supervisor.ts
|
||||
- src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts
|
||||
- src/prompts/groupChat/index.ts (All prompts here)
|
||||
|
||||
## Snippets
|
||||
|
||||
```tsx
|
||||
// Detect whether in group chat
|
||||
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
||||
|
||||
// Member actions
|
||||
const addAgentsToGroup = useChatGroupStore((s) => s.addAgentsToGroup);
|
||||
const removeAgentFromGroup = useChatGroupStore((s) => s.removeAgentFromGroup);
|
||||
const persistReorder = useChatGroupStore((s) => s.reorderGroupMembers);
|
||||
|
||||
// Get group info
|
||||
const groupConfig = useChatGroupStore(chatGroupSelectors.currentGroupConfig);
|
||||
const currentGroupMemebers = useSessionStore(sessionSelectors.currentGroupAgents);
|
||||
```
|
||||
@@ -819,7 +819,7 @@ Every bit counts and your one-time donation sparkles in our galaxy of support! Y
|
||||
</details>
|
||||
|
||||
Copyright © 2025 [LobeHub][profile-link]. <br />
|
||||
This project is [Apache 2.0](./LICENSE) licensed.
|
||||
This project is [LobeHub Community License](./LICENSE) licensed.
|
||||
|
||||
<!-- LINK GROUP -->
|
||||
|
||||
|
||||
+1
-1
@@ -840,7 +840,7 @@ $ pnpm run dev
|
||||
</details>
|
||||
|
||||
Copyright © 2025 [LobeHub][profile-link]. <br />
|
||||
This project is [Apache 2.0](./LICENSE) licensed.
|
||||
This project is [LobeHub Community License](./LICENSE) licensed.
|
||||
|
||||
<!-- LINK GROUP -->
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ table chat_groups {
|
||||
config jsonb
|
||||
client_id text
|
||||
user_id text [not null]
|
||||
group_id text
|
||||
pinned boolean [default: false]
|
||||
accessed_at "timestamp with time zone" [not null, default: `now()`]
|
||||
created_at "timestamp with time zone" [not null, default: `now()`]
|
||||
@@ -387,7 +388,7 @@ table message_translates {
|
||||
|
||||
table messages {
|
||||
id text [pk, not null]
|
||||
role text [not null]
|
||||
role varchar(255) [not null]
|
||||
content text
|
||||
reasoning jsonb
|
||||
search jsonb
|
||||
|
||||
+3
-2
@@ -159,11 +159,11 @@
|
||||
"@lobehub/charts": "^2.1.2",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
||||
"@lobehub/editor": "^1.9.2",
|
||||
"@lobehub/editor": "^1.11.0",
|
||||
"@lobehub/icons": "^2.32.2",
|
||||
"@lobehub/market-sdk": "^0.22.7",
|
||||
"@lobehub/tts": "^2.0.1",
|
||||
"@lobehub/ui": "^2.12.4",
|
||||
"@lobehub/ui": "^2.13.0",
|
||||
"@modelcontextprotocol/sdk": "^1.18.0",
|
||||
"@neondatabase/serverless": "^1.0.1",
|
||||
"@next/third-parties": "^15.5.3",
|
||||
@@ -344,6 +344,7 @@
|
||||
"glob": "^11.0.3",
|
||||
"happy-dom": "^18.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"import-in-the-middle": "^1.14.2",
|
||||
"just-diff": "^6.0.2",
|
||||
"lint-staged": "^15.5.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -4,6 +4,8 @@ import { BRANDING_LOGO_URL } from './branding';
|
||||
|
||||
export const DEFAULT_AVATAR = '🤖';
|
||||
export const DEFAULT_USER_AVATAR = '😀';
|
||||
export const DEFAULT_SUPERVISOR_AVATAR = '🎙️';
|
||||
export const DEFAULT_SUPERVISOR_ID = 'supervisor';
|
||||
export const DEFAULT_BACKGROUND_COLOR = 'rgba(0,0,0,0)';
|
||||
export const DEFAULT_AGENT_META: MetaData = {};
|
||||
export const DEFAULT_INBOX_AVATAR = BRANDING_LOGO_URL || '🤯';
|
||||
|
||||
@@ -3,7 +3,9 @@ export const PLUGIN_SCHEMA_API_MD5_PREFIX = 'MD5HASH_';
|
||||
|
||||
export const ARTIFACT_TAG = 'lobeArtifact';
|
||||
export const ARTIFACT_THINKING_TAG = 'lobeThinking';
|
||||
|
||||
export const MENTION_TAG = 'mention';
|
||||
export const THINKING_TAG = 'think';
|
||||
export const LOCAL_FILE_TAG = 'localFile';
|
||||
// https://regex101.com/r/TwzTkf/2
|
||||
export const ARTIFACT_TAG_REGEX = /<lobeArtifact\b[^>]*>(?<content>[\S\s]*?)(?:<\/lobeArtifact>|$)/;
|
||||
|
||||
@@ -14,3 +16,5 @@ export const ARTIFACT_TAG_CLOSED_REGEX = /<lobeArtifact\b[^>]*>([\S\s]*?)<\/lobe
|
||||
export const ARTIFACT_THINKING_TAG_REGEX = /<lobeThinking\b[^>]*>([\S\s]*?)(?:<\/lobeThinking>|$)/;
|
||||
|
||||
export const THINKING_TAG_REGEX = /<think\b[^>]*>([\S\s]*?)(?:<\/think>|$)/;
|
||||
|
||||
export const MENTION_TAG_REGEX = /<mention\b[^>]*>([\S\s]*?)(?:<\/mention>|$)/;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LobeAgentSession, LobeSessionType } from '@lobechat/types';
|
||||
import { LobeAgentSession, LobeGroupSession, LobeSessionType } from '@lobechat/types';
|
||||
|
||||
import { DEFAULT_AGENT_META } from './meta';
|
||||
import { DEFAULT_AGENT_META, DEFAULT_INBOX_AVATAR } from './meta';
|
||||
import { DEFAULT_AGENT_CONFIG } from './settings';
|
||||
import { merge } from './utils/merge';
|
||||
|
||||
export const INBOX_SESSION_ID = 'inbox';
|
||||
|
||||
@@ -16,3 +17,19 @@ export const DEFAULT_AGENT_LOBE_SESSION: LobeAgentSession = {
|
||||
type: LobeSessionType.Agent,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const DEFAULT_GROUP_LOBE_SESSION: LobeGroupSession = {
|
||||
createdAt: new Date(),
|
||||
id: '',
|
||||
members: [],
|
||||
meta: DEFAULT_AGENT_META,
|
||||
type: LobeSessionType.Group,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
export const DEFAULT_INBOX_SESSION: LobeAgentSession = merge(DEFAULT_AGENT_LOBE_SESSION, {
|
||||
id: 'inbox',
|
||||
meta: {
|
||||
avatar: DEFAULT_INBOX_AVATAR,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
LobeChatGroupChatConfig,
|
||||
LobeChatGroupFullConfig,
|
||||
LobeChatGroupMetaConfig,
|
||||
} from '@lobechat/types';
|
||||
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from './llm';
|
||||
|
||||
export const DEFAULT_CHAT_GROUP_CHAT_CONFIG: LobeChatGroupChatConfig = {
|
||||
allowDM: true,
|
||||
enableSupervisor: true,
|
||||
maxResponseInRow: 10,
|
||||
orchestratorModel: DEFAULT_MODEL,
|
||||
orchestratorProvider: DEFAULT_PROVIDER,
|
||||
responseOrder: 'natural',
|
||||
responseSpeed: 'fast',
|
||||
revealDM: false,
|
||||
scene: 'productive',
|
||||
systemPrompt: '',
|
||||
};
|
||||
|
||||
export const DEFAULT_CHAT_GROUP_META_CONFIG: LobeChatGroupMetaConfig = {
|
||||
description: '',
|
||||
title: '',
|
||||
};
|
||||
|
||||
export const DEFAULT_CHAT_GROUP_CONFIG: LobeChatGroupFullConfig = {
|
||||
chat: DEFAULT_CHAT_GROUP_CHAT_CONFIG,
|
||||
meta: DEFAULT_CHAT_GROUP_META_CONFIG,
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { DEFAULT_TOOL_CONFIG } from './tool';
|
||||
import { DEFAULT_TTS_CONFIG } from './tts';
|
||||
|
||||
export * from './agent';
|
||||
export * from './group';
|
||||
export * from './hotkey';
|
||||
export * from './llm';
|
||||
export * from './systemAgent';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { merge as _merge, isEmpty, mergeWith } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 用于合并对象,如果是数组则直接替换
|
||||
* @param target
|
||||
* @param source
|
||||
*/
|
||||
export const merge: typeof _merge = <T = object>(target: T, source: T) =>
|
||||
mergeWith({}, target, source, (obj, src) => {
|
||||
if (Array.isArray(obj)) return src;
|
||||
});
|
||||
|
||||
type MergeableItem = {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge two arrays based on id, preserving metadata from default items
|
||||
* @param defaultItems Items with default configuration and metadata
|
||||
* @param userItems User-defined items with higher priority
|
||||
*/
|
||||
export const mergeArrayById = <T extends MergeableItem>(defaultItems: T[], userItems: T[]): T[] => {
|
||||
// Create a map of default items for faster lookup
|
||||
const defaultItemsMap = new Map(defaultItems.map((item) => [item.id, item]));
|
||||
|
||||
// 使用 Map 存储合并结果,这样重复 ID 的后项会自然覆盖前项
|
||||
const mergedItemsMap = new Map<string, T>();
|
||||
|
||||
// Process user items with default metadata
|
||||
userItems.forEach((userItem) => {
|
||||
const defaultItem = defaultItemsMap.get(userItem.id);
|
||||
if (!defaultItem) {
|
||||
mergedItemsMap.set(userItem.id, userItem);
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedItem: T = { ...defaultItem };
|
||||
Object.entries(userItem).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && !(typeof value === 'object' && isEmpty(value))) {
|
||||
// @ts-expect-error
|
||||
mergedItem[key] = value;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && !isEmpty(value)) {
|
||||
// @ts-expect-error
|
||||
mergedItem[key] = merge(defaultItem[key], value);
|
||||
}
|
||||
});
|
||||
|
||||
mergedItemsMap.set(userItem.id, mergedItem);
|
||||
});
|
||||
|
||||
// 添加只在默认配置中存在的项
|
||||
defaultItems.forEach((item) => {
|
||||
if (!mergedItemsMap.has(item.id)) {
|
||||
mergedItemsMap.set(item.id, item);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(mergedItemsMap.values());
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "messages" ALTER COLUMN "role" SET DATA TYPE varchar(255);--> statement-breakpoint
|
||||
ALTER TABLE "chat_groups" ADD COLUMN IF NOT EXISTS "group_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "chat_groups" DROP CONSTRAINT IF EXISTS "chat_groups_group_id_session_groups_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "chat_groups" ADD CONSTRAINT "chat_groups_group_id_session_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."session_groups"("id") ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -236,7 +236,14 @@
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1758012348218,
|
||||
"tag": "0033_modern_mercury",
|
||||
"tag": "0033_add_table_index",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 34,
|
||||
"version": "7",
|
||||
"when": 1758825944181,
|
||||
"tag": "0034_fix_chat_group",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
|
||||
@@ -615,5 +615,16 @@
|
||||
"bps": true,
|
||||
"folderMillis": 1758012348218,
|
||||
"hash": "ce04ef4cde2db479d28ff08dced8383052c5052c904bab8343b5493fa10b0679"
|
||||
},
|
||||
{
|
||||
"sql": [
|
||||
"ALTER TABLE \"messages\" ALTER COLUMN \"role\" SET DATA TYPE varchar(255);",
|
||||
"\nALTER TABLE \"chat_groups\" ADD COLUMN IF NOT EXISTS \"group_id\" text;",
|
||||
"\nALTER TABLE \"chat_groups\" DROP CONSTRAINT IF EXISTS \"chat_groups_group_id_session_groups_id_fk\";",
|
||||
"\nALTER TABLE \"chat_groups\" ADD CONSTRAINT \"chat_groups_group_id_session_groups_id_fk\" FOREIGN KEY (\"group_id\") REFERENCES \"public\".\"session_groups\"(\"id\") ON DELETE set null ON UPDATE no action;\n"
|
||||
],
|
||||
"bps": true,
|
||||
"folderMillis": 1758825944181,
|
||||
"hash": "1ba9b1f74ea13348da98d6fcdad7867ab4316ed565bf75d84d160c526cdac14b"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,15 +7,14 @@ import {
|
||||
primaryKey,
|
||||
text,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { createInsertSchema } from 'drizzle-zod';
|
||||
|
||||
import { idGenerator } from '@/database/utils/idGenerator';
|
||||
import type { ChatGroupConfig } from '@/database/types/chatGroup';
|
||||
|
||||
import type { ChatGroupConfig } from '../types/chatGroup';
|
||||
import { idGenerator } from '../utils/idGenerator';
|
||||
import { timestamps } from './_helpers';
|
||||
import { agents } from './agent';
|
||||
import { sessionGroups } from './session';
|
||||
import { users } from './user';
|
||||
|
||||
/**
|
||||
@@ -32,9 +31,6 @@ export const chatGroups = pgTable(
|
||||
title: text('title'),
|
||||
description: text('description'),
|
||||
|
||||
/**
|
||||
* Group configuration
|
||||
*/
|
||||
config: jsonb('config').$type<ChatGroupConfig>(),
|
||||
|
||||
clientId: text('client_id'),
|
||||
@@ -43,6 +39,8 @@ export const chatGroups = pgTable(
|
||||
.references(() => users.id, { onDelete: 'cascade' })
|
||||
.notNull(),
|
||||
|
||||
groupId: text('group_id').references(() => sessionGroups.id, { onDelete: 'set null' }),
|
||||
|
||||
pinned: boolean('pinned').default(false),
|
||||
|
||||
...timestamps,
|
||||
@@ -95,4 +93,4 @@ export const chatGroupsAgents = pgTable(
|
||||
);
|
||||
|
||||
export type NewChatGroupAgent = typeof chatGroupsAgents.$inferInsert;
|
||||
export type ChatGroupAgentItem = typeof agents.$inferInsert
|
||||
export type ChatGroupAgentItem = typeof agents.$inferInsert;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
text,
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { createSelectSchema } from 'drizzle-zod';
|
||||
|
||||
@@ -33,7 +34,7 @@ export const messages = pgTable(
|
||||
.$defaultFn(() => idGenerator('messages'))
|
||||
.primaryKey(),
|
||||
|
||||
role: text('role', { enum: ['user', 'system', 'assistant', 'tool'] }).notNull(),
|
||||
role: varchar('role', { length: 255 }).notNull(),
|
||||
content: text('content'),
|
||||
reasoning: jsonb('reasoning').$type<ModelReasoning>(),
|
||||
search: jsonb('search').$type<GroundingSearch>(),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export interface ChatGroupConfig {
|
||||
allowDM?: boolean;
|
||||
enableSupervisor?: boolean;
|
||||
maxResponseInRow?: number;
|
||||
orchestratorModel?: string;
|
||||
orchestratorProvider?: string;
|
||||
responseOrder?: 'sequential' | 'natural';
|
||||
responseSpeed?: 'slow' | 'medium' | 'fast';
|
||||
revealDM?: boolean;
|
||||
scene: 'casual' | 'productive';
|
||||
systemPrompt?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
||||
async chat({ responseMode, ...payload }: ChatStreamPayload, options?: ChatMethodOptions) {
|
||||
try {
|
||||
const inputStartAt = Date.now();
|
||||
|
||||
|
||||
// 工厂级 Responses API 路由控制(支持实例覆盖)
|
||||
const modelId = (payload as any).model as string | undefined;
|
||||
const shouldUseResponses = (() => {
|
||||
@@ -208,7 +208,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
||||
if (shouldUseResponses) {
|
||||
processedPayload = { ...payload, apiMode: 'responses' } as any;
|
||||
}
|
||||
|
||||
|
||||
// 再进行工厂级处理
|
||||
const postPayload = chatCompletion?.handlePayload
|
||||
? chatCompletion.handlePayload(processedPayload, this._options)
|
||||
@@ -232,7 +232,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
||||
};
|
||||
|
||||
if (customClient?.createChatCompletionStream) {
|
||||
response = customClient.createChatCompletionStream(this.client, processedPayload, this) as any;
|
||||
response = customClient.createChatCompletionStream(
|
||||
this.client,
|
||||
processedPayload,
|
||||
this,
|
||||
) as any;
|
||||
} else {
|
||||
const finalPayload = {
|
||||
...postPayload,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { GenerateContentConfig, GoogleGenAI, Type as SchemaType } from '@google/genai';
|
||||
import Debug from 'debug';
|
||||
|
||||
import { GenerateObjectOptions } from '../../types';
|
||||
|
||||
const debug = Debug('mode-runtime:google:generateObject');
|
||||
|
||||
enum HarmCategory {
|
||||
HARM_CATEGORY_DANGEROUS_CONTENT = 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
||||
HARM_CATEGORY_HARASSMENT = 'HARM_CATEGORY_HARASSMENT',
|
||||
@@ -107,8 +110,18 @@ export const createGoogleGenerateObject = async (
|
||||
) => {
|
||||
const { schema, contents, model } = payload;
|
||||
|
||||
debug('createGoogleGenerateObject started', {
|
||||
contentsLength: contents.length,
|
||||
hasSchema: !!schema,
|
||||
model,
|
||||
});
|
||||
|
||||
// Convert OpenAI schema to Google schema format
|
||||
const responseSchema = convertOpenAISchemaToGoogleSchema(schema);
|
||||
debug('Schema conversion completed', {
|
||||
convertedSchema: responseSchema,
|
||||
originalSchema: schema,
|
||||
});
|
||||
|
||||
const config: GenerateContentConfig = {
|
||||
abortSignal: options?.signal,
|
||||
@@ -135,18 +148,30 @@ export const createGoogleGenerateObject = async (
|
||||
],
|
||||
};
|
||||
|
||||
debug('Config prepared', {
|
||||
hasAbortSignal: !!config.abortSignal,
|
||||
hasSafetySettings: !!config.safetySettings,
|
||||
model,
|
||||
responseMimeType: config.responseMimeType,
|
||||
});
|
||||
|
||||
const response = await client.models.generateContent({
|
||||
config,
|
||||
contents,
|
||||
model,
|
||||
});
|
||||
|
||||
debug('API response received', { hasText: !!response.text, textLength: response.text?.length });
|
||||
|
||||
const text = response.text;
|
||||
|
||||
try {
|
||||
return JSON.parse(text!);
|
||||
const result = JSON.parse(text!);
|
||||
debug('JSON parsing successful', result);
|
||||
return result;
|
||||
} catch {
|
||||
console.error('parse json error:', text);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,73 +1,3 @@
|
||||
import { LLMParams } from 'model-bank';
|
||||
import { FileItem } from '../files';
|
||||
import { KnowledgeBaseItem } from '../knowledgeBase';
|
||||
import { FewShots } from '../llm';
|
||||
import { LobeAgentChatConfig } from './chatConfig';
|
||||
|
||||
export type TTSServer = 'openai' | 'edge' | 'microsoft';
|
||||
|
||||
export interface LobeAgentTTSConfig {
|
||||
showAllLocaleVoice?: boolean;
|
||||
sttLocale: 'auto' | string;
|
||||
ttsService: TTSServer;
|
||||
voice: {
|
||||
edge?: string;
|
||||
microsoft?: string;
|
||||
openai: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LobeAgentConfig {
|
||||
chatConfig: LobeAgentChatConfig;
|
||||
fewShots?: FewShots;
|
||||
files?: FileItem[];
|
||||
id?: string;
|
||||
/**
|
||||
* knowledge bases
|
||||
*/
|
||||
knowledgeBases?: KnowledgeBaseItem[];
|
||||
/**
|
||||
* 角色所使用的语言模型
|
||||
* @default gpt-4o-mini
|
||||
*/
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* 开场白
|
||||
*/
|
||||
openingMessage?: string;
|
||||
/**
|
||||
* 开场问题
|
||||
*/
|
||||
openingQuestions?: string[];
|
||||
|
||||
/**
|
||||
* 语言模型参数
|
||||
*/
|
||||
params: LLMParams;
|
||||
/**
|
||||
* 启用的插件
|
||||
*/
|
||||
plugins?: string[];
|
||||
|
||||
/**
|
||||
* 模型供应商
|
||||
*/
|
||||
provider?: string;
|
||||
|
||||
/**
|
||||
* 系统角色
|
||||
*/
|
||||
systemRole: string;
|
||||
|
||||
/**
|
||||
* 语音服务
|
||||
*/
|
||||
tts: LobeAgentTTSConfig;
|
||||
}
|
||||
|
||||
export type LobeAgentConfigKeys =
|
||||
| keyof LobeAgentConfig
|
||||
| ['params', keyof LobeAgentConfig['params']];
|
||||
|
||||
export * from './chatConfig';
|
||||
export * from './item';
|
||||
export * from './tts';
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { LLMParams } from 'model-bank';
|
||||
|
||||
import { FileItem } from '../files';
|
||||
import { KnowledgeBaseItem } from '../knowledgeBase';
|
||||
import { FewShots } from '../llm';
|
||||
import { LobeAgentChatConfig } from './chatConfig';
|
||||
import { LobeAgentTTSConfig } from './tts';
|
||||
|
||||
export interface LobeAgentConfig {
|
||||
chatConfig: LobeAgentChatConfig;
|
||||
fewShots?: FewShots;
|
||||
files?: FileItem[];
|
||||
id?: string;
|
||||
/**
|
||||
* knowledge bases
|
||||
*/
|
||||
knowledgeBases?: KnowledgeBaseItem[];
|
||||
/**
|
||||
* 角色所使用的语言模型
|
||||
* @default gpt-4o-mini
|
||||
*/
|
||||
model: string;
|
||||
|
||||
/**
|
||||
* 开场白
|
||||
*/
|
||||
openingMessage?: string;
|
||||
/**
|
||||
* 开场问题
|
||||
*/
|
||||
openingQuestions?: string[];
|
||||
|
||||
/**
|
||||
* 语言模型参数
|
||||
*/
|
||||
params: LLMParams;
|
||||
/**
|
||||
* 启用的插件
|
||||
*/
|
||||
plugins?: string[];
|
||||
|
||||
/**
|
||||
* 模型供应商
|
||||
*/
|
||||
provider?: string;
|
||||
|
||||
/**
|
||||
* 系统角色
|
||||
*/
|
||||
systemRole: string;
|
||||
|
||||
/**
|
||||
* 语音服务
|
||||
*/
|
||||
tts: LobeAgentTTSConfig;
|
||||
}
|
||||
|
||||
export type LobeAgentConfigKeys =
|
||||
| keyof LobeAgentConfig
|
||||
| ['params', keyof LobeAgentConfig['params']];
|
||||
|
||||
// Agent database item type (independent from schema)
|
||||
export interface AgentItem {
|
||||
avatar?: string | null;
|
||||
backgroundColor?: string | null;
|
||||
chatConfig?: LobeAgentChatConfig | null;
|
||||
clientId?: string | null;
|
||||
createdAt: Date;
|
||||
description?: string | null;
|
||||
fewShots?: any | null;
|
||||
id: string;
|
||||
model?: string | null;
|
||||
openingMessage?: string | null;
|
||||
openingQuestions?: string[];
|
||||
params?: any;
|
||||
plugins?: string[];
|
||||
provider?: string | null;
|
||||
slug?: string | null;
|
||||
systemRole?: string | null;
|
||||
tags?: string[];
|
||||
title?: string | null;
|
||||
tts?: LobeAgentTTSConfig | null;
|
||||
updatedAt: Date;
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type TTSServer = 'openai' | 'edge' | 'microsoft';
|
||||
|
||||
export interface LobeAgentTTSConfig {
|
||||
showAllLocaleVoice?: boolean;
|
||||
sttLocale: 'auto' | string;
|
||||
ttsService: TTSServer;
|
||||
voice: {
|
||||
edge?: string;
|
||||
microsoft?: string;
|
||||
openai: string;
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,16 @@
|
||||
export { type ApiKeyItem } from '@/database/schemas/apiKey';
|
||||
// API Key database item type (independent from schema)
|
||||
export interface ApiKeyItem {
|
||||
accessedAt: Date;
|
||||
createdAt: Date;
|
||||
enabled?: boolean | null;
|
||||
expiresAt?: Date | null;
|
||||
id: number;
|
||||
key: string;
|
||||
lastUsedAt?: Date | null;
|
||||
name: string;
|
||||
updatedAt: Date;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface CreateApiKeyParams {
|
||||
expiresAt?: Date | null;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
export interface LobeChatGroupMetaConfig {
|
||||
description: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LobeChatGroupChatConfig {
|
||||
allowDM: boolean;
|
||||
enableSupervisor: boolean;
|
||||
maxResponseInRow: number;
|
||||
orchestratorModel: string;
|
||||
orchestratorProvider: string;
|
||||
responseOrder: 'sequential' | 'natural';
|
||||
responseSpeed: 'slow' | 'medium' | 'fast';
|
||||
revealDM: boolean;
|
||||
scene: 'casual' | 'productive';
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
// Database config type (flat structure)
|
||||
export type LobeChatGroupConfig = LobeChatGroupChatConfig;
|
||||
|
||||
// Full group type with nested structure for UI components
|
||||
export interface LobeChatGroupFullConfig {
|
||||
chat: LobeChatGroupChatConfig;
|
||||
meta: LobeChatGroupMetaConfig;
|
||||
}
|
||||
|
||||
// Chat Group Agent types (independent from schema)
|
||||
export interface ChatGroupAgent {
|
||||
agentId: string;
|
||||
chatGroupId: string;
|
||||
createdAt: Date;
|
||||
enabled?: boolean;
|
||||
order?: number;
|
||||
role?: string;
|
||||
updatedAt: Date;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface NewChatGroupAgent {
|
||||
agentId: string;
|
||||
chatGroupId: string;
|
||||
enabled?: boolean;
|
||||
order?: number;
|
||||
role?: string;
|
||||
userId: string;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ export const ChatErrorType = {
|
||||
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // 订阅用户超限
|
||||
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // 订阅 key 不匹配
|
||||
|
||||
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // 主持人决策失败
|
||||
|
||||
InvalidUserKey: 'InvalidUserKey', // is not valid User key
|
||||
CreateMessageError: 'CreateMessageError',
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from './aiProvider';
|
||||
export * from './artifact';
|
||||
export * from './asyncTask';
|
||||
export * from './auth';
|
||||
export * from './chatGroup';
|
||||
export * from './chunk';
|
||||
export * from './clientDB';
|
||||
export * from './discover';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UploadFileItem } from '../files';
|
||||
import { MetaData } from '../meta';
|
||||
import { MessageSemanticSearchChunk } from '../rag';
|
||||
import { GroundingSearch } from '../search';
|
||||
@@ -46,6 +47,8 @@ export interface ChatMessageExtra {
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
// Group chat fields (alphabetically before other fields)
|
||||
agentId?: string | 'supervisor';
|
||||
chunksList?: ChatFileChunk[];
|
||||
content: string;
|
||||
createdAt: number;
|
||||
@@ -61,6 +64,7 @@ export interface ChatMessage {
|
||||
* @deprecated
|
||||
*/
|
||||
files?: string[];
|
||||
groupId?: string;
|
||||
id: string;
|
||||
imageList?: ChatImageItem[];
|
||||
meta: MetaData;
|
||||
@@ -90,6 +94,10 @@ export interface ChatMessage {
|
||||
role: MessageRoleType;
|
||||
search?: GroundingSearch | null;
|
||||
sessionId?: string;
|
||||
/**
|
||||
* target member ID for DM messages in group chat
|
||||
*/
|
||||
targetId?: string | null;
|
||||
threadId?: string | null;
|
||||
tool_call_id?: string;
|
||||
tools?: ChatToolPayload[];
|
||||
@@ -113,9 +121,54 @@ export interface CreateMessageParams
|
||||
files?: string[];
|
||||
fromModel?: string;
|
||||
fromProvider?: string;
|
||||
groupId?: string;
|
||||
role: MessageRoleType;
|
||||
sessionId: string;
|
||||
threadId?: string | null;
|
||||
targetId?: string | null;
|
||||
topicId?: string;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
export interface SendMessageParams {
|
||||
/**
|
||||
* create a thread
|
||||
*/
|
||||
createThread?: boolean;
|
||||
files?: UploadFileItem[];
|
||||
/**
|
||||
*
|
||||
* https://github.com/lobehub/lobe-chat/pull/2086
|
||||
*/
|
||||
isWelcomeQuestion?: boolean;
|
||||
message: string;
|
||||
/**
|
||||
* Additional metadata for the message (e.g., mentioned users)
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
onlyAddUserMessage?: boolean;
|
||||
}
|
||||
|
||||
export interface SendThreadMessageParams {
|
||||
/**
|
||||
* create a thread
|
||||
*/
|
||||
createNewThread?: boolean;
|
||||
// files?: UploadFileItem[];
|
||||
message: string;
|
||||
onlyAddUserMessage?: boolean;
|
||||
}
|
||||
|
||||
export interface SendGroupMessageParams {
|
||||
files?: UploadFileItem[];
|
||||
groupId: string;
|
||||
message: string;
|
||||
/**
|
||||
* Additional metadata for the message (e.g., mentioned users)
|
||||
*/
|
||||
metadata?: Record<string, any>;
|
||||
onlyAddUserMessage?: boolean;
|
||||
/**
|
||||
* for group chat
|
||||
*/
|
||||
targetMemberId?: string | null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { UploadFileItem } from '../files';
|
||||
|
||||
export * from './base';
|
||||
export * from './chat';
|
||||
export * from './image';
|
||||
@@ -7,31 +5,6 @@ export * from './rag';
|
||||
export * from './tools';
|
||||
export * from './video';
|
||||
|
||||
export interface SendMessageParams {
|
||||
/**
|
||||
* create a thread
|
||||
*/
|
||||
createThread?: boolean;
|
||||
files?: UploadFileItem[];
|
||||
/**
|
||||
*
|
||||
* https://github.com/lobehub/lobe-chat/pull/2086
|
||||
*/
|
||||
isWelcomeQuestion?: boolean;
|
||||
message: string;
|
||||
onlyAddUserMessage?: boolean;
|
||||
}
|
||||
|
||||
export interface SendThreadMessageParams {
|
||||
/**
|
||||
* create a thread
|
||||
*/
|
||||
createNewThread?: boolean;
|
||||
// files?: UploadFileItem[];
|
||||
message: string;
|
||||
onlyAddUserMessage?: boolean;
|
||||
}
|
||||
|
||||
export interface ModelRankItem {
|
||||
count: number;
|
||||
id: string | null;
|
||||
|
||||
@@ -2,6 +2,26 @@ import { LLMRoleType } from '../llm';
|
||||
import { MessageToolCall } from '../message';
|
||||
import { OpenAIFunctionCall } from './functionCall';
|
||||
|
||||
export type ChatResponseFormat =
|
||||
| { type: 'json_object' }
|
||||
| {
|
||||
json_schema: {
|
||||
/**
|
||||
* Schema identifier required by OpenAI.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* JSON schema definition used for validation.
|
||||
*/
|
||||
schema: Record<string, any>;
|
||||
/**
|
||||
* Enforce strict schema validation when true.
|
||||
*/
|
||||
strict?: boolean;
|
||||
};
|
||||
type: 'json_schema';
|
||||
};
|
||||
|
||||
interface UserMessageContentPartText {
|
||||
text: string;
|
||||
type: 'text';
|
||||
@@ -79,6 +99,8 @@ export interface ChatStreamPayload {
|
||||
* @default openai
|
||||
*/
|
||||
provider?: string;
|
||||
responseMode?: 'stream' | 'json';
|
||||
response_format?: ChatResponseFormat;
|
||||
/**
|
||||
* @title 是否开启流式请求
|
||||
* @default true
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { LobeAgentConfig } from '../agent';
|
||||
import { AgentItem, LobeAgentConfig } from '../agent';
|
||||
import { NewChatGroupAgent } from '../chatGroup';
|
||||
import { MetaData } from '../meta';
|
||||
|
||||
export type SessionGroupId = 'default' | 'pinned' | string;
|
||||
|
||||
export enum LobeSessionType {
|
||||
Agent = 'agent',
|
||||
Group = 'group',
|
||||
}
|
||||
|
||||
/**
|
||||
* Lobe Agent
|
||||
* Extended group member that includes both relation data and agent details
|
||||
*/
|
||||
export type GroupMemberWithAgent = NewChatGroupAgent & AgentItem;
|
||||
|
||||
/**
|
||||
* Lobe Agent Session
|
||||
*/
|
||||
export interface LobeAgentSession {
|
||||
config: LobeAgentConfig;
|
||||
createdAt: Date;
|
||||
group?: SessionGroupId;
|
||||
group?: string;
|
||||
id: string;
|
||||
meta: MetaData;
|
||||
model: string;
|
||||
@@ -24,6 +28,21 @@ export interface LobeAgentSession {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group chat (not confuse with session group)
|
||||
*/
|
||||
export interface LobeGroupSession {
|
||||
createdAt: Date;
|
||||
group?: string;
|
||||
id: string; // Start with 'cg_'
|
||||
members?: GroupMemberWithAgent[];
|
||||
meta: MetaData;
|
||||
pinned?: boolean;
|
||||
tags?: string[];
|
||||
type: LobeSessionType.Group;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface LobeAgentSettings {
|
||||
/**
|
||||
* 语言模型角色设定
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LobeSessions, SessionGroupId } from './agentSession';
|
||||
import { LobeSessionGroups } from './sessionGroup';
|
||||
import { LobeSessions } from './agentSession';
|
||||
import { LobeSessionGroups, SessionGroupId } from './sessionGroup';
|
||||
|
||||
export * from './agentSession';
|
||||
export * from './sessionGroup';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { LobeSessions } from './agentSession';
|
||||
|
||||
export type SessionGroupId = string;
|
||||
|
||||
export enum SessionDefaultGroup {
|
||||
Default = 'default',
|
||||
Pinned = 'pinned',
|
||||
|
||||
@@ -330,8 +330,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
||||
|
||||
// 添加文本buffer和计时器相关变量
|
||||
let textBuffer = '';
|
||||
// eslint-disable-next-line no-undef
|
||||
let bufferTimer: NodeJS.Timeout | null = null;
|
||||
let bufferTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const BUFFER_INTERVAL = 300; // 300ms
|
||||
|
||||
const flushTextBuffer = () => {
|
||||
@@ -362,8 +361,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
||||
});
|
||||
|
||||
let thinkingBuffer = '';
|
||||
// eslint-disable-next-line no-undef
|
||||
let thinkingBufferTimer: NodeJS.Timeout | null = null;
|
||||
let thinkingBufferTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// 创建一个函数来处理buffer的刷新
|
||||
const flushThinkingBuffer = () => {
|
||||
|
||||
@@ -35,9 +35,11 @@ export const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
}));
|
||||
|
||||
const ProviderItem = memo<AiProviderListItem & {
|
||||
onClick: (id: string) => void
|
||||
}>(
|
||||
interface ProviderItemProps extends AiProviderListItem {
|
||||
onClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const ProviderItem = memo<ProviderItemProps>(
|
||||
({ id, name, source, enabled, logo, onClick = () => {} }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -117,92 +117,72 @@ function getScopeDescription(scope: string, t: any): string {
|
||||
return t(`consent.scope.${scope.replace(':', '-')}`, scope);
|
||||
}
|
||||
|
||||
const ConsentClient = memo<ClientProps>(
|
||||
({ uid, clientId, scopes, clientMetadata, redirectUri }) => {
|
||||
const { styles, theme } = useStyles();
|
||||
const { t } = useTranslation('oauth');
|
||||
const ConsentClient = memo<ClientProps>(({ uid, clientId, scopes, clientMetadata }) => {
|
||||
const { styles } = useStyles();
|
||||
const { t } = useTranslation('oauth');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const clientDisplayName = clientMetadata?.clientName || clientId;
|
||||
return (
|
||||
<Center className={styles.container} gap={16}>
|
||||
<Flexbox gap={40}>
|
||||
<OAuthApplicationLogo
|
||||
clientDisplayName={clientDisplayName}
|
||||
isFirstParty={clientMetadata.isFirstParty}
|
||||
logoUrl={clientMetadata.logo}
|
||||
/>
|
||||
<Text as={'h3'} className={styles.title}>
|
||||
{t('consent.title', { clientName: clientDisplayName })}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Card className={styles.card}>
|
||||
<Flexbox gap={8}>
|
||||
<Flexbox gap={12}>
|
||||
<Text>{t('consent.description', { clientName: clientDisplayName })}</Text>
|
||||
const clientDisplayName = clientMetadata?.clientName || clientId;
|
||||
return (
|
||||
<Center className={styles.container} gap={16}>
|
||||
<Flexbox gap={40}>
|
||||
<OAuthApplicationLogo
|
||||
clientDisplayName={clientDisplayName}
|
||||
isFirstParty={clientMetadata.isFirstParty}
|
||||
logoUrl={clientMetadata.logo}
|
||||
/>
|
||||
<Text as={'h3'} className={styles.title}>
|
||||
{t('consent.title', { clientName: clientDisplayName })}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Card className={styles.card}>
|
||||
<Flexbox gap={8}>
|
||||
<Flexbox gap={12}>
|
||||
<Text>{t('consent.description', { clientName: clientDisplayName })}</Text>
|
||||
|
||||
<div className={styles.scopes}>
|
||||
<Text type={'secondary'}>{t('consent.permissionsTitle')}</Text>
|
||||
{scopes.map((scope) => (
|
||||
<div className={styles.scope} key={scope}>
|
||||
<Text>{getScopeDescription(scope, t)}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.scopes}>
|
||||
<Text type={'secondary'}>{t('consent.permissionsTitle')}</Text>
|
||||
{scopes.map((scope) => (
|
||||
<div className={styles.scope} key={scope}>
|
||||
<Text>{getScopeDescription(scope, t)}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider dashed />
|
||||
<Flexbox gap={16}>
|
||||
<form action="/oidc/consent" method="post" style={{ width: '100%' }}>
|
||||
<input name="uid" type="hidden" value={uid} />
|
||||
<Flexbox gap={12} horizontal>
|
||||
<Button
|
||||
className={styles.cancelButton}
|
||||
htmlType="submit"
|
||||
name="consent"
|
||||
value="deny"
|
||||
>
|
||||
{t('consent.buttons.deny')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.authButton}
|
||||
htmlType="submit"
|
||||
loading={isLoading}
|
||||
name="consent"
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
type="primary"
|
||||
value="accept"
|
||||
>
|
||||
{t('consent.buttons.accept')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</form>
|
||||
<Center>
|
||||
<div style={{ color: theme.colorTextTertiary, fontSize: 12, height: '18px' }}>
|
||||
{t('consent.redirectUri')}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
color: theme.colorTextSecondary,
|
||||
fontSize: 12,
|
||||
height: '18px',
|
||||
}}
|
||||
>
|
||||
{redirectUri}
|
||||
</div>
|
||||
</div>
|
||||
</Center>
|
||||
<Divider dashed />
|
||||
<form action="/oidc/consent" method="post" style={{ width: '100%' }}>
|
||||
<input name="uid" type="hidden" value={uid} />
|
||||
<Flexbox gap={12} horizontal>
|
||||
<Button
|
||||
className={styles.cancelButton}
|
||||
htmlType="submit"
|
||||
name="consent"
|
||||
value="deny"
|
||||
>
|
||||
{t('consent.buttons.deny')}
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.authButton}
|
||||
htmlType="submit"
|
||||
loading={isLoading}
|
||||
name="consent"
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
type="primary"
|
||||
value="accept"
|
||||
>
|
||||
{t('consent.buttons.accept')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</form>
|
||||
</Flexbox>
|
||||
</Card>
|
||||
</Center>
|
||||
);
|
||||
},
|
||||
);
|
||||
</Flexbox>
|
||||
</Card>
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
|
||||
ConsentClient.displayName = 'ConsentClient';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AgentItem, LobeAgentConfig } from '@lobechat/types';
|
||||
|
||||
import { INBOX_SESSION_ID } from '@/const/session';
|
||||
import { clientDB } from '@/database/client/db';
|
||||
import { SessionModel } from '@/database/models/session';
|
||||
import { SessionGroupModel } from '@/database/models/sessionGroup';
|
||||
import { AgentItem } from '@/database/schemas';
|
||||
import { BaseClientService } from '@/services/baseClientService';
|
||||
import { LobeAgentConfig } from '@/types/agent';
|
||||
|
||||
import { ISessionService } from './type';
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ beforeEach(async () => {
|
||||
|
||||
// 创建测试数据
|
||||
await clientDB.transaction(async (tx) => {
|
||||
await tx.insert(users).values({ id: userId });
|
||||
await tx.insert(users).values({ id: userId }).onConflictDoNothing();
|
||||
await tx.insert(sessions).values({ id: sessionId, userId });
|
||||
await tx.insert(topics).values({ ...mockTopic, sessionId, userId });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user