agentworkshop 0.7.4 → 0.7.6
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/cli/commands/stop.mjs +108 -0
- package/cli/infra.mjs +108 -109
- package/config.yml +1 -1
- package/package.json +1 -1
- package/scripts/e2e-aw-tui-global.mjs +277 -0
- package/scripts/start.mjs +31 -8
- package/scripts/test-tui-commands.mjs +17 -0
- package/scripts/tui-smoke.mjs +13 -4
- package/shared/config/single-instance.mjs +129 -86
- package/tui/README.md +11 -2
- package/tui/aw-tui.mjs +64 -14
- package/tui/commands/index.mjs +43 -6
- package/tui/components/status-bar.mjs +4 -0
- package/tui/lib/config.mjs +62 -58
- package/tui/lib/pickers.mjs +102 -0
- package/tui/lib/state.mjs +6 -0
package/tui/lib/config.mjs
CHANGED
|
@@ -1,58 +1,62 @@
|
|
|
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
|
-
/** 配置根(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
/** 配置根(与服务端同策略):仅当用户自建了 <检出根>/.AgentWorkShop 时优先使用;
|
|
23
|
+
* 否则一律回落 ~/.AgentWorkShop(绝不在 pwd 下自动创建)。 */
|
|
24
|
+
export function configRoot(cwd = process.cwd()) {
|
|
25
|
+
const marker = findUp(cwd, 'config.yml')
|
|
26
|
+
if (marker) {
|
|
27
|
+
const candidate = join(dirname(marker), '.AgentWorkShop')
|
|
28
|
+
if (existsSync(candidate)) return candidate
|
|
29
|
+
}
|
|
30
|
+
return join(homedir(), '.AgentWorkShop')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const authPath = cwd => join(configRoot(cwd), 'tui-auth.json')
|
|
34
|
+
|
|
35
|
+
/** 读取已存凭据({ baseUrl, token, email? } 或 null) */
|
|
36
|
+
export function loadAuth(cwd = process.cwd()) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(readFileSync(authPath(cwd), 'utf8'))
|
|
39
|
+
if (raw && typeof raw.token === 'string' && typeof raw.baseUrl === 'string') return raw
|
|
40
|
+
}
|
|
41
|
+
catch { /* 无凭据/损坏 → 重新登录 */ }
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 持久化凭据(0600;路径加入 .gitignore) */
|
|
46
|
+
export function saveAuth(auth, cwd = process.cwd()) {
|
|
47
|
+
const file = authPath(cwd)
|
|
48
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
49
|
+
writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, 'utf8')
|
|
50
|
+
try {
|
|
51
|
+
chmodSync(file, 0o600)
|
|
52
|
+
}
|
|
53
|
+
catch { /* Windows FAT 卷 chmod 无效,忽略 */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 清除凭据(登出) */
|
|
57
|
+
export function clearAuth(cwd = process.cwd()) {
|
|
58
|
+
try {
|
|
59
|
+
existsSync(authPath(cwd)) && (writeFileSync(authPath(cwd), '{}\n', 'utf8'), true)
|
|
60
|
+
}
|
|
61
|
+
catch { /* 尽力而为 */ }
|
|
62
|
+
}
|
|
@@ -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
|
+
}
|
package/tui/lib/state.mjs
CHANGED
|
@@ -28,6 +28,12 @@ export function createState() {
|
|
|
28
28
|
/** 正在作答的条目(index → /hitl <n>;null = 常规输入) */
|
|
29
29
|
hitlAnswering: null,
|
|
30
30
|
|
|
31
|
+
/** 对话目标:null = 频道(普通文本=发布任务);{agentId,name} = 成员(普通文本=通信消息) */
|
|
32
|
+
target: null,
|
|
33
|
+
/** 浮层开关(按键路由用:Tab 呼出目标选择;启动/`/channel` 呼出频道选择) */
|
|
34
|
+
channelPickerOpen: false,
|
|
35
|
+
targetPickerOpen: false,
|
|
36
|
+
|
|
31
37
|
monitor: {
|
|
32
38
|
agentId: null,
|
|
33
39
|
name: null,
|