新增: 无人值守安全系列(H1-H3) + P5 审计日志与部署运维 + #14 异步委托

- H3 外部干预感知: run_agentic_loop 增 h3_watch_dir 参数, loop 启动取 git HEAD 基线, 每 5 轮探测, HEAD 被外部改写(切分支/reset/他话提交)即 emit_task_parked(stop_kind=external_change) + teardown 停靠; 4 调用方(发送/编辑/强制发送/审批续跑)从 allowed_dirs.persistent 排序取首目录, 修复初版 current_dir 恒空缺陷
- P5 审计日志: df-im-gateway audit.rs append-only SQLite(tunnel_audit 表, payload 4KB 截断, recent/by_request 查询), main.rs 8 打点点位(inbound/command/outbound), 写失败不阻断主链路, 17 tests passed
- P5 部署运维: publish-im-gateway.ps1(musl 交叉编译→scp .new→systemd 原子切换→9191 验证→自动回滚) + systemd unit + nginx im.1216.top 反代 + 运维手册
- #14 异步委托: 受理/完成两段式 + 桌面任务卡(后台进行中徽章/轮次/切换) + 飞书双端同构, 关联缺陷 #8-#12 速清

验收: cargo check workspace EXIT 0, cargo test workspace 全绿(711+560+33+12...), vue-tsc EXIT 0
This commit is contained in:
lxy
2026-10-06 15:59:10 +08:00
parent 7c252cff96
commit 438d47bc9f
17 changed files with 987 additions and 12 deletions
+62 -6
View File
@@ -166,11 +166,13 @@
</button>
<template v-if="recoverableTasks.length">
<div class="ai-running-tasks-sep"></div>
<button v-for="task in recoverableTasks" :key="task.id" type="button" class="ai-running-task-item" @click="switchToRunning(task.id)">
<span class="ai-running-task-dot ai-running-task-dot--parked"></span>
<span class="ai-running-task-title">{{ task.title }}</span>
<span class="ai-running-task-round ai-running-task-round--parked">{{ t('aiChat.recoverableTask') }}</span>
</button>
<div v-for="task in recoverableTasks" :key="task.id" class="ai-running-task-item ai-running-task-item--parked">
<button type="button" class="ai-running-task-main" @click="switchToRunning(task.id)">
<span class="ai-running-task-dot ai-running-task-dot--parked"></span>
<span class="ai-running-task-title">{{ task.title }}</span>
</button>
<button type="button" class="ai-running-task-recover" @click="recoverParked(task.id)" :title="t('aiChat.recoverNow')">{{ t('aiChat.recoverNow') }}</button>
</div>
</template>
</div>
</Transition>
@@ -487,6 +489,21 @@ async function switchToRunning(id: string) {
await switchConversation(id)
}
}
/** B4:恢复 parked 会话(注入恢复引导消息续跑),成功后切回该会话 */
const recoveringIds = ref(new Set<string>())
async function recoverParked(id: string) {
if (recoveringIds.value.has(id)) return
recoveringIds.value.add(id)
try {
await aiApi.recoverParked(id)
recoveringIds.value.delete(id)
await switchToRunning(id)
} catch (e) {
recoveringIds.value.delete(id)
console.warn('[topbar] 恢复失败:', e)
}
}
const taskProgressText = computed(() => {
const p = taskProgress.value
if (!p) return ''
@@ -1112,9 +1129,48 @@ onBeforeUnmount(() => {
.ai-running-task-dot--parked {
background: var(--df-warning, #e6a23c);
}
.ai-running-task-round--parked {
/* B4 parked 行改双钮布局(切回 | 恢复),恢复钮琥珀描边醒目 */
.ai-running-task-item--parked {
display: flex;
align-items: center;
gap: 6px;
padding: 0;
}
.ai-running-task-item--parked:hover {
background: none;
}
.ai-running-task-main {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
padding: 5px 0 5px 10px;
border: none;
background: none;
cursor: pointer;
text-align: left;
font-size: 12px;
color: var(--df-text);
}
.ai-running-task-main:hover {
background: var(--df-hover, rgba(128,128,128,0.08));
}
.ai-running-task-recover {
flex-shrink: 0;
margin-right: 10px;
padding: 2px 8px;
border: 1px solid var(--df-warning, #e6a23c);
border-radius: 4px;
background: none;
cursor: pointer;
font-size: 11px;
color: var(--df-warning, #e6a23c);
}
.ai-running-task-recover:hover {
background: var(--df-warning, #e6a23c);
color: #fff;
}
.ai-history-inline-list {
position: absolute;
left: auto; right: 0;