@vebing-tools/aicoding 0.0.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 +117 -0
- package/bin/aicoding.js +710 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @vebing-tools/aicoding
|
|
2
|
+
|
|
3
|
+
一键配置 OKMCode 到 AI 编程工具(Codex、OpenCode、OpenClaw)。
|
|
4
|
+
|
|
5
|
+
## 当前行为
|
|
6
|
+
|
|
7
|
+
- **快捷配置入口**:默认按**覆盖写入**处理
|
|
8
|
+
- **默认平台**:`codex` + `opencode`
|
|
9
|
+
- **可选平台**:`openclaw`(默认不选中)
|
|
10
|
+
- **测速选线**:启动时自动测速 OKMCode 候选线路,可手动切换
|
|
11
|
+
- **写前备份**:已有目标文件会先备份,再覆盖写入
|
|
12
|
+
|
|
13
|
+
这和 `aat okm` 的快捷配置语义保持一致。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
### `npx`
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @vebing-tools/aicoding
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 全局安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @vebing-tools/aicoding
|
|
27
|
+
aicoding
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `curl`
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
curl -fsSL https://raw.githubusercontent.com/2ue/anyaitools/main/scripts/aicoding.sh | bash
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 使用
|
|
37
|
+
|
|
38
|
+
### 交互式
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx @vebing-tools/aicoding
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 直接传入 API Key
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @vebing-tools/aicoding sk-ant-xxx
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 指定平台
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 仅配置 Codex + OpenCode
|
|
54
|
+
npx @vebing-tools/aicoding sk-ant-xxx -p codex,opencode
|
|
55
|
+
|
|
56
|
+
# 包含 OpenClaw
|
|
57
|
+
npx @vebing-tools/aicoding sk-ant-xxx -p codex,opencode,openclaw
|
|
58
|
+
|
|
59
|
+
# 所有支持平台
|
|
60
|
+
npx @vebing-tools/aicoding sk-ant-xxx -p all
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 指定 OpenAI Base URL
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @vebing-tools/aicoding sk-ant-xxx --openai-base-url https://okmcode.com
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 兼容旧参数
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx @vebing-tools/aicoding sk-ant-xxx --overwrite
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--overwrite` 仍可使用,但当前版本的快捷入口本身就采用覆盖写入,因此该参数只是兼容旧用法。
|
|
76
|
+
|
|
77
|
+
## 写入规则
|
|
78
|
+
|
|
79
|
+
### Codex
|
|
80
|
+
|
|
81
|
+
- 写入 `~/.codex/config.toml`
|
|
82
|
+
- 写入 `~/.codex/auth.json`
|
|
83
|
+
- 覆盖托管配置,写前备份已有文件
|
|
84
|
+
|
|
85
|
+
### OpenCode
|
|
86
|
+
|
|
87
|
+
- 写入 `~/.config/opencode/opencode.json`
|
|
88
|
+
- 覆盖 `provider.okmcode` 及快捷配置管理的相关结构
|
|
89
|
+
- 写前备份已有文件
|
|
90
|
+
|
|
91
|
+
### OpenClaw
|
|
92
|
+
|
|
93
|
+
- 写入 `~/.openclaw/openclaw.json`
|
|
94
|
+
- 写入 `~/.openclaw/agents/main/agent/models.json`
|
|
95
|
+
- 端点固定为选中 OpenAI Base URL 的 `/v1`
|
|
96
|
+
- 写前备份已有文件
|
|
97
|
+
|
|
98
|
+
## 配置文件
|
|
99
|
+
|
|
100
|
+
| 工具 | 文件 |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Codex | `~/.codex/config.toml` / `~/.codex/auth.json` |
|
|
103
|
+
| OpenCode | `~/.config/opencode/opencode.json` |
|
|
104
|
+
| OpenClaw | `~/.openclaw/openclaw.json` / `~/.openclaw/agents/main/agent/models.json` |
|
|
105
|
+
|
|
106
|
+
## 与 `anyaitools` 的区别
|
|
107
|
+
|
|
108
|
+
| 特性 | `@vebing-tools/aicoding` | `@vebing-tools/anyaitools` |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 用途 | 快捷配置 OKMCode | 完整 provider 管理 |
|
|
111
|
+
| 写入语义 | 快捷覆盖 | 常规管理增量,快捷命令覆盖 |
|
|
112
|
+
| 适用场景 | 快速落配置 | 日常维护、切换 provider |
|
|
113
|
+
|
|
114
|
+
## 建议
|
|
115
|
+
|
|
116
|
+
- 只想快速把 OKMCode 落到工具配置里:用 `@vebing-tools/aicoding`
|
|
117
|
+
- 需要长期维护多个 provider:用 `@vebing-tools/anyaitools`
|
package/bin/aicoding.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OKMCode 快速配置脚本(独立版本,不依赖 anyaitools)
|
|
4
|
+
*
|
|
5
|
+
* 功能:直接修改 Codex、OpenCode、OpenClaw 的配置文件
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* npx @vebing-tools/aicoding # 交互式输入(快捷覆盖模式)
|
|
9
|
+
* npx @vebing-tools/aicoding sk-ant-xxx # 直接传入 API Key(快捷覆盖模式)
|
|
10
|
+
* npx @vebing-tools/aicoding --overwrite # 兼容旧参数(行为不变)
|
|
11
|
+
* npx @vebing-tools/aicoding sk-ant-xxx --overwrite # 兼容旧参数(行为不变)
|
|
12
|
+
*
|
|
13
|
+
* 策略说明:
|
|
14
|
+
* - 快捷配置入口统一采用覆盖写入:直接落下选中工具的托管配置
|
|
15
|
+
* - 写入前会备份已有目标文件
|
|
16
|
+
*
|
|
17
|
+
* 依赖:Node.js 内置 API + inquirer(交互式选择)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from 'fs'
|
|
21
|
+
import * as os from 'os'
|
|
22
|
+
import * as path from 'path'
|
|
23
|
+
import { request as httpRequest } from 'node:http'
|
|
24
|
+
import { request as httpsRequest } from 'node:https'
|
|
25
|
+
import { performance } from 'node:perf_hooks'
|
|
26
|
+
import inquirer from 'inquirer'
|
|
27
|
+
|
|
28
|
+
const OKMCODE_ENDPOINTS = [
|
|
29
|
+
{
|
|
30
|
+
label: 'OKMCode 官方地址',
|
|
31
|
+
url: 'https://okmcode.com',
|
|
32
|
+
description: 'OKMCode 官方入口',
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
const DEFAULT_OPENAI_BASE_URL = OKMCODE_ENDPOINTS[0].url
|
|
36
|
+
let OPENAI_BASE_URL = DEFAULT_OPENAI_BASE_URL
|
|
37
|
+
const VALID_PLATFORMS = ['codex', 'opencode', 'openclaw']
|
|
38
|
+
const DEFAULT_PLATFORMS = ['codex', 'opencode']
|
|
39
|
+
const TOTAL_STEPS = 5
|
|
40
|
+
const BASE_URL_PROBE_SAMPLE_COUNT = 3
|
|
41
|
+
const BASE_URL_PROBE_TIMEOUT_MS = 2500
|
|
42
|
+
|
|
43
|
+
// 统一路径策略(与 @anyaitools/core 保持一致)
|
|
44
|
+
const NODE_ENV = process.env.NODE_ENV
|
|
45
|
+
const HOME_DIR =
|
|
46
|
+
NODE_ENV === 'test'
|
|
47
|
+
? path.join('/tmp', 'anyaitools-test')
|
|
48
|
+
: NODE_ENV === 'development'
|
|
49
|
+
? path.join(os.tmpdir(), 'anyaitools-dev')
|
|
50
|
+
: os.homedir()
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// 工具函数
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 确保目录存在
|
|
58
|
+
*/
|
|
59
|
+
function ensureDir(dirPath) {
|
|
60
|
+
if (!fs.existsSync(dirPath)) {
|
|
61
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 原子性写入文件
|
|
67
|
+
*/
|
|
68
|
+
function atomicWrite(filePath, content, mode = 0o600) {
|
|
69
|
+
const tempPath = `${filePath}.tmp`
|
|
70
|
+
fs.writeFileSync(tempPath, content, { mode })
|
|
71
|
+
fs.renameSync(tempPath, filePath)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function backupFileOrThrow(filePath, operation) {
|
|
75
|
+
if (!fs.existsSync(filePath)) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const backupPath = `${filePath}.bak`
|
|
80
|
+
try {
|
|
81
|
+
fs.copyFileSync(filePath, backupPath)
|
|
82
|
+
fs.chmodSync(backupPath, 0o600)
|
|
83
|
+
return backupPath
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`备份失败,已中止后续写入(${operation}): ${error.message}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildOpenClawBaseUrl(openaiBaseUrl) {
|
|
90
|
+
return openaiBaseUrl.replace(/\/+$/, '') + '/v1'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeEndpointUrl(url) {
|
|
94
|
+
const normalized = url.trim().replace(/\/+$/, '')
|
|
95
|
+
new URL(normalized)
|
|
96
|
+
return normalized
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function calculateMedian(values) {
|
|
100
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
101
|
+
const middle = Math.floor(sorted.length / 2)
|
|
102
|
+
if (sorted.length % 2 === 0) {
|
|
103
|
+
return Math.round((sorted[middle - 1] + sorted[middle]) / 2)
|
|
104
|
+
}
|
|
105
|
+
return sorted[middle]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function probeOnce(url, timeoutMs) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const target = new URL(url)
|
|
111
|
+
const requester = target.protocol === 'http:' ? httpRequest : httpsRequest
|
|
112
|
+
const start = performance.now()
|
|
113
|
+
let settled = false
|
|
114
|
+
|
|
115
|
+
const finish = (result) => {
|
|
116
|
+
if (settled) return
|
|
117
|
+
settled = true
|
|
118
|
+
resolve(result)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const req = requester(
|
|
122
|
+
target,
|
|
123
|
+
{
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: {
|
|
126
|
+
Accept: 'application/json',
|
|
127
|
+
'Cache-Control': 'no-cache',
|
|
128
|
+
'User-Agent': 'aicoding-latency-probe/1.0',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
(response) => {
|
|
132
|
+
const latencyMs = Math.max(1, Math.round(performance.now() - start))
|
|
133
|
+
response.destroy()
|
|
134
|
+
finish({ latencyMs, statusCode: response.statusCode || null })
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
req.setTimeout(timeoutMs, () => {
|
|
139
|
+
req.destroy(new Error(`测速超时(>${timeoutMs}ms)`))
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
req.on('error', (error) => {
|
|
143
|
+
finish({
|
|
144
|
+
latencyMs: null,
|
|
145
|
+
statusCode: null,
|
|
146
|
+
error: error.message,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
req.end()
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function sortProbeResults(results) {
|
|
155
|
+
return [...results].sort((left, right) => {
|
|
156
|
+
const leftReachable = left.latencyMs !== null
|
|
157
|
+
const rightReachable = right.latencyMs !== null
|
|
158
|
+
|
|
159
|
+
if (leftReachable && !rightReachable) return -1
|
|
160
|
+
if (!leftReachable && rightReachable) return 1
|
|
161
|
+
if (!leftReachable && !rightReachable) return left.originalIndex - right.originalIndex
|
|
162
|
+
if (left.latencyMs !== right.latencyMs) return left.latencyMs - right.latencyMs
|
|
163
|
+
return left.originalIndex - right.originalIndex
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatLatency(result) {
|
|
168
|
+
if (result.latencyMs === null) {
|
|
169
|
+
return result.error || '测速失败'
|
|
170
|
+
}
|
|
171
|
+
return `${result.latencyMs} ms`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function probeEndpoint(endpoint, probeUrl, originalIndex) {
|
|
175
|
+
const samples = []
|
|
176
|
+
let lastError = null
|
|
177
|
+
|
|
178
|
+
for (let index = 0; index < BASE_URL_PROBE_SAMPLE_COUNT; index += 1) {
|
|
179
|
+
const result = await probeOnce(probeUrl, BASE_URL_PROBE_TIMEOUT_MS)
|
|
180
|
+
if (result.latencyMs !== null) {
|
|
181
|
+
samples.push(result.latencyMs)
|
|
182
|
+
} else if (!lastError) {
|
|
183
|
+
lastError = result.error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...endpoint,
|
|
189
|
+
url: normalizeEndpointUrl(endpoint.url),
|
|
190
|
+
probeUrl,
|
|
191
|
+
originalIndex,
|
|
192
|
+
samples,
|
|
193
|
+
latencyMs: samples.length > 0 ? calculateMedian(samples) : null,
|
|
194
|
+
error: samples.length > 0 ? null : lastError || '测速失败',
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function resolveOpenAiBaseUrl(openaiBaseUrlArg, platforms) {
|
|
199
|
+
if (openaiBaseUrlArg && openaiBaseUrlArg.trim().length > 0) {
|
|
200
|
+
const normalized = normalizeEndpointUrl(openaiBaseUrlArg)
|
|
201
|
+
console.log(`已通过参数指定 Base URL: ${normalized}`)
|
|
202
|
+
return normalized
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const openclawOnly = platforms.length === 1 && platforms[0] === 'openclaw'
|
|
206
|
+
const probeResults = sortProbeResults(
|
|
207
|
+
await Promise.all(
|
|
208
|
+
OKMCODE_ENDPOINTS.map((endpoint, index) =>
|
|
209
|
+
probeEndpoint(
|
|
210
|
+
endpoint,
|
|
211
|
+
openclawOnly ? buildOpenClawBaseUrl(endpoint.url) : endpoint.url,
|
|
212
|
+
index
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
console.log(
|
|
219
|
+
`测速方式:HTTPS 首包延迟(${BASE_URL_PROBE_SAMPLE_COUNT} 次中位数)${
|
|
220
|
+
openclawOnly ? ',当前仅检测 OpenClaw 的 /v1 端点' : ''
|
|
221
|
+
}`
|
|
222
|
+
)
|
|
223
|
+
for (const result of probeResults) {
|
|
224
|
+
console.log(` ${result.label} · ${formatLatency(result)}`)
|
|
225
|
+
console.log(` ${result.url}`)
|
|
226
|
+
console.log(` ${result.description}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const defaultResult = probeResults[0]
|
|
230
|
+
const allFailed = probeResults.every((result) => result.latencyMs === null)
|
|
231
|
+
if (allFailed) {
|
|
232
|
+
console.log('⚠️ 所有候选地址测速失败,将默认选中第一个地址,你也可以手动切换。')
|
|
233
|
+
} else {
|
|
234
|
+
console.log(`默认已选延迟最低地址:${defaultResult.url}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (probeResults.length === 1) {
|
|
238
|
+
console.log(`已自动选择: ${defaultResult.url}`)
|
|
239
|
+
return defaultResult.url
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
243
|
+
console.log(`非交互环境,已自动使用默认地址:${defaultResult.url}`)
|
|
244
|
+
return defaultResult.url
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const answers = await inquirer.prompt([
|
|
248
|
+
{
|
|
249
|
+
type: 'list',
|
|
250
|
+
name: 'baseUrl',
|
|
251
|
+
message: '选择要使用的 Base URL(默认已选延迟最低):',
|
|
252
|
+
choices: probeResults.map((result) => ({
|
|
253
|
+
name: `${result.label} · ${result.url} · ${formatLatency(result)} · ${result.description}`,
|
|
254
|
+
value: result.url,
|
|
255
|
+
})),
|
|
256
|
+
default: defaultResult.url,
|
|
257
|
+
},
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
return answers.baseUrl
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// 交互式输入
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
function renderStep(current, total, title) {
|
|
268
|
+
const barLength = total
|
|
269
|
+
const filledLength = Math.min(current, total)
|
|
270
|
+
const bar = `${'■'.repeat(filledLength)}${'□'.repeat(barLength - filledLength)}`
|
|
271
|
+
return `步骤 ${current}/${total} [${bar}] ${title}`
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function printBanner() {
|
|
275
|
+
console.log(
|
|
276
|
+
[
|
|
277
|
+
' ██████╗ ███╗ ███╗███╗ ██╗',
|
|
278
|
+
' ██╔════╝ ████╗ ████║████╗ ██║',
|
|
279
|
+
' ██║ ███╗ ██╔████╔██║██╔██╗ ██║',
|
|
280
|
+
' ██║ ██║ ██║╚██╔╝██║██║╚██╗██║',
|
|
281
|
+
' ╚██████╔╝ ██║ ╚═╝ ██║██║ ╚████║',
|
|
282
|
+
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝',
|
|
283
|
+
' OKMCode 一键配置向导 · 独立脚本',
|
|
284
|
+
' 自动写入选中工具配置,支持多选。\n',
|
|
285
|
+
].join('\n')
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function printKeyNotice() {
|
|
290
|
+
console.log(
|
|
291
|
+
[
|
|
292
|
+
'提示:本命令支持 Codex、OpenCode、OpenClaw。',
|
|
293
|
+
'Codex 与 OpenCode 共享 OpenAI 端点,OpenClaw 使用 /v1 端点。',
|
|
294
|
+
'VS Code 的 Codex 插件若使用本机默认配置,也会跟随本次写入生效。',
|
|
295
|
+
].join('\n')
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parsePlatforms(platformArg) {
|
|
300
|
+
if (platformArg === 'all') {
|
|
301
|
+
return [...VALID_PLATFORMS]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const platforms = platformArg.split(',').map((p) => p.trim().toLowerCase())
|
|
305
|
+
|
|
306
|
+
for (const platform of platforms) {
|
|
307
|
+
if (!VALID_PLATFORMS.includes(platform)) {
|
|
308
|
+
throw new Error(`无效的平台 "${platform}"。有效值: ${VALID_PLATFORMS.join(', ')}, all`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return platforms
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function promptPlatforms() {
|
|
316
|
+
const answers = await inquirer.prompt([
|
|
317
|
+
{
|
|
318
|
+
type: 'checkbox',
|
|
319
|
+
name: 'platforms',
|
|
320
|
+
message: '选择要配置的工具(可多选,空格选择 / a全选 / i反选 / 回车确认):',
|
|
321
|
+
dontShowHints: true,
|
|
322
|
+
choices: [
|
|
323
|
+
{ name: 'Codex(需单独订阅 OpenAI 套餐)', value: 'codex' },
|
|
324
|
+
{ name: 'OpenCode(与 Codex 共享 OpenAI 套餐)', value: 'opencode' },
|
|
325
|
+
{ name: 'OpenClaw(OKMCode /v1 端点,默认不选中)', value: 'openclaw' },
|
|
326
|
+
{ name: '全部(将依次配置 Codex、OpenCode、OpenClaw)', value: 'all' },
|
|
327
|
+
],
|
|
328
|
+
default: DEFAULT_PLATFORMS,
|
|
329
|
+
validate: (value) => {
|
|
330
|
+
if (!value || value.length === 0) return '至少选择一个平台'
|
|
331
|
+
return true
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
])
|
|
335
|
+
|
|
336
|
+
const selected = answers.platforms
|
|
337
|
+
if (selected.includes('all')) {
|
|
338
|
+
return [...VALID_PLATFORMS]
|
|
339
|
+
}
|
|
340
|
+
return selected
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function promptApiKey() {
|
|
344
|
+
const answers = await inquirer.prompt([
|
|
345
|
+
{
|
|
346
|
+
type: 'password',
|
|
347
|
+
name: 'apiKey',
|
|
348
|
+
message: '请输入 OKMCode API Key:',
|
|
349
|
+
mask: '*',
|
|
350
|
+
validate: (value) => {
|
|
351
|
+
if (!value) return 'API Key 不能为空'
|
|
352
|
+
return true
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
return answers.apiKey
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Codex 配置
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
function configureCodex(apiKey) {
|
|
365
|
+
const configDir = path.join(HOME_DIR, '.codex')
|
|
366
|
+
const configPath = path.join(configDir, 'config.toml')
|
|
367
|
+
const authPath = path.join(configDir, 'auth.json')
|
|
368
|
+
const providerKey = 'okmcode'
|
|
369
|
+
|
|
370
|
+
ensureDir(configDir)
|
|
371
|
+
|
|
372
|
+
// 1. 处理 config.toml(先备份,再覆盖写入)
|
|
373
|
+
const minimalConfig = [
|
|
374
|
+
`model_provider = "${providerKey}"`,
|
|
375
|
+
'model = "gpt-5.5"',
|
|
376
|
+
'review_model = "gpt-5.5"',
|
|
377
|
+
'model_reasoning_effort = "xhigh"',
|
|
378
|
+
'plan_mode_reasoning_effort = "xhigh"',
|
|
379
|
+
'model_reasoning_summary = "auto"',
|
|
380
|
+
'model_verbosity = "high"',
|
|
381
|
+
'personality = "pragmatic"',
|
|
382
|
+
'sandbox_mode = "danger-full-access"',
|
|
383
|
+
'approval_policy = "never"',
|
|
384
|
+
'web_search = "cached"',
|
|
385
|
+
'',
|
|
386
|
+
'[windows]',
|
|
387
|
+
'sandbox = "elevated"',
|
|
388
|
+
'',
|
|
389
|
+
'[features]',
|
|
390
|
+
'multi_agent = true',
|
|
391
|
+
'shell_tool = true',
|
|
392
|
+
'shell_snapshot = true',
|
|
393
|
+
'fast_mode = true',
|
|
394
|
+
'personality = true',
|
|
395
|
+
'',
|
|
396
|
+
`[model_providers.${providerKey}]`,
|
|
397
|
+
`name = "${providerKey}"`,
|
|
398
|
+
`base_url = "${OPENAI_BASE_URL}"`,
|
|
399
|
+
'wire_api = "responses"',
|
|
400
|
+
'requires_openai_auth = true',
|
|
401
|
+
'',
|
|
402
|
+
].join('\n')
|
|
403
|
+
|
|
404
|
+
if (fs.existsSync(configPath)) {
|
|
405
|
+
backupFileOrThrow(configPath, 'aicoding.codex.config.toml')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
atomicWrite(configPath, minimalConfig)
|
|
409
|
+
|
|
410
|
+
// 2. 处理 auth.json(先备份,再覆盖写入,仅保留 OPENAI_API_KEY)
|
|
411
|
+
if (fs.existsSync(authPath)) {
|
|
412
|
+
backupFileOrThrow(authPath, 'aicoding.codex.auth.json')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const auth = { OPENAI_API_KEY: apiKey }
|
|
416
|
+
atomicWrite(authPath, JSON.stringify(auth, null, 2))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// OpenCode 配置
|
|
421
|
+
// ============================================================================
|
|
422
|
+
|
|
423
|
+
function configureOpenCode(apiKey) {
|
|
424
|
+
const configDir = path.join(HOME_DIR, '.config', 'opencode')
|
|
425
|
+
const configPath = path.join(configDir, 'opencode.json')
|
|
426
|
+
|
|
427
|
+
ensureDir(configDir)
|
|
428
|
+
|
|
429
|
+
// 构建 OKMCode provider 配置
|
|
430
|
+
const okmcodeProvider = {
|
|
431
|
+
npm: '@ai-sdk/openai',
|
|
432
|
+
name: 'OKMCode',
|
|
433
|
+
options: {
|
|
434
|
+
baseURL: OPENAI_BASE_URL,
|
|
435
|
+
apiKey: apiKey,
|
|
436
|
+
},
|
|
437
|
+
models: {
|
|
438
|
+
'gpt-5.5': {
|
|
439
|
+
options: {
|
|
440
|
+
store: false,
|
|
441
|
+
},
|
|
442
|
+
variants: {
|
|
443
|
+
xhigh: {
|
|
444
|
+
reasoningEffort: 'xhigh',
|
|
445
|
+
textVerbosity: 'low',
|
|
446
|
+
reasoningSummary: 'auto',
|
|
447
|
+
},
|
|
448
|
+
high: {
|
|
449
|
+
reasoningEffort: 'high',
|
|
450
|
+
textVerbosity: 'low',
|
|
451
|
+
reasoningSummary: 'auto',
|
|
452
|
+
},
|
|
453
|
+
medium: {
|
|
454
|
+
reasoningEffort: 'medium',
|
|
455
|
+
textVerbosity: 'low',
|
|
456
|
+
reasoningSummary: 'auto',
|
|
457
|
+
},
|
|
458
|
+
low: {
|
|
459
|
+
reasoningEffort: 'low',
|
|
460
|
+
textVerbosity: 'low',
|
|
461
|
+
reasoningSummary: 'auto',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const config = {
|
|
469
|
+
$schema: 'https://opencode.ai/config.json',
|
|
470
|
+
model: 'okmcode/gpt-5.5',
|
|
471
|
+
agent: {
|
|
472
|
+
build: { options: { store: false } },
|
|
473
|
+
plan: { options: { store: false } },
|
|
474
|
+
},
|
|
475
|
+
provider: {
|
|
476
|
+
okmcode: okmcodeProvider,
|
|
477
|
+
},
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
backupFileOrThrow(configPath, 'aicoding.opencode.opencode.json')
|
|
481
|
+
atomicWrite(configPath, JSON.stringify(config, null, 2))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// OpenClaw 配置(始终直接覆盖)
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
function createOpenClawModel(id) {
|
|
489
|
+
return {
|
|
490
|
+
id,
|
|
491
|
+
name: id,
|
|
492
|
+
api: 'openai-responses',
|
|
493
|
+
reasoning: true,
|
|
494
|
+
input: ['text', 'image'],
|
|
495
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
496
|
+
contextWindow: 1050000,
|
|
497
|
+
maxTokens: 128000,
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function configureOpenClaw(apiKey) {
|
|
502
|
+
const openclawDir = path.join(HOME_DIR, '.openclaw')
|
|
503
|
+
const openclawPath = path.join(openclawDir, 'openclaw.json')
|
|
504
|
+
const modelsPath = path.join(openclawDir, 'agents', 'main', 'agent', 'models.json')
|
|
505
|
+
|
|
506
|
+
ensureDir(path.dirname(openclawPath))
|
|
507
|
+
ensureDir(path.dirname(modelsPath))
|
|
508
|
+
|
|
509
|
+
const providerKey = 'okmcode'
|
|
510
|
+
const primaryModelId = 'gpt-5.5'
|
|
511
|
+
const openclawBaseUrl = buildOpenClawBaseUrl(OPENAI_BASE_URL)
|
|
512
|
+
|
|
513
|
+
const modelsConfig = {
|
|
514
|
+
providers: {
|
|
515
|
+
[providerKey]: {
|
|
516
|
+
baseUrl: openclawBaseUrl,
|
|
517
|
+
apiKey,
|
|
518
|
+
api: 'openai-responses',
|
|
519
|
+
authHeader: true,
|
|
520
|
+
headers: {
|
|
521
|
+
'User-Agent': 'curl/8.0',
|
|
522
|
+
'OpenAI-Beta': 'responses=v1',
|
|
523
|
+
},
|
|
524
|
+
models: [createOpenClawModel(primaryModelId)],
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const openclawConfig = {
|
|
530
|
+
models: {
|
|
531
|
+
mode: 'merge',
|
|
532
|
+
providers: {
|
|
533
|
+
[providerKey]: {
|
|
534
|
+
baseUrl: openclawBaseUrl,
|
|
535
|
+
apiKey,
|
|
536
|
+
api: 'openai-responses',
|
|
537
|
+
headers: {
|
|
538
|
+
'User-Agent': 'curl/8.0',
|
|
539
|
+
'OpenAI-Beta': 'responses=v1',
|
|
540
|
+
},
|
|
541
|
+
authHeader: true,
|
|
542
|
+
models: [createOpenClawModel(primaryModelId)],
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
agents: {
|
|
547
|
+
defaults: {
|
|
548
|
+
workspace: HOME_DIR,
|
|
549
|
+
imageModel: {
|
|
550
|
+
primary: `${providerKey}/${primaryModelId}`,
|
|
551
|
+
},
|
|
552
|
+
model: {
|
|
553
|
+
primary: `${providerKey}/${primaryModelId}`,
|
|
554
|
+
},
|
|
555
|
+
thinkingDefault: 'xhigh',
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// OpenClaw 在快捷入口中固定直接覆盖托管配置。
|
|
561
|
+
backupFileOrThrow(modelsPath, 'aicoding.openclaw.models.json')
|
|
562
|
+
backupFileOrThrow(openclawPath, 'aicoding.openclaw.openclaw.json')
|
|
563
|
+
atomicWrite(modelsPath, JSON.stringify(modelsConfig, null, 2))
|
|
564
|
+
atomicWrite(openclawPath, JSON.stringify(openclawConfig, null, 2))
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ============================================================================
|
|
568
|
+
// 主函数
|
|
569
|
+
// ============================================================================
|
|
570
|
+
|
|
571
|
+
async function main() {
|
|
572
|
+
printBanner()
|
|
573
|
+
|
|
574
|
+
// 1. 解析命令行参数
|
|
575
|
+
const args = process.argv.slice(2)
|
|
576
|
+
let apiKey = null
|
|
577
|
+
let platformArg = null
|
|
578
|
+
let openaiBaseUrl = null
|
|
579
|
+
let overwriteArgProvided = false
|
|
580
|
+
|
|
581
|
+
for (let i = 0; i < args.length; i++) {
|
|
582
|
+
const arg = args[i]
|
|
583
|
+
if (arg === '--overwrite') {
|
|
584
|
+
overwriteArgProvided = true
|
|
585
|
+
} else if (arg === '-p' || arg === '--platform') {
|
|
586
|
+
platformArg = args[i + 1]
|
|
587
|
+
i++ // 跳过下一个参数
|
|
588
|
+
} else if (arg.startsWith('-p=')) {
|
|
589
|
+
platformArg = arg.substring(3)
|
|
590
|
+
} else if (arg.startsWith('--platform=')) {
|
|
591
|
+
platformArg = arg.substring(11)
|
|
592
|
+
} else if (arg === '--openai-base-url' || arg === '--base-url') {
|
|
593
|
+
openaiBaseUrl = args[i + 1]
|
|
594
|
+
i++ // 跳过下一个参数
|
|
595
|
+
} else if (arg.startsWith('--openai-base-url=')) {
|
|
596
|
+
openaiBaseUrl = arg.substring('--openai-base-url='.length)
|
|
597
|
+
} else if (arg.startsWith('--base-url=')) {
|
|
598
|
+
openaiBaseUrl = arg.substring('--base-url='.length)
|
|
599
|
+
} else if (!arg.startsWith('-')) {
|
|
600
|
+
apiKey = arg
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 2. 快捷配置策略说明(与 anyaitools okmcode 保持一致)
|
|
605
|
+
console.log(`\n${renderStep(1, TOTAL_STEPS, '确认快捷写入策略')}`)
|
|
606
|
+
if (overwriteArgProvided) {
|
|
607
|
+
console.log('已显式指定 --overwrite(当前快捷入口默认即为覆盖写入)')
|
|
608
|
+
}
|
|
609
|
+
console.log('快捷配置入口将直接覆盖选中工具的托管配置,并在覆盖前备份已有目标文件。')
|
|
610
|
+
|
|
611
|
+
console.log(`\n${renderStep(2, TOTAL_STEPS, '选择要配置的工具')}`)
|
|
612
|
+
let platforms
|
|
613
|
+
try {
|
|
614
|
+
if (platformArg && platformArg.trim().length > 0) {
|
|
615
|
+
platforms = parsePlatforms(platformArg)
|
|
616
|
+
} else {
|
|
617
|
+
platforms = await promptPlatforms()
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
621
|
+
console.error(`❌ ${message}`)
|
|
622
|
+
process.exit(1)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(`已选择: ${platforms.join(', ')}`)
|
|
626
|
+
printKeyNotice()
|
|
627
|
+
|
|
628
|
+
const needsOpenAIBaseUrl = platforms.includes('codex') || platforms.includes('opencode')
|
|
629
|
+
const needsBaseUrl = needsOpenAIBaseUrl || platforms.includes('openclaw')
|
|
630
|
+
|
|
631
|
+
if (needsBaseUrl) {
|
|
632
|
+
console.log(`\n${renderStep(3, TOTAL_STEPS, '测速并选择 Base URL')}`)
|
|
633
|
+
try {
|
|
634
|
+
openaiBaseUrl = await resolveOpenAiBaseUrl(openaiBaseUrl, platforms)
|
|
635
|
+
OPENAI_BASE_URL = openaiBaseUrl
|
|
636
|
+
} catch (error) {
|
|
637
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
638
|
+
console.error(`❌ ${message}`)
|
|
639
|
+
process.exit(1)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
console.log(`\n${renderStep(4, TOTAL_STEPS, '输入 API Key')}`)
|
|
644
|
+
if (!apiKey) {
|
|
645
|
+
apiKey = await promptApiKey()
|
|
646
|
+
} else {
|
|
647
|
+
console.log('已通过参数提供 API Key(已隐藏)')
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!apiKey?.trim()) {
|
|
651
|
+
throw new Error('API Key 不能为空')
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(`\n${renderStep(5, TOTAL_STEPS, '开始写入配置')}`)
|
|
655
|
+
console.log('模式: 快捷覆盖模式')
|
|
656
|
+
console.log(`平台: ${platforms.join(', ')}`)
|
|
657
|
+
if (needsBaseUrl && openaiBaseUrl) {
|
|
658
|
+
console.log(`OpenAI Base URL: ${openaiBaseUrl}`)
|
|
659
|
+
}
|
|
660
|
+
if (platforms.includes('openclaw')) {
|
|
661
|
+
console.log(`OpenClaw Base URL: ${buildOpenClawBaseUrl(openaiBaseUrl || OPENAI_BASE_URL)}`)
|
|
662
|
+
}
|
|
663
|
+
console.log('\n开始配置...\n')
|
|
664
|
+
|
|
665
|
+
// 6. 配置选中的工具
|
|
666
|
+
const ALL_TOOLS = {
|
|
667
|
+
codex: { name: 'Codex', configure: configureCodex },
|
|
668
|
+
opencode: { name: 'OpenCode', configure: configureOpenCode },
|
|
669
|
+
openclaw: { name: 'OpenClaw', configure: configureOpenClaw },
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const tools = platforms.map((p) => ALL_TOOLS[p])
|
|
673
|
+
|
|
674
|
+
let completed = 0
|
|
675
|
+
for (const { name, configure } of tools) {
|
|
676
|
+
try {
|
|
677
|
+
console.log(`→ 配置 ${name}...`)
|
|
678
|
+
configure(apiKey)
|
|
679
|
+
completed += 1
|
|
680
|
+
console.log(`✅ ${name}`)
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.error(`❌ ${name}: ${error.message}`)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
console.log(`\n🎉 OKMCode 配置完成!(${completed}/${tools.length})`)
|
|
687
|
+
|
|
688
|
+
// 只显示配置的工具的文件位置
|
|
689
|
+
console.log('\n配置文件位置:')
|
|
690
|
+
if (platforms.includes('codex')) {
|
|
691
|
+
console.log(` - Codex: ${path.join(HOME_DIR, '.codex/config.toml')}`)
|
|
692
|
+
console.log(` - Codex: ${path.join(HOME_DIR, '.codex/auth.json')}`)
|
|
693
|
+
}
|
|
694
|
+
if (platforms.includes('opencode')) {
|
|
695
|
+
console.log(` - OpenCode: ${path.join(HOME_DIR, '.config/opencode/opencode.json')}`)
|
|
696
|
+
}
|
|
697
|
+
if (platforms.includes('openclaw')) {
|
|
698
|
+
console.log(` - OpenClaw: ${path.join(HOME_DIR, '.openclaw/openclaw.json')}`)
|
|
699
|
+
console.log(
|
|
700
|
+
` - OpenClaw: ${path.join(HOME_DIR, '.openclaw/agents/main/agent/models.json')}`
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
console.log('\n提示:请重启对应的工具以使配置生效。')
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
main().catch((err) => {
|
|
708
|
+
console.error(`\n❌ 错误: ${err.message}`)
|
|
709
|
+
process.exit(1)
|
|
710
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vebing-tools/aicoding",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "一键配置 OKMCode 到 Codex、OpenCode、OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aicoding": "./bin/aicoding.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"okmcode",
|
|
19
|
+
"codex",
|
|
20
|
+
"opencode",
|
|
21
|
+
"openclaw",
|
|
22
|
+
"ai",
|
|
23
|
+
"config",
|
|
24
|
+
"aicoding"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"author": "anyaitools",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/2ue/anyaitools.git",
|
|
34
|
+
"directory": "packages/aicoding"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/2ue/anyaitools/tree/main/packages/aicoding",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"inquirer": "^8.2.6"
|
|
39
|
+
}
|
|
40
|
+
}
|