botschat 0.1.19 → 0.1.20
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/migrations/0013_agents_table.sql +29 -0
- package/migrations/0014_agent_sessions.sql +19 -0
- package/migrations/0015_message_traces.sql +27 -0
- package/migrations/0016_multi_agent_channels_messages.sql +9 -0
- package/migrations/0017_rename_cron_job_id.sql +2 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +382 -186
- package/packages/api/src/index.ts +67 -50
- package/packages/api/src/protocol-v2.ts +154 -0
- package/packages/api/src/routes/agents-v2.ts +192 -0
- package/packages/api/src/routes/agents.ts +3 -3
- package/packages/api/src/routes/auth.ts +0 -1
- package/packages/api/src/routes/channels.ts +11 -11
- package/packages/api/src/routes/history-v2.ts +221 -0
- package/packages/api/src/routes/migrate-v2.ts +110 -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.d.ts +10 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +10 -51
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +13 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/package.json +2 -18
- package/packages/web/dist/assets/index-BARPtt0v.css +1 -0
- package/packages/web/dist/assets/index-Bf-XL3te.js +2 -0
- package/packages/web/dist/assets/{index-C_GamcQc.js → index-CYQMu_-c.js} +1 -1
- package/packages/web/dist/assets/index-CYlvfpX9.js +1519 -0
- package/packages/web/dist/assets/index-CxcpA4Qo.js +1 -0
- package/packages/web/dist/assets/{index-MyoWvQAH.js → index-DYCO-ry1.js} +1 -1
- package/packages/web/dist/assets/index-QebPVqwj.js +2 -0
- package/packages/web/dist/assets/{index.esm-BpQAwtdR.js → index.esm-CvOpngZM.js} +1 -1
- package/packages/web/dist/assets/{web-cnzjgNfD.js → web-1cdhq2RW.js} +1 -1
- package/packages/web/dist/assets/{web-BbTzVNLt.js → web-D3LMODYp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/src/App.tsx +56 -9
- package/packages/web/src/api.ts +61 -5
- package/packages/web/src/components/AgentSettings.tsx +328 -0
- package/packages/web/src/components/ChatWindow.tsx +9 -9
- package/packages/web/src/components/CronDetail.tsx +1 -1
- package/packages/web/src/components/LoginPage.tsx +1 -59
- package/packages/web/src/components/MessageContent.tsx +2 -17
- package/packages/web/src/components/SessionTabs.tsx +1 -1
- package/packages/web/src/components/Sidebar.tsx +3 -1
- package/packages/web/src/hooks/useIMEComposition.ts +9 -14
- package/packages/web/src/store.ts +39 -7
- package/packages/web/src/ws.ts +1 -0
- package/scripts/dev.sh +53 -0
- package/scripts/mock-openclaw-v2.mjs +486 -0
- package/packages/api/src/routes/demo.ts +0 -156
- package/packages/web/dist/assets/index-BtPyCBCl.css +0 -1
- package/packages/web/dist/assets/index-BtpsFe4Z.js +0 -2
- package/packages/web/dist/assets/index-CQbIYr6_.js +0 -2
- package/packages/web/dist/assets/index-LiBjPMg2.js +0 -1
- package/packages/web/dist/assets/index-STIPTMK8.js +0 -1516
- package/packages/web/src/components/ImageLightbox.tsx +0 -96
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* History Query API v2 — supports verbose-level filtering, agent filtering,
|
|
6
|
+
* trace type filtering, and keyword search across messages + message_traces.
|
|
7
|
+
*
|
|
8
|
+
* GET /api/v2/messages/query?sessionKey=...&verboseLevel=1&limit=50
|
|
9
|
+
*/
|
|
10
|
+
const historyV2 = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
11
|
+
|
|
12
|
+
historyV2.get("/query", async (c) => {
|
|
13
|
+
const userId = c.get("userId");
|
|
14
|
+
const sessionKey = c.req.query("sessionKey");
|
|
15
|
+
if (!sessionKey) return c.json({ error: "sessionKey required" }, 400);
|
|
16
|
+
|
|
17
|
+
const verboseLevel = Math.min(Number(c.req.query("verboseLevel") ?? 1), 3);
|
|
18
|
+
const limit = Math.min(Number(c.req.query("limit") ?? 50), 200);
|
|
19
|
+
const senderFilter = c.req.query("senderFilter"); // "user" | "agent" | undefined
|
|
20
|
+
const agentIdFilter = c.req.query("agentIdFilter");
|
|
21
|
+
const traceTypeFilter = c.req.query("traceTypeFilter");
|
|
22
|
+
const keyword = c.req.query("keyword");
|
|
23
|
+
const beforeMessageId = c.req.query("beforeMessageId");
|
|
24
|
+
|
|
25
|
+
// Build lv1 query (messages table)
|
|
26
|
+
const conditions: string[] = ["m.session_key = ?", "m.user_id = ?"];
|
|
27
|
+
const params: unknown[] = [sessionKey, userId];
|
|
28
|
+
|
|
29
|
+
if (senderFilter === "user" || senderFilter === "agent") {
|
|
30
|
+
conditions.push("m.sender = ?");
|
|
31
|
+
params.push(senderFilter);
|
|
32
|
+
}
|
|
33
|
+
if (agentIdFilter) {
|
|
34
|
+
conditions.push("(m.sender_agent_id = ? OR m.target_agent_id = ?)");
|
|
35
|
+
params.push(agentIdFilter, agentIdFilter);
|
|
36
|
+
}
|
|
37
|
+
if (keyword) {
|
|
38
|
+
conditions.push("m.text LIKE ?");
|
|
39
|
+
params.push(`%${keyword}%`);
|
|
40
|
+
}
|
|
41
|
+
if (beforeMessageId) {
|
|
42
|
+
conditions.push("m.created_at < (SELECT created_at FROM messages WHERE id = ?)");
|
|
43
|
+
params.push(beforeMessageId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
params.push(limit);
|
|
47
|
+
|
|
48
|
+
const { results: messages } = await c.env.DB.prepare(
|
|
49
|
+
`SELECT m.id, m.sender, m.sender_agent_id, m.target_agent_id, m.text, m.media_url, m.encrypted, m.created_at
|
|
50
|
+
FROM messages m
|
|
51
|
+
WHERE ${conditions.join(" AND ")}
|
|
52
|
+
ORDER BY m.created_at DESC
|
|
53
|
+
LIMIT ?`,
|
|
54
|
+
)
|
|
55
|
+
.bind(...params)
|
|
56
|
+
.all<{
|
|
57
|
+
id: string;
|
|
58
|
+
sender: string;
|
|
59
|
+
sender_agent_id: string | null;
|
|
60
|
+
target_agent_id: string | null;
|
|
61
|
+
text: string;
|
|
62
|
+
media_url: string | null;
|
|
63
|
+
encrypted: number;
|
|
64
|
+
created_at: number;
|
|
65
|
+
}>();
|
|
66
|
+
|
|
67
|
+
// Resolve agent names
|
|
68
|
+
const agentIds = new Set<string>();
|
|
69
|
+
for (const m of messages ?? []) {
|
|
70
|
+
if (m.sender_agent_id) agentIds.add(m.sender_agent_id);
|
|
71
|
+
if (m.target_agent_id) agentIds.add(m.target_agent_id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const agentNames: Record<string, string> = {};
|
|
75
|
+
if (agentIds.size > 0) {
|
|
76
|
+
const placeholders = [...agentIds].map(() => "?").join(",");
|
|
77
|
+
const { results: agents } = await c.env.DB.prepare(
|
|
78
|
+
`SELECT id, name FROM agents WHERE id IN (${placeholders})`,
|
|
79
|
+
)
|
|
80
|
+
.bind(...agentIds)
|
|
81
|
+
.all<{ id: string; name: string }>();
|
|
82
|
+
for (const a of agents ?? []) {
|
|
83
|
+
agentNames[a.id] = a.name;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build response
|
|
88
|
+
type MessageResponse = {
|
|
89
|
+
id: string;
|
|
90
|
+
sender: string;
|
|
91
|
+
senderAgentId?: string;
|
|
92
|
+
senderAgentName?: string;
|
|
93
|
+
targetAgentId?: string;
|
|
94
|
+
text: string;
|
|
95
|
+
mediaUrl?: string;
|
|
96
|
+
encrypted: boolean;
|
|
97
|
+
timestamp: number;
|
|
98
|
+
traces?: Array<{
|
|
99
|
+
verboseLevel: number;
|
|
100
|
+
traceType: string;
|
|
101
|
+
content: string;
|
|
102
|
+
metadata?: Record<string, unknown>;
|
|
103
|
+
}>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result: MessageResponse[] = [];
|
|
107
|
+
const messageIds = (messages ?? []).map((m) => m.id);
|
|
108
|
+
|
|
109
|
+
// Fetch traces if verboseLevel >= 2
|
|
110
|
+
let tracesByMessageId: Record<string, Array<{ verbose_level: number; trace_type: string; content: string; metadata_json: string }>> = {};
|
|
111
|
+
|
|
112
|
+
if (verboseLevel >= 2 && messageIds.length > 0) {
|
|
113
|
+
const traceConds: string[] = ["message_id IN (" + messageIds.map(() => "?").join(",") + ")"];
|
|
114
|
+
const traceParams: unknown[] = [...messageIds];
|
|
115
|
+
|
|
116
|
+
traceConds.push("verbose_level <= ?");
|
|
117
|
+
traceParams.push(verboseLevel);
|
|
118
|
+
|
|
119
|
+
if (traceTypeFilter) {
|
|
120
|
+
traceConds.push("trace_type = ?");
|
|
121
|
+
traceParams.push(traceTypeFilter);
|
|
122
|
+
}
|
|
123
|
+
if (agentIdFilter) {
|
|
124
|
+
traceConds.push("agent_id = ?");
|
|
125
|
+
traceParams.push(agentIdFilter);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { results: traces } = await c.env.DB.prepare(
|
|
129
|
+
`SELECT message_id, verbose_level, trace_type, content, metadata_json
|
|
130
|
+
FROM message_traces
|
|
131
|
+
WHERE ${traceConds.join(" AND ")}
|
|
132
|
+
ORDER BY created_at ASC`,
|
|
133
|
+
)
|
|
134
|
+
.bind(...traceParams)
|
|
135
|
+
.all<{ message_id: string; verbose_level: number; trace_type: string; content: string; metadata_json: string }>();
|
|
136
|
+
|
|
137
|
+
for (const t of traces ?? []) {
|
|
138
|
+
if (!tracesByMessageId[t.message_id]) tracesByMessageId[t.message_id] = [];
|
|
139
|
+
tracesByMessageId[t.message_id].push(t);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const m of messages ?? []) {
|
|
144
|
+
const msg: MessageResponse = {
|
|
145
|
+
id: m.id,
|
|
146
|
+
sender: m.sender,
|
|
147
|
+
senderAgentId: m.sender_agent_id ?? undefined,
|
|
148
|
+
senderAgentName: m.sender_agent_id ? agentNames[m.sender_agent_id] : undefined,
|
|
149
|
+
targetAgentId: m.target_agent_id ?? undefined,
|
|
150
|
+
text: typeof m.text === "string" ? m.text : "",
|
|
151
|
+
mediaUrl: m.media_url ?? undefined,
|
|
152
|
+
encrypted: !!m.encrypted,
|
|
153
|
+
timestamp: m.created_at,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (verboseLevel >= 2 && tracesByMessageId[m.id]) {
|
|
157
|
+
msg.traces = tracesByMessageId[m.id].map((t) => ({
|
|
158
|
+
verboseLevel: t.verbose_level,
|
|
159
|
+
traceType: t.trace_type,
|
|
160
|
+
content: typeof t.content === "string" ? t.content : "",
|
|
161
|
+
metadata: t.metadata_json ? JSON.parse(t.metadata_json) : undefined,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
result.push(msg);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Return in chronological order (query was DESC for LIMIT, reverse for output)
|
|
169
|
+
result.reverse();
|
|
170
|
+
|
|
171
|
+
return c.json({ messages: result, hasMore: (messages?.length ?? 0) === limit });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/** GET /api/v2/messages/traces/:messageId — fetch traces for a specific message */
|
|
175
|
+
historyV2.get("/traces/:messageId", async (c) => {
|
|
176
|
+
const userId = c.get("userId");
|
|
177
|
+
const messageId = c.req.param("messageId");
|
|
178
|
+
const verboseLevel = Math.min(Number(c.req.query("verboseLevel") ?? 3), 3);
|
|
179
|
+
|
|
180
|
+
// Verify message belongs to user
|
|
181
|
+
const msg = await c.env.DB.prepare(
|
|
182
|
+
"SELECT id FROM messages WHERE id = ? AND user_id = ?",
|
|
183
|
+
)
|
|
184
|
+
.bind(messageId, userId)
|
|
185
|
+
.first();
|
|
186
|
+
|
|
187
|
+
if (!msg) return c.json({ error: "Message not found" }, 404);
|
|
188
|
+
|
|
189
|
+
const { results: traces } = await c.env.DB.prepare(
|
|
190
|
+
`SELECT id, verbose_level, trace_type, content, metadata_json, agent_id, encrypted, created_at
|
|
191
|
+
FROM message_traces
|
|
192
|
+
WHERE message_id = ? AND verbose_level <= ?
|
|
193
|
+
ORDER BY created_at ASC`,
|
|
194
|
+
)
|
|
195
|
+
.bind(messageId, verboseLevel)
|
|
196
|
+
.all<{
|
|
197
|
+
id: string;
|
|
198
|
+
verbose_level: number;
|
|
199
|
+
trace_type: string;
|
|
200
|
+
content: string;
|
|
201
|
+
metadata_json: string;
|
|
202
|
+
agent_id: string;
|
|
203
|
+
encrypted: number;
|
|
204
|
+
created_at: number;
|
|
205
|
+
}>();
|
|
206
|
+
|
|
207
|
+
return c.json({
|
|
208
|
+
traces: (traces ?? []).map((t) => ({
|
|
209
|
+
id: t.id,
|
|
210
|
+
verboseLevel: t.verbose_level,
|
|
211
|
+
traceType: t.trace_type,
|
|
212
|
+
content: typeof t.content === "string" ? t.content : "",
|
|
213
|
+
metadata: t.metadata_json ? JSON.parse(t.metadata_json) : undefined,
|
|
214
|
+
agentId: t.agent_id,
|
|
215
|
+
encrypted: !!t.encrypted,
|
|
216
|
+
timestamp: t.created_at,
|
|
217
|
+
})),
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export { historyV2 };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../env.js";
|
|
3
|
+
import { generateId } from "../utils/id.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One-time migration endpoint: converts legacy pairing_tokens into agents,
|
|
7
|
+
* backfills channels.default_agent_id, and sets messages.sender_agent_id.
|
|
8
|
+
*
|
|
9
|
+
* POST /api/v2/migrate — idempotent, safe to run multiple times.
|
|
10
|
+
* Only available in development mode.
|
|
11
|
+
*/
|
|
12
|
+
const migrateV2 = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
13
|
+
|
|
14
|
+
migrateV2.post("/", async (c) => {
|
|
15
|
+
if (c.env.ENVIRONMENT !== "development") {
|
|
16
|
+
return c.json({ error: "Migration endpoint only available in development" }, 403);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userId = c.get("userId");
|
|
20
|
+
const stats = { agentsCreated: 0, channelsUpdated: 0, messagesUpdated: 0, skipped: 0 };
|
|
21
|
+
|
|
22
|
+
// Step 1: Migrate pairing_tokens → agents (skip if agent with same token already exists)
|
|
23
|
+
const { results: tokens } = await c.env.DB.prepare(
|
|
24
|
+
"SELECT id, token, label, last_connected_at, last_ip, connection_count FROM pairing_tokens WHERE user_id = ? AND revoked_at IS NULL",
|
|
25
|
+
)
|
|
26
|
+
.bind(userId)
|
|
27
|
+
.all<{
|
|
28
|
+
id: string;
|
|
29
|
+
token: string;
|
|
30
|
+
label: string | null;
|
|
31
|
+
last_connected_at: number | null;
|
|
32
|
+
last_ip: string | null;
|
|
33
|
+
connection_count: number;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
let defaultAgentId: string | null = null;
|
|
37
|
+
|
|
38
|
+
for (const pt of tokens ?? []) {
|
|
39
|
+
const existing = await c.env.DB.prepare(
|
|
40
|
+
"SELECT id FROM agents WHERE pairing_token = ?",
|
|
41
|
+
)
|
|
42
|
+
.bind(pt.token)
|
|
43
|
+
.first<{ id: string }>();
|
|
44
|
+
|
|
45
|
+
if (existing) {
|
|
46
|
+
if (!defaultAgentId) defaultAgentId = existing.id;
|
|
47
|
+
stats.skipped++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const agentId = generateId("agt_");
|
|
52
|
+
await c.env.DB.prepare(
|
|
53
|
+
`INSERT INTO agents (id, user_id, name, type, role, pairing_token, capabilities,
|
|
54
|
+
status, last_connected_at, last_ip, connection_count)
|
|
55
|
+
VALUES (?, ?, ?, 'openclaw', 'general', ?, '["chat","streaming","cron","a2ui","media","code_edit","delegate"]',
|
|
56
|
+
'disconnected', ?, ?, ?)`,
|
|
57
|
+
)
|
|
58
|
+
.bind(
|
|
59
|
+
agentId,
|
|
60
|
+
userId,
|
|
61
|
+
pt.label ?? "OpenClaw",
|
|
62
|
+
pt.token,
|
|
63
|
+
pt.last_connected_at,
|
|
64
|
+
pt.last_ip,
|
|
65
|
+
pt.connection_count ?? 0,
|
|
66
|
+
)
|
|
67
|
+
.run();
|
|
68
|
+
|
|
69
|
+
if (!defaultAgentId) defaultAgentId = agentId;
|
|
70
|
+
stats.agentsCreated++;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no pairing tokens exist but user has no agents, skip backfill
|
|
74
|
+
if (!defaultAgentId) {
|
|
75
|
+
const anyAgent = await c.env.DB.prepare(
|
|
76
|
+
"SELECT id FROM agents WHERE user_id = ? LIMIT 1",
|
|
77
|
+
)
|
|
78
|
+
.bind(userId)
|
|
79
|
+
.first<{ id: string }>();
|
|
80
|
+
defaultAgentId = anyAgent?.id ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 2: Backfill channels.default_agent_id (set to first agent if null)
|
|
84
|
+
if (defaultAgentId) {
|
|
85
|
+
const { meta } = await c.env.DB.prepare(
|
|
86
|
+
"UPDATE channels SET default_agent_id = ?, updated_at = unixepoch() WHERE user_id = ? AND default_agent_id IS NULL",
|
|
87
|
+
)
|
|
88
|
+
.bind(defaultAgentId, userId)
|
|
89
|
+
.run();
|
|
90
|
+
stats.channelsUpdated = meta.changes ?? 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Step 3: Backfill messages.sender_agent_id for agent messages (set to default agent)
|
|
94
|
+
if (defaultAgentId) {
|
|
95
|
+
const { meta } = await c.env.DB.prepare(
|
|
96
|
+
"UPDATE messages SET sender_agent_id = ? WHERE user_id = ? AND sender = 'agent' AND sender_agent_id IS NULL",
|
|
97
|
+
)
|
|
98
|
+
.bind(defaultAgentId, userId)
|
|
99
|
+
.run();
|
|
100
|
+
stats.messagesUpdated = meta.changes ?? 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return c.json({
|
|
104
|
+
ok: true,
|
|
105
|
+
defaultAgentId,
|
|
106
|
+
stats,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export { migrateV2 };
|
|
@@ -11,10 +11,10 @@ sessions.get("/", async (c) => {
|
|
|
11
11
|
|
|
12
12
|
// Verify channel ownership
|
|
13
13
|
const channel = await c.env.DB.prepare(
|
|
14
|
-
"SELECT id,
|
|
14
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
15
15
|
)
|
|
16
16
|
.bind(channelId, userId)
|
|
17
|
-
.first<{ id: string;
|
|
17
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
18
18
|
|
|
19
19
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
20
20
|
|
|
@@ -48,10 +48,10 @@ sessions.post("/", async (c) => {
|
|
|
48
48
|
|
|
49
49
|
// Verify channel ownership
|
|
50
50
|
const channel = await c.env.DB.prepare(
|
|
51
|
-
"SELECT id,
|
|
51
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
52
52
|
)
|
|
53
53
|
.bind(channelId, userId)
|
|
54
|
-
.first<{ id: string;
|
|
54
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
55
55
|
|
|
56
56
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
57
57
|
|
|
@@ -67,7 +67,7 @@ sessions.post("/", async (c) => {
|
|
|
67
67
|
|
|
68
68
|
const sessionName = name?.trim() || `Session ${count + 1}`;
|
|
69
69
|
const id = generateId("ses_");
|
|
70
|
-
const sessionKey = `agent:${channel.
|
|
70
|
+
const sessionKey = `agent:${channel.provider_agent_id}:botschat:${userId}:ses:${id}`;
|
|
71
71
|
|
|
72
72
|
await c.env.DB.prepare(
|
|
73
73
|
"INSERT INTO sessions (id, channel_id, user_id, name, session_key) VALUES (?, ?, ?, ?, ?)",
|
|
@@ -10,7 +10,7 @@ const tasks = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
|
|
|
10
10
|
async function pushScheduleToOpenClaw(
|
|
11
11
|
env: Env,
|
|
12
12
|
userId: string,
|
|
13
|
-
task: { taskId: string; name?: string;
|
|
13
|
+
task: { taskId: string; name?: string; providerJobId: string; agentId: string; schedule: string; instructions: string; enabled: boolean; model?: string },
|
|
14
14
|
): Promise<void> {
|
|
15
15
|
try {
|
|
16
16
|
const doId = env.CONNECTION_DO.idFromName(userId);
|
|
@@ -23,7 +23,7 @@ async function pushScheduleToOpenClaw(
|
|
|
23
23
|
type: "task.schedule",
|
|
24
24
|
taskId: task.taskId,
|
|
25
25
|
name: task.name,
|
|
26
|
-
cronJobId: task.
|
|
26
|
+
cronJobId: task.providerJobId,
|
|
27
27
|
agentId: task.agentId,
|
|
28
28
|
schedule: task.schedule,
|
|
29
29
|
instructions: task.instructions,
|
|
@@ -70,24 +70,24 @@ tasks.get("/", async (c) => {
|
|
|
70
70
|
|
|
71
71
|
// Verify channel ownership
|
|
72
72
|
const channel = await c.env.DB.prepare(
|
|
73
|
-
"SELECT id,
|
|
73
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
74
74
|
)
|
|
75
75
|
.bind(channelId, userId)
|
|
76
|
-
.first<{ id: string;
|
|
76
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
77
77
|
|
|
78
78
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
79
79
|
|
|
80
80
|
// Note: schedule, instructions, model are NOT stored in D1.
|
|
81
81
|
// They belong to OpenClaw and are delivered to the frontend via WebSocket task.scan.result.
|
|
82
82
|
const { results } = await c.env.DB.prepare(
|
|
83
|
-
"SELECT id, name, kind,
|
|
83
|
+
"SELECT id, name, kind, provider_job_id, session_key, enabled, created_at, updated_at FROM tasks WHERE channel_id = ? ORDER BY kind ASC, created_at ASC",
|
|
84
84
|
)
|
|
85
85
|
.bind(channelId)
|
|
86
86
|
.all<{
|
|
87
87
|
id: string;
|
|
88
88
|
name: string;
|
|
89
89
|
kind: string;
|
|
90
|
-
|
|
90
|
+
provider_job_id: string | null;
|
|
91
91
|
session_key: string | null;
|
|
92
92
|
enabled: number;
|
|
93
93
|
created_at: number;
|
|
@@ -99,7 +99,7 @@ tasks.get("/", async (c) => {
|
|
|
99
99
|
id: r.id,
|
|
100
100
|
name: r.name,
|
|
101
101
|
kind: r.kind,
|
|
102
|
-
|
|
102
|
+
providerJobId: r.provider_job_id,
|
|
103
103
|
sessionKey: r.session_key,
|
|
104
104
|
enabled: !!r.enabled,
|
|
105
105
|
createdAt: r.created_at,
|
|
@@ -115,10 +115,10 @@ tasks.post("/", async (c) => {
|
|
|
115
115
|
|
|
116
116
|
// Verify channel ownership
|
|
117
117
|
const channel = await c.env.DB.prepare(
|
|
118
|
-
"SELECT id,
|
|
118
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
119
119
|
)
|
|
120
120
|
.bind(channelId, userId)
|
|
121
|
-
.first<{ id: string;
|
|
121
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
122
122
|
|
|
123
123
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
124
124
|
|
|
@@ -137,7 +137,7 @@ tasks.post("/", async (c) => {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const id = generateId("tsk_");
|
|
140
|
-
const agentId = channel.
|
|
140
|
+
const agentId = channel.provider_agent_id;
|
|
141
141
|
|
|
142
142
|
// Build session key based on task kind
|
|
143
143
|
let sessionKey: string;
|
|
@@ -153,10 +153,10 @@ tasks.post("/", async (c) => {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// D1 only stores basic task metadata — schedule/instructions/model belong to OpenClaw.
|
|
156
|
-
//
|
|
156
|
+
// providerJobId is initially null for new background tasks; it will be set
|
|
157
157
|
// when the plugin creates the cron job and sends back a task.schedule.ack.
|
|
158
158
|
await c.env.DB.prepare(
|
|
159
|
-
"INSERT INTO tasks (id, channel_id, name, kind,
|
|
159
|
+
"INSERT INTO tasks (id, channel_id, name, kind, provider_job_id, session_key) VALUES (?, ?, ?, ?, ?, ?)",
|
|
160
160
|
)
|
|
161
161
|
.bind(id, channelId, name.trim(), kind, null, sessionKey)
|
|
162
162
|
.run();
|
|
@@ -166,7 +166,7 @@ tasks.post("/", async (c) => {
|
|
|
166
166
|
await pushScheduleToOpenClaw(c.env, userId, {
|
|
167
167
|
taskId: id,
|
|
168
168
|
name: name.trim(),
|
|
169
|
-
|
|
169
|
+
providerJobId: "",
|
|
170
170
|
agentId,
|
|
171
171
|
schedule: schedule.trim(),
|
|
172
172
|
instructions: instructions?.trim() ?? "",
|
|
@@ -179,7 +179,7 @@ tasks.post("/", async (c) => {
|
|
|
179
179
|
id,
|
|
180
180
|
name: name.trim(),
|
|
181
181
|
kind,
|
|
182
|
-
|
|
182
|
+
providerJobId: null,
|
|
183
183
|
sessionKey,
|
|
184
184
|
enabled: true,
|
|
185
185
|
},
|
|
@@ -195,21 +195,21 @@ tasks.patch("/:taskId", async (c) => {
|
|
|
195
195
|
|
|
196
196
|
// Verify ownership and get task+channel info
|
|
197
197
|
const channel = await c.env.DB.prepare(
|
|
198
|
-
"SELECT id,
|
|
198
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
199
199
|
)
|
|
200
200
|
.bind(channelId, userId)
|
|
201
|
-
.first<{ id: string;
|
|
201
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
202
202
|
|
|
203
203
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
204
204
|
|
|
205
205
|
const existingTask = await c.env.DB.prepare(
|
|
206
|
-
"SELECT id, kind,
|
|
206
|
+
"SELECT id, kind, provider_job_id, enabled FROM tasks WHERE id = ? AND channel_id = ?",
|
|
207
207
|
)
|
|
208
208
|
.bind(taskId, channelId)
|
|
209
209
|
.first<{
|
|
210
210
|
id: string;
|
|
211
211
|
kind: string;
|
|
212
|
-
|
|
212
|
+
provider_job_id: string | null;
|
|
213
213
|
enabled: number;
|
|
214
214
|
}>();
|
|
215
215
|
|
|
@@ -250,13 +250,13 @@ tasks.patch("/:taskId", async (c) => {
|
|
|
250
250
|
// Push OpenClaw-owned fields directly to OpenClaw for background tasks.
|
|
251
251
|
// The client must send ALL OpenClaw fields (schedule, instructions, enabled)
|
|
252
252
|
// together since they are not stored in D1.
|
|
253
|
-
if (existingTask.kind === "background" && existingTask.
|
|
253
|
+
if (existingTask.kind === "background" && existingTask.provider_job_id) {
|
|
254
254
|
const needsPush = body.schedule !== undefined || body.instructions !== undefined || body.enabled !== undefined || body.model !== undefined;
|
|
255
255
|
if (needsPush) {
|
|
256
256
|
await pushScheduleToOpenClaw(c.env, userId, {
|
|
257
257
|
taskId: taskId,
|
|
258
|
-
|
|
259
|
-
agentId: channel.
|
|
258
|
+
providerJobId: existingTask.provider_job_id,
|
|
259
|
+
agentId: channel.provider_agent_id,
|
|
260
260
|
schedule: body.schedule ?? "",
|
|
261
261
|
instructions: body.instructions ?? "",
|
|
262
262
|
enabled: body.enabled ?? !!existingTask.enabled,
|
|
@@ -276,26 +276,26 @@ tasks.post("/:taskId/run", async (c) => {
|
|
|
276
276
|
|
|
277
277
|
// Verify ownership
|
|
278
278
|
const channel = await c.env.DB.prepare(
|
|
279
|
-
"SELECT id,
|
|
279
|
+
"SELECT id, provider_agent_id FROM channels WHERE id = ? AND user_id = ?",
|
|
280
280
|
)
|
|
281
281
|
.bind(channelId, userId)
|
|
282
|
-
.first<{ id: string;
|
|
282
|
+
.first<{ id: string; provider_agent_id: string }>();
|
|
283
283
|
|
|
284
284
|
if (!channel) return c.json({ error: "Channel not found" }, 404);
|
|
285
285
|
|
|
286
286
|
const task = await c.env.DB.prepare(
|
|
287
|
-
"SELECT id, kind,
|
|
287
|
+
"SELECT id, kind, provider_job_id FROM tasks WHERE id = ? AND channel_id = ?",
|
|
288
288
|
)
|
|
289
289
|
.bind(taskId, channelId)
|
|
290
290
|
.first<{
|
|
291
291
|
id: string;
|
|
292
292
|
kind: string;
|
|
293
|
-
|
|
293
|
+
provider_job_id: string | null;
|
|
294
294
|
}>();
|
|
295
295
|
|
|
296
296
|
if (!task) return c.json({ error: "Task not found" }, 404);
|
|
297
297
|
if (task.kind !== "background") return c.json({ error: "Only background tasks can be triggered" }, 400);
|
|
298
|
-
if (!task.
|
|
298
|
+
if (!task.provider_job_id) return c.json({ error: "Task has no associated cron job" }, 400);
|
|
299
299
|
|
|
300
300
|
// Send a task.run message to OpenClaw via ConnectionDO.
|
|
301
301
|
// Instructions and model are not included — the plugin reads them
|
|
@@ -309,8 +309,8 @@ tasks.post("/:taskId/run", async (c) => {
|
|
|
309
309
|
headers: { "Content-Type": "application/json" },
|
|
310
310
|
body: JSON.stringify({
|
|
311
311
|
type: "task.run",
|
|
312
|
-
cronJobId: task.
|
|
313
|
-
agentId: channel.
|
|
312
|
+
cronJobId: task.provider_job_id,
|
|
313
|
+
agentId: channel.provider_agent_id,
|
|
314
314
|
}),
|
|
315
315
|
}),
|
|
316
316
|
);
|
|
@@ -343,10 +343,10 @@ tasks.delete("/:taskId", async (c) => {
|
|
|
343
343
|
|
|
344
344
|
// Get task to check if it's background (need to delete CronJob)
|
|
345
345
|
const task = await c.env.DB.prepare(
|
|
346
|
-
"SELECT kind,
|
|
346
|
+
"SELECT kind, provider_job_id FROM tasks WHERE id = ? AND channel_id = ?",
|
|
347
347
|
)
|
|
348
348
|
.bind(taskId, channelId)
|
|
349
|
-
.first<{ kind: string;
|
|
349
|
+
.first<{ kind: string; provider_job_id: string | null }>();
|
|
350
350
|
|
|
351
351
|
await c.env.DB.prepare(
|
|
352
352
|
"DELETE FROM tasks WHERE id = ? AND channel_id = ?",
|
|
@@ -358,15 +358,15 @@ tasks.delete("/:taskId", async (c) => {
|
|
|
358
358
|
await c.env.DB.prepare("DELETE FROM jobs WHERE task_id = ?").bind(taskId).run();
|
|
359
359
|
|
|
360
360
|
// Push delete to OpenClaw for background tasks
|
|
361
|
-
if (task?.kind === "background" && task.
|
|
361
|
+
if (task?.kind === "background" && task.provider_job_id) {
|
|
362
362
|
// Record the deletion so task.scan won't re-create the task
|
|
363
363
|
await c.env.DB.prepare(
|
|
364
364
|
"INSERT OR IGNORE INTO deleted_cron_jobs (cron_job_id, user_id) VALUES (?, ?)",
|
|
365
365
|
)
|
|
366
|
-
.bind(task.
|
|
366
|
+
.bind(task.provider_job_id, userId)
|
|
367
367
|
.run();
|
|
368
368
|
|
|
369
|
-
await pushDeleteToOpenClaw(c.env, userId, task.
|
|
369
|
+
await pushDeleteToOpenClaw(c.env, userId, task.provider_job_id);
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
return c.json({ ok: true });
|
|
@@ -30,6 +30,7 @@ export declare const botschatPlugin: {
|
|
|
30
30
|
};
|
|
31
31
|
readonly reload: {
|
|
32
32
|
readonly configPrefixes: readonly ["channels.botschat"];
|
|
33
|
+
readonly noopPrefixes: readonly ["plugins.installs.botschat"];
|
|
33
34
|
};
|
|
34
35
|
readonly config: {
|
|
35
36
|
readonly listAccountIds: (cfg: unknown) => string[];
|
|
@@ -56,6 +57,15 @@ export declare const botschatPlugin: {
|
|
|
56
57
|
};
|
|
57
58
|
readonly outbound: {
|
|
58
59
|
readonly deliveryMode: "direct";
|
|
60
|
+
readonly resolveTarget: ({ to }: {
|
|
61
|
+
to?: string;
|
|
62
|
+
allowFrom?: string[];
|
|
63
|
+
accountId?: string | null;
|
|
64
|
+
mode?: string;
|
|
65
|
+
}) => {
|
|
66
|
+
ok: true;
|
|
67
|
+
to: string;
|
|
68
|
+
};
|
|
59
69
|
readonly sendText: (ctx: {
|
|
60
70
|
to: string;
|
|
61
71
|
text: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAuC,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAoMrD,eAAO,MAAM,cAAc;;;;;;;;;;;;;4BAeqB,MAAM,EAAE;;;;;;;;;;;;wCAc/B,MAAM,eAAe,MAAM
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
|