🔨 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:
Arvin Xu
2025-09-25 21:47:25 +02:00
committed by GitHub
parent 4fb18ac6a8
commit 273e0277d1
39 changed files with 7174 additions and 221 deletions
+35
View File
@@ -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);
```
+1 -1
View File
@@ -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
View File
@@ -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 -->
+2 -1
View File
@@ -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
View File
@@ -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",
+2
View File
@@ -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 || '🤯';
+5 -1
View File
@@ -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>|$)/;
+19 -2
View File
@@ -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,
},
});
+30
View File
@@ -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,
};
+1
View File
@@ -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';
+62
View File
@@ -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"
}
]
+6 -8
View File
@@ -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;
+2 -1
View File
@@ -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>(),
+4 -1
View File
@@ -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;
}
};
+2 -72
View File
@@ -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';
+85
View File
@@ -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;
}
+12
View File
@@ -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;
};
}
+13 -1
View File
@@ -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;
+47
View File
@@ -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;
}
+2
View File
@@ -10,6 +10,8 @@ export const ChatErrorType = {
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // 订阅用户超限
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // 订阅 key 不匹配
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // 主持人决策失败
InvalidUserKey: 'InvalidUserKey', // is not valid User key
CreateMessageError: 'CreateMessageError',
/**
+1
View File
@@ -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';
+54 -1
View File
@@ -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;
}
-27
View File
@@ -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;
+22
View File
@@ -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
+24 -5
View File
@@ -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 {
/**
*
+2 -2
View File
@@ -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',
+2 -4
View File
@@ -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';
+2 -2
View File
@@ -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';
+1 -1
View File
@@ -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 });
});