thincoder 0.7.1 → 0.7.2
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/README.md +11 -0
- package/bin/thincoder.mjs +3 -0
- package/package.json +1 -1
- package/src/agent.mjs +12 -15
- package/src/provider.mjs +145 -10
- package/src/repomap.mjs +100 -10
- package/src/session.mjs +16 -5
- package/src/tui.mjs +463 -412
package/src/tui.mjs
CHANGED
|
@@ -513,7 +513,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
513
513
|
if (overlay.selectedLine >= overlay.scroll + winH) overlay.scroll = overlay.selectedLine - winH + 1
|
|
514
514
|
const start = Math.max(0, Math.min(overlay.scroll, Math.max(0, overlay.lines.length - winH)))
|
|
515
515
|
const shown = overlay.lines.slice(start, start + winH)
|
|
516
|
-
const overlayTitle = state.picker ?
|
|
516
|
+
const overlayTitle = state.picker ? ` ❯ ${state.picker.title} ` : " ❯ 初始配置 "
|
|
517
517
|
out.push(`${ansi.bold}${C.tool}${overlayTitle}${ansi.reset}${ansi.dim}${state.picker ? "(↑↓ 移动, Enter 确认, Esc 取消)" : ""}${ansi.reset}${ansi.clearLine}`)
|
|
518
518
|
for (const l of shown) {
|
|
519
519
|
out.push(`${l.color}${sliceByWidth(l.text, cols - 1)}${ansi.reset}${ansi.clearLine}`)
|
|
@@ -559,7 +559,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
559
559
|
title = ` Allow ${state.permission.name}? (y/n/a) `
|
|
560
560
|
}
|
|
561
561
|
} else if (state.picker) {
|
|
562
|
-
title = "
|
|
562
|
+
title = " Select "
|
|
563
563
|
} else if (state.wizard) {
|
|
564
564
|
title = " Setup "
|
|
565
565
|
} else if (state.processing) {
|
|
@@ -600,13 +600,21 @@ export async function startTUI(agent, opts = {}) {
|
|
|
600
600
|
const cmds = SLASH_COMMANDS.filter((c) => c.name.startsWith(cmd))
|
|
601
601
|
const match = cmds.length === 1 ? cmds[0] : null
|
|
602
602
|
if (match?.name === "/config" && cmd === "/config") {
|
|
603
|
-
statusLine = " /config
|
|
603
|
+
statusLine = " /config 打开配置菜单"
|
|
604
604
|
} else if (match?.name === "/provider" && cmd === "/provider") {
|
|
605
|
-
statusLine = " /provider
|
|
605
|
+
statusLine = " /provider 打开 Provider 管理菜单"
|
|
606
606
|
} else if (match?.name === "/model" && cmd === "/model" && !sub) {
|
|
607
|
-
statusLine = " /model
|
|
607
|
+
statusLine = " /model 打开模型选择器"
|
|
608
608
|
} else if (match?.name === "/think" && cmd === "/think") {
|
|
609
|
-
statusLine = " /think
|
|
609
|
+
statusLine = " /think 打开思维模式菜单"
|
|
610
|
+
} else if (match?.name === "/mcp" && cmd === "/mcp") {
|
|
611
|
+
statusLine = " /mcp 打开 MCP 管理菜单"
|
|
612
|
+
} else if (match?.name === "/goal" && cmd === "/goal") {
|
|
613
|
+
statusLine = " /goal 打开目标管理菜单"
|
|
614
|
+
} else if (match?.name === "/session" && cmd === "/session") {
|
|
615
|
+
statusLine = " /session 选择归档会话"
|
|
616
|
+
} else if (match?.name === "/rewind" && cmd === "/rewind") {
|
|
617
|
+
statusLine = " /rewind 选择存档点回滚"
|
|
610
618
|
} else if (cmds.length > 0) {
|
|
611
619
|
if (cmds.length <= 4) {
|
|
612
620
|
statusLine = ` ${cmds.map((c) => `${c.name} ${c.desc}`).join(" │ ")}`
|
|
@@ -784,6 +792,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
784
792
|
state.tokens.cacheHit += usage.prompt_cache_hit_tokens ?? 0
|
|
785
793
|
state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
|
|
786
794
|
},
|
|
795
|
+
// 节流等待(主动闸门 / 429 退避):状态栏明示,防用户以为卡死
|
|
796
|
+
onWait: ({ phase, seconds }) => {
|
|
797
|
+
state.status = phase === "gate" ? `TPM 节流等待 ~${seconds}s` : `限流 429,${seconds}s 后重试`
|
|
798
|
+
render()
|
|
799
|
+
},
|
|
787
800
|
onTaskUpdate: (items) => {
|
|
788
801
|
state.tasks = items
|
|
789
802
|
const done = items.filter((i) => i.status === "done").length
|
|
@@ -986,36 +999,35 @@ export async function startTUI(agent, opts = {}) {
|
|
|
986
999
|
setTimeout(() => process.exit(0), 100) // 延迟一拍:fetch 后立刻 exit 在 Windows/Node 24 会触发 libuv 断言
|
|
987
1000
|
return
|
|
988
1001
|
case "/session": {
|
|
989
|
-
const
|
|
990
|
-
if (
|
|
991
|
-
|
|
992
|
-
const data = switchToSlot(agent.cwd, slotNum)
|
|
993
|
-
if (!data) {
|
|
994
|
-
pushLine(`槽位 ${slotNum} 不存在`, C.dim)
|
|
995
|
-
} else {
|
|
996
|
-
applySession(agent, data)
|
|
997
|
-
state.lines = data.display.length
|
|
998
|
-
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
999
|
-
: []
|
|
1000
|
-
state.tasks = agent.tasks ?? []
|
|
1001
|
-
// 切换过来的会话如果任务全完成,自动收起面板
|
|
1002
|
-
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
1003
|
-
state.tasks = []
|
|
1004
|
-
}
|
|
1005
|
-
pushLabel(`── 已切换到槽位 ${slotNum}(${data.history.length} 条消息)──`, C.warn)
|
|
1006
|
-
render()
|
|
1007
|
-
}
|
|
1002
|
+
const slots = listSlots(agent.cwd)
|
|
1003
|
+
if (slots.length === 0) {
|
|
1004
|
+
pushLine("没有归档会话(用 /new 后旧会话会自动归档)", C.dim)
|
|
1008
1005
|
} else {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1006
|
+
const entries = [
|
|
1007
|
+
{ type: "header", text: "归档会话(↑↓ 选择, Enter 切换, Esc 取消)" },
|
|
1008
|
+
...slots.map((s) => ({ type: "item", text: `槽位 ${s.slot} — ${s.date}`, slot: s.slot })),
|
|
1009
|
+
]
|
|
1010
|
+
openPicker({
|
|
1011
|
+
title: "切换会话",
|
|
1012
|
+
entries,
|
|
1013
|
+
onSelect: (e) => {
|
|
1014
|
+
const data = switchToSlot(agent.cwd, e.slot)
|
|
1015
|
+
if (!data) {
|
|
1016
|
+
pushLine(`槽位 ${e.slot} 不存在`, C.dim)
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
applySession(agent, data)
|
|
1020
|
+
state.lines = data.display.length
|
|
1021
|
+
? data.display.map((l) => ({ text: l.text, color: l.color }))
|
|
1022
|
+
: []
|
|
1023
|
+
state.tasks = agent.tasks ?? []
|
|
1024
|
+
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
1025
|
+
state.tasks = []
|
|
1026
|
+
}
|
|
1027
|
+
pushLabel(`── 已切换到槽位 ${e.slot}(${data.history.length} 条消息)──`, C.warn)
|
|
1028
|
+
render()
|
|
1029
|
+
},
|
|
1030
|
+
})
|
|
1019
1031
|
}
|
|
1020
1032
|
return
|
|
1021
1033
|
}
|
|
@@ -1136,27 +1148,33 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1136
1148
|
pushLine("[rewind] 当前目录不是 git 仓库,无法使用存档点", C.error)
|
|
1137
1149
|
return
|
|
1138
1150
|
}
|
|
1139
|
-
const
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
pushLabel(`❯ Checkpoints`, ansi.bold + C.tool)
|
|
1143
|
-
if (cps.length === 0) {
|
|
1144
|
-
pushLine("(暂无存档点——每次提交任务前自动创建)", C.dim)
|
|
1145
|
-
}
|
|
1146
|
-
for (const cp of cps.slice(0, 10)) {
|
|
1147
|
-
pushLine(` ${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} 个未跟踪文件)`, C.dim)
|
|
1148
|
-
}
|
|
1149
|
-
pushLine("回滚: /rewind <id>(恢复前会先存当前状态,回滚可逆)", C.dim)
|
|
1151
|
+
const cps = await listCheckpoints(agent.cwd)
|
|
1152
|
+
if (cps.length === 0) {
|
|
1153
|
+
pushLine("(暂无存档点——每次提交任务前自动创建)", C.dim)
|
|
1150
1154
|
return
|
|
1151
1155
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1156
|
+
const entries = [
|
|
1157
|
+
{ type: "header", text: "存档点(↑↓ 选择, Enter 回滚, Esc 取消)" },
|
|
1158
|
+
...cps.slice(0, 12).map((cp) => ({
|
|
1159
|
+
type: "item",
|
|
1160
|
+
text: `${cp.id} ${new Date(cp.time).toLocaleString()} (+${cp.untracked} 个未跟踪文件)`,
|
|
1161
|
+
id: cp.id,
|
|
1162
|
+
})),
|
|
1163
|
+
]
|
|
1164
|
+
openPicker({
|
|
1165
|
+
title: "回滚存档点",
|
|
1166
|
+
entries,
|
|
1167
|
+
onSelect: async (e) => {
|
|
1168
|
+
try {
|
|
1169
|
+
const summary = await rewind(agent.cwd, e.id)
|
|
1170
|
+
pushLabel(`❯ Rewind`, ansi.bold + C.warn)
|
|
1171
|
+
pushLine(`已回滚到 ${e.id}:补丁${summary.patchApplied ? "已应用" : "无"},删除新建文件 ${summary.deleted} 个,还原文件 ${summary.restored} 个`, C.tool)
|
|
1172
|
+
pushLine("(当前状态已先存为新存档点,可再次 /rewind 回到刚才)", C.dim)
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
pushLine(`[rewind] ${error.message}`, C.error)
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
})
|
|
1160
1178
|
return
|
|
1161
1179
|
}
|
|
1162
1180
|
case "/plan": {
|
|
@@ -1177,37 +1195,46 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1177
1195
|
return
|
|
1178
1196
|
}
|
|
1179
1197
|
case "/goal": {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
1185
|
-
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
1186
|
-
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
1187
|
-
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
1188
|
-
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1189
|
-
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
1190
|
-
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1191
|
-
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
1192
|
-
return
|
|
1193
|
-
}
|
|
1194
|
-
if (sub === "cancel") {
|
|
1195
|
-
agent.goal = null
|
|
1196
|
-
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
1197
|
-
pushLine(`目标已取消。`, C.dim)
|
|
1198
|
-
return
|
|
1199
|
-
}
|
|
1198
|
+
const entries = [
|
|
1199
|
+
{ type: "header", text: agent.goal ? `当前目标: ${agent.goal.objective.slice(0, 60)}` : "操作" },
|
|
1200
|
+
{ type: "item", text: "设置新目标", action: "set" },
|
|
1201
|
+
]
|
|
1200
1202
|
if (agent.goal) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1203
|
+
entries.push({ type: "item", text: "取消目标", action: "cancel" })
|
|
1204
|
+
entries.push({ type: "item", text: "查看详情", action: "view" })
|
|
1205
|
+
}
|
|
1206
|
+
openPicker({
|
|
1207
|
+
title: "目标管理",
|
|
1208
|
+
entries,
|
|
1209
|
+
onSelect: (e) => {
|
|
1210
|
+
if (e.action === "view") {
|
|
1211
|
+
const statusText = { active: "进行中", complete: "已完成", blocked: "已阻塞" }[agent.goal.status] ?? agent.goal.status
|
|
1212
|
+
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1213
|
+
pushLine(`目标: ${agent.goal.objective}`, C.tool)
|
|
1214
|
+
if (agent.goal.criteria) pushLine(` 完成条件: ${agent.goal.criteria}`, C.dim)
|
|
1215
|
+
pushLine(` 状态: ${statusText} │ 已用轮数: ${agent.goal.turnsUsed ?? 0} │ 设置于: ${new Date(agent.goal.setAt).toLocaleString()}`, C.dim)
|
|
1216
|
+
return
|
|
1217
|
+
}
|
|
1218
|
+
if (e.action === "cancel") {
|
|
1219
|
+
agent.goal = null
|
|
1220
|
+
pushLabel(`❯ Goal`, ansi.bold + C.dim)
|
|
1221
|
+
pushLine(`目标已取消。`, C.dim)
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
// set — 需要输入目标文本
|
|
1225
|
+
askQuestion("请输入目标描述(; 分隔完成条件)").then((text) => {
|
|
1226
|
+
if (!text) return
|
|
1227
|
+
const semi = text.indexOf(";") >= 0 ? ";" : text.indexOf(";") >= 0 ? ";" : null
|
|
1228
|
+
const objective = semi ? text.slice(0, semi).trim() : text.trim()
|
|
1229
|
+
const criteria = semi ? text.slice(semi + 1).trim() : ""
|
|
1230
|
+
agent.goal = { objective, criteria, setAt: Date.now(), status: "active", turnsUsed: 0, _blockTally: null }
|
|
1231
|
+
pushLabel(`❯ Goal`, ansi.bold + C.warn)
|
|
1232
|
+
pushLine(`目标已设置: ${objective}`, C.tool)
|
|
1233
|
+
if (criteria) pushLine(` 完成条件: ${criteria}`, C.dim)
|
|
1234
|
+
else pushLine(` ⚠ 未完成条件——agent 用 goal set 设立时会被要求补上可验证的完成条件`, C.warn)
|
|
1235
|
+
})
|
|
1236
|
+
},
|
|
1237
|
+
})
|
|
1211
1238
|
return
|
|
1212
1239
|
}
|
|
1213
1240
|
case "/skills": {
|
|
@@ -1224,93 +1251,107 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1224
1251
|
return
|
|
1225
1252
|
}
|
|
1226
1253
|
case "/mcp": {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const color = connected ? C.tool : C.dim
|
|
1239
|
-
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
1240
|
-
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1241
|
-
pushLine(` ${mark} ${srv.name}: ${desc} (${toolCount} tools)`, color)
|
|
1242
|
-
}
|
|
1243
|
-
pushLabel(`❯ 操作`, ansi.bold + C.tool)
|
|
1244
|
-
pushLine("/mcp add <name> <url|command> [args|headers...]", C.dim)
|
|
1245
|
-
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1246
|
-
pushLine(" 例: /mcp add myapi https://api.example.com/mcp Authorization=\"Bearer x\"", C.dim)
|
|
1247
|
-
pushLine(" 例: /mcp add github npx -y @modelcontextprotocol/server-github", C.dim)
|
|
1248
|
-
pushLine(`/mcp remove <name> 断开并移除`, C.dim)
|
|
1249
|
-
pushLine(`/mcp connect <name> 重连已配置的 server`, C.dim)
|
|
1250
|
-
return
|
|
1251
|
-
}
|
|
1252
|
-
// ---- /mcp add <name> <url|command> [args|headers...] (统一入口,自动识别传输类型) ----
|
|
1253
|
-
// url / ws 子命令作为别名保留(兼容旧配置)
|
|
1254
|
-
if (sub === "add" || sub === "url" || sub === "ws") {
|
|
1255
|
-
const args = rest.slice(1)
|
|
1256
|
-
if (args.length < 2) {
|
|
1257
|
-
pushLine("用法: /mcp add <name> <url|command> [args|headers...]", C.error)
|
|
1258
|
-
pushLine(" URL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令", C.dim)
|
|
1259
|
-
return
|
|
1260
|
-
}
|
|
1261
|
-
const name = args[0]
|
|
1262
|
-
const second = args[1]
|
|
1263
|
-
const extras = args.slice(2)
|
|
1264
|
-
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1265
|
-
if (existing) { pushLine(`[mcp] "${name}" 已存在,用 /mcp remove ${name} 先移除`, C.error); return }
|
|
1266
|
-
|
|
1267
|
-
const isWS = /^wss?:\/\//.test(second)
|
|
1268
|
-
const isHTTP = /^https?:\/\//.test(second)
|
|
1269
|
-
let srv
|
|
1270
|
-
if (isWS || sub === "ws") {
|
|
1271
|
-
const headers = parseHeaders(extras)
|
|
1272
|
-
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1273
|
-
} else if (isHTTP || sub === "url") {
|
|
1274
|
-
const headers = parseHeaders(extras)
|
|
1275
|
-
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1276
|
-
} else {
|
|
1277
|
-
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
1278
|
-
}
|
|
1279
|
-
await addAndConnect(srv)
|
|
1280
|
-
return
|
|
1281
|
-
}
|
|
1282
|
-
// ---- /mcp remove <name> ----
|
|
1283
|
-
if (sub === "remove") {
|
|
1284
|
-
const name = rest[1]
|
|
1285
|
-
if (!name) { pushLine("用法: /mcp remove <name>", C.error); return }
|
|
1286
|
-
const { removeMcpTools } = await import("./mcp.mjs")
|
|
1287
|
-
removeMcpTools(agent, name)
|
|
1288
|
-
await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = raw.mcp.servers.filter((s) => s.name !== name) })
|
|
1289
|
-
if (agent.config?.mcp?.servers) agent.config.mcp.servers = agent.config.mcp.servers.filter((s) => s.name !== name)
|
|
1290
|
-
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1291
|
-
pushLine(`${name} 已断开并从配置移除。`, C.tool)
|
|
1292
|
-
return
|
|
1293
|
-
}
|
|
1294
|
-
// ---- /mcp connect <name> — 重连 ----
|
|
1295
|
-
if (sub === "connect") {
|
|
1296
|
-
const name = rest[1]
|
|
1297
|
-
if (!name) { pushLine("用法: /mcp connect <name>", C.error); return }
|
|
1298
|
-
const srv = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1299
|
-
if (!srv) { pushLine(`[mcp] "${name}" 未在配置中找到(先用 /mcp add 添加)`, C.error); return }
|
|
1300
|
-
const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
|
|
1301
|
-
removeMcpTools(agent, name)
|
|
1302
|
-
try {
|
|
1303
|
-
pushLine(`[mcp] 重连 ${name}...`, C.dim)
|
|
1304
|
-
const tools = await connectMcpServer(srv)
|
|
1305
|
-
agent.tools.push(...tools)
|
|
1306
|
-
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1307
|
-
pushLine(`${name} 已重连,${tools.length} 个工具可用。`, C.tool)
|
|
1308
|
-
} catch (error) {
|
|
1309
|
-
pushLine(`[mcp] ${name}: ${error.message}`, C.error)
|
|
1310
|
-
}
|
|
1311
|
-
return
|
|
1254
|
+
const servers = agent.config?.mcp?.servers ?? []
|
|
1255
|
+
const entries = [
|
|
1256
|
+
{ type: "header", text: `已配置 ${servers.length} 个 MCP server` },
|
|
1257
|
+
{ type: "item", text: "查看列表", action: "list" },
|
|
1258
|
+
{ type: "item", text: "添加服务器", action: "add" },
|
|
1259
|
+
]
|
|
1260
|
+
if (servers.length > 0) {
|
|
1261
|
+
entries.push(
|
|
1262
|
+
{ type: "item", text: "移除服务器", action: "remove" },
|
|
1263
|
+
{ type: "item", text: "重连服务器", action: "connect" },
|
|
1264
|
+
)
|
|
1312
1265
|
}
|
|
1313
|
-
|
|
1266
|
+
openPicker({
|
|
1267
|
+
title: "MCP 管理",
|
|
1268
|
+
entries,
|
|
1269
|
+
onSelect: async (e) => {
|
|
1270
|
+
if (e.action === "list") {
|
|
1271
|
+
pushLabel(`❯ MCP Servers`, ansi.bold + C.tool)
|
|
1272
|
+
if (servers.length === 0) {
|
|
1273
|
+
pushLine("(无 MCP server)", C.dim)
|
|
1274
|
+
}
|
|
1275
|
+
for (const srv of servers) {
|
|
1276
|
+
const connected = agent.tools.some((t) => t._mcpName === srv.name)
|
|
1277
|
+
const mark = connected ? "●" : "○"
|
|
1278
|
+
const color = connected ? C.tool : C.dim
|
|
1279
|
+
const toolCount = agent.tools.filter((t) => t._mcpName === srv.name).length
|
|
1280
|
+
const desc = srv.wsUrl ? srv.wsUrl : srv.url ? srv.url : `${srv.command} ${(srv.args ?? []).join(" ")}`
|
|
1281
|
+
pushLine(` ${mark} ${srv.name}: ${desc} (${toolCount} tools)`, color)
|
|
1282
|
+
}
|
|
1283
|
+
return
|
|
1284
|
+
}
|
|
1285
|
+
if (e.action === "remove") {
|
|
1286
|
+
const removeEntries = [
|
|
1287
|
+
{ type: "header", text: "选择要移除的服务器" },
|
|
1288
|
+
...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
|
|
1289
|
+
]
|
|
1290
|
+
openPicker({
|
|
1291
|
+
title: "移除 MCP",
|
|
1292
|
+
entries: removeEntries,
|
|
1293
|
+
onSelect: async (se) => {
|
|
1294
|
+
const { removeMcpTools } = await import("./mcp.mjs")
|
|
1295
|
+
removeMcpTools(agent, se.name)
|
|
1296
|
+
await persistRaw((raw) => { raw.mcp ??= { servers: [] }; raw.mcp.servers = raw.mcp.servers.filter((s) => s.name !== se.name) })
|
|
1297
|
+
if (agent.config?.mcp?.servers) agent.config.mcp.servers = agent.config.mcp.servers.filter((s) => s.name !== se.name)
|
|
1298
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1299
|
+
pushLine(`${se.name} 已断开并从配置移除。`, C.tool)
|
|
1300
|
+
},
|
|
1301
|
+
})
|
|
1302
|
+
return
|
|
1303
|
+
}
|
|
1304
|
+
if (e.action === "connect") {
|
|
1305
|
+
const connEntries = [
|
|
1306
|
+
{ type: "header", text: "选择要重连的服务器" },
|
|
1307
|
+
...servers.map((s) => ({ type: "item", text: s.name, name: s.name })),
|
|
1308
|
+
]
|
|
1309
|
+
openPicker({
|
|
1310
|
+
title: "重连 MCP",
|
|
1311
|
+
entries: connEntries,
|
|
1312
|
+
onSelect: async (se) => {
|
|
1313
|
+
const srv = servers.find((s) => s.name === se.name)
|
|
1314
|
+
if (!srv) return
|
|
1315
|
+
const { removeMcpTools, connectMcpServer } = await import("./mcp.mjs")
|
|
1316
|
+
removeMcpTools(agent, se.name)
|
|
1317
|
+
try {
|
|
1318
|
+
pushLine(`[mcp] 重连 ${se.name}...`, C.dim)
|
|
1319
|
+
const tools = await connectMcpServer(srv)
|
|
1320
|
+
agent.tools.push(...tools)
|
|
1321
|
+
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
1322
|
+
pushLine(`${se.name} 已重连,${tools.length} 个工具可用。`, C.tool)
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
pushLine(`[mcp] ${se.name}: ${error.message}`, C.error)
|
|
1325
|
+
}
|
|
1326
|
+
},
|
|
1327
|
+
})
|
|
1328
|
+
return
|
|
1329
|
+
}
|
|
1330
|
+
if (e.action === "add") {
|
|
1331
|
+
askQuestion("输入: <名称> <URL|命令> [参数...]\nURL 自动识别: https://… → HTTP, ws://… → WebSocket, 其他 → stdio 命令").then(async (text) => {
|
|
1332
|
+
if (!text) return
|
|
1333
|
+
const parts = text.split(/\s+/)
|
|
1334
|
+
if (parts.length < 2) { pushLine("用法: <名称> <URL|命令> [参数...]", C.error); return }
|
|
1335
|
+
const [name, second, ...extras] = parts
|
|
1336
|
+
const existing = (agent.config?.mcp?.servers ?? []).find((s) => s.name === name)
|
|
1337
|
+
if (existing) { pushLine(`[mcp] "${name}" 已存在`, C.error); return }
|
|
1338
|
+
const isWS = /^wss?:\/\//.test(second)
|
|
1339
|
+
const isHTTP = /^https?:\/\//.test(second)
|
|
1340
|
+
let srv
|
|
1341
|
+
if (isWS) {
|
|
1342
|
+
const headers = parseHeaders(extras)
|
|
1343
|
+
srv = { name, wsUrl: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1344
|
+
} else if (isHTTP) {
|
|
1345
|
+
const headers = parseHeaders(extras)
|
|
1346
|
+
srv = { name, url: second, headers: Object.keys(headers).length > 0 ? headers : undefined }
|
|
1347
|
+
} else {
|
|
1348
|
+
srv = { name, command: second, args: extras.length > 0 ? extras : undefined }
|
|
1349
|
+
}
|
|
1350
|
+
await addAndConnect(srv)
|
|
1351
|
+
})
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
})
|
|
1314
1355
|
return
|
|
1315
1356
|
}
|
|
1316
1357
|
|
|
@@ -1367,235 +1408,235 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1367
1408
|
)
|
|
1368
1409
|
return
|
|
1369
1410
|
case "/think": {
|
|
1370
|
-
const sub = rest[0]
|
|
1371
1411
|
const cur = agent.provider
|
|
1372
1412
|
const thinkingEnabled = cur.thinking?.type === "enabled" || cur.thinking?.type === undefined
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
else
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1413
|
+
const { specForModel } = await import("./config.mjs")
|
|
1414
|
+
const spec = specForModel(cur.model)
|
|
1415
|
+
const isEffortOnly = spec.thinkApi === "effort"
|
|
1416
|
+
|
|
1417
|
+
const entries = [
|
|
1418
|
+
{ type: "header", text: "思维模式" },
|
|
1419
|
+
{ type: "item", text: `开启${thinkingEnabled ? " ← 当前" : ""}`, action: "on" },
|
|
1420
|
+
{ type: "item", text: `关闭${!thinkingEnabled ? " ← 当前" : ""}`, action: "off" },
|
|
1421
|
+
{ type: "header", text: "推理强度" },
|
|
1422
|
+
...["low", "high", "max"].map((l) => ({
|
|
1423
|
+
type: "item",
|
|
1424
|
+
text: `${l}${cur.reasoningEffort === l ? " ← 当前" : ""}`,
|
|
1425
|
+
action: "effort",
|
|
1426
|
+
level: l,
|
|
1427
|
+
})),
|
|
1428
|
+
]
|
|
1429
|
+
openPicker({
|
|
1430
|
+
title: "思维模式",
|
|
1431
|
+
entries,
|
|
1432
|
+
defaultIndex: thinkingEnabled ? 0 : 1,
|
|
1433
|
+
onSelect: async (e) => {
|
|
1434
|
+
if (e.action === "effort") {
|
|
1435
|
+
cur.reasoningEffort = e.level
|
|
1436
|
+
await syncProviderField("reasoningEffort", e.level)
|
|
1437
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1438
|
+
pushLine(`推理强度已设为 ${e.level}`, C.tool)
|
|
1439
|
+
} else {
|
|
1440
|
+
const enable = e.action === "on"
|
|
1441
|
+
if (isEffortOnly) {
|
|
1442
|
+
if (!enable) delete cur.reasoningEffort
|
|
1443
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1444
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1445
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1446
|
+
} else {
|
|
1447
|
+
cur.thinking = enable ? { type: "enabled" } : { type: "disabled" }
|
|
1448
|
+
if (!enable) delete cur.reasoningEffort
|
|
1449
|
+
else if (!cur.reasoningEffort) cur.reasoningEffort = "high"
|
|
1450
|
+
await syncProviderField("thinking", cur.thinking)
|
|
1451
|
+
if (!enable) await syncProviderField("reasoningEffort", undefined)
|
|
1452
|
+
else await syncProviderField("reasoningEffort", cur.reasoningEffort)
|
|
1453
|
+
}
|
|
1454
|
+
pushLabel(`❯ Think`, ansi.bold + C.tool)
|
|
1455
|
+
pushLine(`思维模式已${enable ? "开启" : "关闭"}`, C.tool)
|
|
1456
|
+
if (enable) pushLine(`推理强度: ${cur.reasoningEffort}`, C.dim)
|
|
1457
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
})
|
|
1420
1460
|
return
|
|
1421
1461
|
}
|
|
1422
1462
|
case "/model": {
|
|
1423
|
-
|
|
1424
|
-
if (!arg) {
|
|
1425
|
-
// 打开交互选择器:全部 provider 的全部模型,方向键选择
|
|
1426
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1427
|
-
pushLine(`/model <名称> 直接切换 provider 或模型(如 /model deepseek-v4-pro)`, C.dim)
|
|
1428
|
-
pushLine(`/provider 管理 provider(添加/删除/配 key)`, C.dim)
|
|
1429
|
-
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
1430
|
-
return
|
|
1431
|
-
}
|
|
1432
|
-
if (arg === "add" || arg === "--add") {
|
|
1433
|
-
pushLine(`添加 provider 已移到 /provider add(/provider 查看全部管理命令)`, C.warn)
|
|
1434
|
-
return
|
|
1435
|
-
}
|
|
1436
|
-
// 一个参数两种含义:先按 provider 名匹配,匹配不到就当模型名改当前 provider
|
|
1437
|
-
const { resolveCompactThreshold } = await import("./config.mjs")
|
|
1438
|
-
const p = agent.providers.find((pp) => pp.name === arg)
|
|
1439
|
-
const newModel = p ? p.model : arg
|
|
1440
|
-
let thresholdNote = ""
|
|
1441
|
-
if (agent.config?.agent?.compactThresholdAuto) {
|
|
1442
|
-
const { value } = resolveCompactThreshold(null, newModel)
|
|
1443
|
-
agent.config.agent.compactThreshold = value
|
|
1444
|
-
thresholdNote = `,压缩阈值随模型调整为 ${value}`
|
|
1445
|
-
}
|
|
1446
|
-
if (p) {
|
|
1447
|
-
agent.activeProvider = arg
|
|
1448
|
-
agent.provider = { ...p }
|
|
1449
|
-
// key 的环境变量兜底和 loadConfig 保持一致(提供商专用变量只对同名生效)
|
|
1450
|
-
if (!agent.provider.apiKey) {
|
|
1451
|
-
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[arg]
|
|
1452
|
-
if (envKey && process.env[envKey]) agent.provider.apiKey = process.env[envKey]
|
|
1453
|
-
}
|
|
1454
|
-
if (!agent.provider.apiKey) agent.provider.apiKey = process.env.THINCODER_API_KEY
|
|
1455
|
-
await persistRaw((raw) => { raw.activeProvider = arg })
|
|
1456
|
-
agent.config.activeProvider = arg
|
|
1457
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1458
|
-
pushLine(`已切换到 ${arg} / ${p.model}${thresholdNote}(已持久化)`, C.tool)
|
|
1459
|
-
if (!agent.provider.apiKey) pushLine(`该 provider 还没配 key: /provider key <apikey>`, C.warn)
|
|
1460
|
-
} else {
|
|
1461
|
-
const target = agent.providers.find((pp) => pp.name === agent.activeProvider) ?? agent.providers[0]
|
|
1462
|
-
if (target) target.model = arg
|
|
1463
|
-
agent.provider.model = arg
|
|
1464
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1465
|
-
pushLabel(`❯ Model`, ansi.bold + C.tool)
|
|
1466
|
-
pushLine(`已将 ${target?.name ?? agent.activeProvider} 的模型改为 ${arg}${thresholdNote}(已持久化)`, C.tool)
|
|
1467
|
-
}
|
|
1463
|
+
openModelPicker().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
1468
1464
|
return
|
|
1469
1465
|
}
|
|
1470
1466
|
case "/provider": {
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (agent.providers.some((p) => p.name === name)) {
|
|
1480
|
-
pushLine(`"${name}" 已存在;要重建可先 /provider remove ${name}`, C.warn)
|
|
1481
|
-
return
|
|
1482
|
-
}
|
|
1483
|
-
const preset = PRESETS[name]
|
|
1484
|
-
const baseURL = (rest[2] ?? preset?.baseURL)?.replace(/\/+$/, "")
|
|
1485
|
-
const model = rest[3] ?? preset?.model
|
|
1486
|
-
if (!baseURL || !model) {
|
|
1487
|
-
pushLine(`缺少参数: /provider add ${name} <baseURL> <模型>`, C.error)
|
|
1488
|
-
if (!preset) pushLine(`("${name}" 不是预设;预设: ${Object.keys(PRESETS).join(", ")})`, C.dim)
|
|
1489
|
-
return
|
|
1490
|
-
}
|
|
1491
|
-
if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL 应以 http(s):// 开头`, C.error); return }
|
|
1492
|
-
agent.providers.push({ name, baseURL, model, ...(preset?.desc ? { desc: preset.desc } : {}) })
|
|
1493
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1494
|
-
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1495
|
-
pushLine(`已添加 ${name}(${baseURL} / ${model})`, C.tool)
|
|
1496
|
-
pushLine(`下一步: /provider key ${name} <apikey> 配 key,/model ${name} 切换`, C.dim)
|
|
1497
|
-
return
|
|
1498
|
-
}
|
|
1499
|
-
// ---- /provider remove <名称> ----
|
|
1500
|
-
if (sub === "remove" || sub === "rm") {
|
|
1501
|
-
const name = rest[1]
|
|
1502
|
-
if (!name) { pushLine("用法: /provider remove <名称>", C.error); return }
|
|
1503
|
-
const at = agent.providers.findIndex((p) => p.name === name)
|
|
1504
|
-
if (at < 0) { pushLine(`未找到 provider "${name}"`, C.error); return }
|
|
1505
|
-
if (name === agent.activeProvider) { pushLine(`"${name}" 正在使用中,先 /model 切换到别的 provider 再删`, C.warn); return }
|
|
1506
|
-
agent.providers.splice(at, 1)
|
|
1507
|
-
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1508
|
-
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1509
|
-
pushLine(`已删除 ${name}`, C.tool)
|
|
1510
|
-
return
|
|
1511
|
-
}
|
|
1512
|
-
// ---- /provider key [名称] <apikey> ----
|
|
1513
|
-
if (sub === "key") {
|
|
1514
|
-
let name = agent.activeProvider
|
|
1515
|
-
let keyParts = rest.slice(1)
|
|
1516
|
-
if (rest[1] && agent.providers.some((p) => p.name === rest[1])) {
|
|
1517
|
-
name = rest[1]
|
|
1518
|
-
keyParts = rest.slice(2)
|
|
1519
|
-
}
|
|
1520
|
-
const key = keyParts.join(" ")
|
|
1521
|
-
if (!key) { pushLine("用法: /provider key [名称] <apikey>(不填名称配当前 provider)", C.error); return }
|
|
1522
|
-
await setProviderKey(name, key)
|
|
1523
|
-
return
|
|
1524
|
-
}
|
|
1525
|
-
if (sub) { pushLine(`未知: ${sub}(/provider add | remove | key)`, C.error); return }
|
|
1526
|
-
// ---- /provider(无参): 列表 ----
|
|
1527
|
-
pushLabel(`❯ Providers (${agent.providers.length})`, ansi.bold + C.tool)
|
|
1528
|
-
for (const p of agent.providers) {
|
|
1529
|
-
const active = p.name === agent.activeProvider
|
|
1530
|
-
pushLine(
|
|
1531
|
-
`${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○无key"}${active ? " ← 当前" : ""}`,
|
|
1532
|
-
active ? C.tool : C.dim,
|
|
1467
|
+
const entries = [
|
|
1468
|
+
{ type: "header", text: `已配置 ${agent.providers.length} 个 provider` },
|
|
1469
|
+
{ type: "item", text: "查看列表", action: "list" },
|
|
1470
|
+
{ type: "item", text: "添加 provider", action: "add" },
|
|
1471
|
+
]
|
|
1472
|
+
if (agent.providers.length > 0) {
|
|
1473
|
+
entries.push(
|
|
1474
|
+
{ type: "item", text: "移除 provider", action: "remove" },
|
|
1533
1475
|
)
|
|
1534
1476
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1477
|
+
if (!agent.provider.apiKey) {
|
|
1478
|
+
entries.push({ type: "item", text: "设置 API Key", action: "key" })
|
|
1479
|
+
} else {
|
|
1480
|
+
entries.push({ type: "item", text: "更换 API Key", action: "key" })
|
|
1481
|
+
}
|
|
1482
|
+
openPicker({
|
|
1483
|
+
title: "Provider 管理",
|
|
1484
|
+
entries,
|
|
1485
|
+
onSelect: async (e) => {
|
|
1486
|
+
if (e.action === "list") {
|
|
1487
|
+
pushLabel(`❯ Providers (${agent.providers.length})`, ansi.bold + C.tool)
|
|
1488
|
+
for (const p of agent.providers) {
|
|
1489
|
+
const active = p.name === agent.activeProvider
|
|
1490
|
+
pushLine(
|
|
1491
|
+
`${active ? " ▸" : " "} ${p.name.padEnd(12)} ${p.model.padEnd(20)} ${p.baseURL}${p.apiKey ? " ●key" : " ○无key"}${active ? " ← 当前" : ""}`,
|
|
1492
|
+
active ? C.tool : C.dim,
|
|
1493
|
+
)
|
|
1494
|
+
}
|
|
1495
|
+
return
|
|
1496
|
+
}
|
|
1497
|
+
if (e.action === "remove") {
|
|
1498
|
+
const candidates = agent.providers.filter((p) => p.name !== agent.activeProvider)
|
|
1499
|
+
if (candidates.length === 0) {
|
|
1500
|
+
pushLine("只有当前 provider,无法移除(先用 /model 切换到别的 provider)", C.warn)
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
const removeEntries = [
|
|
1504
|
+
{ type: "header", text: "选择要移除的 provider(当前使用的不可移除)" },
|
|
1505
|
+
...candidates.map((p) => ({ type: "item", text: `${p.name} (${p.model})`, name: p.name })),
|
|
1506
|
+
]
|
|
1507
|
+
openPicker({
|
|
1508
|
+
title: "移除 Provider",
|
|
1509
|
+
entries: removeEntries,
|
|
1510
|
+
onSelect: async (se) => {
|
|
1511
|
+
const at = agent.providers.findIndex((p) => p.name === se.name)
|
|
1512
|
+
agent.providers.splice(at, 1)
|
|
1513
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1514
|
+
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1515
|
+
pushLine(`已删除 ${se.name}`, C.tool)
|
|
1516
|
+
},
|
|
1517
|
+
})
|
|
1518
|
+
return
|
|
1519
|
+
}
|
|
1520
|
+
if (e.action === "add") {
|
|
1521
|
+
// Add needs text input: name baseURL model
|
|
1522
|
+
askQuestion(
|
|
1523
|
+
`输入: <名称> <baseURL> <模型>\n预设可用: ${Object.keys(PRESETS).join(", ")}\n或只输预设名(如 deepseek)自动补全`,
|
|
1524
|
+
).then(async (text) => {
|
|
1525
|
+
if (!text) return
|
|
1526
|
+
const parts = text.split(/\s+/)
|
|
1527
|
+
const name = parts[0]
|
|
1528
|
+
if (!name) return
|
|
1529
|
+
if (agent.providers.some((p) => p.name === name)) {
|
|
1530
|
+
pushLine(`"${name}" 已存在;先 /provider → 移除`, C.warn)
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
const preset = PRESETS[name]
|
|
1534
|
+
const baseURL = (parts[1] ?? preset?.baseURL)?.replace(/\/+$/, "")
|
|
1535
|
+
const model = parts[2] ?? preset?.model
|
|
1536
|
+
if (!baseURL || !model) {
|
|
1537
|
+
pushLine(`缺少参数: ${name} <baseURL> <模型>`, C.error)
|
|
1538
|
+
return
|
|
1539
|
+
}
|
|
1540
|
+
if (!/^https?:\/\//.test(baseURL)) { pushLine(`baseURL 应以 http(s):// 开头`, C.error); return }
|
|
1541
|
+
agent.providers.push({ name, baseURL, model, ...(preset?.desc ? { desc: preset.desc } : {}) })
|
|
1542
|
+
await persistRaw((raw) => { raw.providers = agent.providers })
|
|
1543
|
+
pushLabel(`❯ Provider`, ansi.bold + C.tool)
|
|
1544
|
+
pushLine(`已添加 ${name}(${baseURL} / ${model})`, C.tool)
|
|
1545
|
+
pushLine(`下一步: /provider → 设置 Key`, C.dim)
|
|
1546
|
+
})
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
if (e.action === "key") {
|
|
1550
|
+
// Key: pick which provider, then prompt for key
|
|
1551
|
+
const keyEntries = [
|
|
1552
|
+
{ type: "header", text: "选择要配 key 的 provider" },
|
|
1553
|
+
...agent.providers.map((p) => ({
|
|
1554
|
+
type: "item",
|
|
1555
|
+
text: `${p.name}${p.name === agent.activeProvider ? " ← 当前" : ""}${p.apiKey ? " ●已有key" : " ○无key"}`,
|
|
1556
|
+
name: p.name,
|
|
1557
|
+
})),
|
|
1558
|
+
]
|
|
1559
|
+
openPicker({
|
|
1560
|
+
title: "配置 API Key",
|
|
1561
|
+
entries: keyEntries,
|
|
1562
|
+
onSelect: (se) => {
|
|
1563
|
+
askQuestion(`为 ${se.name} 输入 API Key:`).then(async (key) => {
|
|
1564
|
+
if (!key) return
|
|
1565
|
+
await setProviderKey(se.name, key)
|
|
1566
|
+
})
|
|
1567
|
+
},
|
|
1568
|
+
})
|
|
1569
|
+
}
|
|
1570
|
+
},
|
|
1571
|
+
})
|
|
1540
1572
|
return
|
|
1541
1573
|
}
|
|
1542
1574
|
case "/config": {
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1575
|
+
const entries = [
|
|
1576
|
+
{ type: "header", text: "配置管理" },
|
|
1577
|
+
{ type: "item", text: "查看当前配置", action: "view" },
|
|
1578
|
+
{ type: "item", text: "设置 embedding key(向量检索)", action: "embedkey" },
|
|
1579
|
+
{ type: "item", text: "高级设置(set path value)", action: "set" },
|
|
1580
|
+
]
|
|
1581
|
+
openPicker({
|
|
1582
|
+
title: "配置管理",
|
|
1583
|
+
entries,
|
|
1584
|
+
onSelect: async (e) => {
|
|
1585
|
+
if (e.action === "view") {
|
|
1586
|
+
const { configPath: cp } = await import("./config.mjs")
|
|
1587
|
+
pushLabel(`❯ 配置`, ansi.bold + C.tool)
|
|
1588
|
+
pushLine(`激活: ${agent.activeProvider} / ${agent.provider.model}`, C.dim)
|
|
1589
|
+
pushLine(`Key: ${maskKey(agent.provider.apiKey)}`, C.dim)
|
|
1590
|
+
const ac = agent.config?.agent ?? {}
|
|
1591
|
+
const tn = `${ac.compactThreshold ?? 100000}${ac.compactThresholdAuto ? " (auto)" : ""}`
|
|
1592
|
+
pushLine(`agent: maxTurns=${ac.maxTurns ?? 100} | compactThreshold=${tn}`, C.dim)
|
|
1593
|
+
pushLine(`embedding: ${agent.memory?.embedder ? `enabled (${agent.config?.embedding?.model ?? ""})` : "disabled(纯 FTS 检索)"}`, C.dim)
|
|
1594
|
+
pushLine(`配置文件: ${cp}`, C.dim)
|
|
1595
|
+
return
|
|
1596
|
+
}
|
|
1597
|
+
if (e.action === "embedkey") {
|
|
1598
|
+
askQuestion("输入 embedding 服务的 API Key(默认 SiliconFlow bge-m3):").then(async (key) => {
|
|
1599
|
+
if (!key) return
|
|
1600
|
+
agent.config.embedding ??= {}
|
|
1601
|
+
agent.config.embedding.apiKey = key
|
|
1602
|
+
await persistRaw((raw) => { raw.embedding = { ...(raw.embedding ?? {}), apiKey: key } })
|
|
1603
|
+
if (agent.memory) {
|
|
1604
|
+
const { createEmbedder } = await import("./embedding.mjs")
|
|
1605
|
+
agent.memory.embedder = createEmbedder(agent.config.embedding)
|
|
1606
|
+
}
|
|
1607
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
1608
|
+
pushLine(`embedding key 已保存,向量检索已启用`, C.tool)
|
|
1609
|
+
})
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
if (e.action === "set") {
|
|
1613
|
+
askQuestion("输入: <path> <value>(如 agent.maxTurns 80,支持 a.b 嵌套):").then(async (text) => {
|
|
1614
|
+
if (!text) return
|
|
1615
|
+
const parts = text.split(/\s+/)
|
|
1616
|
+
const [path, value] = [parts[0], parts.slice(1).join(" ")]
|
|
1617
|
+
if (!path || !value) { pushLine("用法: <path> <value> 如 agent.maxTurns 80", C.error); return }
|
|
1618
|
+
try {
|
|
1619
|
+
const { configPath, loadConfig, saveConfig } = await import("./config.mjs")
|
|
1620
|
+
const raw = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf8")) : {}
|
|
1621
|
+
const keys = path.split(".")
|
|
1622
|
+
let obj = raw
|
|
1623
|
+
for (let i = 0; i < keys.length - 1; i++) { obj[keys[i]] ??= {}; obj = obj[keys[i]] }
|
|
1624
|
+
obj[keys[keys.length - 1]] = isNaN(value) ? value : Number(value)
|
|
1625
|
+
saveConfig(raw)
|
|
1626
|
+
const cfg = loadConfig()
|
|
1627
|
+
agent.provider = cfg.provider
|
|
1628
|
+
agent.providers = cfg.providersList
|
|
1629
|
+
agent.activeProvider = cfg.activeProvider
|
|
1630
|
+
agent.config = cfg
|
|
1631
|
+
pushLabel(`❯ Config`, ansi.bold + C.tool)
|
|
1632
|
+
pushLine(`已保存: ${path} = ${value}`, C.tool)
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
pushLine(`保存失败: ${error.message}`, C.error)
|
|
1635
|
+
}
|
|
1636
|
+
})
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
})
|
|
1599
1640
|
return
|
|
1600
1641
|
}
|
|
1601
1642
|
case "/help": {
|
|
@@ -1703,9 +1744,22 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1703
1744
|
|
|
1704
1745
|
// ---------------------------------------------------------- 模型选择器(/model)
|
|
1705
1746
|
|
|
1706
|
-
const pickerItems = () => state.picker
|
|
1747
|
+
const pickerItems = () => state.picker?.entries.filter((e) => e.type === "item") ?? []
|
|
1707
1748
|
|
|
1708
|
-
/**
|
|
1749
|
+
/** 打开通用列表选择器。entries 含 { type: "header"|"item", text, note?, ...extra },
|
|
1750
|
+
* onSelect 拿到选中条目(含 extra 字段透传),onCancel 在 Esc 时调。 */
|
|
1751
|
+
function openPicker({ title, entries, onSelect, onCancel, defaultIndex = 0 }) {
|
|
1752
|
+
state.picker = { title, entries, lines: [], index: defaultIndex, scroll: 0, selectedLine: 0, onSelect, onCancel }
|
|
1753
|
+
renderPickerLines()
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function closePicker() {
|
|
1757
|
+
state.picker?.onCancel?.()
|
|
1758
|
+
state.picker = null
|
|
1759
|
+
render()
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/** 按 entries 重建显示行并刷新 */
|
|
1709
1763
|
function renderPickerLines() {
|
|
1710
1764
|
const p = state.picker
|
|
1711
1765
|
if (!p) return
|
|
@@ -1714,13 +1768,13 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1714
1768
|
let selectedLine = 0
|
|
1715
1769
|
for (const e of p.entries) {
|
|
1716
1770
|
if (e.type === "header") {
|
|
1717
|
-
lines.push({ text: ` ${e.
|
|
1771
|
+
lines.push({ text: ` ${e.text}${e.note ? ` ${e.note}` : ""}`, color: ansi.bold + C.tool })
|
|
1718
1772
|
} else {
|
|
1719
1773
|
const selected = row === p.index
|
|
1720
1774
|
if (selected) selectedLine = lines.length
|
|
1721
|
-
const
|
|
1775
|
+
const marker = e.marker ? ` ${e.marker}` : ""
|
|
1722
1776
|
lines.push({
|
|
1723
|
-
text: `${selected ? " ▸ " : " "}${e.
|
|
1777
|
+
text: `${selected ? " ▸ " : " "}${e.text}${marker}`,
|
|
1724
1778
|
color: selected ? ansi.bold + C.text : C.dim,
|
|
1725
1779
|
})
|
|
1726
1780
|
row++
|
|
@@ -1731,14 +1785,16 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1731
1785
|
render()
|
|
1732
1786
|
}
|
|
1733
1787
|
|
|
1734
|
-
|
|
1788
|
+
// ========== 模型选择器(基于通用 picker,异步拉取远端模型列表) ==========
|
|
1789
|
+
|
|
1735
1790
|
async function openModelPicker() {
|
|
1736
1791
|
const entries = []
|
|
1737
1792
|
for (const p of agent.providers) {
|
|
1738
|
-
entries.push({ type: "header",
|
|
1739
|
-
entries.push({ type: "item", provider: p.name, model: p.model })
|
|
1793
|
+
entries.push({ type: "header", text: p.name, note: `${p.baseURL}${p.apiKey ? "" : "(未配 key)"} 加载中...` })
|
|
1794
|
+
entries.push({ type: "item", text: p.model, provider: p.name, model: p.model })
|
|
1740
1795
|
}
|
|
1741
|
-
|
|
1796
|
+
const onSelect = (e) => selectModel(e).catch((err) => pushLine(`[error] ${err.message}`, C.error))
|
|
1797
|
+
openPicker({ title: "选择模型", entries, onSelect })
|
|
1742
1798
|
// 默认选中当前在用的模型
|
|
1743
1799
|
const current = pickerItems().findIndex(
|
|
1744
1800
|
(e) => e.provider === agent.activeProvider && e.model === agent.provider.model,
|
|
@@ -1749,10 +1805,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1749
1805
|
const { listModels } = await import("./provider.mjs")
|
|
1750
1806
|
await Promise.all(
|
|
1751
1807
|
agent.providers.map(async (p) => {
|
|
1752
|
-
const header = entries.find((e) => e.type === "header" && e.
|
|
1808
|
+
const header = entries.find((e) => e.type === "header" && e.provider === undefined && e.text === p.name)
|
|
1753
1809
|
const noteBase = `${p.baseURL}${p.apiKey ? "" : "(未配 key)"}`
|
|
1754
1810
|
try {
|
|
1755
|
-
// key 的环境变量兜底和 loadConfig 保持一致(提供商专用变量只对同名生效)
|
|
1756
1811
|
const envKey = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }[p.name]
|
|
1757
1812
|
let apiKey = p.apiKey
|
|
1758
1813
|
if (!apiKey && envKey && process.env[envKey]) apiKey = process.env[envKey]
|
|
@@ -1761,27 +1816,21 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1761
1816
|
{ baseURL: p.baseURL, apiKey: apiKey ?? "" },
|
|
1762
1817
|
{ signal: AbortSignal.timeout(10000) },
|
|
1763
1818
|
)
|
|
1764
|
-
// 展开到该 provider 已配置模型的后面(去重)
|
|
1765
1819
|
const at = entries.findIndex((e) => e.type === "item" && e.provider === p.name && e.model === p.model)
|
|
1766
1820
|
entries.splice(
|
|
1767
1821
|
at + 1,
|
|
1768
1822
|
0,
|
|
1769
|
-
...models.filter((m) => m !== p.model).map((m) => ({ type: "item", provider: p.name, model: m })),
|
|
1823
|
+
...models.filter((m) => m !== p.model).map((m) => ({ type: "item", text: m, provider: p.name, model: m })),
|
|
1770
1824
|
)
|
|
1771
|
-
header.note = noteBase
|
|
1825
|
+
if (header) header.note = noteBase
|
|
1772
1826
|
} catch (error) {
|
|
1773
|
-
header.note = `${noteBase} (拉取失败: ${sliceByWidth(error.message, 60)})`
|
|
1827
|
+
if (header) header.note = `${noteBase} (拉取失败: ${sliceByWidth(error.message, 60)})`
|
|
1774
1828
|
}
|
|
1775
|
-
if (state.picker?.entries === entries) renderPickerLines()
|
|
1829
|
+
if (state.picker?.entries === entries) renderPickerLines()
|
|
1776
1830
|
}),
|
|
1777
1831
|
)
|
|
1778
1832
|
}
|
|
1779
1833
|
|
|
1780
|
-
function closeModelPicker() {
|
|
1781
|
-
state.picker = null
|
|
1782
|
-
render()
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
1834
|
/** 给指定 provider 写 key(内存 + 配置文件);若它是当前激活的,同步运行时 */
|
|
1786
1835
|
async function setProviderKey(name, key) {
|
|
1787
1836
|
const target = agent.providers.find((p) => p.name === name)
|
|
@@ -1949,7 +1998,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
1949
1998
|
|
|
1950
1999
|
/** 选中:切换 provider + 模型,持久化,阈值随模型走 */
|
|
1951
2000
|
async function selectModel(item) {
|
|
1952
|
-
|
|
2001
|
+
closePicker()
|
|
1953
2002
|
const target = agent.providers.find((pp) => pp.name === item.provider)
|
|
1954
2003
|
if (!target) return
|
|
1955
2004
|
target.model = item.model
|
|
@@ -2109,11 +2158,11 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2109
2158
|
setTimeout(() => process.exit(0), 100)
|
|
2110
2159
|
}
|
|
2111
2160
|
|
|
2112
|
-
//
|
|
2161
|
+
// 通用列表选择器:↑↓ 移动,Enter 确认,Esc 取消
|
|
2113
2162
|
if (state.picker) {
|
|
2114
2163
|
const items = pickerItems()
|
|
2115
2164
|
if (key.name === "escape") {
|
|
2116
|
-
|
|
2165
|
+
closePicker()
|
|
2117
2166
|
} else if (key.name === "up" && items.length) {
|
|
2118
2167
|
state.picker.index = (state.picker.index - 1 + items.length) % items.length
|
|
2119
2168
|
renderPickerLines()
|
|
@@ -2121,7 +2170,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2121
2170
|
state.picker.index = (state.picker.index + 1) % items.length
|
|
2122
2171
|
renderPickerLines()
|
|
2123
2172
|
} else if (key.name === "return" && items.length) {
|
|
2124
|
-
|
|
2173
|
+
const selected = items[state.picker.index]
|
|
2174
|
+
state.picker.onSelect?.(selected)
|
|
2175
|
+
closePicker()
|
|
2125
2176
|
}
|
|
2126
2177
|
return
|
|
2127
2178
|
}
|
|
@@ -2238,7 +2289,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
2238
2289
|
return
|
|
2239
2290
|
}
|
|
2240
2291
|
if (key.name === "return") {
|
|
2241
|
-
submit()
|
|
2292
|
+
submit().catch((e) => pushLine(`[error] ${e.message}`, C.error))
|
|
2242
2293
|
return
|
|
2243
2294
|
}
|
|
2244
2295
|
|