Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddfa51ba31 | ||
|
|
467d32ae45 | ||
|
|
e54b9ac743 |
No files matched your search
+2
-2
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.22 (Unreleased)
|
## 2026.1.22
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||||
- Node: launch node host service with `node run` (not `node start`) and align node host hints/docs. (#1461) Thanks @ameno-.
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
|
|
||||||
## 2026.1.21-2
|
## 2026.1.21-2
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ Manage the service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node status
|
clawdbot node status
|
||||||
|
clawdbot node run
|
||||||
clawdbot node stop
|
clawdbot node stop
|
||||||
clawdbot node restart
|
clawdbot node restart
|
||||||
clawdbot node uninstall
|
clawdbot node uninstall
|
||||||
|
|||||||
+1
-3
@@ -57,11 +57,9 @@ clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||||
clawdbot node status
|
clawdbot node start
|
||||||
```
|
```
|
||||||
|
|
||||||
Install starts the service. Use `clawdbot node restart` if you need to re-launch it.
|
|
||||||
|
|
||||||
### Pair + name
|
### Pair + name
|
||||||
|
|
||||||
On the gateway host:
|
On the gateway host:
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("block streaming", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function waitForCalls(fn: () => number, calls: number) {
|
async function waitForCalls(fn: () => number, calls: number) {
|
||||||
const deadline = Date.now() + 5000;
|
const deadline = Date.now() + 1500;
|
||||||
while (fn() < calls) {
|
while (fn() < calls) {
|
||||||
if (Date.now() > deadline) {
|
if (Date.now() > deadline) {
|
||||||
throw new Error(`Expected ${calls} call(s), got ${fn()}`);
|
throw new Error(`Expected ${calls} call(s), got ${fn()}`);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type NodeDaemonStatusOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function renderNodeServiceStartHints(): string[] {
|
function renderNodeServiceStartHints(): string[] {
|
||||||
const base = [formatCliCommand("clawdbot node install"), formatCliCommand("clawdbot node run")];
|
const base = [formatCliCommand("clawdbot node install"), formatCliCommand("clawdbot node start")];
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export async function resolveNodeProgramArguments(params: {
|
|||||||
runtime?: GatewayRuntimePreference;
|
runtime?: GatewayRuntimePreference;
|
||||||
nodePath?: string;
|
nodePath?: string;
|
||||||
}): Promise<GatewayProgramArgs> {
|
}): Promise<GatewayProgramArgs> {
|
||||||
const args = ["node", "run", "--host", params.host, "--port", String(params.port)];
|
const args = ["node", "start", "--host", params.host, "--port", String(params.port)];
|
||||||
if (params.tls || params.tlsFingerprint) args.push("--tls");
|
if (params.tls || params.tlsFingerprint) args.push("--tls");
|
||||||
if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
|
if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
|
||||||
if (params.nodeId) args.push("--node-id", params.nodeId);
|
if (params.nodeId) args.push("--node-id", params.nodeId);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveAssistantAvatarUrl } from "./control-ui.js";
|
||||||
|
|
||||||
|
describe("resolveAssistantAvatarUrl", () => {
|
||||||
|
it("keeps remote and data URLs", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("https://example.com/avatar.png");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "data:image/png;base64,abc",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("data:image/png;base64,abc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefixes basePath for /avatar endpoints", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "/avatar/main",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "/ui/avatar/main",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps local avatar paths to the avatar endpoint", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "avatars/me.png",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "avatars/profile",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("/ui/avatar/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps short text avatars", () => {
|
||||||
|
expect(
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: "PS",
|
||||||
|
agentId: "main",
|
||||||
|
basePath: "/ui",
|
||||||
|
}),
|
||||||
|
).toBe("PS");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -98,7 +98,7 @@ function sendJson(res: ServerResponse, status: number, body: unknown) {
|
|||||||
res.end(JSON.stringify(body));
|
res.end(JSON.stringify(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAvatarUrl(basePath: string, agentId: string): string {
|
export function buildAvatarUrl(basePath: string, agentId: string): string {
|
||||||
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
return basePath ? `${basePath}${AVATAR_PREFIX}/${agentId}` : `${AVATAR_PREFIX}/${agentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +206,49 @@ interface ServeIndexHtmlOpts {
|
|||||||
agentId?: string;
|
agentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function looksLikeLocalAvatarPath(value: string): boolean {
|
||||||
|
if (/[\\/]/.test(value)) return true;
|
||||||
|
return /\.(png|jpe?g|gif|webp|svg|ico)$/i.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAssistantAvatarUrl(params: {
|
||||||
|
avatar?: string | null;
|
||||||
|
agentId?: string | null;
|
||||||
|
basePath?: string;
|
||||||
|
}): string | undefined {
|
||||||
|
const avatar = params.avatar?.trim();
|
||||||
|
if (!avatar) return undefined;
|
||||||
|
if (/^https?:\/\//i.test(avatar) || /^data:image\//i.test(avatar)) return avatar;
|
||||||
|
|
||||||
|
const basePath = normalizeControlUiBasePath(params.basePath);
|
||||||
|
const baseAvatarPrefix = basePath ? `${basePath}${AVATAR_PREFIX}/` : `${AVATAR_PREFIX}/`;
|
||||||
|
if (basePath && avatar.startsWith(`${AVATAR_PREFIX}/`)) {
|
||||||
|
return `${basePath}${avatar}`;
|
||||||
|
}
|
||||||
|
if (avatar.startsWith(baseAvatarPrefix)) return avatar;
|
||||||
|
|
||||||
|
if (!params.agentId) return avatar;
|
||||||
|
if (looksLikeLocalAvatarPath(avatar)) {
|
||||||
|
return buildAvatarUrl(basePath, params.agentId);
|
||||||
|
}
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
|
function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndexHtmlOpts) {
|
||||||
const { basePath, config, agentId } = opts;
|
const { basePath, config, agentId } = opts;
|
||||||
const identity = config
|
const identity = config
|
||||||
? resolveAssistantIdentity({ cfg: config, agentId })
|
? resolveAssistantIdentity({ cfg: config, agentId })
|
||||||
: DEFAULT_ASSISTANT_IDENTITY;
|
: DEFAULT_ASSISTANT_IDENTITY;
|
||||||
|
const resolvedAgentId =
|
||||||
|
typeof (identity as { agentId?: string }).agentId === "string"
|
||||||
|
? (identity as { agentId?: string }).agentId
|
||||||
|
: agentId;
|
||||||
|
const avatarValue =
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: identity.avatar,
|
||||||
|
agentId: resolvedAgentId,
|
||||||
|
basePath,
|
||||||
|
}) ?? identity.avatar;
|
||||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||||
res.setHeader("Cache-Control", "no-cache");
|
res.setHeader("Cache-Control", "no-cache");
|
||||||
const raw = fs.readFileSync(indexPath, "utf8");
|
const raw = fs.readFileSync(indexPath, "utf8");
|
||||||
@@ -218,7 +256,7 @@ function serveIndexHtml(res: ServerResponse, indexPath: string, opts: ServeIndex
|
|||||||
injectControlUiConfig(raw, {
|
injectControlUiConfig(raw, {
|
||||||
basePath,
|
basePath,
|
||||||
assistantName: identity.name,
|
assistantName: identity.name,
|
||||||
assistantAvatar: identity.avatar,
|
assistantAvatar: avatarValue,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
import { loadSessionEntry } from "../session-utils.js";
|
import { loadSessionEntry } from "../session-utils.js";
|
||||||
import { formatForLog } from "../ws-log.js";
|
import { formatForLog } from "../ws-log.js";
|
||||||
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
import { resolveAssistantIdentity } from "../assistant-identity.js";
|
||||||
|
import { resolveAssistantAvatarUrl } from "../control-ui.js";
|
||||||
import { waitForAgentJob } from "./agent-job.js";
|
import { waitForAgentJob } from "./agent-job.js";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
|
|
||||||
@@ -407,7 +408,13 @@ export const agentHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const identity = resolveAssistantIdentity({ cfg, agentId });
|
const identity = resolveAssistantIdentity({ cfg, agentId });
|
||||||
respond(true, identity, undefined);
|
const avatarValue =
|
||||||
|
resolveAssistantAvatarUrl({
|
||||||
|
avatar: identity.avatar,
|
||||||
|
agentId: identity.agentId,
|
||||||
|
basePath: cfg.gateway?.controlUi?.basePath,
|
||||||
|
}) ?? identity.avatar;
|
||||||
|
respond(true, { ...identity, avatar: avatarValue }, undefined);
|
||||||
},
|
},
|
||||||
"agent.wait": async ({ params, respond }) => {
|
"agent.wait": async ({ params, respond }) => {
|
||||||
if (!validateAgentWaitParams(params)) {
|
if (!validateAgentWaitParams(params)) {
|
||||||
|
|||||||
@@ -158,7 +158,8 @@ function renderAvatar(
|
|||||||
function isAvatarUrl(value: string): boolean {
|
function isAvatarUrl(value: string): boolean {
|
||||||
return (
|
return (
|
||||||
/^https?:\/\//i.test(value) ||
|
/^https?:\/\//i.test(value) ||
|
||||||
/^data:image\//i.test(value)
|
/^data:image\//i.test(value) ||
|
||||||
|
/^\//.test(value) // Relative paths from avatar endpoint
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user