@autotask/atools-tool 0.1.9 → 0.1.10
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 +4 -4
- package/bin/cli.mjs +17 -1
- package/lib/config-codex.mjs +429 -0
- package/lib/config-openclaw.mjs +76 -33
- package/lib/install.mjs +7 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Recommended selections:
|
|
|
44
44
|
1. `gpt-5.3-codex`
|
|
45
45
|
2. `off`
|
|
46
46
|
3. `low`
|
|
47
|
-
4. Input your
|
|
47
|
+
4. Input your ATools key (same as ATOOLS_OAI_KEY)
|
|
48
48
|
|
|
49
49
|
## Install (npm)
|
|
50
50
|
|
|
@@ -55,16 +55,16 @@ npm i -g @autotask/atools-tool
|
|
|
55
55
|
Default full install (recommended):
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
npm i -g @autotask/atools-tool && atools-tool-
|
|
58
|
+
npm i -g @autotask/atools-tool && atools-tool install-proxy && atools-tool openclaw
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Optional installer command:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
atools-tool-
|
|
64
|
+
atools-tool install-proxy --help
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`atools-tool-
|
|
67
|
+
`atools-tool install-proxy` service behavior by platform (legacy alias: `atools-tool-install`):
|
|
68
68
|
|
|
69
69
|
- Linux: systemd user service (fallback to detached process if `systemctl --user` is unavailable)
|
|
70
70
|
- macOS: launchd user agent (`~/Library/LaunchAgents`)
|
package/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProxyServer } from '../lib/proxy-server.mjs';
|
|
3
3
|
import { runConfigOpenclaw } from '../lib/config-openclaw.mjs';
|
|
4
|
+
import { runConfigCodex } from '../lib/config-codex.mjs';
|
|
5
|
+
import { runInstall } from '../lib/install.mjs';
|
|
4
6
|
|
|
5
7
|
function parseArgs(argv) {
|
|
6
8
|
const out = {
|
|
@@ -47,6 +49,8 @@ Commands:
|
|
|
47
49
|
serve Run proxy service
|
|
48
50
|
openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
|
|
49
51
|
config-openclaw Alias of 'openclaw'
|
|
52
|
+
install-proxy Install background proxy service for OpenClaw
|
|
53
|
+
codex Auto-configure Codex CLI to use sub2api (direct upstream)
|
|
50
54
|
|
|
51
55
|
Options:
|
|
52
56
|
--openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
|
|
@@ -62,13 +66,25 @@ Options:
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
async function main() {
|
|
65
|
-
const
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
|
|
71
|
+
if (argv[0] === 'install-proxy') {
|
|
72
|
+
await runInstall(argv.slice(1));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const args = parseArgs(argv);
|
|
66
77
|
|
|
67
78
|
if (args.help) {
|
|
68
79
|
printHelp();
|
|
69
80
|
return;
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
if (args.cmd === 'codex') {
|
|
84
|
+
await runConfigCodex();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
|
|
73
89
|
await runConfigOpenclaw({ openclawHome: args.openclawHome });
|
|
74
90
|
return;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import readlinePromises from 'node:readline/promises';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const PROVIDER = 'atools';
|
|
9
|
+
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
10
|
+
const DEFAULT_MODEL_ID = 'gpt-5.3-codex';
|
|
11
|
+
const UPSTREAM_BASE_URL = 'https://sub2api.atools.live/v1';
|
|
12
|
+
const ENV_KEY = 'ATOOLS_OAI_KEY';
|
|
13
|
+
|
|
14
|
+
const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
|
|
15
|
+
const ANSI = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function withColor(text, ...codes) {
|
|
24
|
+
if (!USE_COLOR || !codes.length) return text;
|
|
25
|
+
const prefix = codes.join('');
|
|
26
|
+
return `${prefix}${text}${ANSI.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sectionTitle(text) {
|
|
30
|
+
return withColor(text, ANSI.cyan, ANSI.bold);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ok(text) {
|
|
34
|
+
return withColor(text, ANSI.green);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function warn(text) {
|
|
38
|
+
return withColor(text, ANSI.yellow);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bold(text) {
|
|
42
|
+
return withColor(text, ANSI.bold);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nowStamp() {
|
|
46
|
+
const d = new Date();
|
|
47
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
48
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function backup(filePath) {
|
|
52
|
+
if (!fs.existsSync(filePath)) return null;
|
|
53
|
+
const dest = `${filePath}.bak.${nowStamp()}`;
|
|
54
|
+
fs.copyFileSync(filePath, dest);
|
|
55
|
+
return dest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureDir(dirPath) {
|
|
59
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function patchCodexConfigText(original, { modelId } = {}) {
|
|
63
|
+
const provider = PROVIDER;
|
|
64
|
+
const model = modelId || DEFAULT_MODEL_ID;
|
|
65
|
+
const baseUrl = UPSTREAM_BASE_URL;
|
|
66
|
+
|
|
67
|
+
if (!original || !original.trim()) {
|
|
68
|
+
return [
|
|
69
|
+
'# Codex config generated by atools-tool',
|
|
70
|
+
`model_provider = "${provider}"`,
|
|
71
|
+
`model = "${model}"`,
|
|
72
|
+
'',
|
|
73
|
+
`[model_providers.${provider}]`,
|
|
74
|
+
'name = "ATools"',
|
|
75
|
+
`base_url = "${baseUrl}"`,
|
|
76
|
+
'wire_api = "responses"',
|
|
77
|
+
'requires_openai_auth = false',
|
|
78
|
+
`env_key = "${ENV_KEY}"`,
|
|
79
|
+
''
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lines = original.replace(/\r\n/g, '\n').split('\n');
|
|
84
|
+
let currentSection = '';
|
|
85
|
+
let inProviderSection = false;
|
|
86
|
+
let sawProviderSection = false;
|
|
87
|
+
let sawModelProviderTop = false;
|
|
88
|
+
let sawModelTop = false;
|
|
89
|
+
|
|
90
|
+
const updated = [];
|
|
91
|
+
|
|
92
|
+
for (const rawLine of lines) {
|
|
93
|
+
let line = rawLine;
|
|
94
|
+
const trimmed = rawLine.trim();
|
|
95
|
+
|
|
96
|
+
const sectionMatch = trimmed.match(/^\[(.+?)\]\s*$/);
|
|
97
|
+
if (sectionMatch) {
|
|
98
|
+
currentSection = sectionMatch[1];
|
|
99
|
+
inProviderSection = currentSection === `model_providers.${provider}`;
|
|
100
|
+
if (inProviderSection) sawProviderSection = true;
|
|
101
|
+
updated.push(line);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!currentSection) {
|
|
106
|
+
if (/^model_provider\s*=/.test(trimmed)) {
|
|
107
|
+
line = `model_provider = "${provider}"`;
|
|
108
|
+
sawModelProviderTop = true;
|
|
109
|
+
} else if (/^model\s*=/.test(trimmed)) {
|
|
110
|
+
line = `model = "${model}"`;
|
|
111
|
+
sawModelTop = true;
|
|
112
|
+
}
|
|
113
|
+
updated.push(line);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (inProviderSection) {
|
|
118
|
+
if (/^name\s*=/.test(trimmed)) {
|
|
119
|
+
line = 'name = "ATools"';
|
|
120
|
+
} else if (/^base_url\s*=/.test(trimmed)) {
|
|
121
|
+
line = `base_url = "${baseUrl}"`;
|
|
122
|
+
} else if (/^wire_api\s*=/.test(trimmed)) {
|
|
123
|
+
line = 'wire_api = "responses"';
|
|
124
|
+
} else if (/^requires_openai_auth\s*=/.test(trimmed)) {
|
|
125
|
+
line = 'requires_openai_auth = false';
|
|
126
|
+
} else if (/^env_key\s*=/.test(trimmed)) {
|
|
127
|
+
line = `env_key = "${ENV_KEY}"`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updated.push(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let resultLines = updated;
|
|
135
|
+
|
|
136
|
+
const topInsert = [];
|
|
137
|
+
if (!sawModelProviderTop) topInsert.push(`model_provider = "${provider}"`);
|
|
138
|
+
if (!sawModelTop) topInsert.push(`model = "${model}"`);
|
|
139
|
+
|
|
140
|
+
if (topInsert.length > 0) {
|
|
141
|
+
const withTop = [];
|
|
142
|
+
let inserted = false;
|
|
143
|
+
for (let i = 0; i < resultLines.length; i += 1) {
|
|
144
|
+
if (!inserted && resultLines[i].trim() && !resultLines[i].trim().startsWith('#')) {
|
|
145
|
+
withTop.push(...topInsert, '');
|
|
146
|
+
inserted = true;
|
|
147
|
+
}
|
|
148
|
+
withTop.push(resultLines[i]);
|
|
149
|
+
}
|
|
150
|
+
if (!inserted) {
|
|
151
|
+
resultLines = [...topInsert, '', ...resultLines];
|
|
152
|
+
} else {
|
|
153
|
+
resultLines = withTop;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!sawProviderSection) {
|
|
158
|
+
if (resultLines.length && resultLines[resultLines.length - 1].trim() !== '') {
|
|
159
|
+
resultLines.push('');
|
|
160
|
+
}
|
|
161
|
+
resultLines.push(`[model_providers.${provider}]`);
|
|
162
|
+
resultLines.push('name = "ATools"');
|
|
163
|
+
resultLines.push(`base_url = "${baseUrl}"`);
|
|
164
|
+
resultLines.push('wire_api = "responses"');
|
|
165
|
+
resultLines.push('requires_openai_auth = false');
|
|
166
|
+
resultLines.push(`env_key = "${ENV_KEY}"`);
|
|
167
|
+
resultLines.push('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return resultLines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function upsertExportLine(content, key, value) {
|
|
174
|
+
const line = `export ${key}=${value}`;
|
|
175
|
+
const re = new RegExp(`^\s*export\s+${key}=.*$`, 'm');
|
|
176
|
+
if (re.test(content)) {
|
|
177
|
+
return content.replace(re, line);
|
|
178
|
+
}
|
|
179
|
+
if (!content || !content.trim()) {
|
|
180
|
+
return `${line}\n`;
|
|
181
|
+
}
|
|
182
|
+
return `${content.replace(/\s*$/, '')}\n${line}\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function detectPosixProfile() {
|
|
186
|
+
const shell = process.env.SHELL || '';
|
|
187
|
+
const home = os.homedir();
|
|
188
|
+
if (shell.includes('zsh')) return path.join(home, '.zshrc');
|
|
189
|
+
if (shell.includes('bash')) return path.join(home, '.bashrc');
|
|
190
|
+
return path.join(home, '.bashrc');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function persistEnvVar(key, value) {
|
|
194
|
+
if (!value) {
|
|
195
|
+
return { changed: false, reason: 'empty', type: '' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.platform === 'win32') {
|
|
199
|
+
try {
|
|
200
|
+
const out = spawnSync('setx', [key, value], { encoding: 'utf8' });
|
|
201
|
+
if (out.status !== 0) {
|
|
202
|
+
return {
|
|
203
|
+
changed: false,
|
|
204
|
+
type: 'windows',
|
|
205
|
+
reason: String(out.stderr || out.stdout || 'setx failed')
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { changed: true, type: 'windows', reason: '' };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { changed: false, type: 'windows', reason: String(err) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const profilePath = detectPosixProfile();
|
|
215
|
+
let content = '';
|
|
216
|
+
if (fs.existsSync(profilePath)) {
|
|
217
|
+
content = fs.readFileSync(profilePath, 'utf8');
|
|
218
|
+
}
|
|
219
|
+
const next = upsertExportLine(content, key, value);
|
|
220
|
+
if (next === content) {
|
|
221
|
+
return { changed: false, type: 'posix', path: profilePath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (content) {
|
|
225
|
+
backup(profilePath);
|
|
226
|
+
}
|
|
227
|
+
ensureDir(path.dirname(profilePath));
|
|
228
|
+
fs.writeFileSync(profilePath, next, 'utf8');
|
|
229
|
+
return { changed: true, type: 'posix', path: profilePath };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function canUseArrowMenu() {
|
|
233
|
+
return Boolean(
|
|
234
|
+
process.stdin.isTTY
|
|
235
|
+
&& process.stdout.isTTY
|
|
236
|
+
&& typeof process.stdin.setRawMode === 'function'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
let index = defaultIndex;
|
|
243
|
+
|
|
244
|
+
const draw = () => {
|
|
245
|
+
readline.clearLine(process.stdout, 0);
|
|
246
|
+
readline.cursorTo(process.stdout, 0);
|
|
247
|
+
const label = `${index + 1}. ${options[index]}`;
|
|
248
|
+
const line = withColor(`> ${label}`, ANSI.cyan, ANSI.bold);
|
|
249
|
+
process.stdout.write(`${line} (↑/↓, Enter; 数字直接跳转)`);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
process.stdin.off('keypress', onKeyPress);
|
|
254
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
255
|
+
process.stdin.setRawMode(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const onKeyPress = (str, key = {}) => {
|
|
260
|
+
if (key.ctrl && key.name === 'c') {
|
|
261
|
+
cleanup();
|
|
262
|
+
reject(new Error('aborted by user'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key.name === 'up') {
|
|
266
|
+
index = (index - 1 + options.length) % options.length;
|
|
267
|
+
draw();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.name === 'down') {
|
|
271
|
+
index = (index + 1) % options.length;
|
|
272
|
+
draw();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
276
|
+
cleanup();
|
|
277
|
+
readline.clearLine(process.stdout, 0);
|
|
278
|
+
readline.cursorTo(process.stdout, 0);
|
|
279
|
+
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
280
|
+
resolve(options[index]);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const n = Number(str);
|
|
284
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
285
|
+
index = n - 1;
|
|
286
|
+
draw();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
process.stdout.write(`\n${question}\n`);
|
|
292
|
+
readline.emitKeypressEvents(process.stdin);
|
|
293
|
+
process.stdin.setRawMode(true);
|
|
294
|
+
process.stdin.resume();
|
|
295
|
+
process.stdin.on('keypress', onKeyPress);
|
|
296
|
+
draw();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
cleanup();
|
|
299
|
+
reject(err);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function askModel(rl, currentModel = DEFAULT_MODEL_ID) {
|
|
305
|
+
const defaultIndex = Math.max(0, MODEL_OPTIONS.indexOf(currentModel));
|
|
306
|
+
if (canUseArrowMenu()) {
|
|
307
|
+
return askChoiceArrow('1) 选择默认模型', MODEL_OPTIONS, defaultIndex);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
while (true) {
|
|
311
|
+
process.stdout.write('\n1) 选择默认模型\n');
|
|
312
|
+
MODEL_OPTIONS.forEach((m, idx) => {
|
|
313
|
+
const mark = idx === defaultIndex ? ' (default)' : '';
|
|
314
|
+
process.stdout.write(` ${idx + 1}. ${m}${mark}\n`);
|
|
315
|
+
});
|
|
316
|
+
const input = (await rl.question('请选择编号: ')).trim();
|
|
317
|
+
if (!input) return MODEL_OPTIONS[defaultIndex];
|
|
318
|
+
const n = Number(input);
|
|
319
|
+
if (Number.isInteger(n) && n >= 1 && n <= MODEL_OPTIONS.length) {
|
|
320
|
+
return MODEL_OPTIONS[n - 1];
|
|
321
|
+
}
|
|
322
|
+
process.stdout.write('输入无效,请重新输入。\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function askKey(rl, currentKey) {
|
|
327
|
+
const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
|
|
328
|
+
process.stdout.write(`\n2) 输入 ATools API Key(当前: ${masked})\n`);
|
|
329
|
+
process.stdout.write('留空表示保持当前 key\n');
|
|
330
|
+
const input = (await rl.question('请输入 key: ')).trim();
|
|
331
|
+
return input || currentKey || '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function askKeyWithGuard(rl, currentKey) {
|
|
335
|
+
let selectedKey = await askKey(rl, currentKey);
|
|
336
|
+
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
337
|
+
process.stdout.write(`\n${warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。')}\n`);
|
|
338
|
+
const choice = (await rl.question('是否重新输入 ATools key? [Y/n]: ')).trim().toLowerCase();
|
|
339
|
+
if (choice === 'n') break;
|
|
340
|
+
selectedKey = await askKey(rl, selectedKey);
|
|
341
|
+
}
|
|
342
|
+
return selectedKey;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function runConfigCodex({ codexHome } = {}) {
|
|
346
|
+
const home = codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
347
|
+
const configPath = path.join(home, 'config.toml');
|
|
348
|
+
|
|
349
|
+
const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
350
|
+
|
|
351
|
+
process.stdout.write(`${sectionTitle('Codex 配置')}:\n`);
|
|
352
|
+
process.stdout.write(`- Home: ${bold(home)}\n`);
|
|
353
|
+
process.stdout.write(`- Provider: ${bold(PROVIDER)}\n`);
|
|
354
|
+
process.stdout.write(`- Upstream Base URL: ${bold(UPSTREAM_BASE_URL)}\n`);
|
|
355
|
+
process.stdout.write(`- Env Var: ${bold(ENV_KEY)}\n`);
|
|
356
|
+
|
|
357
|
+
const initialKey = process.env[ENV_KEY] || process.env.SUB_OAI_KEY || '';
|
|
358
|
+
|
|
359
|
+
let selectedModel = DEFAULT_MODEL_ID;
|
|
360
|
+
let selectedKey = initialKey;
|
|
361
|
+
|
|
362
|
+
const rl = readlinePromises.createInterface({
|
|
363
|
+
input: process.stdin,
|
|
364
|
+
output: process.stdout
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
selectedModel = await askModel(rl, DEFAULT_MODEL_ID);
|
|
369
|
+
selectedKey = await askKeyWithGuard(rl, initialKey);
|
|
370
|
+
} finally {
|
|
371
|
+
rl.close();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const next = patchCodexConfigText(existing, { modelId: selectedModel });
|
|
375
|
+
|
|
376
|
+
ensureDir(path.dirname(configPath));
|
|
377
|
+
if (existing && next !== existing) {
|
|
378
|
+
const bak = backup(configPath);
|
|
379
|
+
if (bak) {
|
|
380
|
+
process.stdout.write(`${ok(`[atools-tool] backup created: ${bak}`)}\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (next !== existing) {
|
|
385
|
+
fs.writeFileSync(configPath, `${next.trimEnd()}\n`, 'utf8');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let envResult = { changed: false, type: '', reason: '' };
|
|
389
|
+
if (selectedKey) {
|
|
390
|
+
envResult = persistEnvVar(ENV_KEY, selectedKey);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
process.stdout.write(`\n${sectionTitle('[atools-tool] Codex 配置结果')}:\n`);
|
|
394
|
+
process.stdout.write(`- Config: ${bold(configPath)}\n`);
|
|
395
|
+
process.stdout.write(`- model_provider = "${PROVIDER}"\n`);
|
|
396
|
+
process.stdout.write(`- model = "${selectedModel}"\n`);
|
|
397
|
+
process.stdout.write(`- base_url = "${UPSTREAM_BASE_URL}"\n`);
|
|
398
|
+
|
|
399
|
+
process.stdout.write(`\n${sectionTitle('Environment (持久化)')}:\n`);
|
|
400
|
+
if (!selectedKey) {
|
|
401
|
+
process.stdout.write(`${warn('- ATools key 未提供,未更新环境变量。')}\n`);
|
|
402
|
+
} else if (envResult.type === 'windows') {
|
|
403
|
+
if (envResult.changed) {
|
|
404
|
+
process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to Windows user env (setx).`)}\n`);
|
|
405
|
+
process.stdout.write('- 请重新打开终端,使新的环境变量生效。\n');
|
|
406
|
+
} else {
|
|
407
|
+
process.stdout.write(`${warn(`- Failed to persist ${ENV_KEY} via setx: ${envResult.reason}`)}\n`);
|
|
408
|
+
}
|
|
409
|
+
} else if (envResult.type === 'posix') {
|
|
410
|
+
if (envResult.changed) {
|
|
411
|
+
process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to shell profile: ${envResult.path}`)}\n`);
|
|
412
|
+
process.stdout.write('- 请重新打开终端,或手动 source 该文件以生效。\n');
|
|
413
|
+
} else {
|
|
414
|
+
process.stdout.write(`- ${ENV_KEY} 已在 profile 中存在(${envResult.path}),未做修改。\n`);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
process.stdout.write(`- Env var not persisted (reason: ${envResult.reason || 'unknown'}).\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (selectedKey) {
|
|
421
|
+
process.stdout.write(`\n${sectionTitle('Environment (当前会话手动生效命令)')}:\n`);
|
|
422
|
+
if (process.platform === 'win32') {
|
|
423
|
+
process.stdout.write(`- CMD: ${bold(`set ${ENV_KEY}=${selectedKey}`)}\n`);
|
|
424
|
+
process.stdout.write(`- PowerShell: ${bold(`$env:${ENV_KEY}="${selectedKey}"`)}\n`);
|
|
425
|
+
} else {
|
|
426
|
+
process.stdout.write(`- bash/zsh: ${bold(`export ${ENV_KEY}=${selectedKey}`)}\n`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
package/lib/config-openclaw.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import readlinePromises from 'node:readline/promises';
|
|
9
9
|
import readline from 'node:readline';
|
|
10
10
|
|
|
11
|
-
const PROVIDER = '
|
|
11
|
+
const PROVIDER = 'atools';
|
|
12
12
|
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
13
13
|
const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
14
14
|
|
|
@@ -19,6 +19,38 @@ const PROXY_UPSTREAM = 'https://sub2api.atools.live';
|
|
|
19
19
|
const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
20
20
|
const PROXY_SERVICE = 'openclaw-atools-proxy.service';
|
|
21
21
|
|
|
22
|
+
const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
|
|
23
|
+
const ANSI = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
bold: '\x1b[1m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function withColor(text, ...codes) {
|
|
32
|
+
if (!USE_COLOR || !codes.length) return text;
|
|
33
|
+
const prefix = codes.join("");
|
|
34
|
+
return prefix + text + ANSI.reset;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sectionTitle(text) {
|
|
38
|
+
return withColor(text, ANSI.cyan, ANSI.bold);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ok(text) {
|
|
42
|
+
return withColor(text, ANSI.green);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function warn(text) {
|
|
46
|
+
return withColor(text, ANSI.yellow);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bold(text) {
|
|
50
|
+
return withColor(text, ANSI.bold);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
22
54
|
function readJson(filePath) {
|
|
23
55
|
if (!fs.existsSync(filePath)) {
|
|
24
56
|
throw new Error(`missing file: ${filePath}`);
|
|
@@ -71,59 +103,64 @@ async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
|
|
|
71
103
|
async function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
72
104
|
return new Promise((resolve, reject) => {
|
|
73
105
|
let index = defaultIndex;
|
|
74
|
-
process.stdout.write(
|
|
75
|
-
|
|
106
|
+
process.stdout.write("\n" + question + "\n");
|
|
107
|
+
let firstDraw = true;
|
|
76
108
|
const draw = () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
if (!firstDraw) {
|
|
110
|
+
readline.moveCursor(process.stdout, 0, -options.length);
|
|
111
|
+
} else {
|
|
112
|
+
firstDraw = false;
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
115
|
+
readline.clearLine(process.stdout, 0);
|
|
116
|
+
const prefix = i === index ? '> ' : ' ';
|
|
117
|
+
const label = String(i + 1) + ". " + String(options[i]);
|
|
118
|
+
let line = prefix + label;
|
|
119
|
+
if (i === index) {
|
|
120
|
+
line = withColor(line, ANSI.cyan, ANSI.bold) + ' (↑/↓, Enter; 数字直接跳转)';
|
|
121
|
+
}
|
|
122
|
+
process.stdout.write(line + "\n");
|
|
123
|
+
}
|
|
80
124
|
};
|
|
81
|
-
|
|
82
125
|
const cleanup = () => {
|
|
83
|
-
process.stdin.off(
|
|
84
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode ===
|
|
126
|
+
process.stdin.off("keypress", onKeyPress);
|
|
127
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
85
128
|
process.stdin.setRawMode(false);
|
|
86
129
|
}
|
|
87
130
|
};
|
|
88
|
-
|
|
89
131
|
const onKeyPress = (str, key = {}) => {
|
|
90
|
-
if (key.ctrl && key.name ===
|
|
132
|
+
if (key.ctrl && key.name === "c") {
|
|
91
133
|
cleanup();
|
|
92
|
-
reject(new Error(
|
|
134
|
+
reject(new Error("aborted by user"));
|
|
93
135
|
return;
|
|
94
136
|
}
|
|
95
|
-
|
|
96
|
-
if (key.name === 'up') {
|
|
137
|
+
if (key.name === "up") {
|
|
97
138
|
index = (index - 1 + options.length) % options.length;
|
|
98
139
|
draw();
|
|
99
140
|
return;
|
|
100
141
|
}
|
|
101
|
-
if (key.name ===
|
|
142
|
+
if (key.name === "down") {
|
|
102
143
|
index = (index + 1) % options.length;
|
|
103
144
|
draw();
|
|
104
145
|
return;
|
|
105
146
|
}
|
|
106
|
-
if (key.name ===
|
|
147
|
+
if (key.name === "return" || key.name === "enter") {
|
|
107
148
|
cleanup();
|
|
108
|
-
|
|
109
|
-
readline.cursorTo(process.stdout, 0);
|
|
110
|
-
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
149
|
+
process.stdout.write("已选择: " + String(options[index]) + "\n");
|
|
111
150
|
resolve(options[index]);
|
|
112
151
|
return;
|
|
113
152
|
}
|
|
114
|
-
|
|
115
153
|
const n = Number(str);
|
|
116
154
|
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
117
155
|
index = n - 1;
|
|
118
156
|
draw();
|
|
119
157
|
}
|
|
120
158
|
};
|
|
121
|
-
|
|
122
159
|
try {
|
|
123
160
|
readline.emitKeypressEvents(process.stdin);
|
|
124
161
|
process.stdin.setRawMode(true);
|
|
125
162
|
process.stdin.resume();
|
|
126
|
-
process.stdin.on(
|
|
163
|
+
process.stdin.on("keypress", onKeyPress);
|
|
127
164
|
draw();
|
|
128
165
|
} catch (err) {
|
|
129
166
|
cleanup();
|
|
@@ -207,7 +244,7 @@ async function askKey(rl, currentKey) {
|
|
|
207
244
|
async function askKeyWithGuard(rl, currentKey) {
|
|
208
245
|
let selectedKey = await askKey(rl, currentKey);
|
|
209
246
|
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
210
|
-
process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于
|
|
247
|
+
process.stdout.write('\n' + warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。') + '\n');
|
|
211
248
|
const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
|
|
212
249
|
if (choice === '继续使用当前 key') break;
|
|
213
250
|
selectedKey = await askKey(rl, selectedKey);
|
|
@@ -369,7 +406,7 @@ function buildProxyTroubleshootingHints({ linuxSystemdError } = {}) {
|
|
|
369
406
|
|
|
370
407
|
if (/Failed to connect to bus|No medium found/i.test(err)) {
|
|
371
408
|
hints.push('当前环境没有可用的 systemd 用户会话(常见于容器、root、无登录会话)。');
|
|
372
|
-
hints.push('建议:直接重新执行 `atools-tool-install
|
|
409
|
+
hints.push('建议:直接重新执行 `atools-tool install-proxy`(或兼容别名 `atools-tool-install`),新版会自动降级到非-systemd进程模式。');
|
|
373
410
|
hints.push('如果是容器环境,请确保进程管理策略允许后台常驻进程。');
|
|
374
411
|
return hints;
|
|
375
412
|
}
|
|
@@ -495,9 +532,10 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
495
532
|
let rl = null;
|
|
496
533
|
|
|
497
534
|
try {
|
|
498
|
-
process.stdout.write(
|
|
499
|
-
process.stdout.write(
|
|
500
|
-
process.stdout.write(
|
|
535
|
+
process.stdout.write(sectionTitle('OpenClaw / ATools 配置') + ':\n');
|
|
536
|
+
process.stdout.write('- OpenClaw Home: ' + bold(home) + '\n');
|
|
537
|
+
process.stdout.write('- 固定代理目标: ' + bold(PROXY_UPSTREAM) + '\n');
|
|
538
|
+
process.stdout.write('- 固定 Provider Base URL: ' + bold(PROXY_BASE_URL) + '\n');
|
|
501
539
|
|
|
502
540
|
const proxyStatus = await ensureProxyReady();
|
|
503
541
|
if (!proxyStatus.ok) {
|
|
@@ -521,10 +559,13 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
521
559
|
const selectedThinking = await askChoice(rl, '3) 思考强度', THINKING_OPTIONS, 2);
|
|
522
560
|
const reasoning = reasoningSwitch === 'on';
|
|
523
561
|
const currentKey =
|
|
524
|
-
openclawCfg?.models?.providers?.[PROVIDER]?.apiKey
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
562
|
+
openclawCfg?.models?.providers?.[PROVIDER]?.apiKey
|
|
563
|
+
|| openclawCfg?.models?.providers?.sub2api?.apiKey
|
|
564
|
+
|| modelsCfg?.providers?.[PROVIDER]?.apiKey
|
|
565
|
+
|| modelsCfg?.providers?.sub2api?.apiKey
|
|
566
|
+
|| authCfg?.profiles?.[`${PROVIDER}:default`]?.key
|
|
567
|
+
|| authCfg?.profiles?.['sub2api:default']?.key
|
|
568
|
+
|| '';
|
|
528
569
|
const selectedKey = await askKeyWithGuard(rl, currentKey);
|
|
529
570
|
|
|
530
571
|
openclawCfg.models = openclawCfg.models || {};
|
|
@@ -553,8 +594,10 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
553
594
|
if (Array.isArray(openclawCfg.agents.list)) {
|
|
554
595
|
openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
|
|
555
596
|
if (!agent || typeof agent !== 'object') return agent;
|
|
556
|
-
if (typeof agent.model === 'string'
|
|
557
|
-
|
|
597
|
+
if (typeof agent.model === 'string') {
|
|
598
|
+
if (agent.model.startsWith(`${PROVIDER}/`) || agent.model.startsWith('sub2api/')) {
|
|
599
|
+
return { ...agent, model: `${PROVIDER}/${selectedModel}` };
|
|
600
|
+
}
|
|
558
601
|
}
|
|
559
602
|
return agent;
|
|
560
603
|
});
|
package/lib/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import { execFileSync, spawn } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
const DEFAULT_PROVIDER = '
|
|
7
|
+
const DEFAULT_PROVIDER = 'atools';
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-5.3-codex';
|
|
9
9
|
const DEFAULT_FORCE_MODEL = '';
|
|
10
10
|
const DEFAULT_PORT = 18888;
|
|
@@ -95,7 +95,7 @@ function parseArgs(argv) {
|
|
|
95
95
|
maxReqBytes: 68000,
|
|
96
96
|
logFile: DEFAULT_LOG_FILE,
|
|
97
97
|
serviceName: DEFAULT_SERVICE_NAME,
|
|
98
|
-
apiKey: process.env.SUB_OAI_KEY || '',
|
|
98
|
+
apiKey: process.env.ATOOLS_OAI_KEY || process.env.SUB_OAI_KEY || '',
|
|
99
99
|
dryRun: false,
|
|
100
100
|
noRestart: false
|
|
101
101
|
};
|
|
@@ -193,7 +193,7 @@ function detectApiKey(openclawHome, explicitKey) {
|
|
|
193
193
|
if (explicitKey) return explicitKey;
|
|
194
194
|
const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
|
|
195
195
|
const auth = readJsonIfExists(authPath);
|
|
196
|
-
return auth?.profiles?.['sub2api:default']?.key || '';
|
|
196
|
+
return (auth?.profiles?.['atools:default']?.key || auth?.profiles?.['sub2api:default']?.key || '');
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
function patchOpenclawConfig(openclawHome, provider, model, port, dryRun) {
|
|
@@ -547,14 +547,14 @@ function restartOpenclawGateway({ noRestart, dryRun }) {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
function printHelp() {
|
|
550
|
-
process.stdout.write(`atools-tool-
|
|
550
|
+
process.stdout.write(`atools-tool install-proxy
|
|
551
551
|
|
|
552
552
|
Usage:
|
|
553
|
-
atools-tool-
|
|
553
|
+
atools-tool install-proxy [options]
|
|
554
554
|
|
|
555
555
|
Options:
|
|
556
556
|
--openclaw-home <path> Default: ~/.openclaw
|
|
557
|
-
--provider <name> Default:
|
|
557
|
+
--provider <name> Default: atools
|
|
558
558
|
--model <id> Default: gpt-5.3-codex
|
|
559
559
|
--force-model <id> Default: disabled (optional hard rewrite)
|
|
560
560
|
--port <number> Default: 18888
|
|
@@ -562,7 +562,7 @@ Options:
|
|
|
562
562
|
--max-req-bytes <number> Default: 68000
|
|
563
563
|
--log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
|
|
564
564
|
--service-name <name> Linux: *.service; macOS/Windows: service id/task name
|
|
565
|
-
--api-key <key> Prefer explicit key; otherwise use SUB_OAI_KEY or auth-profiles
|
|
565
|
+
--api-key <key> Prefer explicit key; otherwise use ATOOLS_OAI_KEY (or legacy SUB_OAI_KEY) or auth-profiles
|
|
566
566
|
--no-restart Do not restart openclaw-gateway.service
|
|
567
567
|
--dry-run Print actions without changing files
|
|
568
568
|
-h, --help Show help
|