agentworkshop 0.7.3 → 0.7.5
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/stop.mjs +108 -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-aw-tui-global.mjs +277 -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 +148 -0
- package/scripts/test-tui-reducers.mjs +72 -0
- package/scripts/tsconfig.json +14 -0
- package/scripts/tui-smoke.mjs +100 -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 +74 -0
- package/tui/aw-tui.mjs +406 -0
- package/tui/commands/index.mjs +287 -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 +33 -0
- package/tui/lib/api.mjs +82 -0
- package/tui/lib/config.mjs +58 -0
- package/tui/lib/pickers.mjs +102 -0
- package/tui/lib/reducers.mjs +218 -0
- package/tui/lib/state.mjs +86 -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
package/tui/aw-tui.mjs
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AgentWorkShop TUI 入口 —— 终端工作台(与 AgentTeam 交互作业)。
|
|
3
|
+
//
|
|
4
|
+
// node tui/aw-tui.mjs [--url http://127.0.0.1:3000] [--token ut-*]
|
|
5
|
+
// [--channel <名>] [--headless]
|
|
6
|
+
// (或 `aw tui …` / `pnpm tui`)
|
|
7
|
+
//
|
|
8
|
+
// 装配:认证(配置根 tui-auth.json / 交互登录)→ AEP 事件流(频道 sub)+
|
|
9
|
+
// 命令系统(/help)+ 监控面板(终端 WS)+ HITL 统一作答(/hitl)。
|
|
10
|
+
// --headless:VirtualTerminal 驱动(无 TTY;scripts/tui-smoke.mjs 用)。
|
|
11
|
+
// ============================================================
|
|
12
|
+
import { TuiMainScreen, Editor, CombinedAutocompleteProvider, ProcessTerminal } from '@earendil-works/pi-tui'
|
|
13
|
+
import { theme } from './theme.mjs'
|
|
14
|
+
import { loadAuth, saveAuth, configRoot } from './lib/config.mjs'
|
|
15
|
+
import { createApi } from './lib/api.mjs'
|
|
16
|
+
import { connectAep, connectTerm } from './lib/ws.mjs'
|
|
17
|
+
import { createState, pushLog, withLog } from './lib/state.mjs'
|
|
18
|
+
import { reduceEnvelope, reduceTermFrame } from './lib/reducers.mjs'
|
|
19
|
+
import { openChannelPicker, openTargetPicker } from './lib/pickers.mjs'
|
|
20
|
+
import { dispatchCommand, slashCommandCompletions } from './commands/index.mjs'
|
|
21
|
+
import { ChatLog } from './components/chat-log.mjs'
|
|
22
|
+
import { StatusBar } from './components/status-bar.mjs'
|
|
23
|
+
import { MonitorPane } from './components/monitor-pane.mjs'
|
|
24
|
+
import { HitlCard } from './components/hitl-card.mjs'
|
|
25
|
+
import { buildTree } from './components/root.mjs'
|
|
26
|
+
import { VirtualTerminal } from './lib/virtual-terminal.mjs'
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const opts = { headless: false }
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i]
|
|
32
|
+
if (a === '--headless') opts.headless = true
|
|
33
|
+
else if (a === '--url') opts.url = argv[++i]
|
|
34
|
+
else if (a === '--token') opts.token = argv[++i]
|
|
35
|
+
else if (a === '--channel') opts.channel = argv[++i]
|
|
36
|
+
}
|
|
37
|
+
return opts
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 认证:已存凭据 → --token → 环境变量 → 交互登录 */
|
|
41
|
+
async function ensureAuth(opts) {
|
|
42
|
+
const saved = loadAuth()
|
|
43
|
+
const baseUrl = opts.url ?? process.env.AW_TUI_URL ?? saved?.baseUrl ?? 'http://127.0.0.1:3000'
|
|
44
|
+
if (opts.token) return { baseUrl, token: opts.token }
|
|
45
|
+
if (saved?.token && saved.baseUrl === baseUrl) return { baseUrl, token: saved.token }
|
|
46
|
+
|
|
47
|
+
const email = process.env.AW_TUI_EMAIL
|
|
48
|
+
const password = process.env.AW_TUI_PASSWORD
|
|
49
|
+
if (email && password) {
|
|
50
|
+
const api = createApi({ baseUrl })
|
|
51
|
+
const res = await api.login(email, password)
|
|
52
|
+
const auth = { baseUrl, token: res.token, email }
|
|
53
|
+
saveAuth(auth)
|
|
54
|
+
return auth
|
|
55
|
+
}
|
|
56
|
+
if (opts.headless) throw new Error('无凭据:headless 模式请传 --token 或设 AW_TUI_EMAIL/AW_TUI_PASSWORD')
|
|
57
|
+
|
|
58
|
+
const rl = (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
|
|
59
|
+
try {
|
|
60
|
+
console.log(`AgentWorkShop TUI 首次使用(凭据落 ${configRoot()}/tui-auth.json)`)
|
|
61
|
+
const mail = await rl.question('邮箱: ')
|
|
62
|
+
const pass = await rl.question('密码: ')
|
|
63
|
+
const api = createApi({ baseUrl })
|
|
64
|
+
const res = await api.login(mail.trim(), pass)
|
|
65
|
+
const auth = { baseUrl, token: res.token, email: mail.trim() }
|
|
66
|
+
saveAuth(auth)
|
|
67
|
+
console.log('✔ 登录成功,凭据已保存')
|
|
68
|
+
return auth
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rl.close()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
76
|
+
const opts = parseArgs(argv)
|
|
77
|
+
const auth = await ensureAuth(opts)
|
|
78
|
+
|
|
79
|
+
const store = createState()
|
|
80
|
+
const state = withLog(store.state)
|
|
81
|
+
state.baseUrl = auth.baseUrl
|
|
82
|
+
state.token = auth.token
|
|
83
|
+
const api = createApi(auth)
|
|
84
|
+
|
|
85
|
+
// ── 终端 + 组件树 ──
|
|
86
|
+
const terminal = opts.headless ? new VirtualTerminal() : new ProcessTerminal()
|
|
87
|
+
if (opts.headless) (globalThis).__tuiSmokeTerminal = terminal // 无头 e2e 注入句柄
|
|
88
|
+
const tui = new TuiMainScreen(terminal)
|
|
89
|
+
const chatLog = new ChatLog(state)
|
|
90
|
+
const monitorPane = new MonitorPane(state)
|
|
91
|
+
const hitlCard = new HitlCard(state)
|
|
92
|
+
const statusBar = new StatusBar(state)
|
|
93
|
+
const editor = new Editor(tui, theme.editor)
|
|
94
|
+
const { stack } = buildTree({ state, chatLog, monitorPane, hitlCard, statusBar, editor })
|
|
95
|
+
|
|
96
|
+
const push = (kind, text) => {
|
|
97
|
+
pushLog(state, kind, text)
|
|
98
|
+
store.notify()
|
|
99
|
+
}
|
|
100
|
+
const echo = (kind, text) => push(kind === 'warn' ? 'hitl' : kind, text)
|
|
101
|
+
|
|
102
|
+
// ── 装配动作(命令系统/提交路由的副作用入口) ──
|
|
103
|
+
let termConn = null
|
|
104
|
+
const actions = {
|
|
105
|
+
async switchChannel(channelId) {
|
|
106
|
+
if (state.activeChannelId && state.activeChannelId !== channelId) aep.unsubscribe(state.activeChannelId)
|
|
107
|
+
state.activeChannelId = channelId
|
|
108
|
+
aep.subscribe(channelId, seqCursor.get(channelId))
|
|
109
|
+
await actions.refreshAgents()
|
|
110
|
+
// 时间线回填:最近 20 条(人类/Agent 消息)
|
|
111
|
+
try {
|
|
112
|
+
const msgs = await api.listMessages(channelId, 20)
|
|
113
|
+
for (const m of msgs) {
|
|
114
|
+
const text = (m.parts ?? []).map(p => p?.text ?? '').join('').trim()
|
|
115
|
+
if (!text) continue
|
|
116
|
+
if (m.role === 'USER' || m.fromAgentId == null) push('user', text)
|
|
117
|
+
else push('agent', text)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch { /* 回填失败不阻塞 */ }
|
|
121
|
+
const ch = state.channels.find(c => c.id === channelId)
|
|
122
|
+
push('system', `已切换到频道「${ch?.name ?? channelId.slice(0, 8)}」—— 普通文本发送任务,/help 查看命令。`)
|
|
123
|
+
store.notify()
|
|
124
|
+
},
|
|
125
|
+
async refreshAgents() {
|
|
126
|
+
if (!state.activeChannelId) return
|
|
127
|
+
const list = await api.listAgents(state.activeChannelId)
|
|
128
|
+
state.agents = list.map(a => ({ id: a.id, name: a.name, role: a.role, harness: a.harness, enabled: a.enabled }))
|
|
129
|
+
store.notify()
|
|
130
|
+
},
|
|
131
|
+
openMonitor(agent) {
|
|
132
|
+
termConn?.close()
|
|
133
|
+
termConn = null
|
|
134
|
+
state.monitor = { agentId: agent.id, name: agent.name, connected: false, waiting: false, streaming: false, lines: [], streamText: '' }
|
|
135
|
+
const mon = state.monitor
|
|
136
|
+
const addLine = (text, tone = 'dim') => {
|
|
137
|
+
mon.lines.push({ text, tone })
|
|
138
|
+
if (mon.lines.length > 120) mon.lines.splice(0, mon.lines.length - 120)
|
|
139
|
+
}
|
|
140
|
+
const applyFrame = (frame) => {
|
|
141
|
+
if (frame.type === 'message_update') {
|
|
142
|
+
const ev = frame.assistantMessageEvent
|
|
143
|
+
const delta = typeof ev?.delta === 'string' ? ev.delta : typeof frame.text === 'string' ? frame.text : ''
|
|
144
|
+
if (delta) mon.streamText = (mon.streamText + delta).slice(-2000)
|
|
145
|
+
mon.streaming = true
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (frame.type === 'message_end' || frame.type === 'turn_end') mon.streaming = false
|
|
149
|
+
const { lines } = reduceTermFrameSafe(frame)
|
|
150
|
+
for (const l of lines) addLine(l.text, l.tone)
|
|
151
|
+
}
|
|
152
|
+
termConn = connectTerm({
|
|
153
|
+
baseUrl: state.baseUrl,
|
|
154
|
+
token: state.token,
|
|
155
|
+
agentId: agent.id,
|
|
156
|
+
channelId: state.activeChannelId,
|
|
157
|
+
onMessage(msg) {
|
|
158
|
+
if (msg.type === 'term.init') {
|
|
159
|
+
mon.connected = true
|
|
160
|
+
mon.waiting = false
|
|
161
|
+
addLine(`── 已接入 ${msg.meta?.name ?? agent.name} pid ${msg.meta?.pid ?? '?'}(重放 ${msg.lastSeq ?? 0} 帧)──`, 'accent')
|
|
162
|
+
if (msg.hitl) {
|
|
163
|
+
upsertHitlLocal(state, { kind: 'omp-dialog', id: msg.hitl.id, channelId: state.activeChannelId, agentId: agent.id, agentName: agent.name, method: msg.hitl.method, title: msg.hitl.title, options: msg.hitl.options, message: msg.hitl.message })
|
|
164
|
+
addLine(`⏸ 待答对话框:${msg.hitl.title}(/hitl 作答)`, 'warn')
|
|
165
|
+
}
|
|
166
|
+
// 重放尾部 40 帧给上下文
|
|
167
|
+
for (const f of (msg.replay ?? []).slice(-40)) applyFrame(f.frame)
|
|
168
|
+
}
|
|
169
|
+
else if (msg.type === 'term.frames') {
|
|
170
|
+
for (const f of msg.frames ?? []) applyFrame(f.frame)
|
|
171
|
+
}
|
|
172
|
+
else if (msg.type === 'term.state') {
|
|
173
|
+
mon.streaming = msg.streaming
|
|
174
|
+
}
|
|
175
|
+
else if (msg.type === 'term.notice') {
|
|
176
|
+
addLine(msg.message, msg.level === 'error' ? 'error' : 'dim')
|
|
177
|
+
}
|
|
178
|
+
store.notify()
|
|
179
|
+
},
|
|
180
|
+
onClose(code) {
|
|
181
|
+
if (mon.agentId !== agent.id) return
|
|
182
|
+
mon.connected = false
|
|
183
|
+
if (code === 4404) {
|
|
184
|
+
mon.waiting = true
|
|
185
|
+
addLine('omp 进程未启动(随首个任务 spawn),等待接入…', 'warn')
|
|
186
|
+
}
|
|
187
|
+
store.notify()
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
push('system', `监控已开启:${agent.name}(右侧面板;/monitor off 关闭)`)
|
|
191
|
+
},
|
|
192
|
+
closeMonitor() {
|
|
193
|
+
termConn?.close()
|
|
194
|
+
termConn = null
|
|
195
|
+
state.monitor = { agentId: null, name: null, connected: false, waiting: false, streaming: false, lines: [], streamText: '' }
|
|
196
|
+
store.notify()
|
|
197
|
+
},
|
|
198
|
+
async submitHitlAnswer(text) {
|
|
199
|
+
const item = state.hitlAnswering
|
|
200
|
+
if (!item) return false
|
|
201
|
+
const body = { kind: item.kind, id: item.id }
|
|
202
|
+
if (item.kind === 'omp-dialog') {
|
|
203
|
+
if (item.method === 'confirm') {
|
|
204
|
+
const norm = text.trim().toLowerCase()
|
|
205
|
+
if (!['y', 'n', 'yes', 'no'].includes(norm)) {
|
|
206
|
+
push('error', 'confirm 对话框请输入 y(批准)或 n(拒绝)')
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
body.confirmed = norm.startsWith('y')
|
|
210
|
+
}
|
|
211
|
+
else if (item.method === 'select') {
|
|
212
|
+
const idx = Number.parseInt(text.trim(), 10)
|
|
213
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > (item.options?.length ?? 0)) {
|
|
214
|
+
push('error', `select 请输入 1-${item.options?.length ?? 0} 的序号`)
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
body.value = item.options[idx - 1]
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
body.value = text
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// dcw-approval:confirm 语义(y=批准/n=拒绝)+ 备注
|
|
225
|
+
const norm = text.trim().toLowerCase()
|
|
226
|
+
body.confirmed = norm.startsWith('y')
|
|
227
|
+
if (norm.length > 1) body.comment = text
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
await api.hitlRespond(body)
|
|
231
|
+
push('system', `✔ 应答已提交(${item.title})`)
|
|
232
|
+
state.hitl = state.hitl.filter(i => !(i.kind === item.kind && i.id === item.id))
|
|
233
|
+
state.hitlAnswering = null
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
push('error', `应答失败:${err.message}`)
|
|
237
|
+
if (String(err.code) === 'ALREADY_RESOLVED') state.hitlAnswering = null
|
|
238
|
+
}
|
|
239
|
+
store.notify()
|
|
240
|
+
return true
|
|
241
|
+
},
|
|
242
|
+
quit() {
|
|
243
|
+
aep.close()
|
|
244
|
+
termConn?.close()
|
|
245
|
+
tui.stop()
|
|
246
|
+
process.exit(0)
|
|
247
|
+
},
|
|
248
|
+
/** 交互式频道选择(启动/`/channel use` 无参) */
|
|
249
|
+
pickChannel() {
|
|
250
|
+
openChannelPicker(tui, state, {
|
|
251
|
+
onSelect: async (id) => {
|
|
252
|
+
tui.setFocus(editor)
|
|
253
|
+
await actions.switchChannel(id)
|
|
254
|
+
},
|
|
255
|
+
onCancel: async () => {
|
|
256
|
+
tui.setFocus(editor)
|
|
257
|
+
// Esc 兜底:未进入任何频道时自动进第一个
|
|
258
|
+
if (!state.activeChannelId && state.channels[0]) await actions.switchChannel(state.channels[0].id)
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
const ctx = { state, store, api, echo, push, actions }
|
|
264
|
+
|
|
265
|
+
// ── 编辑器提交路由:HITL 作答 → 命令 → 普通文本按目标分流 ──
|
|
266
|
+
editor.onSubmit = async (text) => {
|
|
267
|
+
const trimmed = text.trim()
|
|
268
|
+
editor.setText('')
|
|
269
|
+
editor.addToHistory?.(text)
|
|
270
|
+
if (!trimmed) return
|
|
271
|
+
if (state.hitlAnswering) {
|
|
272
|
+
await actions.submitHitlAnswer(trimmed)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (trimmed.startsWith('/')) {
|
|
276
|
+
await dispatchCommand(ctx, trimmed)
|
|
277
|
+
store.notify()
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
if (!state.activeChannelId) {
|
|
281
|
+
push('error', '未选择频道:Tab 或 /channel use 打开选择器')
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
// 普通文本按目标分流:选定了成员 = 通信消息(immediate);频道 = 发布正式任务
|
|
285
|
+
if (state.target) {
|
|
286
|
+
const to = state.agents.find(a => a.id === state.target.agentId)
|
|
287
|
+
if (!to) {
|
|
288
|
+
state.target = null
|
|
289
|
+
push('error', '对话目标已失效(成员被移除),已重置为频道模式')
|
|
290
|
+
store.notify()
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await api.sendMessage(state.activeChannelId, { toAgentId: to.id, text: trimmed, priority: 'immediate', requireReply: true, fromLabel: state.userName || '用户' })
|
|
295
|
+
push('system', `✔ 通信消息已发 → @${to.name}`)
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
push('error', `发送失败:${err.message}`)
|
|
299
|
+
}
|
|
300
|
+
store.notify()
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
// 频道模式 = 发布正式任务(状态经 /tasks 跟踪)
|
|
304
|
+
try {
|
|
305
|
+
const task = await api.submitTask(state.activeChannelId, { title: trimmed, mode: 'goal' })
|
|
306
|
+
push('system', `✔ 任务已发布:${task?.id?.slice(0, 8) ?? ''}「${trimmed}」`)
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
push('error', `任务发布失败:${err.message}`)
|
|
310
|
+
}
|
|
311
|
+
store.notify()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Tab:呼出对话目标选择器(成员 + 频道模式;选定成员自动开独立监控) ──
|
|
315
|
+
tui.addInputListener((data) => {
|
|
316
|
+
if (data !== '\t' || state.targetPickerOpen) return undefined
|
|
317
|
+
openTargetPicker(tui, state, {
|
|
318
|
+
onPick: (agent) => {
|
|
319
|
+
tui.setFocus(editor)
|
|
320
|
+
if (agent) actions.openMonitor(agent)
|
|
321
|
+
store.notify()
|
|
322
|
+
},
|
|
323
|
+
onCancel: () => tui.setFocus(editor),
|
|
324
|
+
})
|
|
325
|
+
return { consume: true }
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// ── AEP 事件流 ──
|
|
329
|
+
const seqCursor = new Map()
|
|
330
|
+
const aep = connectAep({
|
|
331
|
+
baseUrl: state.baseUrl,
|
|
332
|
+
token: state.token,
|
|
333
|
+
onEvent(e) {
|
|
334
|
+
if (e.channelId && typeof e.seq === 'number') {
|
|
335
|
+
aep.cursor(e.channelId, e.seq)
|
|
336
|
+
seqCursor.set(e.channelId, e.seq)
|
|
337
|
+
}
|
|
338
|
+
reduceEnvelope(state, e)
|
|
339
|
+
store.notify()
|
|
340
|
+
},
|
|
341
|
+
onState(s) {
|
|
342
|
+
state.connState = s
|
|
343
|
+
store.notify()
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
store.onChange(() => tui.requestRender())
|
|
348
|
+
|
|
349
|
+
// ── 组装视图 ──
|
|
350
|
+
tui.addChild({ render: width => [...stack.render(width), '', ...editor.render(width)] })
|
|
351
|
+
editor.setAutocompleteProvider?.(new CombinedAutocompleteProvider(slashCommandCompletions(), process.cwd()))
|
|
352
|
+
tui.setFocus(editor)
|
|
353
|
+
|
|
354
|
+
push('system', `AgentWorkShop TUI 已就绪 ${state.baseUrl}`)
|
|
355
|
+
push('system', '普通文本 = 发任务到当前频道;/help 命令列表;/hitl 处理待办。')
|
|
356
|
+
|
|
357
|
+
// ── 启动:显式 --channel 直接进入;否则 TUI 就绪后弹出频道选择器 ──
|
|
358
|
+
let needPick = false
|
|
359
|
+
try {
|
|
360
|
+
const channels = await api.listChannels()
|
|
361
|
+
state.channels = channels
|
|
362
|
+
if (opts.channel) {
|
|
363
|
+
const target = channels.find(c => c.name === opts.channel || c.id === opts.channel)
|
|
364
|
+
if (target) await actions.switchChannel(target.id)
|
|
365
|
+
else push('warn', `频道不存在:${opts.channel}(/channels 查看)`)
|
|
366
|
+
}
|
|
367
|
+
else if (channels.length > 0) needPick = true
|
|
368
|
+
else push('warn', '尚无频道:/channel new <名称> 创建一个。')
|
|
369
|
+
const me = await api.get('/api/workshop/users/me').catch(() => null)
|
|
370
|
+
state.userName = me?.user?.name ?? me?.name ?? '用户'
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
push('error', `初始化失败:${err.message}`)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
tui.start()
|
|
377
|
+
if (needPick) {
|
|
378
|
+
push('system', '↑↓ 选择要进入的频道,Enter 确认(Esc 自动进入第一个)。')
|
|
379
|
+
actions.pickChannel()
|
|
380
|
+
}
|
|
381
|
+
return tui
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function upsertHitlLocal(state, item) {
|
|
385
|
+
const i = state.hitl.findIndex(x => x.kind === item.kind && x.id === item.id)
|
|
386
|
+
if (i >= 0) state.hitl[i] = item
|
|
387
|
+
else state.hitl.push(item)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** reduceTermFrame 的防御包装(未知帧形状不炸渲染) */
|
|
391
|
+
function reduceTermFrameSafe(frame) {
|
|
392
|
+
try {
|
|
393
|
+
return reduceTermFrame(frame)
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return { lines: [] }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 直接运行入口(aw tui / pnpm tui 均落到这里)
|
|
401
|
+
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll('\\', '/').split('/').pop())) {
|
|
402
|
+
main().catch((err) => {
|
|
403
|
+
console.error('[tui] 启动失败:', err.message)
|
|
404
|
+
process.exit(1)
|
|
405
|
+
})
|
|
406
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// TUI 命令系统 —— SlashCommand 集中注册 + 解析(仿 openclaw tui/commands.ts)。
|
|
3
|
+
// handler(ctx, args):ctx = { state, notify, api, ws, actions, echo }。
|
|
4
|
+
// actions 由 aw-tui 注入(switchChannel/openMonitor/submitHitl 等带副作用的装配)。
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
/** 解析 '/cmd arg1 arg2 --flag val --bool' → { name, args, flags } */
|
|
8
|
+
export function parseCommand(input) {
|
|
9
|
+
const text = String(input ?? '').trim()
|
|
10
|
+
if (!text.startsWith('/')) return null
|
|
11
|
+
const tokens = text.slice(1).split(/\s+/).filter(Boolean)
|
|
12
|
+
const name = tokens.shift() ?? ''
|
|
13
|
+
const args = []
|
|
14
|
+
const flags = {}
|
|
15
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
16
|
+
const tok = tokens[i]
|
|
17
|
+
if (tok.startsWith('--')) {
|
|
18
|
+
const key = tok.slice(2)
|
|
19
|
+
const next = tokens[i + 1]
|
|
20
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
21
|
+
flags[key] = next
|
|
22
|
+
i++
|
|
23
|
+
}
|
|
24
|
+
else flags[key] = true
|
|
25
|
+
}
|
|
26
|
+
else args.push(tok)
|
|
27
|
+
}
|
|
28
|
+
return { name, args, flags, rest: args.join(' ') }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const commands = [
|
|
32
|
+
{
|
|
33
|
+
name: 'help',
|
|
34
|
+
description: '命令列表',
|
|
35
|
+
handler(ctx) {
|
|
36
|
+
const rows = commands.map(c => ` /${c.name}${c.usage ? ` ${c.usage}` : ''} — ${c.description}`)
|
|
37
|
+
ctx.echo('system', `可用命令:\n${rows.join('\n')}\n普通文本按当前目标发送:频道 → 发布正式任务;@成员(Tab 选择) → 通信消息。/msg /task 可显式指定。`)
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'channels',
|
|
42
|
+
description: '列出我的频道',
|
|
43
|
+
async handler(ctx) {
|
|
44
|
+
const list = await ctx.api.listChannels()
|
|
45
|
+
ctx.state.channels = list
|
|
46
|
+
ctx.echo('system', `频道 ${list.length} 个:`)
|
|
47
|
+
for (const [i, c] of list.entries()) {
|
|
48
|
+
ctx.echo('system', ` ${i + 1}. ${c.name}${c.id === ctx.state.activeChannelId ? ' ◀ 当前' : ''}(id ${c.id.slice(0, 8)})`)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'channel',
|
|
54
|
+
usage: 'new <name> [--desc …] [--lead <名>] | use [名|序号] | add <模板|名> [--role lead] [--harness omp] [--config JSON]',
|
|
55
|
+
description: '创建/切换频道(use 不带参数 = 交互选择)、向频道放置 Agent',
|
|
56
|
+
async handler(ctx, args, flags) {
|
|
57
|
+
const sub = args.shift()
|
|
58
|
+
if (sub === 'new') {
|
|
59
|
+
const name = args.shift()
|
|
60
|
+
if (!name) return ctx.echo('error', '用法:/channel new <name> [--desc …] [--lead <名>]')
|
|
61
|
+
const desc = args.join(' ')
|
|
62
|
+
const body = { name, description: desc }
|
|
63
|
+
if (flags.lead) body.leadAgent = { name: String(flags.lead), harness: String(flags.harness ?? 'omp') }
|
|
64
|
+
// 响应为 { channelId, leadAgentId }(manager.createChannel 收敛)
|
|
65
|
+
const result = await ctx.api.createChannel(body)
|
|
66
|
+
ctx.state.channels.push({ id: result.channelId, name, description: desc, leadAgentId: result.leadAgentId ?? null })
|
|
67
|
+
ctx.echo('system', `✔ 频道已创建:${name}(id ${result.channelId.slice(0, 8)})`)
|
|
68
|
+
await ctx.actions.switchChannel(result.channelId)
|
|
69
|
+
}
|
|
70
|
+
else if (sub === 'use') {
|
|
71
|
+
const key = args.shift()
|
|
72
|
+
if (!key) {
|
|
73
|
+
// 交互式选择(浮层)
|
|
74
|
+
ctx.actions.pickChannel?.()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
const ch = resolveChannel(ctx.state, key)
|
|
78
|
+
if (!ch) return ctx.echo('error', `频道不存在:${key}`)
|
|
79
|
+
await ctx.actions.switchChannel(ch.id)
|
|
80
|
+
}
|
|
81
|
+
else if (sub === 'add') {
|
|
82
|
+
const name = args.shift()
|
|
83
|
+
if (!name) return ctx.echo('error', '用法:/channel add <模板名|名称> [--role lead|worker] [--harness omp] [--config JSON]')
|
|
84
|
+
const role = flags.role === 'lead' ? 'lead' : 'worker'
|
|
85
|
+
const harness = String(flags.harness ?? 'omp')
|
|
86
|
+
let body
|
|
87
|
+
const templates = await ctx.api.listTemplates().catch(() => [])
|
|
88
|
+
const tpl = templates.find(t => t.name === name)
|
|
89
|
+
if (tpl) {
|
|
90
|
+
body = { agentId: tpl.id, role, configOverride: parseConfigFlag(flags.config) }
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
body = { name, harness, role, config: parseConfigFlag(flags.config) }
|
|
94
|
+
ctx.echo('system', `未找到模板「${name}」,按内联定义创建(harness=${harness})。`)
|
|
95
|
+
}
|
|
96
|
+
const inst = await ctx.api.addAgent(ctx.state.activeChannelId, body)
|
|
97
|
+
await ctx.actions.refreshAgents()
|
|
98
|
+
ctx.echo('system', `✔ Agent 已放入频道:${inst?.name ?? name}(${role})`)
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
ctx.echo('error', '用法:/channel new|use|add …(/help)')
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'agents',
|
|
107
|
+
description: '当前频道成员与实时状态',
|
|
108
|
+
async handler(ctx) {
|
|
109
|
+
await ctx.actions.refreshAgents()
|
|
110
|
+
const rows = ctx.state.agents.map((a, i) => {
|
|
111
|
+
const st = ctx.state.agentStates[a.id]
|
|
112
|
+
const state = st?.state ?? 'idle'
|
|
113
|
+
const queued = st?.queued ?? 0
|
|
114
|
+
return ` ${i + 1}. ${a.name}${a.role === 'lead' ? '(lead)' : ''} ${state === 'busy' ? '● busy' : state === 'stopped' ? '○ stopped' : '○ idle'}${queued ? ` 队列${queued}` : ''} [${a.id.slice(0, 8)}]`
|
|
115
|
+
})
|
|
116
|
+
ctx.echo('system', `成员 ${rows.length}:\n${rows.join('\n') || ' (空 —— /channel add <名>)'}`)
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'send',
|
|
121
|
+
usage: '<agent名|序号> <文本…>',
|
|
122
|
+
description: '向指定 Agent 直发消息(忙碌时 steer 注入当前回合)',
|
|
123
|
+
async handler(ctx, args) {
|
|
124
|
+
const key = args.shift()
|
|
125
|
+
const text = args.join(' ')
|
|
126
|
+
if (!key || !text) return ctx.echo('error', '用法:/send <agent名|序号> <文本…>')
|
|
127
|
+
const agent = resolveAgent(ctx.state, key)
|
|
128
|
+
if (!agent) return ctx.echo('error', `成员不存在:${key}(/agents 查看)`)
|
|
129
|
+
await ctx.api.sendMessage(ctx.state.activeChannelId, { toAgentId: agent.id, text, priority: 'immediate', requireReply: true, fromLabel: ctx.state.userName || '用户' })
|
|
130
|
+
ctx.echo('system', `✔ 已直发 ${agent.name}`)
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'msg',
|
|
135
|
+
usage: '[<agent>] <文本…>',
|
|
136
|
+
description: '发布通信消息(即时送达,忙碌时 steer 注入;缺省收件人 = 当前对话目标或 lead)',
|
|
137
|
+
async handler(ctx, args) {
|
|
138
|
+
let target = ctx.state.target
|
|
139
|
+
const first = args[0]
|
|
140
|
+
if (first && !ctx.state.agents.some(a => a.name === first || a.id === first) && !/^\d+$/.test(first)) {
|
|
141
|
+
// 首参不是成员 → 整段是文本
|
|
142
|
+
}
|
|
143
|
+
else if (first) {
|
|
144
|
+
const key = args.shift()
|
|
145
|
+
target = resolveAgent(ctx.state, key)
|
|
146
|
+
if (!target) return ctx.echo('error', `成员不存在:${key}(/agents 查看)`)
|
|
147
|
+
}
|
|
148
|
+
const text = args.join(' ')
|
|
149
|
+
if (!text) return ctx.echo('error', '用法:/msg [<agent>] <文本…>')
|
|
150
|
+
const to = target ?? leadOf(ctx.state)
|
|
151
|
+
if (!to) return ctx.echo('error', '频道没有 lead,请指定收件人:/msg <agent> <文本…>')
|
|
152
|
+
await ctx.api.sendMessage(ctx.state.activeChannelId, { toAgentId: to.id, text, priority: 'immediate', requireReply: true, fromLabel: ctx.state.userName || '用户' })
|
|
153
|
+
ctx.echo('system', `✔ 通信消息已发 → @${to.name}`)
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'task',
|
|
158
|
+
usage: '<标题…> [--mode goal|loop|pipeline] [--assignee <agent>]',
|
|
159
|
+
description: '发布正式任务(缺省路由 lead,状态经 /tasks 跟踪)',
|
|
160
|
+
async handler(ctx, args, flags) {
|
|
161
|
+
const title = args.join(' ')
|
|
162
|
+
if (!title) return ctx.echo('error', '用法:/task <标题…> [--mode goal|loop|pipeline] [--assignee <agent>]')
|
|
163
|
+
const body = { title, mode: flags.mode ?? 'goal' }
|
|
164
|
+
if (flags.assignee) {
|
|
165
|
+
const a = resolveAgent(ctx.state, String(flags.assignee))
|
|
166
|
+
if (!a) return ctx.echo('error', `assignee 不存在:${flags.assignee}`)
|
|
167
|
+
body.assigneeId = a.id
|
|
168
|
+
}
|
|
169
|
+
else if (ctx.state.target) {
|
|
170
|
+
body.assigneeId = ctx.state.target.agentId
|
|
171
|
+
}
|
|
172
|
+
const task = await ctx.api.submitTask(ctx.state.activeChannelId, body)
|
|
173
|
+
ctx.echo('system', `✔ 任务已发布:${task?.id?.slice(0, 8) ?? ''}「${title}」`)
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'tasks',
|
|
178
|
+
description: '任务列表与状态',
|
|
179
|
+
async handler(ctx) {
|
|
180
|
+
const list = await ctx.api.listTasks(ctx.state.activeChannelId)
|
|
181
|
+
ctx.state.tasks = list
|
|
182
|
+
const rows = list.slice(0, 20).map((t, i) => ` ${i + 1}. [${t.state}] ${t.title ?? t.taskId?.slice(0, 8)}${t.progress ? ` ${t.progress}%` : ''}`)
|
|
183
|
+
ctx.echo('system', `任务 ${list.length}:\n${rows.join('\n') || ' (无)'}`)
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'monitor',
|
|
188
|
+
usage: '<agent名|序号|off>',
|
|
189
|
+
description: '开/关右侧终端镜像面板(实时查看该 Agent 执行过程)',
|
|
190
|
+
async handler(ctx, args) {
|
|
191
|
+
const key = args.shift()
|
|
192
|
+
if (!key || key === 'off') {
|
|
193
|
+
ctx.actions.closeMonitor()
|
|
194
|
+
ctx.echo('system', '监控面板已关闭')
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
await ctx.actions.refreshAgents()
|
|
198
|
+
const agent = resolveAgent(ctx.state, key)
|
|
199
|
+
if (!agent) return ctx.echo('error', `成员不存在:${key}(/agents 查看)`)
|
|
200
|
+
ctx.actions.openMonitor(agent)
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'hitl',
|
|
205
|
+
usage: '[序号|off]',
|
|
206
|
+
description: '列出待人工处理事项;带序号进入作答;off 放弃作答',
|
|
207
|
+
async handler(ctx, args) {
|
|
208
|
+
const key = args.shift()
|
|
209
|
+
if (key === 'off') {
|
|
210
|
+
ctx.state.hitlAnswering = null
|
|
211
|
+
ctx.echo('system', '已放弃当前作答(对话框仍在待办中,可 /hitl 重进)')
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (key === undefined) {
|
|
215
|
+
const list = ctx.state.hitl.length > 0 ? ctx.state.hitl : await ctx.api.hitlPending(ctx.state.activeChannelId)
|
|
216
|
+
ctx.state.hitl = list
|
|
217
|
+
if (list.length === 0) return ctx.echo('system', '当前没有待人工处理的事项。')
|
|
218
|
+
const rows = list.map((it, i) => ` ${i + 1}. [${it.kind}] ${it.title} —— ${it.agentName}${it.expiresAt ? `(park 截止 ${it.expiresAt.slice(11, 19)})` : ''}`)
|
|
219
|
+
ctx.echo('warn', `待人工处理 ${list.length} 项:\n${rows.join('\n')}\n/hitl <序号> 进入作答。`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
const idx = Number.parseInt(key, 10) - 1
|
|
223
|
+
const item = ctx.state.hitl[idx]
|
|
224
|
+
if (!item) return ctx.echo('error', `序号无效:${key}(/hitl 查看列表)`)
|
|
225
|
+
ctx.state.hitlAnswering = item
|
|
226
|
+
ctx.echo('warn', `进入作答:${item.title} —— 完成输入回车提交,/hitl off 放弃。`)
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'quit',
|
|
231
|
+
description: '退出 TUI',
|
|
232
|
+
handler(ctx) {
|
|
233
|
+
ctx.actions.quit()
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
function resolveChannel(state, key) {
|
|
239
|
+
const byIdx = state.channels[Number.parseInt(key, 10) - 1]
|
|
240
|
+
return state.channels.find(c => c.name === key || c.id === key) ?? (Number.isInteger(Number.parseInt(key, 10)) ? byIdx : undefined)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveAgent(state, key) {
|
|
244
|
+
const byIdx = state.agents[Number.parseInt(key, 10) - 1]
|
|
245
|
+
return state.agents.find(a => a.name === key || a.id === key) ?? (Number.isInteger(Number.parseInt(key, 10)) ? byIdx : undefined)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 当前对话目标的收件人解析:目标成员 → lead */
|
|
249
|
+
function leadOf(state) {
|
|
250
|
+
const ch = state.channels.find(c => c.id === state.activeChannelId)
|
|
251
|
+
const leadId = ch?.leadAgentId ?? state.agents.find(a => a.role === 'lead')?.id
|
|
252
|
+
return state.agents.find(a => a.id === leadId) ?? null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseConfigFlag(raw) {
|
|
256
|
+
if (!raw || raw === true) return undefined
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(String(raw))
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
throw new Error(`--config 不是合法 JSON:${raw}`)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 命令分发(编辑器 onSubmit 入口;返回 true = 已作为命令消费) */
|
|
266
|
+
export async function dispatchCommand(ctx, input) {
|
|
267
|
+
const parsed = parseCommand(input)
|
|
268
|
+
if (!parsed) return false
|
|
269
|
+
const cmd = commands.find(c => c.name === parsed.name)
|
|
270
|
+
if (!cmd) {
|
|
271
|
+
ctx.echo('error', `未知命令:/${parsed.name}(/help 查看命令列表)`)
|
|
272
|
+
return true
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
await cmd.handler(ctx, parsed.args, parsed.flags)
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
ctx.echo('error', `/${cmd.name} 失败:${err.message}`)
|
|
279
|
+
}
|
|
280
|
+
return true
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** pi-tui CombinedAutocompleteProvider 的命令表(斜杠补全;name 不带 '/'——
|
|
284
|
+
* provider 把 '/' 当触发符自带,带了会在应用补全时拼出 '//cmd') */
|
|
285
|
+
export function slashCommandCompletions() {
|
|
286
|
+
return commands.map(c => ({ name: c.name, description: c.description }))
|
|
287
|
+
}
|