botschat 0.1.20 → 0.1.21
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/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +186 -382
- package/packages/api/src/index.ts +50 -67
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/auth.ts +1 -0
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/demo.ts +156 -0
- package/packages/api/src/routes/sessions.ts +5 -5
- package/packages/api/src/routes/tasks.ts +33 -33
- package/packages/plugin/dist/src/channel.js +50 -0
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +18 -2
- package/packages/web/dist/assets/index-BtPyCBCl.css +1 -0
- package/packages/web/dist/assets/index-BtpsFe4Z.js +2 -0
- package/packages/web/dist/assets/index-CQbIYr6_.js +2 -0
- package/packages/web/dist/assets/{index-CYQMu_-c.js → index-C_GamcQc.js} +1 -1
- package/packages/web/dist/assets/index-LiBjPMg2.js +1 -0
- package/packages/web/dist/assets/{index-DYCO-ry1.js → index-MyoWvQAH.js} +1 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +1516 -0
- package/packages/web/dist/assets/{index.esm-CvOpngZM.js → index.esm-BpQAwtdR.js} +1 -1
- package/packages/web/dist/assets/{web-D3LMODYp.js → web-BbTzVNLt.js} +1 -1
- package/packages/web/dist/assets/{web-1cdhq2RW.js → web-cnzjgNfD.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +9 -56
- package/packages/web/src/api.ts +5 -61
- package/packages/web/src/components/ChatWindow.tsx +9 -9
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/ImageLightbox.tsx +96 -0
- package/packages/web/src/components/LoginPage.tsx +59 -1
- package/packages/web/src/components/MessageContent.tsx +17 -2
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +1 -3
- package/packages/web/src/hooks/useIMEComposition.ts +14 -9
- package/packages/web/src/store.ts +7 -39
- package/packages/web/src/ws.ts +0 -1
- package/scripts/dev.sh +0 -53
- package/migrations/0013_agents_table.sql +0 -29
- package/migrations/0014_agent_sessions.sql +0 -19
- package/migrations/0015_message_traces.sql +0 -27
- package/migrations/0016_multi_agent_channels_messages.sql +0 -9
- package/migrations/0017_rename_cron_job_id.sql +0 -2
- package/packages/api/src/protocol-v2.ts +0 -154
- package/packages/api/src/routes/agents-v2.ts +0 -192
- package/packages/api/src/routes/history-v2.ts +0 -221
- package/packages/api/src/routes/migrate-v2.ts +0 -110
- package/packages/web/dist/assets/index-BARPtt0v.css +0 -1
- package/packages/web/dist/assets/index-Bf-XL3te.js +0 -2
- package/packages/web/dist/assets/index-CYlvfpX9.js +0 -1519
- package/packages/web/dist/assets/index-CxcpA4Qo.js +0 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +0 -2
- package/packages/web/src/components/AgentSettings.tsx +0 -328
- package/scripts/mock-openclaw-v2.mjs +0 -486
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useEffect, useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
type LightboxState = { url: string; filename?: string } | null;
|
|
4
|
+
|
|
5
|
+
let _setLightbox: React.Dispatch<React.SetStateAction<LightboxState>> | null = null;
|
|
6
|
+
|
|
7
|
+
export function openImageLightbox(url: string, filename?: string) {
|
|
8
|
+
_setLightbox?.({ url, filename });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ImageLightbox() {
|
|
12
|
+
const [state, setState] = useState<LightboxState>(null);
|
|
13
|
+
_setLightbox = setState;
|
|
14
|
+
|
|
15
|
+
const close = useCallback(() => setState(null), []);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!state) return;
|
|
19
|
+
const onKey = (e: KeyboardEvent) => {
|
|
20
|
+
if (e.key === "Escape") close();
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("keydown", onKey);
|
|
23
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
24
|
+
}, [state, close]);
|
|
25
|
+
|
|
26
|
+
if (!state) return null;
|
|
27
|
+
|
|
28
|
+
const handleDownload = async () => {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(state.url);
|
|
31
|
+
const blob = await res.blob();
|
|
32
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
33
|
+
const a = document.createElement("a");
|
|
34
|
+
a.href = blobUrl;
|
|
35
|
+
a.download = state.filename || guessFilename(state.url);
|
|
36
|
+
document.body.appendChild(a);
|
|
37
|
+
a.click();
|
|
38
|
+
document.body.removeChild(a);
|
|
39
|
+
URL.revokeObjectURL(blobUrl);
|
|
40
|
+
} catch {
|
|
41
|
+
window.open(state.url, "_blank");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className="fixed inset-0 z-[100] flex items-center justify-center"
|
|
48
|
+
style={{ background: "rgba(0,0,0,0.85)" }}
|
|
49
|
+
onClick={close}
|
|
50
|
+
>
|
|
51
|
+
{/* Toolbar */}
|
|
52
|
+
<div
|
|
53
|
+
className="fixed top-0 right-0 flex items-center gap-1 p-3 z-[101]"
|
|
54
|
+
onClick={(e) => e.stopPropagation()}
|
|
55
|
+
>
|
|
56
|
+
<button
|
|
57
|
+
onClick={handleDownload}
|
|
58
|
+
className="p-2 rounded-full hover:bg-white/15 transition-colors"
|
|
59
|
+
title="Download"
|
|
60
|
+
>
|
|
61
|
+
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
62
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
<button
|
|
66
|
+
onClick={close}
|
|
67
|
+
className="p-2 rounded-full hover:bg-white/15 transition-colors"
|
|
68
|
+
title="Close"
|
|
69
|
+
>
|
|
70
|
+
<svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
71
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Image */}
|
|
77
|
+
<img
|
|
78
|
+
src={state.url}
|
|
79
|
+
alt=""
|
|
80
|
+
className="max-w-[90vw] max-h-[90vh] object-contain rounded select-none"
|
|
81
|
+
style={{ boxShadow: "0 0 40px rgba(0,0,0,0.5)" }}
|
|
82
|
+
onClick={(e) => e.stopPropagation()}
|
|
83
|
+
draggable={false}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function guessFilename(url: string): string {
|
|
90
|
+
try {
|
|
91
|
+
const path = new URL(url, window.location.href).pathname;
|
|
92
|
+
const name = path.split("/").pop();
|
|
93
|
+
if (name && name.includes(".")) return name;
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
return `image-${Date.now()}.png`;
|
|
96
|
+
}
|
|
@@ -5,6 +5,14 @@ import { useAppDispatch } from "../store";
|
|
|
5
5
|
import { dlog } from "../debug-log";
|
|
6
6
|
import { isFirebaseConfigured, signInWithGoogle, signInWithGitHub, signInWithApple } from "../firebase";
|
|
7
7
|
|
|
8
|
+
function PlayIcon() {
|
|
9
|
+
return (
|
|
10
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}>
|
|
11
|
+
<path d="M8 5v14l11-7z"/>
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
/** Google "G" logo SVG */
|
|
9
17
|
function GoogleIcon() {
|
|
10
18
|
return (
|
|
@@ -44,10 +52,11 @@ export function LoginPage() {
|
|
|
44
52
|
const [error, setError] = useState("");
|
|
45
53
|
const [loading, setLoading] = useState(false);
|
|
46
54
|
const [oauthLoading, setOauthLoading] = useState<"google" | "github" | "apple" | null>(null);
|
|
55
|
+
const [demoLoading, setDemoLoading] = useState(false);
|
|
47
56
|
const [authConfig, setAuthConfig] = useState<AuthConfig | null>(null);
|
|
48
57
|
|
|
49
58
|
const firebaseEnabled = isFirebaseConfigured();
|
|
50
|
-
const anyLoading = loading || !!oauthLoading;
|
|
59
|
+
const anyLoading = loading || !!oauthLoading || demoLoading;
|
|
51
60
|
|
|
52
61
|
// Fetch server-side auth config to determine which methods are available
|
|
53
62
|
useEffect(() => {
|
|
@@ -133,6 +142,23 @@ export function LoginPage() {
|
|
|
133
142
|
}
|
|
134
143
|
};
|
|
135
144
|
|
|
145
|
+
const handleDemoLogin = async () => {
|
|
146
|
+
setError("");
|
|
147
|
+
setDemoLoading(true);
|
|
148
|
+
try {
|
|
149
|
+
dlog.info("Auth", "Starting demo login");
|
|
150
|
+
const res = await authApi.demo();
|
|
151
|
+
dlog.info("Auth", `Demo login success — user ${res.id}`);
|
|
152
|
+
handleAuthSuccess(res);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : "Demo login failed";
|
|
155
|
+
dlog.error("Auth", `Demo login failed: ${message}`);
|
|
156
|
+
setError(message);
|
|
157
|
+
} finally {
|
|
158
|
+
setDemoLoading(false);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
136
162
|
return (
|
|
137
163
|
<div
|
|
138
164
|
className="h-screen overflow-y-auto"
|
|
@@ -267,6 +293,38 @@ export function LoginPage() {
|
|
|
267
293
|
</>
|
|
268
294
|
)}
|
|
269
295
|
|
|
296
|
+
{/* Demo mode */}
|
|
297
|
+
{configLoaded && (
|
|
298
|
+
<>
|
|
299
|
+
<div className="flex items-center gap-3 my-5">
|
|
300
|
+
<div className="flex-1 h-px" style={{ background: "var(--border)" }} />
|
|
301
|
+
<span className="text-caption" style={{ color: "var(--text-muted)" }}>
|
|
302
|
+
or
|
|
303
|
+
</span>
|
|
304
|
+
<div className="flex-1 h-px" style={{ background: "var(--border)" }} />
|
|
305
|
+
</div>
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
onClick={handleDemoLogin}
|
|
309
|
+
disabled={anyLoading}
|
|
310
|
+
className="w-full flex items-center justify-center gap-3 py-2.5 px-4 font-medium text-body rounded-sm disabled:opacity-50 transition-colors hover:brightness-95"
|
|
311
|
+
style={{
|
|
312
|
+
background: "var(--bg-active)",
|
|
313
|
+
color: "#fff",
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{demoLoading ? (
|
|
317
|
+
<span>Loading demo...</span>
|
|
318
|
+
) : (
|
|
319
|
+
<>
|
|
320
|
+
<PlayIcon />
|
|
321
|
+
<span>Try Demo</span>
|
|
322
|
+
</>
|
|
323
|
+
)}
|
|
324
|
+
</button>
|
|
325
|
+
</>
|
|
326
|
+
)}
|
|
327
|
+
|
|
270
328
|
{/* Error display (always visible, e.g. OAuth errors) */}
|
|
271
329
|
{error && (
|
|
272
330
|
<div
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useCallback, useMemo, useEffect } from "react";
|
|
2
2
|
import { E2eService } from "../e2e";
|
|
3
|
+
import { openImageLightbox } from "./ImageLightbox";
|
|
3
4
|
import ReactMarkdown from "react-markdown";
|
|
4
5
|
import remarkGfm from "remark-gfm";
|
|
5
6
|
import rehypeHighlight from "rehype-highlight";
|
|
@@ -441,8 +442,9 @@ function renderA2UIComponent(
|
|
|
441
442
|
key={id}
|
|
442
443
|
src={url}
|
|
443
444
|
alt={alt}
|
|
444
|
-
className="max-w-[360px] max-h-64 rounded-md object-contain my-1"
|
|
445
|
+
className="max-w-[360px] max-h-64 rounded-md object-contain my-1 cursor-pointer hover:opacity-90 transition-opacity"
|
|
445
446
|
style={{ border: "1px solid var(--border)" }}
|
|
447
|
+
onClick={() => openImageLightbox(url)}
|
|
446
448
|
/>
|
|
447
449
|
);
|
|
448
450
|
}
|
|
@@ -591,6 +593,19 @@ function buildMarkdownComponents(
|
|
|
591
593
|
</blockquote>
|
|
592
594
|
);
|
|
593
595
|
},
|
|
596
|
+
// Images — open in lightbox
|
|
597
|
+
img({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) {
|
|
598
|
+
return (
|
|
599
|
+
<img
|
|
600
|
+
src={src}
|
|
601
|
+
alt={alt ?? ""}
|
|
602
|
+
className="max-w-[360px] max-h-64 rounded-md object-contain my-1 cursor-pointer hover:opacity-90 transition-opacity"
|
|
603
|
+
style={{ border: "1px solid var(--border)" }}
|
|
604
|
+
onClick={() => src && openImageLightbox(src)}
|
|
605
|
+
{...props}
|
|
606
|
+
/>
|
|
607
|
+
);
|
|
608
|
+
},
|
|
594
609
|
};
|
|
595
610
|
}
|
|
596
611
|
|
|
@@ -1138,7 +1153,7 @@ function MediaPreview({ url, mediaContextId }: { url: string; mediaContextId?: s
|
|
|
1138
1153
|
alt=""
|
|
1139
1154
|
className="max-w-[360px] max-h-64 rounded-md object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
|
1140
1155
|
style={{ border: "1px solid var(--border)" }}
|
|
1141
|
-
onClick={() =>
|
|
1156
|
+
onClick={() => openImageLightbox(effectiveUrl)}
|
|
1142
1157
|
/>
|
|
1143
1158
|
);
|
|
1144
1159
|
}
|
|
@@ -143,7 +143,7 @@ export function SessionTabs({ channelId }: SessionTabsProps) {
|
|
|
143
143
|
// Auto-create a "General" channel for the default agent (no channelId yet)
|
|
144
144
|
if (!effectiveChannelId) {
|
|
145
145
|
dlog.info("Session", "No channel for default agent — auto-creating General channel");
|
|
146
|
-
const channel = await channelsApi.create({ name: "General",
|
|
146
|
+
const channel = await channelsApi.create({ name: "General", openclawAgentId: "main" });
|
|
147
147
|
effectiveChannelId = channel.id;
|
|
148
148
|
// Reload agents and channels so the default agent picks up the new channelId
|
|
149
149
|
const [{ agents }, { channels: chs }] = await Promise.all([
|
|
@@ -128,9 +128,7 @@ export function Sidebar({ onOpenSettings, onNavigate }: { onOpenSettings?: () =>
|
|
|
128
128
|
style={{ background: state.openclawConnected ? "var(--accent-green)" : "var(--accent-red)" }}
|
|
129
129
|
/>
|
|
130
130
|
<span className="text-tiny text-[--text-muted]">
|
|
131
|
-
{state.openclawConnected
|
|
132
|
-
? `${Object.values(state.agentConnections).filter(Boolean).length || 1} agent(s) connected`
|
|
133
|
-
: "No agents connected"}
|
|
131
|
+
{state.openclawConnected ? "OpenClaw connected" : "OpenClaw offline"}
|
|
134
132
|
</span>
|
|
135
133
|
</div>
|
|
136
134
|
</div>
|
|
@@ -7,28 +7,33 @@ import { useRef, useCallback } from "react";
|
|
|
7
7
|
* In WebKit/WKWebView, when a user presses Enter to accept an IME candidate,
|
|
8
8
|
* the event order is: compositionend → keydown(Enter, isComposing=false).
|
|
9
9
|
* The native `isComposing` flag is already false by the time keydown fires,
|
|
10
|
-
* so checking it alone is insufficient.
|
|
11
|
-
*
|
|
10
|
+
* so checking it alone is insufficient.
|
|
11
|
+
*
|
|
12
|
+
* We use a timestamp-based guard instead of requestAnimationFrame because
|
|
13
|
+
* rAF timing is unreliable in WKWebView — the callback may fire before the
|
|
14
|
+
* trailing keydown, causing a race condition where Enter is sometimes
|
|
15
|
+
* swallowed and sometimes not.
|
|
12
16
|
*/
|
|
17
|
+
const COMPOSITION_END_GUARD_MS = 80;
|
|
18
|
+
|
|
13
19
|
export function useIMEComposition() {
|
|
14
20
|
const composingRef = useRef(false);
|
|
15
|
-
const
|
|
21
|
+
const compositionEndTimeRef = useRef(0);
|
|
16
22
|
|
|
17
23
|
const onCompositionStart = useCallback(() => {
|
|
18
24
|
composingRef.current = true;
|
|
19
|
-
|
|
25
|
+
compositionEndTimeRef.current = 0;
|
|
20
26
|
}, []);
|
|
21
27
|
|
|
22
28
|
const onCompositionEnd = useCallback(() => {
|
|
23
29
|
composingRef.current = false;
|
|
24
|
-
|
|
25
|
-
requestAnimationFrame(() => {
|
|
26
|
-
justEndedRef.current = false;
|
|
27
|
-
});
|
|
30
|
+
compositionEndTimeRef.current = Date.now();
|
|
28
31
|
}, []);
|
|
29
32
|
|
|
30
33
|
const isIMEActive = useCallback(
|
|
31
|
-
() =>
|
|
34
|
+
() =>
|
|
35
|
+
composingRef.current ||
|
|
36
|
+
Date.now() - compositionEndTimeRef.current < COMPOSITION_END_GUARD_MS,
|
|
32
37
|
[],
|
|
33
38
|
);
|
|
34
39
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Minimal reactive store using React context + useState. */
|
|
2
2
|
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
|
-
import type { Agent as ApiAgent,
|
|
4
|
+
import type { Agent as ApiAgent, Channel, Task, TaskWithChannel, Job, ModelInfo, Session } from "./api";
|
|
5
5
|
|
|
6
6
|
export type ChatMessage = {
|
|
7
7
|
id: string;
|
|
@@ -19,9 +19,6 @@ export type ChatMessage = {
|
|
|
19
19
|
encrypted?: boolean | number;
|
|
20
20
|
/** Whether the media binary was E2E encrypted (derived from sender + encrypted flag) */
|
|
21
21
|
mediaEncrypted?: boolean;
|
|
22
|
-
senderAgentId?: string;
|
|
23
|
-
senderAgentName?: string;
|
|
24
|
-
targetAgentId?: string;
|
|
25
22
|
activities?: ActivityItem[];
|
|
26
23
|
};
|
|
27
24
|
|
|
@@ -54,10 +51,6 @@ export type AppState = {
|
|
|
54
51
|
activeThreadId: string | null;
|
|
55
52
|
threadReplyCounts: Record<string, number>;
|
|
56
53
|
openclawConnected: boolean;
|
|
57
|
-
/** Per-agent connection status (v2 multi-agent). */
|
|
58
|
-
agentConnections: Record<string, boolean>;
|
|
59
|
-
/** v2 agent list with detailed info (type, role, skills, capabilities). */
|
|
60
|
-
v2Agents: AgentV2[];
|
|
61
54
|
/** Per-session model override (set via /model or dropdown). null = using defaultModel. */
|
|
62
55
|
sessionModel: string | null;
|
|
63
56
|
wsConnected: boolean;
|
|
@@ -94,8 +87,6 @@ export const initialState: AppState = {
|
|
|
94
87
|
activeThreadId: null,
|
|
95
88
|
threadReplyCounts: {},
|
|
96
89
|
openclawConnected: false,
|
|
97
|
-
agentConnections: {},
|
|
98
|
-
v2Agents: [],
|
|
99
90
|
sessionModel: null,
|
|
100
91
|
wsConnected: false,
|
|
101
92
|
models: [],
|
|
@@ -150,8 +141,6 @@ export type AppAction =
|
|
|
150
141
|
| { type: "ADD_CRON_JOB"; job: Job }
|
|
151
142
|
| { type: "UPDATE_CRON_JOB"; job: Job }
|
|
152
143
|
| { type: "APPEND_JOB_OUTPUT"; jobId: string; text: string }
|
|
153
|
-
| { type: "SET_V2_AGENTS"; agents: AgentV2[] }
|
|
154
|
-
| { type: "SET_AGENT_CONNECTION"; agentId: string; connected: boolean }
|
|
155
144
|
| { type: "LOGOUT" };
|
|
156
145
|
|
|
157
146
|
export function appReducer(state: AppState, action: AppAction): AppState {
|
|
@@ -335,14 +324,14 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
335
324
|
threadReplyCounts: updatedCounts,
|
|
336
325
|
};
|
|
337
326
|
}
|
|
338
|
-
case "SET_OPENCLAW_CONNECTED":
|
|
339
|
-
|
|
327
|
+
case "SET_OPENCLAW_CONNECTED":
|
|
328
|
+
// connection.status carries the gateway defaultModel (OpenClaw primary).
|
|
329
|
+
// Prefer user's saved default (from API/settings); only use gateway default when user has not set one.
|
|
340
330
|
return {
|
|
341
331
|
...state,
|
|
342
|
-
openclawConnected:
|
|
332
|
+
openclawConnected: action.connected,
|
|
343
333
|
defaultModel: state.defaultModel ?? action.defaultModel ?? null,
|
|
344
334
|
};
|
|
345
|
-
}
|
|
346
335
|
case "SET_SESSION_MODEL":
|
|
347
336
|
return { ...state, sessionModel: action.model };
|
|
348
337
|
case "SET_WS_CONNECTED":
|
|
@@ -445,7 +434,7 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
445
434
|
// These fields belong to OpenClaw and are NOT stored in D1.
|
|
446
435
|
const scanMap = new Map(action.scanTasks.map((s) => [s.cronJobId, s]));
|
|
447
436
|
const merged = state.cronTasks.map((task) => {
|
|
448
|
-
const scan = task.
|
|
437
|
+
const scan = task.openclawCronJobId ? scanMap.get(task.openclawCronJobId) : null;
|
|
449
438
|
if (!scan) return task;
|
|
450
439
|
return {
|
|
451
440
|
...task,
|
|
@@ -457,7 +446,7 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
457
446
|
});
|
|
458
447
|
// Also merge into the messages-view tasks list
|
|
459
448
|
const mergedTasks = state.tasks.map((task) => {
|
|
460
|
-
const scan = task.
|
|
449
|
+
const scan = task.openclawCronJobId ? scanMap.get(task.openclawCronJobId) : null;
|
|
461
450
|
if (!scan) return task;
|
|
462
451
|
return {
|
|
463
452
|
...task,
|
|
@@ -539,27 +528,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
539
528
|
cronJobs: updateSummary(state.cronJobs),
|
|
540
529
|
};
|
|
541
530
|
}
|
|
542
|
-
case "SET_V2_AGENTS": {
|
|
543
|
-
const connections = Object.fromEntries(
|
|
544
|
-
action.agents.map((a) => [a.id, a.status === "connected"]),
|
|
545
|
-
);
|
|
546
|
-
const anyV2Connected = Object.values(connections).some(Boolean);
|
|
547
|
-
return {
|
|
548
|
-
...state,
|
|
549
|
-
v2Agents: action.agents,
|
|
550
|
-
agentConnections: connections,
|
|
551
|
-
openclawConnected: anyV2Connected || state.openclawConnected,
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
case "SET_AGENT_CONNECTION": {
|
|
555
|
-
const newConnections = { ...state.agentConnections, [action.agentId]: action.connected };
|
|
556
|
-
const anyConnected = Object.values(newConnections).some(Boolean);
|
|
557
|
-
return {
|
|
558
|
-
...state,
|
|
559
|
-
agentConnections: newConnections,
|
|
560
|
-
openclawConnected: anyConnected || state.openclawConnected,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
531
|
case "LOGOUT":
|
|
564
532
|
return { ...initialState };
|
|
565
533
|
default:
|
package/packages/web/src/ws.ts
CHANGED
package/scripts/dev.sh
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
# ./scripts/dev.sh sync — sync plugin to mini.local + rebuild + restart gateway
|
|
10
10
|
# ./scripts/dev.sh logs — tail gateway logs on mini.local
|
|
11
11
|
# ./scripts/dev.sh mock — start mock OpenClaw standalone (foreground)
|
|
12
|
-
# ./scripts/dev.sh v2 — v2 env: build + migrate (v2 DB) + start on port 8788
|
|
13
|
-
# ./scripts/dev.sh v2:reset — nuke v2 local DB, re-migrate, then start
|
|
14
|
-
# ./scripts/dev.sh v2:deploy — deploy v2 to Cloudflare
|
|
15
12
|
set -euo pipefail
|
|
16
13
|
|
|
17
14
|
cd "$(dirname "$0")/.."
|
|
@@ -217,40 +214,6 @@ do_logs() {
|
|
|
217
214
|
ssh mini.local 'tail -f ~/.openclaw/logs/gateway.log'
|
|
218
215
|
}
|
|
219
216
|
|
|
220
|
-
# ── v2 environment ──────────────────────────────────────────────────
|
|
221
|
-
|
|
222
|
-
V2_CONFIG="wrangler-v2.toml"
|
|
223
|
-
V2_DB="botschat-v2-db"
|
|
224
|
-
V2_PORT=8788
|
|
225
|
-
|
|
226
|
-
do_v2_migrate() {
|
|
227
|
-
info "Applying D1 migrations (v2 local)…"
|
|
228
|
-
npx wrangler d1 migrations apply "$V2_DB" --local --config "$V2_CONFIG"
|
|
229
|
-
ok "v2 migrations applied"
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
do_v2_reset() {
|
|
233
|
-
warn "Nuking v2 local state (DB + DO)…"
|
|
234
|
-
rm -rf "$ROOT/.wrangler/state"
|
|
235
|
-
ok "v2 local state wiped"
|
|
236
|
-
do_v2_migrate
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
do_v2_start() {
|
|
240
|
-
kill_port "$V2_PORT"
|
|
241
|
-
info "Starting wrangler dev (v2) on 0.0.0.0:${V2_PORT}…"
|
|
242
|
-
exec npx wrangler dev --config "$V2_CONFIG" --ip 0.0.0.0 --port "$V2_PORT" \
|
|
243
|
-
--var ENVIRONMENT:development --var DEV_AUTH_SECRET:"$DEV_AUTH_SECRET"
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
do_v2_deploy() {
|
|
247
|
-
info "Building web frontend…"
|
|
248
|
-
npm run build -w packages/web
|
|
249
|
-
info "Deploying v2 to Cloudflare…"
|
|
250
|
-
npx wrangler deploy --config "$V2_CONFIG"
|
|
251
|
-
ok "v2 deployed"
|
|
252
|
-
}
|
|
253
|
-
|
|
254
217
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
255
218
|
|
|
256
219
|
cmd="${1:-}"
|
|
@@ -282,22 +245,6 @@ case "$cmd" in
|
|
|
282
245
|
shift
|
|
283
246
|
do_mock "$@"
|
|
284
247
|
;;
|
|
285
|
-
v2)
|
|
286
|
-
do_build_web
|
|
287
|
-
do_v2_migrate
|
|
288
|
-
do_v2_start
|
|
289
|
-
;;
|
|
290
|
-
v2:reset)
|
|
291
|
-
do_v2_reset
|
|
292
|
-
do_build_web
|
|
293
|
-
do_v2_start
|
|
294
|
-
;;
|
|
295
|
-
v2:migrate)
|
|
296
|
-
do_v2_migrate
|
|
297
|
-
;;
|
|
298
|
-
v2:deploy)
|
|
299
|
-
do_v2_deploy
|
|
300
|
-
;;
|
|
301
248
|
*)
|
|
302
249
|
# Default: full dev experience — build + migrate + server + mock + browser
|
|
303
250
|
do_build_web
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
-- Multi-Agent Architecture: agents table
|
|
2
|
-
-- Agent = Type (engine) x Role (persona), independent of channels.
|
|
3
|
-
|
|
4
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
5
|
-
id TEXT PRIMARY KEY, -- 'agt_xxx'
|
|
6
|
-
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
7
|
-
name TEXT NOT NULL, -- display name: "OpenClaw", "Cursor", "PM小明"
|
|
8
|
-
type TEXT NOT NULL CHECK (type IN ('openclaw', 'cursor_cli', 'cursor_cloud', 'claude_code', 'mock')),
|
|
9
|
-
-- Role & Skills
|
|
10
|
-
role TEXT NOT NULL DEFAULT 'general', -- 'product_manager', 'developer', 'qa', 'devops', 'general'
|
|
11
|
-
system_prompt TEXT NOT NULL DEFAULT '',
|
|
12
|
-
skills_json TEXT NOT NULL DEFAULT '[]', -- JSON: [{"name":"...","description":"..."}]
|
|
13
|
-
-- Connection credentials (provider-specific, at most one populated)
|
|
14
|
-
pairing_token TEXT, -- OpenClaw bc_pat_xxx
|
|
15
|
-
api_key TEXT, -- Cursor / Claude API key
|
|
16
|
-
config_json TEXT NOT NULL DEFAULT '{}', -- extra: { workspace, repository, ref, ... }
|
|
17
|
-
-- Technical capabilities (derived from type)
|
|
18
|
-
capabilities TEXT NOT NULL DEFAULT '["chat"]', -- JSON: ["chat","streaming","cron","a2ui","media","code_edit","delegate"]
|
|
19
|
-
-- Runtime state
|
|
20
|
-
status TEXT NOT NULL DEFAULT 'disconnected' CHECK (status IN ('connected', 'disconnected')),
|
|
21
|
-
last_connected_at INTEGER,
|
|
22
|
-
connection_count INTEGER NOT NULL DEFAULT 0,
|
|
23
|
-
last_ip TEXT,
|
|
24
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
25
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
26
|
-
);
|
|
27
|
-
CREATE INDEX IF NOT EXISTS idx_agents_user ON agents(user_id);
|
|
28
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_pairing_token ON agents(pairing_token) WHERE pairing_token IS NOT NULL;
|
|
29
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_api_key ON agents(api_key) WHERE api_key IS NOT NULL;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
-- Multi-Agent Architecture: agent_sessions mapping table
|
|
2
|
-
-- Maps BotsChat sessions to each agent's provider-side session identifier.
|
|
3
|
-
-- e.g. BotsChat ses_001 x Cursor agent -> Cursor chatId "abc-123"
|
|
4
|
-
|
|
5
|
-
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
6
|
-
id TEXT PRIMARY KEY, -- 'as_xxx'
|
|
7
|
-
session_id TEXT NOT NULL, -- BotsChat session (ses_xxx)
|
|
8
|
-
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
9
|
-
provider_session_id TEXT, -- provider-specific session ID
|
|
10
|
-
-- OpenClaw: session_key "agent:main:botschat:u_xxx:ses:ses_xxx"
|
|
11
|
-
-- Cursor CLI: chatId (uuid from `agent create-chat`)
|
|
12
|
-
-- Cursor Cloud: cloud agent id "bc_xxx"
|
|
13
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
14
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
15
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
16
|
-
UNIQUE(session_id, agent_id)
|
|
17
|
-
);
|
|
18
|
-
CREATE INDEX IF NOT EXISTS idx_agent_sessions_session ON agent_sessions(session_id);
|
|
19
|
-
CREATE INDEX IF NOT EXISTS idx_agent_sessions_agent ON agent_sessions(agent_id);
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
-- Multi-Agent Architecture: message_traces table
|
|
2
|
-
-- Stores agent execution traces at different verbosity levels.
|
|
3
|
-
-- lv1 = conclusions (stored in messages table)
|
|
4
|
-
-- lv2 = thinking / reasoning process
|
|
5
|
-
-- lv3 = reference material / tool call results
|
|
6
|
-
|
|
7
|
-
CREATE TABLE IF NOT EXISTS message_traces (
|
|
8
|
-
id TEXT PRIMARY KEY, -- 'mt_xxx'
|
|
9
|
-
message_id TEXT NOT NULL, -- parent lv1 message (messages.id)
|
|
10
|
-
user_id TEXT NOT NULL,
|
|
11
|
-
session_key TEXT NOT NULL,
|
|
12
|
-
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
13
|
-
verbose_level INTEGER NOT NULL CHECK (verbose_level IN (2, 3)),
|
|
14
|
-
trace_type TEXT NOT NULL,
|
|
15
|
-
-- lv2: 'thinking', 'planning', 'reasoning', 'decision'
|
|
16
|
-
-- lv3: 'file_read', 'file_write', 'command_exec', 'search_result', 'tool_call', 'reference'
|
|
17
|
-
content BLOB NOT NULL,
|
|
18
|
-
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
19
|
-
-- lv3 file_read: { "path": "src/auth.ts", "lines": 42 }
|
|
20
|
-
-- lv3 command_exec: { "command": "npm test", "exitCode": 0 }
|
|
21
|
-
-- lv3 file_write: { "path": "src/auth.ts", "linesChanged": 5 }
|
|
22
|
-
encrypted INTEGER NOT NULL DEFAULT 0,
|
|
23
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
24
|
-
);
|
|
25
|
-
CREATE INDEX IF NOT EXISTS idx_traces_message ON message_traces(message_id);
|
|
26
|
-
CREATE INDEX IF NOT EXISTS idx_traces_session_level ON message_traces(session_key, verbose_level, created_at);
|
|
27
|
-
CREATE INDEX IF NOT EXISTS idx_traces_agent ON message_traces(agent_id, created_at);
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
-- Multi-Agent Architecture: extend channels and messages for multi-agent support.
|
|
2
|
-
|
|
3
|
-
-- Channels: add default agent + rename openclaw-specific column
|
|
4
|
-
ALTER TABLE channels ADD COLUMN default_agent_id TEXT REFERENCES agents(id);
|
|
5
|
-
ALTER TABLE channels RENAME COLUMN openclaw_agent_id TO provider_agent_id;
|
|
6
|
-
|
|
7
|
-
-- Messages: track which agent sent/received each message
|
|
8
|
-
ALTER TABLE messages ADD COLUMN sender_agent_id TEXT REFERENCES agents(id);
|
|
9
|
-
ALTER TABLE messages ADD COLUMN target_agent_id TEXT REFERENCES agents(id);
|