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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ChatLog 组件 —— 会话时间线(AEP 归约后的 log 行渲染)。
|
|
3
|
+
// 每次 render 全量输出(有界 400 行),增量绘制由 TuiMainScreen 差分完成。
|
|
4
|
+
// 输出经 truncateToWidth 兜底(CJK 宽字符行宽;pi-tui 对超宽行直接 crash)。
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { theme } from '../theme.mjs'
|
|
7
|
+
import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
|
|
8
|
+
|
|
9
|
+
const TONE_OF = {
|
|
10
|
+
'system': s => theme.faint(s),
|
|
11
|
+
'user': s => `${theme.info('◆ 你')} ${s}`,
|
|
12
|
+
'agent': s => `${theme.accent('◆ Agent')} ${s}`,
|
|
13
|
+
'stream': s => theme.faint(s),
|
|
14
|
+
'task': s => theme.info(s),
|
|
15
|
+
'hitl': s => theme.warn(s),
|
|
16
|
+
'hitl-resolved': s => theme.accent(s),
|
|
17
|
+
'error': s => theme.error(`✗ ${s}`),
|
|
18
|
+
'notice': s => theme.faint(`· ${s}`),
|
|
19
|
+
'monitor': s => s,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ChatLog {
|
|
23
|
+
constructor(state) {
|
|
24
|
+
this.state = state
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
render(width) {
|
|
28
|
+
const w = Math.max(width, 20)
|
|
29
|
+
const lines = []
|
|
30
|
+
for (const row of this.state.log) {
|
|
31
|
+
const paint = TONE_OF[row.kind] ?? (s => s)
|
|
32
|
+
const prefix = row.kind === 'agent' && row.agentName ? `${theme.faint(`[${row.agentName}]`)} ` : ''
|
|
33
|
+
const wrapped = wrapTextWithAnsi(paint(`${prefix}${row.text}`), w)
|
|
34
|
+
lines.push(...wrapped)
|
|
35
|
+
}
|
|
36
|
+
if (this.state.statusMsg && Date.now() - this.state.statusAt < 15_000) {
|
|
37
|
+
lines.push('')
|
|
38
|
+
lines.push(theme.faint(this.state.statusMsg))
|
|
39
|
+
}
|
|
40
|
+
return lines.map(l => truncateToWidth(l, w))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// HitlCard —— 作答模式卡片(state.hitlAnswering 非空时渲染于输入框上方)。
|
|
3
|
+
// 作答约定:confirm → y/n;select → 选项序号;input/editor → 自由文本;
|
|
4
|
+
// /hitl off 放弃作答(等价 Esc)。提交经 POST /api/workshop/hitl/respond。
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { theme } from '../theme.mjs'
|
|
7
|
+
import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
|
|
8
|
+
|
|
9
|
+
const HINT = {
|
|
10
|
+
confirm: '输入 y 批准 / n 拒绝',
|
|
11
|
+
select: '输入选项序号(如 1)',
|
|
12
|
+
input: '输入回答文本',
|
|
13
|
+
editor: '输入回答文本',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class HitlCard {
|
|
17
|
+
constructor(state) {
|
|
18
|
+
this.state = state
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(width) {
|
|
22
|
+
const item = this.state.hitlAnswering
|
|
23
|
+
if (!item) return []
|
|
24
|
+
const w = Math.max(width - 4, 20)
|
|
25
|
+
const lines = []
|
|
26
|
+
lines.push(theme.warn(`⏸ HITL 作答 · ${item.agentName} · ${item.kind === 'omp-dialog' ? 'omp 对话框' : '下发审批'}`))
|
|
27
|
+
lines.push(truncateToWidth(theme.bold(String(item.title ?? '')), w))
|
|
28
|
+
if (item.detail) lines.push(...wrapTextWithAnsi(theme.faint(item.detail), w))
|
|
29
|
+
if (item.message) lines.push(...wrapTextWithAnsi(String(item.message), w))
|
|
30
|
+
if (item.method === 'select' && Array.isArray(item.options)) {
|
|
31
|
+
item.options.forEach((opt, i) => lines.push(` ${theme.accent(`${i + 1}.`)} ${opt}`))
|
|
32
|
+
}
|
|
33
|
+
lines.push(theme.faint(`${HINT[item.method] ?? '输入回答'} · /hitl off 放弃`))
|
|
34
|
+
return lines.map(l => truncateToWidth(l, Math.max(width, 20)))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MonitorPane —— 右侧 agent 终端镜像面板(term 帧 → 行)。
|
|
3
|
+
// 行来源:aw-tui 的终端 WS 接线把 term.* 帧写入 state.monitor。
|
|
4
|
+
// ============================================================
|
|
5
|
+
import { theme } from '../theme.mjs'
|
|
6
|
+
import { wrapTextWithAnsi, truncateToWidth } from '@earendil-works/pi-tui'
|
|
7
|
+
|
|
8
|
+
const TONE = {
|
|
9
|
+
dim: s => theme.faint(s),
|
|
10
|
+
info: s => theme.info(s),
|
|
11
|
+
warn: s => theme.warn(s),
|
|
12
|
+
error: s => theme.error(s),
|
|
13
|
+
accent: s => theme.accent(s),
|
|
14
|
+
monitor: s => s,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class MonitorPane {
|
|
18
|
+
constructor(state, width = 44) {
|
|
19
|
+
this.state = state
|
|
20
|
+
this.width = width
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
render() {
|
|
24
|
+
const m = this.state.monitor
|
|
25
|
+
const w = Math.max(this.width - 2, 16)
|
|
26
|
+
const head = theme.info(`┌─ 监控 ${m.name ?? m.agentId?.slice(0, 8) ?? ''} ${m.streaming ? '· 流式中' : ''}`)
|
|
27
|
+
const lines = [head]
|
|
28
|
+
if (m.waiting) lines.push(theme.warn('omp 进程未启动,等待首个任务…'))
|
|
29
|
+
if (!m.agentId) lines.push(theme.faint('(未开启 —— /monitor <agent名|序号>)'))
|
|
30
|
+
for (const l of m.lines) {
|
|
31
|
+
const paint = TONE[l.tone] ?? (s => s)
|
|
32
|
+
lines.push(...wrapTextWithAnsi(paint(l.text), w))
|
|
33
|
+
}
|
|
34
|
+
if (m.streamText) {
|
|
35
|
+
lines.push(...wrapTextWithAnsi(theme.faint(m.streamText.slice(-600)), w))
|
|
36
|
+
}
|
|
37
|
+
return lines.slice(0, 200).map(l => truncateToWidth(l, w))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Root —— 组件树组装:双栏(会话流 | 监控面板) + HITL 卡 + 状态条 + 编辑器。
|
|
3
|
+
// 双栏为手工列合成(pi-tui Container 只做纵向;列宽恒定防抖动)。
|
|
4
|
+
// 列宽计算必须用 CJK 感知的 visibleWidth(中文占 2 列;stripAnsi().length 会低估)。
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { visibleWidth, truncateToWidth } from '@earendil-works/pi-tui'
|
|
7
|
+
import { theme } from '../theme.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 双栏容器:左栏自适应,右栏固定宽;任一为空退化为单栏。
|
|
11
|
+
* left/right 需实现 render(width) → string[](Component duck-typing)。
|
|
12
|
+
*/
|
|
13
|
+
export class Columns {
|
|
14
|
+
constructor(left, right = null, rightWidth = 46) {
|
|
15
|
+
this.left = left
|
|
16
|
+
this.right = right
|
|
17
|
+
this.rightWidth = rightWidth
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
render(width) {
|
|
21
|
+
const gutter = 3
|
|
22
|
+
const leftWidth = this.right ? width - this.rightWidth - gutter : width
|
|
23
|
+
const leftLines = this.left.render(leftWidth).map(l => truncateToWidth(l, leftWidth))
|
|
24
|
+
if (!this.right || this.right.render === undefined) return leftLines
|
|
25
|
+
const rightLines = this.right.render(this.rightWidth).map(l => truncateToWidth(l, this.rightWidth - 1))
|
|
26
|
+
const height = Math.max(leftLines.length, rightLines.length)
|
|
27
|
+
const out = []
|
|
28
|
+
for (let i = 0; i < height; i++) {
|
|
29
|
+
const l = leftLines[i] ?? ''
|
|
30
|
+
const r = rightLines[i] ?? ''
|
|
31
|
+
const padL = ' '.repeat(Math.max(leftWidth - visibleWidth(l), 0))
|
|
32
|
+
out.push(`${l}${padL}${theme.faint(' │ ')}${r}`)
|
|
33
|
+
}
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 组装整棵组件树(aw-tui 调用;返回可挂到 TUI 的顶层组件) */
|
|
39
|
+
export function buildTree({ state, chatLog, monitorPane, hitlCard, statusBar, editor }) {
|
|
40
|
+
const columns = new Columns(chatLog, monitorPane)
|
|
41
|
+
const stack = {
|
|
42
|
+
render(width) {
|
|
43
|
+
return [
|
|
44
|
+
...columns.render(width),
|
|
45
|
+
...hitlCard.render(width),
|
|
46
|
+
...statusBar.render(width),
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
void state
|
|
51
|
+
void editor
|
|
52
|
+
return { stack, editor }
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// StatusBar —— 底部状态条:频道/成员忙碌/HITL 待办/连接态。
|
|
3
|
+
// ============================================================
|
|
4
|
+
import { theme } from '../theme.mjs'
|
|
5
|
+
import { truncateToWidth } from '@earendil-works/pi-tui'
|
|
6
|
+
|
|
7
|
+
const CONN_LABEL = { open: '实时', connecting: '连接中', closed: '离线' }
|
|
8
|
+
|
|
9
|
+
export class StatusBar {
|
|
10
|
+
constructor(state) {
|
|
11
|
+
this.state = state
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render(width) {
|
|
15
|
+
const s = this.state
|
|
16
|
+
const channel = s.channels.find(c => c.id === s.activeChannelId)
|
|
17
|
+
const members = s.agents.length
|
|
18
|
+
const busy = Object.values(s.agentStates).filter(a => a.state === 'busy').length
|
|
19
|
+
const target = s.target
|
|
20
|
+
? theme.info(`@${s.target.name}(通信)`)
|
|
21
|
+
: theme.accent('频道(任务)')
|
|
22
|
+
const parts = [
|
|
23
|
+
theme.accent(`▣ ${channel?.name ?? '未选择频道'}`),
|
|
24
|
+
`${theme.faint('输入→')} ${target}`,
|
|
25
|
+
members > 0 ? theme.faint(`成员 ${members}(忙 ${busy})`) : theme.faint('成员 0'),
|
|
26
|
+
s.hitl.length > 0 ? theme.warn(`⏸ HITL 待处理 ${s.hitl.length}(/hitl)`) : theme.faint('HITL 0'),
|
|
27
|
+
s.monitor.agentId ? theme.info(`监控:${s.monitor.waiting ? '等待进程' : s.monitor.name}`) : null,
|
|
28
|
+
s.connState === 'open' ? theme.faint(`● ${CONN_LABEL[s.connState]}`) : theme.warn(`● ${CONN_LABEL[s.connState] ?? s.connState}`),
|
|
29
|
+
].filter(Boolean)
|
|
30
|
+
const line = parts.join(theme.faint(' · '))
|
|
31
|
+
return [truncateToWidth(line, Math.max(width, 20))]
|
|
32
|
+
}
|
|
33
|
+
}
|
package/tui/lib/api.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// TUI REST 客户端 —— 统一信封 { code, message, data } 解包 + Bearer 注入。
|
|
3
|
+
// 与 sdk/api.mjs 同语义(轻量自包含:TUI 不依赖构建产物,保持 .mjs 直跑)。
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 创建 API 客户端
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {string} opts.baseUrl 形如 http://127.0.0.1:3000
|
|
10
|
+
* @param {string} opts.token 用户 token(ut-*)
|
|
11
|
+
*/
|
|
12
|
+
export function createApi({ baseUrl, token, timeoutMs = 15_000 }) {
|
|
13
|
+
const base = String(baseUrl ?? '').replace(/\/+$/, '')
|
|
14
|
+
// 动态 token:登录/运行中换凭据即时生效(避免实例化时闭包捕获 undefined)
|
|
15
|
+
let currentToken = token ?? ''
|
|
16
|
+
|
|
17
|
+
async function request(path, { method = 'GET', body, query } = {}) {
|
|
18
|
+
const url = new URL(base + path)
|
|
19
|
+
if (query) for (const [k, v] of Object.entries(query)) url.searchParams.set(k, String(v))
|
|
20
|
+
const ctrl = new AbortController()
|
|
21
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method,
|
|
25
|
+
headers: {
|
|
26
|
+
...(currentToken ? { authorization: `Bearer ${currentToken}` } : {}),
|
|
27
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
28
|
+
},
|
|
29
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
30
|
+
signal: ctrl.signal,
|
|
31
|
+
})
|
|
32
|
+
const json = await res.json().catch(() => null)
|
|
33
|
+
if (!json || json.code === undefined) {
|
|
34
|
+
throw new Error(`HTTP ${res.status}(响应非标准信封)`)
|
|
35
|
+
}
|
|
36
|
+
if (json.code !== 0) {
|
|
37
|
+
const err = new Error(json.message ?? String(json.code))
|
|
38
|
+
err.code = json.code
|
|
39
|
+
throw err
|
|
40
|
+
}
|
|
41
|
+
return json.data
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timer)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
request,
|
|
50
|
+
/** 运行中更新凭据(登录后/换号) */
|
|
51
|
+
setToken(t) {
|
|
52
|
+
currentToken = t ?? ''
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// ── 登录(无 token 引导;成功自动记住 token) ──
|
|
56
|
+
async login(email, password) {
|
|
57
|
+
const res = await request('/api/users/login', { method: 'POST', body: { email, password } })
|
|
58
|
+
currentToken = res.token
|
|
59
|
+
return res
|
|
60
|
+
},
|
|
61
|
+
get: (path, query) => request(path, { query }),
|
|
62
|
+
post: (path, body) => request(path, { method: 'POST', body }),
|
|
63
|
+
|
|
64
|
+
// ── channel/agent/task 管理面 ──
|
|
65
|
+
listChannels: () => request('/api/workshop/channels'),
|
|
66
|
+
createChannel: body => request('/api/workshop/channels', { method: 'POST', body }),
|
|
67
|
+
listAgents: channelId => request(`/api/workshop/channels/${channelId}/agents`),
|
|
68
|
+
addAgent: (channelId, body) => request(`/api/workshop/channels/${channelId}/agents`, { method: 'POST', body }),
|
|
69
|
+
listTemplates: () => request('/api/workshop/agents'),
|
|
70
|
+
listMessages: (channelId, limit = 50) => request(`/api/workshop/channels/${channelId}/messages`, { query: { limit } }),
|
|
71
|
+
sendMessage: (channelId, body) => request(`/api/workshop/channels/${channelId}/messages`, { method: 'POST', body }),
|
|
72
|
+
listTasks: channelId => request(`/api/workshop/channels/${channelId}/tasks`),
|
|
73
|
+
submitTask: (channelId, body) => request(`/api/workshop/channels/${channelId}/tasks`, { method: 'POST', body }),
|
|
74
|
+
|
|
75
|
+
// ── HITL 统一面(pending 响应为 { items: [...] },此处解包为数组) ──
|
|
76
|
+
hitlPending: async (channelId) => {
|
|
77
|
+
const data = await request('/api/workshop/hitl/pending', { query: channelId ? { channelId } : {} })
|
|
78
|
+
return data?.items ?? []
|
|
79
|
+
},
|
|
80
|
+
hitlRespond: body => request('/api/workshop/hitl/respond', { method: 'POST', body }),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// TUI 认证/连接配置 —— 落配置根 .AgentWorkShop/tui-auth.json(0600)。
|
|
3
|
+
// 解析口径与服务端一致:cwd 向上找 config.yml → <repo>/.AgentWorkShop,
|
|
4
|
+
// 找不到(全局安装任意 cwd)→ ~/.AgentWorkShop。
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { join, resolve, dirname } from 'node:path'
|
|
9
|
+
|
|
10
|
+
/** 向上查找文件(repo 模式判定) */
|
|
11
|
+
function findUp(startDir, filename) {
|
|
12
|
+
let dir = resolve(startDir)
|
|
13
|
+
for (;;) {
|
|
14
|
+
const candidate = join(dir, filename)
|
|
15
|
+
if (existsSync(candidate)) return candidate
|
|
16
|
+
const parent = dirname(dir)
|
|
17
|
+
if (parent === dir) return null
|
|
18
|
+
dir = parent
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 配置根(repo 内 cwd → <repo>/.AgentWorkShop;否则 ~/.AgentWorkShop) */
|
|
23
|
+
export function configRoot(cwd = process.cwd()) {
|
|
24
|
+
const marker = findUp(cwd, 'config.yml')
|
|
25
|
+
if (marker) return join(dirname(marker), '.AgentWorkShop')
|
|
26
|
+
return join(homedir(), '.AgentWorkShop')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const authPath = cwd => join(configRoot(cwd), 'tui-auth.json')
|
|
30
|
+
|
|
31
|
+
/** 读取已存凭据({ baseUrl, token, email? } 或 null) */
|
|
32
|
+
export function loadAuth(cwd = process.cwd()) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = JSON.parse(readFileSync(authPath(cwd), 'utf8'))
|
|
35
|
+
if (raw && typeof raw.token === 'string' && typeof raw.baseUrl === 'string') return raw
|
|
36
|
+
}
|
|
37
|
+
catch { /* 无凭据/损坏 → 重新登录 */ }
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 持久化凭据(0600;路径加入 .gitignore) */
|
|
42
|
+
export function saveAuth(auth, cwd = process.cwd()) {
|
|
43
|
+
const file = authPath(cwd)
|
|
44
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
45
|
+
writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, 'utf8')
|
|
46
|
+
try {
|
|
47
|
+
chmodSync(file, 0o600)
|
|
48
|
+
}
|
|
49
|
+
catch { /* Windows FAT 卷 chmod 无效,忽略 */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 清除凭据(登出) */
|
|
53
|
+
export function clearAuth(cwd = process.cwd()) {
|
|
54
|
+
try {
|
|
55
|
+
existsSync(authPath(cwd)) && (writeFileSync(authPath(cwd), '{}\n', 'utf8'), true)
|
|
56
|
+
}
|
|
57
|
+
catch { /* 尽力而为 */ }
|
|
58
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 交互选择器(浮层)—— 频道选择 / Agent 选择。
|
|
3
|
+
// 基于 pi-tui showOverlay + SelectList:方向键移动、Enter 选择、Esc 取消。
|
|
4
|
+
// ============================================================
|
|
5
|
+
import { SelectList } from '@earendil-works/pi-tui'
|
|
6
|
+
import { theme } from '../theme.mjs'
|
|
7
|
+
|
|
8
|
+
/** 组合"标题 + SelectList"的浮层组件。返回 handle { hide, list, component, __bindHandle } */
|
|
9
|
+
function pickerComponent(title, items, maxVisible, onSelect, onCancel) {
|
|
10
|
+
const list = new SelectList(items, maxVisible, theme.editor.selectList)
|
|
11
|
+
const wrap = {
|
|
12
|
+
render(width) {
|
|
13
|
+
return [theme.accent(`┌─ ${title}`), ...list.render(Math.max(width - 2, 24))]
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
const handle = { hide() {}, list, component: wrap }
|
|
17
|
+
list.onSelect = (item) => {
|
|
18
|
+
handle.hide()
|
|
19
|
+
onSelect(item)
|
|
20
|
+
}
|
|
21
|
+
list.onCancel = () => {
|
|
22
|
+
handle.hide()
|
|
23
|
+
onCancel?.()
|
|
24
|
+
}
|
|
25
|
+
// 二次装配:showOverlay 返回真实 handle 后回填 hide(调用方调 picker.__bindHandle(real))
|
|
26
|
+
handle.__bindHandle = (h) => {
|
|
27
|
+
handle.hide = () => h.hide()
|
|
28
|
+
}
|
|
29
|
+
return handle
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 频道选择浮层。Enter 选择并切换;Esc 取消(fallback 频道由调用方决定)。
|
|
34
|
+
* @returns handle { close }
|
|
35
|
+
*/
|
|
36
|
+
export function openChannelPicker(tui, state, { onSelect, onCancel } = {}) {
|
|
37
|
+
const items = state.channels.map(c => ({
|
|
38
|
+
value: c.id,
|
|
39
|
+
label: c.name,
|
|
40
|
+
description: c.leadAgentId ? 'lead 已配置' : '无 lead(仅收发消息)',
|
|
41
|
+
}))
|
|
42
|
+
const picker = pickerComponent('选择要进入的频道(↑↓ 移动 · Enter 进入 · Esc 取消)', items, Math.min(10, items.length),
|
|
43
|
+
(item) => {
|
|
44
|
+
state.channelPickerOpen = false
|
|
45
|
+
onSelect?.(item.value)
|
|
46
|
+
},
|
|
47
|
+
() => {
|
|
48
|
+
state.channelPickerOpen = false
|
|
49
|
+
onCancel?.()
|
|
50
|
+
})
|
|
51
|
+
state.channelPickerOpen = true
|
|
52
|
+
const handle = tui.showOverlay(picker.component, { anchor: 'center' })
|
|
53
|
+
picker.__bindHandle(handle)
|
|
54
|
+
tui.setFocus(picker.list)
|
|
55
|
+
return handle
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Agent/对话目标选择浮层(Tab 触发)。首项 = 频道(发布任务模式);
|
|
60
|
+
* 选具体成员 = 切换通信目标并自动打开其独立监控。
|
|
61
|
+
*/
|
|
62
|
+
export function openTargetPicker(tui, state, { onPick, onCancel } = {}) {
|
|
63
|
+
const s = state.agents
|
|
64
|
+
.filter(a => a.enabled !== 0)
|
|
65
|
+
.map(a => ({
|
|
66
|
+
value: a.id,
|
|
67
|
+
label: `${a.name}${a.role === 'lead' ? '(lead)' : ''}`,
|
|
68
|
+
description: `通信对话 + 独立监控 · ${state.agentStates[a.id]?.state ?? 'idle'}`,
|
|
69
|
+
}))
|
|
70
|
+
const items = [{ value: '__channel__', label: '频道(发布任务)', description: '普通文本 = 向频道发布正式任务' }, ...s]
|
|
71
|
+
if (items.length === 1 && s.length === 0) {
|
|
72
|
+
onCancel?.()
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const picker = pickerComponent('选择对话目标(Tab 呼出 · ↑↓ 移动 · Enter 确认 · Esc 取消)', items, Math.min(10, items.length),
|
|
76
|
+
(item) => {
|
|
77
|
+
if (process.env.AW_TUI_DEBUG) console.error('[picker-debug] item=', JSON.stringify(item), 'agentIds=', JSON.stringify(s.map(x => x.value)))
|
|
78
|
+
state.targetPickerOpen = false
|
|
79
|
+
if (item.value === '__channel__') {
|
|
80
|
+
state.target = null
|
|
81
|
+
onPick?.(null)
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const a = state.agents.find(x => x.id === item.value)
|
|
85
|
+
if (!a) {
|
|
86
|
+
onCancel?.()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
state.target = { agentId: a.id, name: a.name }
|
|
90
|
+
onPick?.(a)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
() => {
|
|
94
|
+
state.targetPickerOpen = false
|
|
95
|
+
onCancel?.()
|
|
96
|
+
})
|
|
97
|
+
state.targetPickerOpen = true
|
|
98
|
+
const handle = tui.showOverlay(picker.component, { anchor: 'center' })
|
|
99
|
+
picker.__bindHandle(handle)
|
|
100
|
+
tui.setFocus(picker.list)
|
|
101
|
+
return handle
|
|
102
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// AEP 帧归约(纯函数式:输入 state+信封 → 原地更新 state 与 log 行)。
|
|
3
|
+
// 与 WebUI 的 events.store 同一套语义:agent.delta 聚合进流式块,落定
|
|
4
|
+
// agent.message 携带全文定稿(去重);hitl 增删幂等(kind+id)。
|
|
5
|
+
// 可被 tsx/单测与 scripts/tui-smoke.mjs 无头驱动。
|
|
6
|
+
// ============================================================
|
|
7
|
+
import { pushLog } from './state.mjs'
|
|
8
|
+
|
|
9
|
+
const STREAM_TAIL = 4000
|
|
10
|
+
|
|
11
|
+
/** 从信封/消息提取展示名 */
|
|
12
|
+
function agentNameOf(state, agentId, fallback = '') {
|
|
13
|
+
if (!agentId) return fallback
|
|
14
|
+
const a = state.agents.find(x => x.id === agentId)
|
|
15
|
+
return a?.name ?? fallback ?? agentId.slice(0, 8)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 消息 parts → 纯文本 */
|
|
19
|
+
function partsText(message) {
|
|
20
|
+
const parts = message?.parts ?? []
|
|
21
|
+
return parts.map((p) => {
|
|
22
|
+
if (typeof p === 'string') return p
|
|
23
|
+
if (p && typeof p === 'object' && typeof p.text === 'string') return p.text
|
|
24
|
+
return ''
|
|
25
|
+
}).join('').trim()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 归约一帧 AEP 信封。返回 'handled' | 'ignored'(测试断言用)。
|
|
30
|
+
* 流式块键 = agentId+taskId(agent.message 到达即定稿替换全文)。
|
|
31
|
+
*/
|
|
32
|
+
export function reduceEnvelope(state, e) {
|
|
33
|
+
switch (e.type) {
|
|
34
|
+
case 'agent.status': {
|
|
35
|
+
const p = e.payload
|
|
36
|
+
state.agentStates[p.agentId] = {
|
|
37
|
+
state: p.state,
|
|
38
|
+
currentTaskId: p.currentTaskId ?? null,
|
|
39
|
+
queued: p.queued ?? 0,
|
|
40
|
+
completed: p.completed ?? 0,
|
|
41
|
+
}
|
|
42
|
+
return 'handled'
|
|
43
|
+
}
|
|
44
|
+
case 'agent.delta': {
|
|
45
|
+
const text = e.payload?.delta ?? ''
|
|
46
|
+
if (!text) return 'ignored'
|
|
47
|
+
const row = findStreamRow(state, e.agentId, e.taskId)
|
|
48
|
+
if (row) {
|
|
49
|
+
row.text = (row.text + text).slice(-STREAM_TAIL)
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
pushLog(state, 'stream', text, { agentName: agentNameOf(state, e.agentId), agentId: e.agentId, taskId: e.taskId ?? null })
|
|
53
|
+
}
|
|
54
|
+
return 'handled'
|
|
55
|
+
}
|
|
56
|
+
case 'agent.message': {
|
|
57
|
+
const msg = e.payload ?? {}
|
|
58
|
+
const text = partsText(msg)
|
|
59
|
+
if (!text) return 'ignored'
|
|
60
|
+
const agentId = e.agentId ?? msg.metadata?.['x-aw-from-agent'] ?? ''
|
|
61
|
+
const name = agentNameOf(state, agentId, msg.metadata?.['x-aw-from-label'] ?? '')
|
|
62
|
+
const row = findStreamRow(state, agentId, e.taskId)
|
|
63
|
+
if (row) {
|
|
64
|
+
// 定稿:流式增量替换为全文,并升级为正式 agent 行(渲染带名高亮)
|
|
65
|
+
row.text = text
|
|
66
|
+
row.kind = 'agent'
|
|
67
|
+
row.agentName = name
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
pushLog(state, 'agent', text, { agentName: name, agentId, taskId: e.taskId ?? null })
|
|
71
|
+
}
|
|
72
|
+
return 'handled'
|
|
73
|
+
}
|
|
74
|
+
case 'a2a.message': {
|
|
75
|
+
const msg = e.payload ?? {}
|
|
76
|
+
const meta = msg.metadata ?? {}
|
|
77
|
+
const fromLabel = meta['x-aw-from-label'] ?? ''
|
|
78
|
+
const fromAgent = meta['x-aw-from-agent'] ?? e.agentId ?? ''
|
|
79
|
+
const text = partsText(msg)
|
|
80
|
+
if (!text) return 'ignored'
|
|
81
|
+
const cross = meta['x-aw-cross-channel'] === 'true'
|
|
82
|
+
const prefix = cross ? '[跨频道] ' : ''
|
|
83
|
+
if (fromAgent) {
|
|
84
|
+
pushLog(state, 'agent', prefix + text, { agentName: agentNameOf(state, fromAgent, fromLabel), agentId: fromAgent, taskId: msg.taskId ?? null })
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// 人类消息(服务端盖章 from-label;本端发出的回显也走这里)
|
|
88
|
+
pushLog(state, 'user', prefix + text, { agentName: fromLabel || '用户' })
|
|
89
|
+
}
|
|
90
|
+
return 'handled'
|
|
91
|
+
}
|
|
92
|
+
case 'agent.status.message': {
|
|
93
|
+
const text = (e.payload?.text ?? '').trim()
|
|
94
|
+
if (text) pushLog(state, 'notice', text, { agentName: agentNameOf(state, e.agentId) })
|
|
95
|
+
return 'handled'
|
|
96
|
+
}
|
|
97
|
+
case 'task.status': {
|
|
98
|
+
const p = e.payload ?? {}
|
|
99
|
+
upsertTask(state, p)
|
|
100
|
+
const title = p.title ?? state.tasks.find(t2 => t2.taskId === p.taskId)?.title ?? ''
|
|
101
|
+
pushLog(state, 'task', `任务 ${p.taskId?.slice(0, 8)}${title ? `「${title}」` : ''} → ${p.state}${p.progress != null ? ` ${p.progress}%` : ''}`)
|
|
102
|
+
return 'handled'
|
|
103
|
+
}
|
|
104
|
+
case 'task.progress': {
|
|
105
|
+
const p = e.payload ?? {}
|
|
106
|
+
upsertTask(state, p)
|
|
107
|
+
return 'handled'
|
|
108
|
+
}
|
|
109
|
+
case 'a2a.artifact': {
|
|
110
|
+
const art = e.payload?.artifact
|
|
111
|
+
const name = art?.name ?? art?.kind ?? 'artifact'
|
|
112
|
+
pushLog(state, 'notice', `交付物:${name}`, { agentName: agentNameOf(state, e.agentId) })
|
|
113
|
+
return 'handled'
|
|
114
|
+
}
|
|
115
|
+
case 'error': {
|
|
116
|
+
pushLog(state, 'error', e.payload?.message ?? '未知错误', { agentName: agentNameOf(state, e.agentId) })
|
|
117
|
+
return 'handled'
|
|
118
|
+
}
|
|
119
|
+
case 'hitl.request': {
|
|
120
|
+
upsertHitl(state, e.payload)
|
|
121
|
+
const it = e.payload
|
|
122
|
+
pushLog(state, 'hitl', `⏸ 需要人工处理:${it.title}(${it.agentName})—— /hitl 查看并作答`)
|
|
123
|
+
return 'handled'
|
|
124
|
+
}
|
|
125
|
+
case 'hitl.resolved': {
|
|
126
|
+
const r = e.payload
|
|
127
|
+
state.hitl = state.hitl.filter(i => !(i.kind === r.kind && i.id === r.id))
|
|
128
|
+
if (state.hitlAnswering && state.hitlAnswering.kind === r.kind && state.hitlAnswering.id === r.id) state.hitlAnswering = null
|
|
129
|
+
pushLog(state, 'hitl-resolved', `✔ 待办已落定:${r.id}(outcome=${r.outcome}${r.by ? `, by ${r.by}` : ''})`)
|
|
130
|
+
return 'handled'
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
return 'ignored'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findStreamRow(state, agentId, taskId) {
|
|
138
|
+
for (let i = state.log.length - 1; i >= 0; i--) {
|
|
139
|
+
const row = state.log[i]
|
|
140
|
+
if (row.kind === 'stream' && row.agentId === agentId && (row.taskId ?? null) === (taskId ?? null)) return row
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function upsertTask(state, p) {
|
|
146
|
+
if (!p.taskId) return
|
|
147
|
+
const idx = state.tasks.findIndex(t => t.taskId === p.taskId)
|
|
148
|
+
if (idx >= 0) state.tasks[idx] = { ...state.tasks[idx], ...p }
|
|
149
|
+
else state.tasks.unshift(p)
|
|
150
|
+
state.tasks = state.tasks.slice(0, 100)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function upsertHitl(state, item) {
|
|
154
|
+
if (!item?.id) return
|
|
155
|
+
const idx = state.hitl.findIndex(i => i.kind === item.kind && i.id === item.id)
|
|
156
|
+
if (idx >= 0) state.hitl[idx] = item
|
|
157
|
+
else state.hitl.push(item)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ===== 终端镜像帧 → monitor 行(纯部分;ws 接线在 aw-tui) =====
|
|
161
|
+
|
|
162
|
+
/** term 帧 → monitor 行描述;返回 { lines:[{text,tone}], streamAppend?, streamEnd?, statePatch? } */
|
|
163
|
+
export function reduceTermFrame(frame) {
|
|
164
|
+
const out = { lines: [], streamAppend: '', streamEnd: null, statePatch: null }
|
|
165
|
+
const type = frame?.type
|
|
166
|
+
switch (type) {
|
|
167
|
+
case 'agent_start':
|
|
168
|
+
out.lines.push({ text: '── 回合开始 ──', tone: 'dim' })
|
|
169
|
+
break
|
|
170
|
+
case 'agent_end':
|
|
171
|
+
out.lines.push({ text: '── 回合结束 ──', tone: 'dim' })
|
|
172
|
+
break
|
|
173
|
+
case 'message_end': {
|
|
174
|
+
const text = String(frame.text ?? '').trim()
|
|
175
|
+
if (text) out.lines.push({ text, tone: 'monitor' })
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
case 'tool_execution_start':
|
|
179
|
+
out.lines.push({ text: `🔧 ${frame.toolName ?? ''}${brief(frame.args, 80)}`, tone: 'info' })
|
|
180
|
+
break
|
|
181
|
+
case 'tool_execution_end':
|
|
182
|
+
out.lines.push({ text: `${frame.isError ? '✗' : '✔'} ${frame.toolName ?? ''}${brief(frame.result, 120)}`, tone: frame.isError ? 'error' : 'dim' })
|
|
183
|
+
break
|
|
184
|
+
case 'host_tool_call':
|
|
185
|
+
out.lines.push({ text: `🛠 host ${frame.toolName ?? ''}${brief(frame.args, 80)}`, tone: 'info' })
|
|
186
|
+
break
|
|
187
|
+
case 'extension_ui_request':
|
|
188
|
+
if (frame.method && frame.method !== 'cancel') {
|
|
189
|
+
out.lines.push({ text: `⏸ HITL ${frame.method}「${String(frame.title ?? '').slice(0, 60)}」(/hitl 作答)`, tone: 'warn' })
|
|
190
|
+
}
|
|
191
|
+
break
|
|
192
|
+
case '__human_input':
|
|
193
|
+
out.lines.push({ text: `⌨ ${String(frame.text ?? '').slice(0, 120)}`, tone: 'info' })
|
|
194
|
+
break
|
|
195
|
+
case '__terminal_notice':
|
|
196
|
+
out.lines.push({ text: String(frame.message ?? ''), tone: frame.level === 'error' ? 'error' : frame.level === 'warning' ? 'warn' : 'dim' })
|
|
197
|
+
break
|
|
198
|
+
default:
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function brief(value, max) {
|
|
205
|
+
if (value == null) return ''
|
|
206
|
+
let s = typeof value === 'string'
|
|
207
|
+
? value
|
|
208
|
+
: (() => {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.stringify(value) ?? ''
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return String(value)
|
|
214
|
+
}
|
|
215
|
+
})()
|
|
216
|
+
s = s.replace(/\s+/g, ' ').trim()
|
|
217
|
+
return s ? ` ${s.slice(0, max)}${s.length > max ? '…' : ''}` : ''
|
|
218
|
+
}
|