@castlekit/castle 0.4.2 → 0.4.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +2 -2
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.html +1 -1
- package/.next/standalone/.next/server/app/chat.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/chat.segments/chat.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.html +1 -1
- package/.next/standalone/.next/server/app/ui-kit.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit.segment.rsc +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__2824c41d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__361bce14._.js +1 -1
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +2 -2
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/CHANGELOG.md +10 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/api/openclaw/agents/[id]/avatar/route.ts +51 -91
- package/.next/standalone/src/app/api/openclaw/agents/route.ts +12 -0
- package/package.json +1 -1
- package/src/app/api/openclaw/agents/[id]/avatar/route.ts +51 -91
- package/src/app/api/openclaw/agents/route.ts +12 -0
- /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/standalone/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_ssgManifest.js +0 -0
- /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_buildManifest.js +0 -0
- /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
-
import JSON5 from "json5";
|
|
6
5
|
import sharp from "sharp";
|
|
7
6
|
import { ensureGateway } from "@/lib/gateway-connection";
|
|
8
7
|
import { checkCsrf } from "@/lib/api-security";
|
|
@@ -20,21 +19,6 @@ const ALLOWED_TYPES = new Set([
|
|
|
20
19
|
"image/gif",
|
|
21
20
|
]);
|
|
22
21
|
|
|
23
|
-
interface AgentConfig {
|
|
24
|
-
id: string;
|
|
25
|
-
workspace?: string;
|
|
26
|
-
identity?: Record<string, unknown>;
|
|
27
|
-
[key: string]: unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface OpenClawConfig {
|
|
31
|
-
agents?: {
|
|
32
|
-
list?: AgentConfig[];
|
|
33
|
-
[key: string]: unknown;
|
|
34
|
-
};
|
|
35
|
-
[key: string]: unknown;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
22
|
/**
|
|
39
23
|
* Resize and compress an avatar image to 256x256, under 100KB.
|
|
40
24
|
*/
|
|
@@ -59,27 +43,13 @@ async function processAvatar(input: Buffer): Promise<{ data: Buffer; ext: string
|
|
|
59
43
|
return { data, ext: ".jpg" };
|
|
60
44
|
}
|
|
61
45
|
|
|
62
|
-
/**
|
|
63
|
-
* Find the OpenClaw config file path.
|
|
64
|
-
*/
|
|
65
|
-
function getOpenClawConfigPath(): string | null {
|
|
66
|
-
const paths = [
|
|
67
|
-
join(homedir(), ".openclaw", "openclaw.json"),
|
|
68
|
-
join(homedir(), ".openclaw", "openclaw.json5"),
|
|
69
|
-
];
|
|
70
|
-
for (const p of paths) {
|
|
71
|
-
if (existsSync(p)) return p;
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
46
|
/**
|
|
77
47
|
* POST /api/openclaw/agents/[id]/avatar
|
|
78
48
|
*
|
|
79
49
|
* Upload a new avatar image for an agent.
|
|
80
50
|
* - Resizes to 256x256 and compresses under 100KB
|
|
81
|
-
* - Saves to
|
|
82
|
-
* -
|
|
51
|
+
* - Saves to Castle's own avatars directory (~/.castle/avatars/)
|
|
52
|
+
* - Updates OpenClaw config via Gateway's config.patch RPC (never writes to OpenClaw files directly)
|
|
83
53
|
*/
|
|
84
54
|
export async function POST(
|
|
85
55
|
request: NextRequest,
|
|
@@ -128,46 +98,9 @@ export async function POST(
|
|
|
128
98
|
);
|
|
129
99
|
}
|
|
130
100
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
return NextResponse.json(
|
|
135
|
-
{ error: "OpenClaw config file not found" },
|
|
136
|
-
{ status: 500 }
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let config: OpenClawConfig;
|
|
141
|
-
try {
|
|
142
|
-
config = JSON5.parse(readFileSync(configPath, "utf-8"));
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return NextResponse.json(
|
|
145
|
-
{ error: `Failed to read config: ${err instanceof Error ? err.message : "unknown"}` },
|
|
146
|
-
{ status: 500 }
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const agents = config.agents?.list || [];
|
|
151
|
-
const agent = agents.find((a) => a.id === safeId);
|
|
152
|
-
|
|
153
|
-
if (!agent) {
|
|
154
|
-
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!agent.workspace) {
|
|
158
|
-
return NextResponse.json(
|
|
159
|
-
{ error: "Agent has no workspace configured" },
|
|
160
|
-
{ status: 400 }
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const workspacePath = agent.workspace.startsWith("~")
|
|
165
|
-
? join(homedir(), agent.workspace.slice(1))
|
|
166
|
-
: agent.workspace;
|
|
167
|
-
|
|
168
|
-
// Save processed avatar to workspace
|
|
169
|
-
const avatarsDir = join(workspacePath, "avatars");
|
|
170
|
-
const fileName = `avatar${processed.ext}`;
|
|
101
|
+
// Save processed avatar to Castle's own directory — never write to OpenClaw's filesystem
|
|
102
|
+
const avatarsDir = join(homedir(), ".castle", "avatars");
|
|
103
|
+
const fileName = `${safeId}${processed.ext}`;
|
|
171
104
|
const filePath = join(avatarsDir, fileName);
|
|
172
105
|
|
|
173
106
|
try {
|
|
@@ -180,37 +113,64 @@ export async function POST(
|
|
|
180
113
|
);
|
|
181
114
|
}
|
|
182
115
|
|
|
183
|
-
// Update config
|
|
184
|
-
//
|
|
116
|
+
// Update OpenClaw config via Gateway's config.patch RPC — the proper way to
|
|
117
|
+
// modify OpenClaw config without writing to its files directly.
|
|
118
|
+
const gw = ensureGateway();
|
|
119
|
+
|
|
120
|
+
if (!gw.isConnected) {
|
|
121
|
+
// Avatar is saved locally; config update will happen when Gateway reconnects
|
|
122
|
+
return NextResponse.json({
|
|
123
|
+
success: true,
|
|
124
|
+
avatar: filePath,
|
|
125
|
+
size: processed.data.length,
|
|
126
|
+
message: "Avatar saved locally. Gateway not connected — config will update when it reconnects.",
|
|
127
|
+
configUpdated: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
185
131
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
132
|
+
// Use config.patch to set the agent's avatar path.
|
|
133
|
+
// Gateway validates and applies the patch, then hot-reloads.
|
|
134
|
+
await gw.request("config.patch", {
|
|
135
|
+
agents: {
|
|
136
|
+
list: [
|
|
137
|
+
{
|
|
138
|
+
id: safeId,
|
|
139
|
+
identity: {
|
|
140
|
+
avatar: filePath,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
console.log(`[Avatar API] Config patched for agent ${safeId}`);
|
|
189
147
|
} catch (err) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
148
|
+
console.error(
|
|
149
|
+
`[Avatar API] config.patch failed for agent ${safeId}:`,
|
|
150
|
+
err instanceof Error ? err.message : "unknown"
|
|
193
151
|
);
|
|
152
|
+
// Avatar is still saved locally — not a total failure
|
|
153
|
+
return NextResponse.json({
|
|
154
|
+
success: true,
|
|
155
|
+
avatar: filePath,
|
|
156
|
+
size: processed.data.length,
|
|
157
|
+
message: "Avatar saved but config update failed. Try restarting the Gateway.",
|
|
158
|
+
configUpdated: false,
|
|
159
|
+
});
|
|
194
160
|
}
|
|
195
161
|
|
|
196
|
-
//
|
|
197
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
198
|
-
|
|
199
|
-
// Refresh Castle's agent list from the Gateway
|
|
162
|
+
// Emit a signal so SSE clients re-fetch agents
|
|
200
163
|
try {
|
|
201
|
-
|
|
202
|
-
if (gw.isConnected) {
|
|
203
|
-
// Emit a signal so SSE clients re-fetch agents
|
|
204
|
-
gw.emit("agentAvatarUpdated", { agentId: safeId });
|
|
205
|
-
}
|
|
164
|
+
gw.emit("agentAvatarUpdated", { agentId: safeId });
|
|
206
165
|
} catch {
|
|
207
166
|
// Non-critical
|
|
208
167
|
}
|
|
209
168
|
|
|
210
169
|
return NextResponse.json({
|
|
211
170
|
success: true,
|
|
212
|
-
avatar:
|
|
171
|
+
avatar: filePath,
|
|
213
172
|
size: processed.data.length,
|
|
214
173
|
message: "Avatar updated",
|
|
174
|
+
configUpdated: true,
|
|
215
175
|
});
|
|
216
176
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
2
4
|
import { ensureGateway } from "@/lib/gateway-connection";
|
|
3
5
|
|
|
4
6
|
export const dynamic = "force-dynamic";
|
|
5
7
|
|
|
8
|
+
const CASTLE_AVATARS_DIR = join(homedir(), ".castle", "avatars");
|
|
9
|
+
|
|
6
10
|
interface AgentIdentity {
|
|
7
11
|
name?: string;
|
|
8
12
|
theme?: string;
|
|
@@ -86,6 +90,14 @@ function resolveAvatarUrl(
|
|
|
86
90
|
return `/api/avatars/${key}`;
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
// Absolute path under ~/.castle/avatars/ (from avatar upload via config.patch)
|
|
94
|
+
if (avatar.startsWith("/") || avatar.startsWith("~")) {
|
|
95
|
+
const normalized = resolve(avatar.replace(/^~/, homedir()));
|
|
96
|
+
if (normalized.startsWith(CASTLE_AVATARS_DIR + "/") || normalized === CASTLE_AVATARS_DIR) {
|
|
97
|
+
return `/api/avatars/${agentId}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
return null;
|
|
90
102
|
}
|
|
91
103
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
-
import JSON5 from "json5";
|
|
6
5
|
import sharp from "sharp";
|
|
7
6
|
import { ensureGateway } from "@/lib/gateway-connection";
|
|
8
7
|
import { checkCsrf } from "@/lib/api-security";
|
|
@@ -20,21 +19,6 @@ const ALLOWED_TYPES = new Set([
|
|
|
20
19
|
"image/gif",
|
|
21
20
|
]);
|
|
22
21
|
|
|
23
|
-
interface AgentConfig {
|
|
24
|
-
id: string;
|
|
25
|
-
workspace?: string;
|
|
26
|
-
identity?: Record<string, unknown>;
|
|
27
|
-
[key: string]: unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface OpenClawConfig {
|
|
31
|
-
agents?: {
|
|
32
|
-
list?: AgentConfig[];
|
|
33
|
-
[key: string]: unknown;
|
|
34
|
-
};
|
|
35
|
-
[key: string]: unknown;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
22
|
/**
|
|
39
23
|
* Resize and compress an avatar image to 256x256, under 100KB.
|
|
40
24
|
*/
|
|
@@ -59,27 +43,13 @@ async function processAvatar(input: Buffer): Promise<{ data: Buffer; ext: string
|
|
|
59
43
|
return { data, ext: ".jpg" };
|
|
60
44
|
}
|
|
61
45
|
|
|
62
|
-
/**
|
|
63
|
-
* Find the OpenClaw config file path.
|
|
64
|
-
*/
|
|
65
|
-
function getOpenClawConfigPath(): string | null {
|
|
66
|
-
const paths = [
|
|
67
|
-
join(homedir(), ".openclaw", "openclaw.json"),
|
|
68
|
-
join(homedir(), ".openclaw", "openclaw.json5"),
|
|
69
|
-
];
|
|
70
|
-
for (const p of paths) {
|
|
71
|
-
if (existsSync(p)) return p;
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
46
|
/**
|
|
77
47
|
* POST /api/openclaw/agents/[id]/avatar
|
|
78
48
|
*
|
|
79
49
|
* Upload a new avatar image for an agent.
|
|
80
50
|
* - Resizes to 256x256 and compresses under 100KB
|
|
81
|
-
* - Saves to
|
|
82
|
-
* -
|
|
51
|
+
* - Saves to Castle's own avatars directory (~/.castle/avatars/)
|
|
52
|
+
* - Updates OpenClaw config via Gateway's config.patch RPC (never writes to OpenClaw files directly)
|
|
83
53
|
*/
|
|
84
54
|
export async function POST(
|
|
85
55
|
request: NextRequest,
|
|
@@ -128,46 +98,9 @@ export async function POST(
|
|
|
128
98
|
);
|
|
129
99
|
}
|
|
130
100
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
return NextResponse.json(
|
|
135
|
-
{ error: "OpenClaw config file not found" },
|
|
136
|
-
{ status: 500 }
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let config: OpenClawConfig;
|
|
141
|
-
try {
|
|
142
|
-
config = JSON5.parse(readFileSync(configPath, "utf-8"));
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return NextResponse.json(
|
|
145
|
-
{ error: `Failed to read config: ${err instanceof Error ? err.message : "unknown"}` },
|
|
146
|
-
{ status: 500 }
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const agents = config.agents?.list || [];
|
|
151
|
-
const agent = agents.find((a) => a.id === safeId);
|
|
152
|
-
|
|
153
|
-
if (!agent) {
|
|
154
|
-
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!agent.workspace) {
|
|
158
|
-
return NextResponse.json(
|
|
159
|
-
{ error: "Agent has no workspace configured" },
|
|
160
|
-
{ status: 400 }
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const workspacePath = agent.workspace.startsWith("~")
|
|
165
|
-
? join(homedir(), agent.workspace.slice(1))
|
|
166
|
-
: agent.workspace;
|
|
167
|
-
|
|
168
|
-
// Save processed avatar to workspace
|
|
169
|
-
const avatarsDir = join(workspacePath, "avatars");
|
|
170
|
-
const fileName = `avatar${processed.ext}`;
|
|
101
|
+
// Save processed avatar to Castle's own directory — never write to OpenClaw's filesystem
|
|
102
|
+
const avatarsDir = join(homedir(), ".castle", "avatars");
|
|
103
|
+
const fileName = `${safeId}${processed.ext}`;
|
|
171
104
|
const filePath = join(avatarsDir, fileName);
|
|
172
105
|
|
|
173
106
|
try {
|
|
@@ -180,37 +113,64 @@ export async function POST(
|
|
|
180
113
|
);
|
|
181
114
|
}
|
|
182
115
|
|
|
183
|
-
// Update config
|
|
184
|
-
//
|
|
116
|
+
// Update OpenClaw config via Gateway's config.patch RPC — the proper way to
|
|
117
|
+
// modify OpenClaw config without writing to its files directly.
|
|
118
|
+
const gw = ensureGateway();
|
|
119
|
+
|
|
120
|
+
if (!gw.isConnected) {
|
|
121
|
+
// Avatar is saved locally; config update will happen when Gateway reconnects
|
|
122
|
+
return NextResponse.json({
|
|
123
|
+
success: true,
|
|
124
|
+
avatar: filePath,
|
|
125
|
+
size: processed.data.length,
|
|
126
|
+
message: "Avatar saved locally. Gateway not connected — config will update when it reconnects.",
|
|
127
|
+
configUpdated: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
185
131
|
try {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
132
|
+
// Use config.patch to set the agent's avatar path.
|
|
133
|
+
// Gateway validates and applies the patch, then hot-reloads.
|
|
134
|
+
await gw.request("config.patch", {
|
|
135
|
+
agents: {
|
|
136
|
+
list: [
|
|
137
|
+
{
|
|
138
|
+
id: safeId,
|
|
139
|
+
identity: {
|
|
140
|
+
avatar: filePath,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
console.log(`[Avatar API] Config patched for agent ${safeId}`);
|
|
189
147
|
} catch (err) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
148
|
+
console.error(
|
|
149
|
+
`[Avatar API] config.patch failed for agent ${safeId}:`,
|
|
150
|
+
err instanceof Error ? err.message : "unknown"
|
|
193
151
|
);
|
|
152
|
+
// Avatar is still saved locally — not a total failure
|
|
153
|
+
return NextResponse.json({
|
|
154
|
+
success: true,
|
|
155
|
+
avatar: filePath,
|
|
156
|
+
size: processed.data.length,
|
|
157
|
+
message: "Avatar saved but config update failed. Try restarting the Gateway.",
|
|
158
|
+
configUpdated: false,
|
|
159
|
+
});
|
|
194
160
|
}
|
|
195
161
|
|
|
196
|
-
//
|
|
197
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
198
|
-
|
|
199
|
-
// Refresh Castle's agent list from the Gateway
|
|
162
|
+
// Emit a signal so SSE clients re-fetch agents
|
|
200
163
|
try {
|
|
201
|
-
|
|
202
|
-
if (gw.isConnected) {
|
|
203
|
-
// Emit a signal so SSE clients re-fetch agents
|
|
204
|
-
gw.emit("agentAvatarUpdated", { agentId: safeId });
|
|
205
|
-
}
|
|
164
|
+
gw.emit("agentAvatarUpdated", { agentId: safeId });
|
|
206
165
|
} catch {
|
|
207
166
|
// Non-critical
|
|
208
167
|
}
|
|
209
168
|
|
|
210
169
|
return NextResponse.json({
|
|
211
170
|
success: true,
|
|
212
|
-
avatar:
|
|
171
|
+
avatar: filePath,
|
|
213
172
|
size: processed.data.length,
|
|
214
173
|
message: "Avatar updated",
|
|
174
|
+
configUpdated: true,
|
|
215
175
|
});
|
|
216
176
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { join, resolve } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
2
4
|
import { ensureGateway } from "@/lib/gateway-connection";
|
|
3
5
|
|
|
4
6
|
export const dynamic = "force-dynamic";
|
|
5
7
|
|
|
8
|
+
const CASTLE_AVATARS_DIR = join(homedir(), ".castle", "avatars");
|
|
9
|
+
|
|
6
10
|
interface AgentIdentity {
|
|
7
11
|
name?: string;
|
|
8
12
|
theme?: string;
|
|
@@ -86,6 +90,14 @@ function resolveAvatarUrl(
|
|
|
86
90
|
return `/api/avatars/${key}`;
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
// Absolute path under ~/.castle/avatars/ (from avatar upload via config.patch)
|
|
94
|
+
if (avatar.startsWith("/") || avatar.startsWith("~")) {
|
|
95
|
+
const normalized = resolve(avatar.replace(/^~/, homedir()));
|
|
96
|
+
if (normalized.startsWith(CASTLE_AVATARS_DIR + "/") || normalized === CASTLE_AVATARS_DIR) {
|
|
97
|
+
return `/api/avatars/${agentId}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
return null;
|
|
90
102
|
}
|
|
91
103
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/.next/static/{-R3HvmmPKzJ9Jfjg7hRqd → PzpTWZTNAsAIh96H48auo}/_clientMiddlewareManifest.json
RENAMED
|
File without changes
|
|
File without changes
|