@inetafrica/open-claudia 3.5.74 → 3.5.76
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/CHANGELOG.md +31 -0
- package/Dockerfile +2 -0
- package/bin/cli.js +47 -0
- package/bin/handoff.js +12 -3
- package/bin/owner-setup.js +56 -0
- package/bin/views.js +255 -0
- package/bot.js +42 -0
- package/channels/kazee/adapter.js +61 -1
- package/channels/kazee/call/camera.js +1 -2
- package/channels/kazee/call/session.js +6 -5
- package/channels/spaces/adapter.js +25 -1
- package/channels/spaces/client.js +6 -2
- package/channels/telegram/adapter.js +2 -1
- package/channels/web/adapter.js +27 -11
- package/core/access.js +25 -0
- package/core/actions.js +8 -0
- package/core/app-pass.js +145 -0
- package/core/bot-profile.js +228 -0
- package/core/config.js +7 -0
- package/core/connected-apps.js +19 -4
- package/core/handlers.js +3 -1
- package/core/identity.js +14 -0
- package/core/lessons.js +2 -1
- package/core/loopback.js +139 -5
- package/core/migration-backup.js +6 -2
- package/core/owner-setup.js +216 -0
- package/core/pack-review.js +77 -10
- package/core/recall/index.js +3 -2
- package/core/recall/warm-walker.js +31 -1
- package/core/runner.js +4 -1
- package/core/system-prompt.js +22 -1
- package/core/thread-handoff.js +44 -8
- package/core/tool-guard.js +15 -8
- package/core/usage-log.js +1 -0
- package/core/views.js +310 -0
- package/core/web-otp.js +17 -8
- package/core/web-sessions.js +11 -2
- package/docker-entrypoint.sh +10 -0
- package/package.json +2 -2
- package/test-provider-language.js +5 -1
- package/test-provider-migration-backup.js +13 -0
- package/web.js +19 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.76
|
|
4
|
+
|
|
5
|
+
3.5.75 plus the memory cuts from the usage work, the lessons ranking fix, the empty-lock fix, `handoff --new`, HTML views and the type-check guard (Sumeet 9 Nov: "all bots get all features"). Every new setting is optional: unset, a bot behaves as 3.5.75 except where noted.
|
|
6
|
+
|
|
7
|
+
- **Recall helper sleeps when idle** (`core/recall/warm-walker.js`): the warm recall walker stops after 10 minutes with no walks (about 160 MB back) and starts again on the next one. `RECALL_WARM_WALKER_IDLE_MS` changes the wait; `0` keeps it running.
|
|
8
|
+
- **Usage history read only when needed** (`core/runner.js`): a bot no longer parses its whole usage history after every turn; it reads it only when usage alerts are on.
|
|
9
|
+
- **Node flags for the bot only** (`docker-entrypoint.sh`): `OC_NODE_FLAGS` in a bot's env (for example a heap limit) goes on the main node command line, never `NODE_OPTIONS`, so agents, hooks and CLI runs do not inherit it. Unset, the start command is unchanged.
|
|
10
|
+
- **`RECALL_ENGINE` in the bot's `.env` works** (`core/recall/index.js`): the engine set in `.env` is now honoured, not only one set in the process env. A per-chat `/engine` choice still comes first.
|
|
11
|
+
- **Pack reviews wait their turn** (`core/pack-review.js`): reviews run one at a time per bot behind a queue instead of being dropped when too many came at once. `OC_REVIEW_CONCURRENCY` (default 1) and `OC_REVIEW_QUEUE_MAX` (default 20; the oldest is dropped with a log line) tune it.
|
|
12
|
+
- **Heavy libraries load on demand**: the call library (werift) loads on the first Kazee call and the Telegram library only when a Telegram token is set (about 40 MB less idle). The first call after a start takes a few seconds longer.
|
|
13
|
+
- **Type checks take their turn** (`core/tool-guard.js`): `tsc` now counts in the test-run guard, so only one test run or type check goes at a time on a bot and the free-memory floor applies.
|
|
14
|
+
- **Lessons keep their ranking** (`core/lessons.js`): reading one pack's topic lessons no longer resets every other lesson's ranking to zero.
|
|
15
|
+
- **An old empty migration lock no longer blocks start** (`core/migration-backup.js`): a lock file left empty by a run that died before writing its pid is treated as stale once old, so the bot stops crash-looping on it. A fresh empty lock still counts as held.
|
|
16
|
+
- **`open-claudia handoff --new "<title>" "<message>"`** (`core/thread-handoff.js`): starts a real titled Kazee thread, posts the hand-off in it tagging the person who asked (so it shows in their Threads list), then wakes it. Refused hand-offs start nothing.
|
|
17
|
+
- **HTML views** (`bin/views.js`, `core/views.js`): `open-claudia view send <page.html> --title "<t>" [--handler <file>] [--wake each|none]` sends an interactive page into Kazee Chat as a card with an Open button; it opens walled off (libraries from public CDNs, no data fetched from the internet). What the person does on the page runs the handler or wakes the conversation. `open-claudia view push` sends data back into open pages; `open-claudia view example` prints a quiz page and handler. Kazee Chat only.
|
|
18
|
+
|
|
19
|
+
Tests: `test-walker-idle-stop.js`, `test-usage-history-lazy.js`, `test-node-flags-entrypoint.js`, `test-recall-engine-env.js`, `test-review-queue.js`, `test-lazy-heavy-libs.js`, `test-lessons-meta-keep.js`, `test-views.js` (all in the suite), plus additions to `test-test-run-guard.js`, `test-provider-migration-backup.js` and `test-thread-handoff.js`.
|
|
20
|
+
|
|
21
|
+
## 3.5.75
|
|
22
|
+
|
|
23
|
+
3.5.74 plus personal bots act for their owner through a pass (from the Maia build), and the one-tap switch-on card. Nothing changes for a bot until Central has `bot:me` (an older Central answers 404 and the bot stays as it was). Autonomous bots are unchanged.
|
|
24
|
+
|
|
25
|
+
- **Type check** (`core/bot-profile.js`): on start, and every 15 minutes, the bot asks Central `GET /api/bots/me` with its own key for its kind, its one owner and the owner's consent (ticked apps, end date, `renewDue`). The answer is kept in `bot-profile.json`, so a restart while Central is down still knows.
|
|
26
|
+
- **One owner**: a personal bot's owner is the person in Central. Her id replaces `KAZEE_OWNER_USER_ID`, from config load and on every refresh. `access.isChatOwner` counts no other owner (no people records, `auth.json` or chat ids marked owner), and the web dashboard and the voice bridge reach her rather than being owners themselves.
|
|
27
|
+
- **One pass helper** (`core/app-pass.js`): every app call of a personal bot trades its key for a 10-minute pass for that app (`POST /api/bots/pass`), reused until a minute before it ends, one request per app at a time; the bot's own key is never sent to an app. Kazee Chat keeps the bot's own key. The owner reads each app's own name (Spaces, CRM, Tickets, Agent Space, Infrastructure) for Central's exact app names.
|
|
28
|
+
- **Spaces**: the conversational channel stays idle for a personal bot (no socket of its own); anything still sent through it, and the connected-apps plane watching the apps the owner ticked, goes out with her pass.
|
|
29
|
+
- **One-tap card** (SEC-28, `core/owner-setup.js`): when an app refuses a pass because she has not switched the bot on there, and a week before her consent ends, the owner gets a Kazee Chat card with one button that opens Central at the form (Switch me on / Renew); at most once a day per app and reason. `open-claudia owner-setup [--apps a,b] [--renew]` sends it on request; `open-claudia pass <app>` prints a fresh pass, never the key.
|
|
30
|
+
- **Instructions**: a personal bot gets its own block in the core instructions (who it works for, approvals, money and deletes need her tap, send the one-tap card).
|
|
31
|
+
|
|
32
|
+
Tests: `test-bot-acts-for-owner.js` (in the suite).
|
|
33
|
+
|
|
3
34
|
## 3.5.74
|
|
4
35
|
|
|
5
36
|
3.5.73 plus the Rust command helper, released on Sumeet's card of 8 Nov ("Yes, Maya Cloud first, then one bot at a time") after 3 clean days on his Mac (about 62,000 commands, no crashes).
|
package/Dockerfile
CHANGED
|
@@ -102,5 +102,7 @@ EXPOSE 8080
|
|
|
102
102
|
# Switch to non-root user
|
|
103
103
|
USER 1001
|
|
104
104
|
|
|
105
|
+
# OC_NODE_FLAGS in a bot's env: docker-entrypoint.sh puts those flags on this
|
|
106
|
+
# node command only. Unset, the command below runs as is.
|
|
105
107
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
|
106
108
|
CMD ["node", "bin/cli.js", "web"]
|
package/bin/cli.js
CHANGED
|
@@ -218,6 +218,41 @@ switch (command) {
|
|
|
218
218
|
break;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
case "pass": {
|
|
222
|
+
// A fresh pass for one app (core/app-pass.js), for a tool that calls an
|
|
223
|
+
// app for a personal bot's owner: e.g. `open-claudia pass kazee-spaces-backend`.
|
|
224
|
+
// Never prints the bot's own key: an autonomous bot has no pass to give.
|
|
225
|
+
const applicationName = args[1];
|
|
226
|
+
if (!applicationName) {
|
|
227
|
+
console.error("Usage: open-claudia pass <Central application name, e.g. kazee-spaces-backend>");
|
|
228
|
+
process.exit(2);
|
|
229
|
+
}
|
|
230
|
+
(async () => {
|
|
231
|
+
const botProfile = require(path.join(botDir, "core/bot-profile"));
|
|
232
|
+
const appPass = require(path.join(botDir, "core/app-pass"));
|
|
233
|
+
await botProfile.refresh();
|
|
234
|
+
if (!botProfile.isPersonal()) {
|
|
235
|
+
console.error("Only a personal bot uses passes; this bot calls apps with its own key.");
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
process.stdout.write(await appPass.tokenFor(applicationName));
|
|
240
|
+
process.stdout.write("\n");
|
|
241
|
+
process.exit(0);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error(e instanceof appPass.PassRefused ? e.ownerMessage : e.message);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case "owner-setup": {
|
|
251
|
+
// The one-tap card for a personal bot's owner (core/owner-setup.js).
|
|
252
|
+
require("./owner-setup").run(args.slice(1)).then((code) => process.exit(code));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
case "health": {
|
|
222
257
|
console.log("Running health checks...\n");
|
|
223
258
|
const verbose = args.includes("-v") || args.includes("--verbose");
|
|
@@ -459,6 +494,10 @@ switch (command) {
|
|
|
459
494
|
require("./cards").runDecide(args.slice(1));
|
|
460
495
|
break;
|
|
461
496
|
}
|
|
497
|
+
case "view": {
|
|
498
|
+
require("./views").runView(args.slice(1));
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
462
501
|
case "teach": {
|
|
463
502
|
Promise.resolve(require("./teach").run(args.slice(1)))
|
|
464
503
|
.catch((e) => { console.error(`teach command failed: ${e.message}`); process.exitCode = 1; });
|
|
@@ -647,6 +686,7 @@ Send tools (only work inside an active bot-spawned task):
|
|
|
647
686
|
open-claudia card pieces List the pieces a card is built from (table, tick, date, slots, rows, board...) and their examples
|
|
648
687
|
open-claudia card update <id> ... Close/open a card you sent, add a note, or replace its spec (card example <name> for specs)
|
|
649
688
|
open-claudia decide "<q>" --option id="label" ... Queue a decision card for the owner (one at a time; decide list|cancel)
|
|
689
|
+
open-claudia view send <page.html> --title "<t>" [--handler <file>] Send an interactive web page (Kazee Chat); view push|example
|
|
650
690
|
open-claudia teach list|show|finish|cancel <id> Browser recordings from Teach me; finish deletes the pictures, cancel the whole recording
|
|
651
691
|
|
|
652
692
|
Memory tools:
|
|
@@ -679,12 +719,19 @@ Background work (only inside an active bot-spawned task):
|
|
|
679
719
|
Cluster control (self-management of this bot's own deployment via the broker):
|
|
680
720
|
open-claudia cluster status|logs|restart|start|stop|scale <0|1>|sync
|
|
681
721
|
|
|
722
|
+
Personal bots (acting for their owner):
|
|
723
|
+
open-claudia pass <app> A fresh 10-minute pass to act as your owner at one app
|
|
724
|
+
(e.g. kazee-spaces-backend); never your own key
|
|
725
|
+
open-claudia owner-setup [--apps a,b] [--renew] Send your owner a one-tap card in Kazee Chat to switch
|
|
726
|
+
you on (default: apps not switched on yet) or renew
|
|
727
|
+
|
|
682
728
|
Multi-user / cross-channel:
|
|
683
729
|
open-claudia people list|show|add|note|link Roster of known team members
|
|
684
730
|
open-claudia intros list|approve|reject Pending introductions from unknown chats
|
|
685
731
|
open-claudia auth list|revoke <chatId> Raw authorization view + direct deauth
|
|
686
732
|
open-claudia send-to --person "<name>" "<msg>" Relay a message to another team member
|
|
687
733
|
open-claudia handoff --thread <id> "<msg>" Post in another Kazee thread and wake it to start work
|
|
734
|
+
open-claudia handoff --new "<title>" "<msg>" Start a titled thread, tag the asker, hand off into it
|
|
688
735
|
([--chat <chatId>] [--title "<label>"]; owner only)
|
|
689
736
|
open-claudia recent --person "<name>" [--limit] Read recent activity from another chat
|
|
690
737
|
|
package/bin/handoff.js
CHANGED
|
@@ -9,8 +9,12 @@ Hand work to another Kazee thread: post the message there (visibly, as the
|
|
|
9
9
|
bot) and wake that thread's conversation to start on it now.
|
|
10
10
|
|
|
11
11
|
open-claudia handoff --thread <threadId> "<message>" [--chat <chatId>] [--title "<short label>"]
|
|
12
|
+
open-claudia handoff --new "<thread title>" "<message>" [--chat <chatId>]
|
|
12
13
|
|
|
13
14
|
--thread the target thread's id (its root message id)
|
|
15
|
+
--new start a NEW titled thread (1-80 characters) and hand off into it;
|
|
16
|
+
the person who asked is tagged so it shows in their Threads list.
|
|
17
|
+
Never post a plain main-chat message and hand off onto that.
|
|
14
18
|
--chat the Kazee chat the thread is in (default: this conversation's chat)
|
|
15
19
|
--title a short label for the wake-up job (cron-list shows it)
|
|
16
20
|
|
|
@@ -35,6 +39,7 @@ async function run(argv) {
|
|
|
35
39
|
process.exit(args[0] === "--help" || args[0] === "help" ? 0 : 2);
|
|
36
40
|
}
|
|
37
41
|
const targetThreadId = takeFlag(args, "thread");
|
|
42
|
+
const newTitle = takeFlag(args, "new");
|
|
38
43
|
const targetChatId = takeFlag(args, "chat");
|
|
39
44
|
const title = takeFlag(args, "title");
|
|
40
45
|
if (args[0] && /^--[a-z]/.test(args[0])) {
|
|
@@ -42,16 +47,20 @@ async function run(argv) {
|
|
|
42
47
|
process.exit(2);
|
|
43
48
|
}
|
|
44
49
|
const message = args.join(" ").trim();
|
|
45
|
-
if (
|
|
50
|
+
if (targetThreadId && newTitle !== null) { console.error('Use --thread <threadId> or --new "<title>", not both.'); process.exit(2); }
|
|
51
|
+
if (!targetThreadId && newTitle === null) { console.error('Missing --thread <threadId> (an existing thread) or --new "<title>" (a new thread).'); process.exit(2); }
|
|
52
|
+
if (newTitle !== null && !newTitle.trim()) { console.error('--new needs a title: --new "<title>".'); process.exit(2); }
|
|
46
53
|
if (!message) { console.error("Missing message text."); process.exit(2); }
|
|
47
54
|
try {
|
|
48
|
-
const body = {
|
|
55
|
+
const body = { message };
|
|
56
|
+
if (targetThreadId) body.targetThreadId = targetThreadId;
|
|
57
|
+
if (newTitle !== null) body.newThreadTitle = newTitle;
|
|
49
58
|
if (targetChatId) body.targetChatId = targetChatId;
|
|
50
59
|
if (title) body.title = title;
|
|
51
60
|
const res = await postJson("thread-handoff", body);
|
|
52
61
|
const target = res.target || {};
|
|
53
62
|
const where = target.title ? `"${target.title}" (${target.threadId})` : `thread ${target.threadId || targetThreadId}`;
|
|
54
|
-
console.log(`Handed off to ${where}: posted message ${res.messageId || "(no id)"}, wake job ${res.jobId || "?"} (hand-off ${res.hop || "?"} of ${res.maxHops || 3} in a row).`);
|
|
63
|
+
console.log(`Handed off to ${res.newThread ? "new thread " : ""}${where}: posted message ${res.messageId || "(no id)"}, wake job ${res.jobId || "?"} (hand-off ${res.hop || "?"} of ${res.maxHops || 3} in a row).`);
|
|
55
64
|
process.exit(0);
|
|
56
65
|
} catch (e) {
|
|
57
66
|
console.error(e.message);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// open-claudia owner-setup — send a personal bot's owner the one-tap card that opens
|
|
2
|
+
// Central at the form to switch the bot on (or renew it). See
|
|
3
|
+
// core/owner-setup.js. The running bot sends it to the owner's direct Kazee chat.
|
|
4
|
+
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
Send your owner a one-tap card to switch you on, or to renew.
|
|
9
|
+
|
|
10
|
+
open-claudia owner-setup the apps you can work in that your owner hasn't switched on yet
|
|
11
|
+
open-claudia owner-setup --apps Tickets just these apps (Central names or Tickets, CRM, Spaces...)
|
|
12
|
+
open-claudia owner-setup --renew renew before your owner's permission ends
|
|
13
|
+
|
|
14
|
+
Personal bots only. You can never switch yourself on: only your owner can confirm.
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
function takeFlag(args, name) {
|
|
18
|
+
const idx = args.indexOf(`--${name}`);
|
|
19
|
+
if (idx < 0) return null;
|
|
20
|
+
const v = args[idx + 1];
|
|
21
|
+
args.splice(idx, 2);
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolves the exit code. deps (tests): refresh, post, log, error.
|
|
26
|
+
async function run(args, deps = {}) {
|
|
27
|
+
const log = deps.log || console.log;
|
|
28
|
+
const error = deps.error || console.error;
|
|
29
|
+
args = [...args];
|
|
30
|
+
if (args[0] === "--help" || args[0] === "help") { log(HELP); return 0; }
|
|
31
|
+
const botProfile = require(path.join(__dirname, "..", "core", "bot-profile"));
|
|
32
|
+
const ownerSetup = require(path.join(__dirname, "..", "core", "owner-setup"));
|
|
33
|
+
const renew = args.includes("--renew");
|
|
34
|
+
const apps = String(takeFlag(args, "apps") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
35
|
+
try {
|
|
36
|
+
await (deps.refresh || botProfile.refresh)();
|
|
37
|
+
} catch (_) { /* the last answer on disk */ }
|
|
38
|
+
const planned = ownerSetup.plan({ apps, renew });
|
|
39
|
+
if (planned.refused) { error(planned.refused); return 2; }
|
|
40
|
+
if (planned.nothing) { log(planned.nothing); return 0; }
|
|
41
|
+
try {
|
|
42
|
+
const post = deps.post || require("./loopback-client").postJson;
|
|
43
|
+
const res = await post("owner-setup", { apps, renew });
|
|
44
|
+
if (res.nothing) { log(res.nothing); return 0; }
|
|
45
|
+
const who = botProfile.ownerFirstName();
|
|
46
|
+
if (res.sent === "already-sent") log(`${who} already got this card in the last day; not sent again.`);
|
|
47
|
+
else if (res.sent === "card") log(`Sent ${who} a card in Kazee Chat: "${res.title}".`);
|
|
48
|
+
else log(`The card could not be sent, so ${who} got the text with the link instead.`);
|
|
49
|
+
return 0;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
error(`owner-setup: ${e.message}`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { run, HELP };
|
package/bin/views.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// `open-claudia view ...`: HTML views in Kazee Chat from inside a turn. A
|
|
2
|
+
// view is a web page sent as a card with an Open button; it opens in a
|
|
3
|
+
// walled-off panel and talks back with kazee.send / kazee.on (core/views.js
|
|
4
|
+
// answers its events).
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { postJson } = require("./loopback-client");
|
|
9
|
+
|
|
10
|
+
const FILE_MAX = 2 * 1024 * 1024;
|
|
11
|
+
const PUSH_MAX = 16384;
|
|
12
|
+
|
|
13
|
+
const VIEW_USAGE = `Usage:
|
|
14
|
+
open-claudia view send <page.html> --title "<title>" [--body "<text>"] [--label "<button>"]
|
|
15
|
+
[--preview <picture.png|.jpg>] [--handler <file>] [--wake each|none]
|
|
16
|
+
open-claudia view push <messageId> <json | -> [--to <userId>]
|
|
17
|
+
open-claudia view example [page|handler]
|
|
18
|
+
Views are Kazee Chat only. The page (.html, at most 2 MB) opens in a walled-off
|
|
19
|
+
panel: it may load scripts, styles and fonts from cdn.jsdelivr.net, unpkg.com,
|
|
20
|
+
cdnjs.cloudflare.com and esm.sh, but cannot fetch anything from the internet,
|
|
21
|
+
so put its data in the page. In the page:
|
|
22
|
+
kazee.send(name, data) send an event to you (name: a-z 0-9 _ . -, data under 4 KB as JSON)
|
|
23
|
+
kazee.on(data => ...) receive what you push (view push, or a handler's {"push": ...})
|
|
24
|
+
Events wake this conversation (--wake each, the default; events close together
|
|
25
|
+
come as one wake) or are dropped (--wake none). With --handler, a script runs
|
|
26
|
+
once per event instead (node for .js, else run directly; 10 s; its folder is the
|
|
27
|
+
working directory). It gets the event as JSON on stdin
|
|
28
|
+
{"messageId","chatId","userId","userName","name","data","at"}
|
|
29
|
+
and each line it prints that is JSON is an order:
|
|
30
|
+
{"push": <data>, "to": "<userId>"} back to the page ("to" optional: only that person)
|
|
31
|
+
{"wake": "<text>"} wake this conversation
|
|
32
|
+
Never put answers you check (a quiz key) in the page: people can read it. Keep them in the handler.`;
|
|
33
|
+
|
|
34
|
+
const EXAMPLE_PAGE = `<!doctype html>
|
|
35
|
+
<html>
|
|
36
|
+
<head>
|
|
37
|
+
<meta charset="utf-8">
|
|
38
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39
|
+
<title>Quick quiz</title>
|
|
40
|
+
<style>
|
|
41
|
+
body { font: 16px/1.4 system-ui, sans-serif; margin: 0; padding: 16px; background: #f7f7f8; color: #1d1d1f; }
|
|
42
|
+
.q { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 12px; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
|
|
43
|
+
.q h3 { margin: 0 0 10px; font-size: 16px; }
|
|
44
|
+
button { display: block; width: 100%; text-align: left; margin: 6px 0; padding: 10px 12px; border: 1px solid #d0d0d6;
|
|
45
|
+
border-radius: 8px; background: #fff; font: inherit; cursor: pointer; }
|
|
46
|
+
button:disabled { cursor: default; opacity: .7; }
|
|
47
|
+
button.right { border-color: #2e9d57; background: #e7f6ec; opacity: 1; }
|
|
48
|
+
button.wrong { border-color: #d0453b; background: #fbeceb; opacity: 1; }
|
|
49
|
+
.note { font-size: 14px; color: #555; min-height: 1.2em; }
|
|
50
|
+
#score { font-weight: 600; text-align: center; }
|
|
51
|
+
</style>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div id="quiz"></div>
|
|
55
|
+
<p id="score"></p>
|
|
56
|
+
<script>
|
|
57
|
+
// The questions only. The right answers stay in the handler: anyone can read this page.
|
|
58
|
+
const QUESTIONS = [
|
|
59
|
+
{ id: "q1", text: "Which planet is closest to the sun?", options: ["Venus", "Mercury", "Mars"] },
|
|
60
|
+
{ id: "q2", text: "How many legs does a spider have?", options: ["6", "8", "10"] },
|
|
61
|
+
{ id: "q3", text: "What is the boiling point of water at sea level?", options: ["90 °C", "100 °C", "120 °C"] },
|
|
62
|
+
];
|
|
63
|
+
const results = {};
|
|
64
|
+
const quiz = document.getElementById("quiz");
|
|
65
|
+
for (const q of QUESTIONS) {
|
|
66
|
+
const box = document.createElement("div");
|
|
67
|
+
box.className = "q";
|
|
68
|
+
box.innerHTML = "<h3></h3><div class='opts'></div><div class='note'></div>";
|
|
69
|
+
box.querySelector("h3").textContent = q.text;
|
|
70
|
+
q.options.forEach((label, i) => {
|
|
71
|
+
const b = document.createElement("button");
|
|
72
|
+
b.textContent = label;
|
|
73
|
+
b.onclick = () => {
|
|
74
|
+
box.querySelectorAll("button").forEach((x) => { x.disabled = true; });
|
|
75
|
+
b.dataset.picked = "1";
|
|
76
|
+
box.querySelector(".note").textContent = "Checking...";
|
|
77
|
+
window.kazee.send("answer", { q: q.id, choice: i });
|
|
78
|
+
};
|
|
79
|
+
box.querySelector(".opts").appendChild(b);
|
|
80
|
+
});
|
|
81
|
+
box.id = q.id;
|
|
82
|
+
quiz.appendChild(box);
|
|
83
|
+
}
|
|
84
|
+
// The bot's replies: { q, correct, right, note } for each answer.
|
|
85
|
+
window.kazee.on((data) => {
|
|
86
|
+
if (!data || !data.q) return;
|
|
87
|
+
const box = document.getElementById(data.q);
|
|
88
|
+
if (!box) return;
|
|
89
|
+
const buttons = box.querySelectorAll("button");
|
|
90
|
+
buttons.forEach((b, i) => {
|
|
91
|
+
if (i === data.right) b.className = "right";
|
|
92
|
+
else if (b.dataset.picked) b.className = "wrong";
|
|
93
|
+
});
|
|
94
|
+
box.querySelector(".note").textContent = data.note || "";
|
|
95
|
+
results[data.q] = !!data.correct;
|
|
96
|
+
const done = Object.keys(results).length;
|
|
97
|
+
if (done === QUESTIONS.length) {
|
|
98
|
+
const score = Object.values(results).filter(Boolean).length;
|
|
99
|
+
document.getElementById("score").textContent = "You got " + score + " of " + QUESTIONS.length + ".";
|
|
100
|
+
window.kazee.send("done", { score, of: QUESTIONS.length });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
</script>
|
|
104
|
+
</body>
|
|
105
|
+
</html>
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
const EXAMPLE_HANDLER = `// handler.js: answers the quiz page's events. Sent with
|
|
109
|
+
// open-claudia view send quiz.html --title "Quick quiz" --handler handler.js
|
|
110
|
+
// It runs once per event: the event is JSON on stdin, each JSON line printed is an order.
|
|
111
|
+
const KEY = { q1: { right: 1, note: "Mercury is closest." },
|
|
112
|
+
q2: { right: 1, note: "Spiders have 8 legs." },
|
|
113
|
+
q3: { right: 1, note: "100 °C at sea level." } };
|
|
114
|
+
|
|
115
|
+
let input = "";
|
|
116
|
+
process.stdin.on("data", (c) => { input += c; });
|
|
117
|
+
process.stdin.on("end", () => {
|
|
118
|
+
const ev = JSON.parse(input);
|
|
119
|
+
const out = (order) => console.log(JSON.stringify(order));
|
|
120
|
+
if (ev.name === "answer" && ev.data && KEY[ev.data.q]) {
|
|
121
|
+
const k = KEY[ev.data.q];
|
|
122
|
+
// Only to the person who answered: others with the page open keep their own quiz.
|
|
123
|
+
out({ push: { q: ev.data.q, correct: ev.data.choice === k.right, right: k.right, note: k.note }, to: ev.userId });
|
|
124
|
+
} else if (ev.name === "done" && ev.data) {
|
|
125
|
+
// Wake the conversation once, when someone finishes.
|
|
126
|
+
out({ wake: \`\${ev.userName || "Someone"} finished the quiz: \${Number(ev.data.score)} of \${Number(ev.data.of)}.\` });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
function flag(args, name) {
|
|
132
|
+
const i = args.indexOf(name);
|
|
133
|
+
if (i < 0) return undefined;
|
|
134
|
+
const v = args[i + 1];
|
|
135
|
+
if (v === undefined || v.startsWith("--")) throw new Error(`${name} needs a value`);
|
|
136
|
+
args.splice(i, 2);
|
|
137
|
+
return v;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readFileFor(file, what, exts) {
|
|
141
|
+
const full = path.resolve(file);
|
|
142
|
+
const ext = path.extname(full).toLowerCase();
|
|
143
|
+
if (!exts.includes(ext)) throw new Error(`the ${what} must be a ${exts.join(" or ")} file: ${file}`);
|
|
144
|
+
let st;
|
|
145
|
+
try { st = fs.statSync(full); } catch (_) { throw new Error(`${what} not found: ${file}`); }
|
|
146
|
+
if (!st.isFile()) throw new Error(`${what} is not a file: ${file}`);
|
|
147
|
+
if (st.size === 0) throw new Error(`the ${what} is empty: ${file}`);
|
|
148
|
+
if (st.size > FILE_MAX) throw new Error(`the ${what} is over 2 MB: ${file}`);
|
|
149
|
+
return { fileName: path.basename(full), data: fs.readFileSync(full).toString("base64") };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `view send` arguments into the loopback body (throws on bad input). */
|
|
153
|
+
function parseSend(argv) {
|
|
154
|
+
const args = [...argv];
|
|
155
|
+
const title = flag(args, "--title");
|
|
156
|
+
const body = flag(args, "--body");
|
|
157
|
+
const label = flag(args, "--label");
|
|
158
|
+
const preview = flag(args, "--preview");
|
|
159
|
+
const handler = flag(args, "--handler");
|
|
160
|
+
const wake = flag(args, "--wake");
|
|
161
|
+
const unknown = args.find((a) => a.startsWith("--"));
|
|
162
|
+
if (unknown) throw new Error(`unknown option ${unknown}`);
|
|
163
|
+
if (args.length !== 1) throw new Error(args.length ? `one page only (got ${args.join(" ")})` : "missing the page (.html file)");
|
|
164
|
+
if (!title || !title.trim()) throw new Error("--title is required");
|
|
165
|
+
if (title.trim().length > 140) throw new Error("--title is over 140 characters");
|
|
166
|
+
if (body && body.length > 600) throw new Error("--body is over 600 characters");
|
|
167
|
+
if (label && label.trim().length > 40) throw new Error("--label is over 40 characters");
|
|
168
|
+
if (wake !== undefined && !["each", "none"].includes(wake)) throw new Error("--wake must be each or none");
|
|
169
|
+
let handlerPath = null;
|
|
170
|
+
if (handler) {
|
|
171
|
+
handlerPath = path.resolve(handler);
|
|
172
|
+
let st = null;
|
|
173
|
+
try { st = fs.statSync(handlerPath); } catch (_) {}
|
|
174
|
+
if (!st || !st.isFile()) throw new Error(`handler not found: ${handler}`);
|
|
175
|
+
if (!/\.(c|m)?js$/i.test(handlerPath)) {
|
|
176
|
+
try { fs.accessSync(handlerPath, fs.constants.X_OK); }
|
|
177
|
+
catch (_) { throw new Error(`the handler is not a .js file and not executable (chmod +x it): ${handler}`); }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
title: title.trim(),
|
|
182
|
+
...(body ? { body } : {}),
|
|
183
|
+
...(label ? { label: label.trim() } : {}),
|
|
184
|
+
...(wake ? { wake } : {}),
|
|
185
|
+
handler: handlerPath,
|
|
186
|
+
page: readFileFor(args[0], "page", [".html", ".htm"]),
|
|
187
|
+
...(preview ? { preview: readFileFor(preview, "preview", [".png", ".jpg", ".jpeg"]) } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `view push` arguments into the loopback body; `readStdin` for "-". */
|
|
192
|
+
function parsePush(argv, readStdin = () => fs.readFileSync(0, "utf8")) {
|
|
193
|
+
const args = [...argv];
|
|
194
|
+
const to = flag(args, "--to");
|
|
195
|
+
const unknown = args.find((a) => a.startsWith("--"));
|
|
196
|
+
if (unknown) throw new Error(`unknown option ${unknown}`);
|
|
197
|
+
const [messageId, raw] = args;
|
|
198
|
+
if (!messageId) throw new Error("missing the view's message id");
|
|
199
|
+
if (raw === undefined) throw new Error("missing the data (JSON, or - for stdin)");
|
|
200
|
+
if (args.length > 2) throw new Error("too many arguments (quote the JSON)");
|
|
201
|
+
const text = raw === "-" ? readStdin() : raw;
|
|
202
|
+
let data;
|
|
203
|
+
try { data = JSON.parse(text); }
|
|
204
|
+
catch (_) { throw new Error(`the data is not JSON (a string needs quotes: '"hello"')`); }
|
|
205
|
+
if (Buffer.byteLength(JSON.stringify(data)) > PUSH_MAX) throw new Error("the data is over 16 KB as JSON");
|
|
206
|
+
return { messageId, data, ...(to ? { to } : {}) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function exampleText(which) {
|
|
210
|
+
if (which === "page") return EXAMPLE_PAGE;
|
|
211
|
+
if (which === "handler") return EXAMPLE_HANDLER;
|
|
212
|
+
if (which) throw new Error("examples: page, handler (or nothing for both)");
|
|
213
|
+
return [
|
|
214
|
+
"# quiz.html (open-claudia view example page > quiz.html)",
|
|
215
|
+
EXAMPLE_PAGE,
|
|
216
|
+
"# handler.js (open-claudia view example handler > handler.js)",
|
|
217
|
+
EXAMPLE_HANDLER,
|
|
218
|
+
'# Send it: open-claudia view send quiz.html --title "Quick quiz" --handler handler.js',
|
|
219
|
+
"",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function fail(msg, code = 1) {
|
|
224
|
+
console.error(msg);
|
|
225
|
+
process.exit(code);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function runView(argv) {
|
|
229
|
+
const args = [...argv];
|
|
230
|
+
const sub = args.shift();
|
|
231
|
+
try {
|
|
232
|
+
if (sub === "example") {
|
|
233
|
+
process.stdout.write(exampleText(args[0]));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (sub === "send") {
|
|
237
|
+
const body = parseSend(args);
|
|
238
|
+
const res = await postJson("view-send", body);
|
|
239
|
+
const events = res.handler ? `events run ${res.handler}` : `events wake this conversation: ${res.wake}`;
|
|
240
|
+
console.log(`Sent view ${res.messageId} (${events})`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (sub === "push") {
|
|
244
|
+
const body = parsePush(args);
|
|
245
|
+
await postJson("view-push", body);
|
|
246
|
+
console.log(`Pushed to view ${body.messageId}${body.to ? ` (only to ${body.to})` : ""}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
fail(VIEW_USAGE, sub && sub !== "help" && sub !== "--help" ? 1 : 0);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
fail(`view ${sub}: ${e.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = { runView, parseSend, parsePush, exampleText, VIEW_USAGE, EXAMPLE_PAGE, EXAMPLE_HANDLER };
|
package/bot.js
CHANGED
|
@@ -264,6 +264,42 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
264
264
|
|
|
265
265
|
// ── Boot ────────────────────────────────────────────────────────────
|
|
266
266
|
|
|
267
|
+
// Tell a personal bot's owner in her Kazee DM that an app refused a pass: a
|
|
268
|
+
// one-tap card to switch the bot on there, else the text (core/owner-setup.js).
|
|
269
|
+
// Best effort: no Kazee adapter or DM means no message.
|
|
270
|
+
async function tellPersonalOwner(text, err) {
|
|
271
|
+
const ownerSetup = require("./core/owner-setup");
|
|
272
|
+
const kazee = adapters.find((a) => a.type === "kazee");
|
|
273
|
+
if (kazee) await ownerSetup.tellOwner(ownerSetup.forRefusal(err, text), { adapter: kazee });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A week before the consent ends: the renew card, once a day.
|
|
277
|
+
async function maybeSendRenewNotice() {
|
|
278
|
+
const kazee = adapters.find((a) => a.type === "kazee");
|
|
279
|
+
if (kazee) await require("./core/owner-setup").maybeSendRenew({ adapter: kazee });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function startBotProfile() {
|
|
283
|
+
const botProfile = require("./core/bot-profile");
|
|
284
|
+
const applyOwner = () => {
|
|
285
|
+
const owner = botProfile.applyOwner(config);
|
|
286
|
+
if (!owner) return;
|
|
287
|
+
for (const a of adapters) {
|
|
288
|
+
if (a.type === "kazee" || a.type === "spaces") a.ownerUserId = owner;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
await botProfile.refresh();
|
|
292
|
+
applyOwner();
|
|
293
|
+
console.log(botProfile.isPersonal()
|
|
294
|
+
? `Bot profile: personal, owner ${botProfile.ownerName() || botProfile.ownerUserId()}; apps ticked: ${botProfile.consentApplications().join(", ") || "none"}`
|
|
295
|
+
: `Bot profile: ${botProfile.get()?.bot?.botKind || "unknown (Central has no bot:me)"}`);
|
|
296
|
+
require("./core/app-pass").setOwnerNotifier(tellPersonalOwner);
|
|
297
|
+
botProfile.startRefreshing(applyOwner);
|
|
298
|
+
const renewTimer = setInterval(() => { maybeSendRenewNotice(); }, 60 * 60 * 1000);
|
|
299
|
+
if (renewTimer.unref) renewTimer.unref();
|
|
300
|
+
setTimeout(() => { maybeSendRenewNotice(); }, 60 * 1000).unref?.();
|
|
301
|
+
}
|
|
302
|
+
|
|
267
303
|
// Minimal boot: adapters + slash commands only. No scheduler/crons, no dream,
|
|
268
304
|
// no connected-apps, no loopback, no web UI, no onboarding — nothing that can
|
|
269
305
|
// run an agent turn or write memory. The alert tells the owner exactly why and
|
|
@@ -332,6 +368,12 @@ async function safeModeBoot() {
|
|
|
332
368
|
}
|
|
333
369
|
} catch (e) {}
|
|
334
370
|
|
|
371
|
+
// Personal or autonomous, and who the one owner is: asked of Central before
|
|
372
|
+
// the adapters start (core/bot-profile.js). A personal bot's app calls go
|
|
373
|
+
// through passes for that owner (core/app-pass.js).
|
|
374
|
+
try { await startBotProfile(); }
|
|
375
|
+
catch (e) { console.error("bot profile init failed:", e.message); }
|
|
376
|
+
|
|
335
377
|
initScheduler(adapters);
|
|
336
378
|
try { require("./core/dream").initDream(adapters); }
|
|
337
379
|
catch (e) { console.error("dream init failed:", e.message); }
|
|
@@ -546,6 +546,25 @@ class KazeeAdapter {
|
|
|
546
546
|
raw: payload,
|
|
547
547
|
});
|
|
548
548
|
});
|
|
549
|
+
// HTML views: someone in the chat sent an event from a view page this bot
|
|
550
|
+
// sent (kazee.send in the page). Routed like a card answer; core/views.js
|
|
551
|
+
// runs the view's handler or wakes the conversation the view came from.
|
|
552
|
+
socket.on("view:event", (payload) => {
|
|
553
|
+
if (!payload || !payload.messageId) return;
|
|
554
|
+
const userId = payload.userId ? String(payload.userId) : "";
|
|
555
|
+
this._emit("action", {
|
|
556
|
+
adapter: this,
|
|
557
|
+
channelId: String(payload.chatId || ""),
|
|
558
|
+
canonicalUserId: userId ? canonicalForChannel("kazee", userId) : null,
|
|
559
|
+
userId: userId || null,
|
|
560
|
+
type: "action",
|
|
561
|
+
messageId: payload.messageId,
|
|
562
|
+
threadId: payload.threadRoot ? String(payload.threadRoot) : null,
|
|
563
|
+
action: { buttonId: `kv:${payload.messageId}`, payload: `kv:${payload.messageId}`, sourceMessageId: payload.messageId },
|
|
564
|
+
view: payload,
|
|
565
|
+
raw: payload,
|
|
566
|
+
});
|
|
567
|
+
});
|
|
549
568
|
socket.on("message:action", (payload) => {
|
|
550
569
|
const channelId = String(payload.chatId);
|
|
551
570
|
const known = this._sentMessages.has(String(payload.messageId || ""));
|
|
@@ -733,6 +752,18 @@ class KazeeAdapter {
|
|
|
733
752
|
// What the bot reads to name a thread: the root's text, the first replies
|
|
734
753
|
// (the bot's own marked) and the title the thread already has, when the
|
|
735
754
|
// server sends one. Throws like _fetchThread.
|
|
755
|
+
// Start a real thread (chat-central startThread): a titled header root the
|
|
756
|
+
// chat's bots never hear. Followers are the starter plus whoever the first
|
|
757
|
+
// reply tags, so callers tag the person the thread is for. Resolves the
|
|
758
|
+
// root message id; throws when chat-central refuses.
|
|
759
|
+
async startThread(chatId, title) {
|
|
760
|
+
const route = `/thread/${encodeURIComponent(String(chatId))}/start`;
|
|
761
|
+
const res = await withTimeout(this._request("POST", route, { userId: String(this.botUserId), title: String(title || "") }), THREAD_TITLE_TIMEOUT_MS, "Kazee thread start");
|
|
762
|
+
const root = res && ((res.message && res.message._id) || (res.data && res.data._id));
|
|
763
|
+
if (!root) throw new Error(`Kazee thread start gave no root id${res && res.error ? `: ${res.error}` : ""}`);
|
|
764
|
+
return String(root);
|
|
765
|
+
}
|
|
766
|
+
|
|
736
767
|
async threadTitleSource(chatId, rootId) {
|
|
737
768
|
const data = await this._fetchThread(String(chatId), String(rootId), THREAD_TITLE_SOURCE_LIMIT);
|
|
738
769
|
const root = data.root && typeof data.root === "object" ? data.root : null;
|
|
@@ -1435,6 +1466,9 @@ class KazeeAdapter {
|
|
|
1435
1466
|
};
|
|
1436
1467
|
if (opts.replyTo) body.replyTo = opts.replyTo;
|
|
1437
1468
|
if (buttons) body.interactive = { buttons };
|
|
1469
|
+
// People tagged as @[Name](id) in the text (a hand-off into a new thread
|
|
1470
|
+
// tags whoever asked, so it shows in their Threads list and as unread).
|
|
1471
|
+
if (Array.isArray(opts.mentions) && opts.mentions.length) body.mentions = opts.mentions.map(String);
|
|
1438
1472
|
const threadId = opts.threadId ? String(opts.threadId) : null;
|
|
1439
1473
|
if (threadId) body.threadRootId = threadId;
|
|
1440
1474
|
// Tags of other bots (./bot-handoffs.js). chat-central reads mentions out
|
|
@@ -1574,6 +1608,32 @@ class KazeeAdapter {
|
|
|
1574
1608
|
return view;
|
|
1575
1609
|
}
|
|
1576
1610
|
|
|
1611
|
+
// Upload one file as chat media (chat-central `uploadMedia`) and answer its
|
|
1612
|
+
// media id. For HTML views: the page and its preview picture go up first,
|
|
1613
|
+
// then the card names them by id. Throws when the server keeps no id.
|
|
1614
|
+
async uploadMedia(channelId, { buffer, fileName, contentType } = {}) {
|
|
1615
|
+
const upload = await this._uploadMultipart(
|
|
1616
|
+
`/chat/media/${encodeURIComponent(channelId)}`,
|
|
1617
|
+
{ media: { buffer, fileName, contentType } },
|
|
1618
|
+
);
|
|
1619
|
+
const saved = Array.isArray(upload?.media) ? upload.media[0] : null;
|
|
1620
|
+
if (!saved || !saved._id) {
|
|
1621
|
+
const why = Array.isArray(upload?.errors) && upload.errors[0]?.error ? `: ${upload.errors[0].error}` : "";
|
|
1622
|
+
throw new Error(`Kazee Chat kept no media id for ${fileName}${why}`);
|
|
1623
|
+
}
|
|
1624
|
+
return String(saved._id);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// HTML views: hand data to an open view page (kazee.on in the page).
|
|
1628
|
+
// Only the message's sender may push; `to` limits it to one person.
|
|
1629
|
+
async pushView(messageId, data, to) {
|
|
1630
|
+
const body = { data: data === undefined ? null : data };
|
|
1631
|
+
if (to) body.to = String(to);
|
|
1632
|
+
const res = await this._request("PUT", `/message/${encodeURIComponent(messageId)}/view/push`, body);
|
|
1633
|
+
if (res && res.success === false) throw new Error(res.error || "view push refused");
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1577
1637
|
// A card as the server shows it now (its state: open, closed, decided),
|
|
1578
1638
|
// read through the one-message lookup. Null when the message is gone.
|
|
1579
1639
|
async readCard(chatId, messageId) {
|
|
@@ -2083,7 +2143,7 @@ class KazeeAdapter {
|
|
|
2083
2143
|
const boundary = `----oc-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2084
2144
|
const parts = [];
|
|
2085
2145
|
for (const [field, info] of Object.entries(files)) {
|
|
2086
|
-
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${field}"; filename="${info.fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`));
|
|
2146
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${field}"; filename="${info.fileName}"\r\nContent-Type: ${info.contentType || "application/octet-stream"}\r\n\r\n`));
|
|
2087
2147
|
parts.push(info.buffer);
|
|
2088
2148
|
parts.push(Buffer.from("\r\n"));
|
|
2089
2149
|
}
|
|
@@ -11,7 +11,6 @@ const fs = require("fs");
|
|
|
11
11
|
const os = require("os");
|
|
12
12
|
const path = require("path");
|
|
13
13
|
const { spawn } = require("child_process");
|
|
14
|
-
const { dePacketizeRtpPackets } = require("werift");
|
|
15
14
|
|
|
16
15
|
const STILL_EVERY_S = 2;
|
|
17
16
|
const STILL_SIZE = 768;
|
|
@@ -152,7 +151,7 @@ class CameraStills {
|
|
|
152
151
|
if ((packets[index].header.sequenceNumber - packets[index - 1].header.sequenceNumber + 65536) % 65536 !== 1) return this._lost();
|
|
153
152
|
}
|
|
154
153
|
let frame;
|
|
155
|
-
try { frame = dePacketizeRtpPackets("VP8", packets); } catch (_) { return this._lost(); }
|
|
154
|
+
try { frame = require("werift").dePacketizeRtpPackets("VP8", packets); } catch (_) { return this._lost(); }
|
|
156
155
|
if (!frame?.data?.length) return;
|
|
157
156
|
const size = frame.isKeyframe ? vp8KeyframeSize(frame.data) : null;
|
|
158
157
|
if (this._needKeyframe) {
|