@chrysb/alphaclaw 0.4.6-beta.4 → 0.4.6-beta.5
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/lib/public/js/app.js +158 -1073
- package/lib/public/js/components/general/index.js +155 -0
- package/lib/public/js/components/models-tab/index.js +286 -0
- package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
- package/lib/public/js/components/models-tab/use-models.js +262 -0
- package/lib/public/js/components/routes/browse-route.js +35 -0
- package/lib/public/js/components/routes/doctor-route.js +21 -0
- package/lib/public/js/components/routes/envars-route.js +11 -0
- package/lib/public/js/components/routes/general-route.js +45 -0
- package/lib/public/js/components/routes/index.js +11 -0
- package/lib/public/js/components/routes/models-route.js +11 -0
- package/lib/public/js/components/routes/providers-route.js +11 -0
- package/lib/public/js/components/routes/route-redirect.js +10 -0
- package/lib/public/js/components/routes/telegram-route.js +11 -0
- package/lib/public/js/components/routes/usage-route.js +15 -0
- package/lib/public/js/components/routes/watchdog-route.js +32 -0
- package/lib/public/js/components/routes/webhooks-route.js +43 -0
- package/lib/public/js/components/sidebar.js +2 -3
- package/lib/public/js/components/usage-tab/constants.js +1 -1
- package/lib/public/js/components/usage-tab/overview-section.js +124 -50
- package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
- package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
- package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
- package/lib/public/js/hooks/use-browse-navigation.js +193 -0
- package/lib/public/js/hooks/use-hash-location.js +32 -0
- package/lib/public/js/lib/api.js +35 -0
- package/lib/public/js/lib/app-navigation.js +39 -0
- package/lib/public/js/lib/browse-restart-policy.js +28 -0
- package/lib/public/js/lib/browse-route.js +57 -0
- package/lib/public/js/lib/format.js +12 -0
- package/lib/server/auth-profiles.js +223 -52
- package/lib/server/gateway.js +29 -9
- package/lib/server/routes/models.js +170 -2
- package/lib/server/watchdog.js +14 -1
- package/lib/server.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
|
|
2
|
+
|
|
3
|
+
export const buildBrowseRoute = (relativePath, options = {}) => {
|
|
4
|
+
const view = String(options?.view || "edit");
|
|
5
|
+
const encodedPath = String(relativePath || "")
|
|
6
|
+
.split("/")
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((segment) => encodeURIComponent(segment))
|
|
9
|
+
.join("/");
|
|
10
|
+
const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
|
|
11
|
+
const params = new URLSearchParams();
|
|
12
|
+
if (view === "diff" && encodedPath) params.set("view", "diff");
|
|
13
|
+
if (options.line) params.set("line", String(options.line));
|
|
14
|
+
if (options.lineEnd) params.set("lineEnd", String(options.lineEnd));
|
|
15
|
+
const query = params.toString();
|
|
16
|
+
return query ? `${baseRoute}?${query}` : baseRoute;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const parseBrowseRoute = ({ location = "", browsePreviewPath = "" } = {}) => {
|
|
20
|
+
const isBrowseRoute = location.startsWith("/browse");
|
|
21
|
+
const browseRoutePath = isBrowseRoute ? String(location || "").split("?")[0] : "";
|
|
22
|
+
const browseRouteQuery =
|
|
23
|
+
isBrowseRoute && String(location || "").includes("?")
|
|
24
|
+
? String(location || "").split("?").slice(1).join("?")
|
|
25
|
+
: "";
|
|
26
|
+
const selectedBrowsePath = isBrowseRoute
|
|
27
|
+
? browseRoutePath
|
|
28
|
+
.replace(/^\/browse\/?/, "")
|
|
29
|
+
.split("/")
|
|
30
|
+
.filter(Boolean)
|
|
31
|
+
.map((segment) => {
|
|
32
|
+
try {
|
|
33
|
+
return decodeURIComponent(segment);
|
|
34
|
+
} catch {
|
|
35
|
+
return segment;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.join("/")
|
|
39
|
+
: "";
|
|
40
|
+
const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
|
|
41
|
+
const browseQueryParams = isBrowseRoute ? new URLSearchParams(browseRouteQuery) : null;
|
|
42
|
+
const browseViewerMode =
|
|
43
|
+
!browsePreviewPath && browseQueryParams?.get("view") === "diff"
|
|
44
|
+
? "diff"
|
|
45
|
+
: "edit";
|
|
46
|
+
const browseLineTarget = Number.parseInt(browseQueryParams?.get("line") || "", 10) || 0;
|
|
47
|
+
const browseLineEndTarget = Number.parseInt(browseQueryParams?.get("lineEnd") || "", 10) || 0;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
activeBrowsePath,
|
|
51
|
+
browseLineEndTarget,
|
|
52
|
+
browseLineTarget,
|
|
53
|
+
browseViewerMode,
|
|
54
|
+
isBrowseRoute,
|
|
55
|
+
selectedBrowsePath,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const kIntegerFormatter = new Intl.NumberFormat("en-US");
|
|
2
|
+
const kCompactNumberFormatter = new Intl.NumberFormat("en-US", {
|
|
3
|
+
notation: "compact",
|
|
4
|
+
minimumFractionDigits: 1,
|
|
5
|
+
maximumFractionDigits: 1,
|
|
6
|
+
});
|
|
2
7
|
const kUsdFormatter = new Intl.NumberFormat("en-US", {
|
|
3
8
|
style: "currency",
|
|
4
9
|
currency: "USD",
|
|
@@ -25,6 +30,13 @@ const isSameDay = (left, right) =>
|
|
|
25
30
|
export const formatInteger = (value) =>
|
|
26
31
|
kIntegerFormatter.format(Number(value || 0));
|
|
27
32
|
|
|
33
|
+
export const formatCompactNumber = (value) => {
|
|
34
|
+
const numberValue = Number(value || 0);
|
|
35
|
+
if (!Number.isFinite(numberValue)) return "0";
|
|
36
|
+
if (Math.abs(numberValue) < 1000) return formatInteger(numberValue);
|
|
37
|
+
return kCompactNumberFormatter.format(numberValue);
|
|
38
|
+
};
|
|
39
|
+
|
|
28
40
|
export const formatUsd = (value) => kUsdFormatter.format(Number(value || 0));
|
|
29
41
|
|
|
30
42
|
export const formatLocaleDateTime = (
|
|
@@ -1,62 +1,216 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const { AUTH_PROFILES_PATH, CODEX_PROFILE_ID } = require("./constants");
|
|
3
|
+
const { AUTH_PROFILES_PATH, CODEX_PROFILE_ID, OPENCLAW_DIR } = require("./constants");
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
const kDefaultAgentId = "main";
|
|
6
|
+
|
|
7
|
+
const normalizeSecret = (raw) =>
|
|
8
|
+
String(raw ?? "")
|
|
9
|
+
.replace(/[\r\n\u2028\u2029]/g, "")
|
|
10
|
+
.trim();
|
|
11
|
+
|
|
12
|
+
const credentialMode = (credential) => {
|
|
13
|
+
if (credential.type === "api_key") return "api_key";
|
|
14
|
+
if (credential.type === "token") return "token";
|
|
15
|
+
return "oauth";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const resolveAgentDir = (agentId = kDefaultAgentId) =>
|
|
19
|
+
path.join(OPENCLAW_DIR, "agents", agentId, "agent");
|
|
20
|
+
|
|
21
|
+
const resolveAuthProfilesPath = (agentId = kDefaultAgentId) =>
|
|
22
|
+
path.join(resolveAgentDir(agentId), "auth-profiles.json");
|
|
23
|
+
|
|
24
|
+
const resolveOpenclawConfigPath = () =>
|
|
25
|
+
path.join(OPENCLAW_DIR, "openclaw.json");
|
|
26
|
+
|
|
27
|
+
const loadAuthStore = (agentId = kDefaultAgentId) => {
|
|
28
|
+
const storePath = resolveAuthProfilesPath(agentId);
|
|
29
|
+
let store = { version: 1, profiles: {} };
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(storePath)) {
|
|
32
|
+
const parsed = JSON.parse(fs.readFileSync(storePath, "utf8"));
|
|
33
|
+
if (
|
|
34
|
+
parsed &&
|
|
35
|
+
typeof parsed === "object" &&
|
|
36
|
+
parsed.profiles &&
|
|
37
|
+
typeof parsed.profiles === "object"
|
|
38
|
+
) {
|
|
39
|
+
store = {
|
|
40
|
+
version: Number(parsed.version || 1),
|
|
41
|
+
profiles: parsed.profiles,
|
|
42
|
+
order: parsed.order,
|
|
43
|
+
lastGood: parsed.lastGood,
|
|
44
|
+
usageStats: parsed.usageStats,
|
|
45
|
+
};
|
|
25
46
|
}
|
|
26
|
-
}
|
|
27
|
-
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return store;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const saveAuthStore = (agentId, store) => {
|
|
53
|
+
const storePath = resolveAuthProfilesPath(agentId);
|
|
54
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
storePath,
|
|
57
|
+
JSON.stringify(
|
|
58
|
+
{
|
|
59
|
+
version: Number(store.version || 1),
|
|
60
|
+
profiles: store.profiles || {},
|
|
61
|
+
...(store.order !== undefined ? { order: store.order } : {}),
|
|
62
|
+
...(store.lastGood !== undefined ? { lastGood: store.lastGood } : {}),
|
|
63
|
+
...(store.usageStats !== undefined
|
|
64
|
+
? { usageStats: store.usageStats }
|
|
65
|
+
: {}),
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
2,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const loadOpenclawConfig = () => {
|
|
74
|
+
const configPath = resolveOpenclawConfigPath();
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const saveOpenclawConfig = (cfg) => {
|
|
83
|
+
const configPath = resolveOpenclawConfigPath();
|
|
84
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const syncConfigAuthReference = (cfg, profileId, credential) => {
|
|
88
|
+
const next = { ...cfg };
|
|
89
|
+
if (!next.auth) next.auth = {};
|
|
90
|
+
if (!next.auth.profiles) next.auth.profiles = {};
|
|
91
|
+
next.auth = { ...next.auth, profiles: { ...next.auth.profiles } };
|
|
92
|
+
next.auth.profiles[profileId] = {
|
|
93
|
+
provider: credential.provider,
|
|
94
|
+
mode: credentialMode(credential),
|
|
28
95
|
};
|
|
96
|
+
return next;
|
|
97
|
+
};
|
|
29
98
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
99
|
+
const removeConfigAuthReference = (cfg, profileId) => {
|
|
100
|
+
if (!cfg.auth?.profiles?.[profileId]) return cfg;
|
|
101
|
+
const next = { ...cfg };
|
|
102
|
+
next.auth = { ...next.auth, profiles: { ...next.auth.profiles } };
|
|
103
|
+
delete next.auth.profiles[profileId];
|
|
104
|
+
if (Object.keys(next.auth.profiles).length === 0) {
|
|
105
|
+
delete next.auth.profiles;
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(next.auth).length === 0) {
|
|
108
|
+
delete next.auth;
|
|
109
|
+
}
|
|
110
|
+
return next;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const createAuthProfiles = () => {
|
|
114
|
+
// ── Generic profile operations ──
|
|
115
|
+
|
|
116
|
+
const listProfiles = (agentId = kDefaultAgentId) => {
|
|
117
|
+
const store = loadAuthStore(agentId);
|
|
118
|
+
return Object.entries(store.profiles || {}).map(([id, cred]) => ({
|
|
119
|
+
id,
|
|
120
|
+
...cred,
|
|
121
|
+
}));
|
|
46
122
|
};
|
|
47
123
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
124
|
+
const listProfilesByProvider = (provider, agentId = kDefaultAgentId) =>
|
|
125
|
+
listProfiles(agentId).filter((p) => p.provider === provider);
|
|
126
|
+
|
|
127
|
+
const getProfile = (profileId, agentId = kDefaultAgentId) => {
|
|
128
|
+
const store = loadAuthStore(agentId);
|
|
129
|
+
const cred = store.profiles?.[profileId];
|
|
130
|
+
if (!cred) return null;
|
|
131
|
+
return { id: profileId, ...cred };
|
|
53
132
|
};
|
|
54
133
|
|
|
134
|
+
const upsertProfile = (profileId, credential, agentId = kDefaultAgentId) => {
|
|
135
|
+
const store = loadAuthStore(agentId);
|
|
136
|
+
const sanitized = { ...credential };
|
|
137
|
+
if (sanitized.key) sanitized.key = normalizeSecret(sanitized.key);
|
|
138
|
+
if (sanitized.token) sanitized.token = normalizeSecret(sanitized.token);
|
|
139
|
+
if (sanitized.access) sanitized.access = normalizeSecret(sanitized.access);
|
|
140
|
+
if (sanitized.refresh)
|
|
141
|
+
sanitized.refresh = normalizeSecret(sanitized.refresh);
|
|
142
|
+
store.profiles[profileId] = sanitized;
|
|
143
|
+
saveAuthStore(agentId, store);
|
|
144
|
+
|
|
145
|
+
const cfg = loadOpenclawConfig();
|
|
146
|
+
const updated = syncConfigAuthReference(cfg, profileId, sanitized);
|
|
147
|
+
saveOpenclawConfig(updated);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const removeProfile = (profileId, agentId = kDefaultAgentId) => {
|
|
151
|
+
const store = loadAuthStore(agentId);
|
|
152
|
+
if (!store.profiles[profileId]) return false;
|
|
153
|
+
delete store.profiles[profileId];
|
|
154
|
+
saveAuthStore(agentId, store);
|
|
155
|
+
|
|
156
|
+
const cfg = loadOpenclawConfig();
|
|
157
|
+
const updated = removeConfigAuthReference(cfg, profileId);
|
|
158
|
+
saveOpenclawConfig(updated);
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const setAuthOrder = (provider, orderedProfileIds, agentId = kDefaultAgentId) => {
|
|
163
|
+
const store = loadAuthStore(agentId);
|
|
164
|
+
if (!store.order) store.order = {};
|
|
165
|
+
store.order[provider] = orderedProfileIds;
|
|
166
|
+
saveAuthStore(agentId, store);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const syncConfigAuthReferencesForAgent = (agentId = kDefaultAgentId) => {
|
|
170
|
+
const store = loadAuthStore(agentId);
|
|
171
|
+
let cfg = loadOpenclawConfig();
|
|
172
|
+
for (const [profileId, credential] of Object.entries(store.profiles || {})) {
|
|
173
|
+
if (!credential?.type || !credential?.provider) continue;
|
|
174
|
+
cfg = syncConfigAuthReference(cfg, profileId, credential);
|
|
175
|
+
}
|
|
176
|
+
saveOpenclawConfig(cfg);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ── Model config operations ──
|
|
180
|
+
|
|
181
|
+
const getModelConfig = () => {
|
|
182
|
+
const cfg = loadOpenclawConfig();
|
|
183
|
+
const defaults = cfg.agents?.defaults || {};
|
|
184
|
+
return {
|
|
185
|
+
primary: defaults.model?.primary || null,
|
|
186
|
+
configuredModels: defaults.models || {},
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const setModelConfig = ({ primary, configuredModels }) => {
|
|
191
|
+
const cfg = loadOpenclawConfig();
|
|
192
|
+
if (!cfg.agents) cfg.agents = {};
|
|
193
|
+
if (!cfg.agents.defaults) cfg.agents.defaults = {};
|
|
194
|
+
if (!cfg.agents.defaults.model) cfg.agents.defaults.model = {};
|
|
195
|
+
if (primary !== undefined) {
|
|
196
|
+
cfg.agents.defaults.model.primary = primary;
|
|
197
|
+
}
|
|
198
|
+
if (configuredModels !== undefined) {
|
|
199
|
+
cfg.agents.defaults.models = configuredModels;
|
|
200
|
+
}
|
|
201
|
+
saveOpenclawConfig(cfg);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// ── Legacy Codex-specific wrappers ──
|
|
205
|
+
|
|
206
|
+
const listCodexProfiles = () => listProfilesByProvider("openai-codex");
|
|
207
|
+
|
|
55
208
|
const getCodexProfile = () => {
|
|
56
209
|
const profiles = listCodexProfiles();
|
|
57
210
|
if (profiles.length === 0) return null;
|
|
58
|
-
const preferred =
|
|
59
|
-
|
|
211
|
+
const preferred =
|
|
212
|
+
profiles.find((p) => p.id === CODEX_PROFILE_ID) || profiles[0];
|
|
213
|
+
return { profileId: preferred.id, ...preferred };
|
|
60
214
|
};
|
|
61
215
|
|
|
62
216
|
const hasCodexOauthProfile = () => {
|
|
@@ -65,20 +219,18 @@ const createAuthProfiles = () => {
|
|
|
65
219
|
};
|
|
66
220
|
|
|
67
221
|
const upsertCodexProfile = ({ access, refresh, expires, accountId }) => {
|
|
68
|
-
|
|
69
|
-
store.profiles[CODEX_PROFILE_ID] = {
|
|
222
|
+
upsertProfile(CODEX_PROFILE_ID, {
|
|
70
223
|
type: "oauth",
|
|
71
224
|
provider: "openai-codex",
|
|
72
225
|
access,
|
|
73
226
|
refresh,
|
|
74
227
|
expires,
|
|
75
228
|
...(accountId ? { accountId } : {}),
|
|
76
|
-
};
|
|
77
|
-
saveAuthProfilesStore(store);
|
|
229
|
+
});
|
|
78
230
|
};
|
|
79
231
|
|
|
80
232
|
const removeCodexProfiles = () => {
|
|
81
|
-
const store =
|
|
233
|
+
const store = loadAuthStore();
|
|
82
234
|
let changed = false;
|
|
83
235
|
for (const [id, cred] of Object.entries(store.profiles || {})) {
|
|
84
236
|
if (cred?.provider === "openai-codex") {
|
|
@@ -86,15 +238,34 @@ const createAuthProfiles = () => {
|
|
|
86
238
|
changed = true;
|
|
87
239
|
}
|
|
88
240
|
}
|
|
89
|
-
if (changed)
|
|
241
|
+
if (changed) {
|
|
242
|
+
saveAuthStore(kDefaultAgentId, store);
|
|
243
|
+
let cfg = loadOpenclawConfig();
|
|
244
|
+
for (const [id, cred] of Object.entries(cfg.auth?.profiles || {})) {
|
|
245
|
+
if (cred?.provider === "openai-codex") {
|
|
246
|
+
cfg = removeConfigAuthReference(cfg, id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
saveOpenclawConfig(cfg);
|
|
250
|
+
}
|
|
90
251
|
return changed;
|
|
91
252
|
};
|
|
92
253
|
|
|
93
254
|
return {
|
|
255
|
+
listProfiles,
|
|
256
|
+
listProfilesByProvider,
|
|
257
|
+
getProfile,
|
|
258
|
+
upsertProfile,
|
|
259
|
+
removeProfile,
|
|
260
|
+
setAuthOrder,
|
|
261
|
+
syncConfigAuthReferencesForAgent,
|
|
262
|
+
getModelConfig,
|
|
263
|
+
setModelConfig,
|
|
94
264
|
getCodexProfile,
|
|
95
265
|
hasCodexOauthProfile,
|
|
96
266
|
upsertCodexProfile,
|
|
97
267
|
removeCodexProfiles,
|
|
268
|
+
loadAuthStore,
|
|
98
269
|
};
|
|
99
270
|
};
|
|
100
271
|
|
package/lib/server/gateway.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
const { spawn, execSync } = require("child_process");
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const net = require("net");
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
OPENCLAW_DIR,
|
|
6
|
+
GATEWAY_HOST,
|
|
7
|
+
GATEWAY_PORT,
|
|
8
|
+
kChannelDefs,
|
|
9
|
+
kRootDir,
|
|
10
|
+
} = require("./constants");
|
|
5
11
|
|
|
6
12
|
let gatewayChild = null;
|
|
7
13
|
let gatewayExitHandler = null;
|
|
@@ -11,7 +17,9 @@ let gatewayStderrTail = [];
|
|
|
11
17
|
const expectedExitPids = new Set();
|
|
12
18
|
|
|
13
19
|
const appendStderrTail = (chunk) => {
|
|
14
|
-
const text = Buffer.isBuffer(chunk)
|
|
20
|
+
const text = Buffer.isBuffer(chunk)
|
|
21
|
+
? chunk.toString("utf8")
|
|
22
|
+
: String(chunk ?? "");
|
|
15
23
|
for (const line of text.split("\n")) {
|
|
16
24
|
const trimmed = line.trimEnd();
|
|
17
25
|
if (!trimmed) continue;
|
|
@@ -76,7 +84,9 @@ const runGatewayCmd = (cmd) => {
|
|
|
76
84
|
|
|
77
85
|
const launchGatewayProcess = () => {
|
|
78
86
|
if (gatewayChild && gatewayChild.exitCode === null && !gatewayChild.killed) {
|
|
79
|
-
console.log(
|
|
87
|
+
console.log(
|
|
88
|
+
"[alphaclaw] Managed gateway process already running — skipping launch",
|
|
89
|
+
);
|
|
80
90
|
return gatewayChild;
|
|
81
91
|
}
|
|
82
92
|
gatewayStderrTail = [];
|
|
@@ -145,20 +155,24 @@ const restartGateway = (reloadEnv) => {
|
|
|
145
155
|
gatewayChild.kill("SIGTERM");
|
|
146
156
|
gatewayChild = null;
|
|
147
157
|
} catch (e) {
|
|
148
|
-
console.log(
|
|
158
|
+
console.log(
|
|
159
|
+
`[alphaclaw] Failed to stop managed gateway process: ${e.message}`,
|
|
160
|
+
);
|
|
149
161
|
runGatewayCmd("stop");
|
|
150
162
|
}
|
|
151
163
|
} else {
|
|
152
164
|
runGatewayCmd("stop");
|
|
153
165
|
}
|
|
154
|
-
runGatewayCmd("
|
|
166
|
+
runGatewayCmd("start");
|
|
155
167
|
const launchWhenReady = async () => {
|
|
156
168
|
const waitUntil = Date.now() + 8000;
|
|
157
169
|
while (Date.now() < waitUntil) {
|
|
158
170
|
if (!(await isGatewayRunning())) break;
|
|
159
171
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
160
172
|
}
|
|
161
|
-
console.log(
|
|
173
|
+
console.log(
|
|
174
|
+
"[alphaclaw] Starting openclaw gateway with refreshed environment...",
|
|
175
|
+
);
|
|
162
176
|
launchGatewayProcess();
|
|
163
177
|
};
|
|
164
178
|
void launchWhenReady();
|
|
@@ -275,7 +289,9 @@ const syncChannelConfig = (savedVars, mode = "all") => {
|
|
|
275
289
|
|
|
276
290
|
const getChannelStatus = () => {
|
|
277
291
|
try {
|
|
278
|
-
const config = JSON.parse(
|
|
292
|
+
const config = JSON.parse(
|
|
293
|
+
fs.readFileSync(`${OPENCLAW_DIR}/openclaw.json`, "utf8"),
|
|
294
|
+
);
|
|
279
295
|
const credDir = `${OPENCLAW_DIR}/credentials`;
|
|
280
296
|
const channels = {};
|
|
281
297
|
|
|
@@ -287,9 +303,13 @@ const getChannelStatus = () => {
|
|
|
287
303
|
try {
|
|
288
304
|
const files = fs
|
|
289
305
|
.readdirSync(credDir)
|
|
290
|
-
.filter(
|
|
306
|
+
.filter(
|
|
307
|
+
(f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
|
|
308
|
+
);
|
|
291
309
|
for (const file of files) {
|
|
292
|
-
const data = JSON.parse(
|
|
310
|
+
const data = JSON.parse(
|
|
311
|
+
fs.readFileSync(`${credDir}/${file}`, "utf8"),
|
|
312
|
+
);
|
|
293
313
|
paired += (data.allowFrom || []).length;
|
|
294
314
|
}
|
|
295
315
|
} catch {}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
const { kFallbackOnboardingModels } = require("../constants");
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const runModelsGitSync = async (shellCmd) => {
|
|
4
|
+
if (typeof shellCmd !== "function") return null;
|
|
5
|
+
try {
|
|
6
|
+
await shellCmd('alphaclaw git-sync -m "models: update config" -f "openclaw.json"', {
|
|
7
|
+
timeout: 30000,
|
|
8
|
+
});
|
|
9
|
+
return null;
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return err?.message || "alphaclaw git-sync failed";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const registerModelRoutes = ({
|
|
16
|
+
app,
|
|
17
|
+
shellCmd,
|
|
18
|
+
gatewayEnv,
|
|
19
|
+
parseJsonFromNoisyOutput,
|
|
20
|
+
normalizeOnboardingModels,
|
|
21
|
+
authProfiles,
|
|
22
|
+
}) => {
|
|
23
|
+
// ── Existing CLI-backed catalog/status routes ──
|
|
24
|
+
|
|
4
25
|
app.get("/api/models", async (req, res) => {
|
|
5
26
|
try {
|
|
6
27
|
const output = await shellCmd("openclaw models list --all --json", {
|
|
@@ -60,7 +81,154 @@ const registerModelRoutes = ({ app, shellCmd, gatewayEnv, parseJsonFromNoisyOutp
|
|
|
60
81
|
});
|
|
61
82
|
res.json({ ok: true });
|
|
62
83
|
} catch (err) {
|
|
63
|
-
res
|
|
84
|
+
res
|
|
85
|
+
.status(400)
|
|
86
|
+
.json({ ok: false, error: err.message || "Failed to set model" });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ── Model config (direct JSON) ──
|
|
91
|
+
|
|
92
|
+
app.get("/api/models/config", (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const { primary, configuredModels } = authProfiles.getModelConfig();
|
|
95
|
+
const agentId = req.query.agentId || undefined;
|
|
96
|
+
const profiles = authProfiles.listProfiles(agentId);
|
|
97
|
+
const store = authProfiles.loadAuthStore(agentId);
|
|
98
|
+
res.json({
|
|
99
|
+
ok: true,
|
|
100
|
+
primary,
|
|
101
|
+
configuredModels,
|
|
102
|
+
authProfiles: profiles,
|
|
103
|
+
authOrder: store.order || {},
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
res
|
|
107
|
+
.status(500)
|
|
108
|
+
.json({ ok: false, error: err.message || "Failed to read config" });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
app.put("/api/models/config", async (req, res) => {
|
|
113
|
+
const { primary, configuredModels, profiles, authOrder } = req.body || {};
|
|
114
|
+
const agentId = req.query.agentId || undefined;
|
|
115
|
+
if (primary !== undefined && (typeof primary !== "string" || !primary.includes("/"))) {
|
|
116
|
+
return res
|
|
117
|
+
.status(400)
|
|
118
|
+
.json({ ok: false, error: "Invalid primary model key" });
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
configuredModels !== undefined &&
|
|
122
|
+
(typeof configuredModels !== "object" || configuredModels === null)
|
|
123
|
+
) {
|
|
124
|
+
return res
|
|
125
|
+
.status(400)
|
|
126
|
+
.json({ ok: false, error: "Invalid configuredModels" });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
authProfiles.setModelConfig({ primary, configuredModels });
|
|
130
|
+
|
|
131
|
+
if (Array.isArray(profiles)) {
|
|
132
|
+
for (const { id: profileId, ...credential } of profiles) {
|
|
133
|
+
if (profileId && credential.type && credential.provider) {
|
|
134
|
+
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (authOrder && typeof authOrder === "object") {
|
|
140
|
+
for (const [provider, order] of Object.entries(authOrder)) {
|
|
141
|
+
if (Array.isArray(order)) {
|
|
142
|
+
authProfiles.setAuthOrder(provider, order, agentId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// `auth-profiles.json` is the durable source of truth. Re-sync
|
|
148
|
+
// `openclaw.json.auth.profiles` on save so model re-adds restore refs.
|
|
149
|
+
authProfiles.syncConfigAuthReferencesForAgent(agentId);
|
|
150
|
+
|
|
151
|
+
const syncWarning = await runModelsGitSync(shellCmd);
|
|
152
|
+
res.json({
|
|
153
|
+
ok: true,
|
|
154
|
+
...(syncWarning ? { syncWarning } : {}),
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
res
|
|
158
|
+
.status(500)
|
|
159
|
+
.json({ ok: false, error: err.message || "Failed to save config" });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Auth profiles (direct JSON) ──
|
|
164
|
+
|
|
165
|
+
app.get("/api/models/auth", (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const agentId = req.query.agentId || undefined;
|
|
168
|
+
const profiles = authProfiles.listProfiles(agentId);
|
|
169
|
+
const store = authProfiles.loadAuthStore(agentId);
|
|
170
|
+
res.json({ ok: true, profiles, order: store.order || {} });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
res
|
|
173
|
+
.status(500)
|
|
174
|
+
.json({
|
|
175
|
+
ok: false,
|
|
176
|
+
error: err.message || "Failed to read auth profiles",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
app.put("/api/models/auth/:profileId", (req, res) => {
|
|
182
|
+
const { profileId } = req.params;
|
|
183
|
+
const credential = req.body;
|
|
184
|
+
if (
|
|
185
|
+
!profileId ||
|
|
186
|
+
!credential?.type ||
|
|
187
|
+
!credential?.provider
|
|
188
|
+
) {
|
|
189
|
+
return res
|
|
190
|
+
.status(400)
|
|
191
|
+
.json({ ok: false, error: "Missing profileId, type, or provider" });
|
|
192
|
+
}
|
|
193
|
+
const validTypes = new Set(["api_key", "token", "oauth"]);
|
|
194
|
+
if (!validTypes.has(credential.type)) {
|
|
195
|
+
return res.status(400).json({
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `Invalid credential type: ${credential.type}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const agentId = req.query.agentId || undefined;
|
|
202
|
+
authProfiles.upsertProfile(profileId, credential, agentId);
|
|
203
|
+
res.json({ ok: true });
|
|
204
|
+
} catch (err) {
|
|
205
|
+
res
|
|
206
|
+
.status(500)
|
|
207
|
+
.json({
|
|
208
|
+
ok: false,
|
|
209
|
+
error: err.message || "Failed to save auth profile",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
app.delete("/api/models/auth/:profileId", (req, res) => {
|
|
215
|
+
const { profileId } = req.params;
|
|
216
|
+
if (!profileId) {
|
|
217
|
+
return res
|
|
218
|
+
.status(400)
|
|
219
|
+
.json({ ok: false, error: "Missing profileId" });
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const agentId = req.query.agentId || undefined;
|
|
223
|
+
const removed = authProfiles.removeProfile(profileId, agentId);
|
|
224
|
+
res.json({ ok: true, removed });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
res
|
|
227
|
+
.status(500)
|
|
228
|
+
.json({
|
|
229
|
+
ok: false,
|
|
230
|
+
error: err.message || "Failed to remove auth profile",
|
|
231
|
+
});
|
|
64
232
|
}
|
|
65
233
|
});
|
|
66
234
|
};
|