新增: 挂机模式 P1 总装: 设置面板+顶栏徽章+边界卡精简+监控告警推送

- 新增 UnattendedPanel.vue 挂机配置面板(引擎自动认领看板任务,Settings KV 热读即改即生效)
- Settings.vue 接入挂机面板;searchIndex.ts 补挂机搜索词条
- TopBar.vue 新增挂机进行中徽章(脉冲动效+透明度过渡)
- TaskBoundaryCard.vue 精简边界状态判定(wallState 合并比对,删冗余 watch)
- i18n zh-CN/en: settings/ai/aiChat 六文件补挂机词条双语
- unattended.rs 新增 push_monitor_alert 监控告警推送(异常快照+告警头直发 monitor_chat)
- feishu_notify.rs push/push_card 出口重构收敛
- lib.rs 注册 unattended_status/unattended_set_enabled 两 IPC
- aiShared.ts/useAiStreamingEvents.ts 挂机状态流接入
This commit is contained in:
lxy
2026-10-07 22:03:41 +08:00
parent e60bd38444
commit 2441101978
16 changed files with 525 additions and 264 deletions
+95
View File
@@ -20,6 +20,25 @@
>
⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }}
</button>
<!-- 挂机引擎态徽章:开启=「挂机中」(在途附「执行中」),熔断=红徽点击复位;关闭态不显示 -->
<button
v-if="unattendedStatus?.breaker_tripped"
type="button"
class="ai-unattended-badge ai-unattended-badge--tripped"
:title="$t('aiChat.unattendedResetHint')"
@click="resetUnattendedBreaker"
>
{{ $t('aiChat.unattendedTripped') }}
</button>
<button
v-else-if="unattendedStatus?.enabled"
type="button"
class="ai-unattended-badge"
:title="$t('aiChat.unattendedBadgeHint')"
@click="jumpToUnattendedSettings"
>
🛌 {{ $t('aiChat.unattendedOn') }}<template v-if="unattendedStatus.in_flight_conv"> · {{ $t('aiChat.unattendedExecuting') }}</template>
</button>
</div>
<!-- 提供商状态 + 模型选择器 -->
<div class="ai-header-center">
@@ -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<UnattendedEngineStatus | null>(null)
let unattendedTimer: ReturnType<typeof setTimeout> | null = null
// 卸载守卫:onBeforeUnmount 清定时器后,在途 invoke 的续延还会回来排下一轮,须拦住防卸载后空转
let unattendedDisposed = false
async function refreshUnattended() {
if (unattendedDisposed) return
try {
unattendedStatus.value = await invoke<UnattendedEngineStatus>('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 {