@songsid/agend 2.2.0-beta.17 → 2.2.0-beta.18
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/README.md +18 -18
- package/README.zh-TW.md +18 -18
- package/dist/agent-whats-new.js +3 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/backend/kiro.d.ts +2 -0
- package/dist/backend/kiro.js +4 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +5 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/compact-watch.d.ts +9 -1
- package/dist/compact-watch.js +47 -6
- package/dist/compact-watch.js.map +1 -1
- package/dist/config-validator.js +26 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +65 -5
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.js +49 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/locale.js +4 -4
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +6 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/ui/chat-render.js +31 -6
- package/dist/ui/chat-store.js +5 -5
- package/dist/ui/chat-strings.js +6 -2
- package/dist/ui/chat-thread.js +15 -2
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-settings.js +1 -1
- package/dist/ui/shared/app.css +7 -0
- package/dist/web-api.d.ts +4 -0
- package/dist/web-api.js +135 -43
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +14 -0
- package/dist/web-chat-history.js +18 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-upload.d.ts +112 -1
- package/dist/web-upload.js +237 -36
- package/dist/web-upload.js.map +1 -1
- package/package.json +3 -3
|
@@ -4,6 +4,9 @@ import { applyWebSecurityHeaders } from "./web-host-guard.js";
|
|
|
4
4
|
import { isServedAsset } from "./auth-api.js";
|
|
5
5
|
import { shellRoute } from "./web-shell-routes.js";
|
|
6
6
|
import { isWebIconPath } from "./web-icons.js";
|
|
7
|
+
import { UPLOAD_REQUEST_TIMEOUT_MS } from "./web-upload.js";
|
|
8
|
+
/** #1937: how long a public-link request other than an upload may take to arrive whole (the gateway's old requestTimeout). */
|
|
9
|
+
export const PUBLIC_GATEWAY_RECEIVE_MS = 10_000;
|
|
7
10
|
/** Reviewed panel routes. No health, agent, issue-code, preview, legacy restart or SSE. */
|
|
8
11
|
export function isPublicWebRoute(method, path) {
|
|
9
12
|
const reads = method === "GET" || method === "HEAD";
|
|
@@ -112,7 +115,9 @@ export function createPublicWebGateway(opts) {
|
|
|
112
115
|
});
|
|
113
116
|
server.maxHeadersCount = 64;
|
|
114
117
|
server.headersTimeout = 5_000;
|
|
115
|
-
|
|
118
|
+
// #1937: an upload may take long on a slow link (its own clocks bound it); every other request is still dropped if it
|
|
119
|
+
// has not arrived whole in PUBLIC_GATEWAY_RECEIVE_MS (limitRequestReceive, where the request is dispatched).
|
|
120
|
+
server.requestTimeout = UPLOAD_REQUEST_TIMEOUT_MS;
|
|
116
121
|
server.keepAliveTimeout = 5_000;
|
|
117
122
|
server.setTimeout(15_000, socket => socket.destroy());
|
|
118
123
|
server.on("connection", socket => { if (sockets.size >= 32) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-web-gateway.js","sourceRoot":"","sources":["../src/public-web-gateway.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0D,MAAM,WAAW,CAAC;AAEjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"public-web-gateway.js","sourceRoot":"","sources":["../src/public-web-gateway.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -345,6 +345,14 @@ export interface WebConfig {
|
|
|
345
345
|
echo_to_channel?: boolean;
|
|
346
346
|
/** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
|
|
347
347
|
usage_panel?: boolean;
|
|
348
|
+
/**
|
|
349
|
+
* #1937: the largest file the web chat takes, in MB (whole number, 1–500; default 50). Never more than
|
|
350
|
+
* upload_max_total_mb; over the public link a file is at most 95 MB whatever this says (the quick tunnel's limit).
|
|
351
|
+
* Read on each upload: `agend reload` applies a change; an open page uses the new limits after it reloads.
|
|
352
|
+
*/
|
|
353
|
+
upload_max_file_mb?: number;
|
|
354
|
+
/** #1937: all files on one web chat message together, in MB (whole number, 1–500; default 100). At most 5 files. */
|
|
355
|
+
upload_max_total_mb?: number;
|
|
348
356
|
/**
|
|
349
357
|
* #1218: the instance Log tab (紀錄) — each agent's steps (prompts, replies, tool calls and results, errors) read on
|
|
350
358
|
* demand from its own transcript, secrets masked. Default false: off, it does no work at all. Turning it on is an
|
package/dist/ui/chat-render.js
CHANGED
|
@@ -489,8 +489,32 @@
|
|
|
489
489
|
return draft ? failedText + "\n" + draft : failedText;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
/** The upload limits the server enforces (web-upload.ts UPLOAD_LIMITS)
|
|
493
|
-
var FILE_LIMITS = { maxFileBytes:
|
|
492
|
+
/** The defaults of the upload limits the server enforces (web-upload.ts UPLOAD_LIMITS), for a page that was given none. */
|
|
493
|
+
var FILE_LIMITS = { maxFileBytes: 50 * 1024 * 1024, maxFiles: 5, maxTotalBytes: 100 * 1024 * 1024 };
|
|
494
|
+
/**
|
|
495
|
+
* #1937: the limits in force for this page — the server's (web.upload_max_file_mb / _total_mb, and the public link's
|
|
496
|
+
* own cap), on <body data-upload-limits>. Checked here first only to say so early: the server checks every upload.
|
|
497
|
+
*/
|
|
498
|
+
function fileLimits() {
|
|
499
|
+
try {
|
|
500
|
+
var raw = JSON.parse(document.body.dataset.uploadLimits || "null");
|
|
501
|
+
var ok = function (n) { return typeof n === "number" && isFinite(n) && n > 0; };
|
|
502
|
+
if (raw && ok(raw.maxFileBytes) && ok(raw.maxFiles) && ok(raw.maxTotalBytes)) {
|
|
503
|
+
return { maxFileBytes: raw.maxFileBytes, maxFiles: raw.maxFiles, maxTotalBytes: raw.maxTotalBytes, publicLinkCapped: raw.publicLinkCapped === true };
|
|
504
|
+
}
|
|
505
|
+
} catch (e) { /* no page, or no limits on it */ }
|
|
506
|
+
return { maxFileBytes: FILE_LIMITS.maxFileBytes, maxFiles: FILE_LIMITS.maxFiles, maxTotalBytes: FILE_LIMITS.maxTotalBytes, publicLinkCapped: false };
|
|
507
|
+
}
|
|
508
|
+
/** A size in MB as the limits are worded ("50", "2.5") — web-upload.ts mbText. */
|
|
509
|
+
function mbText(bytes) { return String(Math.round((bytes / 1024 / 1024) * 10) / 10); }
|
|
510
|
+
/** #1937: the words for a file checkFiles() refused — a chat string key and the numbers it says, from fileLimits(). */
|
|
511
|
+
function fileRejection(reason) {
|
|
512
|
+
var l = fileLimits();
|
|
513
|
+
if (reason === "too-large") return { key: l.publicLinkCapped ? "chat.file_too_large_public" : "chat.file_too_large", values: [mbText(l.maxFileBytes)] };
|
|
514
|
+
if (reason === "too-many") return { key: "chat.file_too_many", values: [l.maxFiles] };
|
|
515
|
+
if (reason === "too-much") return { key: "chat.file_too_much", values: [mbText(l.maxTotalBytes)] };
|
|
516
|
+
return { key: "chat.file_" + String(reason).replace(/-/g, "_"), values: [] };
|
|
517
|
+
}
|
|
494
518
|
|
|
495
519
|
function formatSize(n) {
|
|
496
520
|
var b = Number(n) || 0;
|
|
@@ -504,6 +528,7 @@
|
|
|
504
528
|
* Returns the files kept (pending first, then the accepted new ones) and, for each refused one, why.
|
|
505
529
|
*/
|
|
506
530
|
function checkFiles(pending, adding) {
|
|
531
|
+
var limits = fileLimits();
|
|
507
532
|
var kept = (pending || []).slice();
|
|
508
533
|
var total = 0;
|
|
509
534
|
kept.forEach(function (f) { total += Number(f.size) || 0; });
|
|
@@ -511,9 +536,9 @@
|
|
|
511
536
|
(adding || []).forEach(function (f) {
|
|
512
537
|
var size = Number(f.size) || 0;
|
|
513
538
|
if (size === 0) rejected.push({ name: f.name, reason: "empty" });
|
|
514
|
-
else if (size >
|
|
515
|
-
else if (kept.length >=
|
|
516
|
-
else if (total + size >
|
|
539
|
+
else if (size > limits.maxFileBytes) rejected.push({ name: f.name, reason: "too-large" });
|
|
540
|
+
else if (kept.length >= limits.maxFiles) rejected.push({ name: f.name, reason: "too-many" });
|
|
541
|
+
else if (total + size > limits.maxTotalBytes) rejected.push({ name: f.name, reason: "too-much" });
|
|
517
542
|
else { kept.push(f); total += size; }
|
|
518
543
|
});
|
|
519
544
|
return { kept: kept, rejected: rejected };
|
|
@@ -586,7 +611,7 @@
|
|
|
586
611
|
renderMarkdown: renderMarkdown, escapeHtml: escapeHtml, mergeMessages: mergeMessages, applyReplyButtons: applyReplyButtons, composerKey: composerKey,
|
|
587
612
|
pollRemaining: pollRemaining,
|
|
588
613
|
settleFailedSend: settleFailedSend, putBack: putBack,
|
|
589
|
-
FILE_LIMITS: FILE_LIMITS, formatSize: formatSize, checkFiles: checkFiles, attachmentsHtml: attachmentsHtml, isHtmlAttachment: isHtmlAttachment,
|
|
614
|
+
FILE_LIMITS: FILE_LIMITS, fileLimits: fileLimits, fileRejection: fileRejection, formatSize: formatSize, checkFiles: checkFiles, attachmentsHtml: attachmentsHtml, isHtmlAttachment: isHtmlAttachment,
|
|
590
615
|
nextDeliveryState: nextDeliveryState, applyDelivery: applyDelivery, deliveryHtml: deliveryHtml, isBusy: isBusy,
|
|
591
616
|
isNearBottom: isNearBottom, CODE_FOLD_LINES: CODE_FOLD_LINES, lineCount: lineCount,
|
|
592
617
|
LONG_PASTE_CHARS: LONG_PASTE_CHARS, isLongPaste: isLongPaste, formatElapsed: formatElapsed,
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -269,7 +269,7 @@ export function createChatStore(deps) {
|
|
|
269
269
|
const inFlight = s.inFlightFiles[target] || [];
|
|
270
270
|
const res = R().checkFiles(inFlight.concat(s.pendingFiles[target] || []), [...(list || [])]);
|
|
271
271
|
s.pendingFiles[target] = res.kept.slice(inFlight.length);
|
|
272
|
-
for (const x of res.rejected) deps.toast(`${x.name}: ${t(
|
|
272
|
+
for (const x of res.rejected) { const why = R().fileRejection(x.reason); deps.toast(`${x.name}: ${t(why.key, ...why.values)}`, false); }
|
|
273
273
|
changed(target);
|
|
274
274
|
}
|
|
275
275
|
function removeFile(target, index) { (s.pendingFiles[target] || []).splice(index, 1); changed(target); }
|
|
@@ -353,21 +353,21 @@ export function createChatStore(deps) {
|
|
|
353
353
|
}
|
|
354
354
|
// Board 003648ac: an answer the server marks `sent` leaves by itself; a sent /compact says "compacting…" until the
|
|
355
355
|
// status frame's `compact` (same watch id) says it ended, then leaves when it is done. Everything else — a refusal
|
|
356
|
-
// (even one worded as text), a list to choose from, an answer to read, a compaction timed out or
|
|
356
|
+
// (even one worded as text), a list to choose from, an answer to read, a compaction timed out, lost or not made — stays.
|
|
357
357
|
function showCommand(target, card) {
|
|
358
358
|
s.commands[target] = card;
|
|
359
359
|
const later = (ms, fn) => deps.setTimeout(() => { if (s.commands[target] === card) { fn(); changed(target); } }, ms);
|
|
360
360
|
const leave = () => { delete s.commands[target]; };
|
|
361
|
-
if (card.busy || card.error || card.choices ||
|
|
361
|
+
if (card.busy || card.error || card.choices || ["timeout", "lost", "not_compacted"].includes(card.compact)) return;
|
|
362
362
|
if (card.compact === "running") later(COMPACT_PAGE_LIMIT_MS, () => showCommand(target, { ...card, compact: "timeout" }));
|
|
363
363
|
else if (card.compact === "done") later(COMPACT_DONE_CLEAR_MS, leave);
|
|
364
364
|
else if (card.compact === "unknown") later(COMPACT_UNKNOWN_CLEAR_MS, leave);
|
|
365
365
|
else if (card.sent === true) later(SENT_CLEAR_MS, leave);
|
|
366
366
|
}
|
|
367
|
-
/** The status frame's word on a followed /compact: done, timed out, or lost. */
|
|
367
|
+
/** The status frame's word on a followed /compact: done, not compacted (kiro said so, board 907c7899), timed out, or lost. */
|
|
368
368
|
function noteCompact(name, view) {
|
|
369
369
|
const c = s.commands[name];
|
|
370
|
-
if (!c || c.compact !== "running" || !view || view.id !== c.watch || !["done", "timeout", "lost"].includes(view.state)) return;
|
|
370
|
+
if (!c || c.compact !== "running" || !view || view.id !== c.watch || !["done", "not_compacted", "timeout", "lost"].includes(view.state)) return;
|
|
371
371
|
showCommand(name, { ...c, compact: view.state });
|
|
372
372
|
changed(name);
|
|
373
373
|
}
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -7,6 +7,7 @@ register("chat", {
|
|
|
7
7
|
compactRunning: "Compacting…", compactDone: "✅ Conversation compacted", compactUnknown: "This backend does not report when compaction ends.",
|
|
8
8
|
compactTimeout: "No sign that compaction finished within {0} minutes — check the agent's own chat.",
|
|
9
9
|
compactLost: "Can't follow this compaction: the command did not reach the agent, or the agent stopped or changed backend.",
|
|
10
|
+
compactNotCompacted: "kiro did not compact the conversation: it was too short, or the compaction was stopped.",
|
|
10
11
|
cmdChoose: "Choose", cmdCurrent: "current", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
11
12
|
chipModel: "Model: {0} — choose another", chipEffort: "Effort: {0} — choose another",
|
|
12
13
|
cmd_ctx: "Context use, model and effort", cmd_compact: "Compact the conversation", cmd_clear: "Clear the conversation (asks first)",
|
|
@@ -69,11 +70,12 @@ register("chat", {
|
|
|
69
70
|
pvFail_unknown: "an unexpected error",
|
|
70
71
|
pvServer_notOffered: "Previews are not available on this page.",
|
|
71
72
|
previews: "Allow HTML previews",
|
|
73
|
+
clicked: "Clicked:", clickShow: "Show the message these buttons were on",
|
|
72
74
|
copy: "Copy", copyMessage: "Copy this reply", copied: "Copied", copyFailed: "Copy failed", wrap: "Wrap", showAll: "Show all {0} lines", showMore: "Show more", showLess: "Show less", jumpLatest: "Latest", newBelow: "{0} new",
|
|
73
75
|
disconnected: "disconnected", tick_queued: "Waiting behind another message", tick_processing: "Handed to the agent", tick_delivered: "The agent has it", tick_failed: "Not delivered", tick_cancelled: "Dropped by Stop",
|
|
74
76
|
promptAnswering: "Answering…", promptGone: "This prompt is no longer open", promptFor: "{0}: {1}",
|
|
75
77
|
working: "{0} is working…", stuck: "{0} looks stuck", stopReply: "Stop reply", stopReplyTitle: "Stop the current reply (like Esc) and drop the messages still waiting. The instance keeps running.", stopSent: "Stopped",
|
|
76
|
-
file_empty: "empty file", file_too_large: "over
|
|
78
|
+
file_empty: "empty file", file_too_large: "over {0} MB", file_too_large_public: "over {0} MB — the most the public link carries in one upload", file_too_many: "at most {0} files per message", file_too_much: "the files together would be over {0} MB", removeFile: "Remove", notSent: "Not sent:", putBack: "Put back", discard: "Discard",
|
|
77
79
|
noMessages: "Type a task below.",
|
|
78
80
|
pick: "Choose an agent to start chatting.", noAgents: "Create an agent to start chatting.", newAgent: "New agent", notFound: "No instance called “{0}”", notFoundHint: "It may have been deleted or renamed.", backToList: "Choose an agent",
|
|
79
81
|
details: "Instance details", start: "Start instance", stopInstance: "Stop instance", restart: "Restart instance", delete: "Delete instance…",
|
|
@@ -95,6 +97,7 @@ register("chat", {
|
|
|
95
97
|
compactRunning: "壓縮中…", compactDone: "✅ 對話已壓縮", compactUnknown: "這個後端不會回報壓縮何時完成。",
|
|
96
98
|
compactTimeout: "{0} 分鐘內沒有看到壓縮完成的紀錄,請到代理自己的對話確認。",
|
|
97
99
|
compactLost: "無法再追蹤這次壓縮:指令沒有送到代理,或代理已停止、換了後端。",
|
|
100
|
+
compactNotCompacted: "kiro 沒有壓縮對話:對話太短,或壓縮被中止了。",
|
|
98
101
|
cmdChoose: "選擇", cmdCurrent: "目前", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
99
102
|
chipModel: "模型:{0}——選擇其他", chipEffort: "推理強度:{0}——選擇其他",
|
|
100
103
|
cmd_ctx: "context 用量、模型與推理強度", cmd_compact: "壓縮對話", cmd_clear: "清空對話(會先確認)",
|
|
@@ -156,11 +159,12 @@ register("chat", {
|
|
|
156
159
|
pvFail_unknown: "發生了未預期的錯誤",
|
|
157
160
|
pvServer_notOffered: "這個頁面不提供預覽。",
|
|
158
161
|
previews: "允許預覽 HTML",
|
|
162
|
+
clicked: "點選了:", clickShow: "顯示這些按鈕所在的訊息",
|
|
159
163
|
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showMore: "顯示更多", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
160
164
|
disconnected: "已斷線", tick_queued: "排在前一則訊息之後", tick_processing: "已交給 agent", tick_delivered: "agent 已收到", tick_failed: "未送達", tick_cancelled: "已被中止取消",
|
|
161
165
|
promptAnswering: "回覆中…", promptGone: "這個提示已經結束", promptFor: "{0}:{1}",
|
|
162
166
|
working: "{0} 處理中…", stuck: "{0} 似乎卡住了", stopReply: "中止回覆", stopReplyTitle: "中止目前的回覆(同 Esc),並取消還在排隊的訊息。instance 會繼續運行。", stopSent: "已中止",
|
|
163
|
-
file_empty: "空檔案", file_too_large: "超過
|
|
167
|
+
file_empty: "空檔案", file_too_large: "超過 {0} MB", file_too_large_public: "超過 {0} MB,這是公開連結單次上傳的上限", file_too_many: "每則訊息最多 {0} 個檔案", file_too_much: "檔案合計會超過 {0} MB", removeFile: "移除", notSent: "未送出:", putBack: "放回輸入框", discard: "捨棄",
|
|
164
168
|
noMessages: "直接在下方輸入任務。",
|
|
165
169
|
pick: "選一個 Agent 開始聊天。", noAgents: "先建立一個 Agent,再開始聊天。", newAgent: "新增 Agent", notFound: "沒有叫「{0}」的 instance", notFoundHint: "它可能已被刪除或改名。", backToList: "選擇 Agent",
|
|
166
170
|
details: "Instance 詳細資料", start: "啟動 instance", stopInstance: "停止 instance", restart: "重新啟動 instance", delete: "刪除 instance…",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -86,7 +86,20 @@ export function createThread(list, scroller, opts) {
|
|
|
86
86
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
87
87
|
const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
88
88
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
89
|
-
|
|
89
|
+
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
90
|
+
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
91
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
95
|
+
* reply is in this thread) it is a button that shows that reply; restored from disk, plain.
|
|
96
|
+
*/
|
|
97
|
+
function clickHtml(click) {
|
|
98
|
+
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
99
|
+
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
100
|
+
return `<div class="click-chip">${of
|
|
101
|
+
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
102
|
+
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
90
103
|
}
|
|
91
104
|
/**
|
|
92
105
|
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
@@ -592,7 +605,7 @@ export function createThread(list, scroller, opts) {
|
|
|
592
605
|
else if (act === "replyButton") {
|
|
593
606
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
594
607
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
595
|
-
}
|
|
608
|
+
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
596
609
|
}
|
|
597
610
|
|
|
598
611
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -324,7 +324,7 @@ function CommandCard({ name, lease }) {
|
|
|
324
324
|
${c.text ? html`<pre class="cmd-text">${c.text}</pre>` : null}
|
|
325
325
|
${c.compact ? html`<p class="cmd-text cmd-phase">${c.compact === "running" ? t("chat.compactRunning")
|
|
326
326
|
: c.compact === "done" ? t("chat.compactDone") : c.compact === "timeout" ? t("chat.compactTimeout", "10")
|
|
327
|
-
: c.compact === "lost" ? t("chat.compactLost") : t("chat.compactUnknown")}</p>` : null}
|
|
327
|
+
: c.compact === "lost" ? t("chat.compactLost") : c.compact === "not_compacted" ? t("chat.compactNotCompacted") : t("chat.compactUnknown")}</p>` : null}
|
|
328
328
|
${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
|
|
329
329
|
${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
|
|
330
330
|
class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
|
|
@@ -761,7 +761,7 @@ function General({ ctx }) {
|
|
|
761
761
|
*/
|
|
762
762
|
export const WEB_READ_ONLY = [
|
|
763
763
|
"web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
|
|
764
|
-
"web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
|
|
764
|
+
"web.usage_panel", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
|
|
765
765
|
];
|
|
766
766
|
function WebSection({ ctx, staged }) {
|
|
767
767
|
const { schema } = ctx;
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -382,6 +382,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
382
382
|
such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
|
|
383
383
|
the right under its meta, and a long one still wraps at the message's max-width. */
|
|
384
384
|
.msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
|
|
385
|
+
/* A person's click on a reply button: the bubble shows 🔘 and the button's label (never the value the agent was given);
|
|
386
|
+
live, the label is a button that shows the reply its buttons were on. */
|
|
387
|
+
.click-chip { display: flex; }
|
|
388
|
+
.click-chip .click-link, .click-chip .click-text { display: inline-flex; align-items: baseline; gap: var(--s-1); padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; }
|
|
389
|
+
.click-chip .click-link { cursor: pointer; }
|
|
390
|
+
.click-chip .click-link:hover, .click-chip .click-link:focus-visible { text-decoration: underline; }
|
|
391
|
+
.click-mark { flex: none; }
|
|
385
392
|
.msg.agent { align-self: stretch; }
|
|
386
393
|
/* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
|
|
387
394
|
Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
|
package/dist/web-api.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface WebApiContext {
|
|
|
39
39
|
web?: {
|
|
40
40
|
echo_to_channel?: boolean;
|
|
41
41
|
usage_panel?: boolean;
|
|
42
|
+
upload_max_file_mb?: number;
|
|
43
|
+
upload_max_total_mb?: number;
|
|
42
44
|
};
|
|
43
45
|
defaults?: {
|
|
44
46
|
backend?: string;
|
|
@@ -294,6 +296,8 @@ export interface AppShellContext {
|
|
|
294
296
|
web?: {
|
|
295
297
|
usage_panel?: boolean;
|
|
296
298
|
log_tab?: boolean;
|
|
299
|
+
upload_max_file_mb?: number;
|
|
300
|
+
upload_max_total_mb?: number;
|
|
297
301
|
};
|
|
298
302
|
} | null;
|
|
299
303
|
/** #1665: the host's theme and language go into the page itself, so a device paints them before any script reads. */
|
package/dist/web-api.js
CHANGED
|
@@ -15,7 +15,7 @@ import { isSafeInstanceName } from "./web-shell-routes.js";
|
|
|
15
15
|
import { MAX_COMMAND_ARGS } from "./web-commands.js";
|
|
16
16
|
import { sendPanelHtml } from "./web-host-guard.js";
|
|
17
17
|
import { shellRoute } from "./web-shell-routes.js";
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
18
|
+
import { createReadStream, createWriteStream, readFileSync } from "node:fs";
|
|
19
19
|
import { join, dirname } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { CreateInstanceArgs, CreateScheduleArgs, UpdateScheduleArgs, validateArgs } from "./outbound-schemas.js";
|
|
@@ -26,7 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
|
|
27
27
|
import { dashboardOriginFor } from "./web-preview.js";
|
|
28
28
|
import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
|
|
29
|
-
import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment,
|
|
29
|
+
import { attachmentDelivery, createPartFile, displayName, fileTooLargeText, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, UPLOAD_LIMITS, UPLOAD_MAX_MS, UPLOAD_STALL_MS, UploadSniffer, uploadLimits, wellFormed } from "./web-upload.js";
|
|
30
30
|
import { getAgendHome } from "./paths.js";
|
|
31
31
|
import { OPERATOR } from "./web-prefs.js";
|
|
32
32
|
import { WEB_NAME_FALLBACK } from "./web-profile.js";
|
|
@@ -442,12 +442,14 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
442
442
|
serveMediaRange(req, res, ctx.webFiles, id, media);
|
|
443
443
|
return true;
|
|
444
444
|
}
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
// #1937: a file over MEDIA_RANGE_MAX streams from the descriptor its checks were made on — never whole in memory.
|
|
446
|
+
const big = isFileId(id) && (ctx.webFiles?.servedFile?.(id)?.size ?? 0) > MEDIA_RANGE_MAX ? ctx.webFiles?.open?.(id) ?? null : null;
|
|
447
|
+
const got = big ? null : isFileId(id) ? ctx.webFiles?.read(id) : null;
|
|
448
|
+
const file = big?.file ?? got?.file;
|
|
449
|
+
if (!file) {
|
|
447
450
|
json(res, 404, { error: "No such file" });
|
|
448
451
|
return true;
|
|
449
452
|
}
|
|
450
|
-
const { file, bytes } = got;
|
|
451
453
|
const inline = INLINE_MIME.has(file.mime);
|
|
452
454
|
// Only the four image types are shown in the page; anything else is a download, never rendered.
|
|
453
455
|
res.setHeader("Content-Type", inline ? file.mime : (file.mime.startsWith("text/") ? "text/plain; charset=utf-8" : "application/octet-stream"));
|
|
@@ -455,8 +457,12 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
455
457
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
456
458
|
res.setHeader("Cache-Control", "private, no-store");
|
|
457
459
|
res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
|
|
460
|
+
if (big) {
|
|
461
|
+
streamWhole(res, big);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
458
464
|
res.writeHead(200);
|
|
459
|
-
res.end(bytes);
|
|
465
|
+
res.end(got.bytes);
|
|
460
466
|
return true;
|
|
461
467
|
}
|
|
462
468
|
// ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
|
|
@@ -1622,6 +1628,11 @@ function handleSendMessage(req, res, ctx) {
|
|
|
1622
1628
|
* One file for one instance's chat. The body is the file itself (Content-Type is ignored: the type is
|
|
1623
1629
|
* read from the bytes), `X-Agend-Filename` its name for the label only. Stored in the instance's
|
|
1624
1630
|
* workspace inbox under a name chosen here; answered with the id the message will name.
|
|
1631
|
+
*
|
|
1632
|
+
* #1937: streamed to a part file in that inbox (web-pending-….part) as it arrives — never held whole in memory — and
|
|
1633
|
+
* counted by the bytes that came, whatever Content-Length said. The limits are the ones in force for this request
|
|
1634
|
+
* (uploadLimits(): the config, and the public link's own cap). An upload that goes UPLOAD_STALL_MS without a byte, or
|
|
1635
|
+
* runs past UPLOAD_MAX_MS, is dropped; any upload that does not end in a stored file takes its part file with it.
|
|
1625
1636
|
*/
|
|
1626
1637
|
function handleUpload(req, res, url, ctx) {
|
|
1627
1638
|
const instance = url.searchParams.get("instance") ?? "";
|
|
@@ -1633,57 +1644,112 @@ function handleUpload(req, res, url, ctx) {
|
|
|
1633
1644
|
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
1634
1645
|
return;
|
|
1635
1646
|
}
|
|
1647
|
+
const limits = uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined });
|
|
1648
|
+
const tooLarge = () => json(res, 413, { error: fileTooLargeText(limits), max_bytes: limits.maxFileBytes, ...(limits.publicLinkCapped ? { public_link: true } : {}) });
|
|
1636
1649
|
const declared = Number(req.headers["content-length"] ?? NaN);
|
|
1637
|
-
if (Number.isFinite(declared) && declared >
|
|
1638
|
-
|
|
1650
|
+
if (Number.isFinite(declared) && declared > limits.maxFileBytes) {
|
|
1651
|
+
tooLarge();
|
|
1652
|
+
req.resume();
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
let part, fd, discard;
|
|
1656
|
+
try {
|
|
1657
|
+
({ path: part, fd, discard } = createPartFile(join(getAgendHome(), "workspaces", instance, "inbox")));
|
|
1658
|
+
}
|
|
1659
|
+
catch (err) {
|
|
1660
|
+
ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
|
|
1661
|
+
json(res, 500, { error: "the file could not be stored" });
|
|
1639
1662
|
req.resume();
|
|
1640
1663
|
return;
|
|
1641
1664
|
}
|
|
1642
|
-
const
|
|
1665
|
+
const out = createWriteStream(part, { fd, autoClose: true });
|
|
1666
|
+
const sniffer = new UploadSniffer();
|
|
1643
1667
|
let size = 0;
|
|
1644
|
-
let
|
|
1668
|
+
let over = false; // ended without a stored file: no further write, and the part file goes
|
|
1669
|
+
let stall;
|
|
1670
|
+
const clocks = () => { clearTimeout(stall); clearTimeout(deadline); };
|
|
1671
|
+
/** Stop here: answer (when the client is still there), remove the part file, and stop a body still arriving. */
|
|
1672
|
+
const drop = (answer) => {
|
|
1673
|
+
if (over)
|
|
1674
|
+
return;
|
|
1675
|
+
over = true;
|
|
1676
|
+
clocks();
|
|
1677
|
+
answer?.();
|
|
1678
|
+
out.destroy();
|
|
1679
|
+
discard();
|
|
1680
|
+
if (!req.complete)
|
|
1681
|
+
req.destroy();
|
|
1682
|
+
};
|
|
1683
|
+
const armStall = () => { clearTimeout(stall); stall = setTimeout(() => drop(() => json(res, 408, { error: "the upload stalled — attach the file again" })), UPLOAD_STALL_MS); stall.unref?.(); };
|
|
1684
|
+
const deadline = setTimeout(() => drop(() => json(res, 408, { error: "the upload took too long — attach the file again" })), UPLOAD_MAX_MS);
|
|
1685
|
+
deadline.unref?.();
|
|
1686
|
+
armStall();
|
|
1687
|
+
out.on("error", (err) => { ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored"); drop(() => json(res, 500, { error: "the file could not be stored" })); });
|
|
1688
|
+
out.on("drain", () => { if (!over)
|
|
1689
|
+
req.resume?.(); });
|
|
1645
1690
|
req.on("data", (c) => {
|
|
1646
|
-
if (
|
|
1691
|
+
if (over)
|
|
1647
1692
|
return;
|
|
1648
1693
|
size += c.length;
|
|
1649
|
-
if (size >
|
|
1650
|
-
|
|
1651
|
-
json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
|
|
1652
|
-
req.destroy();
|
|
1694
|
+
if (size > limits.maxFileBytes) {
|
|
1695
|
+
drop(tooLarge);
|
|
1653
1696
|
return;
|
|
1654
1697
|
}
|
|
1655
|
-
|
|
1698
|
+
armStall();
|
|
1699
|
+
sniffer.push(c);
|
|
1700
|
+
if (!out.write(c))
|
|
1701
|
+
req.pause?.();
|
|
1656
1702
|
});
|
|
1657
|
-
req.on("error", () =>
|
|
1703
|
+
req.on("error", () => drop()); // the client went away
|
|
1704
|
+
req.on("close", () => { if (!over && req.complete === false)
|
|
1705
|
+
drop(); });
|
|
1706
|
+
// The instance is checked again now the whole body is here: it may have been deleted (and its files
|
|
1707
|
+
// forgotten) or stopped while the upload streamed in; storing now would bring its id back (#1252 review).
|
|
1708
|
+
// Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
|
|
1709
|
+
// in fleet.yaml, or as a ClassicBot room in classicBot.yaml. Asked when the body ends and again just before storing.
|
|
1710
|
+
const deliverable = () => ctx.instanceIpcClients.has(instance) && (!!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true);
|
|
1711
|
+
const gone = () => drop(() => json(res, 404, { error: `Instance not found: ${instance}` }));
|
|
1658
1712
|
req.on("end", () => {
|
|
1659
|
-
if (
|
|
1713
|
+
if (over)
|
|
1660
1714
|
return;
|
|
1661
|
-
|
|
1715
|
+
clocks();
|
|
1716
|
+
if (!permitWebContinuation(req, res, ctx)) {
|
|
1717
|
+
drop();
|
|
1662
1718
|
return;
|
|
1663
|
-
|
|
1719
|
+
}
|
|
1664
1720
|
const name = displayName(typeof req.headers["x-agend-filename"] === "string" ? req.headers["x-agend-filename"] : "", "file");
|
|
1665
|
-
const type =
|
|
1721
|
+
const type = sniffer.result(name);
|
|
1666
1722
|
if (!type) {
|
|
1667
|
-
json(res, 415, { error: "the file is empty" });
|
|
1723
|
+
drop(() => json(res, 415, { error: "the file is empty" }));
|
|
1668
1724
|
return;
|
|
1669
1725
|
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
// Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
|
|
1673
|
-
// in fleet.yaml, or as a ClassicBot room in classicBot.yaml.
|
|
1674
|
-
const registered = !!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true;
|
|
1675
|
-
if (!ctx.instanceIpcClients.has(instance) || !registered) {
|
|
1676
|
-
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
1726
|
+
if (!deliverable()) {
|
|
1727
|
+
gone();
|
|
1677
1728
|
return;
|
|
1678
1729
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1730
|
+
out.end(() => {
|
|
1731
|
+
if (over)
|
|
1732
|
+
return;
|
|
1733
|
+
// The write's finish is another wait (Prism #1941 P2-1): the session or the public link may have ended in it.
|
|
1734
|
+
if (!permitWebContinuation(req, res, ctx)) {
|
|
1735
|
+
drop();
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
if (!deliverable()) {
|
|
1739
|
+
gone();
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
over = true;
|
|
1743
|
+
try {
|
|
1744
|
+
const entry = ctx.webFiles.storeUploadedFile({ instance, partPath: part, name, type });
|
|
1745
|
+
json(res, 200, publicAttachment(entry));
|
|
1746
|
+
}
|
|
1747
|
+
catch (err) {
|
|
1748
|
+
discard();
|
|
1749
|
+
ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
|
|
1750
|
+
json(res, 500, { error: "the file could not be stored" });
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1687
1753
|
});
|
|
1688
1754
|
}
|
|
1689
1755
|
function shellPreview(req, ctx, mode) {
|
|
@@ -1701,6 +1767,15 @@ function shellPreview(req, ctx, mode) {
|
|
|
1701
1767
|
* No Range, several, or one not in that form → 200 with the whole file, as every file is served (the Download link).
|
|
1702
1768
|
* Inline, nosniff, no caching, and the route's own sandbox policy, as for every file.
|
|
1703
1769
|
*/
|
|
1770
|
+
/** #1937: a whole file (headers already set) streamed from the descriptor its checks were made on: 200, its length. */
|
|
1771
|
+
function streamWhole(res, opened) {
|
|
1772
|
+
res.setHeader("Content-Length", String(opened.file.size));
|
|
1773
|
+
res.writeHead(200);
|
|
1774
|
+
const stream = createReadStream("", { fd: opened.fd, start: 0, end: Math.max(0, opened.file.size - 1), autoClose: true });
|
|
1775
|
+
stream.on("error", () => res.destroy());
|
|
1776
|
+
res.on("close", () => stream.destroy());
|
|
1777
|
+
stream.pipe(res);
|
|
1778
|
+
}
|
|
1704
1779
|
function serveMediaRange(req, res, files, id, file) {
|
|
1705
1780
|
const total = file.size;
|
|
1706
1781
|
const header = typeof req.headers.range === "string" ? req.headers.range.trim() : "";
|
|
@@ -1724,20 +1799,34 @@ function serveMediaRange(req, res, files, id, file) {
|
|
|
1724
1799
|
}
|
|
1725
1800
|
}
|
|
1726
1801
|
end = ranged ? Math.min(end, total - 1, start + MEDIA_RANGE_MAX - 1) : total - 1;
|
|
1802
|
+
const mediaHeaders = () => {
|
|
1803
|
+
res.setHeader("Content-Type", file.mime);
|
|
1804
|
+
res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
|
|
1805
|
+
res.setHeader("Accept-Ranges", "bytes");
|
|
1806
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1807
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
1808
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
|
|
1809
|
+
};
|
|
1810
|
+
// #1937 (Prism #1941 P3-2): the whole of a file over MEDIA_RANGE_MAX (no Range asked) streams, as /ui/file's do.
|
|
1811
|
+
if (!ranged && total > MEDIA_RANGE_MAX) {
|
|
1812
|
+
const opened = files.open(id);
|
|
1813
|
+
if (!opened) {
|
|
1814
|
+
json(res, 404, { error: "No such file" });
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
mediaHeaders();
|
|
1818
|
+
streamWhole(res, opened);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1727
1821
|
const got = files.readSlice(id, start, end);
|
|
1728
1822
|
if (!got || (!ranged && got.bytes.length !== total)) {
|
|
1729
1823
|
json(res, 404, { error: "No such file" });
|
|
1730
1824
|
return;
|
|
1731
1825
|
}
|
|
1732
|
-
|
|
1733
|
-
res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
|
|
1734
|
-
res.setHeader("Accept-Ranges", "bytes");
|
|
1826
|
+
mediaHeaders();
|
|
1735
1827
|
if (ranged)
|
|
1736
1828
|
res.setHeader("Content-Range", `bytes ${got.start}-${got.end}/${total}`);
|
|
1737
1829
|
res.setHeader("Content-Length", String(got.bytes.length));
|
|
1738
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1739
|
-
res.setHeader("Cache-Control", "private, no-store");
|
|
1740
|
-
res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
|
|
1741
1830
|
res.writeHead(ranged ? 206 : 200);
|
|
1742
1831
|
res.end(got.bytes);
|
|
1743
1832
|
}
|
|
@@ -1769,6 +1858,9 @@ function shellBodyTag(req, ctx, mode) {
|
|
|
1769
1858
|
+ ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
|
|
1770
1859
|
// #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
|
|
1771
1860
|
+ ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
|
|
1861
|
+
// #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
|
|
1862
|
+
// checks before sending a file, and the numbers its words say. The server checks again on every upload.
|
|
1863
|
+
+ ` data-upload-limits="${attr(JSON.stringify(uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined })))}"`
|
|
1772
1864
|
// #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
|
|
1773
1865
|
+ ` data-fleet-tz="${attr(resolveTz())}"`
|
|
1774
1866
|
// #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
|