fix(web): repair merge conflicts in chat input area + clean up ManusLeftPanel

- index.tsx: 修复合并导致的 JSX 结构错乱(重复的 Voice/Send 按钮、孤儿块、
  错位的 )}),恢复单一聊天输入区工具栏,融合 main 的 ContextUsageBar/taskPlan
  与本分支的 connector 选择器
- ManusLeftPanel.tsx: 清理未使用变量(hasObservations/hasContent/contentExpanded/
  未用的 t),给保留接口的 onShare/setDownloading 加 _ 前缀,修复 getTodoStep*
  的 TFunction 类型断言,prettier 格式化 import
- en/chat.ts: use_connector 文案统一为 'Select MCP'
- tongyi.toml: 新增 agent_context 上下文窗口预算配置
- scheduled_task/serve.py: 增加 DBGPT_CHAT_TASK_SCHEDULER_ENABLED 开关与内存 jobstore

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
alan.cl
2026-06-04 20:12:44 +08:00
parent e58777768d
commit d554c6c4b9
5 changed files with 76 additions and 312 deletions

View File

@@ -9,6 +9,21 @@ encrypt_key = "your_secret_key"
host = "0.0.0.0"
port = 5670
[service.web.agent_context]
# Agent context-window budget. Set max_context_tokens to 0 to auto-detect from
# the selected model's metadata. The effective budget shown in the UI is
# max_context_tokens - reserved_tokens.
max_context_tokens = 0
reserved_tokens = 4096
warning_threshold = 0.70
error_threshold = 0.90
critical_threshold = 0.95
min_keep_recent_rounds = 3
max_observation_age_rounds = 5
truncated_observation_max_chars = 200
min_keep_tokens = 10000
max_compact_failures = 3
[service.web.database]
type = "sqlite"
path = "pilot/meta_data/dbgpt.db"

View File

@@ -1,6 +1,7 @@
"""ScheduledTaskServe - mount routes, start scheduler, recover jobs on boot."""
import logging
import os
from typing import List, Optional, Union
from sqlalchemy import URL
@@ -22,6 +23,35 @@ from .service.service import ScheduledTaskService
logger = logging.getLogger(__name__)
# Env var name for the master switch that controls whether scheduled tasks
# are actually *executed* on this process. See _scheduler_enabled().
_SCHEDULER_ENABLED_ENV = "DBGPT_CHAT_TASK_SCHEDULER_ENABLED"
# In-memory jobstore sentinel. TaskScheduler treats this exact URL as a signal
# to use APScheduler's MemoryJobStore (no SQLite file, no apscheduler_jobs
# table). The business tables (dbgpt_serve_scheduled_task / _run) are the sole
# source of truth; jobs are rehydrated into memory on boot via
# _recover_jobs_from_db().
_IN_MEMORY_JOBSTORE_URL = "sqlite:///:memory:"
def _scheduler_enabled() -> bool:
"""Env-driven master switch for *executing* scheduled tasks.
Controls rehydrate + triggering only — the REST API is always mounted,
so task CRUD keeps working even when this returns False.
Reads ``DBGPT_CHAT_TASK_SCHEDULER_ENABLED``. Accepts ``1/true/yes/on``
(case-insensitive) as enabled. Unset defaults to enabled.
Returns:
bool: True if scheduled-task execution should run on this process.
"""
raw = os.getenv(_SCHEDULER_ENABLED_ENV)
if raw is None:
return True
return raw.strip().lower() in ("1", "true", "yes", "on")
class ScheduledTaskServe(BaseServe):
"""Serve component for scheduled tasks.
@@ -69,8 +99,13 @@ class ScheduledTaskServe(BaseServe):
# Build scheduler / service. The runner callable is the
# module-level run_scheduled_task (not a bound method) so that
# APScheduler's SQLAlchemyJobStore can pickle the job state.
self._scheduler = TaskScheduler()
# APScheduler can pickle the job state if ever needed.
#
# Use an in-memory jobstore: the business tables are the single source
# of truth and jobs are rehydrated on boot (_recover_jobs_from_db),
# so there is no need for APScheduler to persist its own job table
# (avoids a stray scheduler.db / apscheduler_jobs table in prod DBs).
self._scheduler = TaskScheduler(jobstore_url=_IN_MEMORY_JOBSTORE_URL)
self._service = ScheduledTaskService(
scheduler=self._scheduler,
runner_callable=run_scheduled_task,
@@ -95,7 +130,19 @@ class ScheduledTaskServe(BaseServe):
event loop is already running — the correct context for
AsyncIOScheduler.start(). The runner auth secret is resolved in
init_app, so the scheduler always starts here.
Gated by DBGPT_CHAT_TASK_SCHEDULER_ENABLED: when disabled, the
scheduler is neither started nor rehydrated, but the REST API
(mounted in init_app) stays fully available.
"""
if not _scheduler_enabled():
logger.info(
"Scheduler execution disabled via %s; REST API stays "
"available, no jobs will be rehydrated or triggered.",
_SCHEDULER_ENABLED_ENV,
)
return
if self._scheduler is None:
logger.warning("Scheduler not initialised; skipping startup.")
return

View File

@@ -52,7 +52,7 @@ export const ChatEn = {
use_skill: 'Use Skill',
use_knowledge: 'Use Knowledge Base',
use_database: 'Use Database',
use_connector: 'Select Connector',
use_connector: 'Select MCP',
execution_steps: 'Execution Steps',
db_gpt_computer: "DB-GPT's Computer",
load_skill: 'Load Skill',

View File

@@ -1,4 +1,5 @@
import MarkdownContext from '@/new-components/common/MarkdownContext';
import { AttachedConnector } from '@/new-components/connector/types';
import {
ApiOutlined,
AppstoreOutlined,
@@ -36,7 +37,6 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from '
import { useTranslation } from 'react-i18next';
import ObservationFormatter from './ObservationFormatter';
import TaskPlanCard, { TaskItem } from './TaskPlanCard';
import { AttachedConnector } from '@/new-components/connector/types';
export type StepStatus = 'pending' | 'running' | 'completed' | 'error';
@@ -353,7 +353,7 @@ const SkillCompactCard: React.FC<{
onDownload?: () => void;
}> = memo(({ skillName, onClick, onDownload }) => {
const { t } = useTranslation();
const [downloading, setDownloading] = useState(false);
const [downloading, _setDownloading] = useState(false);
const [isAdded, setIsAdded] = useState(false);
return (
<div
@@ -532,8 +532,9 @@ const StepCard: React.FC<{
);
}
if (isTodoStep) {
const progressText = getTodoStepBadge(t, step);
const todoTitle = getTodoStepTitle(t, step);
const tFn = t as (key: string, options?: Record<string, any>) => string;
const progressText = getTodoStepBadge(tFn, step);
const todoTitle = getTodoStepTitle(tFn, step);
return (
<div
@@ -701,7 +702,6 @@ const SkillResourceCard: React.FC<{
}> = memo(({ step, isActive, onClick }) => {
const { t } = useTranslation();
const [isVisible, setIsVisible] = useState(false);
const [contentExpanded, setContentExpanded] = useState(false);
const parsed = useMemo(() => parseSkillResourceDescription(step.description), [step.description]);
React.useEffect(() => {
@@ -714,7 +714,6 @@ const SkillResourceCard: React.FC<{
}
const resourceName = parsed.resourcePath.split('/').pop() || parsed.resourcePath;
const hasContent = parsed.content.length > 0;
return (
<div
@@ -803,7 +802,6 @@ const SectionBlock: React.FC<{
defaultExpanded?: boolean;
stepThoughts?: Record<string, string>;
}> = memo(({ section, activeStepId, onStepClick, defaultExpanded = true, stepThoughts }) => {
const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const completedCount = section.steps.filter(s => s.status === 'completed').length;
@@ -811,10 +809,6 @@ const SectionBlock: React.FC<{
const isAllCompleted = completedCount === totalCount && totalCount > 0;
const hasRunningStep = section.steps.some(s => s.status === 'running');
const hasObservations = useMemo(() => {
return section.steps.some(step => step.description?.includes('Observation:'));
}, [section.steps]);
return (
<div className='mb-4'>
{/* Section Header */}
@@ -898,7 +892,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
onArtifactClick,
onArtifactDownload,
onViewAllFiles,
onShare,
onShare: _onShare,
isCollapsed,
onExpand,
attachedFile,
@@ -1019,7 +1013,10 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
</div>
)}
{(attachedConnectors ?? []).map(c => (
<div key={c.id} className='flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700/60 bg-white dark:bg-[#1a1b1e] shadow-sm'>
<div
key={c.id}
className='flex items-center gap-2.5 px-3.5 py-2.5 rounded-xl border border-gray-200 dark:border-gray-700/60 bg-white dark:bg-[#1a1b1e] shadow-sm'
>
<div className='w-8 h-8 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center flex-shrink-0'>
<ApiOutlined className='text-violet-500 text-base' />
</div>
@@ -1027,9 +1024,7 @@ const ManusLeftPanel: React.FC<ManusLeftPanelProps> = ({
<div className='text-sm font-medium text-gray-800 dark:text-gray-200 truncate'>
{c.display_name}
</div>
<div className='text-[11px] text-gray-400 dark:text-gray-500'>
{c.connector_type}
</div>
<div className='text-[11px] text-gray-400 dark:text-gray-500'>{c.connector_type}</div>
</div>
</div>
))}

View File

@@ -2564,40 +2564,6 @@ const Playground: NextPage = () => {
})}
</div>
{/* Input Area at Bottom for Chat Mode - Premium Layered Style */}
<div className='bg-gradient-to-t from-white via-white/95 to-white/80 dark:from-[#1a1b1e] dark:via-[#1a1b1e]/95 dark:to-[#1a1b1e]/80 p-4 md:p-6 pt-2'>
<div className='max-w-[720px] mx-auto'>
<div className='relative'>
{/* Context Tags Area */}
<div className='flex flex-wrap gap-2 mb-2'>
{selectedDb && (
<Tag
closable
onClose={() => setSelectedDb(null)}
className='flex items-center gap-1 bg-blue-50 border-blue-200 text-blue-700 px-3 py-1 rounded-full'
>
{getDbIcon(selectedDb.type)} <span className='font-medium ml-1'>{selectedDb.db_name}</span>
</Tag>
)}
{selectedKnowledge && (
<Tag
closable
onClose={() => setSelectedKnowledge(null)}
className='flex items-center gap-1 bg-orange-50 border-orange-200 text-orange-700 px-3 py-1 rounded-full'
>
<BookOutlined /> <span className='font-medium ml-1'>{selectedKnowledge.name}</span>
</Tag>
)}
{uploadedFile && (
<Tag
closable
onClose={() => setUploadedFile(null)}
className='flex items-center gap-1 bg-green-50 border-green-200 text-green-700 px-3 py-1 rounded-full'
>
<FileExcelOutlined /> <span className='font-medium ml-1'>{uploadedFile.name}</span>
</Tag>
)}
</div>
{/* Input Area at Bottom for Chat Mode - Hidden in read-only task replay mode */}
{!router.query.from_task && (
<div className='bg-gradient-to-t from-white via-white/95 to-white/80 dark:from-[#1a1b1e] dark:via-[#1a1b1e]/95 dark:to-[#1a1b1e]/80 p-4 md:p-6'>
@@ -2679,76 +2645,7 @@ const Playground: NextPage = () => {
className='flex-1 resize-none !border-none !shadow-none !bg-transparent px-0 py-2'
style={{ backgroundColor: 'transparent' }}
/>
{/* Outer Frame - Floating Effect */}
<div className='rounded-2xl w-full relative transition-all duration-300 shadow-[0_12px_32px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.06)] hover:shadow-[0_20px_48px_rgba(0,0,0,0.16),0_8px_24px_rgba(0,0,0,0.08)] dark:shadow-[0_12px_32px_rgba(0,0,0,0.4)] dark:hover:shadow-[0_20px_48px_rgba(0,0,0,0.5)]'>
{/* White Inner Box - Clean Glass Card */}
<div className='bg-white/95 backdrop-blur-md dark:bg-[#1e1f24]/95 rounded-2xl border border-gray-100 dark:border-[#33353b] shadow-[inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] p-3 px-4'>
<Input.TextArea
value={query}
onChange={e => {
const newValue = e.target.value;
setQuery(newValue);
if (newValue === '/' && !isSkillPanelOpen && !selectedSkill) {
setIsSkillPanelOpen(true);
}
}}
onPressEnter={e => {
if (!e.shiftKey) {
e.preventDefault();
handleStart();
}
}}
placeholder={
t('ask_data_question') ||
'Ask a question about your database, upload a CSV, or generate a report...'
}
autoSize={{ minRows: 2, maxRows: 6 }}
className='flex-1 resize-none !border-none !shadow-none !bg-transparent px-0 py-2'
style={{ backgroundColor: 'transparent' }}
/>
{/* Toolbar Row */}
<div className='flex items-center justify-between mt-1'>
<div className='flex items-center gap-3'>
{/* Add Button */}
<Dropdown
menu={{
items: [
{
key: 'upload',
label: (
<Upload {...uploadProps}>
<div className='w-full'>Upload File</div>
</Upload>
),
icon: <UploadOutlined />,
},
{
key: 'database',
label: 'Select Data Source',
icon: <DatabaseOutlined />,
onClick: () => setIsDbModalOpen(true),
},
{
key: 'knowledge',
label: 'Select Knowledge Base',
icon: <BookOutlined />,
onClick: () => setIsKnowledgeModalOpen(true),
},
],
}}
trigger={['click']}
>
<Tooltip title={t('add_context')}>
<Button
type='text'
shape='circle'
size='small'
icon={<PlusOutlined />}
className='flex items-center justify-center text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20 transition-all flex-shrink-0'
/>
</Tooltip>
</Dropdown>
{/* Toolbar Row */}
<div className='flex items-center justify-between mt-1'>
<div className='flex items-center gap-3'>
@@ -2798,141 +2695,6 @@ const Playground: NextPage = () => {
</Tooltip>
</Dropdown>
{/* Skill Selector Button with Badge */}
<Popover
trigger='click'
placement='topLeft'
open={isSkillPanelOpen}
onOpenChange={setIsSkillPanelOpen}
overlayClassName='manus-skill-menu'
overlayInnerStyle={{ padding: 0, borderRadius: 12 }}
content={
<div className='w-[320px] bg-white dark:bg-[#2c2d31] rounded-xl shadow-xl overflow-hidden'>
<div className='p-3 border-b border-gray-100 dark:border-gray-700'>
<Input
placeholder={t('search_skill')}
prefix={<SearchOutlined className='text-gray-400' />}
value={skillSearchQuery}
onChange={e => setSkillSearchQuery(e.target.value)}
className='rounded-lg'
allowClear
size='small'
/>
</div>
<div className='max-h-[300px] overflow-y-auto'>
{(skillsList || [])
.filter(
skill =>
!skillSearchQuery ||
skill.name.toLowerCase().includes(skillSearchQuery.toLowerCase()) ||
skill.description.toLowerCase().includes(skillSearchQuery.toLowerCase()),
)
.map(skill => (
<div
key={skill.id}
onClick={() => {
if (selectedSkill?.id === skill.id) {
setSelectedSkill(null);
setQuery('');
} else {
setSelectedSkill(skill);
setQuery(`/${skill.name} `);
}
setIsSkillPanelOpen(false);
setSkillSearchQuery('');
}}
className={`flex items-start gap-3 px-3 py-2.5 cursor-pointer transition-all hover:bg-gray-50 dark:hover:bg-gray-800 ${
selectedSkill?.id === skill.id ? 'bg-purple-50 dark:bg-purple-900/20' : ''
}`}
>
<div className='flex-shrink-0 w-7 h-7 rounded-lg bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center text-white text-xs'>
{skill.icon || <ThunderboltOutlined />}
</div>
<div className='flex-1 min-w-0'>
<div className='flex items-center gap-2'>
<span className='font-medium text-sm text-gray-800 dark:text-gray-200'>
{skill.name}
</span>
<span
className={`text-[10px] px-1.5 py-0.5 rounded ${
skill.type === 'official'
? 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
}`}
>
{skill.type === 'official' ? '官方' : '个人'}
</span>
</div>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-2'>
{skill.description}
</p>
</div>
{selectedSkill?.id === skill.id && (
<CheckCircleFilled className='text-purple-500 flex-shrink-0 text-sm' />
)}
</div>
))}
{(skillsList || []).filter(
skill =>
!skillSearchQuery ||
skill.name.toLowerCase().includes(skillSearchQuery.toLowerCase()) ||
skill.description.toLowerCase().includes(skillSearchQuery.toLowerCase()),
).length === 0 && (
<div className='text-center py-8 text-gray-400'>
<ThunderboltOutlined className='text-2xl mb-2 opacity-50' />
<div className='text-xs'>
{skillSearchQuery ? '未找到匹配的技能' : '暂无可用技能'}
</div>
</div>
)}
</div>
<div className='border-t border-gray-100 dark:border-gray-700 px-3 py-2 flex items-center justify-between bg-gray-50/50 dark:bg-gray-900/50'>
<span className='text-[10px] text-gray-400'>
{(skillsList || []).length}
</span>
<Button
type='link'
size='small'
onClick={() => {
router.push('/construct/skills');
setIsSkillPanelOpen(false);
}}
className='text-[10px] p-0 h-auto'
>
</Button>
</div>
</div>
}
>
<Tooltip
title={
selectedSkill
? t('skill_selected', { name: selectedSkill.name })
: t('select_skill')
}
>
<Button
type='text'
shape='circle'
size='small'
className={`relative flex items-center justify-center flex-shrink-0 transition-all ${
selectedSkill
? 'bg-gradient-to-br from-[#a78bfa] to-[#7c3aed] text-white border border-transparent shadow-[0_2px_4px_rgba(139,92,246,0.3),inset_0_1px_0_rgba(255,255,255,0.3)] hover:-translate-y-[0.5px] hover:shadow-[0_4px_8px_rgba(139,92,246,0.4),inset_0_1px_0_rgba(255,255,255,0.3)]'
: 'text-gray-500 hover:text-violet-600 bg-gradient-to-b from-white to-gray-50 dark:from-[#2a2b2f] dark:to-[#1e1f24] dark:text-gray-300 border border-gray-200/80 dark:border-white/10 shadow-[0_1px_2px_rgba(0,0,0,0.05),inset_0_1px_0_rgba(255,255,255,1)] dark:shadow-[0_1px_2px_rgba(0,0,0,0.2),inset_0_1px_0_rgba(255,255,255,0.05)] hover:-translate-y-[0.5px] hover:shadow-[0_2px_4px_rgba(0,0,0,0.06),inset_0_1px_0_rgba(255,255,255,1)] dark:hover:border-white/20'
}`}
>
<div className='relative'>
<ThunderboltOutlined className={selectedSkill ? 'text-white' : ''} />
{selectedSkill && (
<span className='absolute -top-1.5 -right-1.5 bg-white text-[#7c3aed] text-[8px] rounded-full w-3.5 h-3.5 flex items-center justify-center font-bold shadow-sm ring-1 ring-[#7c3aed]/30'>
1
</span>
)}
</div>
</Button>
</Tooltip>
</Popover>
{/* Skill Selector Button with Badge */}
<Popover
trigger='click'
@@ -3071,8 +2833,6 @@ const Playground: NextPage = () => {
</Tooltip>
</Popover>
{/* Separator dot */}
<div className='w-px h-4 bg-gray-200 dark:bg-gray-700 mx-0.5' />
{/* Connector Selector Button */}
<Popover
trigger='click'
@@ -3275,17 +3035,6 @@ const Playground: NextPage = () => {
className='flex-shrink-0 h-9 w-9 transition-all duration-200 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800'
/>
</Tooltip>
<div className='flex items-center gap-3'>
{/* Voice Button */}
<Tooltip title={t('voice_input')}>
<Button
type='text'
shape='circle'
icon={<AudioOutlined className='text-gray-500 text-[18px]' />}
onClick={() => message.info(t('voice_input_coming_soon'))}
className='flex-shrink-0 h-9 w-9 transition-all duration-200 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800'
/>
</Tooltip>
{/* Send Button with blue gradient + gloss animation */}
<Button
@@ -3329,48 +3078,6 @@ const Playground: NextPage = () => {
</div>
</div>
)}
{/* Send Button with blue gradient + gloss animation */}
<Button
type='primary'
shape='circle'
icon={<ArrowUpOutlined />}
onClick={() => handleStart()}
disabled={(!query.trim() && !uploadedFile) || loading}
loading={loading}
className={`group/send relative overflow-hidden border-none shadow-lg flex-shrink-0 h-9 w-9 transition-all duration-200 ${
query.trim() || uploadedFile
? 'bg-gradient-to-br from-[#3b82f6] to-[#2563eb] hover:shadow-blue-300/40 hover:shadow-xl hover:scale-105'
: 'bg-gray-200 text-gray-400'
}`}
style={
query.trim() || uploadedFile
? { background: 'linear-gradient(135deg, #3b82f6, #2563eb)' }
: undefined
}
>
{(query.trim() || uploadedFile) && (
<span
className='absolute inset-0 opacity-0 group-hover/send:opacity-100 transition-opacity duration-300 pointer-events-none'
style={{
background:
'linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.25) 45%, rgba(255,255,255,0.35) 50%, rgba(255,255,255,0.25) 55%, transparent 60%)',
animation: 'glossSweepChat 1.8s ease-in-out infinite',
}}
/>
)}
</Button>
</div>
<style
dangerouslySetInnerHTML={{
__html: `@keyframes glossSweepChat { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }`,
}}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Panel toggle handle — placed between panels to avoid overflow clipping */}
<div className='relative z-20 flex-shrink-0'>