diff --git a/src-tauri/src/commands/ai/feishu_notify.rs b/src-tauri/src/commands/ai/feishu_notify.rs index 3d79f51..a1ad93d 100644 --- a/src-tauri/src/commands/ai/feishu_notify.rs +++ b/src-tauri/src/commands/ai/feishu_notify.rs @@ -1,33 +1,35 @@ -//! 飞书告警出口(todo 37 监控轮询):挂机引擎自监控快照的推送通道。 +//! 飞书推送出口(todo 37 监控轮询 + 挂机开关确认卡)。 //! -//! 职责单一:把 unattended.rs run_self_monitor 的异常快照送达指定飞书群。 +//! 职责单一:把挂机引擎的异常快照与开关确认消息送达指定飞书群。 //! 复用 feishu_channel 的卡片发送链路(md_to_lark_friendly 净化 + markdown 卡), //! 目标 chat 显式传入(KV df-monitor-chat-id),不走 conv 绑定路由——监控告警 //! 是运维通道,与会话投影(卡片滚动句柄/终态判据)互不污染。 //! -//! 失败语义:告警是锦上添花,发送失败只落 feishu_log 不 panic 不重试—— +//! 失败语义:推送是锦上添花,发送失败只落 feishu_log 不 panic 不重试—— //! 下一监控周期同段异常仍会再次尝试(同段只告警一次的闸在调用方)。 use crate::feishu_channel::{feishu_log, send_markdown_card}; -/// 监控告警推送:快照文本直发 monitor_chat(飞书 markdown 卡)。 -/// 渠道未启动/发送失败均静默降级为 feishu_log 落盘(告警不阻塞引擎主循环)。 -pub(crate) async fn push_monitor_alert(app: &tauri::AppHandle, chat_id: &str, snapshot: &str) { - let _ = app; // AppHandle 预留(将来查 DB 富化告警),当前快照自包含 +/// 通用推卡:文本直发指定 chat_id(飞书 markdown 卡)。 +/// 未配目标/渠道未启动/发送失败均降级为 feishu_log 落盘,不阻塞调用方主流程。 +pub(crate) async fn push_card(_app: &tauri::AppHandle, chat_id: &str, text: &str) { if chat_id.is_empty() { - feishu_log(&format!("[monitor] 未配置 df-monitor-chat-id,告警仅落日志: {snapshot}")); + feishu_log(&format!("[notify] 未配置 df-monitor-chat-id,仅落日志: {text}")); return; } let Some(client) = crate::feishu_channel::active_client() else { - feishu_log(&format!( - "[monitor] 飞书渠道未启动,告警仅落日志: {snapshot}" - )); + feishu_log(&format!("[notify] 飞书渠道未启动,仅落日志: {text}")); return; }; - let text = format!("🛡 **DevFlow 挂机监控告警**\n{snapshot}"); - let sent = send_markdown_card(client, "", chat_id, &text).await; + let sent = send_markdown_card(client, "", chat_id, text).await; match sent { - Some(mid) => feishu_log(&format!("[monitor] 告警已推送 chat={chat_id} card={mid}")), - None => feishu_log(&format!("[monitor] 告警推送失败(卡片与文本均不可达),快照: {snapshot}")), + Some(mid) => feishu_log(&format!("[notify] 已推送 chat={chat_id} card={mid}")), + None => feishu_log(&format!("[notify] 推送失败(卡片与文本均不可达),原文: {text}")), } } + +/// 监控告警推送:异常快照加告警头直发 monitor_chat。 +pub(crate) async fn push_monitor_alert(app: &tauri::AppHandle, chat_id: &str, snapshot: &str) { + let text = format!("🛡 **DevFlow 挂机监控告警**\n{snapshot}"); + push_card(app, chat_id, &text).await; +} diff --git a/src-tauri/src/commands/ai/unattended.rs b/src-tauri/src/commands/ai/unattended.rs index 7d03c8a..7c022df 100644 --- a/src-tauri/src/commands/ai/unattended.rs +++ b/src-tauri/src/commands/ai/unattended.rs @@ -494,6 +494,64 @@ async fn run_self_monitor(app: &AppHandle, cfg: &EngineConfig) { .await; } +// ============================================================ +// IPC:引擎状态查询 + 模式开关(设置页挂机区块 / TopBar 徽章消费) +// ============================================================ + +/// 引擎状态快照(KV 配置 + 引擎静态状态,纯读零副作用) +#[derive(Debug, Clone, serde::Serialize)] +pub struct UnattendedStatus { + enabled: bool, + claim_scope: String, + in_flight_conv: Option, + breaker_tripped: bool, + poll_secs: u64, +} + +/// 查询挂机引擎当前状态(设置页挂机区块回显 / TopBar 徽章轮询) +#[tauri::command] +pub async fn unattended_status(app: AppHandle) -> Result { + let cfg = read_config(&app).await; + let e = engine(); + let in_flight = e + .in_flight + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone(); + Ok(UnattendedStatus { + enabled: cfg.enabled, + claim_scope: match cfg.claim_scope { + ClaimScope::RedOnly => "red_only".to_string(), + ClaimScope::AllOpen => "all_open".to_string(), + }, + in_flight_conv: in_flight, + breaker_tripped: e.breaker_tripped.load(Ordering::SeqCst), + poll_secs: cfg.poll_secs, + }) +} + +/// 挂机模式开关:写 KV(引擎每轮热读)+ 唤醒轮询器即时生效 + 经飞书推确认卡。 +/// 关闭语义 = 引擎空转(下一轮只读配置即停),不动在途任务。 +#[tauri::command] +pub async fn unattended_set_enabled(app: AppHandle, enabled: bool) -> Result { + let state = app.state::(); + state + .settings + .set(KV_ENABLED, if enabled { "true" } else { "false" }) + .await + .map_err(|e| e.to_string())?; + // 唤醒轮询器:开=不等当前周期走完立即扫板(领单/恢复 parked),关=立即空转并复位熔断 + engine().notify.notify_one(); + let chat = read_config(&app).await.monitor_chat; + let text = if enabled { + "🛌 挂机模式已开启:将自动认领看板高危任务" + } else { + "挂机模式已关闭" + }; + crate::commands::ai::feishu_notify::push_card(&app, chat.as_deref().unwrap_or(""), text).await; + Ok(enabled) +} + const RECOVER_NONE: &str = "无可恢复 parked 会话"; /// 提交冻结标志同步(39 治理):读 KV 覆盖标志 + 看板 🔒 行,写入执行闸静态位。 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 676e333..32bfca2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -508,6 +508,9 @@ pub fn run() { commands::ai::ai_conversation_list, commands::ai::ai_list_recoverable, commands::ai::ai_recover_parked, + // 挂机引擎(无人值守):状态查询 + 模式开关(设置页挂机区块/TopBar 徽章) + commands::ai::unattended::unattended_status, + commands::ai::unattended::unattended_set_enabled, commands::ai::ai_conversation_switch, // S3 子代理轨迹只读窥视(复用 switch 读路径,不切任何会话状态) commands::ai::ai_conversation_peek, diff --git a/src/components/ai/TaskBoundaryCard.vue b/src/components/ai/TaskBoundaryCard.vue index 8ef6bce..dce37ff 100644 --- a/src/components/ai/TaskBoundaryCard.vue +++ b/src/components/ai/TaskBoundaryCard.vue @@ -1,5 +1,7 @@ @@ -413,74 +331,27 @@ const echoSummary = computed(() => { font-size: 10.5px; color: var(--df-danger); } -.ai-boundary-dir-err { - font-size: 10.5px; - color: var(--df-danger); -} .ai-boundary-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } -.ai-boundary-row--col { - flex-direction: column; - align-items: stretch; - gap: 5px; -} .ai-boundary-label { font-weight: 600; color: var(--df-text); } -.ai-boundary-radio { +/* 预算「不限」勾选(勾中即不设该项上限,输入框禁用) */ +.ai-boundary-unlimited { display: inline-flex; align-items: center; gap: 3px; cursor: pointer; color: var(--df-text-dim); -} -.ai-boundary-radio.is-checked { color: var(--df-accent); } -.ai-boundary-radio input { accent-color: var(--df-accent); margin: 0; } -.ai-boundary-radio:has(input:disabled) { cursor: not-allowed; opacity: 0.6; } -.ai-boundary-dirs { - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.ai-boundary-dir { - display: inline-flex; - align-items: center; - gap: 2px; - padding: 1px 4px; - background: var(--df-bg-card); - border: 0.5px solid var(--df-border); - border-radius: calc(var(--df-radius) - 2px); - font-size: 10.5px; - color: var(--df-text); - max-width: 100%; -} -.ai-boundary-dir-path { - overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; - direction: rtl; - font-family: var(--df-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); -} -.ai-boundary-dir-x { - border: none; - background: none; - color: var(--df-text-dim); - cursor: pointer; - font-size: 12px; - line-height: 1; - padding: 0 2px; -} -.ai-boundary-dir-x:hover:not(:disabled) { color: var(--df-danger); } -.ai-boundary-dir-x:disabled { cursor: not-allowed; opacity: 0.5; } -.ai-boundary-dir-add { - display: flex; - gap: 4px; } +.ai-boundary-unlimited input { accent-color: var(--df-accent); margin: 0; } +.ai-boundary-unlimited:has(input:disabled) { cursor: not-allowed; opacity: 0.6; } .ai-boundary-input { flex: 1; min-width: 0; diff --git a/src/components/ai/TopBar.vue b/src/components/ai/TopBar.vue index ffcbc9c..17b8609 100644 --- a/src/components/ai/TopBar.vue +++ b/src/components/ai/TopBar.vue @@ -20,6 +20,25 @@ > ⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }} + + +
@@ -281,7 +300,10 @@ // 父持有 clear/compress/confirm 逻辑 + 单一 toast 源(provider-switched 回父弹 toast)。 import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue' import { useI18n } from 'vue-i18n' +import { useRouter } from 'vue-router' +import { invoke } from '@tauri-apps/api/core' import { useAiStore } from '../../stores/ai' +import { useAppSettingsStore } from '../../stores/appSettings' import { aiApi } from '../../api/ai' import { pendingTaskProgress } from '../../composables/ai/useAiPendingState' import { convRounds } from '../../composables/ai/aiShared' @@ -335,6 +357,54 @@ const emit = defineEmits<{ const store = useAiStore() const { t } = useI18n() +// ── 挂机引擎态徽章(P1 总装):消费 unattended_status,开启/熔断时 30s 级轮询刷新 ── +// 关闭态不显示徽章,轮询降频到 2 分钟(仍能感知设置页里的开启动作;单次 KV 级纯读,开销可忽略) +interface UnattendedEngineStatus { + enabled: boolean + claim_scope: string + in_flight_conv: string | null + breaker_tripped: boolean + poll_secs: number +} +const UNATTENDED_ACTIVE_MS = 30_000 +const UNATTENDED_IDLE_MS = 120_000 +const router = useRouter() +const appSettings = useAppSettingsStore() +const unattendedStatus = ref(null) +let unattendedTimer: ReturnType | null = null +// 卸载守卫:onBeforeUnmount 清定时器后,在途 invoke 的续延还会回来排下一轮,须拦住防卸载后空转 +let unattendedDisposed = false +async function refreshUnattended() { + if (unattendedDisposed) return + try { + unattendedStatus.value = await invoke('unattended_status') + } catch { /* IPC 失败保留上次状态,下轮重试 */ } + if (unattendedDisposed) return + const s = unattendedStatus.value + const delay = s?.enabled || s?.breaker_tripped ? UNATTENDED_ACTIVE_MS : UNATTENDED_IDLE_MS + unattendedTimer = setTimeout(() => void refreshUnattended(), delay) +} +/** 点击徽章跳设置页挂机区块(集成类首面板;activeCategory 由 appSettings 持久化驱动) */ +function jumpToUnattendedSettings() { + void appSettings.set('df-settings-active-category', 'integrations') + void router.push('/settings') +} +/** 熔断复位:开关 false→true(引擎在关态轮询里清熔断与失败计数,再开即恢复领单) */ +async function resetUnattendedBreaker() { + try { + await invoke('unattended_set_enabled', { enabled: false }) + await invoke('unattended_set_enabled', { enabled: true }) + } catch (e) { + console.warn('[topbar] 挂机熔断复位失败:', e) + } + void refreshUnattended() +} +onMounted(() => { void refreshUnattended() }) +onBeforeUnmount(() => { + unattendedDisposed = true + if (unattendedTimer) { clearTimeout(unattendedTimer); unattendedTimer = null } +}) + // ══ 更多菜单 popout 状态(上下文管理操作收纳)══ // 顶部按钮过多时,低频的清空/压缩上下文收入 ... popout;点击按钮切换,点击外部收起。 const moreMenuOpen = ref(false) @@ -800,6 +870,31 @@ onBeforeUnmount(() => { outline: 1.5px solid var(--df-accent, #4a9eff); outline-offset: 1px; } +/* 挂机引擎态徽章(开启=accent 弱底;熔断=红底红字,点击复位) */ +.ai-unattended-badge { + display: inline-flex; + align-items: center; + cursor: pointer; + background: var(--df-accent-bg); + color: var(--df-accent); + padding: 2px 8px; + border-radius: var(--df-radius-sm); + font-size: 11px; + font-weight: 500; + white-space: nowrap; + font-family: inherit; + appearance: none; + border: none; + transition: opacity 0.15s var(--df-ease); +} +.ai-unattended-badge:hover { + opacity: 0.85; +} +.ai-unattended-badge--tripped { + background: var(--df-danger-bg); + color: var(--df-danger); + border: 0.5px solid var(--df-danger); +} /* ═══ 模型选择器(对话透明化 L1:紧凑下拉) ═══ */ .ai-model-picker { diff --git a/src/components/settings/UnattendedPanel.vue b/src/components/settings/UnattendedPanel.vue new file mode 100644 index 0000000..3e8e8ec --- /dev/null +++ b/src/components/settings/UnattendedPanel.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/components/settings/searchIndex.ts b/src/components/settings/searchIndex.ts index 46adb7f..20c0686 100644 --- a/src/components/settings/searchIndex.ts +++ b/src/components/settings/searchIndex.ts @@ -86,6 +86,12 @@ export const SETTINGS_INDEX: SettingIndexEntry[] = [ { category: 'security', key: 'scriptBlacklist', labelKey: 'settings.labelScriptBlacklist', descKey: 'settings.descScriptBlacklist' }, // ===== 集成 ===== + // 挂机模式(引擎自动认领看板任务;TopBar 徽章点击跳转即落到本类) + { category: 'integrations', key: 'unattended', labelKey: 'settings.panelUnattended' }, + { category: 'integrations', key: 'unattendedEnabled', labelKey: 'settings.labelUnattendedEnabled', descKey: 'settings.descUnattendedEnabled' }, + { category: 'integrations', key: 'unattendedScope', labelKey: 'settings.labelUnattendedScope', descKey: 'settings.descUnattendedScope' }, + { category: 'integrations', key: 'unattendedMonitorChat', labelKey: 'settings.labelUnattendedMonitorChat', descKey: 'settings.descUnattendedMonitorChat' }, + { category: 'integrations', key: 'unattendedPollSecs', labelKey: 'settings.labelUnattendedPollSecs', descKey: 'settings.descUnattendedPollSecs' }, { category: 'integrations', key: 'feishu', labelKey: 'settings.panelFeishu' }, { category: 'integrations', key: 'feishuEnabled', labelKey: 'settings.labelFeishuEnabled', descKey: 'settings.descFeishuEnabled' }, { category: 'integrations', key: 'feishuAppId', labelKey: 'settings.labelFeishuAppId', descKey: 'settings.descFeishuAppId' }, diff --git a/src/composables/ai/aiShared.ts b/src/composables/ai/aiShared.ts index 569820c..f43bf51 100644 --- a/src/composables/ai/aiShared.ts +++ b/src/composables/ai/aiShared.ts @@ -532,10 +532,14 @@ export function flushCurrentText(): void { resetDelta() return } - // 从末尾向前找最后一个非 isError assistant 气泡写入(跳过重试错误气泡,保留部分回复) + // 从末尾向前找最后一个非 isError assistant 气泡写入。skip 模式而非 break: + // 尾部可能插着非 assistant 消息(incomplete 系统提示/error 气泡/重试过渡气泡等), + // 原 break 会让循环在非 assistant 处提前终止,本轮 LLM 回复文本不写入任何气泡即被 + // 清空,屏幕只剩空气泡+光标(用户多次反馈的「流式后瞬间清空」主根因)。 + // continue 跳过非 assistant,继续向前找最近的 assistant 气泡承接。 for (let i = s.messages.length - 1; i >= 0; i--) { const m = s.messages[i] - if (m.role !== 'assistant') break + if (m.role !== 'assistant') continue if (!m.isError) { m.content = s.currentText break diff --git a/src/composables/ai/useAiStreamingEvents.ts b/src/composables/ai/useAiStreamingEvents.ts index 01beff5..52a1a12 100644 --- a/src/composables/ai/useAiStreamingEvents.ts +++ b/src/composables/ai/useAiStreamingEvents.ts @@ -49,12 +49,20 @@ export function handleStreamingEvent(event: AiChatEvent): boolean { flushCurrentText() state.currentText = '' clearTextIdleTimer() - state.messages.push({ - id: `ai-${nextMsgId()}` as MessageId, - role: 'assistant', - content: '', - timestamp: Date.now(), - }) + // 防空气泡叠罗汉(BUG-2026-10-07):flush 后若末条仍是「空 content 且非 isError 的 + // assistant 气泡」,说明上一轮 LLM 只发 tool_calls 无文本(工具结果在 toolCalls 桶不在 + // content),直接复用该气泡承接本轮,不再新推——否则纯工具轮每轮推一个空气泡, + // 折叠后尾泡渲染成「空气泡+光标」(BUG-260624 表症根因之二)。 + const tail = state.messages[state.messages.length - 1] + const reusableTail = tail && tail.role === 'assistant' && !tail.isError && !tail.content?.trim() + if (!reusableTail) { + state.messages.push({ + id: `ai-${nextMsgId()}` as MessageId, + role: 'assistant', + content: '', + timestamp: Date.now(), + }) + } // round>0 才更新进度条(round=0 是审批通过后「隔开新一轮」的占位,实际轮次未推进,防闪烁) if (event.round > 0) state.agentRound = event.round // #14 桌面任务卡:per-conv 轮次追踪,前台会话在此写(取 max 防事件乱序回退); diff --git a/src/i18n/en/ai.ts b/src/i18n/en/ai.ts index 9b2e326..89391eb 100644 --- a/src/i18n/en/ai.ts +++ b/src/i18n/en/ai.ts @@ -133,7 +133,7 @@ export default { circuit_breaker: 'Circuit breaker', stall_breaker: 'Stall breaker', max_rounds: 'Max rounds reached', - fatal: 'Repeated failures (retry budget exhausted)', + fatal: 'Repeated failures; paused automatically', midstream: 'Stream interrupted', }, unknownKind: 'Parked', @@ -148,25 +148,17 @@ export default { unlimited: 'Running · round {round} · {tokens} tokens', barHint: 'Round {round}/{max}', }, - // Unattended boundary config card (authorized once at kickoff, effective for the task - // lifetime, dies with the conversation) + // Unattended boundary config card (P1 simplified: single authorization tier, + // only the two budget fields remain user-tunable) taskBoundary: { toggle: 'Unattended', badge: 'Unattended boundary authorized', - riskCeiling: 'Risk ceiling', - riskLow: 'Low', - riskMedium: 'Medium', - dirs: 'Directory allowlist', - dirPlaceholder: 'Enter absolute directory path', - dirAdd: 'Add', - dirEmpty: 'No directories added', forbidDestructive: 'Destructive commands always blocked', budgetToggle: 'Budget (advanced)', budgetRounds: 'Max rounds', - budgetTokens: 'Max tokens', - budgetWallSecs: 'Wall-clock seconds', - budgetHint: 'Leave empty for unlimited', - budgetInvalid: 'Budget values must be positive integers', + budgetWallSecs: 'Max run duration', + budgetUnlimited: 'Unlimited', + budgetHint: 'Check "Unlimited" to lift a limit', confirmSet: 'Authorize once, no interruptions for the whole task', clearBoundary: 'Clear boundary', setOk: 'Unattended boundary authorized', @@ -175,12 +167,10 @@ export default { lockedGenerating: 'Task is running: only clearing (revoke) is allowed, fields are locked', loadFailed: 'Failed to load boundary; the form below is not trustworthy', retryLoad: 'Retry', - dirErrIllegal: 'Path contains illegal characters < > : " | ? *', - dirErrRelative: 'Enter an absolute path (e.g. D:\\projects or /home/user)', clearConfirm: 'Clearing the boundary revokes unattended authorization immediately; tool calls in the running task will fall back to per-action approval. Clear?', - echoRisk: 'risk ceiling {risk}', - echoDirs: '{n} dirs', - echoBudget: 'budget {rounds} rounds / {tokens} tokens / {secs}s', + echoRounds: 'up to {n} rounds', + echoSecs: 'up to {n}s', + echoBudget: 'budget {rounds} / {secs}', echoNoBudget: 'no budget limit', // A12: budget validation field locator; A11: dirty-edit kept notice budgetInvalidField: '{field} must be a positive integer', diff --git a/src/i18n/en/aiChat.ts b/src/i18n/en/aiChat.ts index b167f8f..185f338 100644 --- a/src/i18n/en/aiChat.ts +++ b/src/i18n/en/aiChat.ts @@ -22,7 +22,13 @@ export default { archived: 'Archived', archivedCount: 'Archived ({n})', recoverableCount: 'Recoverable ({n})', - recoverableHint: 'Task parked by restart interruption; click to resume', + recoverableHint: 'Task parked safely; click to resume', + // ── Unattended engine badge (TopBar; shown while on, red badge resets the breaker) ── + unattendedOn: 'Unattended', + unattendedExecuting: 'working', + unattendedTripped: 'Unattended halted', + unattendedBadgeHint: 'Unattended mode is on. Click to open settings', + unattendedResetHint: 'Auto-halted after repeated start failures. Click to reset and resume', recoverableDone: 'Task resumed, running in background', recoverableFail: 'Recover failed: {msg}', emptyConversation: 'No conversations', diff --git a/src/i18n/en/settings.ts b/src/i18n/en/settings.ts index 7941231..e3a3603 100644 --- a/src/i18n/en/settings.ts +++ b/src/i18n/en/settings.ts @@ -272,6 +272,24 @@ export default { // Submit guard: when enabled=true, either of id/secret empty blocks the write feishuValidateIncomplete: 'Fill in App ID and App Secret to enable the Feishu integration', + // ===== Integrations: Unattended mode (engine auto-claims board tasks; TopBar badge jump target) ===== + panelUnattended: 'Unattended Mode', + labelUnattendedEnabled: 'Enable unattended mode', + descUnattendedEnabled: 'Automatically claims open board tasks and keeps working; turning it off stops claiming (running tasks are not interrupted)', + labelUnattendedScope: 'Claim scope', + descUnattendedScope: 'Which tasks are handled automatically while unattended', + unattendedScopeRed: 'High-priority tasks only (recommended)', + unattendedScopeAll: 'All open tasks', + labelUnattendedMonitorChat: 'Notification target', + descUnattendedMonitorChat: 'Mode switches and runtime alerts are pushed to this Feishu group (enable the Feishu integration below first)', + phUnattendedMonitorChat: 'Feishu group chat_id; leave empty to log only', + labelUnattendedPollSecs: 'Check interval (seconds)', + descUnattendedPollSecs: 'How often to look for new board tasks, minimum 30 seconds', + unattendedAdvanced: 'Advanced', + toastUnattendedOn: 'Unattended mode on; board tasks will be claimed automatically', + toastUnattendedOff: 'Unattended mode off', + toastUnattendedFail: 'Failed to switch unattended mode: {msg}', + // ===== Integrations: u-claw (ask_uclaw tool, u-relay commander protocol) ===== panelUclaw: 'u-claw Integration', labelUclawRelayUrl: 'Relay URL', diff --git a/src/i18n/zh-CN/ai.ts b/src/i18n/zh-CN/ai.ts index a6cc162..33ba72b 100644 --- a/src/i18n/zh-CN/ai.ts +++ b/src/i18n/zh-CN/ai.ts @@ -130,7 +130,7 @@ export default { circuit_breaker: '熔断保护', stall_breaker: '停滞保护', max_rounds: '已达轮次上限', - fatal: '连续失败(重入预算耗尽)', + fatal: '连续失败,已自动暂停', midstream: '响应流中断', }, unknownKind: '任务停靠', @@ -144,24 +144,16 @@ export default { unlimited: '进行中 · 第 {round} 轮 · {tokens} tokens', barHint: '第 {round}/{max} 轮', }, - // 无人值守边界配置卡(发起时一次授权,存续期生效,随会话同亡) + // 无人值守边界配置卡(P1 总装简化:授权面收敛单一档,可调项只剩预算两项) taskBoundary: { toggle: '无人值守', badge: '无人值守边界已授权', - riskCeiling: '风险上限', - riskLow: '低', - riskMedium: '中', - dirs: '目录白名单', - dirPlaceholder: '输入目录绝对路径', - dirAdd: '添加', - dirEmpty: '尚未添加目录', forbidDestructive: '破坏性命令恒禁止', budgetToggle: '预算(高级)', budgetRounds: '最大轮次', - budgetTokens: '最大 tokens', - budgetWallSecs: '挂钟秒数', - budgetHint: '留空表示不限', - budgetInvalid: '预算需为正整数', + budgetWallSecs: '最长运行时长', + budgetUnlimited: '不限', + budgetHint: '勾选「不限」即不设该项上限', confirmSet: '一次授权,任务期内不再打扰', clearBoundary: '清除边界', setOk: '已授权无人值守边界', @@ -170,12 +162,10 @@ export default { lockedGenerating: '任务运行中,仅支持清除边界(收权),字段不可修改', loadFailed: '边界读取失败,当前表单不可信', retryLoad: '重试', - dirErrIllegal: '路径含非法字符 < > : " | ? *', - dirErrRelative: '请输入绝对路径(如 D:\\projects 或 /home/user)', clearConfirm: '清除边界将立即收回无人值守授权,运行中的任务后续工具调用回到逐次审批。确定清除?', - echoRisk: '风险上限 {risk}', - echoDirs: '{n} 个目录', - echoBudget: '预算 {rounds} 轮 / {tokens} tokens / {secs}s', + echoRounds: '最多 {n} 轮', + echoSecs: '最长 {n} 秒', + echoBudget: '预算 {rounds} / {secs}', echoNoBudget: '预算不限', // A12: 预算校验错误定位字段;A11: 脏编辑保留提示 budgetInvalidField: '{field} 需为正整数', diff --git a/src/i18n/zh-CN/aiChat.ts b/src/i18n/zh-CN/aiChat.ts index e6ccee6..431a5a9 100644 --- a/src/i18n/zh-CN/aiChat.ts +++ b/src/i18n/zh-CN/aiChat.ts @@ -22,7 +22,13 @@ export default { archived: '已归档', archivedCount: '已归档 ({n})', recoverableCount: '可恢复 ({n})', - recoverableHint: '任务因重启中断停靠,点击恢复继续执行', + recoverableHint: '任务已安全停靠,点击恢复继续执行', + // ── 挂机引擎态徽章(TopBar;开启时显示,熔断红徽点击复位) ── + unattendedOn: '挂机中', + unattendedExecuting: '执行中', + unattendedTripped: '挂机熔断', + unattendedBadgeHint: '挂机模式运行中,点击前往设置', + unattendedResetHint: '连续启动失败已自动熔断,点击复位并重新开启', recoverableDone: '任务已恢复,继续后台执行', recoverableFail: '恢复失败: {msg}', emptyConversation: '暂无对话', diff --git a/src/i18n/zh-CN/settings.ts b/src/i18n/zh-CN/settings.ts index dd545ff..2e25cb4 100644 --- a/src/i18n/zh-CN/settings.ts +++ b/src/i18n/zh-CN/settings.ts @@ -271,6 +271,24 @@ export default { // 提交校验:enabled=true 时 id/secret 任一为空都拦在写库前 feishuValidateIncomplete: '启用飞书集成需先填写 App ID 和 App Secret', + // ===== 集成:挂机模式(引擎自动认领看板任务,TopBar 徽章跳转落点) ===== + panelUnattended: '挂机模式', + labelUnattendedEnabled: '开启挂机模式', + descUnattendedEnabled: '开启后自动认领看板待办任务并持续执行,关闭即全部停止(不影响正在执行的任务)', + labelUnattendedScope: '认领范围', + descUnattendedScope: '挂机时自动处理哪些任务', + unattendedScopeRed: '仅高危任务(推荐)', + unattendedScopeAll: '全部待办', + labelUnattendedMonitorChat: '消息推送目标', + descUnattendedMonitorChat: '挂机开启/关闭确认和运行异常会推送到这个飞书群(需先在下方启用飞书集成)', + phUnattendedMonitorChat: '飞书群 chat_id,留空仅记录日志', + labelUnattendedPollSecs: '轮询周期(秒)', + descUnattendedPollSecs: '每隔多久检查一次看板新任务,最小 30 秒', + unattendedAdvanced: '高级', + toastUnattendedOn: '挂机模式已开启,将自动认领看板任务', + toastUnattendedOff: '挂机模式已关闭', + toastUnattendedFail: '挂机开关操作失败:{msg}', + // ===== 集成:u-claw(ask_uclaw 工具,u-relay commander 协议) ===== panelUclaw: 'u-claw 集成', labelUclawRelayUrl: 'Relay 地址', diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 839c381..bbc5f8b 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -69,9 +69,11 @@ @toast="showToast" /> - @@ -107,6 +109,7 @@ import KnowledgePanel from '@/components/settings/KnowledgePanel.vue' import AllowedDirsPanel from '@/components/settings/AllowedDirsPanel.vue' import FeishuPanel from '@/components/settings/FeishuPanel.vue' import UclawPanel from '@/components/settings/UclawPanel.vue' +import UnattendedPanel from '@/components/settings/UnattendedPanel.vue' // SettingsImportExport 已移除(价值低,占用 UI 空间;跨设备同步建议直接复制 SQLite) import { useAppSettingsStore } from '@/stores/appSettings' import { aiApi } from '@/api'