Skip to content

Commit a8b8c12

Browse files
feat(api): update user settings handling and integrate default model management
- Modified the user settings retrieval and update logic to exclude defaultModel, which is now managed via the plugin. - Implemented functionality to push the user's default model to OpenClaw when updated. - Enhanced the ConnectionDO to handle default model updates and broadcast changes to connected clients. - Updated the plugin to process default model settings and ensure proper integration with OpenClaw configuration.
1 parent e672e90 commit a8b8c12

8 files changed

Lines changed: 99 additions & 21 deletions

File tree

packages/api/src/do/connection-do.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ export class ConnectionDO implements DurableObject {
270270
);
271271
}
272272

273+
// Plugin applied BotsChat default model to OpenClaw config — update and broadcast
274+
if (msg.type === "defaultModel.updated" && typeof msg.model === "string") {
275+
this.defaultModel = msg.model;
276+
await this.state.storage.put("defaultModel", this.defaultModel);
277+
this.broadcastToBrowsers(
278+
JSON.stringify({ type: "connection.status", openclawConnected: true, defaultModel: this.defaultModel, models: this.cachedModels }),
279+
);
280+
}
281+
273282
// Handle job updates from plugin — persist and forward to browsers
274283
if (msg.type === "job.update") {
275284
await this.handleJobUpdate(msg);

packages/api/src/index.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,14 @@ protectedApp.get("/me", async (c) => {
104104
created_at: number;
105105
}>();
106106
if (!row) return c.json({ error: "User not found" }, 404);
107+
const settings = JSON.parse(row.settings_json || "{}");
108+
// defaultModel is not stored in D1 — it comes from the plugin (connection.status).
109+
delete settings.defaultModel;
107110
return c.json({
108111
id: row.id,
109112
email: row.email,
110113
displayName: row.display_name,
111-
settings: JSON.parse(row.settings_json || "{}"),
114+
settings,
112115
createdAt: row.created_at,
113116
});
114117
});
@@ -117,25 +120,44 @@ protectedApp.patch("/me", async (c) => {
117120
const userId = c.get("userId");
118121
const body = await c.req.json<{ defaultModel?: string }>();
119122

123+
// defaultModel is not stored in D1 — get/set only via plugin (connection.status / push).
120124
const existing = await c.env.DB.prepare(
121125
"SELECT settings_json FROM users WHERE id = ?",
122126
)
123127
.bind(userId)
124128
.first<{ settings_json: string }>();
125129

126130
const settings = JSON.parse(existing?.settings_json || "{}");
127-
128-
if (body.defaultModel !== undefined) {
129-
settings.defaultModel = body.defaultModel;
130-
}
131-
131+
delete settings.defaultModel;
132+
// Persist other settings (if any) to D1; defaultModel is never written.
132133
await c.env.DB.prepare(
133134
"UPDATE users SET settings_json = ? WHERE id = ?",
134135
)
135136
.bind(JSON.stringify(settings), userId)
136137
.run();
137138

138-
return c.json({ ok: true, settings });
139+
if (body.defaultModel !== undefined) {
140+
try {
141+
const doId = c.env.CONNECTION_DO.idFromName(userId);
142+
const stub = c.env.CONNECTION_DO.get(doId);
143+
await stub.fetch(
144+
new Request("https://internal/send", {
145+
method: "POST",
146+
headers: { "Content-Type": "application/json" },
147+
body: JSON.stringify({
148+
type: "settings.defaultModel",
149+
defaultModel: body.defaultModel ?? "",
150+
}),
151+
}),
152+
);
153+
} catch (err) {
154+
console.error("Failed to push default model to OpenClaw:", err);
155+
}
156+
}
157+
158+
const outSettings = { ...settings };
159+
delete outSettings.defaultModel;
160+
return c.json({ ok: true, settings: outSettings });
139161
});
140162

141163
// OpenClaw scan data — schedule/instructions/model cached in the ConnectionDO.

packages/api/src/routes/auth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,14 @@ auth.get("/me", async (c) => {
287287

288288
if (!row) return c.json({ error: "User not found" }, 404);
289289

290+
const settings = JSON.parse(row.settings_json || "{}");
291+
delete settings.defaultModel; // not in D1 — comes from plugin (connection.status)
292+
290293
return c.json({
291294
id: row.id,
292295
email: row.email,
293296
displayName: row.display_name,
294-
settings: JSON.parse(row.settings_json || "{}"),
297+
settings,
295298
createdAt: row.created_at,
296299
});
297300
});

packages/plugin/src/channel.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,21 @@ async function handleCloudMessage(
602602
await handleModelsRequest(ctx);
603603
break;
604604

605+
case "settings.defaultModel": {
606+
const model = typeof msg.defaultModel === "string" ? msg.defaultModel.trim() : "";
607+
if (model) {
608+
ctx.log?.info(`[${ctx.accountId}] Setting OpenClaw default model to: ${model}`);
609+
const result = await openclawConfigSetDefaultModel(model, ctx.log);
610+
if (result.ok) {
611+
const client = getCloudClient(ctx.accountId);
612+
if (client?.connected) {
613+
client.send({ type: "defaultModel.updated", model });
614+
}
615+
}
616+
}
617+
break;
618+
}
619+
605620
default:
606621
break;
607622
}
@@ -642,6 +657,33 @@ function parseScheduleToOpenClaw(schedule: string): { kind: string; everyMs?: nu
642657
return null;
643658
}
644659

660+
/**
661+
* Run `openclaw config set agents.defaults.model.primary <model>` so the
662+
* user's default model choice in BotsChat takes effect on the OpenClaw gateway.
663+
*/
664+
async function openclawConfigSetDefaultModel(
665+
model: string,
666+
log?: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void },
667+
): Promise<{ ok: boolean; error?: string }> {
668+
if (!model || !model.trim()) return { ok: true };
669+
const { execFile } = await import("child_process");
670+
const { promisify } = await import("util");
671+
const execFileAsync = promisify(execFile);
672+
const args = ["config", "set", "agents.defaults.model.primary", model.trim()];
673+
log?.info(`Running: openclaw ${args.join(" ")}`);
674+
try {
675+
await execFileAsync("openclaw", args, {
676+
timeout: 10_000,
677+
env: { ...process.env, PATH: `/opt/homebrew/bin:${process.env.PATH}` },
678+
});
679+
return { ok: true };
680+
} catch (err: unknown) {
681+
const message = err instanceof Error ? err.message : String(err);
682+
log?.error(`openclaw config set default model failed: ${message}`);
683+
return { ok: false, error: message };
684+
}
685+
}
686+
645687
/**
646688
* Run `openclaw cron edit` to hot-update the CronService.
647689
* This uses the gateway's RPC (via CLI) so the in-memory scheduler is updated

packages/plugin/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ export type CloudOutbound =
110110
// Models list — plugin reports available providers/models
111111
| { type: "models.list"; models: Array<{ id: string; name: string; provider: string }> }
112112
// Model changed — plugin notifies that /model command switched the active model
113-
| { type: "model.changed"; model: string; sessionKey: string };
113+
| { type: "model.changed"; model: string; sessionKey: string }
114+
// Default model updated — plugin applied BotsChat default model to OpenClaw config
115+
| { type: "defaultModel.updated"; model: string };
114116

115117
/** Cloud → Plugin (inbound, user messages) */
116118
export type CloudInbound =
@@ -171,6 +173,8 @@ export type CloudInbound =
171173
// Task scan request — cloud asks plugin to scan existing cron jobs
172174
| { type: "task.scan.request" }
173175
// Models request — cloud asks plugin for available models/providers
174-
| { type: "models.request" };
176+
| { type: "models.request" }
177+
// Default model — cloud pushes user's default model so plugin applies it in OpenClaw config
178+
| { type: "settings.defaultModel"; defaultModel: string };
175179

176180
export type CloudMessage = CloudOutbound | CloudInbound;

packages/web/src/App.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,7 @@ export default function App() {
122122
.then((user) => {
123123
dlog.info("Auth", `Logged in as ${user.email} (${user.id})`);
124124
dispatch({ type: "SET_USER", user });
125-
if (user.settings?.defaultModel) {
126-
dispatch({ type: "SET_DEFAULT_MODEL", model: user.settings.defaultModel });
127-
}
125+
// defaultModel comes from plugin via connection.status, not from user.settings
128126
})
129127
.catch((err) => {
130128
dlog.warn("Auth", `Auto-login failed: ${err}`);

packages/web/src/store.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,12 @@ export function appReducer(state: AppState, action: AppAction): AppState {
302302
};
303303
}
304304
case "SET_OPENCLAW_CONNECTED":
305-
// connection.status carries the global defaultModel from OpenClaw config.
306-
// It never touches sessionModel — that's per-session and managed separately.
305+
// connection.status carries the gateway defaultModel (OpenClaw primary).
306+
// Prefer user's saved default (from API/settings); only use gateway default when user has not set one.
307307
return {
308308
...state,
309309
openclawConnected: action.connected,
310-
defaultModel: action.defaultModel ?? state.defaultModel,
310+
defaultModel: state.defaultModel ?? action.defaultModel ?? null,
311311
};
312312
case "SET_SESSION_MODEL":
313313
return { ...state, sessionModel: action.model };

scripts/dev.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ do_start() {
5858
}
5959

6060
do_sync_plugin() {
61-
local REMOTE="mini.local"
61+
local REMOTE_USER="mini.local"
6262
local REMOTE_DIR="~/Projects/botsChat/packages/plugin"
6363

64-
info "Syncing plugin to $REMOTE"
64+
info "Syncing plugin to mini.local"
6565
rsync -avz --exclude node_modules --exclude .git --exclude dist --exclude .wrangler \
66-
packages/plugin/ "$REMOTE:$REMOTE_DIR/"
66+
packages/plugin/ "$REMOTE_USER:$REMOTE_DIR/"
6767
ok "Plugin files synced"
6868

6969
info "Building plugin, deploying to extensions, restarting gateway on mini.local…"
70-
ssh "$REMOTE" 'export PATH="/opt/homebrew/bin:$PATH"
70+
ssh "$REMOTE_USER" 'export PATH="/opt/homebrew/bin:$PATH"
7171
cd ~/Projects/botsChat/packages/plugin
7272
npm run build
7373
EXT_DIR=~/.openclaw/extensions/botschat
@@ -83,7 +83,7 @@ echo "Gateway restarted (PID=$!)"'
8383

8484
sleep 4
8585
info "Checking connection…"
86-
ssh "$REMOTE" 'tail -5 /tmp/openclaw-gateway.log | grep -i "authenticated\|error\|Task scan"'
86+
ssh "$REMOTE_USER" 'tail -5 /tmp/openclaw-gateway.log | grep -i "authenticated\|error\|Task scan"'
8787
}
8888

8989
do_logs() {

0 commit comments

Comments
 (0)