@laivc/laicode 0.1.0 → 0.2.1
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 +48 -1
- package/bin/laicode.js +626 -62
- package/lib/adapters.js +666 -0
- package/package.json +2 -1
- package/scripts/smoke-test.js +153 -5
package/bin/laicode.js
CHANGED
|
@@ -6,8 +6,17 @@ const os = require("os")
|
|
|
6
6
|
const path = require("path")
|
|
7
7
|
const readline = require("readline")
|
|
8
8
|
const { spawn } = require("child_process")
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const {
|
|
10
|
+
applyInitPlan,
|
|
11
|
+
applyToolActionPlan,
|
|
12
|
+
buildInitPlan,
|
|
13
|
+
buildToolActionPlan,
|
|
14
|
+
detectTools,
|
|
15
|
+
rollbackLatest,
|
|
16
|
+
safePlan,
|
|
17
|
+
} = require("../lib/adapters")
|
|
18
|
+
|
|
19
|
+
const VERSION = "0.2.1"
|
|
11
20
|
const DEFAULT_SITE_BASE = "https://lai.vc"
|
|
12
21
|
const DEFAULT_API_BASE = "https://api.lai.vc/v1"
|
|
13
22
|
const STATE_DIR = process.env.LAICODE_HOME || path.join(os.homedir(), ".laicode")
|
|
@@ -15,6 +24,9 @@ const CONFIG_FILE = path.join(STATE_DIR, "config.json")
|
|
|
15
24
|
const DASHBOARD_CACHE_TTL_MS = 60_000
|
|
16
25
|
|
|
17
26
|
const COMMAND_SPECS = [
|
|
27
|
+
{ name: "tools", usage: "laicode tools [scan|install|uninstall|reinstall|configure] [--tool codex] [--apply] [--json]", summary: "AI 工具体检、安装、配置和维护", flags: ["scan", "install", "uninstall", "reinstall", "configure", "--tool", "--apply", "--json"] },
|
|
28
|
+
{ name: "init", usage: "laicode init [--tool codex] [--apply] [--json]", summary: "预览或应用一键接入计划", flags: ["--tool", "--model", "--apply", "--json"] },
|
|
29
|
+
{ name: "rollback", usage: "laicode rollback [--json]", summary: "回滚最近一次接入写入", flags: ["--json"] },
|
|
18
30
|
{ name: "menu", usage: "laicode menu", summary: "打开交互控制台", flags: ["--fancy"] },
|
|
19
31
|
{ name: "brand", usage: "laicode brand", summary: "预览终端字标和品牌色板", flags: ["--color", "--plain"] },
|
|
20
32
|
{ name: "status", usage: "laicode status [--refresh] [--json]", summary: "查看实时驾驶舱状态", flags: ["--refresh", "--json"] },
|
|
@@ -39,6 +51,7 @@ const ENV_VARS = [
|
|
|
39
51
|
["LAICODE_SITE_URL", "覆盖主站地址,默认 https://lai.vc"],
|
|
40
52
|
["LAICODE_PLAIN", "设为 1 时禁用终端样式"],
|
|
41
53
|
["LAICODE_COLOR", "设为 1 时忽略 NO_COLOR,强制彩色输出"],
|
|
54
|
+
["LAICODE_NO_SPINNER", "设为 1 时禁用 loading 动画"],
|
|
42
55
|
]
|
|
43
56
|
|
|
44
57
|
function buildHelp() {
|
|
@@ -128,6 +141,7 @@ const VALUE_FLAGS = new Set([
|
|
|
128
141
|
"name",
|
|
129
142
|
"id",
|
|
130
143
|
"shell",
|
|
144
|
+
"tool",
|
|
131
145
|
])
|
|
132
146
|
|
|
133
147
|
function parseArgs(argv) {
|
|
@@ -225,7 +239,111 @@ function compareVersions(a, b) {
|
|
|
225
239
|
return 0
|
|
226
240
|
}
|
|
227
241
|
|
|
242
|
+
let activeSpinnerCleanup = null
|
|
243
|
+
|
|
244
|
+
function spinnerEnabled(opts = {}) {
|
|
245
|
+
if (opts.enabled === false) return false
|
|
246
|
+
if (!process.stdout.isTTY) return false
|
|
247
|
+
if (process.env.LAICODE_NO_SPINNER === "1") return false
|
|
248
|
+
if (process.env.CI === "1" || process.env.CI === "true") return false
|
|
249
|
+
if (process.env.TERM === "dumb") return false
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function createSpinner(label, opts = {}) {
|
|
254
|
+
const enabled = spinnerEnabled(opts)
|
|
255
|
+
const frames = plainMode() ? ["-", "\\", "|", "/"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
256
|
+
const intervalMs = Number(opts.intervalMs || 80)
|
|
257
|
+
const delayMs = Number(opts.delayMs ?? 120)
|
|
258
|
+
let text = label
|
|
259
|
+
let frame = 0
|
|
260
|
+
let active = false
|
|
261
|
+
let visible = false
|
|
262
|
+
let interval = null
|
|
263
|
+
let delay = null
|
|
264
|
+
|
|
265
|
+
const clear = () => {
|
|
266
|
+
readline.clearLine(process.stdout, 0)
|
|
267
|
+
readline.cursorTo(process.stdout, 0)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const paint = () => {
|
|
271
|
+
if (!active || !enabled) return
|
|
272
|
+
clear()
|
|
273
|
+
const glyph = frames[frame % frames.length]
|
|
274
|
+
frame += 1
|
|
275
|
+
process.stdout.write(`${ui.accent(glyph)} ${ui.dim(text)}`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const show = () => {
|
|
279
|
+
if (!active || visible || !enabled) return
|
|
280
|
+
visible = true
|
|
281
|
+
activeSpinnerCleanup = () => spinner.stop()
|
|
282
|
+
paint()
|
|
283
|
+
interval = setInterval(paint, intervalMs)
|
|
284
|
+
if (interval.unref) interval.unref()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const spinner = {
|
|
288
|
+
get enabled() {
|
|
289
|
+
return enabled
|
|
290
|
+
},
|
|
291
|
+
start() {
|
|
292
|
+
if (!enabled || active) return this
|
|
293
|
+
active = true
|
|
294
|
+
if (delayMs > 0) {
|
|
295
|
+
delay = setTimeout(show, delayMs)
|
|
296
|
+
if (delay.unref) delay.unref()
|
|
297
|
+
} else {
|
|
298
|
+
show()
|
|
299
|
+
}
|
|
300
|
+
return this
|
|
301
|
+
},
|
|
302
|
+
update(nextText) {
|
|
303
|
+
if (nextText) text = nextText
|
|
304
|
+
if (visible) paint()
|
|
305
|
+
return this
|
|
306
|
+
},
|
|
307
|
+
stop(finalText) {
|
|
308
|
+
if (!enabled || !active) return this
|
|
309
|
+
active = false
|
|
310
|
+
if (delay) clearTimeout(delay)
|
|
311
|
+
if (interval) clearInterval(interval)
|
|
312
|
+
delay = null
|
|
313
|
+
interval = null
|
|
314
|
+
if (activeSpinnerCleanup) activeSpinnerCleanup = null
|
|
315
|
+
if (visible) clear()
|
|
316
|
+
visible = false
|
|
317
|
+
if (finalText) process.stdout.write(`${finalText}\n`)
|
|
318
|
+
return this
|
|
319
|
+
},
|
|
320
|
+
succeed(finalText) {
|
|
321
|
+
return this.stop(finalText ? `${ui.ok("✓")} ${finalText}` : "")
|
|
322
|
+
},
|
|
323
|
+
fail(finalText) {
|
|
324
|
+
return this.stop(finalText ? `${ui.err("×")} ${finalText}` : "")
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return spinner
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function withSpinner(label, work, opts = {}) {
|
|
332
|
+
const spinner = createSpinner(label, opts).start()
|
|
333
|
+
try {
|
|
334
|
+
const result = await work(spinner)
|
|
335
|
+
if (opts.successText) spinner.succeed(opts.successText)
|
|
336
|
+
else spinner.stop()
|
|
337
|
+
return result
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (opts.failureText) spinner.fail(opts.failureText)
|
|
340
|
+
else spinner.stop()
|
|
341
|
+
throw err
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
228
345
|
function die(message, code = 1) {
|
|
346
|
+
if (activeSpinnerCleanup) activeSpinnerCleanup()
|
|
229
347
|
console.error(message)
|
|
230
348
|
process.exit(code)
|
|
231
349
|
}
|
|
@@ -467,6 +585,229 @@ function cmdCommands(args = {}) {
|
|
|
467
585
|
)
|
|
468
586
|
}
|
|
469
587
|
|
|
588
|
+
function supportLabel(value) {
|
|
589
|
+
if (value === "auto") return ui.ok("可自动接入")
|
|
590
|
+
if (value === "auto-new") return ui.ok("可创建配置")
|
|
591
|
+
if (value === "manual-merge") return ui.warn("需合并")
|
|
592
|
+
if (value === "blocked") return ui.warn("待网关")
|
|
593
|
+
return ui.dim("指引")
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function cmdTools(args = {}) {
|
|
597
|
+
const action = args._[1] || "scan"
|
|
598
|
+
if (action === "configure") {
|
|
599
|
+
return await cmdInit({ ...args, _: ["init"], tool: args.tool || args._[2] || "codex" })
|
|
600
|
+
}
|
|
601
|
+
if (["install", "uninstall", "reinstall"].includes(action)) {
|
|
602
|
+
return await cmdToolAction(action, args)
|
|
603
|
+
}
|
|
604
|
+
if (action !== "scan" && action !== "list") {
|
|
605
|
+
die("Usage: laicode tools [scan|install|uninstall|reinstall|configure] [--tool codex]")
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const tools = await withSpinner("扫描本机 AI 工具", () => Promise.resolve(detectTools()), {
|
|
609
|
+
enabled: !args.json,
|
|
610
|
+
delayMs: 80,
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
if (args.json) {
|
|
614
|
+
console.log(JSON.stringify({ tools }, null, 2))
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
printHero("AI 工具体检")
|
|
619
|
+
printTable(
|
|
620
|
+
"接入状态",
|
|
621
|
+
[
|
|
622
|
+
{ key: "name", label: "工具", width: 12, max: 18 },
|
|
623
|
+
{ key: "detected", label: "检测", width: 6 },
|
|
624
|
+
{ key: "support", label: "能力", width: 12, max: 18 },
|
|
625
|
+
{ key: "status", label: "状态", width: 22, max: 36 },
|
|
626
|
+
{ key: "next", label: "下一步", width: 28, max: 44 },
|
|
627
|
+
],
|
|
628
|
+
tools.map((tool) => ({
|
|
629
|
+
name: tool.name,
|
|
630
|
+
detected: tool.detected ? ui.ok("发现") : ui.dim("未见"),
|
|
631
|
+
support: supportLabel(tool.support),
|
|
632
|
+
status: tool.status,
|
|
633
|
+
next: tool.next,
|
|
634
|
+
}))
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function printToolActionPlan(plan) {
|
|
639
|
+
printHero("AI 工具维护")
|
|
640
|
+
printPanel("动作", [
|
|
641
|
+
`工具 ${plan.tool}`,
|
|
642
|
+
`动作 ${plan.action}`,
|
|
643
|
+
`模式 ${plan.mode}`,
|
|
644
|
+
`状态 ${plan.ok ? "可执行" : "不可执行"}`,
|
|
645
|
+
`说明 ${plan.summary || plan.reason || "-"}`,
|
|
646
|
+
])
|
|
647
|
+
|
|
648
|
+
if (plan.commands?.length) {
|
|
649
|
+
console.log("")
|
|
650
|
+
printPanel("将执行的命令", plan.commands.map((command) => command.display))
|
|
651
|
+
}
|
|
652
|
+
if (plan.warnings?.length) {
|
|
653
|
+
console.log("")
|
|
654
|
+
printPanel("注意", plan.warnings)
|
|
655
|
+
}
|
|
656
|
+
if (plan.postSteps?.length) {
|
|
657
|
+
console.log("")
|
|
658
|
+
printPanel("后续", plan.postSteps)
|
|
659
|
+
}
|
|
660
|
+
if (!plan.ok && plan.reason) console.log(ui.warn(plan.reason))
|
|
661
|
+
else console.log(ui.dim(`执行命令: laicode tools ${plan.action} --tool ${plan.tool} --apply`))
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function cmdToolAction(action, args = {}) {
|
|
665
|
+
const tool = args.tool || args._[2] || "codex"
|
|
666
|
+
const apply = Boolean(args.apply)
|
|
667
|
+
const plan = buildToolActionPlan(tool, action, { env: process.env })
|
|
668
|
+
|
|
669
|
+
if (args.json && !apply) {
|
|
670
|
+
console.log(JSON.stringify({ apply, plan }, null, 2))
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!args.json) printToolActionPlan(plan)
|
|
675
|
+
if (!apply) return
|
|
676
|
+
if (!plan.ok) die(plan.reason || "当前动作不可执行。")
|
|
677
|
+
|
|
678
|
+
const result = await withSpinner(
|
|
679
|
+
`执行 ${plan.tool} ${plan.action}`,
|
|
680
|
+
() => applyToolActionPlan(plan, { stdio: args.json ? "pipe" : "inherit" }),
|
|
681
|
+
{
|
|
682
|
+
enabled: !args.json,
|
|
683
|
+
delayMs: 80,
|
|
684
|
+
}
|
|
685
|
+
)
|
|
686
|
+
if (args.json) {
|
|
687
|
+
console.log(JSON.stringify({ apply, plan, result }, null, 2))
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
console.log("")
|
|
691
|
+
printPanel("已完成", [
|
|
692
|
+
`工具 ${result.tool}`,
|
|
693
|
+
`动作 ${result.action}`,
|
|
694
|
+
`命令数 ${result.commands.length}`,
|
|
695
|
+
...(result.postSteps || []),
|
|
696
|
+
])
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function printInitPlan(plan, safe) {
|
|
700
|
+
printHero(plan.ok ? "接入计划" : "接入指引")
|
|
701
|
+
printPanel("目标", [
|
|
702
|
+
`工具 ${plan.tool}`,
|
|
703
|
+
`模式 ${plan.mode}`,
|
|
704
|
+
`模型 ${plan.model || "-"}`,
|
|
705
|
+
`网关 ${plan.apiBaseUrl || apiBase({})}`,
|
|
706
|
+
`说明 ${plan.summary || plan.reason || "-"}`,
|
|
707
|
+
])
|
|
708
|
+
|
|
709
|
+
if (safe.operations?.length) {
|
|
710
|
+
console.log("")
|
|
711
|
+
printTable(
|
|
712
|
+
"将写入的文件",
|
|
713
|
+
[
|
|
714
|
+
{ key: "action", label: "动作", width: 8 },
|
|
715
|
+
{ key: "path", label: "路径", width: 42, max: 72 },
|
|
716
|
+
{ key: "exists", label: "现状", width: 8 },
|
|
717
|
+
],
|
|
718
|
+
safe.operations.map((op) => ({
|
|
719
|
+
action: op.action,
|
|
720
|
+
path: op.path,
|
|
721
|
+
exists: op.exists ? ui.warn("覆盖") : ui.ok("新建"),
|
|
722
|
+
}))
|
|
723
|
+
)
|
|
724
|
+
console.log("")
|
|
725
|
+
for (const op of safe.operations) {
|
|
726
|
+
console.log(ui.bold(op.description || op.path))
|
|
727
|
+
console.log(ui.dim(op.path))
|
|
728
|
+
console.log("```")
|
|
729
|
+
console.log(op.preview.trimEnd())
|
|
730
|
+
console.log("```")
|
|
731
|
+
}
|
|
732
|
+
console.log("")
|
|
733
|
+
console.log(ui.dim("应用命令: laicode init --tool " + plan.tool + " --apply"))
|
|
734
|
+
if (plan.runCommand) console.log(ui.dim("使用命令: " + plan.runCommand))
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (plan.instructions?.length) {
|
|
739
|
+
printPanel("手动接入步骤", plan.instructions.map((item, index) => `${index + 1}. ${item}`))
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function cmdInit(args = {}) {
|
|
744
|
+
let cfg = await readConfig()
|
|
745
|
+
const tool = args.tool || args._[1] || "codex"
|
|
746
|
+
const apply = Boolean(args.apply)
|
|
747
|
+
const model = args.model || cfg.defaultModel || "gpt-5.5"
|
|
748
|
+
|
|
749
|
+
let key = cfg.apiKey
|
|
750
|
+
if (apply && !key && cfg.accessToken) {
|
|
751
|
+
key = await withSpinner("准备 API Key", () => ensureApiKey(cfg), { enabled: !args.json, delayMs: 80 })
|
|
752
|
+
cfg = await readConfig()
|
|
753
|
+
}
|
|
754
|
+
if (apply && !key) die("未找到 API Key。请先运行 `laicode login` 或 `laicode config set api-key sk-...`。")
|
|
755
|
+
|
|
756
|
+
const plan = buildInitPlan(tool, {
|
|
757
|
+
apiBaseUrl: apiBase(cfg),
|
|
758
|
+
apiKey: apply ? key : key || "sk-preview-only",
|
|
759
|
+
commandName: "laicode",
|
|
760
|
+
env: process.env,
|
|
761
|
+
model,
|
|
762
|
+
})
|
|
763
|
+
const safe = safePlan(plan, [key, cfg.apiKey].filter(Boolean))
|
|
764
|
+
|
|
765
|
+
if (args.json && !apply) {
|
|
766
|
+
console.log(JSON.stringify({ apply, plan: safe }, null, 2))
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (!args.json) printInitPlan(plan, safe)
|
|
771
|
+
if (!apply) return
|
|
772
|
+
if (!plan.ok) die(plan.reason || "当前工具暂不能自动接入。")
|
|
773
|
+
|
|
774
|
+
const manifest = await withSpinner("写入接入配置并创建备份", () => applyInitPlan(plan, { stateDir: STATE_DIR }), {
|
|
775
|
+
enabled: !args.json,
|
|
776
|
+
delayMs: 80,
|
|
777
|
+
})
|
|
778
|
+
if (args.json) {
|
|
779
|
+
console.log(JSON.stringify({ apply, plan: safe, result: manifest }, null, 2))
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
console.log("")
|
|
783
|
+
printPanel("已应用", [
|
|
784
|
+
`工具 ${manifest.tool}`,
|
|
785
|
+
`备份 ${manifest.id}`,
|
|
786
|
+
`文件 ${manifest.operations.map((op) => op.path).join(", ")}`,
|
|
787
|
+
plan.runCommand ? `使用 ${plan.runCommand}` : "使用 打开目标工具验证",
|
|
788
|
+
])
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function cmdRollback(args = {}) {
|
|
792
|
+
const manifest = await withSpinner("查找最近一次备份", () => rollbackLatest({ stateDir: STATE_DIR }), {
|
|
793
|
+
enabled: !args.json,
|
|
794
|
+
delayMs: 80,
|
|
795
|
+
})
|
|
796
|
+
if (args.json) {
|
|
797
|
+
console.log(JSON.stringify({ ok: Boolean(manifest), rollback: manifest || null }, null, 2))
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
if (!manifest) {
|
|
801
|
+
console.log("没有可回滚的 Laicode 接入备份。")
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
printPanel("已回滚", [
|
|
805
|
+
`工具 ${manifest.tool}`,
|
|
806
|
+
`备份 ${manifest.id}`,
|
|
807
|
+
`文件 ${manifest.operations.map((op) => op.path).join(", ")}`,
|
|
808
|
+
])
|
|
809
|
+
}
|
|
810
|
+
|
|
470
811
|
function printHero(subtitle = "Lai.vc 中转控制台") {
|
|
471
812
|
console.clear()
|
|
472
813
|
printBrandMark()
|
|
@@ -1100,10 +1441,15 @@ function statusPayload(cfg, snapshot) {
|
|
|
1100
1441
|
|
|
1101
1442
|
async function cmdStatus(args = {}) {
|
|
1102
1443
|
const cfg = await readConfig()
|
|
1103
|
-
const snapshot = await
|
|
1104
|
-
refresh:
|
|
1105
|
-
|
|
1106
|
-
|
|
1444
|
+
const snapshot = await withSpinner(
|
|
1445
|
+
args.refresh ? "刷新驾驶舱快照" : "同步驾驶舱快照",
|
|
1446
|
+
() =>
|
|
1447
|
+
dashboardSnapshot(cfg, {
|
|
1448
|
+
refresh: args.refresh,
|
|
1449
|
+
ttlMs: args.refresh ? 0 : DASHBOARD_CACHE_TTL_MS,
|
|
1450
|
+
}),
|
|
1451
|
+
{ enabled: !args.json }
|
|
1452
|
+
)
|
|
1107
1453
|
const next = await readConfig()
|
|
1108
1454
|
|
|
1109
1455
|
if (args.json) {
|
|
@@ -1141,21 +1487,34 @@ async function chatCompletion(cfg, messages, opts = {}) {
|
|
|
1141
1487
|
const started = Date.now()
|
|
1142
1488
|
let firstTokenAt = 0
|
|
1143
1489
|
let content = ""
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1490
|
+
let prefixWritten = false
|
|
1491
|
+
const loading =
|
|
1492
|
+
opts.print !== false && opts.loadingText
|
|
1493
|
+
? createSpinner(opts.loadingText, { delayMs: opts.loadingDelayMs ?? 80 }).start()
|
|
1494
|
+
: null
|
|
1495
|
+
|
|
1496
|
+
let res
|
|
1497
|
+
try {
|
|
1498
|
+
res = await fetch(`${apiBase(cfg)}/chat/completions`, {
|
|
1499
|
+
method: "POST",
|
|
1500
|
+
headers: {
|
|
1501
|
+
"Content-Type": "application/json",
|
|
1502
|
+
Authorization: `Bearer ${key}`,
|
|
1503
|
+
},
|
|
1504
|
+
body: JSON.stringify({
|
|
1505
|
+
model,
|
|
1506
|
+
messages,
|
|
1507
|
+
stream: opts.stream !== false,
|
|
1508
|
+
temperature: opts.temperature == null ? 0.2 : Number(opts.temperature),
|
|
1509
|
+
}),
|
|
1510
|
+
})
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
if (loading) loading.stop()
|
|
1513
|
+
throw err
|
|
1514
|
+
}
|
|
1157
1515
|
|
|
1158
1516
|
if (!res.ok) {
|
|
1517
|
+
if (loading) loading.stop()
|
|
1159
1518
|
const data = await readJsonResponse(res)
|
|
1160
1519
|
const msg = data.error?.message || data.error || data.message || `${res.status} ${res.statusText}`
|
|
1161
1520
|
const err = new Error(msg)
|
|
@@ -1166,6 +1525,7 @@ async function chatCompletion(cfg, messages, opts = {}) {
|
|
|
1166
1525
|
|
|
1167
1526
|
if (opts.stream === false) {
|
|
1168
1527
|
const data = await readJsonResponse(res)
|
|
1528
|
+
if (loading) loading.stop()
|
|
1169
1529
|
content = data.choices?.[0]?.message?.content || ""
|
|
1170
1530
|
return { content, firstTokenMs: Date.now() - started, totalMs: Date.now() - started, status: res.status }
|
|
1171
1531
|
}
|
|
@@ -1191,11 +1551,19 @@ async function chatCompletion(cfg, messages, opts = {}) {
|
|
|
1191
1551
|
if (delta) {
|
|
1192
1552
|
if (!firstTokenAt) firstTokenAt = Date.now()
|
|
1193
1553
|
content += delta
|
|
1194
|
-
if (opts.print !== false)
|
|
1554
|
+
if (opts.print !== false) {
|
|
1555
|
+
if (loading) loading.stop()
|
|
1556
|
+
if (opts.prefixOnFirstToken && !prefixWritten) {
|
|
1557
|
+
process.stdout.write(opts.prefixOnFirstToken)
|
|
1558
|
+
prefixWritten = true
|
|
1559
|
+
}
|
|
1560
|
+
process.stdout.write(delta)
|
|
1561
|
+
}
|
|
1195
1562
|
}
|
|
1196
1563
|
}
|
|
1197
1564
|
}
|
|
1198
1565
|
|
|
1566
|
+
if (loading) loading.stop()
|
|
1199
1567
|
if (opts.print !== false) process.stdout.write("\n")
|
|
1200
1568
|
return {
|
|
1201
1569
|
content,
|
|
@@ -1215,10 +1583,15 @@ async function cmdLogin(args) {
|
|
|
1215
1583
|
platform: platformName(),
|
|
1216
1584
|
scopes: ["account:read", "keys:read", "keys:create"],
|
|
1217
1585
|
}
|
|
1218
|
-
const data = await
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1586
|
+
const data = await withSpinner(
|
|
1587
|
+
"创建设备授权码",
|
|
1588
|
+
() =>
|
|
1589
|
+
siteJson({ siteBase: base }, "/api/laicode/device/start", {
|
|
1590
|
+
method: "POST",
|
|
1591
|
+
body: JSON.stringify(body),
|
|
1592
|
+
}),
|
|
1593
|
+
{ delayMs: 80 }
|
|
1594
|
+
)
|
|
1222
1595
|
|
|
1223
1596
|
console.log("打开以下链接授权 LaiCode:")
|
|
1224
1597
|
console.log(data.verificationUriComplete || data.verificationUri)
|
|
@@ -1233,23 +1606,29 @@ async function cmdLogin(args) {
|
|
|
1233
1606
|
|
|
1234
1607
|
let interval = Number(data.interval || 2)
|
|
1235
1608
|
const deadline = Date.now() + Number(data.expiresIn || 600) * 1000
|
|
1609
|
+
const pollSpinner = createSpinner("等待浏览器授权", { delayMs: 0 }).start()
|
|
1610
|
+
const fallbackDots = !pollSpinner.enabled
|
|
1236
1611
|
while (Date.now() < deadline) {
|
|
1237
1612
|
await sleep(interval * 1000)
|
|
1613
|
+
const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000))
|
|
1614
|
+
pollSpinner.update(`等待浏览器授权 · 剩余 ${remaining}s`)
|
|
1238
1615
|
const poll = await siteJson({ siteBase: base }, "/api/laicode/device/poll", {
|
|
1239
1616
|
method: "POST",
|
|
1240
1617
|
body: JSON.stringify({ deviceCode: data.deviceCode }),
|
|
1241
1618
|
})
|
|
1242
1619
|
if (poll.status === "pending") {
|
|
1243
|
-
process.stdout.write(".")
|
|
1620
|
+
if (fallbackDots) process.stdout.write(".")
|
|
1244
1621
|
interval = Number(poll.interval || interval)
|
|
1245
1622
|
continue
|
|
1246
1623
|
}
|
|
1247
1624
|
if (poll.status === "slow_down") {
|
|
1248
1625
|
interval = Math.max(interval + 1, Number(poll.interval || interval))
|
|
1249
|
-
|
|
1626
|
+
pollSpinner.update(`等待浏览器授权 · 降低轮询频率 ${interval}s`)
|
|
1627
|
+
if (fallbackDots) process.stdout.write(" slow_down ")
|
|
1250
1628
|
continue
|
|
1251
1629
|
}
|
|
1252
|
-
process.stdout.write("\n")
|
|
1630
|
+
if (fallbackDots) process.stdout.write("\n")
|
|
1631
|
+
else pollSpinner.stop()
|
|
1253
1632
|
if (poll.status === "denied") die("授权已被拒绝。")
|
|
1254
1633
|
if (poll.status === "expired") die("授权已过期,请重新运行 `laicode login`。")
|
|
1255
1634
|
if (poll.status === "authorized") {
|
|
@@ -1264,7 +1643,7 @@ async function cmdLogin(args) {
|
|
|
1264
1643
|
})
|
|
1265
1644
|
console.log(`已授权: ${poll.user?.email || poll.user?.name || "unknown"}`)
|
|
1266
1645
|
if (!args["no-key"]) {
|
|
1267
|
-
const key = await ensureApiKey(next, { tier: args.tier })
|
|
1646
|
+
const key = await withSpinner("准备 API Key", () => ensureApiKey(next, { tier: args.tier }), { delayMs: 80 })
|
|
1268
1647
|
next = await readConfig()
|
|
1269
1648
|
console.log(`API Key 就绪: ${maskSecret(key)} (${next.apiKeyName || "Laicode key"})`)
|
|
1270
1649
|
}
|
|
@@ -1272,12 +1651,14 @@ async function cmdLogin(args) {
|
|
|
1272
1651
|
}
|
|
1273
1652
|
die(`Unexpected authorization status: ${poll.status}`)
|
|
1274
1653
|
}
|
|
1654
|
+
pollSpinner.stop()
|
|
1655
|
+
if (fallbackDots) process.stdout.write("\n")
|
|
1275
1656
|
die("授权等待超时,请重新运行 `laicode login`。")
|
|
1276
1657
|
}
|
|
1277
1658
|
|
|
1278
1659
|
async function cmdModels(args) {
|
|
1279
1660
|
const cfg = await readConfig()
|
|
1280
|
-
const data = await getModels(cfg)
|
|
1661
|
+
const data = await withSpinner("拉取模型雷达", () => getModels(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1281
1662
|
let models = data.models || []
|
|
1282
1663
|
if (args.online) models = models.filter(modelOnline)
|
|
1283
1664
|
if (args.category) models = models.filter((m) => m.category === args.category)
|
|
@@ -1333,9 +1714,10 @@ async function cmdModels(args) {
|
|
|
1333
1714
|
async function cmdKeys(args) {
|
|
1334
1715
|
const action = args._[1] || "list"
|
|
1335
1716
|
const cfg = await readConfig()
|
|
1717
|
+
if (!cfg.accessToken) die("Not logged in. Run `laicode login` first.")
|
|
1336
1718
|
if (action === "list") {
|
|
1337
|
-
const keys = await listKeys(cfg)
|
|
1338
1719
|
const reveal = await confirmRevealKey(args)
|
|
1720
|
+
const keys = await withSpinner("读取 API Key 列表", () => listKeys(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1339
1721
|
if (args.json) {
|
|
1340
1722
|
console.log(JSON.stringify({ keys: keys.map((k) => safeKeyRecord(k, reveal)), reveal }, null, 2))
|
|
1341
1723
|
return
|
|
@@ -1367,10 +1749,15 @@ async function cmdKeys(args) {
|
|
|
1367
1749
|
const body = { name }
|
|
1368
1750
|
if (args.tier) body.tier = args.tier
|
|
1369
1751
|
const reveal = await confirmRevealKey(args)
|
|
1370
|
-
const data = await
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1752
|
+
const data = await withSpinner(
|
|
1753
|
+
"创建 API Key",
|
|
1754
|
+
() =>
|
|
1755
|
+
authJson(cfg, "/api/laicode/keys", {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
body: JSON.stringify(body),
|
|
1758
|
+
}),
|
|
1759
|
+
{ enabled: !args.json, delayMs: 80 }
|
|
1760
|
+
)
|
|
1374
1761
|
const key = data.key
|
|
1375
1762
|
await patchConfig({
|
|
1376
1763
|
apiKey: key.key,
|
|
@@ -1391,7 +1778,11 @@ async function cmdKeys(args) {
|
|
|
1391
1778
|
if (action === "delete") {
|
|
1392
1779
|
const id = args._[2] || args.id
|
|
1393
1780
|
if (!id) die("Usage: laicode keys delete <id>")
|
|
1394
|
-
await
|
|
1781
|
+
await withSpinner(
|
|
1782
|
+
`删除 API Key ${id}`,
|
|
1783
|
+
() => authJson(cfg, `/api/laicode/keys/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
1784
|
+
{ enabled: !args.json, delayMs: 80 }
|
|
1785
|
+
)
|
|
1395
1786
|
const patch = {}
|
|
1396
1787
|
if (String(cfg.apiKeyId) === String(id)) {
|
|
1397
1788
|
patch.apiKey = undefined
|
|
@@ -1519,16 +1910,29 @@ async function cmdConfig(args) {
|
|
|
1519
1910
|
die("Usage: laicode config [list|get|set|unset]")
|
|
1520
1911
|
}
|
|
1521
1912
|
|
|
1913
|
+
async function cmdCredential(args) {
|
|
1914
|
+
const action = args._[1]
|
|
1915
|
+
if (action !== "api-key") die("Usage: laicode credential api-key")
|
|
1916
|
+
let cfg = await readConfig()
|
|
1917
|
+
let key = cfg.apiKey
|
|
1918
|
+
if (!key && cfg.accessToken) {
|
|
1919
|
+
key = await ensureApiKey(cfg)
|
|
1920
|
+
cfg = await readConfig()
|
|
1921
|
+
}
|
|
1922
|
+
if (!key) die("No API key available. Run `laicode login` first.")
|
|
1923
|
+
process.stdout.write(`${key}\n`)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1522
1926
|
async function cmdChat(args) {
|
|
1523
1927
|
let cfg = await readConfig()
|
|
1524
1928
|
if (!cfg.apiKey && cfg.accessToken) {
|
|
1525
|
-
await ensureApiKey(cfg)
|
|
1929
|
+
await withSpinner("准备 API Key", () => ensureApiKey(cfg), { delayMs: 80 })
|
|
1526
1930
|
cfg = await readConfig()
|
|
1527
1931
|
}
|
|
1528
1932
|
|
|
1529
1933
|
let model = args.model || cfg.defaultModel
|
|
1530
1934
|
if (!model && cfg.accessToken) {
|
|
1531
|
-
const data = await getModels(cfg)
|
|
1935
|
+
const data = await withSpinner("选择默认在线模型", () => getModels(cfg), { delayMs: 80 })
|
|
1532
1936
|
model = pickDefaultModel(data.models)
|
|
1533
1937
|
}
|
|
1534
1938
|
if (!model) die("未选择模型。请使用 `laicode chat --model <model>`。")
|
|
@@ -1536,7 +1940,11 @@ async function cmdChat(args) {
|
|
|
1536
1940
|
const message = args.message || args.m || args._.slice(1).join(" ")
|
|
1537
1941
|
if (message) {
|
|
1538
1942
|
console.log(`model: ${model}`)
|
|
1539
|
-
await chatCompletion(cfg, [{ role: "user", content: message }], {
|
|
1943
|
+
await chatCompletion(cfg, [{ role: "user", content: message }], {
|
|
1944
|
+
model,
|
|
1945
|
+
stream: true,
|
|
1946
|
+
loadingText: "等待模型响应",
|
|
1947
|
+
})
|
|
1540
1948
|
await patchConfig({ defaultModel: model })
|
|
1541
1949
|
return
|
|
1542
1950
|
}
|
|
@@ -1548,8 +1956,12 @@ async function cmdChat(args) {
|
|
|
1548
1956
|
const input = await new Promise((resolve) => rl.question("> ", resolve))
|
|
1549
1957
|
if (!input || input.trim() === "/exit" || input.trim() === "/quit") break
|
|
1550
1958
|
messages.push({ role: "user", content: input })
|
|
1551
|
-
|
|
1552
|
-
|
|
1959
|
+
const res = await chatCompletion(cfg, messages, {
|
|
1960
|
+
model,
|
|
1961
|
+
stream: true,
|
|
1962
|
+
loadingText: "assistant 思考中",
|
|
1963
|
+
prefixOnFirstToken: "assistant: ",
|
|
1964
|
+
})
|
|
1553
1965
|
messages.push({ role: "assistant", content: res.content })
|
|
1554
1966
|
}
|
|
1555
1967
|
rl.close()
|
|
@@ -1559,12 +1971,12 @@ async function cmdChat(args) {
|
|
|
1559
1971
|
async function cmdBench(args) {
|
|
1560
1972
|
let cfg = await readConfig()
|
|
1561
1973
|
if (!cfg.apiKey && cfg.accessToken) {
|
|
1562
|
-
await ensureApiKey(cfg)
|
|
1974
|
+
await withSpinner("准备 API Key", () => ensureApiKey(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1563
1975
|
cfg = await readConfig()
|
|
1564
1976
|
}
|
|
1565
1977
|
let model = args.model || cfg.defaultModel
|
|
1566
1978
|
if (!model && cfg.accessToken) {
|
|
1567
|
-
const data = await getModels(cfg)
|
|
1979
|
+
const data = await withSpinner("选择默认在线模型", () => getModels(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1568
1980
|
model = pickDefaultModel(data.models)
|
|
1569
1981
|
}
|
|
1570
1982
|
if (!model) die("未选择模型。请使用 `laicode bench --model <model>`。")
|
|
@@ -1577,10 +1989,15 @@ async function cmdBench(args) {
|
|
|
1577
1989
|
for (let i = 0; i < count; i++) {
|
|
1578
1990
|
const label = `${i + 1}/${count}`
|
|
1579
1991
|
try {
|
|
1580
|
-
const res = await
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1992
|
+
const res = await withSpinner(
|
|
1993
|
+
`压测 ${label} · 等待首 token`,
|
|
1994
|
+
() =>
|
|
1995
|
+
chatCompletion(
|
|
1996
|
+
cfg,
|
|
1997
|
+
[{ role: "user", content: prompt }],
|
|
1998
|
+
{ model, stream: true, print: false, temperature: 0 }
|
|
1999
|
+
),
|
|
2000
|
+
{ enabled: !args.json, delayMs: 80 }
|
|
1584
2001
|
)
|
|
1585
2002
|
rows.push({ index: i + 1, ok: true, firstTokenMs: res.firstTokenMs, totalMs: res.totalMs })
|
|
1586
2003
|
if (!args.json) console.log(`${ui.ok("●")} ${label} 成功 首 token=${res.firstTokenMs}ms 总耗时=${res.totalMs}ms`)
|
|
@@ -1680,7 +2097,11 @@ async function cmdDoctor(args) {
|
|
|
1680
2097
|
record("config-file", "配置文件", true, fs.existsSync(CONFIG_FILE) ? CONFIG_FILE : "尚未创建")
|
|
1681
2098
|
|
|
1682
2099
|
try {
|
|
1683
|
-
const publicCatalog = await
|
|
2100
|
+
const publicCatalog = await withSpinner(
|
|
2101
|
+
"检查公开模型目录",
|
|
2102
|
+
() => siteJson(cfg, "/api/catalog", { method: "GET", timeoutMs: 10000 }),
|
|
2103
|
+
{ enabled: !args.json, delayMs: 80 }
|
|
2104
|
+
)
|
|
1684
2105
|
const count = publicCatalog.models?.length || 0
|
|
1685
2106
|
record("catalog", "公开模型目录", Boolean(count), `${count} 个模型`, { count })
|
|
1686
2107
|
} catch (err) {
|
|
@@ -1688,7 +2109,7 @@ async function cmdDoctor(args) {
|
|
|
1688
2109
|
}
|
|
1689
2110
|
|
|
1690
2111
|
try {
|
|
1691
|
-
const latest = await getNpmLatestVersion()
|
|
2112
|
+
const latest = await withSpinner("检查 npm 版本", () => getNpmLatestVersion(), { enabled: !args.json, delayMs: 80 })
|
|
1692
2113
|
if (!latest) {
|
|
1693
2114
|
record("version", "版本检查", true, `当前 ${VERSION},未取到 latest`, { current: VERSION, latest: null })
|
|
1694
2115
|
} else {
|
|
@@ -1719,7 +2140,7 @@ async function cmdDoctor(args) {
|
|
|
1719
2140
|
|
|
1720
2141
|
if (cfg.accessToken) {
|
|
1721
2142
|
try {
|
|
1722
|
-
const data = await bootstrap(cfg, true)
|
|
2143
|
+
const data = await withSpinner("验证 CLI 会话", () => bootstrap(cfg, true), { enabled: !args.json, delayMs: 80 })
|
|
1723
2144
|
record("session", "CLI 会话", true, data.user?.email || data.user?.name || "已授权", {
|
|
1724
2145
|
user: data.user || cfg.user || null,
|
|
1725
2146
|
})
|
|
@@ -1732,7 +2153,7 @@ async function cmdDoctor(args) {
|
|
|
1732
2153
|
|
|
1733
2154
|
if (!cfg.apiKey && cfg.accessToken) {
|
|
1734
2155
|
try {
|
|
1735
|
-
await ensureApiKey(cfg)
|
|
2156
|
+
await withSpinner("检查 API Key", () => ensureApiKey(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1736
2157
|
cfg = await readConfig()
|
|
1737
2158
|
record("api-key", "API Key", true, cfg.apiKeyName || "已配置", {
|
|
1738
2159
|
configured: Boolean(cfg.apiKey),
|
|
@@ -1754,14 +2175,19 @@ async function cmdDoctor(args) {
|
|
|
1754
2175
|
try {
|
|
1755
2176
|
let model = args.model || cfg.defaultModel
|
|
1756
2177
|
if (!model && cfg.accessToken) {
|
|
1757
|
-
const data = await getModels(cfg)
|
|
2178
|
+
const data = await withSpinner("选择诊断模型", () => getModels(cfg), { enabled: !args.json, delayMs: 80 })
|
|
1758
2179
|
model = pickDefaultModel(data.models)
|
|
1759
2180
|
}
|
|
1760
2181
|
if (!model) throw new Error("no model selected")
|
|
1761
|
-
const res = await
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2182
|
+
const res = await withSpinner(
|
|
2183
|
+
"执行真实模型调用",
|
|
2184
|
+
() =>
|
|
2185
|
+
chatCompletion(
|
|
2186
|
+
cfg,
|
|
2187
|
+
[{ role: "user", content: "Reply with OK." }],
|
|
2188
|
+
{ model, stream: true, print: false, temperature: 0 }
|
|
2189
|
+
),
|
|
2190
|
+
{ enabled: !args.json, delayMs: 80 }
|
|
1765
2191
|
)
|
|
1766
2192
|
record("chat-completion", "真实模型调用", true, `${model} 首 token=${res.firstTokenMs}ms 总耗时=${res.totalMs}ms`, {
|
|
1767
2193
|
model,
|
|
@@ -1820,7 +2246,9 @@ async function cmdLogout() {
|
|
|
1820
2246
|
const cfg = await readConfig()
|
|
1821
2247
|
if (cfg.accessToken) {
|
|
1822
2248
|
try {
|
|
1823
|
-
await authJson(cfg, "/api/laicode/session/revoke", { method: "POST" })
|
|
2249
|
+
await withSpinner("撤销远程会话", () => authJson(cfg, "/api/laicode/session/revoke", { method: "POST" }), {
|
|
2250
|
+
delayMs: 80,
|
|
2251
|
+
})
|
|
1824
2252
|
console.log("Remote session revoked.")
|
|
1825
2253
|
} catch (err) {
|
|
1826
2254
|
if (err.status !== 401) console.log(`Remote revoke failed: ${err.message}`)
|
|
@@ -1850,7 +2278,7 @@ function localStatusRows(cfg) {
|
|
|
1850
2278
|
}
|
|
1851
2279
|
|
|
1852
2280
|
async function chooseModel(cfg, title = "Choose Model") {
|
|
1853
|
-
const data = await getModels(cfg)
|
|
2281
|
+
const data = await withSpinner("拉取在线模型", () => getModels(cfg), { delayMs: 80 })
|
|
1854
2282
|
const online = (data.models || []).filter(modelOnline)
|
|
1855
2283
|
if (!online.length) {
|
|
1856
2284
|
console.log(ui.warn("没有找到在线模型。"))
|
|
@@ -1882,6 +2310,120 @@ async function menuModels() {
|
|
|
1882
2310
|
await pause()
|
|
1883
2311
|
}
|
|
1884
2312
|
|
|
2313
|
+
async function menuTools() {
|
|
2314
|
+
await cmdTools({ _: ["tools"] })
|
|
2315
|
+
await pause()
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
async function menuInit() {
|
|
2319
|
+
await cmdTools({ _: ["tools"] })
|
|
2320
|
+
console.log("")
|
|
2321
|
+
const tool = await question("接入工具 [codex]: ", "codex")
|
|
2322
|
+
await cmdInit({ _: ["init"], tool: tool || "codex" })
|
|
2323
|
+
const answer = await question("应用这个接入计划?[y/N]: ", "n")
|
|
2324
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2325
|
+
await cmdInit({ _: ["init"], tool: tool || "codex", apply: true })
|
|
2326
|
+
}
|
|
2327
|
+
await pause()
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
async function chooseAiTool(title = "选择 AI 工具") {
|
|
2331
|
+
const tools = detectTools()
|
|
2332
|
+
const items = tools.map((tool) => ({
|
|
2333
|
+
label: tool.name,
|
|
2334
|
+
description: `${tool.detected ? "已发现" : "未检测"} · ${tool.status}`,
|
|
2335
|
+
value: tool.id,
|
|
2336
|
+
}))
|
|
2337
|
+
items.push({ label: "返回", description: "", value: null })
|
|
2338
|
+
return selectMenu(title, items, { cancelValue: null })
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
async function maybeApplyToolCommand(action, tool) {
|
|
2342
|
+
await cmdTools({ _: ["tools", action], tool })
|
|
2343
|
+
const answer = await question(`执行 ${tool} ${action}?[y/N]: `, "n")
|
|
2344
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2345
|
+
await cmdTools({ _: ["tools", action], tool, apply: true })
|
|
2346
|
+
}
|
|
2347
|
+
await pause()
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
async function maybeApplyToolConfig(tool) {
|
|
2351
|
+
await cmdInit({ _: ["init"], tool })
|
|
2352
|
+
const answer = await question(`应用 ${tool} 接入配置?[y/N]: `, "n")
|
|
2353
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2354
|
+
await cmdInit({ _: ["init"], tool, apply: true })
|
|
2355
|
+
}
|
|
2356
|
+
await pause()
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
async function menuToolDetail(tool) {
|
|
2360
|
+
for (;;) {
|
|
2361
|
+
const action = await selectMenu(
|
|
2362
|
+
`管理 ${tool}`,
|
|
2363
|
+
[
|
|
2364
|
+
{ label: "预览接入配置", description: "只展示 diff/计划,不写入", value: "preview" },
|
|
2365
|
+
{ label: "应用接入配置", description: "备份后写入,可 rollback", value: "apply-config" },
|
|
2366
|
+
{ label: "安装工具", description: "预览安装命令,可确认执行", value: "install" },
|
|
2367
|
+
{ label: "卸载工具", description: "仅管理 Laicode 可控的 npm 全局安装", value: "uninstall" },
|
|
2368
|
+
{ label: "重装工具", description: "卸载后重新安装", value: "reinstall" },
|
|
2369
|
+
{ label: "查看工具矩阵", description: "回到总览表", value: "matrix" },
|
|
2370
|
+
{ label: "返回", description: "", value: null },
|
|
2371
|
+
],
|
|
2372
|
+
{ cancelValue: null }
|
|
2373
|
+
)
|
|
2374
|
+
if (!action) return
|
|
2375
|
+
if (action === "preview") {
|
|
2376
|
+
await cmdInit({ _: ["init"], tool })
|
|
2377
|
+
await pause()
|
|
2378
|
+
} else if (action === "apply-config") {
|
|
2379
|
+
await maybeApplyToolConfig(tool)
|
|
2380
|
+
} else if (action === "install" || action === "uninstall" || action === "reinstall") {
|
|
2381
|
+
await maybeApplyToolCommand(action, tool)
|
|
2382
|
+
} else if (action === "matrix") {
|
|
2383
|
+
await cmdTools({ _: ["tools"] })
|
|
2384
|
+
await pause()
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
async function menuToolAction(action) {
|
|
2390
|
+
const tool = await chooseAiTool(action === "configure" ? "选择要配置的工具" : "选择要维护的工具")
|
|
2391
|
+
if (!tool) return
|
|
2392
|
+
if (action === "configure") await maybeApplyToolConfig(tool)
|
|
2393
|
+
else await maybeApplyToolCommand(action, tool)
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
async function menuToolHub() {
|
|
2397
|
+
for (;;) {
|
|
2398
|
+
const action = await selectMenu(
|
|
2399
|
+
"AI 工具一键配置",
|
|
2400
|
+
[
|
|
2401
|
+
{ label: "体检全部工具", description: "扫描 Claude、Codex、VS Code、Cursor、OpenCode", value: "scan" },
|
|
2402
|
+
{ label: "选择工具管理", description: "进入单个工具的完整操作菜单", value: "manage" },
|
|
2403
|
+
{ label: "一键配置工具", description: "预览并应用 Lai.vc 接入配置", value: "configure" },
|
|
2404
|
+
{ label: "安装工具", description: "生成安装命令,可确认执行", value: "install" },
|
|
2405
|
+
{ label: "卸载工具", description: "卸载 Laicode 可控的安装", value: "uninstall" },
|
|
2406
|
+
{ label: "重装工具", description: "卸载并重新安装", value: "reinstall" },
|
|
2407
|
+
{ label: "回滚最近接入", description: "撤销最近一次 Laicode 写入", value: "rollback" },
|
|
2408
|
+
{ label: "返回主菜单", description: "", value: null },
|
|
2409
|
+
],
|
|
2410
|
+
{ cancelValue: null }
|
|
2411
|
+
)
|
|
2412
|
+
if (!action) return
|
|
2413
|
+
if (action === "scan") {
|
|
2414
|
+
await cmdTools({ _: ["tools"] })
|
|
2415
|
+
await pause()
|
|
2416
|
+
} else if (action === "manage") {
|
|
2417
|
+
const tool = await chooseAiTool()
|
|
2418
|
+
if (tool) await menuToolDetail(tool)
|
|
2419
|
+
} else if (action === "configure" || action === "install" || action === "uninstall" || action === "reinstall") {
|
|
2420
|
+
await menuToolAction(action)
|
|
2421
|
+
} else if (action === "rollback") {
|
|
2422
|
+
await menuRollback()
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
1885
2427
|
async function menuChat() {
|
|
1886
2428
|
let cfg = await readConfig()
|
|
1887
2429
|
if (!cfg.accessToken && !cfg.apiKey) {
|
|
@@ -1890,7 +2432,7 @@ async function menuChat() {
|
|
|
1890
2432
|
return
|
|
1891
2433
|
}
|
|
1892
2434
|
if (!cfg.apiKey && cfg.accessToken) {
|
|
1893
|
-
await ensureApiKey(cfg)
|
|
2435
|
+
await withSpinner("准备 API Key", () => ensureApiKey(cfg), { delayMs: 80 })
|
|
1894
2436
|
cfg = await readConfig()
|
|
1895
2437
|
}
|
|
1896
2438
|
const model = await chooseModel(cfg, "Chat Test")
|
|
@@ -1898,7 +2440,11 @@ async function menuChat() {
|
|
|
1898
2440
|
const prompt = await question("Prompt: ", "Say hello in one sentence.")
|
|
1899
2441
|
printHero("Streaming Response")
|
|
1900
2442
|
console.log(`${ui.dim("model")} ${model}\n`)
|
|
1901
|
-
await chatCompletion(cfg, [{ role: "user", content: prompt }], {
|
|
2443
|
+
await chatCompletion(cfg, [{ role: "user", content: prompt }], {
|
|
2444
|
+
model,
|
|
2445
|
+
stream: true,
|
|
2446
|
+
loadingText: "等待模型响应",
|
|
2447
|
+
})
|
|
1902
2448
|
await patchConfig({ defaultModel: model })
|
|
1903
2449
|
await pause()
|
|
1904
2450
|
}
|
|
@@ -1911,7 +2457,7 @@ async function menuBench() {
|
|
|
1911
2457
|
return
|
|
1912
2458
|
}
|
|
1913
2459
|
if (!cfg.apiKey && cfg.accessToken) {
|
|
1914
|
-
await ensureApiKey(cfg)
|
|
2460
|
+
await withSpinner("准备 API Key", () => ensureApiKey(cfg), { delayMs: 80 })
|
|
1915
2461
|
cfg = await readConfig()
|
|
1916
2462
|
}
|
|
1917
2463
|
const model = await chooseModel(cfg, "Benchmark")
|
|
@@ -1977,6 +2523,14 @@ async function menuLogout() {
|
|
|
1977
2523
|
await pause()
|
|
1978
2524
|
}
|
|
1979
2525
|
|
|
2526
|
+
async function menuRollback() {
|
|
2527
|
+
const answer = await question("回滚最近一次 Laicode 接入写入?[y/N]: ", "n")
|
|
2528
|
+
if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
2529
|
+
await cmdRollback({ _: ["rollback"] })
|
|
2530
|
+
}
|
|
2531
|
+
await pause()
|
|
2532
|
+
}
|
|
2533
|
+
|
|
1980
2534
|
async function cmdMenu(args = {}) {
|
|
1981
2535
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1982
2536
|
console.log(buildHelp())
|
|
@@ -1985,13 +2539,14 @@ async function cmdMenu(args = {}) {
|
|
|
1985
2539
|
|
|
1986
2540
|
for (;;) {
|
|
1987
2541
|
const cfg = await readConfig()
|
|
1988
|
-
const snapshot = await dashboardSnapshot(cfg)
|
|
2542
|
+
const snapshot = await withSpinner("同步驾驶舱", () => dashboardSnapshot(cfg), { delayMs: 160 })
|
|
1989
2543
|
printHero("Lai.vc 中转控制台")
|
|
1990
2544
|
printDashboard(cfg, snapshot)
|
|
1991
2545
|
console.log("")
|
|
1992
2546
|
const action = await selectMenu(
|
|
1993
2547
|
"选择任务",
|
|
1994
2548
|
[
|
|
2549
|
+
{ label: "AI 工具一键配置", description: "体检、安装、配置、重装、卸载、回滚", value: "tool-hub" },
|
|
1995
2550
|
{ label: "开始对话", description: "流式测试单个模型", value: "chat" },
|
|
1996
2551
|
{ label: "模型雷达", description: "在线模型、价格、多源、延迟", value: "models" },
|
|
1997
2552
|
{ label: "网关压测", description: "首 token、总耗时、成功率", value: "bench" },
|
|
@@ -2000,6 +2555,7 @@ async function cmdMenu(args = {}) {
|
|
|
2000
2555
|
{ label: "本地配置", description: "查看 ~/.laicode 状态", value: "config" },
|
|
2001
2556
|
{ label: "品牌预览", description: "终端字标和品牌色板", value: "brand" },
|
|
2002
2557
|
{ label: "重新登录", description: "浏览器设备授权", value: "login" },
|
|
2558
|
+
{ label: "回滚接入", description: "撤销最近一次配置写入", value: "rollback" },
|
|
2003
2559
|
{ label: "退出登录", description: "撤销会话并清理缓存", value: "logout" },
|
|
2004
2560
|
{ label: "退出", description: "", value: "exit" },
|
|
2005
2561
|
],
|
|
@@ -2007,7 +2563,10 @@ async function cmdMenu(args = {}) {
|
|
|
2007
2563
|
)
|
|
2008
2564
|
|
|
2009
2565
|
if (!action || action === "exit") return
|
|
2010
|
-
if (action === "
|
|
2566
|
+
if (action === "tool-hub") await menuToolHub()
|
|
2567
|
+
else if (action === "login") await menuLogin()
|
|
2568
|
+
else if (action === "tools") await menuTools()
|
|
2569
|
+
else if (action === "init") await menuInit()
|
|
2011
2570
|
else if (action === "models") await menuModels()
|
|
2012
2571
|
else if (action === "chat") await menuChat()
|
|
2013
2572
|
else if (action === "bench") await menuBench()
|
|
@@ -2018,6 +2577,7 @@ async function cmdMenu(args = {}) {
|
|
|
2018
2577
|
cmdBrand()
|
|
2019
2578
|
await pause()
|
|
2020
2579
|
}
|
|
2580
|
+
else if (action === "rollback") await menuRollback()
|
|
2021
2581
|
else if (action === "logout") await menuLogout()
|
|
2022
2582
|
}
|
|
2023
2583
|
}
|
|
@@ -2043,8 +2603,12 @@ async function main() {
|
|
|
2043
2603
|
try {
|
|
2044
2604
|
if (cmd === "login") return await cmdLogin(args)
|
|
2045
2605
|
if (cmd === "models") return await cmdModels(args)
|
|
2606
|
+
if (cmd === "tools") return await cmdTools(args)
|
|
2607
|
+
if (cmd === "init") return await cmdInit(args)
|
|
2608
|
+
if (cmd === "rollback") return await cmdRollback(args)
|
|
2046
2609
|
if (cmd === "keys") return await cmdKeys(args)
|
|
2047
2610
|
if (cmd === "config") return await cmdConfig(args)
|
|
2611
|
+
if (cmd === "credential") return await cmdCredential(args)
|
|
2048
2612
|
if (cmd === "chat") return await cmdChat(args)
|
|
2049
2613
|
if (cmd === "bench") return await cmdBench(args)
|
|
2050
2614
|
if (cmd === "doctor") return await cmdDoctor(args)
|