新增: 挂机模式 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:
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<!-- ═══ 挂机模式(引擎自动认领看板任务持续执行;配置走 Settings KV 热读,改后即时生效) ═══ -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('settings.panelUnattended') }}</h2>
|
||||
</div>
|
||||
<div class="unattended-settings">
|
||||
<SettingRow :label="$t('settings.labelUnattendedEnabled')" :desc="$t('settings.descUnattendedEnabled')">
|
||||
<label class="toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="enabled"
|
||||
:disabled="toggling"
|
||||
@change="onEnabledChange(($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow :label="$t('settings.labelUnattendedScope')" :desc="$t('settings.descUnattendedScope')">
|
||||
<div class="unattended-radios">
|
||||
<label class="unattended-radio" :class="{ 'is-checked': scope === 'red_only' }">
|
||||
<input v-model="scope" type="radio" value="red_only" />
|
||||
<span>{{ $t('settings.unattendedScopeRed') }}</span>
|
||||
</label>
|
||||
<label class="unattended-radio" :class="{ 'is-checked': scope === 'all_open' }">
|
||||
<input v-model="scope" type="radio" value="all_open" />
|
||||
<span>{{ $t('settings.unattendedScopeAll') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow :label="$t('settings.labelUnattendedMonitorChat')" :desc="$t('settings.descUnattendedMonitorChat')">
|
||||
<input
|
||||
v-model="monitorChat"
|
||||
type="text"
|
||||
class="setting-input unattended-input"
|
||||
:placeholder="$t('settings.phUnattendedMonitorChat')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<button type="button" class="unattended-advanced-toggle" @click="advancedOpen = !advancedOpen">
|
||||
<span class="unattended-chevron" :class="{ 'is-open': advancedOpen }">▶</span>
|
||||
{{ $t('settings.unattendedAdvanced') }}
|
||||
</button>
|
||||
<div v-if="advancedOpen" class="unattended-advanced">
|
||||
<SettingRow :label="$t('settings.labelUnattendedPollSecs')" :desc="$t('settings.descUnattendedPollSecs')">
|
||||
<input
|
||||
:value="pollSecs"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="setting-input unattended-input unattended-input--num"
|
||||
@change="onPollSecsChange(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import SettingRow from './SettingRow.vue'
|
||||
|
||||
// ============================================================
|
||||
// 挂机模式面板 — 引擎配置读写全走 Settings KV(后端每轮热读,改后即时生效):
|
||||
// - 模式开关走专用 IPC unattended_set_enabled(写 KV + 立即唤醒轮询器 + 飞书确认卡),
|
||||
// 其余三项(范围/推送目标/轮询周期)走 appSettings store(settings_get/settings_set,自带防抖)
|
||||
// - 开关关闭语义 = 引擎空转(下一轮只读配置即停),不杀在途任务
|
||||
// ============================================================
|
||||
|
||||
/** 后端引擎状态(unattended_status 纯读,作开关回显的权威真值) */
|
||||
interface UnattendedStatus {
|
||||
enabled: boolean
|
||||
claim_scope: string
|
||||
in_flight_conv: string | null
|
||||
breaker_tripped: boolean
|
||||
poll_secs: number
|
||||
}
|
||||
|
||||
/** 轮询周期下限(与后端 MIN_POLL_SECS 同源钳制,防误配热循环) */
|
||||
const MIN_POLL_SECS = 30
|
||||
const DEFAULT_POLL_SECS = 300
|
||||
|
||||
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
const enabled = ref(false)
|
||||
const toggling = ref(false)
|
||||
const advancedOpen = ref(false)
|
||||
|
||||
// 领单范围/推送目标:store 防抖落库(v-model 即存);范围历史脏值归一到 red_only
|
||||
const scope = appSettings.useSetting<string>('df-unattended-claim-scope', 'red_only')
|
||||
const monitorChat = appSettings.useSetting<string>('df-monitor-chat-id', '')
|
||||
const pollSecs = appSettings.useSetting<number>('df-unattended-poll-secs', DEFAULT_POLL_SECS)
|
||||
|
||||
onMounted(async () => {
|
||||
enabled.value = appSettings.get<boolean>('df-unattended-enabled', false)
|
||||
if (scope.value !== 'all_open') scope.value = 'red_only'
|
||||
try {
|
||||
const s = await invoke<UnattendedStatus>('unattended_status')
|
||||
enabled.value = s.enabled
|
||||
// 展示值对齐后端生效值(后端有 30s 下限钳制,KV 手改成更小值时这里自愈回写)
|
||||
if (s.poll_secs !== pollSecs.value) pollSecs.value = s.poll_secs
|
||||
} catch (e) {
|
||||
console.error('[UNATTENDED] 状态读取失败,回退 KV 缓存值:', e)
|
||||
}
|
||||
})
|
||||
|
||||
/** 模式开关:专用 IPC(确认卡在后端推);失败回拨开关并 toast */
|
||||
async function onEnabledChange(next: boolean) {
|
||||
if (toggling.value || next === enabled.value) return
|
||||
toggling.value = true
|
||||
try {
|
||||
await invoke('unattended_set_enabled', { enabled: next })
|
||||
enabled.value = next
|
||||
// 同步 store 缓存(后端直写 KV 不经前端 store,不回写则下次进面板读到旧值)
|
||||
void appSettings.set('df-unattended-enabled', next)
|
||||
emit('toast', t(next ? 'settings.toastUnattendedOn' : 'settings.toastUnattendedOff'), 'success')
|
||||
} catch (e) {
|
||||
console.error('[UNATTENDED] 开关失败:', e)
|
||||
emit('toast', t('settings.toastUnattendedFail', { msg: String(e) }), 'error')
|
||||
} finally {
|
||||
toggling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询周期:非法输入回退当前值;合法值钳下限后落库(store 防抖) */
|
||||
function onPollSecsChange(raw: string) {
|
||||
const n = Number.parseInt(raw.trim(), 10)
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
pollSecs.value = appSettings.get<number>('df-unattended-poll-secs', DEFAULT_POLL_SECS)
|
||||
return
|
||||
}
|
||||
pollSecs.value = Math.max(n, MIN_POLL_SECS)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* panel/panel-header/toggle/setting-input 均在 settings.css 全局,此处仅本面板特有样式 */
|
||||
.unattended-settings { display: flex; flex-direction: column; gap: 4px; }
|
||||
.unattended-input { width: 280px; }
|
||||
.unattended-input--num { width: 110px; }
|
||||
.unattended-radios { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.unattended-radio {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
.unattended-radio.is-checked { color: var(--df-accent); }
|
||||
.unattended-radio input { accent-color: var(--df-accent); margin: 0; }
|
||||
.unattended-advanced-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
align-self: flex-start;
|
||||
margin-top: 6px;
|
||||
padding: 2px 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.unattended-advanced-toggle:hover { color: var(--df-text); }
|
||||
.unattended-chevron {
|
||||
display: inline-block;
|
||||
font-size: 9px;
|
||||
transition: transform 0.15s var(--df-ease);
|
||||
}
|
||||
.unattended-chevron.is-open { transform: rotate(90deg); }
|
||||
.unattended-advanced { display: flex; flex-direction: column; gap: 4px; }
|
||||
</style>
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user