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,375 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
const tasks = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helper: push task schedule to OpenClaw via ConnectionDO
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
async function pushScheduleToOpenClaw(
|
|
11
|
+
env: Env,
|
|
12
|
+
userId: string,
|
|
13
|
+
task: { taskId: string; name?: string; openclawCronJobId: string; agentId: string; schedule: string; instructions: string; enabled: boolean; model?: string },
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
try {
|
|
16
|
+
const doId = env.CONNECTION_DO.idFromName(userId);
|
|
17
|
+
const stub = env.CONNECTION_DO.get(doId);
|
|
18
|
+
await stub.fetch(
|
|
19
|
+
new Request("https://internal/send", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
type: "task.schedule",
|
|
24
|
+
taskId: task.taskId,
|
|
25
|
+
name: task.name,
|
|
26
|
+
cronJobId: task.openclawCronJobId,
|
|
27
|
+
agentId: task.agentId,
|
|
28
|
+
schedule: task.schedule,
|
|
29
|
+
instructions: task.instructions,
|
|
30
|
+
enabled: task.enabled,
|
|
31
|
+
model: task.model,
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error("Failed to push schedule to OpenClaw:", err);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Helper: push task delete to OpenClaw via ConnectionDO
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
async function pushDeleteToOpenClaw(
|
|
44
|
+
env: Env,
|
|
45
|
+
userId: string,
|
|
46
|
+
cronJobId: string,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const doId = env.CONNECTION_DO.idFromName(userId);
|
|
50
|
+
const stub = env.CONNECTION_DO.get(doId);
|
|
51
|
+
await stub.fetch(
|
|
52
|
+
new Request("https://internal/send", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
type: "task.delete",
|
|
57
|
+
cronJobId,
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error("Failed to push task delete to OpenClaw:", err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** GET /api/channels/:channelId/tasks — list tasks for a channel */
|
|
67
|
+
tasks.get("/", async (c) => {
|
|
68
|
+
const userId = c.get("userId");
|
|
69
|
+
const channelId = c.req.param("channelId");
|
|
70
|
+
|
|
71
|
+
// Verify channel ownership
|
|
72
|
+
const channel = await c.env.DB.prepare(
|
|
73
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
74
|
+
)
|
|
75
|
+
.bind(channelId, userId)
|
|
76
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
77
|
+
|
|
78
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
79
|
+
|
|
80
|
+
// Note: schedule, instructions, model are NOT stored in D1.
|
|
81
|
+
// They belong to OpenClaw and are delivered to the frontend via WebSocket task.scan.result.
|
|
82
|
+
const { results } = await c.env.DB.prepare(
|
|
83
|
+
"SELECT id, name, kind, openclaw_cron_job_id, session_key, enabled, created_at, updated_at FROM tasks WHERE channel_id = ? ORDER BY kind ASC, created_at ASC",
|
|
84
|
+
)
|
|
85
|
+
.bind(channelId)
|
|
86
|
+
.all<{
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
kind: string;
|
|
90
|
+
openclaw_cron_job_id: string | null;
|
|
91
|
+
session_key: string | null;
|
|
92
|
+
enabled: number;
|
|
93
|
+
created_at: number;
|
|
94
|
+
updated_at: number;
|
|
95
|
+
}>();
|
|
96
|
+
|
|
97
|
+
return c.json({
|
|
98
|
+
tasks: (results ?? []).map((r) => ({
|
|
99
|
+
id: r.id,
|
|
100
|
+
name: r.name,
|
|
101
|
+
kind: r.kind,
|
|
102
|
+
openclawCronJobId: r.openclaw_cron_job_id,
|
|
103
|
+
sessionKey: r.session_key,
|
|
104
|
+
enabled: !!r.enabled,
|
|
105
|
+
createdAt: r.created_at,
|
|
106
|
+
updatedAt: r.updated_at,
|
|
107
|
+
})),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** POST /api/channels/:channelId/tasks — create a new task */
|
|
112
|
+
tasks.post("/", async (c) => {
|
|
113
|
+
const userId = c.get("userId");
|
|
114
|
+
const channelId = c.req.param("channelId");
|
|
115
|
+
|
|
116
|
+
// Verify channel ownership
|
|
117
|
+
const channel = await c.env.DB.prepare(
|
|
118
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
119
|
+
)
|
|
120
|
+
.bind(channelId, userId)
|
|
121
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
122
|
+
|
|
123
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
124
|
+
|
|
125
|
+
const { name, kind, schedule, instructions } = await c.req.json<{
|
|
126
|
+
name: string;
|
|
127
|
+
kind: "background" | "adhoc";
|
|
128
|
+
schedule?: string;
|
|
129
|
+
instructions?: string;
|
|
130
|
+
}>();
|
|
131
|
+
|
|
132
|
+
if (!name?.trim()) {
|
|
133
|
+
return c.json({ error: "Task name is required" }, 400);
|
|
134
|
+
}
|
|
135
|
+
if (!["background", "adhoc"].includes(kind)) {
|
|
136
|
+
return c.json({ error: "Task kind must be 'background' or 'adhoc'" }, 400);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const id = generateId("tsk_");
|
|
140
|
+
const agentId = channel.openclaw_agent_id;
|
|
141
|
+
|
|
142
|
+
// Build session key based on task kind
|
|
143
|
+
let sessionKey: string;
|
|
144
|
+
if (kind === "adhoc") {
|
|
145
|
+
const slug = name
|
|
146
|
+
.trim()
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
149
|
+
.replace(/^-|-$/g, "");
|
|
150
|
+
sessionKey = `agent:${agentId}:botschat:${userId}:adhoc:${slug}`;
|
|
151
|
+
} else {
|
|
152
|
+
sessionKey = `agent:${agentId}:botschat:${userId}:task:${id}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// D1 only stores basic task metadata — schedule/instructions/model belong to OpenClaw.
|
|
156
|
+
// openclawCronJobId is initially null for new background tasks; it will be set
|
|
157
|
+
// when the plugin creates the cron job and sends back a task.schedule.ack.
|
|
158
|
+
await c.env.DB.prepare(
|
|
159
|
+
"INSERT INTO tasks (id, channel_id, name, kind, openclaw_cron_job_id, session_key) VALUES (?, ?, ?, ?, ?, ?)",
|
|
160
|
+
)
|
|
161
|
+
.bind(id, channelId, name.trim(), kind, null, sessionKey)
|
|
162
|
+
.run();
|
|
163
|
+
|
|
164
|
+
// Push schedule to OpenClaw for background tasks (plugin will create the cron job)
|
|
165
|
+
if (kind === "background" && schedule) {
|
|
166
|
+
await pushScheduleToOpenClaw(c.env, userId, {
|
|
167
|
+
taskId: id,
|
|
168
|
+
name: name.trim(),
|
|
169
|
+
openclawCronJobId: "",
|
|
170
|
+
agentId,
|
|
171
|
+
schedule: schedule.trim(),
|
|
172
|
+
instructions: instructions?.trim() ?? "",
|
|
173
|
+
enabled: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return c.json(
|
|
178
|
+
{
|
|
179
|
+
id,
|
|
180
|
+
name: name.trim(),
|
|
181
|
+
kind,
|
|
182
|
+
openclawCronJobId: null,
|
|
183
|
+
sessionKey,
|
|
184
|
+
enabled: true,
|
|
185
|
+
},
|
|
186
|
+
201,
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/** PATCH /api/channels/:channelId/tasks/:taskId — update a task */
|
|
191
|
+
tasks.patch("/:taskId", async (c) => {
|
|
192
|
+
const userId = c.get("userId");
|
|
193
|
+
const channelId = c.req.param("channelId");
|
|
194
|
+
const taskId = c.req.param("taskId");
|
|
195
|
+
|
|
196
|
+
// Verify ownership and get task+channel info
|
|
197
|
+
const channel = await c.env.DB.prepare(
|
|
198
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
199
|
+
)
|
|
200
|
+
.bind(channelId, userId)
|
|
201
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
202
|
+
|
|
203
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
204
|
+
|
|
205
|
+
const existingTask = await c.env.DB.prepare(
|
|
206
|
+
"SELECT id, kind, openclaw_cron_job_id, enabled FROM tasks WHERE id = ? AND channel_id = ?",
|
|
207
|
+
)
|
|
208
|
+
.bind(taskId, channelId)
|
|
209
|
+
.first<{
|
|
210
|
+
id: string;
|
|
211
|
+
kind: string;
|
|
212
|
+
openclaw_cron_job_id: string | null;
|
|
213
|
+
enabled: number;
|
|
214
|
+
}>();
|
|
215
|
+
|
|
216
|
+
if (!existingTask) return c.json({ error: "Task not found" }, 404);
|
|
217
|
+
|
|
218
|
+
const body = await c.req.json<{
|
|
219
|
+
name?: string;
|
|
220
|
+
schedule?: string;
|
|
221
|
+
instructions?: string;
|
|
222
|
+
model?: string;
|
|
223
|
+
enabled?: boolean;
|
|
224
|
+
}>();
|
|
225
|
+
|
|
226
|
+
// D1 only stores: name and enabled.
|
|
227
|
+
// Schedule, instructions, model belong to OpenClaw — pushed via WebSocket.
|
|
228
|
+
const sets: string[] = [];
|
|
229
|
+
const values: unknown[] = [];
|
|
230
|
+
|
|
231
|
+
if (body.name !== undefined) {
|
|
232
|
+
sets.push("name = ?");
|
|
233
|
+
values.push(body.name.trim());
|
|
234
|
+
}
|
|
235
|
+
if (body.enabled !== undefined) {
|
|
236
|
+
sets.push("enabled = ?");
|
|
237
|
+
values.push(body.enabled ? 1 : 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (sets.length > 0) {
|
|
241
|
+
sets.push("updated_at = unixepoch()");
|
|
242
|
+
values.push(taskId, channelId);
|
|
243
|
+
await c.env.DB.prepare(
|
|
244
|
+
`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND channel_id = ?`,
|
|
245
|
+
)
|
|
246
|
+
.bind(...values)
|
|
247
|
+
.run();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Push OpenClaw-owned fields directly to OpenClaw for background tasks.
|
|
251
|
+
// The client must send ALL OpenClaw fields (schedule, instructions, enabled)
|
|
252
|
+
// together since they are not stored in D1.
|
|
253
|
+
if (existingTask.kind === "background" && existingTask.openclaw_cron_job_id) {
|
|
254
|
+
const needsPush = body.schedule !== undefined || body.instructions !== undefined || body.enabled !== undefined || body.model !== undefined;
|
|
255
|
+
if (needsPush) {
|
|
256
|
+
await pushScheduleToOpenClaw(c.env, userId, {
|
|
257
|
+
taskId: taskId,
|
|
258
|
+
openclawCronJobId: existingTask.openclaw_cron_job_id,
|
|
259
|
+
agentId: channel.openclaw_agent_id,
|
|
260
|
+
schedule: body.schedule ?? "",
|
|
261
|
+
instructions: body.instructions ?? "",
|
|
262
|
+
enabled: body.enabled ?? !!existingTask.enabled,
|
|
263
|
+
model: body.model,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return c.json({ ok: true });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/** POST /api/channels/:channelId/tasks/:taskId/run — trigger a one-time immediate execution */
|
|
272
|
+
tasks.post("/:taskId/run", async (c) => {
|
|
273
|
+
const userId = c.get("userId");
|
|
274
|
+
const channelId = c.req.param("channelId");
|
|
275
|
+
const taskId = c.req.param("taskId");
|
|
276
|
+
|
|
277
|
+
// Verify ownership
|
|
278
|
+
const channel = await c.env.DB.prepare(
|
|
279
|
+
"SELECT id, openclaw_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
280
|
+
)
|
|
281
|
+
.bind(channelId, userId)
|
|
282
|
+
.first<{ id: string; openclaw_agent_id: string }>();
|
|
283
|
+
|
|
284
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
285
|
+
|
|
286
|
+
const task = await c.env.DB.prepare(
|
|
287
|
+
"SELECT id, kind, openclaw_cron_job_id FROM tasks WHERE id = ? AND channel_id = ?",
|
|
288
|
+
)
|
|
289
|
+
.bind(taskId, channelId)
|
|
290
|
+
.first<{
|
|
291
|
+
id: string;
|
|
292
|
+
kind: string;
|
|
293
|
+
openclaw_cron_job_id: string | null;
|
|
294
|
+
}>();
|
|
295
|
+
|
|
296
|
+
if (!task) return c.json({ error: "Task not found" }, 404);
|
|
297
|
+
if (task.kind !== "background") return c.json({ error: "Only background tasks can be triggered" }, 400);
|
|
298
|
+
if (!task.openclaw_cron_job_id) return c.json({ error: "Task has no associated cron job" }, 400);
|
|
299
|
+
|
|
300
|
+
// Send a task.run message to OpenClaw via ConnectionDO.
|
|
301
|
+
// Instructions and model are not included — the plugin reads them
|
|
302
|
+
// directly from OpenClaw's jobs.json (the single source of truth).
|
|
303
|
+
try {
|
|
304
|
+
const doId = c.env.CONNECTION_DO.idFromName(userId);
|
|
305
|
+
const stub = c.env.CONNECTION_DO.get(doId);
|
|
306
|
+
const resp = await stub.fetch(
|
|
307
|
+
new Request("https://internal/send", {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: { "Content-Type": "application/json" },
|
|
310
|
+
body: JSON.stringify({
|
|
311
|
+
type: "task.run",
|
|
312
|
+
cronJobId: task.openclaw_cron_job_id,
|
|
313
|
+
agentId: channel.openclaw_agent_id,
|
|
314
|
+
}),
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!resp.ok) {
|
|
319
|
+
const err = await resp.json().catch(() => ({ error: "Unknown error" }));
|
|
320
|
+
return c.json({ error: (err as { error?: string }).error ?? "Failed to trigger task" }, 503);
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error("Failed to trigger task run:", err);
|
|
324
|
+
return c.json({ error: "OpenClaw is not connected" }, 503);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return c.json({ ok: true, message: "Task triggered" });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
/** DELETE /api/channels/:channelId/tasks/:taskId */
|
|
331
|
+
tasks.delete("/:taskId", async (c) => {
|
|
332
|
+
const userId = c.get("userId");
|
|
333
|
+
const channelId = c.req.param("channelId");
|
|
334
|
+
const taskId = c.req.param("taskId");
|
|
335
|
+
|
|
336
|
+
const channel = await c.env.DB.prepare(
|
|
337
|
+
"SELECT id FROM channels WHERE id = ? AND user_id = ?",
|
|
338
|
+
)
|
|
339
|
+
.bind(channelId, userId)
|
|
340
|
+
.first();
|
|
341
|
+
|
|
342
|
+
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
343
|
+
|
|
344
|
+
// Get task to check if it's background (need to delete CronJob)
|
|
345
|
+
const task = await c.env.DB.prepare(
|
|
346
|
+
"SELECT kind, openclaw_cron_job_id FROM tasks WHERE id = ? AND channel_id = ?",
|
|
347
|
+
)
|
|
348
|
+
.bind(taskId, channelId)
|
|
349
|
+
.first<{ kind: string; openclaw_cron_job_id: string | null }>();
|
|
350
|
+
|
|
351
|
+
await c.env.DB.prepare(
|
|
352
|
+
"DELETE FROM tasks WHERE id = ? AND channel_id = ?",
|
|
353
|
+
)
|
|
354
|
+
.bind(taskId, channelId)
|
|
355
|
+
.run();
|
|
356
|
+
|
|
357
|
+
// Delete associated jobs
|
|
358
|
+
await c.env.DB.prepare("DELETE FROM jobs WHERE task_id = ?").bind(taskId).run();
|
|
359
|
+
|
|
360
|
+
// Push delete to OpenClaw for background tasks
|
|
361
|
+
if (task?.kind === "background" && task.openclaw_cron_job_id) {
|
|
362
|
+
// Record the deletion so task.scan won't re-create the task
|
|
363
|
+
await c.env.DB.prepare(
|
|
364
|
+
"INSERT OR IGNORE INTO deleted_cron_jobs (cron_job_id, user_id) VALUES (?, ?)",
|
|
365
|
+
)
|
|
366
|
+
.bind(task.openclaw_cron_job_id, userId)
|
|
367
|
+
.run();
|
|
368
|
+
|
|
369
|
+
await pushDeleteToOpenClaw(c.env, userId, task.openclaw_cron_job_id);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return c.json({ ok: true });
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
export { tasks };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
export const upload = new Hono<{
|
|
5
|
+
Bindings: Env;
|
|
6
|
+
Variables: { userId: string };
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
/** POST / — Upload a file to R2 and return its public URL. */
|
|
10
|
+
upload.post("/", async (c) => {
|
|
11
|
+
const userId = c.get("userId");
|
|
12
|
+
const contentType = c.req.header("Content-Type") ?? "";
|
|
13
|
+
|
|
14
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
15
|
+
return c.json({ error: "Expected multipart/form-data" }, 400);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const formData = await c.req.formData();
|
|
19
|
+
const file = formData.get("file") as File | null;
|
|
20
|
+
|
|
21
|
+
if (!file) {
|
|
22
|
+
return c.json({ error: "No file provided" }, 400);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate file type — only images allowed
|
|
26
|
+
if (!file.type.startsWith("image/")) {
|
|
27
|
+
return c.json({ error: "Only image files are allowed" }, 400);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Limit file size to 10 MB
|
|
31
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
32
|
+
if (file.size > MAX_SIZE) {
|
|
33
|
+
return c.json({ error: "File too large (max 10 MB)" }, 413);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate a unique key: media/{userId}/{timestamp}-{random}.{ext}
|
|
37
|
+
const ext = file.name.split(".").pop()?.toLowerCase() ?? "png";
|
|
38
|
+
const safeExt = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico"].includes(ext) ? ext : "png";
|
|
39
|
+
const key = `media/${userId}/${Date.now()}-${crypto.randomUUID().slice(0, 8)}.${safeExt}`;
|
|
40
|
+
|
|
41
|
+
// Upload to R2
|
|
42
|
+
await c.env.MEDIA.put(key, file.stream(), {
|
|
43
|
+
httpMetadata: {
|
|
44
|
+
contentType: file.type,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Return the URL for serving through the API
|
|
49
|
+
const url = `/api/media/${key.replace("media/", "")}`;
|
|
50
|
+
|
|
51
|
+
return c.json({ url, key });
|
|
52
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
// Simple JWT-like token using HMAC-SHA256.
|
|
5
|
+
// In production, use a proper JWT library or Cloudflare Access.
|
|
6
|
+
|
|
7
|
+
type TokenPayload = {
|
|
8
|
+
sub: string; // user ID
|
|
9
|
+
exp: number; // expiration timestamp (seconds)
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ENCODER = new TextEncoder();
|
|
13
|
+
|
|
14
|
+
async function hmacSign(secret: string, data: string): Promise<string> {
|
|
15
|
+
const key = await crypto.subtle.importKey(
|
|
16
|
+
"raw",
|
|
17
|
+
ENCODER.encode(secret),
|
|
18
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
19
|
+
false,
|
|
20
|
+
["sign"],
|
|
21
|
+
);
|
|
22
|
+
const sig = await crypto.subtle.sign("HMAC", key, ENCODER.encode(data));
|
|
23
|
+
return btoa(String.fromCharCode(...new Uint8Array(sig)))
|
|
24
|
+
.replace(/\+/g, "-")
|
|
25
|
+
.replace(/\//g, "_")
|
|
26
|
+
.replace(/=+$/, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function base64UrlEncode(obj: unknown): string {
|
|
30
|
+
return btoa(JSON.stringify(obj))
|
|
31
|
+
.replace(/\+/g, "-")
|
|
32
|
+
.replace(/\//g, "_")
|
|
33
|
+
.replace(/=+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function base64UrlDecode(str: string): unknown {
|
|
37
|
+
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
38
|
+
return JSON.parse(atob(padded));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createToken(
|
|
42
|
+
userId: string,
|
|
43
|
+
secret: string,
|
|
44
|
+
expiresInSeconds = 86400 * 7, // 7 days
|
|
45
|
+
): Promise<string> {
|
|
46
|
+
const payload: TokenPayload = {
|
|
47
|
+
sub: userId,
|
|
48
|
+
exp: Math.floor(Date.now() / 1000) + expiresInSeconds,
|
|
49
|
+
};
|
|
50
|
+
const header = base64UrlEncode({ alg: "HS256", typ: "JWT" });
|
|
51
|
+
const body = base64UrlEncode(payload);
|
|
52
|
+
const signature = await hmacSign(secret, `${header}.${body}`);
|
|
53
|
+
return `${header}.${body}.${signature}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function verifyToken(
|
|
57
|
+
token: string,
|
|
58
|
+
secret: string,
|
|
59
|
+
): Promise<TokenPayload | null> {
|
|
60
|
+
const parts = token.split(".");
|
|
61
|
+
if (parts.length !== 3) return null;
|
|
62
|
+
|
|
63
|
+
const [header, body, signature] = parts;
|
|
64
|
+
const expected = await hmacSign(secret, `${header}.${body}`);
|
|
65
|
+
if (signature !== expected) return null;
|
|
66
|
+
|
|
67
|
+
const payload = base64UrlDecode(body) as TokenPayload;
|
|
68
|
+
if (payload.exp < Math.floor(Date.now() / 1000)) return null;
|
|
69
|
+
|
|
70
|
+
return payload;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Hash a password using SHA-256 (for simplicity; production should use bcrypt/scrypt). */
|
|
74
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
75
|
+
const data = ENCODER.encode(password);
|
|
76
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
77
|
+
return Array.from(new Uint8Array(hash))
|
|
78
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
79
|
+
.join("");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Auth middleware: extracts user ID from Bearer token and sets it on context. */
|
|
83
|
+
export function authMiddleware(): MiddlewareHandler<{ Bindings: Env; Variables: { userId: string } }> {
|
|
84
|
+
return async (c, next) => {
|
|
85
|
+
const authHeader = c.req.header("Authorization");
|
|
86
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
87
|
+
return c.json({ error: "Missing or invalid Authorization header" }, 401);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const token = authHeader.slice(7);
|
|
91
|
+
const secret = c.env.JWT_SECRET ?? "botschat-dev-secret";
|
|
92
|
+
const payload = await verifyToken(token, secret);
|
|
93
|
+
|
|
94
|
+
if (!payload) {
|
|
95
|
+
return c.json({ error: "Invalid or expired token" }, 401);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
c.set("userId", payload.sub);
|
|
99
|
+
await next();
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Generate a short random ID (URL-safe). */
|
|
2
|
+
export function generateId(prefix = ""): string {
|
|
3
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
+
let id = prefix;
|
|
5
|
+
for (let i = 0; i < 16; i++) {
|
|
6
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
7
|
+
}
|
|
8
|
+
return id;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Generate a pairing token with bc_pat_ prefix. */
|
|
12
|
+
export function generatePairingToken(): string {
|
|
13
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
14
|
+
let token = "bc_pat_";
|
|
15
|
+
for (let i = 0; i < 32; i++) {
|
|
16
|
+
token += chars[Math.floor(Math.random() * chars.length)];
|
|
17
|
+
}
|
|
18
|
+
return token;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"rootDir": "src",
|
|
13
|
+
"lib": ["ES2022"],
|
|
14
|
+
"types": ["@cloudflare/workers-types"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { botschatPlugin } from "./src/channel.js";
|
|
2
|
+
declare const plugin: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
configSchema: {
|
|
7
|
+
safeParse: () => {
|
|
8
|
+
success: boolean;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
register(api: {
|
|
12
|
+
runtime: unknown;
|
|
13
|
+
registerChannel: (reg: {
|
|
14
|
+
plugin: typeof botschatPlugin;
|
|
15
|
+
}) => void;
|
|
16
|
+
}): void;
|
|
17
|
+
};
|
|
18
|
+
export default plugin;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAMlD,QAAA,MAAM,MAAM;;;;;;;;;kBAKI;QACZ,OAAO,EAAE,OAAO,CAAC;QACjB,eAAe,EAAE,CAAC,GAAG,EAAE;YAAE,MAAM,EAAE,OAAO,cAAc,CAAA;SAAE,KAAK,IAAI,CAAC;KACnE;CAIF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { botschatPlugin } from "./src/channel.js";
|
|
2
|
+
import { setBotsChatRuntime } from "./src/runtime.js";
|
|
3
|
+
// OpenClaw Plugin Definition
|
|
4
|
+
// This is the entry point loaded by OpenClaw's plugin system.
|
|
5
|
+
// It registers the BotsChat channel plugin.
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "botschat",
|
|
8
|
+
name: "BotsChat",
|
|
9
|
+
description: "Connect to BotsChat cloud chat platform",
|
|
10
|
+
configSchema: { safeParse: () => ({ success: true }) },
|
|
11
|
+
register(api) {
|
|
12
|
+
setBotsChatRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: botschatPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export default plugin;
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,6BAA6B;AAC7B,8DAA8D;AAC9D,4CAA4C;AAC5C,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,yCAAyC;IACtD,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE;IACtD,QAAQ,CAAC,GAGR;QACC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IAClD,CAAC;CACF,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedBotsChatAccount } from "./types.js";
|
|
2
|
+
/** List all configured account IDs. */
|
|
3
|
+
export declare function listBotsChatAccountIds(cfg: unknown): string[];
|
|
4
|
+
/** Resolve default account ID. */
|
|
5
|
+
export declare function resolveDefaultBotsChatAccountId(cfg: unknown): string;
|
|
6
|
+
/** Resolve a single account by ID. */
|
|
7
|
+
export declare function resolveBotsChatAccount(cfg: unknown, accountId?: string | null): ResolvedBotsChatAccount;
|
|
8
|
+
/** Delete an account from config, returning the updated config. */
|
|
9
|
+
export declare function deleteBotsChatAccount(cfg: unknown, accountId: string): unknown;
|
|
10
|
+
/** Enable or disable an account, returning updated config. */
|
|
11
|
+
export declare function setBotsChatAccountEnabled(cfg: unknown, accountId: string, enabled: boolean): unknown;
|
|
12
|
+
//# sourceMappingURL=accounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/accounts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAUpB,uCAAuC;AACvC,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAgB7D;AAED,kCAAkC;AAClC,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAGpE;AAED,sCAAsC;AACtC,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,OAAO,EACZ,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,uBAAuB,CA6BzB;AAED,mEAAmE;AACnE,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAsBT;AAED,8DAA8D;AAC9D,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,GACf,OAAO,CAoBT"}
|