diff --git a/app/protos/grpc.proto b/app/protos/grpc.proto index 8747cb0a44..f8c9a9c7d1 100644 --- a/app/protos/grpc.proto +++ b/app/protos/grpc.proto @@ -2276,6 +2276,22 @@ message AIStartParams { // 为 true 时 request_plan 走 detached plan 审阅流程;默认 false 时走旧版异步 plan-and-execute。 // 需配合 EnablePlan=true 才会暴露 request_plan 动作。 bool EnableDetachedPlan = 47; + + // 执行策略集合。用于承载 multi-agent / goal mode 等顶层运行策略。 + AIExecutionStrategy Strategy = 48; +} + +message AIExecutionStrategy { + // 为 true 时启用多 Agent 模式:顶层 ReAct 暴露并偏好 dispatch_sub_react_agents, + // 倾向于把复杂任务拆成多个并行子 Agent 执行后再汇总。 + bool EnableMultiAgent = 1; + + // 为 true 时启用 Goal 模式:在达到 GoalMinIterations 之前禁止 finish 提前退出, + // 避免“做一点就汇报然后结束”。 + bool EnableGoalMode = 2; + + // Goal 模式下允许 finish 的最小迭代次数。<=0 时由服务端使用默认值。 + int64 GoalMinIterations = 3; } message AITaskFilter { diff --git a/app/renderer/src/main/src/assets/icon/outline.tsx b/app/renderer/src/main/src/assets/icon/outline.tsx index 79543a926c..9b3447ff27 100644 --- a/app/renderer/src/main/src/assets/icon/outline.tsx +++ b/app/renderer/src/main/src/assets/icon/outline.tsx @@ -6346,6 +6346,69 @@ export const OutlineUserRoundCogIcon = (props: Partial) => { return } +const OutlineGoal2 = () => ( + + + + + + + + + + +) +/** + * @description Outline/Goal 2 + */ +export const OutlineGoal2Icon = (props: Partial) => { + return +} + +const OutlineGrid2x2Check = () => ( + + + +) + +/** + * @description Outline/Grid2x2Check + */ +export const OutlineGrid2x2CheckIcon = (props: Partial) => { + return +} + +const OutlineBoxes = () => ( + + + +) + +/** + * @description Outline/Boxes + */ +export const OutlineBoxesIcon = (props: Partial) => { + return +} + const OutlineLandPlot = () => ( = (props) => { ...newCache, SyncPerceptionTrigger: false, EnablePlan: false, + Strategy: { + EnableMultiAgent: false, + EnableGoalMode: false, + GoalMinIterations: AIAgentSettingDefault.Strategy?.GoalMinIterations, + }, }) } catch (error) {} }) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 6c5b9f0c21..ddd329a35a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -285,7 +285,12 @@ export const AIAgentChat: React.FC = memo((props) => { switch (data.type as ReActChatEventEnum) { // 新开聊天对话窗 case ReActChatEventEnum.NEW_CHAT: - setSetting?.((old) => ({ ...old, SyncPerceptionTrigger: false, EnablePlan: false })) + setSetting?.((old) => ({ + ...old, + SyncPerceptionTrigger: false, + EnablePlan: false, + Strategy: { EnableMultiAgent: false, EnableGoalMode: false, GoalMinIterations: 0 }, + })) setActiveChat?.(undefined) setTimeout(() => { setMode('welcome') diff --git a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.module.scss new file mode 100644 index 0000000000..58f9ad8d96 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.module.scss @@ -0,0 +1,129 @@ +.mode-btn { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + .mode-btn-label { + font-size: 12px; + } + .mode-btn-arrow { + width: 12px; + height: 12px; + svg { + width: 12px; + height: 12px; + } + } + &:hover { + color: var(--Colors-Use-Main-Primary); + border-color: var(--Colors-Use-Main-Primary); + } +} + +// 运行模式下拉 +.mode-menu { + width: 215px; + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px 4px; + background: var(--Colors-Use-Basic-Background); + border-radius: 6px; + box-shadow: var(--yakit-box-shadow); + .mode-menu-hint { + padding: 0 8px 4px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + line-height: 16px; + } + .mode-list { + display: flex; + flex-direction: column; + gap: 2px; + } + .mode-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px; + border-radius: 4px; + cursor: pointer; + &:hover { + background: var(--Colors-Use-Neutral-Bg); + } + &.mode-option-disabled { + cursor: not-allowed; + opacity: 0.55; + &:hover { + background: transparent; + } + } + } + .mode-option-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + .mode-option-icon { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--Colors-Use-Neutral-Text-2-Primary); + svg { + width: 16px; + height: 16px; + } + } + .mode-option-label { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + line-height: 16px; + } + .mode-option-check { + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + svg { + width: 14px; + height: 14px; + color: var(--Colors-Use-Main-Primary); + } + } + .mode-option-check-placeholder { + width: 16px; + height: 16px; + flex-shrink: 0; + } + .mode-panel-divider { + height: 1px; + margin: 8px 8px 4px; + background: var(--Colors-Use-Neutral-Border); + } + .mode-goal-iter { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 4px 8px 0; + .mode-goal-iter-label { + font-size: 12px; + color: var(--Colors-Use-Neutral-Text-1-Title); + line-height: 16px; + white-space: nowrap; + } + .mode-goal-iter-input { + width: 96px; + } + } +} +.mode-dropdown { + z-index: 1050; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx new file mode 100644 index 0000000000..2bc23671fd --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx @@ -0,0 +1,274 @@ +import React, { memo, useState, type FC } from 'react' +import { Tooltip } from 'antd' +import { Dropdown } from 'antd' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { + OutlineBoxesIcon, + OutlineBrainCircuitIcon, + OutlineChevrondownIcon, + OutlineGrid2x2CheckIcon, + OutlineGoal2Icon, +} from '@/assets/icon/outline' +import { SolidCheckIcon } from '@/assets/icon/solid' +import { useCreation, useDebounceFn, useMemoizedFn } from 'ahooks' +import classNames from 'classnames' +import styles from './AIRunModeSelect.module.scss' +import { YakitInputNumber } from '@/components/yakitUI/YakitInputNumber/YakitInputNumber' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' +import { AIExecutionStrategy, AIInputEventHotPatchTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' +import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' +import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' +import emiter from '@/utils/eventBus/eventBus' +import { OutlineViewGridIcon } from '@/components/yakChat/icon' + +type ModeOptionKey = 'plan' | 'multiAgent' | 'goal' + +const ModeOptionList: { + key: ModeOptionKey + label: string + icon: FC +}[] = [ + { + key: 'plan', + label: 'Plan', + icon: OutlineBrainCircuitIcon, + }, + { + key: 'multiAgent', + label: 'Multi-Agent', + icon: OutlineBoxesIcon, + }, + { + key: 'goal', + label: 'Goal', + icon: OutlineGoal2Icon, + }, +] + +const AIRunModeSelect: React.FC = memo(() => { + const { setting, activeChat } = useAIAgentStore() + const { setSetting } = useAIAgentDispatcher() + const { handleSendConfigHotpatch } = useChatIPCDispatcher() + const { chatIPCData } = useChatIPCStore() + const execute = useCreation(() => !!chatIPCData.execute, [chatIPCData.execute]) + + const enablePlan = useCreation(() => { + return !!setting?.EnablePlan + }, [setting?.EnablePlan]) + const onSetPlan = useDebounceFn( + useMemoizedFn((checked) => { + if (execute) { + // ai运行才能热更新 plan + handleSendConfigHotpatch({ + hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnablePlan, + params: { + EnablePlan: checked, + }, + }) + } + setSetting?.((v) => ({ + ...v, + EnablePlan: checked, + })) + if (activeChat?.SessionID) { + emiter.emit( + 'sessionData', + JSON.stringify({ + type: 'updateSession', + sessionId: activeChat.SessionID, + updates: { + StartParams: { + ...(activeChat.StartParams || {}), + EnablePlan: checked, + }, + }, + }), + ) + } + }), + { wait: 200, leading: true }, + ).run + const enableMultiAgent = useCreation( + () => !!setting?.Strategy?.EnableMultiAgent, + [setting?.Strategy?.EnableMultiAgent], + ) + const enableGoalMode = useCreation(() => !!setting?.Strategy?.EnableGoalMode, [setting?.Strategy?.EnableGoalMode]) + const goalMinIterations = useCreation( + () => setting?.Strategy?.GoalMinIterations ?? 0, + [setting?.Strategy?.GoalMinIterations], + ) + const [modeVisible, setModeVisible] = useState(false) + const onSetStrategy = useDebounceFn( + useMemoizedFn((next: AIExecutionStrategy) => { + if (execute) return + setSetting?.((v) => ({ + ...v, + Strategy: { ...v.Strategy, ...next }, + })) + if (activeChat?.SessionID) { + emiter.emit( + 'sessionData', + JSON.stringify({ + type: 'updateSession', + sessionId: activeChat.SessionID, + updates: { + StartParams: { + ...(activeChat.StartParams || {}), + Strategy: { ...(activeChat.StartParams?.Strategy || {}), ...next }, + }, + }, + }), + ) + } + // 热加载扩展点:后端支持 Strategy 热更新后取消下方注释启用 hotpatch + // handleSendConfigHotpatch({ + // hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_Strategy, + // params: { Strategy: { ...setting?.Strategy, ...next } }, + // }) + }), + { wait: 200, leading: true }, + ).run + + const isModeSelected = useMemoizedFn((key: ModeOptionKey) => { + switch (key) { + case 'plan': + return enablePlan + case 'multiAgent': + return enableMultiAgent + case 'goal': + return enableGoalMode + default: + return false + } + }) + + const selectedModes = useCreation(() => { + return ModeOptionList.filter((item) => { + switch (item.key) { + case 'plan': + return enablePlan + case 'multiAgent': + return enableMultiAgent + case 'goal': + return enableGoalMode + default: + return false + } + }) + }, [enablePlan, enableMultiAgent, enableGoalMode]) + + const triggerDisplay = useCreation(() => { + const count = selectedModes.length + if (count === 0) { + return { Icon: OutlineViewGridIcon, label: '模式' } + } + if (count === 1) { + return { Icon: selectedModes[0].icon, label: selectedModes[0].label } + } + return { Icon: OutlineGrid2x2CheckIcon, label: '多模式' } + }, [selectedModes]) + + const onToggleMode = useMemoizedFn((key: ModeOptionKey) => { + // Plan 支持热更新,运行中可改;Multi-Agent / Goal 运行中不可改 + if (execute && key !== 'plan') return + switch (key) { + case 'plan': + onSetPlan(!enablePlan) + break + case 'multiAgent': + onSetStrategy({ EnableMultiAgent: !enableMultiAgent }) + break + case 'goal': + onSetStrategy({ EnableGoalMode: !enableGoalMode }) + break + default: + break + } + }) + + const isModeLocked = useMemoizedFn((key: ModeOptionKey) => { + return execute && key !== 'plan' + }) + + const TriggerIcon = triggerDisplay.Icon + + return ( + +
+ {execute ? 'AI 运行中,仅 Plan 可修改,Multi-Agent / Goal 暂不可改' : '请选择模式,可多选'} +
+
+ {ModeOptionList.map((item) => { + const Icon = item.icon + const checked = isModeSelected(item.key) + const locked = isModeLocked(item.key) + return ( +
onToggleMode(item.key)} + > +
+
+ +
+ {item.label} +
+ {checked ? ( + + ) : ( + + )} +
+ ) + })} +
+ {enableGoalMode && ( + <> +
+
+ 最小迭代次数 + +
e.stopPropagation()}> + onSetStrategy({ GoalMinIterations: (v as number) ?? 0 })} + className={styles['mode-goal-iter-input']} + /> +
+
+
+ + )} +
+ } + > + } + onClick={(e) => e.stopPropagation()} + className={styles['mode-btn']} + > + {triggerDisplay.label} + + + + ) +}) + +export default AIRunModeSelect diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index f13b8bead7..efeb431ad9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -117,6 +117,11 @@ export const AIAgentSettingDefault: AIAgentSetting = { EnablePlan: false, PlanExecTaskConcurrency: 2, EnableDetachedPlan: true, + Strategy: { + EnableMultiAgent: false, + EnableGoalMode: false, + GoalMinIterations: 0, + }, } /** mcp 自定义服务器配置类型选项 */ diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index 5180970ceb..fe8ccecb23 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -243,6 +243,11 @@ const HistoryChatList: FC<{ ...old, SyncPerceptionTrigger: info?.StartParams?.SyncPerceptionTrigger ?? false, EnablePlan: info?.StartParams?.EnablePlan ?? false, + Strategy: { + EnableMultiAgent: info?.StartParams?.Strategy?.EnableMultiAgent ?? false, + EnableGoalMode: info?.StartParams?.Strategy?.EnableGoalMode ?? false, + GoalMinIterations: info?.StartParams?.Strategy?.GoalMinIterations ?? 0, + }, })) setActiveChat && setActiveChat(info) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/template/template.module.scss b/app/renderer/src/main/src/pages/ai-agent/template/template.module.scss index 22e053dc2c..da071a3553 100644 --- a/app/renderer/src/main/src/pages/ai-agent/template/template.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/template/template.module.scss @@ -117,17 +117,6 @@ textarea.qs-input-textarea { gap: 4px; overflow: hidden; line-height: 16px; - .plan-btn { - margin-right: 0; - border-radius: 20px; - display: flex; - gap: 2px; - align-items: center; - svg { - width: 14px; - height: 14px; - } - } } .footer-right { display: flex; diff --git a/app/renderer/src/main/src/pages/ai-agent/template/template.tsx b/app/renderer/src/main/src/pages/ai-agent/template/template.tsx index 4030df171d..4339acc243 100644 --- a/app/renderer/src/main/src/pages/ai-agent/template/template.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/template/template.tsx @@ -19,18 +19,17 @@ import { FooterRightTypesComponentProps, QSInputTextareaProps, } from './type' -import { Input, Tooltip } from 'antd' +import { Input } from 'antd' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineArrowupIcon, OutlineAtsymbolIcon, - OutlineBanIcon, OutlineBrainCircuitIcon, OutlineCodeIcon, OutlineCogIcon, OutlineHandIcon, } from '@/assets/icon/outline' -import { useCreation, useDebounceFn, useInViewport, useMemoizedFn } from 'ahooks' +import { useCreation, useInViewport, useMemoizedFn } from 'ahooks' import { TextAreaRef } from 'antd/lib/input/TextArea' import classNames from 'classnames' import styles from './template.module.scss' @@ -51,7 +50,6 @@ import { YakitKeyBoard } from '@/utils/globalShortcutKey/keyboard' import { AIModelSelect } from '../aiModelList/aiModelSelect/AIModelSelect' import AIReviewRuleSelect from '@/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect' import { AIFocusMode } from '@/pages/ai-re-act/aiFocusMode/AIFocusMode' -import useAIAgentStore from '../useContext/useStore' import { isString } from 'lodash' import OpenFileDropdown, { OpenFileDropdownItem } from '../aiChatWelcome/OpenFileDropdown/OpenFileDropdown' import { UploadFileButton } from '@/pages/ai-re-act/aiReActChat/AIReActComponent' @@ -69,10 +67,7 @@ import { import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { AIMilkdownInputRef } from '../components/aiMilkdownInput/type' import { AICodeBlockCommandParams } from '../components/aiMilkdownInput/aiCodeBlock/aiCustomCodeBlockPlugin' -import useAIAgentDispatcher from '../useContext/useDispatcher' -import { YakitCheckableTag } from '@/components/yakitUI/YakitTag/YakitCheckableTag' -import { AIInputEventHotPatchTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' -import useChatIPCDispatcher from '../useContext/ChatIPCContent/useDispatcher' +import AIRunModeSelect from '../aiRunModeSelect/AIRunModeSelect' /** @name AI-Agent专用Textarea组件,行高为20px */ export const QSInputTextarea: React.FC> = memo( @@ -113,7 +108,6 @@ export const AIChatTextarea: React.FC = memo( } = props const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { chatIPCData } = useChatIPCStore() - const { handleSendConfigHotpatch } = useChatIPCDispatcher() const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) const [manualAdditionVisible, setManualAdditionVisible] = useState(false) @@ -174,8 +168,6 @@ export const AIChatTextarea: React.FC = memo( return [{ type: AIInputFooterRightEnum.AIFocusMode }] }, [props.footerRightTypes, isOpen]) - const { setting, activeChat } = useAIAgentStore() - const { setSetting } = useAIAgentDispatcher() const [disabled, setDisabled] = useState(false) const { isHovering, dropRef } = useAIChatDrop({ @@ -386,39 +378,6 @@ export const AIChatTextarea: React.FC = memo( aiMilkdownInputRef.current?.setImage() }) - const enablePlan = useCreation(() => { - return !!setting?.EnablePlan - }, [setting?.EnablePlan]) - const onSetPlan = useDebounceFn( - useMemoizedFn((checked) => { - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnablePlan, - params: { - EnablePlan: checked, - }, - }) - setSetting?.((v) => ({ - ...v, - EnablePlan: checked, - })) - if (activeChat?.SessionID) { - emiter.emit( - 'sessionData', - JSON.stringify({ - type: 'updateSession', - sessionId: activeChat.SessionID, - updates: { - StartParams: { - ...(activeChat.StartParams || {}), - EnablePlan: checked, - }, - }, - }), - ) - } - }), - { wait: 200, leading: true }, - ).run return (
= memo(
{inputFooterLeft ?? (
- - - {enablePlan ? : } - Plan - - + data.SyncPerceptionTrigger = setting.SyncPerceptionTrigger ?? AIAgentSettingDefault.SyncPerceptionTrigger data.EnablePlan = setting.EnablePlan ?? AIAgentSettingDefault.EnablePlan data.PlanExecTaskConcurrency = setting.PlanExecTaskConcurrency ?? AIAgentSettingDefault.PlanExecTaskConcurrency + data.Strategy = { + EnableMultiAgent: setting.Strategy?.EnableMultiAgent ?? AIAgentSettingDefault.Strategy?.EnableMultiAgent, + EnableGoalMode: setting.Strategy?.EnableGoalMode ?? AIAgentSettingDefault.Strategy?.EnableGoalMode, + GoalMinIterations: setting.Strategy?.GoalMinIterations ?? AIAgentSettingDefault.Strategy?.GoalMinIterations, + } if (setting?.AllowPlanUserInteract) { if (!isNil(setting?.PlanUserInteractMaxCount)) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts index 84d99eb904..56cef6e0c4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts @@ -165,6 +165,21 @@ export interface AIStartParams { * 需配合 EnablePlan=true 才会暴露 request_plan 动作。 */ EnableDetachedPlan?: boolean + /** + * 执行策略集合(多 Agent / Goal 模式等顶层运行策略)。 + * 与 EnablePlan 可同时勾选;目前策略侧不支持热更新,变更需下次启动生效。 + */ + Strategy?: AIExecutionStrategy +} + +/** 执行策略,对应后端 AIExecutionStrategy */ +export interface AIExecutionStrategy { + /** 启用多 Agent 模式:把复杂任务拆成多个并行子 Agent 执行后再汇总 */ + EnableMultiAgent?: boolean + /** 启用 Goal 模式:达到 GoalMinIterations 之前禁止提前结束 */ + EnableGoalMode?: boolean + /** Goal 模式下允许 finish 的最小迭代次数,<=0 由服务端默认 */ + GoalMinIterations?: number } interface AIEnabledCapability { @@ -178,6 +193,12 @@ export enum AIInputEventHotPatchTypeEnum { HotPatchType_AgreePolicy = 'AgreePolicy', HotPatchType_SyncPerceptionTrigger = 'SyncPerceptionTrigger', HotPatchType_EnablePlan = 'EnablePlan', + /** + * 预留:Strategy 执行策略热更新。 + * 目前策略侧暂不支持热更新,前端 onSetStrategy 仅写回本地 setting 与会话 StartParams; + * 后端支持热加载后,在 onSetStrategy 中启用该 hotpatch 调用即可。 + */ + HotPatchType_Strategy = 'Strategy', HotPatchType_EnabledCapabilities = 'EnabledCapabilities', HotPatchType_DisabledCapabilities = 'DisabledCapabilities',