@simonyea/holysheep-cli 1.7.54 → 1.7.56

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.
@@ -9,7 +9,7 @@ const fs = require('fs')
9
9
  const path = require('path')
10
10
  const { execSync, spawn } = require('child_process')
11
11
  const { loadConfig, saveConfig, getApiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, BASE_URL_CLAUDE_RELAY, SHOP_URL, CONFIG_FILE } = require('../utils/config')
12
- const { removeEnvFromShell } = require('../utils/shell')
12
+ const { removeEnvFromShell, writeEnvToShell, getShellRcFiles } = require('../utils/shell')
13
13
  const { commandExists } = require('../utils/which')
14
14
  const TOOLS = require('../tools')
15
15
  const pkg = require('../../package.json')
@@ -441,6 +441,122 @@ async function handleToolInstall(req, res) {
441
441
  res.end()
442
442
  }
443
443
 
444
+ // ── Single-tool configure (SSE) ──────────────────────────────────────────────
445
+
446
+ async function handleToolConfigure(req, res) {
447
+ const body = await parseBody(req)
448
+ const { toolId } = body
449
+ const apiKey = getApiKey()
450
+ if (!apiKey) return json(res, { error: '未登录' }, 401)
451
+
452
+ const tool = TOOLS.find(t => t.id === toolId)
453
+ if (!tool) return json(res, { error: '未知工具' }, 400)
454
+
455
+ sseStart(res)
456
+
457
+ if (!tool.checkInstalled()) {
458
+ sseEmit(res, { type: 'error', message: `${tool.name} 未安装` })
459
+ sseEmit(res, { type: 'done', success: false })
460
+ return res.end()
461
+ }
462
+
463
+ const allModelIds = [
464
+ 'gpt-5.4', 'gpt-5.3-codex-spark',
465
+ 'claude-sonnet-4-6', 'claude-sonnet-4-6[1m]',
466
+ 'claude-opus-4-6', 'claude-opus-4-6[1m]',
467
+ 'MiniMax-M2.7-highspeed', 'claude-haiku-4-5',
468
+ ]
469
+ const primaryModel = toolId === 'openclaw' ? 'claude-sonnet-4-6[1m]' : 'claude-sonnet-4-6'
470
+
471
+ sseEmit(res, { type: 'progress', message: `正在配置 ${tool.name}...` })
472
+
473
+ // 劫持 console.log/warn,将 tool.configure() 内部的进度消息转发为 SSE 事件
474
+ const origLog = console.log
475
+ const origWarn = console.warn
476
+ console.log = (...args) => {
477
+ const msg = args.map(a => typeof a === 'string' ? a : String(a)).join(' ')
478
+ const clean = msg.replace(/\x1b\[[0-9;]*m/g, '').trim()
479
+ if (clean) sseEmit(res, { type: 'progress', message: clean })
480
+ }
481
+ console.warn = console.log
482
+
483
+ try {
484
+ const result = tool.configure(apiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, primaryModel, allModelIds)
485
+
486
+ if (result.envVars && Object.keys(result.envVars).length > 0) {
487
+ writeEnvToShell(result.envVars)
488
+ sseEmit(res, { type: 'progress', message: '已写入环境变量到 shell 配置' })
489
+ }
490
+
491
+ try { removeEnvFromShell(['ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL']) } catch {}
492
+
493
+ if (result.manual) {
494
+ sseEmit(res, { type: 'result', status: 'manual', message: `${tool.name} 需要手动完成配置`, steps: result.steps })
495
+ } else if (result.warning) {
496
+ sseEmit(res, { type: 'result', status: 'warning', message: result.warning, file: result.file })
497
+ } else {
498
+ sseEmit(res, { type: 'result', status: 'ok', message: `${tool.name} 配置成功`, file: result.file, hot: result.hot })
499
+ }
500
+
501
+ sseEmit(res, { type: 'done', success: true, file: result.file, hot: result.hot, dashboardUrl: result.dashboardUrl || null })
502
+ } catch (e) {
503
+ sseEmit(res, { type: 'error', message: e.message })
504
+ sseEmit(res, { type: 'done', success: false })
505
+ } finally {
506
+ console.log = origLog
507
+ console.warn = origWarn
508
+ }
509
+ res.end()
510
+ }
511
+
512
+ // ── Single-tool reset ────────────────────────────────────────────────────────
513
+
514
+ async function handleToolReset(req, res) {
515
+ const body = await parseBody(req)
516
+ const { toolId } = body
517
+ const tool = TOOLS.find(t => t.id === toolId)
518
+ if (!tool) return json(res, { error: '未知工具' }, 400)
519
+ try {
520
+ tool.reset()
521
+ json(res, { success: true })
522
+ } catch (e) {
523
+ json(res, { success: false, error: e.message }, 500)
524
+ }
525
+ }
526
+
527
+ // ── Environment variables ────────────────────────────────────────────────────
528
+
529
+ const HS_ENV_KEYS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL']
530
+ const MARKER_START = '# >>> holysheep-cli managed >>>'
531
+
532
+ function handleEnv(_req, res) {
533
+ const vars = {}
534
+ for (const k of HS_ENV_KEYS) {
535
+ const v = process.env[k]
536
+ vars[k] = v ? (k.includes('KEY') || k.includes('TOKEN') ? maskKey(v) : v) : null
537
+ }
538
+
539
+ const rcFiles = []
540
+ try {
541
+ for (const f of getShellRcFiles()) {
542
+ let has = false
543
+ try { has = fs.readFileSync(f, 'utf8').includes(MARKER_START) } catch {}
544
+ rcFiles.push({ path: f.replace(require('os').homedir(), '~'), hasManagedBlock: has })
545
+ }
546
+ } catch {}
547
+
548
+ json(res, { vars, rcFiles })
549
+ }
550
+
551
+ function handleEnvClean(_req, res) {
552
+ try {
553
+ const cleaned = removeEnvFromShell()
554
+ json(res, { success: true, cleaned })
555
+ } catch (e) {
556
+ json(res, { success: false, error: e.message }, 500)
557
+ }
558
+ }
559
+
444
560
  // ── Models list ──────────────────────────────────────────────────────────────
445
561
 
446
562
  function handleModels(_req, res) {
@@ -448,7 +564,9 @@ function handleModels(_req, res) {
448
564
  { id: 'gpt-5.4', label: 'GPT 5.4', desc: '通用编码' },
449
565
  { id: 'gpt-5.3-codex-spark', label: 'GPT 5.3 Codex Spark', desc: '编码' },
450
566
  { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: '均衡推荐' },
567
+ { id: 'claude-sonnet-4-6[1m]', label: 'Sonnet 4.6 (1M)', desc: '均衡推荐·100万上下文' },
451
568
  { id: 'claude-opus-4-6', label: 'Opus 4.6', desc: '强力旗舰' },
569
+ { id: 'claude-opus-4-6[1m]', label: 'Opus 4.6 (1M)', desc: '强力旗舰·100万上下文' },
452
570
  { id: 'MiniMax-M2.7-highspeed', label: 'MiniMax M2.7', desc: '高速经济版' },
453
571
  { id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: '轻快便宜' },
454
572
  ])
@@ -478,15 +596,16 @@ async function handleRequest(req, res) {
478
596
  if (route === '/api/status' && req.method === 'GET') return await handleStatus(req, res)
479
597
  if (route === '/api/login' && req.method === 'POST') return await handleLogin(req, res)
480
598
  if (route === '/api/logout' && req.method === 'POST') return await handleLogout(req, res)
481
- if (route === '/api/whoami' && req.method === 'GET') return await handleWhoami(req, res)
482
599
  if (route === '/api/balance' && req.method === 'GET') return await handleBalance(req, res)
483
600
  if (route === '/api/doctor' && req.method === 'GET') return await handleDoctor(req, res)
484
601
  if (route === '/api/tools' && req.method === 'GET') return await handleTools(req, res)
485
602
  if (route === '/api/models' && req.method === 'GET') return await handleModels(req, res)
486
- if (route === '/api/setup' && req.method === 'POST') return await handleSetup(req, res)
487
- if (route === '/api/reset' && req.method === 'POST') return await handleReset(req, res)
488
603
  if (route === '/api/upgrade' && req.method === 'POST') return await handleUpgrade(req, res)
489
- if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
604
+ if (route === '/api/tool/install' && req.method === 'POST') return await handleToolInstall(req, res)
605
+ if (route === '/api/tool/configure' && req.method === 'POST') return await handleToolConfigure(req, res)
606
+ if (route === '/api/tool/reset' && req.method === 'POST') return await handleToolReset(req, res)
607
+ if (route === '/api/env' && req.method === 'GET') return handleEnv(req, res)
608
+ if (route === '/api/env/clean' && req.method === 'POST') return handleEnvClean(req, res)
490
609
 
491
610
  res.writeHead(404)
492
611
  res.end('Not Found')
@@ -498,9 +617,25 @@ async function handleRequest(req, res) {
498
617
  }
499
618
 
500
619
  function startServer(port) {
501
- const server = http.createServer(handleRequest)
502
- server.listen(port, '127.0.0.1')
503
- return server
620
+ return new Promise((resolve, reject) => {
621
+ const server = http.createServer(handleRequest)
622
+ server.on('error', (err) => {
623
+ if (err.code === 'EADDRINUSE') {
624
+ // Try to kill stale process and retry once
625
+ try {
626
+ execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' })
627
+ } catch {}
628
+ setTimeout(() => {
629
+ const retry = http.createServer(handleRequest)
630
+ retry.on('error', (err2) => reject(err2))
631
+ retry.listen(port, '127.0.0.1', () => resolve(retry))
632
+ }, 500)
633
+ } else {
634
+ reject(err)
635
+ }
636
+ })
637
+ server.listen(port, '127.0.0.1', () => resolve(server))
638
+ })
504
639
  }
505
640
 
506
641
  module.exports = { startServer }