@simonyea/holysheep-cli 1.1.0 → 1.1.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 +2 -2
- package/package.json +1 -1
- package/src/commands/login.js +211 -0
- package/src/commands/setup.js +2 -1
- package/src/index.js +30 -1
- package/src/tools/aider.js +3 -2
- package/src/tools/continue.js +113 -48
- package/src/tools/opencode.js +66 -28
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Instead of manually editing config files and environment variables for each tool
|
|
|
40
40
|
| [Codex CLI](https://github.com/openai/codex) | `npm i -g @openai/codex` | `~/.codex/config.toml` | ✅ Auto |
|
|
41
41
|
| [Aider](https://aider.chat) | `pip install aider-install && aider-install` | `~/.aider.conf.yml` | ✅ Auto |
|
|
42
42
|
| [Continue.dev](https://continue.dev) | VS Code marketplace | `~/.continue/config.yaml` | ✅ Auto |
|
|
43
|
-
| [OpenCode](https://
|
|
43
|
+
| [OpenCode](https://opencode.ai) | `brew install anomalyco/tap/opencode` | `~/.config/opencode/opencode.json` | ✅ Auto |
|
|
44
44
|
| [OpenClaw](https://github.com/iOfficeAI/AionUi) | Download from website | `~/.openclaw/settings.json` | ✅ Auto |
|
|
45
45
|
| [Cursor](https://cursor.sh) | Download from website | GUI only (encrypted storage) | ⚠️ Manual |
|
|
46
46
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `npm i -g @google/gemini-cli` | Google protocol only | ❌ Not supported |
|
|
@@ -134,7 +134,7 @@ HolySheep 是面向中国开发者的 Claude/GPT/Gemini 官方 API 中转服务
|
|
|
134
134
|
| [Codex CLI](https://github.com/openai/codex) | `npm i -g @openai/codex` | `~/.codex/config.toml` | ✅ 自动 |
|
|
135
135
|
| [Aider](https://aider.chat) | `pip install aider-install && aider-install` | `~/.aider.conf.yml` | ✅ 自动 |
|
|
136
136
|
| [Continue.dev](https://continue.dev) | VS Code 插件市场 | `~/.continue/config.yaml` | ✅ 自动 |
|
|
137
|
-
| [OpenCode](https://
|
|
137
|
+
| [OpenCode](https://opencode.ai) | `brew install anomalyco/tap/opencode` | `~/.config/opencode/opencode.json` | ✅ 自动 |
|
|
138
138
|
| [OpenClaw](https://github.com/iOfficeAI/AionUi) | 官网下载 | `~/.openclaw/settings.json` | ✅ 自动 |
|
|
139
139
|
| [Cursor](https://cursor.sh) | 官网下载 | GUI 手动配置(加密存储) | ⚠️ 手动 |
|
|
140
140
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `npm i -g @google/gemini-cli` | 仅支持 Google 官方协议 | ❌ 不支持 |
|
package/package.json
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hs login — 登录并保存 API Key
|
|
3
|
+
* hs logout — 清除本地 API Key
|
|
4
|
+
* hs whoami — 显示当前登录状态
|
|
5
|
+
*/
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const inquirer = require('inquirer')
|
|
9
|
+
const chalk = require('chalk')
|
|
10
|
+
const ora = require('ora')
|
|
11
|
+
const fetch = require('node-fetch')
|
|
12
|
+
const { execSync } = require('child_process')
|
|
13
|
+
const { loadConfig, saveConfig, getApiKey, BASE_URL_OPENAI, SHOP_URL, CONFIG_FILE } = require('../utils/config')
|
|
14
|
+
const fs = require('fs')
|
|
15
|
+
|
|
16
|
+
const MODELS_URL = `${BASE_URL_OPENAI}/models`
|
|
17
|
+
|
|
18
|
+
function maskKey(key) {
|
|
19
|
+
if (!key || key.length < 8) return '****'
|
|
20
|
+
return key.slice(0, 8) + '...' + key.slice(-4)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 调用 /v1/models 验证 API Key 是否有效
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
async function validateApiKey(apiKey) {
|
|
28
|
+
const res = await fetch(MODELS_URL, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${apiKey}`,
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
timeout: 10000,
|
|
35
|
+
})
|
|
36
|
+
return res.status === 200
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── login ────────────────────────────────────────────────────────────────────
|
|
40
|
+
async function login() {
|
|
41
|
+
console.log()
|
|
42
|
+
console.log(chalk.bold('🐑 HolySheep — 登录'))
|
|
43
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
44
|
+
console.log()
|
|
45
|
+
|
|
46
|
+
// 检查是否已登录
|
|
47
|
+
const existing = getApiKey()
|
|
48
|
+
if (existing) {
|
|
49
|
+
console.log(`${chalk.green('✓')} 已登录,当前 API Key: ${chalk.cyan(maskKey(existing))}`)
|
|
50
|
+
const { relogin } = await inquirer.prompt([{
|
|
51
|
+
type: 'confirm',
|
|
52
|
+
name: 'relogin',
|
|
53
|
+
message: '是否使用新的 API Key 重新登录?',
|
|
54
|
+
default: false,
|
|
55
|
+
}])
|
|
56
|
+
if (!relogin) {
|
|
57
|
+
console.log(chalk.gray('保持当前登录,退出。'))
|
|
58
|
+
console.log()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
console.log()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 提示用户获取 key 的方式
|
|
65
|
+
console.log(chalk.cyan('获取 API Key 的方式:'))
|
|
66
|
+
console.log(` ${chalk.bold('a)')} 输入已有的 API Key (cr_xxx)`)
|
|
67
|
+
console.log(` ${chalk.bold('b)')} 打开浏览器前往 ${chalk.cyan(SHOP_URL)} 注册`)
|
|
68
|
+
console.log()
|
|
69
|
+
|
|
70
|
+
const { choice } = await inquirer.prompt([{
|
|
71
|
+
type: 'list',
|
|
72
|
+
name: 'choice',
|
|
73
|
+
message: '请选择:',
|
|
74
|
+
choices: [
|
|
75
|
+
{ name: '输入已有的 API Key', value: 'input' },
|
|
76
|
+
{ name: `打开浏览器注册 (${SHOP_URL})`, value: 'browser' },
|
|
77
|
+
],
|
|
78
|
+
}])
|
|
79
|
+
|
|
80
|
+
if (choice === 'browser') {
|
|
81
|
+
console.log(chalk.gray(`\n正在打开浏览器: ${SHOP_URL}`))
|
|
82
|
+
try {
|
|
83
|
+
const platform = process.platform
|
|
84
|
+
if (platform === 'darwin') execSync(`open "${SHOP_URL}"`)
|
|
85
|
+
else if (platform === 'win32') execSync(`start "" "${SHOP_URL}"`)
|
|
86
|
+
else execSync(`xdg-open "${SHOP_URL}"`)
|
|
87
|
+
} catch {
|
|
88
|
+
console.log(chalk.yellow(`无法自动打开浏览器,请手动访问: ${SHOP_URL}`))
|
|
89
|
+
}
|
|
90
|
+
console.log()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 输入 API Key
|
|
94
|
+
const { apiKey } = await inquirer.prompt([{
|
|
95
|
+
type: 'password',
|
|
96
|
+
name: 'apiKey',
|
|
97
|
+
message: '请输入 API Key (cr_xxx):',
|
|
98
|
+
validate: v => {
|
|
99
|
+
if (!v || !v.trim()) return '请输入 API Key'
|
|
100
|
+
if (!v.trim().startsWith('cr_')) return '请输入以 cr_ 开头的 API Key'
|
|
101
|
+
return true
|
|
102
|
+
},
|
|
103
|
+
}])
|
|
104
|
+
|
|
105
|
+
const key = apiKey.trim()
|
|
106
|
+
|
|
107
|
+
// 验证 API Key
|
|
108
|
+
const spinner = ora('正在验证 API Key...').start()
|
|
109
|
+
try {
|
|
110
|
+
const valid = await validateApiKey(key)
|
|
111
|
+
if (!valid) {
|
|
112
|
+
spinner.fail(chalk.red('API Key 无效,请检查后重试'))
|
|
113
|
+
console.log(chalk.gray(` 前往获取有效 Key: ${SHOP_URL}`))
|
|
114
|
+
console.log()
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
spinner.fail(`验证失败: ${e.message}`)
|
|
119
|
+
console.log(chalk.yellow(' 网络异常,请检查网络连接后重试'))
|
|
120
|
+
console.log()
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 保存
|
|
125
|
+
saveConfig({ apiKey: key, savedAt: new Date().toISOString() })
|
|
126
|
+
spinner.succeed(chalk.green('API Key 验证成功并已保存!'))
|
|
127
|
+
|
|
128
|
+
console.log()
|
|
129
|
+
console.log(` Key: ${chalk.cyan(maskKey(key))}`)
|
|
130
|
+
console.log(` 配置文件: ${chalk.gray(CONFIG_FILE)}`)
|
|
131
|
+
console.log()
|
|
132
|
+
console.log(chalk.bold('接下来:'))
|
|
133
|
+
console.log(` 运行 ${chalk.cyan('hs setup')} 一键配置 AI 工具`)
|
|
134
|
+
console.log(` 运行 ${chalk.cyan('hs whoami')} 查看登录状态`)
|
|
135
|
+
console.log()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── logout ───────────────────────────────────────────────────────────────────
|
|
139
|
+
async function logout() {
|
|
140
|
+
console.log()
|
|
141
|
+
const existing = getApiKey()
|
|
142
|
+
if (!existing) {
|
|
143
|
+
console.log(chalk.yellow('当前未登录(无本地 API Key)'))
|
|
144
|
+
console.log()
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { confirm } = await inquirer.prompt([{
|
|
149
|
+
type: 'confirm',
|
|
150
|
+
name: 'confirm',
|
|
151
|
+
message: `确认退出登录?将删除本地 API Key (${maskKey(existing)})`,
|
|
152
|
+
default: false,
|
|
153
|
+
}])
|
|
154
|
+
|
|
155
|
+
if (!confirm) {
|
|
156
|
+
console.log(chalk.gray('取消,保持当前登录。'))
|
|
157
|
+
console.log()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const config = loadConfig()
|
|
163
|
+
delete config.apiKey
|
|
164
|
+
delete config.savedAt
|
|
165
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
166
|
+
console.log(chalk.green('✓ 已退出登录,本地 API Key 已清除'))
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.log(chalk.red(`退出失败: ${e.message}`))
|
|
169
|
+
}
|
|
170
|
+
console.log()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── whoami ───────────────────────────────────────────────────────────────────
|
|
174
|
+
async function whoami() {
|
|
175
|
+
console.log()
|
|
176
|
+
console.log(chalk.bold('🐑 HolySheep — 登录状态'))
|
|
177
|
+
console.log(chalk.gray('━'.repeat(50)))
|
|
178
|
+
console.log()
|
|
179
|
+
|
|
180
|
+
const apiKey = getApiKey()
|
|
181
|
+
if (!apiKey) {
|
|
182
|
+
console.log(chalk.yellow('未登录 — 本地无 API Key'))
|
|
183
|
+
console.log(chalk.gray(`运行 ${chalk.cyan('hs login')} 登录`))
|
|
184
|
+
console.log()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`状态: ${chalk.green('● 已登录')}`)
|
|
189
|
+
console.log(`Key: ${chalk.cyan(maskKey(apiKey))}`)
|
|
190
|
+
|
|
191
|
+
const config = loadConfig()
|
|
192
|
+
if (config.savedAt) {
|
|
193
|
+
console.log(`保存时间: ${chalk.gray(new Date(config.savedAt).toLocaleString())}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 验证 key 是否仍然有效
|
|
197
|
+
const spinner = ora('验证 Key 有效性...').start()
|
|
198
|
+
try {
|
|
199
|
+
const valid = await validateApiKey(apiKey)
|
|
200
|
+
if (valid) {
|
|
201
|
+
spinner.succeed(chalk.green('Key 有效'))
|
|
202
|
+
} else {
|
|
203
|
+
spinner.fail(chalk.red('Key 已失效,请重新登录 (hs login)'))
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
spinner.warn(`无法验证(网络异常): ${e.message}`)
|
|
207
|
+
}
|
|
208
|
+
console.log()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { login, logout, whoami }
|
package/src/commands/setup.js
CHANGED
|
@@ -74,7 +74,8 @@ async function setup(options) {
|
|
|
74
74
|
|
|
75
75
|
if (!apiKey) {
|
|
76
76
|
console.log(chalk.yellow('需要 API Key 才能配置工具。'))
|
|
77
|
-
console.log(chalk.cyan(`还没有账号?前往注册:${SHOP_URL}
|
|
77
|
+
console.log(chalk.cyan(`还没有账号?前往注册:${SHOP_URL}`))
|
|
78
|
+
console.log(chalk.gray(`提示:可先运行 ${chalk.cyan('hs login')} 登录并保存 Key,之后 setup 将自动读取。\n`))
|
|
78
79
|
|
|
79
80
|
const { key } = await inquirer.prompt([{
|
|
80
81
|
type: 'password',
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,33 @@ program
|
|
|
23
23
|
支持工具: Claude Code · Codex · Gemini CLI · OpenCode · OpenClaw · Aider · Cursor · Continue
|
|
24
24
|
`)
|
|
25
25
|
|
|
26
|
+
// ── login ────────────────────────────────────────────────────────────────────
|
|
27
|
+
program
|
|
28
|
+
.command('login')
|
|
29
|
+
.description('登录 HolySheep,保存 API Key 到本地')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const { login } = require('./commands/login')
|
|
32
|
+
await login()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// ── logout ───────────────────────────────────────────────────────────────────
|
|
36
|
+
program
|
|
37
|
+
.command('logout')
|
|
38
|
+
.description('退出登录,清除本地 API Key')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
const { logout } = require('./commands/login')
|
|
41
|
+
await logout()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// ── whoami ───────────────────────────────────────────────────────────────────
|
|
45
|
+
program
|
|
46
|
+
.command('whoami')
|
|
47
|
+
.description('显示当前登录状态')
|
|
48
|
+
.action(async () => {
|
|
49
|
+
const { whoami } = require('./commands/login')
|
|
50
|
+
await whoami()
|
|
51
|
+
})
|
|
52
|
+
|
|
26
53
|
// ── setup ────────────────────────────────────────────────────────────────────
|
|
27
54
|
program
|
|
28
55
|
.command('setup')
|
|
@@ -86,12 +113,14 @@ program
|
|
|
86
113
|
printBanner()
|
|
87
114
|
console.log()
|
|
88
115
|
console.log(chalk.cyan('快速开始:'))
|
|
116
|
+
console.log(` ${chalk.bold('hs login')} 登录并保存 API Key`)
|
|
89
117
|
console.log(` ${chalk.bold('hs setup')} 一键配置所有 AI 工具`)
|
|
118
|
+
console.log(` ${chalk.bold('hs whoami')} 查看当前登录状态`)
|
|
90
119
|
console.log(` ${chalk.bold('hs doctor')} 检查配置状态`)
|
|
91
120
|
console.log(` ${chalk.bold('hs balance')} 查看账户余额`)
|
|
92
121
|
console.log(` ${chalk.bold('hs tools')} 查看支持的工具列表`)
|
|
93
122
|
console.log()
|
|
94
|
-
console.log(chalk.gray(`注册账号: https://
|
|
123
|
+
console.log(chalk.gray(`注册账号: https://holysheep.ai`))
|
|
95
124
|
console.log()
|
|
96
125
|
})
|
|
97
126
|
|
package/src/tools/aider.js
CHANGED
|
@@ -44,10 +44,11 @@ module.exports = {
|
|
|
44
44
|
const content = readConfig()
|
|
45
45
|
return content.includes('holysheep')
|
|
46
46
|
},
|
|
47
|
-
configure(apiKey, baseUrlOpenAI) {
|
|
47
|
+
configure(apiKey, _baseUrlAnthropicNoV1, baseUrlOpenAI) {
|
|
48
48
|
let content = readConfig()
|
|
49
49
|
content = removeHsBlock(content)
|
|
50
|
-
// Aider 用 openai-api-base
|
|
50
|
+
// Aider 用 openai-api-base(OpenAI 兼容格式,带 /v1)
|
|
51
|
+
// model 格式: openai/<model-name> 表示使用 OpenAI 兼容接口
|
|
51
52
|
const block = `
|
|
52
53
|
# holysheep-cli managed — https://shop.holysheep.ai
|
|
53
54
|
openai-api-key: ${apiKey}
|
package/src/tools/continue.js
CHANGED
|
@@ -1,79 +1,144 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Continue.dev 适配器 (VS Code / JetBrains 插件)
|
|
3
|
-
* 配置文件: ~/.continue/config.json
|
|
4
3
|
*
|
|
5
|
-
* Continue
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
4
|
+
* ⚠️ 重要:Continue 新版本已将配置文件从 config.json 迁移到 config.yaml (YAML格式)
|
|
5
|
+
* 配置文件: ~/.continue/config.yaml (优先) 或 ~/.continue/config.json (旧版兼容)
|
|
6
|
+
*
|
|
7
|
+
* YAML 格式示例:
|
|
8
|
+
* models:
|
|
9
|
+
* - name: HolySheep Claude Sonnet
|
|
10
|
+
* provider: openai
|
|
11
|
+
* model: claude-sonnet-4-5
|
|
12
|
+
* apiKey: cr_xxx
|
|
13
|
+
* apiBase: https://api.holysheep.ai/v1
|
|
14
|
+
*
|
|
15
|
+
* Continue 支持 OpenAI 兼容 provider,可接入任何兼容端点
|
|
16
|
+
* 官方文档: https://docs.continue.dev
|
|
15
17
|
*/
|
|
16
18
|
const fs = require('fs')
|
|
17
19
|
const path = require('path')
|
|
18
20
|
const os = require('os')
|
|
19
21
|
|
|
20
|
-
const
|
|
22
|
+
const CONTINUE_DIR = path.join(os.homedir(), '.continue')
|
|
23
|
+
const CONFIG_YAML = path.join(CONTINUE_DIR, 'config.yaml')
|
|
24
|
+
const CONFIG_JSON = path.join(CONTINUE_DIR, 'config.json')
|
|
25
|
+
|
|
26
|
+
function getConfigFile() {
|
|
27
|
+
// 优先使用 config.yaml (新版)
|
|
28
|
+
if (fs.existsSync(CONFIG_YAML)) return { file: CONFIG_YAML, format: 'yaml' }
|
|
29
|
+
if (fs.existsSync(CONFIG_JSON)) return { file: CONFIG_JSON, format: 'json' }
|
|
30
|
+
// 默认创建 yaml
|
|
31
|
+
return { file: CONFIG_YAML, format: 'yaml' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJsonConfig() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(CONFIG_JSON, 'utf8'))
|
|
37
|
+
} catch {
|
|
38
|
+
return { models: [] }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
21
41
|
|
|
22
|
-
function
|
|
42
|
+
function writeJsonConfig(data) {
|
|
43
|
+
fs.mkdirSync(CONTINUE_DIR, { recursive: true })
|
|
44
|
+
fs.writeFileSync(CONFIG_JSON, JSON.stringify(data, null, 2), 'utf8')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readYamlRaw() {
|
|
23
48
|
try {
|
|
24
|
-
if (fs.existsSync(
|
|
49
|
+
if (fs.existsSync(CONFIG_YAML)) return fs.readFileSync(CONFIG_YAML, 'utf8')
|
|
25
50
|
} catch {}
|
|
26
|
-
return
|
|
51
|
+
return ''
|
|
27
52
|
}
|
|
28
53
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
/**
|
|
55
|
+
* 写入 YAML 配置(简单字符串追加/替换方式,不依赖 yaml 库)
|
|
56
|
+
* 先移除已有 HolySheep 模型块,再在 models: 下插入新的
|
|
57
|
+
*/
|
|
58
|
+
function writeYamlConfig(apiKey, baseUrl, models) {
|
|
59
|
+
fs.mkdirSync(CONTINUE_DIR, { recursive: true })
|
|
60
|
+
|
|
61
|
+
let content = readYamlRaw()
|
|
62
|
+
|
|
63
|
+
// 移除旧的 holysheep 模型块(以 " - name: HolySheep" 开头到下一个 " - " 或 EOF)
|
|
64
|
+
content = content.replace(/ - name: HolySheep[^\n]*\n( [^\n]*\n)*/g, '')
|
|
65
|
+
|
|
66
|
+
// 如果没有 models: 段,则加上
|
|
67
|
+
if (!content.includes('models:')) {
|
|
68
|
+
content = 'models:\n' + content
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 构建要插入的模型 YAML 块
|
|
72
|
+
const modelBlocks = models.map(m => [
|
|
73
|
+
` - name: ${m.name}`,
|
|
74
|
+
` provider: openai`,
|
|
75
|
+
` model: ${m.model}`,
|
|
76
|
+
` apiKey: ${apiKey}`,
|
|
77
|
+
` apiBase: ${baseUrl}`,
|
|
78
|
+
].join('\n')).join('\n')
|
|
79
|
+
|
|
80
|
+
// 在 models: 之后插入
|
|
81
|
+
content = content.replace('models:\n', `models:\n${modelBlocks}\n`)
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(CONFIG_YAML, content.replace(/\n{3,}/g, '\n\n').trimStart(), 'utf8')
|
|
32
84
|
}
|
|
33
85
|
|
|
34
|
-
const HS_MODELS =
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
provider: 'openai',
|
|
38
|
-
model: 'claude-sonnet-4-5',
|
|
39
|
-
apiKey,
|
|
40
|
-
apiBase: baseUrl,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
title: 'HolySheep — Claude Opus',
|
|
44
|
-
provider: 'openai',
|
|
45
|
-
model: 'claude-opus-4-5',
|
|
46
|
-
apiKey,
|
|
47
|
-
apiBase: baseUrl,
|
|
48
|
-
},
|
|
86
|
+
const HS_MODELS = [
|
|
87
|
+
{ name: 'HolySheep — Claude Sonnet', model: 'claude-sonnet-4-5' },
|
|
88
|
+
{ name: 'HolySheep — Claude Opus', model: 'claude-opus-4-5' },
|
|
49
89
|
]
|
|
50
90
|
|
|
51
91
|
module.exports = {
|
|
52
92
|
name: 'Continue.dev',
|
|
53
93
|
id: 'continue',
|
|
54
94
|
checkInstalled() {
|
|
55
|
-
return fs.existsSync(
|
|
95
|
+
return fs.existsSync(CONTINUE_DIR)
|
|
56
96
|
},
|
|
57
97
|
isConfigured() {
|
|
58
|
-
|
|
98
|
+
if (fs.existsSync(CONFIG_YAML)) {
|
|
99
|
+
return readYamlRaw().includes('holysheep')
|
|
100
|
+
}
|
|
101
|
+
const c = readJsonConfig()
|
|
59
102
|
return (c.models || []).some(m => m.apiBase?.includes('holysheep'))
|
|
60
103
|
},
|
|
61
|
-
configure(apiKey, baseUrlOpenAI) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
104
|
+
configure(apiKey, _baseUrlAnthropicNoV1, baseUrlOpenAI) {
|
|
105
|
+
const { format } = getConfigFile()
|
|
106
|
+
|
|
107
|
+
if (format === 'yaml') {
|
|
108
|
+
writeYamlConfig(apiKey, baseUrlOpenAI, HS_MODELS)
|
|
109
|
+
return { file: CONFIG_YAML, hot: true }
|
|
110
|
+
} else {
|
|
111
|
+
// 兼容旧版 JSON 格式
|
|
112
|
+
const config = readJsonConfig()
|
|
113
|
+
config.models = (config.models || []).filter(m => !m.apiBase?.includes('holysheep'))
|
|
114
|
+
config.models = [
|
|
115
|
+
...HS_MODELS.map(m => ({
|
|
116
|
+
title: m.name,
|
|
117
|
+
provider: 'openai',
|
|
118
|
+
model: m.model,
|
|
119
|
+
apiKey,
|
|
120
|
+
apiBase: baseUrlOpenAI,
|
|
121
|
+
})),
|
|
122
|
+
...config.models,
|
|
123
|
+
]
|
|
124
|
+
writeJsonConfig(config)
|
|
125
|
+
return { file: CONFIG_JSON, hot: true }
|
|
126
|
+
}
|
|
69
127
|
},
|
|
70
128
|
reset() {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
129
|
+
if (fs.existsSync(CONFIG_YAML)) {
|
|
130
|
+
let content = readYamlRaw()
|
|
131
|
+
content = content.replace(/ - name: HolySheep[^\n]*\n( [^\n]*\n)*/g, '')
|
|
132
|
+
fs.writeFileSync(CONFIG_YAML, content, 'utf8')
|
|
133
|
+
}
|
|
134
|
+
if (fs.existsSync(CONFIG_JSON)) {
|
|
135
|
+
const config = readJsonConfig()
|
|
136
|
+
config.models = (config.models || []).filter(m => !m.apiBase?.includes('holysheep'))
|
|
137
|
+
writeJsonConfig(config)
|
|
138
|
+
}
|
|
74
139
|
},
|
|
75
|
-
getConfigPath() { return
|
|
76
|
-
hint: '
|
|
140
|
+
getConfigPath() { return fs.existsSync(CONFIG_YAML) ? CONFIG_YAML : CONFIG_JSON },
|
|
141
|
+
hint: '新版使用 config.yaml;配置后在 VS Code Continue 面板选择 HolySheep 模型',
|
|
77
142
|
installCmd: 'VS Code 插件市场搜索 "Continue"',
|
|
78
143
|
docsUrl: 'https://continue.dev',
|
|
79
144
|
}
|
package/src/tools/opencode.js
CHANGED
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode 适配器 (sst/opencode)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* OpenCode 适配器 (anomalyco/opencode,原 sst/opencode)
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 重要:OpenCode 仓库已从 sst/opencode 迁移到 anomalyco/opencode
|
|
5
|
+
* 全局配置文件: ~/.config/opencode/opencode.json (不是 config.json!)
|
|
6
|
+
* 格式: JSON/JSONC,provider 配置格式如下:
|
|
5
7
|
*
|
|
6
|
-
* OpenCode provider 格式:
|
|
7
8
|
* {
|
|
8
|
-
* "
|
|
9
|
+
* "$schema": "https://opencode.ai/config.json",
|
|
10
|
+
* "model": "anthropic/claude-sonnet-4-5",
|
|
11
|
+
* "provider": {
|
|
9
12
|
* "anthropic": {
|
|
10
|
-
* "
|
|
11
|
-
*
|
|
13
|
+
* "options": {
|
|
14
|
+
* "baseURL": "https://api.holysheep.ai",
|
|
15
|
+
* "apiKey": "cr_xxx"
|
|
16
|
+
* }
|
|
12
17
|
* },
|
|
13
18
|
* "openai": {
|
|
14
|
-
* "
|
|
15
|
-
*
|
|
19
|
+
* "options": {
|
|
20
|
+
* "baseURL": "https://api.holysheep.ai/v1",
|
|
21
|
+
* "apiKey": "cr_xxx"
|
|
22
|
+
* }
|
|
16
23
|
* }
|
|
17
24
|
* }
|
|
18
25
|
* }
|
|
26
|
+
*
|
|
27
|
+
* 安装方式(推荐): brew install anomalyco/tap/opencode
|
|
28
|
+
* 或: npm i -g opencode-ai@latest
|
|
29
|
+
* 官方文档: https://opencode.ai/docs/config
|
|
19
30
|
*/
|
|
20
31
|
const fs = require('fs')
|
|
21
32
|
const path = require('path')
|
|
@@ -23,22 +34,30 @@ const os = require('os')
|
|
|
23
34
|
|
|
24
35
|
function getConfigFile() {
|
|
25
36
|
const candidates = [
|
|
37
|
+
// 新版标准路径(官方文档)
|
|
38
|
+
path.join(os.homedir(), '.config', 'opencode', 'opencode.json'),
|
|
39
|
+
// 旧版路径兼容
|
|
26
40
|
path.join(os.homedir(), '.config', 'opencode', 'config.json'),
|
|
41
|
+
path.join(os.homedir(), '.opencode', 'opencode.json'),
|
|
27
42
|
path.join(os.homedir(), '.opencode', 'config.json'),
|
|
28
43
|
// Windows
|
|
29
|
-
path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', '
|
|
44
|
+
path.join(os.homedir(), 'AppData', 'Roaming', 'opencode', 'opencode.json'),
|
|
30
45
|
]
|
|
31
46
|
for (const f of candidates) {
|
|
32
47
|
if (fs.existsSync(f)) return f
|
|
33
48
|
}
|
|
34
|
-
//
|
|
35
|
-
return path.join(os.homedir(), '.config', 'opencode', '
|
|
49
|
+
// 默认路径(新版标准)
|
|
50
|
+
return path.join(os.homedir(), '.config', 'opencode', 'opencode.json')
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
function readConfig() {
|
|
39
54
|
const file = getConfigFile()
|
|
40
55
|
try {
|
|
41
|
-
if (fs.existsSync(file))
|
|
56
|
+
if (fs.existsSync(file)) {
|
|
57
|
+
// 支持 JSONC(带注释的 JSON)
|
|
58
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
59
|
+
return JSON.parse(content.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''))
|
|
60
|
+
}
|
|
42
61
|
} catch {}
|
|
43
62
|
return {}
|
|
44
63
|
}
|
|
@@ -57,33 +76,52 @@ module.exports = {
|
|
|
57
76
|
},
|
|
58
77
|
isConfigured() {
|
|
59
78
|
const c = readConfig()
|
|
60
|
-
return !!(
|
|
61
|
-
|
|
79
|
+
return !!(
|
|
80
|
+
c.provider?.anthropic?.options?.baseURL?.includes('holysheep') ||
|
|
81
|
+
c.provider?.openai?.options?.baseURL?.includes('holysheep')
|
|
82
|
+
)
|
|
62
83
|
},
|
|
63
84
|
configure(apiKey, baseUrlAnthropicNoV1, baseUrlOpenAI) {
|
|
64
85
|
const config = readConfig()
|
|
65
|
-
if (!config.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
if (!config.provider) config.provider = {}
|
|
87
|
+
|
|
88
|
+
// 设置 schema(方便编辑器自动补全)
|
|
89
|
+
if (!config['$schema']) config['$schema'] = 'https://opencode.ai/config.json'
|
|
90
|
+
|
|
91
|
+
// 配置 Anthropic provider(Claude 模型)
|
|
92
|
+
config.provider.anthropic = {
|
|
93
|
+
options: {
|
|
94
|
+
baseURL: baseUrlAnthropicNoV1, // https://api.holysheep.ai (无 /v1)
|
|
95
|
+
apiKey,
|
|
96
|
+
},
|
|
69
97
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
|
|
99
|
+
// 配置 OpenAI provider(GPT 模型)
|
|
100
|
+
config.provider.openai = {
|
|
101
|
+
options: {
|
|
102
|
+
baseURL: baseUrlOpenAI, // https://api.holysheep.ai/v1
|
|
103
|
+
apiKey,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 设置默认模型
|
|
108
|
+
if (!config.model) {
|
|
109
|
+
config.model = 'anthropic/claude-sonnet-4-5'
|
|
73
110
|
}
|
|
111
|
+
|
|
74
112
|
writeConfig(config)
|
|
75
113
|
return { file: getConfigFile(), hot: false }
|
|
76
114
|
},
|
|
77
115
|
reset() {
|
|
78
116
|
const config = readConfig()
|
|
79
|
-
if (config.
|
|
80
|
-
delete config.
|
|
81
|
-
delete config.
|
|
117
|
+
if (config.provider) {
|
|
118
|
+
delete config.provider.anthropic
|
|
119
|
+
delete config.provider.openai
|
|
82
120
|
}
|
|
83
121
|
writeConfig(config)
|
|
84
122
|
},
|
|
85
123
|
getConfigPath() { return getConfigFile() },
|
|
86
|
-
hint: '切换后重启 OpenCode
|
|
87
|
-
installCmd: '
|
|
88
|
-
docsUrl: 'https://
|
|
124
|
+
hint: '切换后重启 OpenCode 生效;配置文件: ~/.config/opencode/opencode.json',
|
|
125
|
+
installCmd: 'brew install anomalyco/tap/opencode # 或: npm i -g opencode-ai@latest',
|
|
126
|
+
docsUrl: 'https://opencode.ai',
|
|
89
127
|
}
|