botschat 0.1.0
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/LICENSE +201 -0
- package/README.md +213 -0
- package/migrations/0001_initial.sql +88 -0
- package/migrations/0002_rename_projects_to_channels.sql +53 -0
- package/migrations/0003_messages.sql +14 -0
- package/migrations/0004_jobs.sql +15 -0
- package/migrations/0005_deleted_cron_jobs.sql +6 -0
- package/migrations/0006_tasks_add_model.sql +2 -0
- package/migrations/0007_sessions.sql +25 -0
- package/migrations/0008_remove_openclaw_fields.sql +8 -0
- package/package.json +53 -0
- package/packages/api/package.json +17 -0
- package/packages/api/src/do/connection-do.ts +929 -0
- package/packages/api/src/env.ts +8 -0
- package/packages/api/src/index.ts +297 -0
- package/packages/api/src/routes/agents.ts +68 -0
- package/packages/api/src/routes/auth.ts +105 -0
- package/packages/api/src/routes/channels.ts +185 -0
- package/packages/api/src/routes/jobs.ts +65 -0
- package/packages/api/src/routes/models.ts +22 -0
- package/packages/api/src/routes/pairing.ts +76 -0
- package/packages/api/src/routes/projects.ts +177 -0
- package/packages/api/src/routes/sessions.ts +171 -0
- package/packages/api/src/routes/tasks.ts +375 -0
- package/packages/api/src/routes/upload.ts +52 -0
- package/packages/api/src/utils/auth.ts +101 -0
- package/packages/api/src/utils/id.ts +19 -0
- package/packages/api/tsconfig.json +18 -0
- package/packages/plugin/dist/index.d.ts +19 -0
- package/packages/plugin/dist/index.d.ts.map +1 -0
- package/packages/plugin/dist/index.js +17 -0
- package/packages/plugin/dist/index.js.map +1 -0
- package/packages/plugin/dist/src/accounts.d.ts +12 -0
- package/packages/plugin/dist/src/accounts.d.ts.map +1 -0
- package/packages/plugin/dist/src/accounts.js +103 -0
- package/packages/plugin/dist/src/accounts.js.map +1 -0
- package/packages/plugin/dist/src/channel.d.ts +206 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -0
- package/packages/plugin/dist/src/channel.js +1248 -0
- package/packages/plugin/dist/src/channel.js.map +1 -0
- package/packages/plugin/dist/src/runtime.d.ts +3 -0
- package/packages/plugin/dist/src/runtime.d.ts.map +1 -0
- package/packages/plugin/dist/src/runtime.js +18 -0
- package/packages/plugin/dist/src/runtime.js.map +1 -0
- package/packages/plugin/dist/src/types.d.ts +179 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -0
- package/packages/plugin/dist/src/types.js +6 -0
- package/packages/plugin/dist/src/types.js.map +1 -0
- package/packages/plugin/dist/src/ws-client.d.ts +51 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -0
- package/packages/plugin/dist/src/ws-client.js +170 -0
- package/packages/plugin/dist/src/ws-client.js.map +1 -0
- package/packages/plugin/openclaw.plugin.json +11 -0
- package/packages/plugin/package.json +39 -0
- package/packages/plugin/tsconfig.json +20 -0
- package/packages/web/dist/assets/index-C-wI8eHy.css +1 -0
- package/packages/web/dist/assets/index-CbPEKHLG.js +93 -0
- package/packages/web/dist/index.html +17 -0
- package/packages/web/index.html +16 -0
- package/packages/web/package.json +29 -0
- package/packages/web/postcss.config.js +6 -0
- package/packages/web/src/App.tsx +827 -0
- package/packages/web/src/api.ts +242 -0
- package/packages/web/src/components/ChatWindow.tsx +864 -0
- package/packages/web/src/components/CronDetail.tsx +943 -0
- package/packages/web/src/components/CronSidebar.tsx +123 -0
- package/packages/web/src/components/DebugLogPanel.tsx +258 -0
- package/packages/web/src/components/IconRail.tsx +163 -0
- package/packages/web/src/components/JobList.tsx +120 -0
- package/packages/web/src/components/LoginPage.tsx +178 -0
- package/packages/web/src/components/MessageContent.tsx +1082 -0
- package/packages/web/src/components/ModelSelect.tsx +87 -0
- package/packages/web/src/components/ScheduleEditor.tsx +403 -0
- package/packages/web/src/components/SessionTabs.tsx +246 -0
- package/packages/web/src/components/Sidebar.tsx +331 -0
- package/packages/web/src/components/TaskBar.tsx +413 -0
- package/packages/web/src/components/ThreadPanel.tsx +212 -0
- package/packages/web/src/debug-log.ts +58 -0
- package/packages/web/src/index.css +170 -0
- package/packages/web/src/main.tsx +10 -0
- package/packages/web/src/store.ts +492 -0
- package/packages/web/src/ws.ts +99 -0
- package/packages/web/tailwind.config.js +65 -0
- package/packages/web/tsconfig.json +18 -0
- package/packages/web/vite.config.ts +20 -0
- package/scripts/dev.sh +122 -0
- package/tsconfig.json +18 -0
- package/wrangler.toml +40 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Jobs API — background task execution history.
|
|
6
|
+
* Mounted at /api/channels/:channelId/tasks/:taskId/jobs
|
|
7
|
+
*/
|
|
8
|
+
const jobs = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
9
|
+
|
|
10
|
+
/** GET / — list jobs for a background task */
|
|
11
|
+
jobs.get("/", async (c) => {
|
|
12
|
+
const userId = c.get("userId");
|
|
13
|
+
const channelId = c.req.param("channelId");
|
|
14
|
+
const taskId = c.req.param("taskId");
|
|
15
|
+
const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
|
|
16
|
+
|
|
17
|
+
// Verify ownership chain
|
|
18
|
+
const channel = await c.env.DB.prepare(
|
|
19
|
+
"SELECT id FROM channels WHERE id = ? AND user_id = ?",
|
|
20
|
+
)
|
|
21
|
+
.bind(channelId, userId)
|
|
22
|
+
.first();
|
|
23
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
24
|
+
|
|
25
|
+
const task = await c.env.DB.prepare(
|
|
26
|
+
"SELECT id, kind FROM tasks WHERE id = ? AND channel_id = ?",
|
|
27
|
+
)
|
|
28
|
+
.bind(taskId, channelId)
|
|
29
|
+
.first<{ id: string; kind: string }>();
|
|
30
|
+
if (!task) return c.json({ error: "Task not found" }, 404);
|
|
31
|
+
if (task.kind !== "background") return c.json({ error: "Only background tasks have jobs" }, 400);
|
|
32
|
+
|
|
33
|
+
const { results } = await c.env.DB.prepare(
|
|
34
|
+
`SELECT id, session_key, status, started_at, finished_at, duration_ms, summary, created_at
|
|
35
|
+
FROM jobs WHERE task_id = ? AND user_id = ?
|
|
36
|
+
ORDER BY started_at DESC LIMIT ?`,
|
|
37
|
+
)
|
|
38
|
+
.bind(taskId, userId, limit)
|
|
39
|
+
.all<{
|
|
40
|
+
id: string;
|
|
41
|
+
session_key: string;
|
|
42
|
+
status: string;
|
|
43
|
+
started_at: number;
|
|
44
|
+
finished_at: number | null;
|
|
45
|
+
duration_ms: number | null;
|
|
46
|
+
summary: string;
|
|
47
|
+
created_at: number;
|
|
48
|
+
}>();
|
|
49
|
+
|
|
50
|
+
return c.json({
|
|
51
|
+
jobs: (results ?? []).map((r, idx, arr) => ({
|
|
52
|
+
id: r.id,
|
|
53
|
+
number: arr.length - idx,
|
|
54
|
+
sessionKey: r.session_key,
|
|
55
|
+
status: r.status,
|
|
56
|
+
startedAt: r.started_at,
|
|
57
|
+
finishedAt: r.finished_at,
|
|
58
|
+
durationMs: r.duration_ms,
|
|
59
|
+
summary: r.summary,
|
|
60
|
+
time: new Date(r.started_at * 1000).toLocaleString(),
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export { jobs };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
export type ModelInfo = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const models = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
11
|
+
|
|
12
|
+
/** GET /api/models — list available models (fetched from OpenClaw plugin via DO) */
|
|
13
|
+
models.get("/", async (c) => {
|
|
14
|
+
const userId = c.get("userId");
|
|
15
|
+
const doId = c.env.CONNECTION_DO.idFromName(userId);
|
|
16
|
+
const stub = c.env.CONNECTION_DO.get(doId);
|
|
17
|
+
const res = await stub.fetch(new Request("https://internal/models"));
|
|
18
|
+
const data = await res.json<{ models: ModelInfo[] }>();
|
|
19
|
+
return c.json(data);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export { models };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId, generatePairingToken } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
const pairing = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
6
|
+
|
|
7
|
+
/** GET /api/pairing-tokens — list pairing tokens for the current user */
|
|
8
|
+
pairing.get("/", async (c) => {
|
|
9
|
+
const userId = c.get("userId");
|
|
10
|
+
|
|
11
|
+
const { results } = await c.env.DB.prepare(
|
|
12
|
+
"SELECT id, token, label, last_connected_at, created_at FROM pairing_tokens WHERE user_id = ? ORDER BY created_at DESC",
|
|
13
|
+
)
|
|
14
|
+
.bind(userId)
|
|
15
|
+
.all<{
|
|
16
|
+
id: string;
|
|
17
|
+
token: string;
|
|
18
|
+
label: string | null;
|
|
19
|
+
last_connected_at: number | null;
|
|
20
|
+
created_at: number;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
return c.json({
|
|
24
|
+
tokens: (results ?? []).map((r) => ({
|
|
25
|
+
id: r.id,
|
|
26
|
+
// Show only last 8 chars for security
|
|
27
|
+
token: r.token,
|
|
28
|
+
tokenPreview: `bc_pat_...${r.token.slice(-8)}`,
|
|
29
|
+
label: r.label,
|
|
30
|
+
lastConnectedAt: r.last_connected_at,
|
|
31
|
+
createdAt: r.created_at,
|
|
32
|
+
})),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** POST /api/pairing-tokens — generate a new pairing token */
|
|
37
|
+
pairing.post("/", async (c) => {
|
|
38
|
+
const userId = c.get("userId");
|
|
39
|
+
const { label } = await c.req.json<{ label?: string }>().catch(() => ({
|
|
40
|
+
label: undefined,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const id = generateId("pt_");
|
|
44
|
+
const token = generatePairingToken();
|
|
45
|
+
|
|
46
|
+
await c.env.DB.prepare(
|
|
47
|
+
"INSERT INTO pairing_tokens (id, user_id, token, label) VALUES (?, ?, ?, ?)",
|
|
48
|
+
)
|
|
49
|
+
.bind(id, userId, token, label?.trim() ?? null)
|
|
50
|
+
.run();
|
|
51
|
+
|
|
52
|
+
return c.json(
|
|
53
|
+
{
|
|
54
|
+
id,
|
|
55
|
+
token, // Show full token only on creation
|
|
56
|
+
label: label?.trim() ?? null,
|
|
57
|
+
},
|
|
58
|
+
201,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** DELETE /api/pairing-tokens/:id — revoke a pairing token */
|
|
63
|
+
pairing.delete("/:id", async (c) => {
|
|
64
|
+
const userId = c.get("userId");
|
|
65
|
+
const tokenId = c.req.param("id");
|
|
66
|
+
|
|
67
|
+
await c.env.DB.prepare(
|
|
68
|
+
"DELETE FROM pairing_tokens WHERE id = ? AND user_id = ?",
|
|
69
|
+
)
|
|
70
|
+
.bind(tokenId, userId)
|
|
71
|
+
.run();
|
|
72
|
+
|
|
73
|
+
return c.json({ ok: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export { pairing };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
const projects = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
6
|
+
|
|
7
|
+
/** GET /api/projects — list all projects for the current user */
|
|
8
|
+
projects.get("/", async (c) => {
|
|
9
|
+
const userId = c.get("userId");
|
|
10
|
+
|
|
11
|
+
const { results } = await c.env.DB.prepare(
|
|
12
|
+
"SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM projects WHERE user_id = ? ORDER BY created_at DESC",
|
|
13
|
+
)
|
|
14
|
+
.bind(userId)
|
|
15
|
+
.all<{
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
openclaw_agent_id: string;
|
|
20
|
+
system_prompt: string;
|
|
21
|
+
created_at: number;
|
|
22
|
+
updated_at: number;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
return c.json({
|
|
26
|
+
projects: (results ?? []).map((r) => ({
|
|
27
|
+
id: r.id,
|
|
28
|
+
name: r.name,
|
|
29
|
+
description: r.description,
|
|
30
|
+
openclawAgentId: r.openclaw_agent_id,
|
|
31
|
+
systemPrompt: r.system_prompt,
|
|
32
|
+
createdAt: r.created_at,
|
|
33
|
+
updatedAt: r.updated_at,
|
|
34
|
+
})),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** POST /api/projects — create a new project */
|
|
39
|
+
projects.post("/", async (c) => {
|
|
40
|
+
const userId = c.get("userId");
|
|
41
|
+
const { name, description, openclawAgentId, systemPrompt } = await c.req.json<{
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
openclawAgentId?: string;
|
|
45
|
+
systemPrompt?: string;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
if (!name?.trim()) {
|
|
49
|
+
return c.json({ error: "Project name is required" }, 400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const id = generateId("prj_");
|
|
53
|
+
// Default agent ID derived from project name (slug)
|
|
54
|
+
const agentId =
|
|
55
|
+
openclawAgentId?.trim() ||
|
|
56
|
+
name
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
60
|
+
.replace(/^-|-$/g, "");
|
|
61
|
+
|
|
62
|
+
await c.env.DB.prepare(
|
|
63
|
+
"INSERT INTO projects (id, user_id, name, description, openclaw_agent_id, system_prompt) VALUES (?, ?, ?, ?, ?, ?)",
|
|
64
|
+
)
|
|
65
|
+
.bind(id, userId, name.trim(), description?.trim() ?? "", agentId, systemPrompt?.trim() ?? "")
|
|
66
|
+
.run();
|
|
67
|
+
|
|
68
|
+
// Auto-create a default "Ad Hoc Chat" task
|
|
69
|
+
const taskId = generateId("tsk_");
|
|
70
|
+
const sessionKey = `agent:${agentId}:botschat:${userId}:adhoc`;
|
|
71
|
+
await c.env.DB.prepare(
|
|
72
|
+
"INSERT INTO tasks (id, project_id, name, kind, session_key) VALUES (?, ?, ?, ?, ?)",
|
|
73
|
+
)
|
|
74
|
+
.bind(taskId, id, "Ad Hoc Chat", "adhoc", sessionKey)
|
|
75
|
+
.run();
|
|
76
|
+
|
|
77
|
+
return c.json(
|
|
78
|
+
{
|
|
79
|
+
id,
|
|
80
|
+
name: name.trim(),
|
|
81
|
+
description: description?.trim() ?? "",
|
|
82
|
+
openclawAgentId: agentId,
|
|
83
|
+
systemPrompt: systemPrompt?.trim() ?? "",
|
|
84
|
+
},
|
|
85
|
+
201,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** GET /api/projects/:id — get a single project */
|
|
90
|
+
projects.get("/:id", async (c) => {
|
|
91
|
+
const userId = c.get("userId");
|
|
92
|
+
const projectId = c.req.param("id");
|
|
93
|
+
|
|
94
|
+
const row = await c.env.DB.prepare(
|
|
95
|
+
"SELECT id, name, description, openclaw_agent_id, system_prompt, created_at, updated_at FROM projects WHERE id = ? AND user_id = ?",
|
|
96
|
+
)
|
|
97
|
+
.bind(projectId, userId)
|
|
98
|
+
.first<{
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
description: string;
|
|
102
|
+
openclaw_agent_id: string;
|
|
103
|
+
system_prompt: string;
|
|
104
|
+
created_at: number;
|
|
105
|
+
updated_at: number;
|
|
106
|
+
}>();
|
|
107
|
+
|
|
108
|
+
if (!row) return c.json({ error: "Project not found" }, 404);
|
|
109
|
+
|
|
110
|
+
return c.json({
|
|
111
|
+
id: row.id,
|
|
112
|
+
name: row.name,
|
|
113
|
+
description: row.description,
|
|
114
|
+
openclawAgentId: row.openclaw_agent_id,
|
|
115
|
+
systemPrompt: row.system_prompt,
|
|
116
|
+
createdAt: row.created_at,
|
|
117
|
+
updatedAt: row.updated_at,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** PATCH /api/projects/:id — update a project */
|
|
122
|
+
projects.patch("/:id", async (c) => {
|
|
123
|
+
const userId = c.get("userId");
|
|
124
|
+
const projectId = c.req.param("id");
|
|
125
|
+
const body = await c.req.json<{
|
|
126
|
+
name?: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
systemPrompt?: string;
|
|
129
|
+
}>();
|
|
130
|
+
|
|
131
|
+
const sets: string[] = [];
|
|
132
|
+
const values: unknown[] = [];
|
|
133
|
+
|
|
134
|
+
if (body.name !== undefined) {
|
|
135
|
+
sets.push("name = ?");
|
|
136
|
+
values.push(body.name.trim());
|
|
137
|
+
}
|
|
138
|
+
if (body.description !== undefined) {
|
|
139
|
+
sets.push("description = ?");
|
|
140
|
+
values.push(body.description.trim());
|
|
141
|
+
}
|
|
142
|
+
if (body.systemPrompt !== undefined) {
|
|
143
|
+
sets.push("system_prompt = ?");
|
|
144
|
+
values.push(body.systemPrompt.trim());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (sets.length === 0) {
|
|
148
|
+
return c.json({ error: "No fields to update" }, 400);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
sets.push("updated_at = unixepoch()");
|
|
152
|
+
values.push(projectId, userId);
|
|
153
|
+
|
|
154
|
+
await c.env.DB.prepare(
|
|
155
|
+
`UPDATE projects SET ${sets.join(", ")} WHERE id = ? AND user_id = ?`,
|
|
156
|
+
)
|
|
157
|
+
.bind(...values)
|
|
158
|
+
.run();
|
|
159
|
+
|
|
160
|
+
return c.json({ ok: true });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
/** DELETE /api/projects/:id — delete a project */
|
|
164
|
+
projects.delete("/:id", async (c) => {
|
|
165
|
+
const userId = c.get("userId");
|
|
166
|
+
const projectId = c.req.param("id");
|
|
167
|
+
|
|
168
|
+
await c.env.DB.prepare(
|
|
169
|
+
"DELETE FROM projects WHERE id = ? AND user_id = ?",
|
|
170
|
+
)
|
|
171
|
+
.bind(projectId, userId)
|
|
172
|
+
.run();
|
|
173
|
+
|
|
174
|
+
return c.json({ ok: true });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export { projects };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
const sessions = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
6
|
+
|
|
7
|
+
/** GET /api/channels/:channelId/sessions — list all sessions for a channel */
|
|
8
|
+
sessions.get("/", async (c) => {
|
|
9
|
+
const userId = c.get("userId");
|
|
10
|
+
const channelId = c.req.param("channelId");
|
|
11
|
+
|
|
12
|
+
// Verify channel ownership
|
|
13
|
+
const channel = await c.env.DB.prepare(
|
|
14
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
15
|
+
)
|
|
16
|
+
.bind(channelId, userId)
|
|
17
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
18
|
+
|
|
19
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
20
|
+
|
|
21
|
+
const { results } = await c.env.DB.prepare(
|
|
22
|
+
"SELECT id, name, session_key, created_at, updated_at FROM sessions WHERE channel_id = ? AND user_id = ? ORDER BY created_at ASC",
|
|
23
|
+
)
|
|
24
|
+
.bind(channelId, userId)
|
|
25
|
+
.all<{
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
session_key: string;
|
|
29
|
+
created_at: number;
|
|
30
|
+
updated_at: number;
|
|
31
|
+
}>();
|
|
32
|
+
|
|
33
|
+
return c.json({
|
|
34
|
+
sessions: (results ?? []).map((r) => ({
|
|
35
|
+
id: r.id,
|
|
36
|
+
name: r.name,
|
|
37
|
+
sessionKey: r.session_key,
|
|
38
|
+
createdAt: r.created_at,
|
|
39
|
+
updatedAt: r.updated_at,
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** POST /api/channels/:channelId/sessions — create a new session */
|
|
45
|
+
sessions.post("/", async (c) => {
|
|
46
|
+
const userId = c.get("userId");
|
|
47
|
+
const channelId = c.req.param("channelId");
|
|
48
|
+
|
|
49
|
+
// Verify channel ownership
|
|
50
|
+
const channel = await c.env.DB.prepare(
|
|
51
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
52
|
+
)
|
|
53
|
+
.bind(channelId, userId)
|
|
54
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
55
|
+
|
|
56
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
57
|
+
|
|
58
|
+
const { name } = await c.req.json<{ name?: string }>();
|
|
59
|
+
|
|
60
|
+
// Count existing sessions for auto-naming
|
|
61
|
+
const countRow = await c.env.DB.prepare(
|
|
62
|
+
"SELECT COUNT(*) as cnt FROM sessions WHERE channel_id = ? AND user_id = ?",
|
|
63
|
+
)
|
|
64
|
+
.bind(channelId, userId)
|
|
65
|
+
.first<{ cnt: number }>();
|
|
66
|
+
const count = countRow?.cnt ?? 0;
|
|
67
|
+
|
|
68
|
+
const sessionName = name?.trim() || `Session ${count + 1}`;
|
|
69
|
+
const id = generateId("ses_");
|
|
70
|
+
const sessionKey = `agent:${channel.openclaw_agent_id}:botschat:${userId}:ses:${id}`;
|
|
71
|
+
|
|
72
|
+
await c.env.DB.prepare(
|
|
73
|
+
"INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, ?, ?)",
|
|
74
|
+
)
|
|
75
|
+
.bind(id, channelId, userId, sessionName, sessionKey)
|
|
76
|
+
.run();
|
|
77
|
+
|
|
78
|
+
return c.json(
|
|
79
|
+
{
|
|
80
|
+
id,
|
|
81
|
+
name: sessionName,
|
|
82
|
+
sessionKey,
|
|
83
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
84
|
+
updatedAt: Math.floor(Date.now() / 1000),
|
|
85
|
+
},
|
|
86
|
+
201,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** PATCH /api/channels/:channelId/sessions/:sessionId — rename a session */
|
|
91
|
+
sessions.patch("/:sessionId", async (c) => {
|
|
92
|
+
const userId = c.get("userId");
|
|
93
|
+
const channelId = c.req.param("channelId");
|
|
94
|
+
const sessionId = c.req.param("sessionId");
|
|
95
|
+
|
|
96
|
+
// Verify channel ownership
|
|
97
|
+
const channel = await c.env.DB.prepare(
|
|
98
|
+
"SELECT id FROM channels WHERE id = ? AND user_id = ?",
|
|
99
|
+
)
|
|
100
|
+
.bind(channelId, userId)
|
|
101
|
+
.first();
|
|
102
|
+
|
|
103
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
104
|
+
|
|
105
|
+
const { name } = await c.req.json<{ name: string }>();
|
|
106
|
+
|
|
107
|
+
if (!name?.trim()) {
|
|
108
|
+
return c.json({ error: "Session name is required" }, 400);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await c.env.DB.prepare(
|
|
112
|
+
"UPDATE sessions SET name = ?, updated_at = unixepoch() WHERE id = ? AND channel_id = ? AND user_id = ?",
|
|
113
|
+
)
|
|
114
|
+
.bind(name.trim(), sessionId, channelId, userId)
|
|
115
|
+
.run();
|
|
116
|
+
|
|
117
|
+
return c.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** DELETE /api/channels/:channelId/sessions/:sessionId — delete a session */
|
|
121
|
+
sessions.delete("/:sessionId", async (c) => {
|
|
122
|
+
const userId = c.get("userId");
|
|
123
|
+
const channelId = c.req.param("channelId");
|
|
124
|
+
const sessionId = c.req.param("sessionId");
|
|
125
|
+
|
|
126
|
+
// Verify channel ownership
|
|
127
|
+
const channel = await c.env.DB.prepare(
|
|
128
|
+
"SELECT id FROM channels WHERE id = ? AND user_id = ?",
|
|
129
|
+
)
|
|
130
|
+
.bind(channelId, userId)
|
|
131
|
+
.first();
|
|
132
|
+
|
|
133
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
134
|
+
|
|
135
|
+
// Don't allow deleting the last session
|
|
136
|
+
const countRow = await c.env.DB.prepare(
|
|
137
|
+
"SELECT COUNT(*) as cnt FROM sessions WHERE channel_id = ? AND user_id = ?",
|
|
138
|
+
)
|
|
139
|
+
.bind(channelId, userId)
|
|
140
|
+
.first<{ cnt: number }>();
|
|
141
|
+
|
|
142
|
+
if ((countRow?.cnt ?? 0) <= 1) {
|
|
143
|
+
return c.json({ error: "Cannot delete the last session" }, 400);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get session_key before deleting (to clean up messages)
|
|
147
|
+
const session = await c.env.DB.prepare(
|
|
148
|
+
"SELECT session_key FROM sessions WHERE id = ? AND channel_id = ? AND user_id = ?",
|
|
149
|
+
)
|
|
150
|
+
.bind(sessionId, channelId, userId)
|
|
151
|
+
.first<{ session_key: string }>();
|
|
152
|
+
|
|
153
|
+
await c.env.DB.prepare(
|
|
154
|
+
"DELETE FROM sessions WHERE id = ? AND channel_id = ? AND user_id = ?",
|
|
155
|
+
)
|
|
156
|
+
.bind(sessionId, channelId, userId)
|
|
157
|
+
.run();
|
|
158
|
+
|
|
159
|
+
// Clean up messages for this session
|
|
160
|
+
if (session?.session_key) {
|
|
161
|
+
await c.env.DB.prepare(
|
|
162
|
+
"DELETE FROM messages WHERE session_key = ?",
|
|
163
|
+
)
|
|
164
|
+
.bind(session.session_key)
|
|
165
|
+
.run();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return c.json({ ok: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export { sessions };
|