Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.ja-JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ ClawXは公式の**OpenClaw**コアを直接ベースに構築されています
インストールから最初のAIインタラクションまで、すべてのセットアップを直感的なグラフィカルインターフェースで完了できます。ターミナルコマンド不要、YAMLファイル不要、環境変数の探索も不要です。

### 💬 インテリジェントチャットインターフェース
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、Markdownによるリッチコンテンツレンダリングに加え、チャットツールバーから会話単位で複数エージェントを参加させられます。
チャット内の `+ Add Agent` を使うと、既存エージェントを1つ以上その会話にアタッチできます。現在の会話は主ストリームのまま維持され、追加エージェントはそれぞれの関連コンテキストで後続処理を行い、その返信がエージェント名付きで同じチャット画面へミラー表示されます。各エージェントのワークスペースは既定で分離されていますが、より強い実行時分離は OpenClaw の sandbox 設定に依存します。
ClawX には **設定 → 詳細設定** から有効化できる mem0 ベースのメモリ層もあります。これを有効にすると、完了した会話ターンを長期メモリとして保存し、新しい送信の前に関連コンテキストを取得し、会話履歴が大きくなりすぎたときはトランスクリプトを圧縮して、直近のスライディングウィンドウと取得メモリだけをモデルへ渡します。
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、Markdownによるリッチコンテンツレンダリングに加え、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。
`@agent` で別のエージェントを選ぶと、ClawX はデフォルトエージェントを経由せず、そのエージェント自身の会話コンテキストへ直接切り替えます。各エージェントのワークスペースは既定で分離されていますが、より強い実行時分離は OpenClaw の sandbox 設定に依存します。
各 Agent は `provider/model` の実行時設定を個別に上書きできます。上書きしていない Agent は引き続きグローバルの既定モデルを継承します。
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ We are committed to maintaining strict alignment with the upstream OpenClaw proj
Complete the entire setup—from installation to your first AI interaction—through an intuitive graphical interface. No terminal commands, no YAML files, no environment variable hunting.

### 💬 Intelligent Chat Interface
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, rich content rendering with Markdown, and session-level multi-agent participation from the chat toolbar.
Use the `+ Add Agent` control inside a chat to attach one or more existing agents to that conversation. The current session remains the primary streamed thread, while attached agents are invoked afterward in their own linked contexts and their replies are mirrored back into the same chat window with agent labels. Agent workspaces stay separate by default, and stronger isolation still depends on OpenClaw sandbox settings.
ClawX also includes an optional mem0-backed memory layer in **Settings → Advanced**. When enabled, ClawX stores completed chat turns as long-term memories, recalls relevant context before each new send, and compacts oversized session transcripts so the live prompt only carries a recent sliding window plus retrieved memory.
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, rich content rendering with Markdown, and direct `@agent` routing in the main composer for multi-agent setups.
When you target another agent with `@agent`, ClawX switches into that agent's own conversation context directly instead of relaying through the default agent. Agent workspaces stay separate by default, and stronger isolation depends on OpenClaw sandbox settings.
Each agent can also override its own `provider/model` runtime setting; agents without overrides continue inheriting the global default model.
Expand Down
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
从安装到第一次 AI 对话,全程通过直观的图形界面完成。无需终端命令,无需 YAML 文件,无需到处寻找环境变量。

### 💬 智能聊天界面
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、Markdown 富文本渲染,以及在聊天工具栏中按会话附加多个 Agent。
在聊天窗口里使用 `+ Add Agent`,即可把一个或多个现有 Agent 附加到当前会话。当前会话仍然是主流式对话线程,而附加 Agent 会在各自关联的上下文中继续处理,并将回复带着 Agent 标识镜像回同一个聊天窗口。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
ClawX 还在 **设置 → 高级** 中提供可选的 mem0 记忆层。启用后,应用会把已完成的对话轮次写入长期记忆,在每次新消息发送前召回相关上下文,并在会话转录过长时自动压缩,只把最近的滑动窗口消息与召回记忆送给模型。
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、Markdown 富文本渲染,以及在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。
当你使用 `@agent` 选择其他智能体时,ClawX 会直接切换到该智能体自己的对话上下文,而不是经过默认智能体转发。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
每个 Agent 还可以单独覆盖自己的 `provider/model` 运行时设置;未覆盖的 Agent 会继续继承全局默认模型。
Expand Down
2 changes: 2 additions & 0 deletions electron/api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type { BrowserWindow } from 'electron';
import type { GatewayManager } from '../gateway/manager';
import type { ClawHubService } from '../gateway/clawhub';
import type { HostEventBus } from './event-bus';
import type { Mem0Service } from '../services/mem0/service';

export interface HostApiContext {
gatewayManager: GatewayManager;
clawHubService: ClawHubService;
mem0Service: Mem0Service;
eventBus: HostEventBus;
mainWindow: BrowserWindow | null;
}
12 changes: 11 additions & 1 deletion electron/api/routes/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export async function handleGatewayRoutes(
deliver?: boolean;
idempotencyKey: string;
media?: Array<{ filePath: string; mimeType: string; fileName: string }>;
memoryContext?: { rootSessionKey?: string };
}>(req);
const VISION_MIME_TYPES = new Set([
'image/png', 'image/jpeg', 'image/bmp', 'image/webp',
Expand All @@ -109,15 +110,24 @@ export async function handleGatewayRoutes(
const message = fileReferences.length > 0
? [body.message, ...fileReferences].filter(Boolean).join('\n')
: body.message;
const rpcParams: Record<string, unknown> = {
let rpcParams: Record<string, unknown> = {
sessionKey: body.sessionKey,
message,
deliver: body.deliver ?? false,
idempotencyKey: body.idempotencyKey,
memoryContext: body.memoryContext,
};
if (imageAttachments.length > 0) {
rpcParams.attachments = imageAttachments;
}
rpcParams = await ctx.mem0Service.prepareChatSend(rpcParams as {
sessionKey: string;
message: string;
deliver?: boolean;
idempotencyKey?: string;
attachments?: unknown;
memoryContext?: { rootSessionKey?: string };
}) as Record<string, unknown>;
const result = await ctx.gatewayManager.rpc('chat.send', rpcParams, 120000);
sendJson(res, 200, { success: true, result });
} catch (error) {
Expand Down
28 changes: 28 additions & 0 deletions electron/api/routes/mem0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { IncomingMessage, ServerResponse } from 'http';
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
import type { Mem0Settings } from '../../../shared/mem0';

export async function handleMem0Routes(
req: IncomingMessage,
res: ServerResponse,
url: URL,
ctx: HostApiContext,
): Promise<boolean> {
if (url.pathname === '/api/mem0/config' && req.method === 'GET') {
sendJson(res, 200, await ctx.mem0Service.getConfigSnapshot());
return true;
}

if (url.pathname === '/api/mem0/config' && req.method === 'PUT') {
try {
const body = await parseJsonBody<Partial<Mem0Settings> & { apiKey?: string; clearApiKey?: boolean }>(req);
sendJson(res, 200, await ctx.mem0Service.saveConfig(body));
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
return true;
}

return false;
}
3 changes: 3 additions & 0 deletions electron/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { handleSkillRoutes } from './routes/skills';
import { handleFileRoutes } from './routes/files';
import { handleSessionRoutes } from './routes/sessions';
import { handleCronRoutes } from './routes/cron';
import { handleMem0Routes } from './routes/mem0';
import { sendJson } from './route-utils';
import { sendJson, setCorsHeaders, requireJsonContentType } from './route-utils';

type RouteHandler = (
Expand All @@ -35,6 +37,7 @@ const routeHandlers: RouteHandler[] = [
handleFileRoutes,
handleSessionRoutes,
handleCronRoutes,
handleMem0Routes,
handleLogRoutes,
handleUsageRoutes,
];
Expand Down
6 changes: 5 additions & 1 deletion electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { deviceOAuthManager } from '../utils/device-oauth';
import { browserOAuthManager } from '../utils/browser-oauth';
import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync';
import { Mem0Service } from '../services/mem0/service';

const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop';

Expand Down Expand Up @@ -112,6 +113,7 @@ let mainWindow: BrowserWindow | null = null;
let gatewayManager!: GatewayManager;
let clawHubService!: ClawHubService;
let hostEventBus!: HostEventBus;
let mem0Service!: Mem0Service;
let hostApiServer: Server | null = null;
const mainWindowFocusState = createMainWindowFocusState();
const quitLifecycleState = createQuitLifecycleState();
Expand Down Expand Up @@ -307,11 +309,12 @@ async function initialize(): Promise<void> {
);

// Register IPC handlers
registerIpcHandlers(gatewayManager, clawHubService, window);
registerIpcHandlers(gatewayManager, clawHubService, window, mem0Service);

hostApiServer = startHostApiServer({
gatewayManager,
clawHubService,
mem0Service,
eventBus: hostEventBus,
mainWindow: window,
});
Expand Down Expand Up @@ -481,6 +484,7 @@ if (gotTheLock) {
gatewayManager = new GatewayManager();
clawHubService = new ClawHubService();
hostEventBus = new HostEventBus();
mem0Service = new Mem0Service(gatewayManager);

// When a second instance is launched, focus the existing window instead.
app.on('second-instance', () => {
Expand Down
55 changes: 50 additions & 5 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ import {
} from '../services/providers/provider-runtime-sync';
import { validateApiKeyWithProvider } from '../services/providers/provider-validation';
import { appUpdater } from './updater';
import { PORTS } from '../utils/config';
import { Mem0Service } from '../services/mem0/service';

type AppRequest = {
id?: string;
module: string;
action: string;
payload?: unknown;
};

type AppErrorCode = 'VALIDATION' | 'PERMISSION' | 'TIMEOUT' | 'GATEWAY' | 'INTERNAL' | 'UNSUPPORTED';

type AppResponse = {
id?: string;
ok: boolean;
data?: unknown;
error?: {
code: AppErrorCode;
message: string;
details?: unknown;
};
};
import { registerHostApiProxyHandlers } from './ipc/host-api-proxy';
import {
isLaunchAtStartupKey,
Expand All @@ -76,7 +98,8 @@ import {
export function registerIpcHandlers(
gatewayManager: GatewayManager,
clawHubService: ClawHubService,
mainWindow: BrowserWindow
mainWindow: BrowserWindow,
mem0Service: Mem0Service,
): void {
// Unified request protocol (non-breaking: legacy channels remain available)
registerUnifiedRequestHandlers(gatewayManager);
Expand All @@ -85,7 +108,7 @@ export function registerIpcHandlers(
registerHostApiProxyHandlers();

// Gateway handlers
registerGatewayHandlers(gatewayManager, mainWindow);
registerGatewayHandlers(gatewayManager, mainWindow, mem0Service);

// ClawHub handlers
registerClawHubHandlers(clawHubService);
Expand Down Expand Up @@ -1097,7 +1120,8 @@ function registerLogHandlers(): void {
*/
function registerGatewayHandlers(
gatewayManager: GatewayManager,
mainWindow: BrowserWindow
mainWindow: BrowserWindow,
mem0Service: Mem0Service,
): void {
type GatewayHttpProxyRequest = {
path?: string;
Expand Down Expand Up @@ -1150,7 +1174,17 @@ function registerGatewayHandlers(
// Gateway RPC call
ipcMain.handle('gateway:rpc', async (_, method: string, params?: unknown, timeoutMs?: number) => {
try {
const result = await gatewayManager.rpc(method, params, timeoutMs);
const resolvedParams = method === 'chat.send' && params && typeof params === 'object'
? await mem0Service.prepareChatSend(params as {
sessionKey: string;
message: string;
deliver?: boolean;
idempotencyKey?: string;
attachments?: unknown;
memoryContext?: { rootSessionKey?: string };
})
: params;
const result = await gatewayManager.rpc(method, resolvedParams, timeoutMs);
return { success: true, result };
} catch (error) {
return { success: false, error: String(error) };
Expand Down Expand Up @@ -1242,6 +1276,7 @@ function registerGatewayHandlers(
deliver?: boolean;
idempotencyKey: string;
media?: Array<{ filePath: string; mimeType: string; fileName: string }>;
memoryContext?: { rootSessionKey?: string };
}) => {
try {
let message = params.message;
Expand Down Expand Up @@ -1289,17 +1324,27 @@ function registerGatewayHandlers(
message = message ? `${message}\n\n${refs}` : refs;
}

const rpcParams: Record<string, unknown> = {
let rpcParams: Record<string, unknown> = {
sessionKey: params.sessionKey,
message,
deliver: params.deliver ?? false,
idempotencyKey: params.idempotencyKey,
memoryContext: params.memoryContext,
};

if (imageAttachments.length > 0) {
rpcParams.attachments = imageAttachments;
}

rpcParams = await mem0Service.prepareChatSend(rpcParams as {
sessionKey: string;
message: string;
deliver?: boolean;
idempotencyKey?: string;
attachments?: unknown;
memoryContext?: { rootSessionKey?: string };
}) as Record<string, unknown>;

logger.info(`[chat:sendWithMedia] Sending: message="${message.substring(0, 100)}", attachments=${imageAttachments.length}, fileRefs=${fileReferences.length}`);

// Longer timeout for chat sends to tolerate high-latency networks (avoids connect error)
Expand Down
33 changes: 33 additions & 0 deletions electron/services/mem0/secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
deleteProviderSecret,
getProviderSecret,
setProviderSecret,
} from '../secrets/secret-store';
import { MEM0_SECRET_ACCOUNT_ID } from '../../../shared/mem0';

export async function getMem0ApiKey(): Promise<string | null> {
const secret = await getProviderSecret(MEM0_SECRET_ACCOUNT_ID);
if (secret?.type === 'api_key') {
return secret.apiKey;
}
if (secret?.type === 'local') {
return secret.apiKey ?? null;
}
return null;
}

export async function hasMem0ApiKey(): Promise<boolean> {
return Boolean(await getMem0ApiKey());
}

export async function setMem0ApiKey(apiKey: string): Promise<void> {
await setProviderSecret({
type: 'api_key',
accountId: MEM0_SECRET_ACCOUNT_ID,
apiKey,
});
}

export async function deleteMem0ApiKey(): Promise<void> {
await deleteProviderSecret(MEM0_SECRET_ACCOUNT_ID);
}
Loading