ocean-crew 0.0.0-stage → 0.4.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 +21 -0
- package/README.md +156 -2
- package/THIRD_PARTY_NOTICES.md +159 -0
- package/client.js +31389 -0
- package/cordis.patch.yml +6 -0
- package/dist/host/chat/btw.js +436 -0
- package/dist/host/chat/channel.js +1241 -0
- package/dist/host/chat/chat-index.js +344 -0
- package/dist/host/chat/delivery.js +231 -0
- package/dist/host/chat/feed.js +165 -0
- package/dist/host/chat/handoffs.js +625 -0
- package/dist/host/chat/parts.js +2033 -0
- package/dist/host/chat/requests.js +155 -0
- package/dist/host/chat/resume.js +364 -0
- package/dist/host/chat/side.js +457 -0
- package/dist/host/chat/steer.js +346 -0
- package/dist/host/chat/turns.js +1457 -0
- package/dist/host/connectors/catalog.js +331 -0
- package/dist/host/connectors/gateway.js +339 -0
- package/dist/host/connectors/index.js +1235 -0
- package/dist/host/connectors/mail/index.js +912 -0
- package/dist/host/connectors/mail/presets.js +53 -0
- package/dist/host/connectors/mail/transport.js +519 -0
- package/dist/host/connectors/oauth.js +337 -0
- package/dist/host/core/constants.js +123 -0
- package/dist/host/core/diagnostics.js +310 -0
- package/dist/host/core/dsh-update.js +212 -0
- package/dist/host/core/events.js +16 -0
- package/dist/host/core/files.js +37 -0
- package/dist/host/core/folders.js +283 -0
- package/dist/host/core/home-lock.js +137 -0
- package/dist/host/core/links.js +52 -0
- package/dist/host/core/runtime.js +5 -0
- package/dist/host/core/store.js +662 -0
- package/dist/host/core/text.js +59 -0
- package/dist/host/core/thinking.js +60 -0
- package/dist/host/core/time-context.js +69 -0
- package/dist/host/core/update.js +466 -0
- package/dist/host/groups/calls.js +684 -0
- package/dist/host/groups/index.js +79 -0
- package/dist/host/groups/log.js +334 -0
- package/dist/host/groups/prompt.js +124 -0
- package/dist/host/groups/relay.js +489 -0
- package/dist/host/groups/round.js +992 -0
- package/dist/host/groups/tasks.js +217 -0
- package/dist/host/groups/tools.js +478 -0
- package/dist/host/groups/types.js +8 -0
- package/dist/host/im/format.js +228 -0
- package/dist/host/im/index.js +2461 -0
- package/dist/host/im/platforms/feishu.js +1134 -0
- package/dist/host/im/platforms/index.js +6 -0
- package/dist/host/im/platforms/media-type.js +70 -0
- package/dist/host/im/platforms/weixin.js +1117 -0
- package/dist/host/im/reaction.js +51 -0
- package/dist/host/im/supervisor.js +56 -0
- package/dist/host/team/agents.js +161 -0
- package/dist/host/team/models.js +427 -0
- package/dist/host/team/permissions.js +263 -0
- package/dist/host/team/privacy.js +108 -0
- package/dist/host/team/prompt.js +86 -0
- package/dist/host/team/providers.js +461 -0
- package/dist/host/team/roster.js +1011 -0
- package/dist/host/team/tools.js +1638 -0
- package/dist/host/work/approvals.js +290 -0
- package/dist/host/work/avatar-library.js +155 -0
- package/dist/host/work/avatar.js +132 -0
- package/dist/host/work/capabilities.js +862 -0
- package/dist/host/work/companion.js +306 -0
- package/dist/host/work/goal-skills.js +91 -0
- package/dist/host/work/goals.js +1337 -0
- package/dist/host/work/images.js +283 -0
- package/dist/host/work/learning.js +1021 -0
- package/dist/host/work/memory-pending.js +108 -0
- package/dist/host/work/memory.js +3237 -0
- package/dist/host/work/notify.js +146 -0
- package/dist/host/work/reset.js +43 -0
- package/dist/host/work/schedules.js +309 -0
- package/dist/host/work/secrets.js +702 -0
- package/dist/host/work/steward.js +166 -0
- package/dist/host/work/tts.js +192 -0
- package/dist/host/work/usage.js +84 -0
- package/dist/index.js +137 -0
- package/dist/shared/api.js +32 -0
- package/dist/shared/companion.js +110 -0
- package/dist/shared/config.js +3 -0
- package/dist/shared/coordination.js +6 -0
- package/dist/shared/group-replies.js +95 -0
- package/dist/shared/im-platforms.js +35 -0
- package/dist/shared/im.js +3 -0
- package/dist/shared/mentions.js +85 -0
- package/dist/shared/state.js +25 -0
- package/package.json +81 -5
|
@@ -0,0 +1,1241 @@
|
|
|
1
|
+
// Channel: the `POST /api/bot` connection the desktop shell uses. A buffered JSON request
|
|
2
|
+
// names one endpoint in a closed set and always receives HTTP 200 carrying either
|
|
3
|
+
// `{ ok: true, value }` or `{ ok: false, error: { code, message, details } }`. The code is
|
|
4
|
+
// the machine field the client branches on; the message is text a user may read.
|
|
5
|
+
//
|
|
6
|
+
// The channel itself answers the roster, session, group, card and preference endpoints the
|
|
7
|
+
// shell needs to draw and drive a team. Endpoints owned by another host module are wired
|
|
8
|
+
// dynamically: each answers from the runtime member its module publishes and keeps
|
|
9
|
+
// `bot/unavailable` (naming the missing member) until that module is installed, so one
|
|
10
|
+
// channel serves every combination of installed and missing modules. There is no fake
|
|
11
|
+
// success and no silent omission. A second host without the team lock still answers READS,
|
|
12
|
+
// and every other endpoint keeps its `bot/read-only` refusal.
|
|
13
|
+
//
|
|
14
|
+
// - `run-stats` reads DSH's own session projection;
|
|
15
|
+
// - `view-reply` needs the parts/cards module to publish `validateViewAnswer` and `viewReply`;
|
|
16
|
+
// until then it refuses with `bot/unavailable` rather than queueing an unvalidated answer.
|
|
17
|
+
// for the optional member, which this build treats as "the module has not landed".
|
|
18
|
+
// - `folderEndpoint` receives the request signal, so a folder pick is abortable as before.
|
|
19
|
+
// - `browser-*`: a host without the browser module refuses `bot/unavailable`; a host whose
|
|
20
|
+
// default branch did; an unknown suffix is still the module's call.
|
|
21
|
+
// - `archivesView`, `capabilityLibraryView`, `secretsView`, `imView`, the
|
|
22
|
+
// live views and the activity/work views are optional runtime callers: when their module
|
|
23
|
+
// is absent the field keeps its honest empty value, and only an endpoint that needs the
|
|
24
|
+
// module refuses with `bot/unavailable`.
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
27
|
+
import { COLORS, EXTRA_COLORS, REPLY_KIND, ROUTE } from "../core/constants.js";
|
|
28
|
+
const LANGUAGES = { zh: "Chinese (简体中文)", en: "English" };
|
|
29
|
+
const RUN_STATS = ["sessionStats", "tokenUsage", "contextPressure"];
|
|
30
|
+
/** The read endpoints a host without the team lock still answers (docs/contracts.md). */
|
|
31
|
+
export const READS = new Set([
|
|
32
|
+
"state",
|
|
33
|
+
"models",
|
|
34
|
+
"efforts",
|
|
35
|
+
"providers",
|
|
36
|
+
"provider-models",
|
|
37
|
+
"earlier",
|
|
38
|
+
"chat",
|
|
39
|
+
"run-stats",
|
|
40
|
+
"room-progress",
|
|
41
|
+
"exchange",
|
|
42
|
+
"exchange-text",
|
|
43
|
+
"file-text",
|
|
44
|
+
"usage",
|
|
45
|
+
"companion-view",
|
|
46
|
+
"schedules",
|
|
47
|
+
"memory-view",
|
|
48
|
+
"diagnostics",
|
|
49
|
+
"diagnostics-log",
|
|
50
|
+
"diagnostics-clear",
|
|
51
|
+
"update-check",
|
|
52
|
+
"update-install",
|
|
53
|
+
"dsh-update-install",
|
|
54
|
+
"repo-stars",
|
|
55
|
+
"approval-answer",
|
|
56
|
+
"thinking",
|
|
57
|
+
"avatar-library",
|
|
58
|
+
]);
|
|
59
|
+
/** The endpoints the channel itself answers. Every other known name is deferred or unknown. */
|
|
60
|
+
export const CHANNEL_ENDPOINTS = [
|
|
61
|
+
"state",
|
|
62
|
+
"models",
|
|
63
|
+
"efforts",
|
|
64
|
+
"set-prefs",
|
|
65
|
+
"exchange",
|
|
66
|
+
"exchange-text",
|
|
67
|
+
"file-text",
|
|
68
|
+
"room-progress",
|
|
69
|
+
"create-bot",
|
|
70
|
+
"update-bot",
|
|
71
|
+
"duplicate-bot",
|
|
72
|
+
"delete-bot",
|
|
73
|
+
"set-main",
|
|
74
|
+
"set-flags",
|
|
75
|
+
"resume",
|
|
76
|
+
"create-room",
|
|
77
|
+
"update-room",
|
|
78
|
+
"delete-room",
|
|
79
|
+
"send",
|
|
80
|
+
"answer-question",
|
|
81
|
+
"dismiss-question",
|
|
82
|
+
"dismiss-view",
|
|
83
|
+
"run-stats",
|
|
84
|
+
"stop-member-work",
|
|
85
|
+
"stop-room-work",
|
|
86
|
+
];
|
|
87
|
+
/**
|
|
88
|
+
* Endpoints another host module answers: wired dynamically. Each answers from the runtime
|
|
89
|
+
* member its module publishes and stays `bot/unavailable` until that module is installed,
|
|
90
|
+
* so one channel serves every combination of installed and missing modules.
|
|
91
|
+
*/
|
|
92
|
+
export const MODULE_ENDPOINTS = [
|
|
93
|
+
"providers",
|
|
94
|
+
"provider-key",
|
|
95
|
+
"provider-add",
|
|
96
|
+
"provider-models",
|
|
97
|
+
"provider-remove",
|
|
98
|
+
"thinking",
|
|
99
|
+
"earlier",
|
|
100
|
+
"chat",
|
|
101
|
+
"bot-new-part",
|
|
102
|
+
"bot-open-part",
|
|
103
|
+
"usage",
|
|
104
|
+
// folders and archives (folders.ts answers them)
|
|
105
|
+
"folders",
|
|
106
|
+
"folder-pick",
|
|
107
|
+
"folder-list",
|
|
108
|
+
"team-workspace",
|
|
109
|
+
"private-release",
|
|
110
|
+
"reset-team",
|
|
111
|
+
"avatar-library",
|
|
112
|
+
"avatar-import",
|
|
113
|
+
"avatar-delete",
|
|
114
|
+
"add-agent",
|
|
115
|
+
"agent-session",
|
|
116
|
+
"agent-adopt",
|
|
117
|
+
"agent-archive",
|
|
118
|
+
"agent-rename",
|
|
119
|
+
"remove-agent",
|
|
120
|
+
"approval-answer",
|
|
121
|
+
"goal-draft",
|
|
122
|
+
"goal-control",
|
|
123
|
+
"room-budget",
|
|
124
|
+
"companion-view",
|
|
125
|
+
"companion-set",
|
|
126
|
+
"companion-audio",
|
|
127
|
+
"tts-settings",
|
|
128
|
+
"tts-set",
|
|
129
|
+
"tts-voices",
|
|
130
|
+
"tts-test",
|
|
131
|
+
"secret-set",
|
|
132
|
+
"secret-allow",
|
|
133
|
+
"secret-cancel",
|
|
134
|
+
"secret-update",
|
|
135
|
+
"secret-delete",
|
|
136
|
+
"connector-connect",
|
|
137
|
+
"connector-disconnect",
|
|
138
|
+
"connector-retry",
|
|
139
|
+
"connector-sign-in-code",
|
|
140
|
+
"connector-cancel",
|
|
141
|
+
"connector-client",
|
|
142
|
+
"connector-set",
|
|
143
|
+
"connector-add",
|
|
144
|
+
"connector-remove",
|
|
145
|
+
"connector-import-scan",
|
|
146
|
+
"connector-import",
|
|
147
|
+
"connector-request-cancel",
|
|
148
|
+
"mail-account-add",
|
|
149
|
+
"mail-account-remove",
|
|
150
|
+
"mail-rule-set",
|
|
151
|
+
"mail-send-answer",
|
|
152
|
+
"mail-sign-in-code",
|
|
153
|
+
"capability-save",
|
|
154
|
+
"capability-delete",
|
|
155
|
+
"capability-approve",
|
|
156
|
+
"bot-capability",
|
|
157
|
+
"schedules",
|
|
158
|
+
"schedule-create",
|
|
159
|
+
"schedule-delete",
|
|
160
|
+
"memory-view",
|
|
161
|
+
"memory-ask",
|
|
162
|
+
"memory-forget",
|
|
163
|
+
"memory-history",
|
|
164
|
+
"memory-restore",
|
|
165
|
+
"learning-answer",
|
|
166
|
+
"view-reply",
|
|
167
|
+
"btw-delete",
|
|
168
|
+
"side-new",
|
|
169
|
+
"side-ask",
|
|
170
|
+
"side-cancel",
|
|
171
|
+
"side-close",
|
|
172
|
+
"update-check",
|
|
173
|
+
"update-install",
|
|
174
|
+
"dsh-update-install",
|
|
175
|
+
"repo-stars",
|
|
176
|
+
"diagnostics",
|
|
177
|
+
"diagnostics-log",
|
|
178
|
+
"diagnostics-clear",
|
|
179
|
+
];
|
|
180
|
+
// Endpoint families whose module answers the whole group through one `*Endpoint` member.
|
|
181
|
+
const SECRET_ENDPOINTS = [
|
|
182
|
+
"secret-set",
|
|
183
|
+
"secret-allow",
|
|
184
|
+
"secret-cancel",
|
|
185
|
+
"secret-update",
|
|
186
|
+
"secret-delete",
|
|
187
|
+
];
|
|
188
|
+
const CONNECTOR_MAIL_ENDPOINTS = [
|
|
189
|
+
"connector-connect",
|
|
190
|
+
"connector-disconnect",
|
|
191
|
+
"connector-retry",
|
|
192
|
+
"connector-sign-in-code",
|
|
193
|
+
"connector-cancel",
|
|
194
|
+
"connector-client",
|
|
195
|
+
"connector-set",
|
|
196
|
+
"connector-add",
|
|
197
|
+
"connector-remove",
|
|
198
|
+
"connector-import-scan",
|
|
199
|
+
"connector-import",
|
|
200
|
+
"connector-request-cancel",
|
|
201
|
+
"mail-account-add",
|
|
202
|
+
"mail-account-remove",
|
|
203
|
+
"mail-rule-set",
|
|
204
|
+
"mail-send-answer",
|
|
205
|
+
"mail-sign-in-code",
|
|
206
|
+
];
|
|
207
|
+
const CAPABILITY_ENDPOINTS = [
|
|
208
|
+
"capability-save",
|
|
209
|
+
"capability-delete",
|
|
210
|
+
"capability-approve",
|
|
211
|
+
"bot-capability",
|
|
212
|
+
];
|
|
213
|
+
const SCHEDULE_ENDPOINTS = ["schedules", "schedule-create", "schedule-delete"];
|
|
214
|
+
const MEMORY_ENDPOINTS = [
|
|
215
|
+
"memory-view",
|
|
216
|
+
"memory-ask",
|
|
217
|
+
"memory-forget",
|
|
218
|
+
"memory-history",
|
|
219
|
+
"memory-restore",
|
|
220
|
+
];
|
|
221
|
+
/** IM endpoints, one `imEndpoint(endpoint, body)` module call each. */
|
|
222
|
+
export const IM_ENDPOINTS = [
|
|
223
|
+
"im-add",
|
|
224
|
+
"im-update",
|
|
225
|
+
"im-remove",
|
|
226
|
+
"im-pair",
|
|
227
|
+
"im-unpair",
|
|
228
|
+
"im-chat",
|
|
229
|
+
"im-retry",
|
|
230
|
+
"im-login-start",
|
|
231
|
+
"im-login-wait",
|
|
232
|
+
];
|
|
233
|
+
/** Every IM endpoint; each refuses by missing member until its module loads. */
|
|
234
|
+
export const SERVICE_ENDPOINTS = [...IM_ENDPOINTS];
|
|
235
|
+
/**
|
|
236
|
+
* Endpoints still only placeholders. Every known endpoint is wired in `endpoints` and
|
|
237
|
+
* refuses from `missingMember` until its module publishes the member; the export stays so a
|
|
238
|
+
* module can register its surface here first.
|
|
239
|
+
*/
|
|
240
|
+
export const UNAVAILABLE_ENDPOINTS = new Map();
|
|
241
|
+
const invalid = (message) => Object.assign(new Error(message), { code: "bot/invalid" });
|
|
242
|
+
const unavailable = (what) => Object.assign(new Error(`${what} is not available in this build yet.`), { code: "bot/unavailable" });
|
|
243
|
+
const stale = (message) => Object.assign(new Error(message), { code: "bot/stale" });
|
|
244
|
+
/** An endpoint whose host module has not been installed. */
|
|
245
|
+
const missingMember = (endpoint, member) => Object.assign(new Error(`${endpoint}: the ${member} host member is not installed in this build.`), {
|
|
246
|
+
code: "bot/unavailable",
|
|
247
|
+
});
|
|
248
|
+
// Programming and file-system errors are Ocean Crew's own faults and are logged with their
|
|
249
|
+
// stack; every other error is a refusal whose message is meant for the user.
|
|
250
|
+
const isInternal = (error) => {
|
|
251
|
+
const code = error?.code;
|
|
252
|
+
return (error instanceof TypeError ||
|
|
253
|
+
error instanceof ReferenceError ||
|
|
254
|
+
error instanceof RangeError ||
|
|
255
|
+
error instanceof SyntaxError ||
|
|
256
|
+
(typeof code === "string" && /^E[A-Z_]+$/.test(code)));
|
|
257
|
+
};
|
|
258
|
+
/** The machine code a thrown error maps to (docs/contracts.md - `bot/` error codes). */
|
|
259
|
+
export function errorCode(error) {
|
|
260
|
+
const code = error?.code;
|
|
261
|
+
if (typeof code === "string" && code.startsWith("bot/"))
|
|
262
|
+
return code;
|
|
263
|
+
return isInternal(error) ? "bot/internal" : "bot/refused";
|
|
264
|
+
}
|
|
265
|
+
const ok = (value) => ({ ok: true, value });
|
|
266
|
+
const fail = (message, code = "bot/error") => ({
|
|
267
|
+
ok: false,
|
|
268
|
+
error: { code, message, details: {} },
|
|
269
|
+
});
|
|
270
|
+
const own = (table, key) => table != null && Object.hasOwn(table, key) ? table[key] : undefined;
|
|
271
|
+
export function install(rt) {
|
|
272
|
+
const { ctx } = rt;
|
|
273
|
+
const state = rt.state;
|
|
274
|
+
let uiLocale;
|
|
275
|
+
// The DSH interface language of the last browser that asked for the team.
|
|
276
|
+
const uiLanguage = () => uiLocale === undefined ? undefined : (LANGUAGES[uiLocale.split("-")[0].toLowerCase()] ?? uiLocale);
|
|
277
|
+
/** An optional cross-module function; undefined until its module is installed. */
|
|
278
|
+
const fn = (name) => {
|
|
279
|
+
const value = rt[name];
|
|
280
|
+
return typeof value === "function" ? value : undefined;
|
|
281
|
+
};
|
|
282
|
+
const sync = (name, ...args) => fn(name)?.(...args);
|
|
283
|
+
const call = async (name, ...args) => {
|
|
284
|
+
const wanted = fn(name);
|
|
285
|
+
if (wanted === undefined)
|
|
286
|
+
throw unavailable(`The ${name} host module`);
|
|
287
|
+
return await wanted(...args);
|
|
288
|
+
};
|
|
289
|
+
const record = async (name, ...args) => {
|
|
290
|
+
const wanted = fn(name);
|
|
291
|
+
if (wanted === undefined)
|
|
292
|
+
return;
|
|
293
|
+
await wanted(...args);
|
|
294
|
+
};
|
|
295
|
+
/** A member an endpoint needs; a missing one refuses the endpoint, not the host. */
|
|
296
|
+
const member = (endpoint, name) => {
|
|
297
|
+
const found = fn(name);
|
|
298
|
+
if (found === undefined)
|
|
299
|
+
throw missingMember(endpoint, name);
|
|
300
|
+
return found;
|
|
301
|
+
};
|
|
302
|
+
const callMember = async (endpoint, name, ...args) => await member(endpoint, name)(...args);
|
|
303
|
+
const liveMap = () => rt.live;
|
|
304
|
+
/** The diagnostics object; it publishes `report`, `logText` and `clear` together. */
|
|
305
|
+
const diagnosticsService = (endpoint, method) => {
|
|
306
|
+
const service = rt.diagnostics;
|
|
307
|
+
if (typeof service?.[method] !== "function")
|
|
308
|
+
throw missingMember(endpoint, `diagnostics.${method}`);
|
|
309
|
+
return service;
|
|
310
|
+
};
|
|
311
|
+
const botOf = (id) => (id === undefined ? undefined : fn("botOf")?.(id));
|
|
312
|
+
const roomOf = (id) => (id === undefined ? undefined : fn("roomOf")?.(id));
|
|
313
|
+
const chatOf = (id) => fn("chatOf")?.(id) ?? id;
|
|
314
|
+
const selfOf = (id) => fn("selfOf")?.(id);
|
|
315
|
+
const sessionOfRoom = (room) => fn("sessionOfRoom")?.(room) ?? room.sessionId ?? room.id;
|
|
316
|
+
const pastOwner = (ref) => rt.pastOwners?.get(ref);
|
|
317
|
+
const roomProgress = () => rt.roomProgress;
|
|
318
|
+
const prefs = () => state.prefs;
|
|
319
|
+
const load = async () => {
|
|
320
|
+
const wanted = fn("load");
|
|
321
|
+
if (wanted !== undefined)
|
|
322
|
+
await wanted();
|
|
323
|
+
};
|
|
324
|
+
const save = async () => {
|
|
325
|
+
const wanted = fn("save");
|
|
326
|
+
if (wanted !== undefined)
|
|
327
|
+
await wanted();
|
|
328
|
+
};
|
|
329
|
+
// Models come from DSH, not state.json, so a provider added in Harness settings changes
|
|
330
|
+
// the answer without a new revision; the serialized pair is what `state` compares.
|
|
331
|
+
const modelsKey = () => {
|
|
332
|
+
const catalog = sync("cachedCatalog");
|
|
333
|
+
const entries = Array.isArray(catalog) ? catalog : [];
|
|
334
|
+
const refs = entries.map((entry) => `${entry?.ref ?? ""}${entry?.needsKey === true ? "!" : ""}${entry?.reasoning ? `:${JSON.stringify(entry.reasoning)}` : ""}`);
|
|
335
|
+
return `${refs.join(",")}|${sync("cachedDefault") ?? ""}`;
|
|
336
|
+
};
|
|
337
|
+
// Fields with no change counter of their own: the serialized answer is the detector, so
|
|
338
|
+
// an unchanged poll gets the key back instead of the fields again. The key carries a
|
|
339
|
+
// per-run prefix so a window that outlives this host cannot match a stale key.
|
|
340
|
+
const liveRun = randomUUID();
|
|
341
|
+
let liveSeq = 0;
|
|
342
|
+
let liveCache;
|
|
343
|
+
const live = () => ({
|
|
344
|
+
activity: sync("activityView") ?? {},
|
|
345
|
+
work: {
|
|
346
|
+
bots: sync("workView") ?? {},
|
|
347
|
+
rooms: sync("roomWorkView") ?? {},
|
|
348
|
+
approvals: sync("approvalsView") ?? [],
|
|
349
|
+
waiting: sync("waitingView") ?? {},
|
|
350
|
+
turns: sync("turnsView") ?? {},
|
|
351
|
+
steered: sync("steeredView") ?? {},
|
|
352
|
+
tasks: sync("tasksView") ?? [],
|
|
353
|
+
},
|
|
354
|
+
memoryNews: sync("memoryNews") ?? {},
|
|
355
|
+
memoryReviews: sync("memoryReviews") ?? {},
|
|
356
|
+
memoryReviewStates: sync("memoryReviewStates") ?? {},
|
|
357
|
+
learning: sync("learningView") ?? {},
|
|
358
|
+
learningSettled: sync("learningSettledView") ?? {},
|
|
359
|
+
connectors: sync("connectorsView") ?? {},
|
|
360
|
+
capabilityStatuses: sync("capabilityStatusesView") ?? {},
|
|
361
|
+
imStatus: sync("imStatusView") ?? {},
|
|
362
|
+
imVia: sync("imViaView") ?? {},
|
|
363
|
+
computerLive: sync("computerLive") ?? {},
|
|
364
|
+
});
|
|
365
|
+
const liveAnswer = () => {
|
|
366
|
+
const value = live();
|
|
367
|
+
const json = JSON.stringify(value);
|
|
368
|
+
if (liveCache?.json === json)
|
|
369
|
+
return liveCache;
|
|
370
|
+
liveCache = { key: `${liveRun}:${++liveSeq}`, value, json };
|
|
371
|
+
return liveCache;
|
|
372
|
+
};
|
|
373
|
+
// What the browser renders. a field
|
|
374
|
+
const view = () => {
|
|
375
|
+
const companions = sync("companionMap") ?? {};
|
|
376
|
+
const bots = Object.values(state.bots).map((bot) => {
|
|
377
|
+
const { segments, kickoff, ...rest } = bot;
|
|
378
|
+
return {
|
|
379
|
+
...rest,
|
|
380
|
+
sessionId: chatOf(bot.id),
|
|
381
|
+
parts: (segments ?? []).map((segment) => segment.sessionId),
|
|
382
|
+
...(kickoff ? { waiting: true } : {}),
|
|
383
|
+
...(companions[bot.id] === undefined ? {} : { companion: companions[bot.id] }),
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
const rooms = Object.values(state.rooms).map((room) => {
|
|
387
|
+
const { log, sessions, segments, parts: ownParts, ...rest } = room;
|
|
388
|
+
// The log is rendered as `preview`; the read marks and the pause flag stay internal.
|
|
389
|
+
const roomView = { ...rest };
|
|
390
|
+
delete roomView.seen;
|
|
391
|
+
delete roomView.paused;
|
|
392
|
+
const parts = [
|
|
393
|
+
...Object.values(sessions ?? {}),
|
|
394
|
+
...Object.values(segments ?? {})
|
|
395
|
+
.flat()
|
|
396
|
+
.map((segment) => segment.sessionId),
|
|
397
|
+
...(ownParts ?? []).map((segment) => segment.sessionId),
|
|
398
|
+
];
|
|
399
|
+
const last = (Array.isArray(log) ? log : []).filter((line) => line.kind === undefined).at(-1);
|
|
400
|
+
return {
|
|
401
|
+
...roomView,
|
|
402
|
+
sessionId: sessionOfRoom(room),
|
|
403
|
+
ownParts: (ownParts ?? []).map((segment) => segment.sessionId),
|
|
404
|
+
parts,
|
|
405
|
+
...(last ? { preview: last.who === "User" ? last.text : `${String(last.who)}:${last.text ?? ""}` } : {}),
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
revision: state.revision,
|
|
410
|
+
workspaceId: state.workspaceId ?? null,
|
|
411
|
+
// `mainBotId` is the first Main Bot, the sidebar's tile; the list keeps the order.
|
|
412
|
+
mainBotId: state.mainBotIds[0] ?? null,
|
|
413
|
+
mainBotIds: state.mainBotIds,
|
|
414
|
+
bots,
|
|
415
|
+
rooms,
|
|
416
|
+
// Deleted private Bots whose folder, memory and chats stay closed to the others.
|
|
417
|
+
privateArchives: sync("archivesView") ?? [],
|
|
418
|
+
exchangeTurns: state.exchangeTurns,
|
|
419
|
+
questions: state.questions,
|
|
420
|
+
views: state.views,
|
|
421
|
+
closedCards: state.closedCards,
|
|
422
|
+
// Aside answers and side chats of each Bot's current part, once parts is installed.
|
|
423
|
+
btwCards: sync("btwView") ?? {},
|
|
424
|
+
sideChats: sync("sideView") ?? {},
|
|
425
|
+
agents: Object.values(state.agents),
|
|
426
|
+
// Key names and who may use them; the values never leave the host.
|
|
427
|
+
secrets: sync("secretsView") ?? [],
|
|
428
|
+
capabilities: sync("capabilityLibraryView") ?? [],
|
|
429
|
+
secretRequests: state.secretRequests ?? {},
|
|
430
|
+
connectorRequests: state.connectorRequests ?? {},
|
|
431
|
+
mailRequests: state.mailRequests ?? {},
|
|
432
|
+
colors: [...COLORS, ...EXTRA_COLORS],
|
|
433
|
+
models: sync("cachedCatalog") ?? [],
|
|
434
|
+
defaultModel: sync("cachedDefault") ?? null,
|
|
435
|
+
permissions: sync("permissionCatalog") ?? null,
|
|
436
|
+
modelsKey: modelsKey(),
|
|
437
|
+
prefs: state.prefs,
|
|
438
|
+
// False when config.memory turns memory off: no Memory row or panel.
|
|
439
|
+
memory: rt.memoryEnabled !== false,
|
|
440
|
+
// Set while another dsh host holds the team: why this one cannot change it.
|
|
441
|
+
readOnly: sync("readOnly") ?? null,
|
|
442
|
+
im: sync("imView") ?? { connections: [], platforms: [] },
|
|
443
|
+
computer: sync("computerView") ?? { enabled: false },
|
|
444
|
+
};
|
|
445
|
+
};
|
|
446
|
+
// either resolves to its current part. The request id lands on the message's source.
|
|
447
|
+
async function sendUser({ sessionId: ref, content, requestId = randomUUID(), }) {
|
|
448
|
+
const past = pastOwner(ref);
|
|
449
|
+
const room = roomOf(ref);
|
|
450
|
+
const bot = botOf(ref);
|
|
451
|
+
const sessionId = bot !== undefined
|
|
452
|
+
? chatOf(ref)
|
|
453
|
+
: room !== undefined
|
|
454
|
+
? sessionOfRoom(room)
|
|
455
|
+
: past?.roomId === undefined && past?.botId !== undefined
|
|
456
|
+
? chatOf(past.botId)
|
|
457
|
+
: past?.roomId !== undefined
|
|
458
|
+
? sessionOfRoom(roomOf(past.roomId))
|
|
459
|
+
: ref;
|
|
460
|
+
await record("ensureCapabilities", sessionId);
|
|
461
|
+
const controller = ctx.sessionController;
|
|
462
|
+
if (typeof controller?.prompt !== "function")
|
|
463
|
+
throw unavailable("Sending a message");
|
|
464
|
+
await controller.prompt({ requestId, sessionId, mode: "queue", content }, AbortSignal.timeout(30_000));
|
|
465
|
+
}
|
|
466
|
+
// The user's answer to an `ask_user` card. The card is claimed before queueing, so a
|
|
467
|
+
// second answer is refused; a failed queue puts it back.
|
|
468
|
+
function answerReply(question, answer) {
|
|
469
|
+
const record = question;
|
|
470
|
+
const options = Array.isArray(record.options) ? record.options : [];
|
|
471
|
+
const labels = new Set(options.map((option) => option?.label).filter((label) => typeof label === "string"));
|
|
472
|
+
const given = (answer ?? {});
|
|
473
|
+
const picked = (Array.isArray(given.selected) ? given.selected : []).filter((label) => typeof label === "string" && labels.has(label));
|
|
474
|
+
const selected = record.multiSelect === true ? [...new Set(picked)] : picked.slice(0, 1);
|
|
475
|
+
const custom = typeof given.custom === "string" ? given.custom.trim() : "";
|
|
476
|
+
if (selected.length === 0 && custom === "")
|
|
477
|
+
return undefined;
|
|
478
|
+
const callId = typeof record.callId === "string" ? record.callId : question.id;
|
|
479
|
+
const asked = {
|
|
480
|
+
id: question.id,
|
|
481
|
+
question: record.question ?? record.title,
|
|
482
|
+
...(record.detail ? { detail: record.detail } : {}),
|
|
483
|
+
options: record.options,
|
|
484
|
+
multiSelect: record.multiSelect === true,
|
|
485
|
+
};
|
|
486
|
+
const givenAnswer = { id: question.id, selected, ...(custom !== "" ? { custom } : {}) };
|
|
487
|
+
return {
|
|
488
|
+
text: JSON.stringify({
|
|
489
|
+
kind: "answer_to_pending_question",
|
|
490
|
+
tool: "ask_user",
|
|
491
|
+
callId,
|
|
492
|
+
questions: [asked],
|
|
493
|
+
answers: [givenAnswer],
|
|
494
|
+
}),
|
|
495
|
+
source: { kind: REPLY_KIND, callId, outcome: "answered" },
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function answerQuestion(sessionRef, questionId, answer) {
|
|
499
|
+
const past = pastOwner(sessionRef);
|
|
500
|
+
const sessionId = botOf(sessionRef) !== undefined
|
|
501
|
+
? chatOf(sessionRef)
|
|
502
|
+
: past?.roomId === undefined && past?.botId !== undefined
|
|
503
|
+
? chatOf(past.botId)
|
|
504
|
+
: sessionRef;
|
|
505
|
+
const question = own(state.questions, sessionId);
|
|
506
|
+
if (question === undefined || question.id !== questionId)
|
|
507
|
+
throw stale("This question is no longer open");
|
|
508
|
+
const reply = answerReply(question, answer);
|
|
509
|
+
if (reply === undefined)
|
|
510
|
+
throw invalid("Nothing to send");
|
|
511
|
+
// Claim the question before queueing so a second answer is refused.
|
|
512
|
+
delete state.questions[sessionId];
|
|
513
|
+
try {
|
|
514
|
+
await record("ensureCapabilities", sessionId);
|
|
515
|
+
const agent = await call("resolveAgent", sessionId);
|
|
516
|
+
// DSH source kinds are merge-extensible per producer. Until the card layer owns
|
|
517
|
+
// shape and the cast states the invariant.
|
|
518
|
+
const source = reply.source;
|
|
519
|
+
agent.followup(createUserMessage({ content: [{ type: "text", text: reply.text }], source }));
|
|
520
|
+
const sessions = ctx.sessions;
|
|
521
|
+
await sessions?.flush?.(agent.session);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
state.questions[sessionId] ??= question;
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
await save();
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
// An answer to an interactive view: validated by the view layer, claimed before the
|
|
531
|
+
// follow-up is queued so a second answer is refused, and put back when queueing fails.
|
|
532
|
+
async function answerView(sessionRef, viewId, action, values) {
|
|
533
|
+
const validate = member("view-reply", "validateViewAnswer");
|
|
534
|
+
const build = member("view-reply", "viewReply");
|
|
535
|
+
await load();
|
|
536
|
+
const past = pastOwner(sessionRef);
|
|
537
|
+
const sessionId = botOf(sessionRef) !== undefined
|
|
538
|
+
? chatOf(sessionRef)
|
|
539
|
+
: past?.roomId === undefined && past?.botId !== undefined
|
|
540
|
+
? chatOf(past.botId)
|
|
541
|
+
: sessionRef;
|
|
542
|
+
const pending = own(state.views, sessionId);
|
|
543
|
+
if (pending === undefined || pending.id !== viewId)
|
|
544
|
+
throw stale("This view is no longer open");
|
|
545
|
+
const checked = validate(pending.view, action, values);
|
|
546
|
+
if (checked !== undefined && checked.error !== undefined)
|
|
547
|
+
throw invalid(checked.error);
|
|
548
|
+
const reply = build(pending, action, values);
|
|
549
|
+
if (reply === undefined)
|
|
550
|
+
throw invalid("Nothing to send");
|
|
551
|
+
// Claim the view before queueing so a second answer is refused.
|
|
552
|
+
delete state.views[sessionId];
|
|
553
|
+
try {
|
|
554
|
+
await record("ensureCapabilities", sessionId);
|
|
555
|
+
const agent = await call("resolveAgent", sessionId);
|
|
556
|
+
const source = reply.source;
|
|
557
|
+
agent.followup(createUserMessage({ content: [{ type: "text", text: reply.text }], source }));
|
|
558
|
+
const sessions = ctx.sessions;
|
|
559
|
+
await sessions?.flush?.(agent.session);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
state.views[sessionId] ??= pending;
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
await save();
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
const setPrefs = async (body) => {
|
|
569
|
+
const current = prefs();
|
|
570
|
+
if (body.theme !== undefined) {
|
|
571
|
+
if (typeof body.theme !== "string" || !/^[a-z][\w-]{0,31}$/.test(body.theme))
|
|
572
|
+
throw invalid("Unknown theme");
|
|
573
|
+
current.theme = body.theme;
|
|
574
|
+
}
|
|
575
|
+
if (body.accent !== undefined) {
|
|
576
|
+
if (body.accent === null || body.accent === "")
|
|
577
|
+
delete current.accent;
|
|
578
|
+
else if (/^#[0-9a-f]{6}$/i.test(body.accent))
|
|
579
|
+
current.accent = body.accent;
|
|
580
|
+
else
|
|
581
|
+
throw invalid("Accent must be a #rrggbb color");
|
|
582
|
+
}
|
|
583
|
+
if (body.motion !== undefined) {
|
|
584
|
+
if (body.motion === null || body.motion === "normal")
|
|
585
|
+
delete current.motion;
|
|
586
|
+
else if (body.motion === "quiet" || body.motion === "lively")
|
|
587
|
+
current.motion = body.motion;
|
|
588
|
+
else
|
|
589
|
+
throw invalid("Motion must be quiet, normal or lively");
|
|
590
|
+
}
|
|
591
|
+
if (body.layout !== undefined) {
|
|
592
|
+
if (body.layout === null || body.layout === "bubble")
|
|
593
|
+
delete current.layout;
|
|
594
|
+
else if (body.layout === "flat")
|
|
595
|
+
current.layout = body.layout;
|
|
596
|
+
else
|
|
597
|
+
throw invalid("Layout must be bubble or flat");
|
|
598
|
+
}
|
|
599
|
+
if (body.width !== undefined) {
|
|
600
|
+
if (body.width === null || body.width === 80)
|
|
601
|
+
delete current.width;
|
|
602
|
+
else if (Number.isInteger(body.width) && body.width >= 50 && body.width <= 100)
|
|
603
|
+
current.width = body.width;
|
|
604
|
+
else
|
|
605
|
+
throw invalid("Width must be a whole percent from 50 to 100");
|
|
606
|
+
}
|
|
607
|
+
if (body.flatWidth !== undefined) {
|
|
608
|
+
if (body.flatWidth === null || body.flatWidth === 100)
|
|
609
|
+
delete current.flatWidth;
|
|
610
|
+
else if (Number.isInteger(body.flatWidth) && body.flatWidth >= 50 && body.flatWidth <= 100)
|
|
611
|
+
current.flatWidth = body.flatWidth;
|
|
612
|
+
else
|
|
613
|
+
throw invalid("Flat text width must be a whole percent from 50 to 100");
|
|
614
|
+
}
|
|
615
|
+
if (body.defaultBotModel !== undefined) {
|
|
616
|
+
// The saved ref is one the catalog served at save time; a create re-checks it.
|
|
617
|
+
if (body.defaultBotModel === null || body.defaultBotModel === "")
|
|
618
|
+
delete current.defaultBotModel;
|
|
619
|
+
else {
|
|
620
|
+
if (fn("isLocalAgentRef")?.(body.defaultBotModel) === true) {
|
|
621
|
+
throw invalid("A local agent cannot be the default model: a new Bot chooses to run on one when it is created");
|
|
622
|
+
}
|
|
623
|
+
const resolve = fn("resolveModel");
|
|
624
|
+
if (resolve === undefined)
|
|
625
|
+
throw unavailable("Choosing a default model");
|
|
626
|
+
current.defaultBotModel = await resolve(body.defaultBotModel);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (body.thinking !== undefined) {
|
|
630
|
+
// A value this version does not know is ignored, not refused: the toggle may grow.
|
|
631
|
+
if (body.thinking === true)
|
|
632
|
+
current.thinking = true;
|
|
633
|
+
else if (body.thinking === false || body.thinking === null)
|
|
634
|
+
delete current.thinking;
|
|
635
|
+
}
|
|
636
|
+
if (body.enterToSend !== undefined) {
|
|
637
|
+
if (body.enterToSend === false)
|
|
638
|
+
current.enterToSend = false;
|
|
639
|
+
else if (body.enterToSend === true || body.enterToSend === null)
|
|
640
|
+
delete current.enterToSend;
|
|
641
|
+
}
|
|
642
|
+
await save();
|
|
643
|
+
};
|
|
644
|
+
const endpoints = {
|
|
645
|
+
async state(body) {
|
|
646
|
+
if (typeof body.locale === "string" && /^[a-z]{2,3}(-[\w-]+)?$/i.test(body.locale))
|
|
647
|
+
uiLocale = body.locale;
|
|
648
|
+
if (sync("readOnly") === undefined) {
|
|
649
|
+
await record("bootstrap");
|
|
650
|
+
}
|
|
651
|
+
await record("catalog");
|
|
652
|
+
await record("loadSecrets");
|
|
653
|
+
const answer = liveAnswer();
|
|
654
|
+
if (body.since === state.revision && (body.models === undefined || body.models === modelsKey())) {
|
|
655
|
+
if (body.live === answer.key) {
|
|
656
|
+
return ok({ revision: state.revision, unchanged: true, liveKey: answer.key });
|
|
657
|
+
}
|
|
658
|
+
return ok({ revision: state.revision, unchanged: true, liveKey: answer.key, ...answer.value });
|
|
659
|
+
}
|
|
660
|
+
return ok({ ...view(), liveKey: answer.key, ...answer.value });
|
|
661
|
+
},
|
|
662
|
+
async models() {
|
|
663
|
+
await call("catalog", { fresh: true });
|
|
664
|
+
return ok({ modelsKey: modelsKey() });
|
|
665
|
+
},
|
|
666
|
+
async efforts(body) {
|
|
667
|
+
const efforts = await call("reasoningOf", String(body.ref ?? ""));
|
|
668
|
+
return ok(efforts ?? { efforts: [] });
|
|
669
|
+
},
|
|
670
|
+
async "set-prefs"(body) {
|
|
671
|
+
await load();
|
|
672
|
+
await setPrefs(body);
|
|
673
|
+
return ok(state.prefs);
|
|
674
|
+
},
|
|
675
|
+
async exchange(body) {
|
|
676
|
+
await load();
|
|
677
|
+
const pair = new Set([body.a, body.b]);
|
|
678
|
+
return ok(state.exchanges.filter((entry) => pair.has(entry.from) && pair.has(entry.to)));
|
|
679
|
+
},
|
|
680
|
+
async "exchange-text"(body) {
|
|
681
|
+
await load();
|
|
682
|
+
const entry = state.exchanges.find((item) => item.id === body.id);
|
|
683
|
+
if (entry === undefined)
|
|
684
|
+
return fail("This message is no longer kept", "bot/stale");
|
|
685
|
+
if (fn("exchangeText") === undefined)
|
|
686
|
+
throw unavailable("Reading a kept message");
|
|
687
|
+
return ok(await call("exchangeText", entry));
|
|
688
|
+
},
|
|
689
|
+
async "file-text"(body) {
|
|
690
|
+
if (typeof body.file?.attachmentId !== "string")
|
|
691
|
+
return fail("No file named", "bot/invalid");
|
|
692
|
+
if (fn("fileText") === undefined)
|
|
693
|
+
throw unavailable("Reading a file");
|
|
694
|
+
const read = await call("fileText", body.file);
|
|
695
|
+
return read === undefined ? fail("This file cannot be read here", "bot/unavailable") : ok(read);
|
|
696
|
+
},
|
|
697
|
+
async "room-progress"(body) {
|
|
698
|
+
// Any part id of the group's own Session means the group itself.
|
|
699
|
+
const room = roomOf(body.roomId) ?? roomOf(pastOwner(body.roomId)?.roomId);
|
|
700
|
+
const progress = roomProgress();
|
|
701
|
+
const wanted = room?.id ?? body.roomId;
|
|
702
|
+
const rows = [...(progress ?? new Map()).entries()]
|
|
703
|
+
.filter(([, entry]) => entry?.roomId === wanted)
|
|
704
|
+
.map(([callId, entry]) => ({
|
|
705
|
+
callId,
|
|
706
|
+
replies: entry.replies,
|
|
707
|
+
speaking: (entry.slots ?? [])
|
|
708
|
+
.filter((slot) => slot.status === "speaking")
|
|
709
|
+
.map((slot) => slot.id),
|
|
710
|
+
}));
|
|
711
|
+
return ok(rows);
|
|
712
|
+
},
|
|
713
|
+
async "create-bot"(body) {
|
|
714
|
+
await record("bootstrap");
|
|
715
|
+
const bot = await call("createBot", {
|
|
716
|
+
name: body.name,
|
|
717
|
+
role: body.role,
|
|
718
|
+
color: body.color,
|
|
719
|
+
instructions: body.instructions,
|
|
720
|
+
brief: body.brief,
|
|
721
|
+
avatar: body.avatar,
|
|
722
|
+
model: body.model,
|
|
723
|
+
effort: body.effort,
|
|
724
|
+
workDir: body.workDir,
|
|
725
|
+
});
|
|
726
|
+
return ok(bot);
|
|
727
|
+
},
|
|
728
|
+
async "update-bot"(body) {
|
|
729
|
+
await load();
|
|
730
|
+
return ok(await call("updateBot", body.id, body));
|
|
731
|
+
},
|
|
732
|
+
async "duplicate-bot"(body) {
|
|
733
|
+
await load();
|
|
734
|
+
return ok(await call("duplicateBot", body.id));
|
|
735
|
+
},
|
|
736
|
+
async "delete-bot"(body) {
|
|
737
|
+
await load();
|
|
738
|
+
await call("deleteBot", body.id);
|
|
739
|
+
return ok(view());
|
|
740
|
+
},
|
|
741
|
+
async "set-main"(body) {
|
|
742
|
+
await load();
|
|
743
|
+
await call("setMain", body.id, body.main !== false, {
|
|
744
|
+
primary: body.primary === true,
|
|
745
|
+
replace: typeof body.replace === "string" ? body.replace : undefined,
|
|
746
|
+
});
|
|
747
|
+
return ok(view());
|
|
748
|
+
},
|
|
749
|
+
async "set-flags"(body) {
|
|
750
|
+
await load();
|
|
751
|
+
return ok(await call("setFlags", body.id, body));
|
|
752
|
+
},
|
|
753
|
+
async resume(body) {
|
|
754
|
+
await load();
|
|
755
|
+
const bot = botOf(String(body.botId ?? ""));
|
|
756
|
+
if (bot === undefined)
|
|
757
|
+
return fail("Unknown Bot", "bot/not-found");
|
|
758
|
+
await call("resumeBot", bot);
|
|
759
|
+
return ok(true);
|
|
760
|
+
},
|
|
761
|
+
async "create-room"(body) {
|
|
762
|
+
await load();
|
|
763
|
+
const room = await call("createRoom", {
|
|
764
|
+
members: Array.isArray(body.members) ? body.members : [],
|
|
765
|
+
name: body.name,
|
|
766
|
+
...(typeof body.admin === "string" ? { admin: body.admin } : {}),
|
|
767
|
+
});
|
|
768
|
+
if (body.greet === true) {
|
|
769
|
+
const greet = fn("greetRoom");
|
|
770
|
+
if (greet !== undefined) {
|
|
771
|
+
await greet(room.id).catch((error) => rt.warn("ocean-crew: greeting in %s failed: %s", room.name, error));
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return ok(room);
|
|
775
|
+
},
|
|
776
|
+
async "update-room"(body) {
|
|
777
|
+
await load();
|
|
778
|
+
const { room, changes } = await call("updateRoom", body.id, {
|
|
779
|
+
name: body.name,
|
|
780
|
+
notice: body.notice,
|
|
781
|
+
mode: body.mode,
|
|
782
|
+
admin: body.admin,
|
|
783
|
+
members: Array.isArray(body.members) ? body.members : undefined,
|
|
784
|
+
add: Array.isArray(body.add) ? body.add : undefined,
|
|
785
|
+
remove: Array.isArray(body.remove) ? body.remove : undefined,
|
|
786
|
+
workDir: body.workDir,
|
|
787
|
+
permission: body.permission,
|
|
788
|
+
});
|
|
789
|
+
const rooms = view().rooms;
|
|
790
|
+
return ok({ room: rooms.find((entry) => entry.id === room.id), changes });
|
|
791
|
+
},
|
|
792
|
+
async "delete-room"(body) {
|
|
793
|
+
await load();
|
|
794
|
+
await call("deleteRoom", body.id);
|
|
795
|
+
return ok(view());
|
|
796
|
+
},
|
|
797
|
+
async send(body) {
|
|
798
|
+
const text = String(body.text ?? "").trim();
|
|
799
|
+
if (text === "" || typeof body.sessionId !== "string")
|
|
800
|
+
return fail("Nothing to send", "bot/invalid");
|
|
801
|
+
await load();
|
|
802
|
+
await sendUser({ sessionId: body.sessionId, content: [{ type: "text", text }] });
|
|
803
|
+
return ok(true);
|
|
804
|
+
},
|
|
805
|
+
async "answer-question"(body) {
|
|
806
|
+
if (typeof body.sessionId !== "string")
|
|
807
|
+
return fail("Nothing to send", "bot/invalid");
|
|
808
|
+
await load();
|
|
809
|
+
return ok(await answerQuestion(body.sessionId, body.questionId, body.answer));
|
|
810
|
+
},
|
|
811
|
+
async "dismiss-question"(body) {
|
|
812
|
+
await load();
|
|
813
|
+
const sessionId = String(body.sessionId ?? "");
|
|
814
|
+
const question = own(state.questions, sessionId);
|
|
815
|
+
if (question !== undefined) {
|
|
816
|
+
sync("closeCard", sessionId, question, "dismissed");
|
|
817
|
+
delete state.questions[sessionId];
|
|
818
|
+
await save();
|
|
819
|
+
}
|
|
820
|
+
return ok(true);
|
|
821
|
+
},
|
|
822
|
+
async "dismiss-view"(body) {
|
|
823
|
+
await load();
|
|
824
|
+
const sessionId = String(body.sessionId ?? "");
|
|
825
|
+
const pending = own(state.views, sessionId);
|
|
826
|
+
if (pending !== undefined) {
|
|
827
|
+
sync("closeCard", sessionId, pending, "dismissed");
|
|
828
|
+
delete state.views[sessionId];
|
|
829
|
+
await save();
|
|
830
|
+
}
|
|
831
|
+
return ok(true);
|
|
832
|
+
},
|
|
833
|
+
async "run-stats"(body) {
|
|
834
|
+
await load();
|
|
835
|
+
const bot = botOf(String(body.botId ?? ""));
|
|
836
|
+
if (bot === undefined)
|
|
837
|
+
return fail("Unknown Bot", "bot/not-found");
|
|
838
|
+
const projections = ctx.get?.("sessionProjections");
|
|
839
|
+
if (typeof projections?.snapshot !== "function")
|
|
840
|
+
return ok(null);
|
|
841
|
+
const agent = await call("resolveAgent", chatOf(bot.id));
|
|
842
|
+
return ok(projections.snapshot(agent.session, RUN_STATS).values);
|
|
843
|
+
},
|
|
844
|
+
async "stop-member-work"(body) {
|
|
845
|
+
const roomId = String(body.roomId ?? "");
|
|
846
|
+
try {
|
|
847
|
+
return ok(await call("stopMemberWork", roomId, String(body.botId ?? "")));
|
|
848
|
+
}
|
|
849
|
+
finally {
|
|
850
|
+
sync("goalUserStopped", roomId);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
async "stop-room-work"(body) {
|
|
854
|
+
const roomId = String(body.roomId ?? "");
|
|
855
|
+
try {
|
|
856
|
+
return ok(await call("stopRoomWork", roomId));
|
|
857
|
+
}
|
|
858
|
+
finally {
|
|
859
|
+
sync("goalUserStopped", roomId);
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
// --- Module wiring -----------------------------------------------------
|
|
863
|
+
// Each branch calls the runtime member its module publishes; the module landing is
|
|
864
|
+
// the only change needed for the endpoint to answer, and a missing one refuses only
|
|
865
|
+
// this endpoint through `missingMember`.
|
|
866
|
+
async providers() {
|
|
867
|
+
return ok(await callMember("providers", "providersView"));
|
|
868
|
+
},
|
|
869
|
+
async "provider-key"(body) {
|
|
870
|
+
const providersView = member("provider-key", "providersView");
|
|
871
|
+
if (body.value === null)
|
|
872
|
+
await callMember("provider-key", "clearProviderKey", String(body.provider ?? ""));
|
|
873
|
+
else
|
|
874
|
+
await callMember("provider-key", "setProviderKey", String(body.provider ?? ""), body.value);
|
|
875
|
+
return ok(await providersView());
|
|
876
|
+
},
|
|
877
|
+
async "provider-add"(body) {
|
|
878
|
+
return ok(await callMember("provider-add", "addProvider", body));
|
|
879
|
+
},
|
|
880
|
+
async "provider-models"(body) {
|
|
881
|
+
return ok(await callMember("provider-models", "discoverModels", body));
|
|
882
|
+
},
|
|
883
|
+
async "provider-remove"(body) {
|
|
884
|
+
return ok(await callMember("provider-remove", "removeProvider", String(body.provider ?? "")));
|
|
885
|
+
},
|
|
886
|
+
// A Turn's exact thinking time, from the timed stream in its events; `ms` omitted
|
|
887
|
+
// when the stream carries no reasoning records.
|
|
888
|
+
async thinking(body) {
|
|
889
|
+
const thinkingOf = member("thinking", "thinkingOf");
|
|
890
|
+
const sessionId = String(body.sessionId ?? "");
|
|
891
|
+
const turn = Number(body.turn);
|
|
892
|
+
if (!Number.isFinite(turn) || selfOf(sessionId) === undefined)
|
|
893
|
+
return ok(null);
|
|
894
|
+
await load();
|
|
895
|
+
const ms = await thinkingOf(sessionId, turn);
|
|
896
|
+
return ok(ms === undefined ? {} : { ms });
|
|
897
|
+
},
|
|
898
|
+
async earlier(body) {
|
|
899
|
+
await load();
|
|
900
|
+
return ok(await callMember("earlier", "earlierPage", String(body.sessionId ?? ""), body.cursor ?? undefined));
|
|
901
|
+
},
|
|
902
|
+
async chat(body) {
|
|
903
|
+
await load();
|
|
904
|
+
return ok(await callMember("chat", "chatPage", String(body.botId ?? ""), body.cursor ?? undefined));
|
|
905
|
+
},
|
|
906
|
+
async "bot-new-part"(body) {
|
|
907
|
+
await load();
|
|
908
|
+
const bot = botOf(String(body.botId ?? ""));
|
|
909
|
+
if (bot === undefined)
|
|
910
|
+
return fail("Unknown Bot", "bot/not-found");
|
|
911
|
+
return ok(await callMember("bot-new-part", "newPart", chatOf(bot.id)));
|
|
912
|
+
},
|
|
913
|
+
async "bot-open-part"(body) {
|
|
914
|
+
await load();
|
|
915
|
+
const bot = botOf(String(body.botId ?? ""));
|
|
916
|
+
if (bot === undefined)
|
|
917
|
+
return fail("Unknown Bot", "bot/not-found");
|
|
918
|
+
return ok(await callMember("bot-open-part", "switchPart", bot.id, String(body.sessionId ?? "")));
|
|
919
|
+
},
|
|
920
|
+
async usage(body) {
|
|
921
|
+
await load();
|
|
922
|
+
return ok(await callMember("usage", "usageView", {
|
|
923
|
+
timeZone: typeof body.timeZone === "string" ? body.timeZone.slice(0, 64) : undefined,
|
|
924
|
+
}));
|
|
925
|
+
},
|
|
926
|
+
async folders(body, signal) {
|
|
927
|
+
await load();
|
|
928
|
+
return ok(await callMember("folders", "folderEndpoint", "folders", body, signal));
|
|
929
|
+
},
|
|
930
|
+
async "folder-pick"(body, signal) {
|
|
931
|
+
await load();
|
|
932
|
+
return ok(await callMember("folder-pick", "folderEndpoint", "folder-pick", body, signal));
|
|
933
|
+
},
|
|
934
|
+
async "folder-list"(body, signal) {
|
|
935
|
+
await load();
|
|
936
|
+
return ok(await callMember("folder-list", "folderEndpoint", "folder-list", body, signal));
|
|
937
|
+
},
|
|
938
|
+
async "team-workspace"(body) {
|
|
939
|
+
await load();
|
|
940
|
+
return ok({ team: await callMember("team-workspace", "setTeamWorkspace", String(body.path ?? "")) });
|
|
941
|
+
},
|
|
942
|
+
async "reset-team"() {
|
|
943
|
+
return ok(await callMember("reset-team", "resetTeam"));
|
|
944
|
+
},
|
|
945
|
+
async "private-release"(body) {
|
|
946
|
+
await load();
|
|
947
|
+
await callMember("private-release", "releaseArchive", String(body.id ?? ""));
|
|
948
|
+
return ok(view());
|
|
949
|
+
},
|
|
950
|
+
// The avatar library: photos the user imported once, kept beside the team.
|
|
951
|
+
async "avatar-library"(body) {
|
|
952
|
+
await load();
|
|
953
|
+
return ok(await callMember("avatar-library", "avatarLibraryView", { offset: body.offset, limit: body.limit }));
|
|
954
|
+
},
|
|
955
|
+
async "avatar-import"(body) {
|
|
956
|
+
await load();
|
|
957
|
+
return ok(await callMember("avatar-import", "avatarLibraryImport", Array.isArray(body.items) ? body.items : []));
|
|
958
|
+
},
|
|
959
|
+
async "avatar-delete"(body) {
|
|
960
|
+
await load();
|
|
961
|
+
return ok(await callMember("avatar-delete", "avatarLibraryDelete", String(body.id ?? "")));
|
|
962
|
+
},
|
|
963
|
+
// The team's one DSH Agent.
|
|
964
|
+
async "add-agent"() {
|
|
965
|
+
await load();
|
|
966
|
+
return ok(await callMember("add-agent", "addAgent"));
|
|
967
|
+
},
|
|
968
|
+
async "agent-session"(body) {
|
|
969
|
+
await load();
|
|
970
|
+
return ok({
|
|
971
|
+
sessionId: await callMember("agent-session", "createAgentSession", body.agentId, body.workspaceId),
|
|
972
|
+
});
|
|
973
|
+
},
|
|
974
|
+
async "agent-adopt"(body) {
|
|
975
|
+
await load();
|
|
976
|
+
return ok({
|
|
977
|
+
sessionId: await callMember("agent-adopt", "adoptAgentSession", body.agentId, body.sessionId),
|
|
978
|
+
});
|
|
979
|
+
},
|
|
980
|
+
async "agent-archive"(body) {
|
|
981
|
+
await load();
|
|
982
|
+
await callMember("agent-archive", "archiveAgentSession", body.agentId, body.sessionId);
|
|
983
|
+
return ok(true);
|
|
984
|
+
},
|
|
985
|
+
async "agent-rename"(body) {
|
|
986
|
+
await load();
|
|
987
|
+
await callMember("agent-rename", "renameAgentSession", body.agentId, body.sessionId, body.title);
|
|
988
|
+
return ok(true);
|
|
989
|
+
},
|
|
990
|
+
async "remove-agent"(body) {
|
|
991
|
+
await load();
|
|
992
|
+
await callMember("remove-agent", "removeAgent", body.agentId);
|
|
993
|
+
return ok(view());
|
|
994
|
+
},
|
|
995
|
+
async "approval-answer"(body) {
|
|
996
|
+
member("approval-answer", "answerApproval")(body.id, body.choice);
|
|
997
|
+
return ok(true);
|
|
998
|
+
},
|
|
999
|
+
async "goal-draft"(body) {
|
|
1000
|
+
await load();
|
|
1001
|
+
const result = await callMember("goal-draft", "goalDraft", String(body.roomId ?? ""), body.text);
|
|
1002
|
+
return result?.error !== undefined ? fail(result.error, result.code ?? "bot/refused") : ok(view());
|
|
1003
|
+
},
|
|
1004
|
+
async "room-budget"(body) {
|
|
1005
|
+
const roomId = String(body.roomId ?? "");
|
|
1006
|
+
const changing = body.clear === true ||
|
|
1007
|
+
(body.members !== undefined && body.members !== null) ||
|
|
1008
|
+
(body.summary !== undefined && body.summary !== null);
|
|
1009
|
+
if (changing)
|
|
1010
|
+
await callMember("room-budget", "setGroupBudget", roomId, body);
|
|
1011
|
+
const view = await callMember("room-budget", "groupBudgetView", roomId);
|
|
1012
|
+
if (view === undefined)
|
|
1013
|
+
return fail("No such group chat", "bot/not-found");
|
|
1014
|
+
return ok(view);
|
|
1015
|
+
},
|
|
1016
|
+
async "goal-control"(body) {
|
|
1017
|
+
await load();
|
|
1018
|
+
const result = await callMember("goal-control", "goalControl", String(body.roomId ?? ""), body);
|
|
1019
|
+
return result?.error !== undefined ? fail(result.error, result.code ?? "bot/refused") : ok(view());
|
|
1020
|
+
},
|
|
1021
|
+
async "view-reply"(body) {
|
|
1022
|
+
if (typeof body.sessionId !== "string")
|
|
1023
|
+
return fail("Nothing to send", "bot/invalid");
|
|
1024
|
+
try {
|
|
1025
|
+
await answerView(body.sessionId, body.viewId, body.action, body.values);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
// The endpoint's own codes survive: a missing module is unavailable, a refused
|
|
1029
|
+
// queue is refused; anything else is the caller's payload.
|
|
1030
|
+
return fail(String(error?.message ?? error), errorCode(error));
|
|
1031
|
+
}
|
|
1032
|
+
return ok(true);
|
|
1033
|
+
},
|
|
1034
|
+
async "btw-delete"(body) {
|
|
1035
|
+
if (typeof body.sessionId !== "string" ||
|
|
1036
|
+
body.sessionId === "" ||
|
|
1037
|
+
typeof body.cardId !== "string" ||
|
|
1038
|
+
body.cardId === "") {
|
|
1039
|
+
return fail("Nothing to delete", "bot/invalid");
|
|
1040
|
+
}
|
|
1041
|
+
await load();
|
|
1042
|
+
return ok(await callMember("btw-delete", "btwDelete", { sessionId: body.sessionId, cardId: body.cardId }));
|
|
1043
|
+
},
|
|
1044
|
+
// Side chats: each question is an aside on a fresh snapshot of this Bot's private
|
|
1045
|
+
// chat; Nothing is written into the Session.
|
|
1046
|
+
async "side-new"(body) {
|
|
1047
|
+
if (typeof body.sessionId !== "string" || body.sessionId === "")
|
|
1048
|
+
return fail("Nothing to open", "bot/invalid");
|
|
1049
|
+
await load();
|
|
1050
|
+
return ok(await callMember("side-new", "sideNew", { sessionId: body.sessionId }));
|
|
1051
|
+
},
|
|
1052
|
+
async "side-ask"(body) {
|
|
1053
|
+
if (typeof body.sessionId !== "string" || body.sessionId === "")
|
|
1054
|
+
return fail("Nothing to ask", "bot/invalid");
|
|
1055
|
+
if (body.chatId !== undefined && typeof body.chatId !== "string")
|
|
1056
|
+
return fail("Nothing to ask", "bot/invalid");
|
|
1057
|
+
if (String(body.text ?? "").trim() === "")
|
|
1058
|
+
return fail("Write a question.", "bot/invalid");
|
|
1059
|
+
await load();
|
|
1060
|
+
return ok(await callMember("side-ask", "sideAsk", {
|
|
1061
|
+
sessionId: body.sessionId,
|
|
1062
|
+
chatId: body.chatId,
|
|
1063
|
+
recent: body.recent === true,
|
|
1064
|
+
text: String(body.text),
|
|
1065
|
+
}));
|
|
1066
|
+
},
|
|
1067
|
+
async "side-cancel"(body) {
|
|
1068
|
+
if (typeof body.chatId !== "string" ||
|
|
1069
|
+
body.chatId === "" ||
|
|
1070
|
+
typeof body.entryId !== "string" ||
|
|
1071
|
+
body.entryId === "") {
|
|
1072
|
+
return fail("Nothing to cancel", "bot/invalid");
|
|
1073
|
+
}
|
|
1074
|
+
await load();
|
|
1075
|
+
return ok(await callMember("side-cancel", "sideCancel", { chatId: body.chatId, entryId: body.entryId }));
|
|
1076
|
+
},
|
|
1077
|
+
async "side-close"(body) {
|
|
1078
|
+
if (typeof body.chatId !== "string" || body.chatId === "")
|
|
1079
|
+
return fail("Nothing to close", "bot/invalid");
|
|
1080
|
+
await load();
|
|
1081
|
+
return ok(await callMember("side-close", "sideClose", { chatId: body.chatId }));
|
|
1082
|
+
},
|
|
1083
|
+
async "learning-answer"(body) {
|
|
1084
|
+
await load();
|
|
1085
|
+
return ok(await callMember("learning-answer", "learningEndpoint", "learning-answer", body));
|
|
1086
|
+
},
|
|
1087
|
+
async "update-check"(body) {
|
|
1088
|
+
const fresh = body.fresh === true;
|
|
1089
|
+
const updateCheck = member("update-check", "updateCheck");
|
|
1090
|
+
const dshUpdateCheck = fn("dshUpdateCheck");
|
|
1091
|
+
return ok({
|
|
1092
|
+
...(await updateCheck({ fresh })),
|
|
1093
|
+
dsh: (dshUpdateCheck === undefined ? undefined : await dshUpdateCheck({ fresh })) ?? null,
|
|
1094
|
+
});
|
|
1095
|
+
},
|
|
1096
|
+
async "update-install"() {
|
|
1097
|
+
return ok(member("update-install", "updateStart")());
|
|
1098
|
+
},
|
|
1099
|
+
async "dsh-update-install"() {
|
|
1100
|
+
return ok(member("dsh-update-install", "dshUpdateStart")());
|
|
1101
|
+
},
|
|
1102
|
+
async "repo-stars"() {
|
|
1103
|
+
return ok(await callMember("repo-stars", "repoStars"));
|
|
1104
|
+
},
|
|
1105
|
+
async "companion-audio"(body) {
|
|
1106
|
+
const botId = String(body?.botId ?? "");
|
|
1107
|
+
const text = String(body?.text ?? "");
|
|
1108
|
+
if (botId === "" || text === "")
|
|
1109
|
+
throw Object.assign(new Error("Nothing to speak"), { code: "bot/invalid" });
|
|
1110
|
+
return ok(await callMember("companion-audio", "companionAudio", botId, text));
|
|
1111
|
+
},
|
|
1112
|
+
async "companion-view"() {
|
|
1113
|
+
return ok(await callMember("companion-view", "companionView"));
|
|
1114
|
+
},
|
|
1115
|
+
async "companion-set"(body) {
|
|
1116
|
+
const botId = String(body?.botId ?? "");
|
|
1117
|
+
if (botId === "")
|
|
1118
|
+
throw Object.assign(new Error("Choose a Bot first"), { code: "bot/invalid" });
|
|
1119
|
+
const patch = body?.patch !== null &&
|
|
1120
|
+
typeof body?.patch === "object"
|
|
1121
|
+
? body.patch
|
|
1122
|
+
: {};
|
|
1123
|
+
return ok(await callMember("companion-set", "companionSet", botId, patch));
|
|
1124
|
+
},
|
|
1125
|
+
async "tts-settings"() {
|
|
1126
|
+
return ok(await callMember("tts-settings", "ttsSettingsView"));
|
|
1127
|
+
},
|
|
1128
|
+
async "tts-set"(body) {
|
|
1129
|
+
const patch = body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
1130
|
+
return ok(await callMember("tts-set", "ttsSettingsSet", patch));
|
|
1131
|
+
},
|
|
1132
|
+
async "tts-voices"() {
|
|
1133
|
+
return ok(await callMember("tts-voices", "ttsVoices"));
|
|
1134
|
+
},
|
|
1135
|
+
async "tts-test"() {
|
|
1136
|
+
return ok(await callMember("tts-test", "ttsTest"));
|
|
1137
|
+
},
|
|
1138
|
+
async diagnostics(body) {
|
|
1139
|
+
const service = diagnosticsService("diagnostics", "report");
|
|
1140
|
+
await load();
|
|
1141
|
+
return ok(await service.report(body));
|
|
1142
|
+
},
|
|
1143
|
+
async "diagnostics-log"() {
|
|
1144
|
+
return ok(await diagnosticsService("diagnostics-log", "logText").logText());
|
|
1145
|
+
},
|
|
1146
|
+
async "diagnostics-clear"() {
|
|
1147
|
+
await diagnosticsService("diagnostics-clear", "clear").clear();
|
|
1148
|
+
return ok(true);
|
|
1149
|
+
},
|
|
1150
|
+
// --- IM wiring ---------------------------------------------------------
|
|
1151
|
+
};
|
|
1152
|
+
// Groups whose whole family is one pass-through member. The endpoint name and the body
|
|
1153
|
+
for (const [endpoint, memberName] of [
|
|
1154
|
+
...SECRET_ENDPOINTS.map((name) => [name, "secretEndpoint"]),
|
|
1155
|
+
...CONNECTOR_MAIL_ENDPOINTS.map((name) => [name, "connectorEndpoint"]),
|
|
1156
|
+
...CAPABILITY_ENDPOINTS.map((name) => [name, "capabilityEndpoint"]),
|
|
1157
|
+
...SCHEDULE_ENDPOINTS.map((name) => [name, "scheduleEndpoint"]),
|
|
1158
|
+
...MEMORY_ENDPOINTS.map((name) => [name, "memoryEndpoint"]),
|
|
1159
|
+
]) {
|
|
1160
|
+
endpoints[endpoint] = async (body) => {
|
|
1161
|
+
await load();
|
|
1162
|
+
return ok(await callMember(endpoint, memberName, endpoint, body));
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
for (const endpoint of IM_ENDPOINTS) {
|
|
1166
|
+
endpoints[endpoint] = async (body) => {
|
|
1167
|
+
await load();
|
|
1168
|
+
return ok(await member(endpoint, "imEndpoint")(endpoint, body));
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const handle = async (endpoint, payload, signal) => {
|
|
1172
|
+
try {
|
|
1173
|
+
const body = (payload ?? {});
|
|
1174
|
+
await record("refresh");
|
|
1175
|
+
const readOnly = sync("readOnly");
|
|
1176
|
+
// A reading host keeps the read surface; every write endpoint names the other host.
|
|
1177
|
+
if (readOnly !== undefined && !READS.has(endpoint))
|
|
1178
|
+
return fail(readOnly, "bot/read-only");
|
|
1179
|
+
const handler = endpoints[endpoint];
|
|
1180
|
+
if (handler !== undefined)
|
|
1181
|
+
return await handler(body, signal);
|
|
1182
|
+
const module = UNAVAILABLE_ENDPOINTS.get(endpoint);
|
|
1183
|
+
if (module !== undefined) {
|
|
1184
|
+
return fail(`${endpoint}: the ${module} module is not installed in this build.`, "bot/unavailable");
|
|
1185
|
+
}
|
|
1186
|
+
return fail(`Unknown endpoint ${endpoint}`, "bot/not-found");
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
const code = errorCode(error);
|
|
1190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1191
|
+
if (code === "bot/internal") {
|
|
1192
|
+
const failure = error;
|
|
1193
|
+
sync("noteFailure", {
|
|
1194
|
+
source: "api",
|
|
1195
|
+
text: `${endpoint}: ${message}`,
|
|
1196
|
+
code: failure.code ?? error?.name,
|
|
1197
|
+
stack: failure.stack,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
return fail(message, code);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
// The buffered route on the shared /api channel; the browser authenticates with its
|
|
1204
|
+
// session cookie and always reads HTTP 200, the envelope says what happened.
|
|
1205
|
+
const route = {
|
|
1206
|
+
path: ROUTE,
|
|
1207
|
+
methods: ["POST"],
|
|
1208
|
+
requestBody: "buffered",
|
|
1209
|
+
async fetch(request) {
|
|
1210
|
+
let body = {};
|
|
1211
|
+
try {
|
|
1212
|
+
body = (await request.json());
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
body = {};
|
|
1216
|
+
}
|
|
1217
|
+
const result = await handle(String(body?.endpoint ?? ""), body?.payload, request.signal);
|
|
1218
|
+
return new Response(JSON.stringify(result), {
|
|
1219
|
+
status: 200,
|
|
1220
|
+
headers: { "content-type": "application/json" },
|
|
1221
|
+
});
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
ctx.effect(() => {
|
|
1225
|
+
const connection = ctx.connection ?? ctx.get?.("connection");
|
|
1226
|
+
if (connection === undefined || typeof connection.fetch?.register !== "function") {
|
|
1227
|
+
rt.warn("ocean-crew: /api/bot not registered: the connection service is missing");
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
return connection.fetch.register(route);
|
|
1231
|
+
}, "ocean-crew: api route");
|
|
1232
|
+
ctx.effect(() => {
|
|
1233
|
+
// Registered after the route, so this disposer runs before the state lock is released.
|
|
1234
|
+
const wanted = fn("load");
|
|
1235
|
+
if (wanted !== undefined) {
|
|
1236
|
+
void wanted().catch((error) => rt.warn("ocean-crew: state not loaded: %s", error));
|
|
1237
|
+
}
|
|
1238
|
+
return undefined;
|
|
1239
|
+
}, "ocean-crew: load state");
|
|
1240
|
+
Object.assign(rt, { sendUser, answerQuestion, answerView, uiLanguage });
|
|
1241
|
+
}
|