agentworkshop 0.7.3 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/B9Yie5HN.js +2 -0
- package/.output/public/_nuxt/CN7Zh8GQ.js +1 -0
- package/.output/public/_nuxt/CdSgfcYR.js +1 -0
- package/.output/public/_nuxt/CdfAP_IH2.js +1 -0
- package/.output/public/_nuxt/ChnfyQHz.js +1 -0
- package/.output/public/_nuxt/{BkKYf8xY.js → CyygA0q4.js} +4 -4
- package/.output/public/_nuxt/{Bz40vyZS.js → DFnGFEav.js} +7 -7
- package/.output/public/_nuxt/Xaf_HIR3.js +1 -0
- package/.output/public/_nuxt/ZugRk0CM2.js +4 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/55c10854-ee0e-4be4-924b-a1b0e100fe11.json +1 -0
- package/.output/public/_nuxt/{default.iGJy9dP-.css → default.D17kXlD6.css} +1 -1
- package/.output/public/_nuxt/monitor.CmwpEUCI.css +1 -0
- package/.output/public/_nuxt/yIKY_V1P.js +1 -0
- package/.output/server/chunks/_/nitro.mjs +1131 -920
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs → TownView-Bl8Gkcxi.mjs} +3 -3
- package/.output/server/chunks/build/{TownView-BkHxMiwZ.mjs.map → TownView-Bl8Gkcxi.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs → _id_-CWLFGZjV.mjs} +2 -2
- package/.output/server/chunks/build/{_id_-Bna1_JuB.mjs.map → _id_-CWLFGZjV.mjs.map} +1 -1
- package/.output/server/chunks/build/_id_-GnRJPykX.mjs +1 -1
- package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs → _wsId_-BhQMjyHG.mjs} +3 -4
- package/.output/server/chunks/build/{_wsId_-CzLDGT_V.mjs.map → _wsId_-BhQMjyHG.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-CmeM4u5u.mjs → daq-BOgpn9sl.mjs} +4 -5
- package/.output/server/chunks/build/daq-BOgpn9sl.mjs.map +1 -0
- package/.output/server/chunks/build/dcw-BTM5YU-B.mjs +1 -1
- package/.output/server/chunks/build/{default-Dt6xLdpd.mjs → default-CKJWhkot.mjs} +60 -17
- package/.output/server/chunks/build/{default-Dt6xLdpd.mjs.map → default-CKJWhkot.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-CUQt1z7D.mjs → logs-Dyk0I2I9.mjs} +3 -4
- package/.output/server/chunks/build/logs-Dyk0I2I9.mjs.map +1 -0
- package/.output/server/chunks/build/{monitor-yNoV98rf.mjs → monitor-KW8WLOJd.mjs} +34 -33
- package/.output/server/chunks/build/{monitor-yNoV98rf.mjs.map → monitor-KW8WLOJd.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-BN4vQPb8.mjs → town-_4tEEEVW.mjs} +3 -4
- package/.output/server/chunks/build/town-_4tEEEVW.mjs.map +1 -0
- package/.output/server/chunks/build/useDaqStream-hwkhJHFU.mjs +1 -1
- package/.output/server/chunks/build/useDcwStream-Bdew_yrr.mjs +1 -1
- package/.output/server/chunks/build/{useWorkshopWs-xcdeiSmJ.mjs → useWorkshopWs-B3j5wIAK.mjs} +177 -3
- package/.output/server/chunks/build/useWorkshopWs-B3j5wIAK.mjs.map +1 -0
- package/.output/server/chunks/build/{workshop-CLhwe5eo.mjs → workshop-k0XZTFKg.mjs} +2 -3
- package/.output/server/chunks/build/workshop-k0XZTFKg.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +32 -0
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +56 -0
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +2 -2
- package/.output/server/chunks/virtual/entry.mjs +17 -11
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README.md +435 -434
- package/app/components/AppHeader.vue +138 -0
- package/app/composables/workshop/useWorkshopWs.ts +4 -0
- package/app/pages/monitor.vue +17 -1
- package/app/stores/workshop/hitl.ts +80 -0
- package/bin/aw.mjs +0 -0
- package/cli/commands/tui.mjs +19 -0
- package/config.yml +134 -134
- package/docs/tui.md +43 -0
- package/i18n/locales/en.ts +3 -0
- package/i18n/locales/zh-CN.ts +3 -0
- package/package.json +140 -144
- package/scripts/_spike-pitui.mjs +116 -0
- package/scripts/e2e-hitl-live.mjs +323 -0
- package/scripts/e2e-line-live.mjs +144 -0
- package/scripts/test-hitl-registry.ts +112 -0
- package/scripts/test-tui-commands.mjs +131 -0
- package/scripts/test-tui-reducers.mjs +72 -0
- package/scripts/tsconfig.json +14 -0
- package/scripts/tui-smoke.mjs +91 -0
- package/server/api/workshop/hitl/pending.get.ts +28 -0
- package/server/api/workshop/hitl/respond.post.ts +67 -0
- package/server/api/workshop/ws.ts +30 -1
- package/server/data/daqs.json +39 -17
- package/server/data/dcw-lines.json +14 -0
- package/server/data/dcw-products.json +35 -0
- package/server/data/dcw-recipes.json +140 -0
- package/server/data/dcw-rollback.json +111 -1
- package/server/data/dcw-runs.json +135 -90
- package/server/data/dcw-writes.json +120 -0
- package/server/data/dcws.json +400 -275
- package/server/data/line-runs.json +11 -1
- package/server/plugins/workshop.ts +7 -0
- package/server/services/workshop/agents/harness-terminal.ts +92 -25
- package/server/services/workshop/agents/hitl-registry.ts +181 -0
- package/server/services/workshop/agents/tool-approvals.ts +15 -0
- package/server/services/workshop/scene-events.ts +19 -9
- package/shared/workshop-protocol.ts +37 -0
- package/tui/README.md +65 -0
- package/tui/aw-tui.mjs +356 -0
- package/tui/commands/index.mjs +250 -0
- package/tui/components/chat-log.mjs +42 -0
- package/tui/components/hitl-card.mjs +36 -0
- package/tui/components/monitor-pane.mjs +39 -0
- package/tui/components/root.mjs +53 -0
- package/tui/components/status-bar.mjs +29 -0
- package/tui/lib/api.mjs +82 -0
- package/tui/lib/config.mjs +58 -0
- package/tui/lib/reducers.mjs +218 -0
- package/tui/lib/state.mjs +80 -0
- package/tui/lib/virtual-terminal.mjs +48 -0
- package/tui/lib/ws.mjs +124 -0
- package/tui/theme.mjs +38 -0
- package/.output/public/_nuxt/B7V9IfTL.js +0 -1
- package/.output/public/_nuxt/BE5QyG5J2.js +0 -1
- package/.output/public/_nuxt/BuKI8z-5.js +0 -1
- package/.output/public/_nuxt/D0C5Q0di.js +0 -1
- package/.output/public/_nuxt/DC-QMEk1.js +0 -2
- package/.output/public/_nuxt/DEkQ0qQ02.js +0 -1
- package/.output/public/_nuxt/builds/meta/c6c01cdd-762e-4fb8-86e7-753c5185b856.json +0 -1
- package/.output/public/_nuxt/dt2tKE95.js +0 -4
- package/.output/public/_nuxt/i_MKObHo.js +0 -1
- package/.output/public/_nuxt/monitor.41fCD6TB.css +0 -1
- package/.output/public/_nuxt/tpDMVY5w.js +0 -1
- package/.output/server/chunks/build/connection-Dc0fn6yd.mjs +0 -120
- package/.output/server/chunks/build/connection-Dc0fn6yd.mjs.map +0 -1
- package/.output/server/chunks/build/daq-CmeM4u5u.mjs.map +0 -1
- package/.output/server/chunks/build/logs-CUQt1z7D.mjs.map +0 -1
- package/.output/server/chunks/build/town-BN4vQPb8.mjs.map +0 -1
- package/.output/server/chunks/build/useWorkshopWs-xcdeiSmJ.mjs.map +0 -1
- package/.output/server/chunks/build/workshop-CLhwe5eo.mjs.map +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"productId": "pd-6348a595",
|
|
8
8
|
"productName": "0.8mm 光学膜",
|
|
9
9
|
"startedAt": "2026-09-03T11:18:01.213Z",
|
|
10
|
-
"taggedSamples":
|
|
10
|
+
"taggedSamples": 5708
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"lineId": "ln-9a60a0cd",
|
|
@@ -48,5 +48,15 @@
|
|
|
48
48
|
"productName": "闭环验证产品",
|
|
49
49
|
"startedAt": "2026-09-03T17:12:52.205Z",
|
|
50
50
|
"taggedSamples": 0
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"lineId": "ln-1b318320",
|
|
54
|
+
"runId": "rr-9e70a9dc",
|
|
55
|
+
"recipeId": "rc-ccee1b1b",
|
|
56
|
+
"recipeName": "探针配方",
|
|
57
|
+
"productId": "pd-db432902",
|
|
58
|
+
"productName": "探针产品",
|
|
59
|
+
"startedAt": "2026-09-04T05:08:10.103Z",
|
|
60
|
+
"taggedSamples": 470
|
|
51
61
|
}
|
|
52
62
|
]
|
|
@@ -30,6 +30,7 @@ import { createUserRepo } from '../services/workshop/db/user.repo'
|
|
|
30
30
|
import { createChannelEventRepo } from '../services/workshop/db/channel-event.repo'
|
|
31
31
|
import { createApprovalHistoryRepo, createAlarmEventRepo, createAuditRepo, createApprovalRequestRepo } from '../services/workshop/db/ops.repo'
|
|
32
32
|
import { bindOpsRepos } from '../services/workshop/ops/ops'
|
|
33
|
+
import { configureHitlResolver } from '../services/workshop/agents/hitl-registry'
|
|
33
34
|
import { ensureAllEventRecorders } from '../api/workshop/ws'
|
|
34
35
|
import { createAgentImpl } from '../services/workshop/agents/factory'
|
|
35
36
|
import {
|
|
@@ -123,6 +124,12 @@ export default function workshopPlugin(nitroApp: {
|
|
|
123
124
|
const manager = createAgentChannelManager(deps)
|
|
124
125
|
globalThis.__workshopManager = manager
|
|
125
126
|
|
|
127
|
+
// HITL 全局待办:agentId → channelId/agentName 解析器(避免 registry 反向依赖 manager)
|
|
128
|
+
configureHitlResolver((agentId) => {
|
|
129
|
+
const row = repos.channelAgents.findById(agentId)
|
|
130
|
+
return row ? { channelId: row.channelId, agentName: row.name } : null
|
|
131
|
+
})
|
|
132
|
+
|
|
126
133
|
// 懒加载恢复:仅激活有待办任务的 channel(装配 lead + 调度循环);其余纯持久化
|
|
127
134
|
manager.restore()
|
|
128
135
|
|
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
*
|
|
10
10
|
* HITL 语义:extension_ui_request(select/confirm/input/editor)到达时
|
|
11
11
|
* - 有 WS 订阅者(有人在看终端)→ 等待人类应答,无超时(真 HITL);
|
|
12
|
-
* - 无订阅者 →
|
|
13
|
-
*
|
|
12
|
+
* - 无订阅者 → park 至 hitl-registry 并启动倒计时(security.hitl_timeout_ms,
|
|
13
|
+
* 默认 180s;倒计时仅在零订阅期间进行,任一客户端接入终端即暂停)——
|
|
14
|
+
* 到期自动 cancelled(等价 TUI 按 Esc;agent 的 ask 回合中止),并落一条
|
|
15
|
+
* notice 帧让事后接入的观众可见。TTL 配 0 恢复旧的"无人即秒取消"行为。
|
|
16
|
+
* 待办全局可见性/应答路由由 hitl-registry + /api/workshop/hitl/* 承接。
|
|
14
17
|
*
|
|
15
18
|
* 定位:模块级单例(globalThis 跨 HMR 存活,与 workshop ws hub 同风格);
|
|
16
19
|
* omp-agent 装配时 attach,manager 监控层读取 hasTerminalSession 标记进程行。
|
|
17
20
|
*/
|
|
18
|
-
import { createLogger } from '../logger'
|
|
21
|
+
import { createLogger } from '../logger'
|
|
22
|
+
import { securityHitlTimeoutMs } from '../settings'
|
|
23
|
+
import { getHitlRegistry } from './hitl-registry'
|
|
19
24
|
import type { OmpRpcClient } from './adapters/omp-rpc-client'
|
|
20
25
|
import {
|
|
21
26
|
TERM_FRAME_TEXT_PREVIEW_MAX,
|
|
@@ -43,6 +48,8 @@ interface TerminalSession {
|
|
|
43
48
|
alive: boolean
|
|
44
49
|
exitCode: number | null
|
|
45
50
|
pendingHitl: TerminalHitlDialog | null
|
|
51
|
+
/** 零订阅 park 倒计时(有人接入即清除;到期自动取消待答对话框) */
|
|
52
|
+
parkTimer: NodeJS.Timeout | null
|
|
46
53
|
unsubRaw: (() => void) | null
|
|
47
54
|
/** 微批缓冲 + 定时器(50ms 合并 delta 帧,防 WS 洪泛) */
|
|
48
55
|
batch: TermFrame[]
|
|
@@ -234,29 +241,83 @@ function hitlViewOf(frame: Record<string, unknown> | null): TerminalHitlDialog |
|
|
|
234
241
|
}
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
/** HITL 对话框到达:登记 pending
|
|
244
|
+
/** HITL 对话框到达:登记 pending + hitl-registry;零订阅时进入 park 倒计时 */
|
|
238
245
|
function handleUiRequest(session: TerminalSession, frame: Record<string, unknown>): void {
|
|
239
246
|
const method = frame.method as string
|
|
240
247
|
if (method === 'cancel') {
|
|
241
|
-
// omp 主动撤销对话框(如回合中止):清 pending
|
|
248
|
+
// omp 主动撤销对话框(如回合中止):清 pending + 落定全局待办
|
|
242
249
|
const targetId = String(frame.targetId ?? '')
|
|
243
|
-
if (session.pendingHitl && session.pendingHitl.id === targetId)
|
|
250
|
+
if (session.pendingHitl && session.pendingHitl.id === targetId) {
|
|
251
|
+
session.pendingHitl = null
|
|
252
|
+
clearParkTimer(session)
|
|
253
|
+
getHitlRegistry().resolve('omp-dialog', targetId, 'cancelled')
|
|
254
|
+
}
|
|
244
255
|
return
|
|
245
256
|
}
|
|
246
257
|
const view = hitlViewOf(frame)
|
|
247
258
|
if (!view) return
|
|
248
259
|
session.pendingHitl = view
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
260
|
+
getHitlRegistry().register({
|
|
261
|
+
kind: 'omp-dialog',
|
|
262
|
+
id: view.id,
|
|
263
|
+
agentId: session.meta.agentId,
|
|
264
|
+
agentName: session.meta.name,
|
|
265
|
+
channelId: session.meta.channelId,
|
|
266
|
+
pid: session.meta.pid,
|
|
267
|
+
method: view.method,
|
|
268
|
+
title: view.title,
|
|
269
|
+
options: view.options,
|
|
270
|
+
message: view.message,
|
|
271
|
+
})
|
|
272
|
+
if (session.listeners.size === 0) armParkTimer(session)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ===== park 倒计时(无人观看时挂起等待;有人接入暂停) =====
|
|
276
|
+
|
|
277
|
+
/** 零订阅 park 窗口(security.hitl_timeout_ms;0 = 恢复旧的"无人即秒取消") */
|
|
278
|
+
const HITL_PARK_MS = (): number => securityHitlTimeoutMs()
|
|
279
|
+
|
|
280
|
+
function clearParkTimer(session: TerminalSession): void {
|
|
281
|
+
if (session.parkTimer) {
|
|
282
|
+
clearTimeout(session.parkTimer)
|
|
283
|
+
session.parkTimer = null
|
|
257
284
|
}
|
|
258
285
|
}
|
|
259
286
|
|
|
287
|
+
/** 进入 park(仅零订阅且有待答对话框时生效;TTL<=0 立即取消 = 旧行为) */
|
|
288
|
+
function armParkTimer(session: TerminalSession): void {
|
|
289
|
+
clearParkTimer(session)
|
|
290
|
+
if (!session.pendingHitl || session.listeners.size > 0) return
|
|
291
|
+
const ttl = HITL_PARK_MS()
|
|
292
|
+
if (ttl <= 0) {
|
|
293
|
+
expirePendingHitl(session, true)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
getHitlRegistry().setParkDeadline('omp-dialog', session.pendingHitl.id, new Date(Date.now() + ttl).toISOString())
|
|
297
|
+
session.parkTimer = setTimeout(() => {
|
|
298
|
+
session.parkTimer = null
|
|
299
|
+
if (session.pendingHitl && session.listeners.size === 0) expirePendingHitl(session)
|
|
300
|
+
}, ttl)
|
|
301
|
+
session.parkTimer.unref?.()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** park 到期(或 TTL=0 立即):超时收敛 —— 先落定 expired,再走 cancelled 应答写 omp stdin */
|
|
305
|
+
function expirePendingHitl(session: TerminalSession, immediate = false): void {
|
|
306
|
+
const hitl = session.pendingHitl
|
|
307
|
+
if (!hitl) return
|
|
308
|
+
session.pendingHitl = null
|
|
309
|
+
clearParkTimer(session)
|
|
310
|
+
getHitlRegistry().resolve('omp-dialog', hitl.id, 'expired')
|
|
311
|
+
respondUi(session, { id: hitl.id, cancelled: true })
|
|
312
|
+
pushFrame(session, {
|
|
313
|
+
type: '__terminal_notice',
|
|
314
|
+
level: 'warning',
|
|
315
|
+
message: immediate
|
|
316
|
+
? `HITL 对话框(${hitl.method} "${hitl.title.slice(0, 40)}")无人接入,已自动取消`
|
|
317
|
+
: `HITL 对话框(${hitl.method} "${hitl.title.slice(0, 40)}")park 超时无人处理,已自动取消`,
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
260
321
|
// ===== 对外 API =====
|
|
261
322
|
|
|
262
323
|
/**
|
|
@@ -287,6 +348,7 @@ export function attachTerminalTap(
|
|
|
287
348
|
alive: true,
|
|
288
349
|
exitCode: null,
|
|
289
350
|
pendingHitl: null,
|
|
351
|
+
parkTimer: null,
|
|
290
352
|
unsubRaw: null,
|
|
291
353
|
batch: [],
|
|
292
354
|
batchTimer: null,
|
|
@@ -337,7 +399,11 @@ export function markTerminalSessionExit(pid: number, exitCode: number | null): v
|
|
|
337
399
|
detachTerminalTap(pid)
|
|
338
400
|
session.alive = false
|
|
339
401
|
session.exitCode = exitCode
|
|
340
|
-
session.pendingHitl
|
|
402
|
+
if (session.pendingHitl) {
|
|
403
|
+
getHitlRegistry().resolve('omp-dialog', session.pendingHitl.id, 'cancelled')
|
|
404
|
+
session.pendingHitl = null
|
|
405
|
+
}
|
|
406
|
+
clearParkTimer(session)
|
|
341
407
|
session.running = false
|
|
342
408
|
session.streaming = false
|
|
343
409
|
flushNow(session)
|
|
@@ -442,19 +508,17 @@ export function subscribeTerminal(
|
|
|
442
508
|
): (() => void) | null {
|
|
443
509
|
const session = sessions.get(pid)
|
|
444
510
|
if (!session) return null
|
|
511
|
+
const wasEmpty = session.listeners.size === 0
|
|
445
512
|
session.listeners.add(listener)
|
|
513
|
+
if (wasEmpty && session.pendingHitl) {
|
|
514
|
+
// 有人观看:暂停 park 倒计时(真 HITL 等待无上限;观看者离开后重新计时)
|
|
515
|
+
clearParkTimer(session)
|
|
516
|
+
getHitlRegistry().setParkDeadline('omp-dialog', session.pendingHitl.id, null)
|
|
517
|
+
}
|
|
446
518
|
return () => {
|
|
447
519
|
session.listeners.delete(listener)
|
|
448
|
-
//
|
|
449
|
-
if (session.listeners.size === 0 && session.pendingHitl)
|
|
450
|
-
const hitl = session.pendingHitl
|
|
451
|
-
respondUi(session, { id: hitl.id, cancelled: true })
|
|
452
|
-
pushFrame(session, {
|
|
453
|
-
type: '__terminal_notice',
|
|
454
|
-
level: 'warning',
|
|
455
|
-
message: `观看者已全部离开,HITL 对话框(${hitl.method} "${hitl.title.slice(0, 40)}")自动取消`,
|
|
456
|
-
})
|
|
457
|
-
}
|
|
520
|
+
// 订阅者归零时若还有待应答对话框:park 倒计时(超时自动取消;不再是秒取消)
|
|
521
|
+
if (session.listeners.size === 0 && session.pendingHitl) armParkTimer(session)
|
|
458
522
|
}
|
|
459
523
|
}
|
|
460
524
|
|
|
@@ -526,6 +590,7 @@ function respondUi(
|
|
|
526
590
|
response: { id: string, value?: string, confirmed?: boolean, cancelled?: boolean },
|
|
527
591
|
): void {
|
|
528
592
|
if (session.pendingHitl && session.pendingHitl.id === response.id) session.pendingHitl = null
|
|
593
|
+
clearParkTimer(session)
|
|
529
594
|
const frame: Record<string, unknown> = { type: 'extension_ui_response', id: response.id }
|
|
530
595
|
if (response.cancelled) {
|
|
531
596
|
frame.cancelled = true
|
|
@@ -542,4 +607,6 @@ function respondUi(
|
|
|
542
607
|
catch (err) {
|
|
543
608
|
log.error(`[harness-terminal] extension_ui_response 写入失败(pid=${session.meta.pid}):`, err)
|
|
544
609
|
}
|
|
610
|
+
// 全局待办落定(expired 收敛路径已先行 resolve,此处幂等 no-op)
|
|
611
|
+
getHitlRegistry().resolve('omp-dialog', response.id, response.cancelled ? 'cancelled' : 'answered')
|
|
545
612
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HitlRegistry —— 全局"待人工处理"登记处(进程内,globalThis 单例)。
|
|
3
|
+
*
|
|
4
|
+
* 把两条互相独立的 HITL 链路统一成一个可发现的视图:
|
|
5
|
+
* - omp-dialog omp `rpc-ui` 的 extension_ui_request 对话框(harness-terminal 登记;
|
|
6
|
+
* park/暂停计时语义由 terminal 订阅者状态驱动,本模块只存状态与广播)
|
|
7
|
+
* - dcw-approval 手动确认模式的工具执行审批(tool-approvals 登记/裁决)
|
|
8
|
+
*
|
|
9
|
+
* 职责:登记(hitl.request)/ 落定(hitl.resolved)事件经 per-channel 订阅扇出,
|
|
10
|
+
* 由 workshop ws hub 转接进 AEP 频道流(seq/落库);统一 REST 快照与应答路由
|
|
11
|
+
* (/api/workshop/hitl/pending、/respond)也读本模块。裁决留痕仍在各链路
|
|
12
|
+
* (approval_history / audit),这里不做持久化 —— omp 对话框本就随子进程消亡。
|
|
13
|
+
*
|
|
14
|
+
* 定位:模块级单例(globalThis 跨 HMR 存活,与 tool-approvals 同风格);
|
|
15
|
+
* 不 import runtime/manager(避免循环初始化),channelId 由插件注入 resolver 解析。
|
|
16
|
+
*/
|
|
17
|
+
import type { AepHitlItem, AepHitlResolved } from '../../../../shared/workshop-protocol'
|
|
18
|
+
|
|
19
|
+
export type HitlKind = AepHitlItem['kind']
|
|
20
|
+
export type HitlOutcome = AepHitlResolved['outcome']
|
|
21
|
+
|
|
22
|
+
/** hitl 帧事件(ws hub 按 channelId 转接进频道流) */
|
|
23
|
+
export interface HitlEvent {
|
|
24
|
+
type: 'hitl.request' | 'hitl.resolved'
|
|
25
|
+
payload: AepHitlItem | AepHitlResolved
|
|
26
|
+
agentId?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type HitlListener = (e: HitlEvent) => void
|
|
30
|
+
|
|
31
|
+
/** 登记入参(channelId/agentName 可由 resolver 补全;缺 channelId 的条目仅入全局快照) */
|
|
32
|
+
export interface HitlRegisterInput {
|
|
33
|
+
kind: HitlKind
|
|
34
|
+
id: string
|
|
35
|
+
agentId: string
|
|
36
|
+
agentName?: string
|
|
37
|
+
channelId?: string
|
|
38
|
+
pid?: number
|
|
39
|
+
method?: AepHitlItem['method']
|
|
40
|
+
title: string
|
|
41
|
+
detail?: string
|
|
42
|
+
options?: string[]
|
|
43
|
+
message?: string
|
|
44
|
+
createdAt?: string
|
|
45
|
+
expiresAt?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class HitlRegistryService {
|
|
49
|
+
/** key = `${kind}:${id}`;value 为 AepHitlItem(parkDeadline 单独存,避免污染协议对象) */
|
|
50
|
+
private items = new Map<string, AepHitlItem>()
|
|
51
|
+
private parkDeadline = new Map<string, string | null>()
|
|
52
|
+
private listeners = new Map<string, Set<HitlListener>>()
|
|
53
|
+
/** agentId → { channelId, agentName }(插件装配 repos 后注入;避免反向依赖 manager) */
|
|
54
|
+
private resolver: ((agentId: string) => { channelId: string, agentName: string } | null) | null = null
|
|
55
|
+
|
|
56
|
+
configureResolver(fn: (agentId: string) => { channelId: string, agentName: string } | null): void {
|
|
57
|
+
this.resolver = fn
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private key(kind: HitlKind, id: string): string {
|
|
61
|
+
return `${kind}:${id}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 登记(幂等:同 kind+id 重复登记只更新 parkDeadline,不重发事件) */
|
|
65
|
+
register(input: HitlRegisterInput): void {
|
|
66
|
+
const key = this.key(input.kind, input.id)
|
|
67
|
+
const existing = this.items.get(key)
|
|
68
|
+
if (existing) return
|
|
69
|
+
let channelId = input.channelId ?? ''
|
|
70
|
+
let agentName = input.agentName ?? ''
|
|
71
|
+
if ((!channelId || !agentName) && this.resolver) {
|
|
72
|
+
try {
|
|
73
|
+
const resolved = this.resolver(input.agentId)
|
|
74
|
+
channelId = channelId || resolved?.channelId || ''
|
|
75
|
+
agentName = agentName || resolved?.agentName || ''
|
|
76
|
+
}
|
|
77
|
+
catch { /* resolver 异常不阻断登记 */ }
|
|
78
|
+
}
|
|
79
|
+
const item: AepHitlItem = {
|
|
80
|
+
kind: input.kind,
|
|
81
|
+
id: input.id,
|
|
82
|
+
channelId,
|
|
83
|
+
agentId: input.agentId,
|
|
84
|
+
agentName: agentName || input.agentId.slice(0, 8),
|
|
85
|
+
pid: input.pid,
|
|
86
|
+
method: input.method,
|
|
87
|
+
title: input.title,
|
|
88
|
+
detail: input.detail,
|
|
89
|
+
options: input.options,
|
|
90
|
+
message: input.message,
|
|
91
|
+
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
92
|
+
expiresAt: input.expiresAt ?? null,
|
|
93
|
+
}
|
|
94
|
+
this.items.set(key, item)
|
|
95
|
+
this.parkDeadline.set(key, item.expiresAt ?? null)
|
|
96
|
+
this.emit({ type: 'hitl.request', payload: item, agentId: item.agentId })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 落定(幂等:未登记的 key 返回 false —— 上层据此实现 409 ALREADY_RESOLVED)。
|
|
101
|
+
* outcome:answered(人类已答复/批准/拒绝)/ cancelled(人工放弃或 omp 撤销)/ expired(超时)。
|
|
102
|
+
*/
|
|
103
|
+
resolve(kind: HitlKind, id: string, outcome: HitlOutcome, by?: string): boolean {
|
|
104
|
+
const key = this.key(kind, id)
|
|
105
|
+
const item = this.items.get(key)
|
|
106
|
+
if (!item) return false
|
|
107
|
+
this.items.delete(key)
|
|
108
|
+
this.parkDeadline.delete(key)
|
|
109
|
+
const payload: AepHitlResolved = {
|
|
110
|
+
kind,
|
|
111
|
+
id,
|
|
112
|
+
channelId: item.channelId,
|
|
113
|
+
agentId: item.agentId,
|
|
114
|
+
outcome,
|
|
115
|
+
by,
|
|
116
|
+
}
|
|
117
|
+
this.emit({ type: 'hitl.resolved', payload, agentId: item.agentId })
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 查询单条(应答路由用:取 pid / channelId / method) */
|
|
122
|
+
find(kind: HitlKind, id: string): AepHitlItem | null {
|
|
123
|
+
return this.items.get(this.key(kind, id)) ?? null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** park 截止更新(harness-terminal 订阅者增减时静默修正;null = 计时暂停) */
|
|
127
|
+
setParkDeadline(kind: HitlKind, id: string, expiresAt: string | null): void {
|
|
128
|
+
const key = this.key(kind, id)
|
|
129
|
+
if (!this.items.has(key)) return
|
|
130
|
+
this.parkDeadline.set(key, expiresAt)
|
|
131
|
+
const item = this.items.get(key)!
|
|
132
|
+
item.expiresAt = expiresAt
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 全局(或指定 channel)待处理快照 */
|
|
136
|
+
snapshot(channelId?: string): AepHitlItem[] {
|
|
137
|
+
return [...this.items.values()].filter(i => !channelId || i.channelId === channelId)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** per-channel 事件订阅(ws hub ensureStream 时接线;返回退订函数) */
|
|
141
|
+
subscribe(channelId: string, fn: HitlListener): () => void {
|
|
142
|
+
let set = this.listeners.get(channelId)
|
|
143
|
+
if (!set) {
|
|
144
|
+
set = new Set()
|
|
145
|
+
this.listeners.set(channelId, set)
|
|
146
|
+
}
|
|
147
|
+
set.add(fn)
|
|
148
|
+
return () => {
|
|
149
|
+
set.delete(fn)
|
|
150
|
+
if (set.size === 0) this.listeners.delete(channelId)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private emit(e: HitlEvent): void {
|
|
155
|
+
const channelId = (e.payload as AepHitlItem).channelId
|
|
156
|
+
const set = channelId ? this.listeners.get(channelId) : undefined
|
|
157
|
+
if (!set) return
|
|
158
|
+
for (const fn of set) {
|
|
159
|
+
try {
|
|
160
|
+
fn(e)
|
|
161
|
+
}
|
|
162
|
+
catch { /* 单个订阅者异常不影响扇出 */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const g = globalThis as typeof globalThis & { __hitlRegistry?: HitlRegistryService }
|
|
168
|
+
|
|
169
|
+
export function getHitlRegistry(): HitlRegistryService {
|
|
170
|
+
g.__hitlRegistry ??= new HitlRegistryService()
|
|
171
|
+
return g.__hitlRegistry
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 插件接线用别名(语义直白) */
|
|
175
|
+
export const configureHitlResolver = (fn: (agentId: string) => { channelId: string, agentName: string } | null): void =>
|
|
176
|
+
getHitlRegistry().configureResolver(fn)
|
|
177
|
+
|
|
178
|
+
/** per-channel 事件订阅(ws hub 建流时挂接;hitl.request/hitl.resolved 扇出) */
|
|
179
|
+
export function subscribeHitlEvents(channelId: string, fn: HitlListener): () => void {
|
|
180
|
+
return getHitlRegistry().subscribe(channelId, fn)
|
|
181
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { randomUUID } from 'node:crypto'
|
|
10
10
|
import { getOps } from '../ops/ops'
|
|
11
11
|
import { securityHitlTimeoutMs } from '../settings'
|
|
12
|
+
import { getHitlRegistry } from './hitl-registry'
|
|
12
13
|
|
|
13
14
|
export interface ToolApproval {
|
|
14
15
|
id: string
|
|
@@ -57,6 +58,17 @@ class ToolApprovalService {
|
|
|
57
58
|
decidedBy: '',
|
|
58
59
|
decidedName: '',
|
|
59
60
|
}
|
|
61
|
+
// 全局 HITL 待办登记(channelId/agentName 由插件注入的 resolver 补全;
|
|
62
|
+
// expiresAt 与审批超时窗同源,前端可显示倒计时)
|
|
63
|
+
getHitlRegistry().register({
|
|
64
|
+
kind: 'dcw-approval',
|
|
65
|
+
id: approval.id,
|
|
66
|
+
agentId: approval.agentId,
|
|
67
|
+
title: `${approval.kind.toUpperCase()} 下发审批`,
|
|
68
|
+
detail: approval.detail,
|
|
69
|
+
createdAt: approval.createdAt,
|
|
70
|
+
expiresAt: approval.expiresAt,
|
|
71
|
+
})
|
|
60
72
|
return new Promise((resolve) => {
|
|
61
73
|
const timer = setTimeout(() => {
|
|
62
74
|
if (!this.pending.has(approval.id)) return
|
|
@@ -65,6 +77,7 @@ class ToolApprovalService {
|
|
|
65
77
|
approval.decidedAt = new Date().toISOString()
|
|
66
78
|
approval.comment = '审批超时未处理,默认拒绝,指令未执行'
|
|
67
79
|
this.remember(approval)
|
|
80
|
+
getHitlRegistry().resolve('dcw-approval', approval.id, 'expired')
|
|
68
81
|
resolve({ approved: false, comment: approval.comment, id: approval.id })
|
|
69
82
|
}, TIMEOUT_MS())
|
|
70
83
|
timer.unref?.()
|
|
@@ -83,6 +96,7 @@ class ToolApprovalService {
|
|
|
83
96
|
entry.approval.decidedBy = decidedBy
|
|
84
97
|
entry.approval.decidedName = decidedName
|
|
85
98
|
this.remember(entry.approval)
|
|
99
|
+
getHitlRegistry().resolve('dcw-approval', id, 'answered', decidedBy || undefined)
|
|
86
100
|
entry.resolve({ approved, comment: entry.approval.comment, id })
|
|
87
101
|
return entry.approval
|
|
88
102
|
}
|
|
@@ -105,6 +119,7 @@ class ToolApprovalService {
|
|
|
105
119
|
entry.approval.comment = '绑定已解除,审批失效'
|
|
106
120
|
entry.approval.decidedAt = new Date().toISOString()
|
|
107
121
|
this.remember(entry.approval)
|
|
122
|
+
getHitlRegistry().resolve('dcw-approval', id, 'cancelled')
|
|
108
123
|
entry.resolve({ approved: false, comment: entry.approval.comment, id })
|
|
109
124
|
n++
|
|
110
125
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* 路由改为从本模块导入广播函数 —— 避免"路由文件被当库静态导入"引发
|
|
8
8
|
* nitro 打包的循环初始化(TDZ)问题。
|
|
9
9
|
*/
|
|
10
|
-
import type { AepEnvelope } from '../../../shared/workshop-protocol'
|
|
11
10
|
import { emitPluginEvent } from './plugins/host.mjs'
|
|
12
11
|
|
|
13
12
|
const AEP_VERSION = 1
|
|
@@ -46,15 +45,26 @@ export function broadcastSceneEvent(type: string, payload: unknown): void {
|
|
|
46
45
|
// 插件宿主事件桥(event:<type> 钩子;宿主未装载时 no-op)——先于 peers 短路,插件不依赖在线页面
|
|
47
46
|
emitPluginEvent(type, payload)
|
|
48
47
|
if (peers().size === 0) return
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const frame = JSON.stringify({ v: AEP_VERSION, type, seq: 0, at: new Date().toISOString(), channelId: '', payload })
|
|
49
|
+
for (const peer of peers()) {
|
|
50
|
+
try {
|
|
51
|
+
peer.send(frame)
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
peers().delete(peer)
|
|
55
|
+
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 全员直推(不触发插件宿主事件桥):给已有频道归属、但需要"全局主动提醒"
|
|
61
|
+
* 语义的低频事件用(hitl.request/hitl.resolved —— WebUI 徽标/TUI 状态条不依赖
|
|
62
|
+
* channel 订阅即达)。与 broadcastSceneEvent 的差别仅在不二次 emitPluginEvent
|
|
63
|
+
* (频道流 publish 路径已各发一次,避免插件钩子重复触发)。
|
|
64
|
+
*/
|
|
65
|
+
export function broadcastPeerEvent(type: string, payload: unknown): void {
|
|
66
|
+
if (peers().size === 0) return
|
|
67
|
+
const frame = JSON.stringify({ v: AEP_VERSION, type, seq: 0, at: new Date().toISOString(), channelId: '', payload })
|
|
58
68
|
for (const peer of peers()) {
|
|
59
69
|
try {
|
|
60
70
|
peer.send(frame)
|
|
@@ -118,6 +118,40 @@ export interface AepSceneLayout {
|
|
|
118
118
|
rotationY: number
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** hitl.request payload:待人工处理条目(omp ask 对话框与 dcw 工具审批的统一视图) */
|
|
122
|
+
export interface AepHitlItem {
|
|
123
|
+
kind: 'omp-dialog' | 'dcw-approval'
|
|
124
|
+
/** omp:对话框 id;dcw:审批 id(ap-*) */
|
|
125
|
+
id: string
|
|
126
|
+
channelId: string
|
|
127
|
+
agentId: string
|
|
128
|
+
agentName: string
|
|
129
|
+
/** omp 子进程 pid(应答路由 respondTerminalUi 用) */
|
|
130
|
+
pid?: number
|
|
131
|
+
/** omp-dialog 专属:对话框形态 */
|
|
132
|
+
method?: 'select' | 'confirm' | 'input' | 'editor'
|
|
133
|
+
title: string
|
|
134
|
+
/** dcw-approval 专属:人读摘要(节点/物理量/目标值) */
|
|
135
|
+
detail?: string
|
|
136
|
+
/** select 的选项列表 */
|
|
137
|
+
options?: string[]
|
|
138
|
+
message?: string
|
|
139
|
+
createdAt: string
|
|
140
|
+
/** park 截止时刻(omp 对话框零订阅倒计时;null = 有订阅者,计时暂停) */
|
|
141
|
+
expiresAt?: string | null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** hitl.resolved payload:待办落定(answered=已答复/cancelled=放弃或撤销/expired=超时) */
|
|
145
|
+
export interface AepHitlResolved {
|
|
146
|
+
kind: AepHitlItem['kind']
|
|
147
|
+
id: string
|
|
148
|
+
channelId: string
|
|
149
|
+
agentId: string
|
|
150
|
+
outcome: 'answered' | 'cancelled' | 'expired'
|
|
151
|
+
/** 落定方(user id / system) */
|
|
152
|
+
by?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
121
155
|
export type AepEvent
|
|
122
156
|
= | { type: 'channel.snapshot', payload: AepSnapshot }
|
|
123
157
|
| { type: 'agent.status', payload: { agentId: string, state: 'idle' | 'busy' | 'stopped', currentTaskId?: string | null, queued?: number, completed?: number } }
|
|
@@ -135,6 +169,8 @@ export type AepEvent
|
|
|
135
169
|
| { type: 'device.deleted', payload: { id: string, name: string } }
|
|
136
170
|
| { type: 'scene.layout.saved', payload: AepSceneLayout }
|
|
137
171
|
| { type: 'scene.layout.removed', payload: { channelId: string } }
|
|
172
|
+
| { type: 'hitl.request', payload: AepHitlItem }
|
|
173
|
+
| { type: 'hitl.resolved', payload: AepHitlResolved }
|
|
138
174
|
| { type: 'daq.reading', payload: AepDaqReading }
|
|
139
175
|
| { type: 'daq.node.changed', payload: AepDaqNodeChange }
|
|
140
176
|
| { type: 'daq.controller', payload: AepDaqControllerState }
|
|
@@ -192,6 +228,7 @@ export const AEP_GROUPS: Record<string, string[]> = {
|
|
|
192
228
|
team: ['agent.member'],
|
|
193
229
|
devices: ['device.created', 'device.updated', 'device.deleted'],
|
|
194
230
|
scene: ['scene.layout.saved', 'scene.layout.removed'],
|
|
231
|
+
hitl: ['hitl.request', 'hitl.resolved'],
|
|
195
232
|
daq: ['daq.reading', 'daq.node.changed', 'daq.controller', 'daq.template.changed', 'daq.alarm', 'daq.alarm.changed'],
|
|
196
233
|
dcw: ['dcw.node.changed', 'dcw.written', 'dcw.controller', 'dcw.optimization.changed'],
|
|
197
234
|
ops: ['ops.log'],
|
package/tui/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# AgentWorkShop TUI —— 终端工作台
|
|
2
|
+
|
|
3
|
+
与 AgentTeam 交互作业的终端客户端(pi-tui 组件化渲染,与 WebUI 平级的瘦客户端):
|
|
4
|
+
创建/切换 Channel、放置 Agent、发送任务、实时查看执行过程,以及 **HITL 待办
|
|
5
|
+
(omp ask 对话框 / dcw 下发审批)的统一提醒与作答**。
|
|
6
|
+
|
|
7
|
+
## 启动
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
aw tui # 或:pnpm tui / node tui/aw-tui.mjs
|
|
11
|
+
aw tui --url http://127.0.0.1:3000 --token ut-xxxx --channel <名>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- 首次使用交互式登录(邮箱+密码);凭据落 `<配置根>/tui-auth.json`(0600,
|
|
15
|
+
已 gitignore)。也可用环境变量 `AW_TUI_EMAIL` / `AW_TUI_PASSWORD` 免交互。
|
|
16
|
+
- 需要服务端已运行(`pnpm dev` 或 `aw start`)。
|
|
17
|
+
|
|
18
|
+
## 界面
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌ 会话时间线(左) ┌ 监控面板(右,/monitor 开启)
|
|
22
|
+
│ ◆ 你 消息 · ◆ Agent 回复 · 任务状态行 │ ┌─ 监控 <agent> · 流式中
|
|
23
|
+
├ HITL 作答卡(/hitl <n> 进入作答时出现)
|
|
24
|
+
├ 状态条:频道 · 成员(忙 n) · HITL 待处理 n · 连接态
|
|
25
|
+
└ 输入框(/ 命令补全 · ↑↓ 历史)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 命令
|
|
29
|
+
|
|
30
|
+
| 命令 | 说明 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| `/help` | 命令列表 |
|
|
33
|
+
| `/channels` | 列出我的频道 |
|
|
34
|
+
| `/channel new <名> [--desc …] [--lead <名>]` | 创建频道(可选内联建 lead) |
|
|
35
|
+
| `/channel use <名\|序号>` | 切换频道(WS 断点续传) |
|
|
36
|
+
| `/channel add <模板\|名> [--role lead\|worker] [--harness omp] [--config JSON]` | 放置 Agent 实例 |
|
|
37
|
+
| `/agents` | 当前频道成员与实时状态 |
|
|
38
|
+
| `/send <agent\|序号> <文本…>` | 向指定 Agent 直发(忙碌时 steer 注入) |
|
|
39
|
+
| `/task <标题…> [--mode goal\|loop\|pipeline] [--assignee <agent>]` | 提交正式任务 |
|
|
40
|
+
| `/tasks` | 任务列表与七态/进度 |
|
|
41
|
+
| `/monitor <agent\|序号\|off>` | 开/关右侧终端镜像(实时执行过程) |
|
|
42
|
+
| `/hitl [序号\|off]` | 待人工处理列表/进入作答/放弃作答 |
|
|
43
|
+
| `/quit` | 退出 |
|
|
44
|
+
|
|
45
|
+
普通文本 = 向当前频道 lead 发送任务(`priority=task`)。
|
|
46
|
+
|
|
47
|
+
## HITL 流程(与 WebUI 同一套服务端)
|
|
48
|
+
|
|
49
|
+
1. Agent 需要人类确认/ask 时,服务端 hitl-registry 登记待办,所有已连接
|
|
50
|
+
客户端(WebUI 徽标 / TUI 状态条)收到 `hitl.request` 帧 → 提醒亮起。
|
|
51
|
+
2. TUI:`/hitl` 看列表 → `/hitl <序号>` 进入作答卡(自动可配合 `/monitor`
|
|
52
|
+
观察现场);confirm 输 `y/n`,select 输序号,input/editor 直接输入。
|
|
53
|
+
3. 回车提交 → `POST /api/workshop/hitl/respond` → extension_ui_response
|
|
54
|
+
直写 omp stdin(或 dcw 审批落定)→ `hitl.resolved` 帧 → 双端提醒消隐。
|
|
55
|
+
4. 无人观看时对话框 park `security.hitl_timeout_ms`(默认 180s,仅零订阅
|
|
56
|
+
期间倒计时,有人接入终端即暂停;配 0 恢复旧的立即取消行为)。
|
|
57
|
+
|
|
58
|
+
## 测试
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node scripts/test-hitl-registry.ts # 服务端 registry + park 语义(需 tsx)
|
|
62
|
+
node scripts/test-tui-reducers.mjs # AEP 帧归约
|
|
63
|
+
node scripts/test-tui-commands.mjs # 命令解析/分发
|
|
64
|
+
node scripts/tui-smoke.mjs # 无头 e2e(需 dev server 运行)
|
|
65
|
+
```
|