@simonyea/holysheep-cli 1.7.136 → 2.0.0

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 CHANGED
@@ -225,6 +225,7 @@ A: OpenClaw 需要 Node.js 20+,运行 `node --version` 确认版本后重试
225
225
 
226
226
  ## Changelog
227
227
 
228
+ - **v2.0.0** — 修复 `hs web` 的关键可用性问题:工具探测改为异步缓存,避免 WebUI 首屏请求卡死;workspace 对话上游请求增加超时保护,防止发送消息时接口悬挂
228
229
  - **v1.7.135** — Droid CLI 的 GPT-5.4 配置切回官方要求的 `provider=openai + https://api.holysheep.ai/v1`;同时服务端兼容桥接 `gpt-5.4` 的 `/responses` 请求到 `/v1/chat/completions`
229
230
  - **v1.7.134** — 修复并发配置/Worker 路径下 Droid CLI 的 GPT-5.4 BYOK 配置:GPT 走 `generic-chat-completion-api + https://api.holysheep.ai/v1`,避免误走 Anthropic `/v1/messages`
230
231
  - **v1.7.53** — 修复 `hs claude` 在绝对 URL 代理路径下未透传 `x-hs-node-proxied` 的问题,避免同一会话里部分 Claude 请求被后端误判为非可信代理请求并随机触发 403
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "1.7.136",
3
+ "version": "2.0.0",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
5
5
  "scripts": {
6
6
  "test": "node tests/droid.test.js && node tests/workspace-store.test.js"
@@ -2,7 +2,7 @@
2
2
  * 跨平台检测命令是否存在
3
3
  * Windows 用 where,Unix 用 which,兜底用 --version
4
4
  */
5
- const { execSync } = require('child_process')
5
+ const { exec, execSync } = require('child_process')
6
6
 
7
7
  function canRun(command, options = {}) {
8
8
  try {
@@ -34,4 +34,30 @@ function commandExists(cmd) {
34
34
  return canRun(`${cmd} --version`, { timeout: 3000 })
35
35
  }
36
36
 
37
- module.exports = { commandExists }
37
+ function canRunAsync(command, options = {}) {
38
+ return new Promise((resolve) => {
39
+ exec(command, { timeout: 3000, windowsHide: true, ...options }, (error) => {
40
+ resolve(!error)
41
+ })
42
+ })
43
+ }
44
+
45
+ async function commandExistsAsync(cmd) {
46
+ if (process.platform === 'win32') {
47
+ const variants = [cmd, `${cmd}.cmd`, `${cmd}.exe`, `${cmd}.bat`]
48
+ for (const variant of variants) {
49
+ if (await canRunAsync(`where ${variant}`)) return true
50
+ }
51
+
52
+ for (const variant of variants) {
53
+ if (await canRunAsync(`cmd /d /s /c "${variant} --version"`)) return true
54
+ }
55
+
56
+ return false
57
+ }
58
+
59
+ if (await canRunAsync(`which ${cmd}`)) return true
60
+ return canRunAsync(`${cmd} --version`)
61
+ }
62
+
63
+ module.exports = { commandExists, commandExistsAsync }
@@ -15,12 +15,15 @@ const path = require('path')
15
15
  const { execSync, spawn } = require('child_process')
16
16
  const { loadConfig, saveConfig, getApiKey, BASE_URL_ANTHROPIC, BASE_URL_OPENAI, BASE_URL_CLAUDE_RELAY, SHOP_URL, CONFIG_FILE } = require('../utils/config')
17
17
  const { removeEnvFromShell, writeEnvToShell, getShellRcFiles } = require('../utils/shell')
18
- const { commandExists } = require('../utils/which')
18
+ const { commandExistsAsync } = require('../utils/which')
19
19
  const TOOLS = require('../tools')
20
20
  const pkg = require('../../package.json')
21
21
  const workspaceStore = require('./workspace-store')
22
22
  const workspaceRuntime = require('./workspace-runtime')
23
23
 
24
+ const TOOL_CHECK_CACHE_TTL_MS = 10_000
25
+ const toolStateCache = new Map()
26
+
24
27
  // ── Helpers ──────────────────────────────────────────────────────────────────
25
28
 
26
29
  function maskKey(key) {
@@ -94,6 +97,52 @@ function parseBody(req) {
94
97
  })
95
98
  }
96
99
 
100
+ function getToolCommand(toolId) {
101
+ const cmds = {
102
+ 'claude-code': 'claude',
103
+ 'codex': 'codex',
104
+ 'droid': 'droid',
105
+ 'gemini-cli': 'gemini',
106
+ 'opencode': 'opencode',
107
+ 'openclaw': 'openclaw',
108
+ 'aider': 'aider',
109
+ 'env-config': null,
110
+ }
111
+ return cmds[toolId] ?? null
112
+ }
113
+
114
+ async function detectToolInstalled(tool) {
115
+ if (tool.id === 'env-config') return true
116
+
117
+ const now = Date.now()
118
+ const cached = toolStateCache.get(tool.id)
119
+ if (cached && now - cached.checkedAt < TOOL_CHECK_CACHE_TTL_MS) {
120
+ return cached.installed
121
+ }
122
+
123
+ const command = getToolCommand(tool.id)
124
+ const installed = command ? await commandExistsAsync(command) : false
125
+ toolStateCache.set(tool.id, { installed, checkedAt: now })
126
+ return installed
127
+ }
128
+
129
+ async function buildToolSummary(tool) {
130
+ const installed = await detectToolInstalled(tool)
131
+ return {
132
+ id: tool.id,
133
+ name: tool.name,
134
+ installed,
135
+ configured: installed ? (tool.isConfigured?.() || false) : false,
136
+ version: installed ? await getVersionAsync(tool) : null,
137
+ installCmd: tool.installCmd,
138
+ hint: tool.hint || null,
139
+ launchCmd: tool.launchCmd || null,
140
+ canAutoInstall: !!AUTO_INSTALL[tool.id],
141
+ canUpgrade: !!UPGRADABLE_TOOLS.find((item) => item.id === tool.id),
142
+ npmPkg: UPGRADABLE_TOOLS.find((item) => item.id === tool.id)?.npmPkg || null,
143
+ }
144
+ }
145
+
97
146
  function json(res, data, status = 200) {
98
147
  res.writeHead(status, { 'Content-Type': 'application/json' })
99
148
  res.end(JSON.stringify(data))
@@ -252,19 +301,7 @@ async function handleDoctor(_req, res) {
252
301
  const nodeMajor = parseInt(process.version.slice(1), 10)
253
302
 
254
303
  // Tools
255
- const tools = TOOLS.map(t => {
256
- const installed = t.checkInstalled()
257
- return {
258
- id: t.id,
259
- name: t.name,
260
- installed,
261
- configured: installed ? (t.isConfigured?.() || false) : false,
262
- version: installed ? getVersion(t) : null,
263
- installCmd: t.installCmd,
264
- hint: t.hint || null,
265
- launchCmd: t.launchCmd || null,
266
- }
267
- })
304
+ const tools = await Promise.all(TOOLS.map((tool) => buildToolSummary(tool)))
268
305
 
269
306
  // Connectivity
270
307
  let connectivity = { ok: false, modelCount: 0 }
@@ -309,22 +346,7 @@ function isClaudeProxyRunning() {
309
346
  }
310
347
 
311
348
  async function handleTools(_req, res) {
312
- const tools = await Promise.all(TOOLS.map(async t => {
313
- const installed = t.checkInstalled()
314
- return {
315
- id: t.id,
316
- name: t.name,
317
- installed,
318
- configured: installed ? (t.isConfigured?.() || false) : false,
319
- version: installed ? await getVersionAsync(t) : null,
320
- installCmd: t.installCmd,
321
- hint: t.hint || null,
322
- launchCmd: t.launchCmd || null,
323
- canAutoInstall: !!AUTO_INSTALL[t.id],
324
- canUpgrade: !!UPGRADABLE_TOOLS.find(u => u.id === t.id),
325
- npmPkg: UPGRADABLE_TOOLS.find(u => u.id === t.id)?.npmPkg || null,
326
- }
327
- }))
349
+ const tools = await Promise.all(TOOLS.map((tool) => buildToolSummary(tool)))
328
350
 
329
351
  // 追加 claude-proxy 虚拟工具
330
352
  const proxyState = isClaudeProxyRunning()
@@ -46,18 +46,31 @@ function buildSystemPrompt(toolId) {
46
46
  }
47
47
 
48
48
  async function requestCompletion(messages, config) {
49
- const response = await fetch(`${config.baseUrl}/chat/completions`, {
50
- method: 'POST',
51
- headers: {
52
- Authorization: `Bearer ${config.apiKey}`,
53
- 'Content-Type': 'application/json',
54
- },
55
- body: JSON.stringify({
56
- model: config.model,
57
- temperature: 0.2,
58
- messages,
59
- }),
60
- })
49
+ const controller = new AbortController()
50
+ const timeout = setTimeout(() => controller.abort(), 20_000)
51
+ let response
52
+ try {
53
+ response = await fetch(`${config.baseUrl}/chat/completions`, {
54
+ method: 'POST',
55
+ headers: {
56
+ Authorization: `Bearer ${config.apiKey}`,
57
+ 'Content-Type': 'application/json',
58
+ },
59
+ body: JSON.stringify({
60
+ model: config.model,
61
+ temperature: 0.2,
62
+ messages,
63
+ }),
64
+ signal: controller.signal,
65
+ })
66
+ } catch (error) {
67
+ if (error.name === 'AbortError') {
68
+ throw new Error('HolySheep API request timed out')
69
+ }
70
+ throw error
71
+ } finally {
72
+ clearTimeout(timeout)
73
+ }
61
74
 
62
75
  const payload = await response.json().catch(() => null)
63
76
  if (!response.ok) {