@autotask/atools-tool 0.1.2 → 0.1.3
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 +36 -8
- package/bin/cli.mjs +3 -2
- package/lib/config-openclaw.mjs +385 -10
- package/lib/install.mjs +206 -24
- package/lib/proxy-server.mjs +85 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,20 +15,39 @@ atools-tool <command>
|
|
|
15
15
|
Available command:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
atools-tool
|
|
18
|
+
atools-tool openclaw
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Interactive flow:
|
|
22
22
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3. Configure
|
|
26
|
-
4.
|
|
23
|
+
1. Check local proxy status (`127.0.0.1:18888`); if not running, ask whether to start it first
|
|
24
|
+
2. Select default model: `gpt-5.2` / `gpt-5.3-codex` / `gpt-5.4`
|
|
25
|
+
3. Configure reasoning: `off` / `on`
|
|
26
|
+
4. Configure thinking strength: `off|minimal|low|medium|high|xhigh`
|
|
27
|
+
5. Input API key (leave empty to keep current key)
|
|
28
|
+
|
|
29
|
+
Selection UX:
|
|
30
|
+
|
|
31
|
+
- TTY terminal: use `↑/↓` and `Enter`
|
|
32
|
+
- Non-TTY: falls back to number input
|
|
33
|
+
|
|
34
|
+
Rollback to codex-compatible defaults:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
atools-tool openclaw
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Recommended selections:
|
|
41
|
+
|
|
42
|
+
1. `gpt-5.3-codex`
|
|
43
|
+
2. `off`
|
|
44
|
+
3. `low`
|
|
45
|
+
4. Input your sub2api-compatible key
|
|
27
46
|
|
|
28
47
|
## Install (npm)
|
|
29
48
|
|
|
30
49
|
```bash
|
|
31
|
-
npm i -g atools-tool
|
|
50
|
+
npm i -g @autotask/atools-tool
|
|
32
51
|
```
|
|
33
52
|
|
|
34
53
|
Optional installer command:
|
|
@@ -37,6 +56,12 @@ Optional installer command:
|
|
|
37
56
|
atools-tool-install --help
|
|
38
57
|
```
|
|
39
58
|
|
|
59
|
+
`atools-tool-install` service behavior by platform:
|
|
60
|
+
|
|
61
|
+
- Linux: systemd user service
|
|
62
|
+
- macOS: launchd user agent (`~/Library/LaunchAgents`)
|
|
63
|
+
- Windows: Task Scheduler user task (ONLOGON + start now)
|
|
64
|
+
|
|
40
65
|
## Proxy Command
|
|
41
66
|
|
|
42
67
|
```bash
|
|
@@ -62,8 +87,11 @@ curl -fsSL https://raw.githubusercontent.com/aak1247/sub2api-openclaw-proxy/main
|
|
|
62
87
|
- patches `~/.openclaw/openclaw.json`
|
|
63
88
|
- patches `~/.openclaw/agents/main/agent/models.json`
|
|
64
89
|
- patches `~/.openclaw/agents/main/agent/auth-profiles.json` (if API key is available)
|
|
65
|
-
-
|
|
66
|
-
-
|
|
90
|
+
- service install by platform:
|
|
91
|
+
- Linux: creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
|
|
92
|
+
- macOS: creates/enables `~/Library/LaunchAgents/openclaw-atools-proxy.plist`
|
|
93
|
+
- Windows: creates/updates scheduled task `openclaw-atools-proxy`
|
|
94
|
+
- restarts `openclaw-gateway.service` only on Linux (unless `--no-restart`)
|
|
67
95
|
|
|
68
96
|
Default proxy log:
|
|
69
97
|
|
package/bin/cli.mjs
CHANGED
|
@@ -47,7 +47,8 @@ Usage:
|
|
|
47
47
|
|
|
48
48
|
Commands:
|
|
49
49
|
serve Run proxy service
|
|
50
|
-
|
|
50
|
+
openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
|
|
51
|
+
config-openclaw Alias of 'openclaw'
|
|
51
52
|
|
|
52
53
|
Options:
|
|
53
54
|
--openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
|
|
@@ -71,7 +72,7 @@ async function main() {
|
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
if (args.cmd === 'config-openclaw') {
|
|
75
|
+
if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
|
|
75
76
|
await runConfigOpenclaw({ openclawHome: args.openclawHome });
|
|
76
77
|
return;
|
|
77
78
|
}
|
package/lib/config-openclaw.mjs
CHANGED
|
@@ -2,12 +2,23 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import readlinePromises from 'node:readline/promises';
|
|
9
|
+
import readline from 'node:readline';
|
|
6
10
|
|
|
7
11
|
const PROVIDER = 'sub2api';
|
|
8
12
|
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
9
13
|
const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
10
14
|
|
|
15
|
+
const PROXY_HOST = '127.0.0.1';
|
|
16
|
+
const PROXY_PORT = 18888;
|
|
17
|
+
const PROXY_BASE_URL = `http://${PROXY_HOST}:${PROXY_PORT}/v1`;
|
|
18
|
+
const PROXY_UPSTREAM = 'https://sub2api.atools.live';
|
|
19
|
+
const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
20
|
+
const PROXY_SERVICE = 'openclaw-atools-proxy.service';
|
|
21
|
+
|
|
11
22
|
function readJson(filePath) {
|
|
12
23
|
if (!fs.existsSync(filePath)) {
|
|
13
24
|
throw new Error(`missing file: ${filePath}`);
|
|
@@ -28,7 +39,19 @@ function backup(filePath) {
|
|
|
28
39
|
fs.copyFileSync(filePath, `${filePath}.bak.${stamp}`);
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canUseArrowMenu() {
|
|
47
|
+
return Boolean(
|
|
48
|
+
process.stdin.isTTY
|
|
49
|
+
&& process.stdout.isTTY
|
|
50
|
+
&& typeof process.stdin.setRawMode === 'function'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
|
|
32
55
|
while (true) {
|
|
33
56
|
process.stdout.write(`\n${question}\n`);
|
|
34
57
|
options.forEach((item, idx) => {
|
|
@@ -45,14 +68,153 @@ async function askChoice(rl, question, options, defaultIndex = 0) {
|
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
70
|
|
|
71
|
+
async function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let index = defaultIndex;
|
|
74
|
+
process.stdout.write(`\n${question}\n`);
|
|
75
|
+
|
|
76
|
+
const draw = () => {
|
|
77
|
+
readline.clearLine(process.stdout, 0);
|
|
78
|
+
readline.cursorTo(process.stdout, 0);
|
|
79
|
+
process.stdout.write(`> ${options[index]} (↑/↓, Enter)`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const cleanup = () => {
|
|
83
|
+
process.stdin.off('keypress', onKeyPress);
|
|
84
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
85
|
+
process.stdin.setRawMode(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onKeyPress = (str, key = {}) => {
|
|
90
|
+
if (key.ctrl && key.name === 'c') {
|
|
91
|
+
cleanup();
|
|
92
|
+
reject(new Error('aborted by user'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (key.name === 'up') {
|
|
97
|
+
index = (index - 1 + options.length) % options.length;
|
|
98
|
+
draw();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.name === 'down') {
|
|
102
|
+
index = (index + 1) % options.length;
|
|
103
|
+
draw();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
107
|
+
cleanup();
|
|
108
|
+
readline.clearLine(process.stdout, 0);
|
|
109
|
+
readline.cursorTo(process.stdout, 0);
|
|
110
|
+
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
111
|
+
resolve(options[index]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const n = Number(str);
|
|
116
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
117
|
+
index = n - 1;
|
|
118
|
+
draw();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
readline.emitKeypressEvents(process.stdin);
|
|
124
|
+
process.stdin.setRawMode(true);
|
|
125
|
+
process.stdin.resume();
|
|
126
|
+
process.stdin.on('keypress', onKeyPress);
|
|
127
|
+
draw();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function askChoice(rl, question, options, defaultIndex = 0) {
|
|
136
|
+
if (!canUseArrowMenu()) {
|
|
137
|
+
return askChoiceNumber(rl, question, options, defaultIndex);
|
|
138
|
+
}
|
|
139
|
+
return askChoiceArrow(question, options, defaultIndex);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function askInputArrow(defaultValue = '') {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
let value = '';
|
|
145
|
+
process.stdout.write('请输入 key: ');
|
|
146
|
+
|
|
147
|
+
const draw = () => {
|
|
148
|
+
readline.clearLine(process.stdout, 1);
|
|
149
|
+
readline.cursorTo(process.stdout, 10);
|
|
150
|
+
process.stdout.write(value);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
process.stdin.off('keypress', onKeyPress);
|
|
155
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
156
|
+
process.stdin.setRawMode(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const onKeyPress = (str, key = {}) => {
|
|
161
|
+
if (key.ctrl && key.name === 'c') {
|
|
162
|
+
cleanup();
|
|
163
|
+
reject(new Error('aborted by user'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
167
|
+
cleanup();
|
|
168
|
+
process.stdout.write('\n');
|
|
169
|
+
resolve(value.trim() || defaultValue || '');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.name === 'backspace') {
|
|
173
|
+
value = value.slice(0, -1);
|
|
174
|
+
draw();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!key.ctrl && !key.meta && typeof str === 'string' && str.length > 0) {
|
|
178
|
+
value += str;
|
|
179
|
+
draw();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
readline.emitKeypressEvents(process.stdin);
|
|
185
|
+
process.stdin.setRawMode(true);
|
|
186
|
+
process.stdin.resume();
|
|
187
|
+
process.stdin.on('keypress', onKeyPress);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
48
195
|
async function askKey(rl, currentKey) {
|
|
49
196
|
const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
|
|
50
|
-
|
|
197
|
+
const question = `4) 输入 API Key(当前: ${masked})`;
|
|
198
|
+
process.stdout.write(`\n${question}\n`);
|
|
51
199
|
process.stdout.write('留空表示保持当前 key\n');
|
|
200
|
+
if (canUseArrowMenu()) {
|
|
201
|
+
return askInputArrow(currentKey);
|
|
202
|
+
}
|
|
52
203
|
const input = (await rl.question('请输入 key: ')).trim();
|
|
53
204
|
return input || currentKey || '';
|
|
54
205
|
}
|
|
55
206
|
|
|
207
|
+
async function askKeyWithGuard(rl, currentKey) {
|
|
208
|
+
let selectedKey = await askKey(rl, currentKey);
|
|
209
|
+
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
210
|
+
process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 sub2api /v1 代理模式。\n');
|
|
211
|
+
const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
|
|
212
|
+
if (choice === '继续使用当前 key') break;
|
|
213
|
+
selectedKey = await askKey(rl, selectedKey);
|
|
214
|
+
}
|
|
215
|
+
return selectedKey;
|
|
216
|
+
}
|
|
217
|
+
|
|
56
218
|
function ensureModel(providerObj, modelId, reasoning) {
|
|
57
219
|
providerObj.models = providerObj.models || [];
|
|
58
220
|
let model = providerObj.models.find((m) => m && m.id === modelId);
|
|
@@ -75,6 +237,190 @@ function ensureModel(providerObj, modelId, reasoning) {
|
|
|
75
237
|
model.api = model.api || 'openai-responses';
|
|
76
238
|
}
|
|
77
239
|
|
|
240
|
+
function escapeRegExp(text) {
|
|
241
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function upsertEnvLine(content, key, value) {
|
|
245
|
+
const line = `Environment=${key}=${value}`;
|
|
246
|
+
const re = new RegExp(`^Environment=${escapeRegExp(key)}=.*$`, 'm');
|
|
247
|
+
if (re.test(content)) {
|
|
248
|
+
return content.replace(re, line);
|
|
249
|
+
}
|
|
250
|
+
const serviceIdx = content.indexOf('[Service]');
|
|
251
|
+
if (serviceIdx === -1) {
|
|
252
|
+
return `${content.trimEnd()}\n${line}\n`;
|
|
253
|
+
}
|
|
254
|
+
const afterServiceHeader = content.indexOf('\n', serviceIdx);
|
|
255
|
+
if (afterServiceHeader < 0) {
|
|
256
|
+
return `${content}\n${line}\n`;
|
|
257
|
+
}
|
|
258
|
+
return `${content.slice(0, afterServiceHeader + 1)}${line}\n${content.slice(afterServiceHeader + 1)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function configureLinuxProxyService() {
|
|
262
|
+
const servicePath = path.join(os.homedir(), '.config/systemd/user', PROXY_SERVICE);
|
|
263
|
+
if (!fs.existsSync(servicePath)) {
|
|
264
|
+
return { exists: false, changed: false, servicePath };
|
|
265
|
+
}
|
|
266
|
+
const original = fs.readFileSync(servicePath, 'utf8');
|
|
267
|
+
let next = original;
|
|
268
|
+
next = upsertEnvLine(next, 'SUB2API_UPSTREAM', PROXY_UPSTREAM);
|
|
269
|
+
next = upsertEnvLine(next, 'SUB2API_FALLBACK_UPSTREAM', '');
|
|
270
|
+
next = upsertEnvLine(next, 'SUB2API_FALLBACK_API_KEY', '');
|
|
271
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT', '0');
|
|
272
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_PORT', String(PROXY_PORT));
|
|
273
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_LOG', PROXY_LOG_FILE);
|
|
274
|
+
|
|
275
|
+
if (next === original) {
|
|
276
|
+
return { exists: true, changed: false, servicePath };
|
|
277
|
+
}
|
|
278
|
+
backup(servicePath);
|
|
279
|
+
fs.writeFileSync(servicePath, next, 'utf8');
|
|
280
|
+
return { exists: true, changed: true, servicePath };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function runSystemctl(args) {
|
|
284
|
+
const out = spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isLinuxProxyServiceActive() {
|
|
289
|
+
const out = runSystemctl(['is-active', PROXY_SERVICE]);
|
|
290
|
+
return out.status === 0 && String(out.stdout || '').trim() === 'active';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function startLinuxProxyService() {
|
|
294
|
+
const reload = runSystemctl(['daemon-reload']);
|
|
295
|
+
if (reload.status !== 0) return false;
|
|
296
|
+
const start = runSystemctl(['enable', '--now', PROXY_SERVICE]);
|
|
297
|
+
return start.status === 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function startDetachedProxy() {
|
|
301
|
+
try {
|
|
302
|
+
const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
|
|
303
|
+
const child = spawn(
|
|
304
|
+
process.execPath,
|
|
305
|
+
[
|
|
306
|
+
cliPath,
|
|
307
|
+
'serve',
|
|
308
|
+
'--port',
|
|
309
|
+
String(PROXY_PORT),
|
|
310
|
+
'--upstream',
|
|
311
|
+
PROXY_UPSTREAM,
|
|
312
|
+
'--log-file',
|
|
313
|
+
PROXY_LOG_FILE
|
|
314
|
+
],
|
|
315
|
+
{
|
|
316
|
+
detached: true,
|
|
317
|
+
stdio: 'ignore',
|
|
318
|
+
env: {
|
|
319
|
+
...process.env,
|
|
320
|
+
SUB2API_UPSTREAM: PROXY_UPSTREAM,
|
|
321
|
+
SUB2API_FALLBACK_UPSTREAM: '',
|
|
322
|
+
SUB2API_FALLBACK_API_KEY: ''
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
);
|
|
326
|
+
child.unref();
|
|
327
|
+
return true;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function isPortOpen(host, port, timeoutMs = 900) {
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
const socket = net.createConnection({ host, port });
|
|
336
|
+
let done = false;
|
|
337
|
+
|
|
338
|
+
const finish = (ok) => {
|
|
339
|
+
if (done) return;
|
|
340
|
+
done = true;
|
|
341
|
+
socket.destroy();
|
|
342
|
+
resolve(ok);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
socket.on('connect', () => finish(true));
|
|
346
|
+
socket.on('error', () => finish(false));
|
|
347
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function ensureProxyReady() {
|
|
352
|
+
if (process.platform === 'linux') {
|
|
353
|
+
const service = configureLinuxProxyService();
|
|
354
|
+
if (service.exists && service.changed) {
|
|
355
|
+
// Service file changed: reload + restart so it points back to sub2api.atools.live.
|
|
356
|
+
startLinuxProxyService();
|
|
357
|
+
await sleep(400);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
362
|
+
|
|
363
|
+
// Linux fallback: service may exist but not running.
|
|
364
|
+
if (process.platform === 'linux') {
|
|
365
|
+
const service = configureLinuxProxyService();
|
|
366
|
+
if (service.exists && isLinuxProxyServiceActive()) {
|
|
367
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
process.stdout.write('\n检测到本地代理未运行(127.0.0.1:18888)。\n');
|
|
372
|
+
const promptRl = readlinePromises.createInterface({
|
|
373
|
+
input: process.stdin,
|
|
374
|
+
output: process.stdout
|
|
375
|
+
});
|
|
376
|
+
let action = '';
|
|
377
|
+
try {
|
|
378
|
+
action = await askChoice(
|
|
379
|
+
promptRl,
|
|
380
|
+
'atools 模型依赖本地代理,是否现在启动代理?',
|
|
381
|
+
['启动代理(推荐)', '取消并退出'],
|
|
382
|
+
0
|
|
383
|
+
);
|
|
384
|
+
} finally {
|
|
385
|
+
promptRl.close();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (action === '取消并退出') {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let started = false;
|
|
393
|
+
if (process.platform === 'linux') {
|
|
394
|
+
started = startLinuxProxyService();
|
|
395
|
+
}
|
|
396
|
+
if (!started) {
|
|
397
|
+
started = startDetachedProxy();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!started) return false;
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < 5; i += 1) {
|
|
403
|
+
await sleep(500);
|
|
404
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) {
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function printRestartHint() {
|
|
412
|
+
process.stdout.write('\n后续建议:\n');
|
|
413
|
+
if (process.platform === 'linux') {
|
|
414
|
+
process.stdout.write('- Linux(systemd): systemctl --user restart openclaw-gateway.service\n');
|
|
415
|
+
} else if (process.platform === 'darwin') {
|
|
416
|
+
process.stdout.write('- macOS: 重启 OpenClaw 进程,或重新执行你本地的 openclaw 启动命令\n');
|
|
417
|
+
} else if (process.platform === 'win32') {
|
|
418
|
+
process.stdout.write('- Windows: 重启 OpenClaw 进程或服务(若以服务方式运行)\n');
|
|
419
|
+
} else {
|
|
420
|
+
process.stdout.write('- 请重启 OpenClaw 进程,使新配置生效\n');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
78
424
|
export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
79
425
|
const home = openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
|
|
80
426
|
const openclawJsonPath = path.join(home, 'openclaw.json');
|
|
@@ -83,17 +429,26 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
83
429
|
|
|
84
430
|
const openclawCfg = readJson(openclawJsonPath);
|
|
85
431
|
const modelsCfg = readJson(modelsJsonPath);
|
|
86
|
-
|
|
432
|
+
const authCfg = fs.existsSync(authProfilesPath)
|
|
87
433
|
? readJson(authProfilesPath)
|
|
88
434
|
: { version: 1, profiles: {}, lastGood: {}, usageStats: {} };
|
|
89
435
|
|
|
90
|
-
|
|
91
|
-
input: process.stdin,
|
|
92
|
-
output: process.stdout
|
|
93
|
-
});
|
|
436
|
+
let rl = null;
|
|
94
437
|
|
|
95
438
|
try {
|
|
96
439
|
process.stdout.write(`OpenClaw Home: ${home}\n`);
|
|
440
|
+
process.stdout.write(`固定代理目标: ${PROXY_UPSTREAM}\n`);
|
|
441
|
+
process.stdout.write(`固定 Provider Base URL: ${PROXY_BASE_URL}\n`);
|
|
442
|
+
|
|
443
|
+
const proxyReady = await ensureProxyReady();
|
|
444
|
+
if (!proxyReady) {
|
|
445
|
+
process.stdout.write('\n已取消:本地代理未就绪,未修改 OpenClaw 模型配置。\n');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
rl = readlinePromises.createInterface({
|
|
449
|
+
input: process.stdin,
|
|
450
|
+
output: process.stdout
|
|
451
|
+
});
|
|
97
452
|
|
|
98
453
|
const selectedModel = await askChoice(rl, '1) 选择默认模型', MODEL_OPTIONS, 1);
|
|
99
454
|
const reasoningSwitch = await askChoice(rl, '2) 配置推理开关', ['off', 'on'], 0);
|
|
@@ -104,13 +459,17 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
104
459
|
modelsCfg?.providers?.[PROVIDER]?.apiKey ||
|
|
105
460
|
authCfg?.profiles?.[`${PROVIDER}:default`]?.key ||
|
|
106
461
|
'';
|
|
107
|
-
const selectedKey = await
|
|
462
|
+
const selectedKey = await askKeyWithGuard(rl, currentKey);
|
|
108
463
|
|
|
109
464
|
openclawCfg.models = openclawCfg.models || {};
|
|
110
465
|
openclawCfg.models.providers = openclawCfg.models.providers || {};
|
|
111
466
|
openclawCfg.models.providers[PROVIDER] = openclawCfg.models.providers[PROVIDER] || {};
|
|
467
|
+
openclawCfg.models.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
|
|
112
468
|
openclawCfg.models.providers[PROVIDER].api = 'openai-responses';
|
|
113
469
|
openclawCfg.models.providers[PROVIDER].authHeader = true;
|
|
470
|
+
openclawCfg.models.providers[PROVIDER].headers = openclawCfg.models.providers[PROVIDER].headers || {};
|
|
471
|
+
openclawCfg.models.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
|
|
472
|
+
openclawCfg.models.providers[PROVIDER].headers.Accept = 'application/json';
|
|
114
473
|
if (selectedKey) {
|
|
115
474
|
openclawCfg.models.providers[PROVIDER].apiKey = selectedKey;
|
|
116
475
|
openclawCfg.models.providers[PROVIDER].auth = 'api-key';
|
|
@@ -125,11 +484,24 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
125
484
|
openclawCfg.agents.defaults.models = openclawCfg.agents.defaults.models || {};
|
|
126
485
|
openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] =
|
|
127
486
|
openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] || { alias: `ATools ${selectedModel}` };
|
|
487
|
+
if (Array.isArray(openclawCfg.agents.list)) {
|
|
488
|
+
openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
|
|
489
|
+
if (!agent || typeof agent !== 'object') return agent;
|
|
490
|
+
if (typeof agent.model === 'string' && agent.model.startsWith(`${PROVIDER}/`)) {
|
|
491
|
+
return { ...agent, model: `${PROVIDER}/${selectedModel}` };
|
|
492
|
+
}
|
|
493
|
+
return agent;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
128
496
|
|
|
129
497
|
modelsCfg.providers = modelsCfg.providers || {};
|
|
130
498
|
modelsCfg.providers[PROVIDER] = modelsCfg.providers[PROVIDER] || {};
|
|
499
|
+
modelsCfg.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
|
|
131
500
|
modelsCfg.providers[PROVIDER].api = 'openai-responses';
|
|
132
501
|
modelsCfg.providers[PROVIDER].authHeader = true;
|
|
502
|
+
modelsCfg.providers[PROVIDER].headers = modelsCfg.providers[PROVIDER].headers || {};
|
|
503
|
+
modelsCfg.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
|
|
504
|
+
modelsCfg.providers[PROVIDER].headers.Accept = 'application/json';
|
|
133
505
|
if (selectedKey) {
|
|
134
506
|
modelsCfg.providers[PROVIDER].apiKey = selectedKey;
|
|
135
507
|
modelsCfg.providers[PROVIDER].auth = 'api-key';
|
|
@@ -161,6 +533,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
161
533
|
writeJson(authProfilesPath, authCfg);
|
|
162
534
|
|
|
163
535
|
process.stdout.write('\n配置已更新:\n');
|
|
536
|
+
process.stdout.write(`- Proxy Upstream: ${PROXY_UPSTREAM}\n`);
|
|
537
|
+
process.stdout.write(`- Provider Base URL: ${PROXY_BASE_URL}\n`);
|
|
164
538
|
process.stdout.write(`- 默认模型: ${PROVIDER}/${selectedModel}\n`);
|
|
165
539
|
process.stdout.write(`- 推理开关: ${reasoning ? 'on' : 'off'}\n`);
|
|
166
540
|
process.stdout.write(`- 思考强度: ${selectedThinking}\n`);
|
|
@@ -168,7 +542,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
168
542
|
process.stdout.write(`- 已写入: ${openclawJsonPath}\n`);
|
|
169
543
|
process.stdout.write(`- 已写入: ${modelsJsonPath}\n`);
|
|
170
544
|
process.stdout.write(`- 已写入: ${authProfilesPath}\n`);
|
|
545
|
+
printRestartHint();
|
|
171
546
|
} finally {
|
|
172
|
-
rl.close();
|
|
547
|
+
if (rl) rl.close();
|
|
173
548
|
}
|
|
174
549
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -8,8 +8,8 @@ const DEFAULT_PROVIDER = 'sub2api';
|
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-5.3-codex';
|
|
9
9
|
const DEFAULT_PORT = 18888;
|
|
10
10
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
11
|
-
const DEFAULT_FALLBACK_UPSTREAM = '
|
|
12
|
-
const DEFAULT_LOG_FILE = '
|
|
11
|
+
const DEFAULT_FALLBACK_UPSTREAM = '';
|
|
12
|
+
const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
13
13
|
const DEFAULT_SERVICE_NAME = 'openclaw-atools-proxy.service';
|
|
14
14
|
const OPENCLAW_GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
15
15
|
|
|
@@ -47,6 +47,46 @@ function ensureDir(dir) {
|
|
|
47
47
|
fs.mkdirSync(dir, { recursive: true });
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function defaultServiceId(name = DEFAULT_SERVICE_NAME) {
|
|
51
|
+
return String(name || DEFAULT_SERVICE_NAME).replace(/\.service$/i, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildProxyServeArgs({ port, upstream, fallbackUpstream, fallbackApiKey, maxReqBytes, logFile }) {
|
|
55
|
+
const args = [
|
|
56
|
+
'serve',
|
|
57
|
+
'--port',
|
|
58
|
+
String(port),
|
|
59
|
+
'--upstream',
|
|
60
|
+
String(upstream),
|
|
61
|
+
'--log-file',
|
|
62
|
+
String(logFile),
|
|
63
|
+
'--max-req-bytes',
|
|
64
|
+
String(maxReqBytes)
|
|
65
|
+
];
|
|
66
|
+
if (fallbackUpstream) {
|
|
67
|
+
args.push('--fallback-upstream', String(fallbackUpstream));
|
|
68
|
+
}
|
|
69
|
+
if (fallbackApiKey) {
|
|
70
|
+
args.push('--fallback-api-key', String(fallbackApiKey));
|
|
71
|
+
}
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function escapeXml(text) {
|
|
76
|
+
return String(text)
|
|
77
|
+
.replace(/&/g, '&')
|
|
78
|
+
.replace(/</g, '<')
|
|
79
|
+
.replace(/>/g, '>')
|
|
80
|
+
.replace(/"/g, '"')
|
|
81
|
+
.replace(/'/g, ''');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function quoteWindowsArg(arg) {
|
|
85
|
+
const s = String(arg ?? '');
|
|
86
|
+
if (!/[\s"]/g.test(s)) return s;
|
|
87
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
88
|
+
}
|
|
89
|
+
|
|
50
90
|
function parseArgs(argv) {
|
|
51
91
|
const out = {
|
|
52
92
|
openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'),
|
|
@@ -95,34 +135,27 @@ function parseArgs(argv) {
|
|
|
95
135
|
}
|
|
96
136
|
|
|
97
137
|
function renderSystemdService({
|
|
98
|
-
serviceName,
|
|
99
138
|
nodeBin,
|
|
100
139
|
cliPath,
|
|
101
|
-
|
|
102
|
-
upstream,
|
|
140
|
+
serveArgs,
|
|
103
141
|
fallbackUpstream,
|
|
104
|
-
|
|
105
|
-
fallbackApiKey,
|
|
106
|
-
logFile
|
|
142
|
+
fallbackApiKey
|
|
107
143
|
}) {
|
|
108
144
|
const fallbackEnv = fallbackUpstream
|
|
109
145
|
? `Environment=SUB2API_FALLBACK_UPSTREAM=${fallbackUpstream}\nEnvironment=SUB2API_FALLBACK_API_KEY=${fallbackApiKey || ''}\n`
|
|
110
146
|
: '';
|
|
147
|
+
const execArgs = [nodeBin, cliPath, ...serveArgs].join(' ');
|
|
111
148
|
return `[Unit]
|
|
112
149
|
Description=OpenClaw ATools Compatibility Proxy
|
|
113
150
|
After=network-online.target
|
|
114
151
|
Wants=network-online.target
|
|
115
152
|
|
|
116
153
|
[Service]
|
|
117
|
-
ExecStart=${
|
|
154
|
+
ExecStart=${execArgs}
|
|
118
155
|
Restart=always
|
|
119
156
|
RestartSec=2
|
|
120
157
|
Environment=HOME=${os.homedir()}
|
|
121
158
|
Environment=TMPDIR=/tmp
|
|
122
|
-
Environment=SUB2API_COMPAT_PORT=${port}
|
|
123
|
-
Environment=SUB2API_UPSTREAM=${upstream}
|
|
124
|
-
Environment=SUB2API_COMPAT_LOG=${logFile}
|
|
125
|
-
Environment=SUB2API_COMPAT_MAX_REQ_BYTES=${maxReqBytes}
|
|
126
159
|
Environment="SUB2API_COMPAT_USER_AGENT=codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal"
|
|
127
160
|
${fallbackEnv}
|
|
128
161
|
|
|
@@ -131,6 +164,42 @@ WantedBy=default.target
|
|
|
131
164
|
`;
|
|
132
165
|
}
|
|
133
166
|
|
|
167
|
+
function renderLaunchdPlist({ label, nodeBin, cliPath, serveArgs, logFile }) {
|
|
168
|
+
const args = [nodeBin, cliPath, ...serveArgs];
|
|
169
|
+
const argsXml = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
|
|
170
|
+
const launchLog = `${logFile}.launchd.log`;
|
|
171
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
172
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
173
|
+
<plist version="1.0">
|
|
174
|
+
<dict>
|
|
175
|
+
<key>Label</key>
|
|
176
|
+
<string>${escapeXml(label)}</string>
|
|
177
|
+
<key>ProgramArguments</key>
|
|
178
|
+
<array>
|
|
179
|
+
${argsXml}
|
|
180
|
+
</array>
|
|
181
|
+
<key>RunAtLoad</key>
|
|
182
|
+
<true/>
|
|
183
|
+
<key>KeepAlive</key>
|
|
184
|
+
<true/>
|
|
185
|
+
<key>WorkingDirectory</key>
|
|
186
|
+
<string>${escapeXml(os.homedir())}</string>
|
|
187
|
+
<key>EnvironmentVariables</key>
|
|
188
|
+
<dict>
|
|
189
|
+
<key>HOME</key>
|
|
190
|
+
<string>${escapeXml(os.homedir())}</string>
|
|
191
|
+
<key>SUB2API_COMPAT_USER_AGENT</key>
|
|
192
|
+
<string>codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal</string>
|
|
193
|
+
</dict>
|
|
194
|
+
<key>StandardOutPath</key>
|
|
195
|
+
<string>${escapeXml(launchLog)}</string>
|
|
196
|
+
<key>StandardErrorPath</key>
|
|
197
|
+
<string>${escapeXml(launchLog)}</string>
|
|
198
|
+
</dict>
|
|
199
|
+
</plist>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
134
203
|
function detectApiKey(openclawHome, explicitKey) {
|
|
135
204
|
if (explicitKey) return explicitKey;
|
|
136
205
|
const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
|
|
@@ -297,7 +366,7 @@ function runSystemctl(args, { dryRun } = {}) {
|
|
|
297
366
|
execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit' });
|
|
298
367
|
}
|
|
299
368
|
|
|
300
|
-
function
|
|
369
|
+
function installLinuxService({ serviceName, content, dryRun }) {
|
|
301
370
|
const serviceDir = path.join(os.homedir(), '.config/systemd/user');
|
|
302
371
|
const servicePath = path.join(serviceDir, serviceName);
|
|
303
372
|
ensureDir(serviceDir);
|
|
@@ -317,12 +386,105 @@ function installService({ serviceName, content, dryRun }) {
|
|
|
317
386
|
runSystemctl(['enable', '--now', serviceName], { dryRun });
|
|
318
387
|
}
|
|
319
388
|
|
|
389
|
+
function runLaunchctl(args, { dryRun, allowFailure = false } = {}) {
|
|
390
|
+
if (dryRun) {
|
|
391
|
+
info(`dry-run: launchctl ${args.join(' ')}`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
execFileSync('launchctl', args, { stdio: 'inherit' });
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (allowFailure) return;
|
|
398
|
+
throw err;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function installMacService({ serviceName, content, dryRun }) {
|
|
403
|
+
const serviceId = defaultServiceId(serviceName);
|
|
404
|
+
const launchAgentDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
405
|
+
const plistPath = path.join(launchAgentDir, `${serviceId}.plist`);
|
|
406
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
407
|
+
if (uid == null) {
|
|
408
|
+
throw new Error('failed to resolve current uid for launchctl');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
ensureDir(launchAgentDir);
|
|
412
|
+
if (dryRun) {
|
|
413
|
+
info(`dry-run: would write ${plistPath}`);
|
|
414
|
+
} else {
|
|
415
|
+
if (fs.existsSync(plistPath)) {
|
|
416
|
+
const bak = backup(plistPath);
|
|
417
|
+
if (bak) info(`backup created: ${bak}`);
|
|
418
|
+
}
|
|
419
|
+
fs.writeFileSync(plistPath, content, 'utf8');
|
|
420
|
+
info(`wrote: ${plistPath}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const target = `gui/${uid}/${serviceId}`;
|
|
424
|
+
runLaunchctl(['bootout', target], { dryRun, allowFailure: true });
|
|
425
|
+
runLaunchctl(['bootstrap', `gui/${uid}`, plistPath], { dryRun });
|
|
426
|
+
runLaunchctl(['enable', target], { dryRun, allowFailure: true });
|
|
427
|
+
runLaunchctl(['kickstart', '-k', target], { dryRun, allowFailure: true });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function runSchtasks(args, { dryRun, allowFailure = false } = {}) {
|
|
431
|
+
if (dryRun) {
|
|
432
|
+
info(`dry-run: schtasks ${args.join(' ')}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
execFileSync('schtasks', args, { stdio: 'inherit' });
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (allowFailure) return;
|
|
439
|
+
throw err;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun }) {
|
|
444
|
+
const taskName = defaultServiceId(serviceName);
|
|
445
|
+
const command = [nodeBin, cliPath, ...serveArgs].map(quoteWindowsArg).join(' ');
|
|
446
|
+
runSchtasks(
|
|
447
|
+
['/Create', '/F', '/SC', 'ONLOGON', '/RL', 'LIMITED', '/TN', taskName, '/TR', command],
|
|
448
|
+
{ dryRun }
|
|
449
|
+
);
|
|
450
|
+
runSchtasks(['/Run', '/TN', taskName], { dryRun, allowFailure: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function installService({
|
|
454
|
+
serviceName,
|
|
455
|
+
linuxServiceContent,
|
|
456
|
+
macPlistContent,
|
|
457
|
+
nodeBin,
|
|
458
|
+
cliPath,
|
|
459
|
+
serveArgs,
|
|
460
|
+
dryRun
|
|
461
|
+
}) {
|
|
462
|
+
if (process.platform === 'linux') {
|
|
463
|
+
installLinuxService({ serviceName, content: linuxServiceContent, dryRun });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (process.platform === 'darwin') {
|
|
467
|
+
installMacService({ serviceName, content: macPlistContent, dryRun });
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (process.platform === 'win32') {
|
|
471
|
+
installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun });
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
throw new Error(`unsupported platform for service install: ${process.platform}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
320
477
|
function restartOpenclawGateway({ noRestart, dryRun }) {
|
|
321
478
|
if (noRestart) {
|
|
322
479
|
info('skip gateway restart (--no-restart)');
|
|
323
480
|
return;
|
|
324
481
|
}
|
|
325
482
|
|
|
483
|
+
if (process.platform !== 'linux') {
|
|
484
|
+
info(`skip gateway restart on ${process.platform} (please restart OpenClaw manually)`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
326
488
|
try {
|
|
327
489
|
runSystemctl(['restart', OPENCLAW_GATEWAY_SERVICE], { dryRun });
|
|
328
490
|
} catch {
|
|
@@ -342,10 +504,10 @@ Options:
|
|
|
342
504
|
--model <id> Default: gpt-5.3-codex
|
|
343
505
|
--port <number> Default: 18888
|
|
344
506
|
--upstream <url> Default: https://sub2api.atools.live
|
|
345
|
-
--fallback-upstream <url> Default:
|
|
507
|
+
--fallback-upstream <url> Default: empty (disabled)
|
|
346
508
|
--max-req-bytes <number> Default: 68000
|
|
347
|
-
--log-file <path> Default:
|
|
348
|
-
--service-name <name>
|
|
509
|
+
--log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
|
|
510
|
+
--service-name <name> Linux: *.service; macOS/Windows: service id/task name
|
|
349
511
|
--api-key <key> Prefer explicit key; fallback env SUB_OAI_KEY then auth-profiles
|
|
350
512
|
--fallback-api-key <key> Prefer explicit key; fallback env FALLBACK_OAI_KEY then openclaw gmn key
|
|
351
513
|
--no-restart Do not restart openclaw-gateway.service
|
|
@@ -366,8 +528,17 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
|
|
|
366
528
|
const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
|
|
367
529
|
const apiKey = detectApiKey(args.openclawHome, args.apiKey);
|
|
368
530
|
const fallbackApiKey = detectFallbackApiKey(args.openclawHome, args.fallbackApiKey);
|
|
531
|
+
const serveArgs = buildProxyServeArgs({
|
|
532
|
+
port: args.port,
|
|
533
|
+
upstream: args.upstream,
|
|
534
|
+
fallbackUpstream: args.fallbackUpstream,
|
|
535
|
+
fallbackApiKey,
|
|
536
|
+
maxReqBytes: args.maxReqBytes,
|
|
537
|
+
logFile: args.logFile
|
|
538
|
+
});
|
|
369
539
|
|
|
370
540
|
info(`openclawHome=${args.openclawHome}`);
|
|
541
|
+
info(`platform=${process.platform}`);
|
|
371
542
|
info(`provider/model=${args.provider}/${args.model}`);
|
|
372
543
|
info(`proxy=http://127.0.0.1:${args.port}/v1 -> ${args.upstream}`);
|
|
373
544
|
if (args.fallbackUpstream) {
|
|
@@ -378,19 +549,30 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
|
|
|
378
549
|
patchAgentModels(args.openclawHome, args.provider, args.model, args.port, apiKey, args.dryRun);
|
|
379
550
|
patchAuthProfiles(args.openclawHome, args.provider, apiKey, args.dryRun);
|
|
380
551
|
|
|
381
|
-
const
|
|
382
|
-
serviceName: args.serviceName,
|
|
552
|
+
const linuxServiceContent = renderSystemdService({
|
|
383
553
|
nodeBin,
|
|
384
554
|
cliPath,
|
|
385
|
-
|
|
386
|
-
upstream: args.upstream,
|
|
555
|
+
serveArgs,
|
|
387
556
|
fallbackUpstream: args.fallbackUpstream,
|
|
388
|
-
|
|
389
|
-
|
|
557
|
+
fallbackApiKey
|
|
558
|
+
});
|
|
559
|
+
const macPlistContent = renderLaunchdPlist({
|
|
560
|
+
label: defaultServiceId(args.serviceName),
|
|
561
|
+
nodeBin,
|
|
562
|
+
cliPath,
|
|
563
|
+
serveArgs,
|
|
390
564
|
logFile: args.logFile
|
|
391
565
|
});
|
|
392
566
|
|
|
393
|
-
installService({
|
|
567
|
+
installService({
|
|
568
|
+
serviceName: args.serviceName,
|
|
569
|
+
linuxServiceContent,
|
|
570
|
+
macPlistContent,
|
|
571
|
+
nodeBin,
|
|
572
|
+
cliPath,
|
|
573
|
+
serveArgs,
|
|
574
|
+
dryRun: args.dryRun
|
|
575
|
+
});
|
|
394
576
|
restartOpenclawGateway({ noRestart: args.noRestart, dryRun: args.dryRun });
|
|
395
577
|
|
|
396
578
|
info('done');
|
package/lib/proxy-server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_PORT = 18888;
|
|
5
6
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
@@ -9,9 +10,10 @@ const DEFAULT_FALLBACK_UPSTREAM = '';
|
|
|
9
10
|
const DEFAULT_FALLBACK_API_KEY = '';
|
|
10
11
|
const DEFAULT_MAX_REQ_BYTES = 68000;
|
|
11
12
|
const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
|
|
13
|
+
const DEFAULT_STRIP_PREVIOUS_RESPONSE_ID = false;
|
|
12
14
|
|
|
13
15
|
function ensureParentDir(filePath) {
|
|
14
|
-
const dir =
|
|
16
|
+
const dir = path.dirname(filePath);
|
|
15
17
|
if (dir) {
|
|
16
18
|
fs.mkdirSync(dir, { recursive: true });
|
|
17
19
|
}
|
|
@@ -54,12 +56,13 @@ function normalizeContent(content) {
|
|
|
54
56
|
return [toInputText('')];
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
function normalizeResponsesPayload(payload) {
|
|
59
|
+
function normalizeResponsesPayload(payload, { stripPreviousResponseId = DEFAULT_STRIP_PREVIOUS_RESPONSE_ID } = {}) {
|
|
58
60
|
if (!payload || typeof payload !== 'object') return payload;
|
|
59
61
|
const out = { ...payload };
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if (stripPreviousResponseId && 'previous_response_id' in out) {
|
|
64
|
+
delete out.previous_response_id;
|
|
65
|
+
}
|
|
63
66
|
|
|
64
67
|
if (typeof out.input === 'string') {
|
|
65
68
|
out.input = [{ role: 'user', content: normalizeContent(out.input) }];
|
|
@@ -84,7 +87,28 @@ function trimTextPart(part, maxChars) {
|
|
|
84
87
|
return { ...part, text: text.length > maxChars ? text.slice(0, maxChars) : text };
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function
|
|
90
|
+
function compactInputMessages(messages, maxRecentMessages = 8) {
|
|
91
|
+
if (!Array.isArray(messages)) return messages;
|
|
92
|
+
const recent = messages.slice(-Math.max(1, maxRecentMessages));
|
|
93
|
+
let latestSystem = null;
|
|
94
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
95
|
+
const msg = messages[i];
|
|
96
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
97
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
98
|
+
latestSystem = msg;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (latestSystem && !recent.includes(latestSystem)) {
|
|
103
|
+
return [latestSystem, ...recent];
|
|
104
|
+
}
|
|
105
|
+
return recent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function compactResponsesPayload(payload, {
|
|
109
|
+
dropToolsOnCompact = DEFAULT_DROP_TOOLS_ON_COMPACT,
|
|
110
|
+
maxRecentMessages
|
|
111
|
+
} = {}) {
|
|
88
112
|
if (!payload || typeof payload !== 'object') return payload;
|
|
89
113
|
const out = { ...payload };
|
|
90
114
|
|
|
@@ -104,13 +128,11 @@ function compactResponsesPayload(payload, { dropToolsOnCompact = DEFAULT_DROP_TO
|
|
|
104
128
|
}
|
|
105
129
|
return item;
|
|
106
130
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
out.input =
|
|
131
|
+
const resolvedMaxRecentMessages = Number.isInteger(maxRecentMessages)
|
|
132
|
+
? Math.max(1, maxRecentMessages)
|
|
133
|
+
: (('previous_response_id' in out) ? 14 : 8);
|
|
134
|
+
out.input = compactInputMessages(normalized, resolvedMaxRecentMessages);
|
|
111
135
|
}
|
|
112
|
-
|
|
113
|
-
if ('previous_response_id' in out) delete out.previous_response_id;
|
|
114
136
|
return out;
|
|
115
137
|
}
|
|
116
138
|
|
|
@@ -149,6 +171,8 @@ export async function createProxyServer(options = {}) {
|
|
|
149
171
|
const maxReqBytes = Number(options.maxReqBytes ?? process.env.SUB2API_COMPAT_MAX_REQ_BYTES ?? DEFAULT_MAX_REQ_BYTES);
|
|
150
172
|
const dropToolsOnCompact = options.dropToolsOnCompact
|
|
151
173
|
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT || '').toLowerCase());
|
|
174
|
+
const stripPreviousResponseId = options.stripPreviousResponseId
|
|
175
|
+
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_STRIP_PREVIOUS_RESPONSE_ID || '').toLowerCase());
|
|
152
176
|
|
|
153
177
|
const server = http.createServer(async (req, res) => {
|
|
154
178
|
const startAt = Date.now();
|
|
@@ -167,34 +191,74 @@ export async function createProxyServer(options = {}) {
|
|
|
167
191
|
const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
|
|
168
192
|
let body = rawBody;
|
|
169
193
|
let compactBody = null;
|
|
194
|
+
let aggressiveBody = null;
|
|
195
|
+
let minimalBody = null;
|
|
170
196
|
|
|
171
197
|
if (isResponsesPath && rawBody.length > 0) {
|
|
172
198
|
try {
|
|
173
199
|
const parsed = JSON.parse(rawBody.toString('utf8'));
|
|
174
|
-
const normalized = normalizeResponsesPayload(parsed);
|
|
200
|
+
const normalized = normalizeResponsesPayload(parsed, { stripPreviousResponseId });
|
|
175
201
|
body = Buffer.from(JSON.stringify(normalized));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
if (body.length > maxReqBytes) {
|
|
203
|
+
const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
|
|
204
|
+
const compactBuf = Buffer.from(JSON.stringify(compacted));
|
|
205
|
+
if (compactBuf.length + 512 < body.length) {
|
|
206
|
+
compactBody = compactBuf;
|
|
207
|
+
}
|
|
208
|
+
const aggressive = compactResponsesPayload(normalized, {
|
|
209
|
+
dropToolsOnCompact,
|
|
210
|
+
maxRecentMessages: 3
|
|
211
|
+
});
|
|
212
|
+
const aggressiveBuf = Buffer.from(JSON.stringify(aggressive));
|
|
213
|
+
if (aggressiveBuf.length + 256 < body.length) {
|
|
214
|
+
aggressiveBody = aggressiveBuf;
|
|
215
|
+
}
|
|
216
|
+
const minimal = compactResponsesPayload(normalized, {
|
|
217
|
+
dropToolsOnCompact,
|
|
218
|
+
maxRecentMessages: 1
|
|
219
|
+
});
|
|
220
|
+
const minimalBuf = Buffer.from(JSON.stringify(minimal));
|
|
221
|
+
if (minimalBuf.length + 128 < body.length) {
|
|
222
|
+
minimalBody = minimalBuf;
|
|
223
|
+
}
|
|
180
224
|
}
|
|
181
225
|
} catch {
|
|
182
226
|
// Keep original body if parse fails.
|
|
183
227
|
}
|
|
184
228
|
}
|
|
185
229
|
|
|
230
|
+
const requestBodies = [body];
|
|
231
|
+
if (compactBody && !compactBody.equals(body)) {
|
|
232
|
+
requestBodies.push(compactBody);
|
|
233
|
+
}
|
|
234
|
+
if (aggressiveBody && !aggressiveBody.equals(body) && !(compactBody && aggressiveBody.equals(compactBody))) {
|
|
235
|
+
requestBodies.push(aggressiveBody);
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
minimalBody
|
|
239
|
+
&& !minimalBody.equals(body)
|
|
240
|
+
&& !(compactBody && minimalBody.equals(compactBody))
|
|
241
|
+
&& !(aggressiveBody && minimalBody.equals(aggressiveBody))
|
|
242
|
+
) {
|
|
243
|
+
requestBodies.push(minimalBody);
|
|
244
|
+
}
|
|
245
|
+
|
|
186
246
|
let resp;
|
|
187
247
|
let attempts = 0;
|
|
188
248
|
let via = 'primary';
|
|
189
249
|
let primaryStatus = null;
|
|
190
250
|
let fallbackStatus = null;
|
|
191
251
|
let compacted = false;
|
|
252
|
+
let strategy = 'full';
|
|
192
253
|
const maxAttempts = isResponsesPath ? retryMax : 1;
|
|
193
254
|
while (attempts < maxAttempts) {
|
|
194
255
|
attempts += 1;
|
|
195
|
-
const
|
|
196
|
-
const requestBody =
|
|
197
|
-
compacted =
|
|
256
|
+
const strategyIndex = Math.min(requestBodies.length - 1, attempts - 1);
|
|
257
|
+
const requestBody = requestBodies[strategyIndex];
|
|
258
|
+
compacted = strategyIndex > 0;
|
|
259
|
+
strategy = strategyIndex === 0
|
|
260
|
+
? 'full'
|
|
261
|
+
: (strategyIndex === 1 ? 'compact' : (strategyIndex === 2 ? 'aggressive' : 'minimal'));
|
|
198
262
|
const primaryHeaders = isResponsesPath ? buildHeaders(baseHeaders, requestBody.length) : baseHeaders;
|
|
199
263
|
const fallbackHeaders = isResponsesPath
|
|
200
264
|
? buildHeaders(baseHeaders, requestBody.length, fallbackApiKey)
|
|
@@ -244,8 +308,9 @@ export async function createProxyServer(options = {}) {
|
|
|
244
308
|
primaryStatus,
|
|
245
309
|
fallbackStatus,
|
|
246
310
|
compacted,
|
|
311
|
+
strategy,
|
|
247
312
|
reqBytes: rawBody.length,
|
|
248
|
-
upstreamReqBytes:
|
|
313
|
+
upstreamReqBytes: requestBodies[Math.min(requestBodies.length - 1, attempts - 1)].length,
|
|
249
314
|
respBytes: outBuffer.length,
|
|
250
315
|
durationMs: Date.now() - startAt
|
|
251
316
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autotask/atools-tool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"start": "node ./bin/cli.mjs serve",
|
|
36
|
-
"config:openclaw": "node ./bin/cli.mjs
|
|
36
|
+
"config:openclaw": "node ./bin/cli.mjs openclaw",
|
|
37
37
|
"install:local": "node ./bin/install.mjs",
|
|
38
38
|
"pack:check": "npm pack"
|
|
39
39
|
},
|