Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,18 @@ function formatCountdown(remainingMs: number) {

/**
* 倒计时提示:优先使用调用方传入的权威截止时间(GUI 读工具挂起表,WebUI 读
* 网关参数上的 deadline 盖章),两端与桌面计时同源;缺失时(历史/降级数据)
* 回退为挂载时刻近似。倒计时归零立即禁止交互,随后 tool_result 把卡片
* 切到只读态
* 网关参数上的 deadline 盖章),两端与桌面计时同源。超长窗口(≈永不超时)时
* 显示很长倒计时;截止时间缺失/已过期时回退为挂载时刻的默认窗口近似,避免把
* 可作答的卡片锁死。倒计时归零立即禁止交互,随后 tool_result 把卡片切到只读态
*
* 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,
* 一个仍在挂起的提问会在挂载瞬间就显示过期(或远超完整窗口)。因此仅当
* 截止时间落在“挂载时刻(不含)~挂载时刻 + 完整应答窗口(含)”内才采信,
* 否则视为时钟不可比、回退挂载近似,避免把可作答的卡片锁死;真正过期的
* 提交仍由桌面挂起表权威拒绝。
* 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,一个
* 仍在挂起的提问会在挂载瞬间显示过期。仅采信“挂载时刻之后”的截止时间;真正
* 过期的提交仍由桌面挂起表权威拒绝。
*/
function useAnswerCountdown(active: boolean, deadlineAt?: number) {
const [mountedAt] = useState(() => Date.now());
const deadline =
deadlineAt !== undefined &&
deadlineAt > mountedAt &&
deadlineAt <= mountedAt + ASK_USER_QUESTION_TIMEOUT_MS
deadlineAt !== undefined && deadlineAt > mountedAt
? deadlineAt
: mountedAt + ASK_USER_QUESTION_TIMEOUT_MS;
const [remainingMs, setRemainingMs] = useState(() => deadline - Date.now());
Expand Down
8 changes: 6 additions & 2 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.systemTools": "系统工具",
"settings.systemToolsDesc":
"查看 LiveAgent 在 Agent 模式下自动注册的内置工具,并为每个工具设置审批策略(放行 / 执行前询问 / 拒绝)。",
"settings.interactiveTimeout.title": "交互式应答超时",
"settings.interactiveTimeout.unit": "分钟",
"settings.builtinToolCategory.fs": "文件系统",
"settings.builtinToolCategory.process": "终端与进程",
"settings.builtinToolCategory.intelligence": "智能与记忆",
Expand Down Expand Up @@ -1316,7 +1318,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.builtinTool.ask_user_question.name": "用户提问",
"settings.builtinTool.ask_user_question.desc": "以选项卡片向你提问并等待选择",
"settings.builtinTool.ask_user_question.detail":
"模型在需要你决策时发起选择题(一次最多 4 个问题,每题 2-6 个选项且各题数量一致,推荐项排在首位)。卡片暂停执行等待作答,3 分钟内未作答自动按推荐项继续执行;桌面端与 WebUI 均可作答,点击停止可跳过。仅在对话场景注册。",
"模型在需要你决策时发起选择题(一次最多 4 个问题,每题 2-6 个选项且各题数量一致,推荐项排在首位)。卡片暂停执行等待作答,超时时间可用下方滑块调整,超时后未作答自动按推荐项继续执行;桌面端与 WebUI 均可作答,点击停止可跳过。仅在对话场景注册。",
"settings.builtinTool.cron_task_manager.name": "定时任务",
"settings.builtinTool.cron_task_manager.desc": "创建与管理定时自动任务",
"settings.builtinTool.cron_task_manager.detail":
Expand Down Expand Up @@ -3466,6 +3468,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.systemTools": "System Tools",
"settings.systemToolsDesc":
"View the built-in tools that LiveAgent registers automatically in Agent mode, and set an approval policy per tool (allow / ask before running / deny).",
"settings.interactiveTimeout.title": "Interactive answer timeout",
"settings.interactiveTimeout.unit": "min",
"settings.builtinToolCategory.fs": "File System",
"settings.builtinToolCategory.process": "Terminal & Processes",
"settings.builtinToolCategory.intelligence": "Intelligence & Memory",
Expand Down Expand Up @@ -3540,7 +3544,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.builtinTool.ask_user_question.desc":
"Ask you multiple-choice questions in a card and wait for your selections",
"settings.builtinTool.ask_user_question.detail":
"Lets the model ask you multiple-choice questions when a decision is yours to make (up to 4 questions per call, 2-6 options each with the same count across questions, recommended option shown first). Execution pauses on an interactive card until you answer — from the desktop or the WebUI; after 3 minutes without an answer the recommended options are auto-selected, and pressing Stop skips the question. Chat sessions only.",
"Lets the model ask you multiple-choice questions when a decision is yours to make (up to 4 questions per call, 2-6 options each with the same count across questions, recommended option shown first). Execution pauses on an interactive card until you answer — from the desktop or the WebUI; the answer window is adjustable with the slider below, and when it elapses without an answer the recommended options are auto-selected. Pressing Stop skips the question. Chat sessions only.",
"settings.builtinTool.cron_task_manager.name": "Scheduled Tasks",
"settings.builtinTool.cron_task_manager.desc": "Create and manage scheduled automations",
"settings.builtinTool.cron_task_manager.detail":
Expand Down
4 changes: 3 additions & 1 deletion crates/agent-gateway/web/src/lib/chat/askUserQuestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
export const ASK_USER_QUESTION_MAX_QUESTIONS = 4;
export const ASK_USER_QUESTION_MIN_OPTIONS = 2;
export const ASK_USER_QUESTION_MAX_OPTIONS = 6;
/** 每轮提问的应答窗口:超时后按推荐项(缺省第一项)自动落定继续执行。 */
/** 每轮提问的应答窗口默认值(毫秒):超时后按推荐项(缺省第一项)自动落定继续执行。
* 运行时实际窗口由设置 system.interactiveTimeoutMinutes 驱动(正数分钟;超长≈永不超时),
* 此常量仅作未注入时的兜底默认,保持历史行为与测试注入口径一致。 */
export const ASK_USER_QUESTION_TIMEOUT_MS = 3 * 60 * 1000;
/** UI 合成"其他(自行输入)"应答的最大长度;超出部分截断。 */
export const ASK_USER_QUESTION_CUSTOM_MAX_LENGTH = 2000;
Expand Down
16 changes: 16 additions & 0 deletions crates/agent-gateway/web/src/lib/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ export type SystemProxyConfig = {
/** 工具审批策略:allow 直接执行、ask 执行前请求用户批准、deny 直接拒绝。 */
export type ToolPolicy = "allow" | "ask" | "deny";

/** 交互式应答(AskUserQuestion 提问卡 + 工具审批栏)的等待窗口,单位分钟。
* 正数 = 超时窗口,超时后按各交互既定姿态落定(提问自动选推荐项并继续、
* 审批按拒绝)。两处交互共用同一窗口。填很大的数(如 99999)≈ 永不超时。 */
export const INTERACTIVE_TIMEOUT_DEFAULT_MINUTES = 3;

export type SystemSettings = {
executionMode: ExecutionMode;
workdir: string;
Expand All @@ -188,6 +193,8 @@ export type SystemSettings = {
* 回写会丢掉桌面端设置的策略。策略的裁决在桌面端 resolveToolPolicy。
*/
toolPolicies?: Record<string, ToolPolicy>;
/** 交互式应答超时(分钟):正数=窗口,超长≈永不。与桌面端对齐原样透传。 */
interactiveTimeoutMinutes: number;
workspaceProjects: WorkspaceProject[];
activeWorkspaceProjectId?: string;
hiddenWorkspaceProjectPaths: string[];
Expand Down Expand Up @@ -1678,12 +1685,20 @@ export function normalizeSystemProxyConfig(input: unknown): SystemProxyConfig {
};
}

/** 归一化交互式应答超时(分钟):正数=窗口(超长≈永不),缺省/非法/非正回 3。 */
export function normalizeInteractiveTimeoutMinutes(input: unknown): number {
return typeof input === "number" && Number.isFinite(input) && input > 0
? input
: INTERACTIVE_TIMEOUT_DEFAULT_MINUTES;
}

export function normalizeSystemSettings(input: unknown): SystemSettings {
const obj = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
return {
executionMode: normalizeExecutionMode(obj.executionMode),
workdir: normalizeWorkdir(obj.workdir),
toolPolicies: normalizeToolPolicies(obj.toolPolicies),
interactiveTimeoutMinutes: normalizeInteractiveTimeoutMinutes(obj.interactiveTimeoutMinutes),
workspaceProjects: normalizeWorkspaceProjects(obj.workspaceProjects),
activeWorkspaceProjectId:
typeof obj.activeWorkspaceProjectId === "string" && obj.activeWorkspaceProjectId.trim()
Expand Down Expand Up @@ -2235,6 +2250,7 @@ export function getDefaultSettings(): AppSettings {
missingWorkspaceProjectPaths: [],
archivedWorkspaceProjectPaths: [],
systemProxy: getDefaultSystemProxyConfig(),
interactiveTimeoutMinutes: INTERACTIVE_TIMEOUT_DEFAULT_MINUTES,
},
customProviders,
mcp: {
Expand Down
39 changes: 37 additions & 2 deletions crates/agent-gateway/web/src/pages/settings/SystemToolsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { type ToolPolicy, updateSystem } from "../../lib/settings";
import { BUILTIN_TOOL_CATALOG, BUILTIN_TOOL_CATEGORIES } from "../../lib/tools/builtinToolCatalog";
import type { SettingsSectionProps } from "./types";

// 档位表:60 分钟内细调,超过 1 小时直跳最大档 99999。
const TIMEOUT_STOPS = [
1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 30, 40, 50, 60, 99999,
];

export function SystemToolsSection(props: SettingsSectionProps) {
const { settings, setSettings } = props;
const { t } = useLocale();
Expand Down Expand Up @@ -51,6 +56,19 @@ export function SystemToolsSection(props: SettingsSectionProps) {

const overriddenCount = Object.keys(policies).length;

const timeoutMinutes = settings.system.interactiveTimeoutMinutes;
const timeoutStopIndex = TIMEOUT_STOPS.reduce(
(best, stop, i) =>
Math.abs(stop - timeoutMinutes) < Math.abs(TIMEOUT_STOPS[best] - timeoutMinutes) ? i : best,
0,
);
const onTimeoutStopChange = (index: number) => {
const next = TIMEOUT_STOPS[index];
if (next !== timeoutMinutes) {
setSettings((prev) => updateSystem(prev, { interactiveTimeoutMinutes: next }));
}
};

return (
<div className="space-y-4">
<div className="flex items-start gap-3">
Expand Down Expand Up @@ -80,7 +98,7 @@ export function SystemToolsSection(props: SettingsSectionProps) {
{entries.map((entry) => {
const policy = effectivePolicy(entry.toolName, entry.isReadOnly);
return (
<div key={entry.id} className="flex items-center gap-3 px-3 py-2.5">
<div key={entry.id} className="flex items-start gap-3 px-3 py-2.5">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">
<span className="text-sm font-medium">
Expand All @@ -99,7 +117,24 @@ export function SystemToolsSection(props: SettingsSectionProps) {
{t(`settings.builtinTool.${entry.id}.desc`)}
</div>
</div>
{entry.isReadOnly ? (
{entry.isReadOnly && entry.id === "ask_user_question" ? (
<div className="w-52 shrink-0">
<input
type="range"
min={0}
max={TIMEOUT_STOPS.length - 1}
step={1}
value={timeoutStopIndex}
aria-label={t("settings.interactiveTimeout.title")}
onChange={(event) => onTimeoutStopChange(Number(event.currentTarget.value))}
className="w-full accent-primary"
/>
<div className="mt-1 text-right text-xs text-muted-foreground">
<span className="font-medium tabular-nums">{timeoutMinutes}</span>{" "}
{t("settings.interactiveTimeout.unit")}
</div>
</div>
) : entry.isReadOnly ? (
<span className="shrink-0 text-[11px] text-muted-foreground/60">
{t("settings.toolPolicy.allow")}
</span>
Expand Down
12 changes: 12 additions & 0 deletions crates/agent-gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
} from "./lib/settings/sync";
import { applyStoredGlobalShortcuts } from "./lib/shortcuts/globalShortcuts";
import { applyFontFamilies } from "./lib/system/fontFamily";
import { setAskUserQuestionTimeoutMs } from "./lib/tools/askUserQuestionTools";
import { setToolApprovalTimeoutMs } from "./lib/tools/toolApproval";
import { ChatPage } from "./pages/ChatPage";
import { SettingsPage } from "./pages/SettingsPage";
import type { SectionId } from "./pages/settings/types";
Expand Down Expand Up @@ -380,6 +382,16 @@ export default function App() {
const getMcpSettings = useCallback(() => settingsRef.current.mcp, []);
const getToolPolicies = useCallback(() => settingsRef.current.system.toolPolicies, []);

// 把交互式应答超时设置(分钟)注入工具运行时窗口(毫秒):AskUserQuestion 与
// 工具审批共用同一窗口;永不超时用很大的分钟数表达。设置变更后新挂起的提问/
// 审批生效,已挂起的沿用旧窗口。模块级配置由工具侧 ensureAskUserQuestionDeadlineAt /
// requestToolApproval 读取,避免在 6+ 处工具预览调用点逐个传参。
useEffect(() => {
const ms = settings.system.interactiveTimeoutMinutes * 60_000;
setAskUserQuestionTimeoutMs(ms);
setToolApprovalTimeoutMs(ms);
}, [settings.system.interactiveTimeoutMinutes]);

const reloadPersistedSettings = useCallback(async () => {
await saveChainRef.current.catch(() => undefined);
const { settings: loaded, defaultWorkdir } = await loadPersistedSettingsWithDefaults();
Expand Down
18 changes: 7 additions & 11 deletions crates/agent-gui/src/components/chat/AskUserQuestionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,18 @@ function formatCountdown(remainingMs: number) {

/**
* 倒计时提示:优先使用调用方传入的权威截止时间(GUI 读工具挂起表,WebUI 读
* 网关参数上的 deadline 盖章),两端与桌面计时同源;缺失时(历史/降级数据)
* 回退为挂载时刻近似。倒计时归零立即禁止交互,随后 tool_result 把卡片
* 切到只读态
* 网关参数上的 deadline 盖章),两端与桌面计时同源。超长窗口(≈永不超时)时
* 显示很长倒计时;截止时间缺失/已过期时回退为挂载时刻的默认窗口近似,避免把
* 可作答的卡片锁死。倒计时归零立即禁止交互,随后 tool_result 把卡片切到只读态
*
* 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,
* 一个仍在挂起的提问会在挂载瞬间就显示过期(或远超完整窗口)。因此仅当
* 截止时间落在“挂载时刻(不含)~挂载时刻 + 完整应答窗口(含)”内才采信,
* 否则视为时钟不可比、回退挂载近似,避免把可作答的卡片锁死;真正过期的
* 提交仍由桌面挂起表权威拒绝。
* 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,一个
* 仍在挂起的提问会在挂载瞬间显示过期。仅采信“挂载时刻之后”的截止时间;真正
* 过期的提交仍由桌面挂起表权威拒绝。
*/
function useAnswerCountdown(active: boolean, deadlineAt?: number) {
const [mountedAt] = useState(() => Date.now());
const deadline =
deadlineAt !== undefined &&
deadlineAt > mountedAt &&
deadlineAt <= mountedAt + ASK_USER_QUESTION_TIMEOUT_MS
deadlineAt !== undefined && deadlineAt > mountedAt
? deadlineAt
: mountedAt + ASK_USER_QUESTION_TIMEOUT_MS;
const [remainingMs, setRemainingMs] = useState(() => deadline - Date.now());
Expand Down
8 changes: 6 additions & 2 deletions crates/agent-gui/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.systemTools": "系统工具",
"settings.systemToolsDesc":
"查看 LiveAgent 在 Agent 模式下自动注册的内置工具,并为每个工具设置审批策略(放行 / 执行前询问 / 拒绝)。",
"settings.interactiveTimeout.title": "交互式应答超时",
"settings.interactiveTimeout.unit": "分钟",
"settings.builtinToolCategory.fs": "文件系统",
"settings.builtinToolCategory.process": "终端与进程",
"settings.builtinToolCategory.intelligence": "智能与记忆",
Expand Down Expand Up @@ -1386,7 +1388,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.builtinTool.ask_user_question.name": "用户提问",
"settings.builtinTool.ask_user_question.desc": "以选项卡片向你提问并等待选择",
"settings.builtinTool.ask_user_question.detail":
"模型在需要你决策时发起选择题(一次最多 4 个问题,每题 2-6 个选项且各题数量一致,推荐项排在首位)。卡片暂停执行等待作答,3 分钟内未作答自动按推荐项继续执行;桌面端与 WebUI 均可作答,点击停止可跳过。仅在对话场景注册。",
"模型在需要你决策时发起选择题(一次最多 4 个问题,每题 2-6 个选项且各题数量一致,推荐项排在首位)。卡片暂停执行等待作答,超时时间可用下方滑块调整,超时后未作答自动按推荐项继续执行;桌面端与 WebUI 均可作答,点击停止可跳过。仅在对话场景注册。",
"settings.builtinTool.cron_task_manager.name": "定时任务",
"settings.builtinTool.cron_task_manager.desc": "创建与管理定时自动任务",
"settings.builtinTool.cron_task_manager.detail":
Expand Down Expand Up @@ -3629,6 +3631,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.systemTools": "System Tools",
"settings.systemToolsDesc":
"View the built-in tools that LiveAgent registers automatically in Agent mode, and set an approval policy per tool (allow / ask before running / deny).",
"settings.interactiveTimeout.title": "Interactive answer timeout",
"settings.interactiveTimeout.unit": "min",
"settings.builtinToolCategory.fs": "File System",
"settings.builtinToolCategory.process": "Terminal & Processes",
"settings.builtinToolCategory.intelligence": "Intelligence & Memory",
Expand Down Expand Up @@ -3703,7 +3707,7 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.builtinTool.ask_user_question.desc":
"Ask you multiple-choice questions in a card and wait for your selections",
"settings.builtinTool.ask_user_question.detail":
"Lets the model ask you multiple-choice questions when a decision is yours to make (up to 4 questions per call, 2-6 options each with the same count across questions, recommended option shown first). Execution pauses on an interactive card until you answer — from the desktop or the WebUI; after 3 minutes without an answer the recommended options are auto-selected, and pressing Stop skips the question. Chat sessions only.",
"Lets the model ask you multiple-choice questions when a decision is yours to make (up to 4 questions per call, 2-6 options each with the same count across questions, recommended option shown first). Execution pauses on an interactive card until you answer — from the desktop or the WebUI; the answer window is adjustable with the slider below, and when it elapses without an answer the recommended options are auto-selected. Pressing Stop skips the question. Chat sessions only.",
"settings.builtinTool.cron_task_manager.name": "Scheduled Tasks",
"settings.builtinTool.cron_task_manager.desc": "Create and manage scheduled automations",
"settings.builtinTool.cron_task_manager.detail":
Expand Down
Loading
Loading