@songsid/agend 2.3.2-beta.5 → 2.3.2-beta.7
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/dist/backend/kiro-identity.d.ts +11 -0
- package/dist/backend/kiro-identity.js +16 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-retired-model.d.ts +62 -0
- package/dist/backend/kiro-retired-model.js +160 -0
- package/dist/backend/kiro-retired-model.js.map +1 -0
- package/dist/backend/kiro.d.ts +17 -0
- package/dist/backend/kiro.js +77 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +64 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/config-validator.js +25 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +5 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +21 -5
- package/dist/fleet-manager.js +154 -133
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +14 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +14 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +23 -8
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +16 -0
- package/dist/locale.js.map +1 -1
- package/dist/notice-delivery.d.ts +7 -6
- package/dist/notice-delivery.js +16 -18
- package/dist/notice-delivery.js.map +1 -1
- package/dist/office-share-page.d.ts +13 -0
- package/dist/office-share-page.js +82 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +89 -0
- package/dist/office-share-service.js +172 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +228 -0
- package/dist/office-share.js +489 -0
- package/dist/office-share.js.map +1 -0
- package/dist/public-web-gateway.js +6 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +0 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/settings-api.js +1 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +48 -5
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +4 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +0 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +12 -3
- package/dist/ui/fleet-office-pack.js +7 -2
- package/dist/ui/fleet-office-skin.js +5 -4
- package/dist/ui/fleet-office-zones.js +17 -6
- package/dist/ui/fleet-office.js +20 -10
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +116 -0
- package/dist/ui/panel-settings.js +12 -8
- package/dist/ui/settings-registry.js +2 -2
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app.css +28 -0
- package/dist/web-api.d.ts +14 -0
- package/dist/web-api.js +91 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-office-skins.d.ts +2 -1
- package/dist/web-office-skins.js +2 -1
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-settings-patch.js +1 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/working-recovery.d.ts +2 -2
- package/dist/working-recovery.js +43 -8
- package/dist/working-recovery.js.map +1 -1
- package/dist/working-state-journal.d.ts +2 -0
- package/dist/working-state-journal.js +16 -1
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +1 -1
- package/templates/windows-shortcuts.ps1 +151 -147
|
@@ -74,8 +74,8 @@ export function assignSeats(pack, wants, prev = new Map()) {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* The zones layout for `nodes` (buildOrgView's) on a pack: everyone but the missing at their seat. The same shape as
|
|
77
|
-
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph
|
|
78
|
-
* seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
|
|
77
|
+
* the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph (and
|
|
78
|
+
* its `straight`, #2634), the seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
|
|
79
79
|
*/
|
|
80
80
|
export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
|
|
81
81
|
// At night (#2568) the idle go to bed: the area asleep people go to while it has a free seat; the rest doze where they are.
|
|
@@ -89,7 +89,7 @@ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {
|
|
|
89
89
|
return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot, night: sleepers.has(n.name) };
|
|
90
90
|
});
|
|
91
91
|
return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
|
|
92
|
-
waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
92
|
+
waypoints: pack.waypoints || [], edges: pack.edges || [], straight: pack.straight === true, door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// ── Walking ──
|
|
@@ -108,6 +108,16 @@ export function headingAt(pts, t) {
|
|
|
108
108
|
}
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* #2634: a polyline with each diagonal leg made an L — across first, then up or down — so a character set's three
|
|
113
|
+
* walking strips always fit; legs already across or up/down are kept, and no point repeats (no zero-length leg).
|
|
114
|
+
*/
|
|
115
|
+
export function straighten(pts) {
|
|
116
|
+
const out = [];
|
|
117
|
+
const add = (x, y) => { const l = out[out.length - 1]; if (!l || l.x !== x || l.y !== y) out.push({ x, y }); };
|
|
118
|
+
for (const p of pts) { const l = out[out.length - 1]; if (l && l.x !== p.x && l.y !== p.y) add(p.x, l.y); add(p.x, p.y); }
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
111
121
|
// ── Night (#2568) ──
|
|
112
122
|
|
|
113
123
|
/** Bedtime on the viewer's clock: from FROM o'clock to TO o'clock the idle sleep (anyone with work still gets up for it). */
|
|
@@ -249,8 +259,8 @@ export function createMail({ max = MAIL.MAX } = {}) {
|
|
|
249
259
|
|
|
250
260
|
/**
|
|
251
261
|
* Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
|
|
252
|
-
* someone whose target moved walks from where they are now along the graph (routeVia
|
|
253
|
-
* `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
|
|
262
|
+
* someone whose target moved walks from where they are now along the graph (routeVia; each diagonal leg an L when the
|
|
263
|
+
* graph is `straight`, #2634) — or is there at once when `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
|
|
254
264
|
* current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
|
|
255
265
|
*/
|
|
256
266
|
export function createWalkers() {
|
|
@@ -269,7 +279,8 @@ export function createWalkers() {
|
|
|
269
279
|
const w = by.get(name);
|
|
270
280
|
if (!w || still) { if (!w || w.to.x !== to.x || w.to.y !== to.y) by.set(name, { pts: null, to }); continue; }
|
|
271
281
|
if (w.to.x === to.x && w.to.y === to.y) continue;
|
|
272
|
-
const here = posAt(w, now),
|
|
282
|
+
const here = posAt(w, now), via = routeVia(graph, { x: here.x, y: here.y }, { x: to.x, y: to.y });
|
|
283
|
+
const pts = graph.straight ? straighten(via) : via; // #2634: timed along the longer way
|
|
273
284
|
by.set(name, { pts, start: now, ms: Math.max(1, pathLength(pts) / WALK.SPEED), to });
|
|
274
285
|
}
|
|
275
286
|
},
|
package/dist/ui/fleet-office.js
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
// ("+N", in its corner, never over its name); there idle and asleep people say nothing, at most a few speak (calls
|
|
35
35
|
// first, then work), no bubble covers a name tag, and the tags show on hover or focus only. "Hide paused and stopped"
|
|
36
36
|
// (per browser) leaves the sleepers out.
|
|
37
|
+
// - Share… (#2636; an admin, on the dashboard reached directly or over the public link): a link anyone can open to watch an office live, read-only,
|
|
38
|
+
// until it ends (office-share-dialog.js; the page it opens is office-share-page.js).
|
|
37
39
|
// - Edit layout (an admin, a stored office on a zones skin): fleet-office-layout.js, saved as the office's overrides.
|
|
38
40
|
// An editing session (#2513 review) is bound to the office it opened on, that office's style (the pack's id and sha)
|
|
39
41
|
// and the list and rev read then: a re-read of the offices meanwhile (the stream) changes the view, never the session,
|
|
@@ -67,6 +69,7 @@ import { actsOk, createOfficeStore } from "./fleet-office-store.js";
|
|
|
67
69
|
import { POLL_MS } from "./fleet-traffic.js";
|
|
68
70
|
import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
|
|
69
71
|
import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
|
|
72
|
+
import { ShareButton } from "./office-share-dialog.js";
|
|
70
73
|
import { applyOverrides } from "./fleet-office-pack.js";
|
|
71
74
|
import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
|
|
72
75
|
import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
|
|
@@ -422,15 +425,18 @@ function People({ nodes, pinned, onChange }) {
|
|
|
422
425
|
|
|
423
426
|
/**
|
|
424
427
|
* The view: `view` buildOrgView's answer; `traffic` the live items (null: Live is off); `q` the Find box's needle;
|
|
425
|
-
* `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat
|
|
428
|
+
* `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat; none: a desk opens nothing,
|
|
429
|
+
* as on a share link's page, #2636); `assetUrl(path)` where a skin pack's picture is read (the share's address);
|
|
430
|
+
* `opening` the office it opens on (the share's), instead of the one this browser chose last.
|
|
426
431
|
*/
|
|
427
|
-
export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
432
|
+
export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening }) {
|
|
428
433
|
const store = useMemo(() => createOfficeStore(lease ? { fetch: (path, o) => lease.fetch(path, o) } : {}), [lease]);
|
|
429
434
|
const [stored, setStored] = useState([]);
|
|
430
435
|
const [storedRev, setStoredRev] = useState(null); // the rev the list was read at (a save sends it)
|
|
431
436
|
const [canEdit, setCanEdit] = useState(false); // an admin session (never a guest or the public link)
|
|
437
|
+
const [canShare, setCanShare] = useState(false); // #2636: an admin session, here or over the public link
|
|
432
438
|
const [editing, setEditing] = useState(false);
|
|
433
|
-
const [choice, setChoice] = useState(() => store.choice());
|
|
439
|
+
const [choice, setChoice] = useState(() => opening ?? store.choice());
|
|
434
440
|
const [filter, setFilter] = useState(() => store.filter());
|
|
435
441
|
const [style, setStyle] = useState(() => store.skin());
|
|
436
442
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
@@ -517,13 +523,15 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
517
523
|
return () => { if (off) off(); };
|
|
518
524
|
}, [lease, store]);
|
|
519
525
|
// Whether this page may edit the offices: an admin session (the host asks again on every save), never a guest page or
|
|
520
|
-
// the public link (whose sessions the host refuses outright).
|
|
526
|
+
// the public link (whose sessions the host refuses outright). #2636 follow-up (the user's choice): Share… is an admin
|
|
527
|
+
// session's here or over the public link (the host asks again too); never a guest's or a read-only one's.
|
|
521
528
|
useEffect(() => {
|
|
522
529
|
const body = globalThis.document?.body;
|
|
523
|
-
if (!body || body.dataset.tier === "guest"
|
|
530
|
+
if (!body || body.dataset.tier === "guest") return undefined;
|
|
531
|
+
const overLink = body.dataset.publicLink === "1";
|
|
524
532
|
let live = true;
|
|
525
533
|
const auth = globalThis.AgendAuth;
|
|
526
|
-
if (auth && typeof auth.canWrite === "function") Promise.resolve(auth.canWrite()).then((v) => { if (live) setCanEdit(v === true); }, () => {});
|
|
534
|
+
if (auth && typeof auth.canWrite === "function") Promise.resolve(auth.canWrite()).then((v) => { if (live) { setCanEdit(v === true && !overLink); setCanShare(v === true); } }, () => {});
|
|
527
535
|
return () => { live = false; };
|
|
528
536
|
}, []);
|
|
529
537
|
// The stage's width (the grid wraps at it), and the theme (the tokens the canvas reads).
|
|
@@ -574,7 +582,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
574
582
|
if (!want.length) return undefined;
|
|
575
583
|
let live = true;
|
|
576
584
|
const get = lease ? (path, o) => lease.fetch(path, o) : undefined;
|
|
577
|
-
for (const [k, pk] of want) loadPackSkin(pk.id, pk.sha, get ? { fetch: get } : {}).then((sk) => { if (live) setPackSkins((m) => new Map(m).set(k, sk)); });
|
|
585
|
+
for (const [k, pk] of want) loadPackSkin(pk.id, pk.sha, { ...(get ? { fetch: get } : {}), ...(assetUrl ? { url: assetUrl } : {}) }).then((sk) => { if (live) setPackSkins((m) => new Map(m).set(k, sk)); });
|
|
578
586
|
return () => { live = false; };
|
|
579
587
|
}, [neededKey]);
|
|
580
588
|
const loading = !!packInfo && !packSkins.has(packKey);
|
|
@@ -877,7 +885,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
877
885
|
return () => root.classList.remove("oo-immersive");
|
|
878
886
|
}, [fs]);
|
|
879
887
|
/** A desk opens its chat: out of fullscreen first (the chat is beside the view, or instead of it). */
|
|
880
|
-
const open = (name) => { if (fsRef.current) exitFs(); onOpen(name); };
|
|
888
|
+
const open = (name) => { if (!onOpen) return; if (fsRef.current) exitFs(); onOpen(name); };
|
|
881
889
|
|
|
882
890
|
// ── Drawing: the back and front layers are pictures of the scene at the view's resolution (remade when the office
|
|
883
891
|
// or the zoom changes, never on a pan); each frame draws the matte, the layers, the people and the items in the air ──
|
|
@@ -1149,6 +1157,8 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
1149
1157
|
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1150
1158
|
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
1151
1159
|
${canLayout ? html`<button type="button" class="btn btn-sm oo-layout" onClick=${openLayout}>${t("orgl.edit")}</button>` : null}
|
|
1160
|
+
${canShare ? html`<${ShareButton} offices=${stored} current=${overview ? "all" : custom ? custom.id : null} disabled=${!!layout}
|
|
1161
|
+
get=${lease ? (path, o) => lease.fetch(path, o) : undefined} />` : null}
|
|
1152
1162
|
</div>
|
|
1153
1163
|
${editing && !overview ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
|
|
1154
1164
|
ask=${ask} current=${() => shown.current} onSaved=${adopt} onClose=${() => setEditing(false)} />` : null}
|
|
@@ -1169,12 +1179,12 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
1169
1179
|
<strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
|
|
1170
1180
|
<div>${tipText.sub}</div><div class="muted">${caption(officePose(tipDesk.node.state), tipDesk.node.state)}</div>
|
|
1171
1181
|
${tipDesk.office ? html`<div class="muted oo-tip-office">${tipDesk.office}</div>` : null}
|
|
1172
|
-
${tipDesk.node.i && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
|
|
1182
|
+
${tipDesk.node.i && onOpen && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
|
|
1173
1183
|
</div>` : null}
|
|
1174
1184
|
<ul class="oo-desks" aria-label=${to("desks")}>${people.map((d) => {
|
|
1175
1185
|
const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state), where = d.office ? ` · ${d.office}` : "";
|
|
1176
1186
|
return html`<li key=${d.key}>${d.node.i
|
|
1177
|
-
? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}
|
|
1187
|
+
? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}.${onOpen ? ` ${t("orgc.openChat")}` : ""}`}
|
|
1178
1188
|
onFocus=${() => setTip(d.key)} onBlur=${() => setTip(null)} onClick=${() => open(d.node.name)}>${nt.primary}</button>`
|
|
1179
1189
|
: html`<span class="oo-desk" data-name=${d.node.name} data-key=${d.key}>${nt.full}</span>`}</li>`;
|
|
1180
1190
|
})}</ul>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// #2636: Office share links on the dashboard — the Office's Share… dialog (make one: which office, how long, whether
|
|
2
|
+
// agents' own lines show; then its URL to copy) and the list of open ones (Settings → Access & security, and under the
|
|
3
|
+
// dialog's form): each with its office, when it ends, whether lines show and who made it, Revoke and Revoke all.
|
|
4
|
+
// GET / POST /ui/office-share, DELETE /ui/office-share/<id|all> (web-api.ts): an admin session on the dashboard reached
|
|
5
|
+
// directly only; the public link never offers these.
|
|
6
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
7
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
8
|
+
import { Dialog } from "/assets/ui-dialog.js";
|
|
9
|
+
import { toast } from "/assets/ui-toast.js";
|
|
10
|
+
|
|
11
|
+
register("oshare", {
|
|
12
|
+
en: {
|
|
13
|
+
share: "Share…", title: "Share the office", lead: "Anyone with the link can watch this office live — names and states, nothing else — until it ends. No sign-in.",
|
|
14
|
+
office: "Office", allOffices: "All offices", how: "For", text: "Include agents' own status lines", create: "Create link", creating: "Starting the link…",
|
|
15
|
+
made: "Anyone with this link can watch until {0}.", copy: "Copy link", copied: "Link copied", open: "Open links", none: "No share links are open.",
|
|
16
|
+
ends: "until {0}", withText: "with status lines", by: "by {0}", revoke: "Revoke", revokeAll: "Revoke all", close: "Close", failed: "The link could not be made.",
|
|
17
|
+
m15: "15 minutes", m30: "30 minutes", h1: "1 hour", h2: "2 hours", h4: "4 hours", h8: "8 hours", h24: "24 hours",
|
|
18
|
+
e_disabled: "Office share links are off on this fleet (web.office_share.enabled: false).", e_office: "That office no longer exists.",
|
|
19
|
+
e_duration: "That is longer than this fleet allows (web.office_share.max_hours).", e_too_many: "At most 10 share links can be open at once: revoke one first.",
|
|
20
|
+
e_rate: "Cloudflare is limiting new quick tunnels right now. Try again in a few minutes.", e_tunnel: "The share link's tunnel did not start ({0}). Try again later.",
|
|
21
|
+
cardTitle: "Office share links", cardHint: "Read-only links to the live Office view, made from Fleet → Org chart → Office → Share…. Each ends at its time; revoke one to end it now.",
|
|
22
|
+
},
|
|
23
|
+
"zh-TW": {
|
|
24
|
+
share: "分享…", title: "分享辦公室", lead: "拿到連結的人都能即時觀看這間辦公室(只有名稱與狀態),直到連結結束,不需要登入。",
|
|
25
|
+
office: "辦公室", allOffices: "所有辦公室", how: "時間", text: "包含 agent 自己的狀態文字", create: "建立連結", creating: "正在啟動連結…",
|
|
26
|
+
made: "拿到這個連結的人可以觀看到 {0}。", copy: "複製連結", copied: "已複製連結", open: "開啟中的連結", none: "目前沒有開啟中的分享連結。",
|
|
27
|
+
ends: "到 {0} 為止", withText: "含狀態文字", by: "由 {0} 建立", revoke: "撤銷", revokeAll: "全部撤銷", close: "關閉", failed: "無法建立連結。",
|
|
28
|
+
m15: "15 分鐘", m30: "30 分鐘", h1: "1 小時", h2: "2 小時", h4: "4 小時", h8: "8 小時", h24: "24 小時",
|
|
29
|
+
e_disabled: "這個 fleet 已關閉辦公室分享連結(web.office_share.enabled: false)。", e_office: "這間辦公室已不存在。",
|
|
30
|
+
e_duration: "超過這個 fleet 允許的時間(web.office_share.max_hours)。", e_too_many: "同時最多 10 個分享連結:先撤銷一個。",
|
|
31
|
+
e_rate: "Cloudflare 目前限制新的臨時通道,請過幾分鐘再試。", e_tunnel: "分享連結的通道沒有啟動({0}),請稍後再試。",
|
|
32
|
+
cardTitle: "辦公室分享連結", cardHint: "即時辦公室畫面的唯讀連結,可在 Fleet → 組織圖 → 辦公室 → 分享… 建立。每個連結到時間就結束;撤銷可立即結束。",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const to = (k, ...v) => t(`oshare.${k}`, ...v);
|
|
36
|
+
const DURATIONS = [["m15", 15], ["m30", 30], ["h1", 60], ["h2", 120], ["h4", 240], ["h8", 480], ["h24", 1440]];
|
|
37
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
38
|
+
const when = (ms) => { try { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; } };
|
|
39
|
+
|
|
40
|
+
/** What a refusal says (the host's `error` and `reason`). */
|
|
41
|
+
export function refusalText(d) {
|
|
42
|
+
const e = d && d.error;
|
|
43
|
+
if (e === "tunnel") return d.reason === "rate-limited" ? to("e_rate") : to("e_tunnel", d.reason || "unknown");
|
|
44
|
+
return ["disabled", "office", "duration", "too_many"].includes(e) ? to(`e_${e}`) : to("failed");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The open shares, read (and re-read after a change): { enabled, shares } or null while unread / unavailable. */
|
|
48
|
+
export function useShares(get = globalThis.fetch, rev = 0) {
|
|
49
|
+
const [d, setD] = useState(null);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
let live = true;
|
|
52
|
+
get("/ui/office-share", { method: "GET", headers: JSON_HEADERS }).then((r) => (r.ok ? r.json() : null)).then((v) => { if (live) setD(v); }, () => { if (live) setD(null); });
|
|
53
|
+
return () => { live = false; };
|
|
54
|
+
}, [rev]);
|
|
55
|
+
return d;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The open shares with Revoke (and Revoke all); `names`: office id → name. */
|
|
59
|
+
export function ShareList({ shares, names = {}, get = globalThis.fetch, onChange }) {
|
|
60
|
+
const [busy, setBusy] = useState(false);
|
|
61
|
+
const revoke = async (id) => {
|
|
62
|
+
setBusy(true);
|
|
63
|
+
try { await get(`/ui/office-share/${id}`, { method: "DELETE", headers: JSON_HEADERS }); } catch { /* the list is read again */ }
|
|
64
|
+
setBusy(false);
|
|
65
|
+
onChange && onChange();
|
|
66
|
+
};
|
|
67
|
+
if (!shares || !shares.length) return html`<p class="muted os-none">${to("none")}</p>`;
|
|
68
|
+
return html`<div class="os-list">
|
|
69
|
+
<ul class="os-rows">${shares.map((s) => html`<li key=${s.id} class="os-row">
|
|
70
|
+
<span class="os-what"><strong>${s.office === "all" ? to("allOffices") : names[s.office] || s.office_name || s.office}</strong>
|
|
71
|
+
<span class="muted"> · ${to("ends", when(s.expiresAt))}${s.text ? ` · ${to("withText")}` : ""}</span></span>
|
|
72
|
+
<button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => revoke(s.id)}>${to("revoke")}</button>
|
|
73
|
+
</li>`)}</ul>
|
|
74
|
+
${shares.length > 1 ? html`<button type="button" class="btn btn-sm danger os-all" disabled=${busy} onClick=${() => revoke("all")}>${to("revokeAll")}</button>` : null}
|
|
75
|
+
</div>`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Share…: make a link for one office (the one shown, by default) or all of them. `offices`: the stored ones. */
|
|
79
|
+
export function ShareDialog({ offices = [], current = null, get = globalThis.fetch, onClose }) {
|
|
80
|
+
const [office, setOffice] = useState(current && offices.some((o) => o.id === current) ? current : "all");
|
|
81
|
+
const [minutes, setMinutes] = useState(60);
|
|
82
|
+
const [text, setText] = useState(false);
|
|
83
|
+
const [busy, setBusy] = useState(false);
|
|
84
|
+
const [made, setMade] = useState(null);
|
|
85
|
+
const [error, setError] = useState("");
|
|
86
|
+
const [rev, setRev] = useState(0);
|
|
87
|
+
const list = useShares(get, rev);
|
|
88
|
+
const names = Object.fromEntries(offices.map((o) => [o.id, o.name]));
|
|
89
|
+
const create = async () => {
|
|
90
|
+
setBusy(true); setError("");
|
|
91
|
+
try {
|
|
92
|
+
const r = await get("/ui/office-share", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify({ office, minutes, text }) });
|
|
93
|
+
const d = await r.json().catch(() => ({}));
|
|
94
|
+
if (r.ok && d && typeof d.url === "string") setMade(d); else setError(refusalText(d));
|
|
95
|
+
} catch { setError(to("failed")); }
|
|
96
|
+
setBusy(false);
|
|
97
|
+
setRev((n) => n + 1);
|
|
98
|
+
};
|
|
99
|
+
const copy = async () => { try { await navigator.clipboard.writeText(made.url); toast(to("copied")); } catch { /* the link is on screen to copy by hand */ } };
|
|
100
|
+
const actions = made ? html`<button type="button" class="btn" onClick=${onClose}>${to("close")}</button>`
|
|
101
|
+
: html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${to("close")}</button>
|
|
102
|
+
<button type="button" class="btn btn-primary" onClick=${create} disabled=${busy}>${busy ? to("creating") : to("create")}</button>`;
|
|
103
|
+
return html`<${Dialog} title=${to("title")} onClose=${onClose} busy=${busy} actions=${actions} wide=${true}>
|
|
104
|
+
<div class="os">
|
|
105
|
+
<p class="muted">${to("lead")}</p>
|
|
106
|
+
${made ? html`<div class="os-made">
|
|
107
|
+
<input class="input mono os-url" readonly value=${made.url} aria-label=${to("copy")} onFocus=${(e) => e.target.select && e.target.select()} />
|
|
108
|
+
<button type="button" class="btn btn-sm" onClick=${copy}>${to("copy")}</button>
|
|
109
|
+
<p class="note">${to("made", when(made.share.expiresAt))}</p>
|
|
110
|
+
</div>` : html`<div class="os-form">
|
|
111
|
+
<label class="field"><span>${to("office")}</span>
|
|
112
|
+
<select class="tb-select" value=${office} onChange=${(e) => setOffice(e.target.value)} disabled=${busy}>
|
|
113
|
+
${offices.length > 1 || !offices.length ? html`<option value="all">${to("allOffices")}</option>` : null}
|
|
114
|
+
${offices.map((o) => html`<option key=${o.id} value=${o.id}>${o.name}</option>`)}
|
|
115
|
+
</select></label>
|
|
116
|
+
<label class="field"><span>${to("how")}</span>
|
|
117
|
+
<select class="tb-select" value=${String(minutes)} onChange=${(e) => setMinutes(Number(e.target.value))} disabled=${busy}>
|
|
118
|
+
${DURATIONS.map(([k, m]) => html`<option key=${k} value=${String(m)}>${to(k)}</option>`)}
|
|
119
|
+
</select></label>
|
|
120
|
+
<label class="check"><input type="checkbox" checked=${text} onChange=${(e) => setText(e.target.checked)} disabled=${busy} /> ${to("text")}</label>
|
|
121
|
+
${error ? html`<p class="err os-err" role="alert">${error}</p>` : null}
|
|
122
|
+
</div>`}
|
|
123
|
+
<h3 class="os-head">${to("open")}</h3>
|
|
124
|
+
<${ShareList} shares=${list && list.shares} names=${names} get=${get} onChange=${() => setRev((n) => n + 1)} />
|
|
125
|
+
</div>
|
|
126
|
+
<//>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Settings → Access & security: the open share links, with Revoke (made from the Office's Share…). */
|
|
130
|
+
export function OfficeSharesCard({ get = globalThis.fetch }) {
|
|
131
|
+
const [rev, setRev] = useState(0);
|
|
132
|
+
const d = useShares(get, rev);
|
|
133
|
+
if (!d) return null; // not kept on this fleet, or not this session's to see
|
|
134
|
+
return html`<section class="card os-card"><h3>${to("cardTitle")}</h3>
|
|
135
|
+
<p class="note">${d.enabled === false ? to("e_disabled") : to("cardHint")}</p>
|
|
136
|
+
<${ShareList} shares=${d.shares} get=${get} onChange=${() => setRev((n) => n + 1)} />
|
|
137
|
+
</section>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The Office toolbar's Share… button (shown to an admin on the dashboard reached directly, when the fleet offers it). */
|
|
141
|
+
export function ShareButton({ offices, current, get, disabled = false }) {
|
|
142
|
+
const [open, setOpen] = useState(false);
|
|
143
|
+
return html`<button type="button" class="btn btn-sm oo-share" disabled=${disabled} onClick=${() => setOpen(true)}>${to("share")}</button>
|
|
144
|
+
${open ? html`<${ShareDialog} offices=${offices} current=${current} get=${get} onClose=${() => setOpen(false)} />` : null}`;
|
|
145
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// #2636: an Office share link's page (/o/<token> on the share tunnel, src/office-share.ts) — the live Office view for
|
|
2
|
+
// people outside the fleet, read-only, with no sign-in. The same Office as Fleet → Org chart → Office (fleet-office.js),
|
|
3
|
+
// fed by the share's own reads instead of the app shell's: its status and structure (/o/<t>/state, every POLL_MS while
|
|
4
|
+
// the page is shown), the offices, acts and skin packs (the Office's own reads, sent to the share's addresses by the
|
|
5
|
+
// lease's fetch). Nothing opens a chat, edits or writes; there is no other page. When the share ends (revoked, expired,
|
|
6
|
+
// its office deleted) its reads answer 404 and the page says so and stops.
|
|
7
|
+
import { html, render, useEffect, useMemo, useState } from "/assets/app-html.js";
|
|
8
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
9
|
+
import { createLease } from "/assets/app-ctx.js";
|
|
10
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
11
|
+
import { buildOrgView } from "./fleet-org.js";
|
|
12
|
+
import { createTrafficStore, POLL_MS } from "./fleet-traffic.js";
|
|
13
|
+
import { Office } from "./fleet-office.js";
|
|
14
|
+
import { ALL_ID } from "./fleet-office-overview.js";
|
|
15
|
+
|
|
16
|
+
register("share", {
|
|
17
|
+
en: { title: "Office", live: "Live view", ends: "until {0}", readOnly: "Read-only: names and states only.", ended: "This Office link has ended.", loading: "Loading the office…", offline: "Can't reach the office right now. Trying again…" },
|
|
18
|
+
"zh-TW": { title: "辦公室", live: "即時畫面", ends: "到 {0} 為止", readOnly: "唯讀:只顯示名稱與狀態。", ended: "這個辦公室連結已結束。", loading: "正在載入辦公室…", offline: "目前連不上辦公室,正在重試…" },
|
|
19
|
+
});
|
|
20
|
+
const to = (k, ...v) => t(`share.${k}`, ...v);
|
|
21
|
+
|
|
22
|
+
/** The share's own address: `/o/<token>` (the page's path). */
|
|
23
|
+
export function shareBase(pathname) {
|
|
24
|
+
const m = /^\/o\/([A-Za-z0-9_-]{43})\/?$/.exec(pathname || "");
|
|
25
|
+
return m ? `/o/${m[1]}` : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The Office's reads, at the share's addresses (anything else is not the share's to answer: refused here). */
|
|
29
|
+
export function shareUrl(base, path) {
|
|
30
|
+
const p = String(path);
|
|
31
|
+
if (p === "/ui/offices") return `${base}/offices`;
|
|
32
|
+
if (p === "/ui/office-acts") return `${base}/office-acts`;
|
|
33
|
+
if (p === "/ui/office-skins" || p.startsWith("/ui/office-skins/") || p.startsWith("/ui/office-skins?")) return `${base}${p.slice(3)}`;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A lease whose fetch reads the share's addresses (createLease's timers), refusing any other. */
|
|
38
|
+
export function shareLease(base, timers = globalThis) {
|
|
39
|
+
return createLease({
|
|
40
|
+
setTimeout: (...a) => timers.setTimeout(...a), clearTimeout: (h) => timers.clearTimeout(h),
|
|
41
|
+
setInterval: (...a) => timers.setInterval(...a), clearInterval: (h) => timers.clearInterval(h),
|
|
42
|
+
fetch: (path, opts) => {
|
|
43
|
+
const url = shareUrl(base, path);
|
|
44
|
+
return url ? timers.fetch(url, opts) : Promise.resolve(new Response("", { status: 404 }));
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The share's /state into the app store's shape (the status frame's, as app-store.js takes it). */
|
|
50
|
+
export function appFromState(d) {
|
|
51
|
+
const instances = Array.isArray(d && d.instances) ? d.instances : [];
|
|
52
|
+
const exec = {}, awaiting = {};
|
|
53
|
+
for (const i of instances) {
|
|
54
|
+
exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
|
|
55
|
+
if (i.state === "awaiting_input") awaiting[i.name] = "";
|
|
56
|
+
}
|
|
57
|
+
return { ready: true, instances, exec, awaiting, needs: Array.isArray(d && d.needs) ? d.needs : [], connection: "polling" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const clock = (ms) => {
|
|
61
|
+
try { return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function SharePage({ base, timers = globalThis, pollMs = POLL_MS }) {
|
|
65
|
+
const lease = useMemo(() => shareLease(base, timers), [base]);
|
|
66
|
+
const traffic = useMemo(createTrafficStore, []);
|
|
67
|
+
const [state, setState] = useState({ phase: "loading" }); // loading | live | ended | offline
|
|
68
|
+
const [items, setItems] = useState([]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let stopped = false;
|
|
71
|
+
const read = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const r = await timers.fetch(`${base}/state`, { method: "GET", headers: { Accept: "application/json" } });
|
|
74
|
+
if (stopped || !lease.current()) return;
|
|
75
|
+
if (r.status === 404) { stopped = true; setState({ phase: "ended" }); return; }
|
|
76
|
+
if (!r.ok) { setState((s) => (s.phase === "live" ? s : { phase: "offline" })); return; }
|
|
77
|
+
const d = await r.json();
|
|
78
|
+
if (stopped || !lease.current()) return;
|
|
79
|
+
appStore.set((s) => ({ ...s, ...appFromState(d) }));
|
|
80
|
+
if (d.traffic && traffic.load(d.traffic)) setItems(traffic.items());
|
|
81
|
+
setState({ phase: "live", org: d.org, office: typeof d.office === "string" ? d.office : null, ends: typeof d.ends === "number" ? d.ends : null });
|
|
82
|
+
} catch { if (!stopped) setState((s) => (s.phase === "live" ? s : { phase: "offline" })); }
|
|
83
|
+
};
|
|
84
|
+
void read();
|
|
85
|
+
const hs = [
|
|
86
|
+
lease.interval(() => { if (!stopped && !globalThis.document?.hidden) void read(); }, pollMs),
|
|
87
|
+
lease.interval(() => { if (traffic.sweep()) setItems(traffic.items()); }, 1000),
|
|
88
|
+
];
|
|
89
|
+
return () => { stopped = true; for (const h of hs) lease.clear(h); };
|
|
90
|
+
}, [lease]);
|
|
91
|
+
useEffect(() => () => lease.dispose(), [lease]);
|
|
92
|
+
const app = useStore(appStore);
|
|
93
|
+
const view = useMemo(() => (state.phase === "live" ? buildOrgView(state.org, app) : null), [state, app.instances, app.needs]);
|
|
94
|
+
if (state.phase === "ended") return html`<main class="share-page share-ended"><p>${to("ended")}</p></main>`;
|
|
95
|
+
return html`<main class="share-page">
|
|
96
|
+
<header class="share-head">
|
|
97
|
+
<strong>${to("title")}</strong>
|
|
98
|
+
<span class="muted">${to("live")}${state.ends ? ` · ${to("ends", clock(state.ends))}` : ""}</span>
|
|
99
|
+
<span class="muted share-note">${to("readOnly")}</span>
|
|
100
|
+
</header>
|
|
101
|
+
${state.phase === "offline" ? html`<p class="note share-note" role="status">${to("offline")}</p>` : null}
|
|
102
|
+
${view ? html`<div class="share-stage"><${Office} view=${view} traffic=${items} lease=${lease} assetUrl=${(p) => shareUrl(base, p) || p}
|
|
103
|
+
opening=${state.office === "all" ? ALL_ID : state.office || undefined} /></div>`
|
|
104
|
+
: html`<p class="muted share-loading" role="status">${to("loading")}</p>`}
|
|
105
|
+
</main>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Mount on the page (the module's entry): the share's address from the path, else nothing to show. */
|
|
109
|
+
export function mount(root = globalThis.document?.getElementById("app")) {
|
|
110
|
+
const base = shareBase(globalThis.location?.pathname);
|
|
111
|
+
if (!root || !base) return;
|
|
112
|
+
globalThis.document.body.dataset.webTransport = "poll";
|
|
113
|
+
render(html`<${SharePage} base=${base} />`, root);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (globalThis.document?.getElementById?.("app")?.dataset?.share === "1") mount();
|
|
@@ -46,6 +46,7 @@ import { startTour } from "./chat-tour.js";
|
|
|
46
46
|
import "./chat-strings.js";
|
|
47
47
|
import { AppearanceSection } from "./settings-appearance.js";
|
|
48
48
|
import { GuestsCard } from "./settings-guests.js";
|
|
49
|
+
import { OfficeSharesCard } from "./office-share-dialog.js";
|
|
49
50
|
import { syncStage, useDraft } from "./settings-draft.js";
|
|
50
51
|
import { WebAuthorityCard } from "./settings-web-authority.js";
|
|
51
52
|
import { SignInList } from "./backend-signin.js";
|
|
@@ -952,10 +953,11 @@ function Classic({ ctx, search, openDialog }) {
|
|
|
952
953
|
})}</div>`;
|
|
953
954
|
}
|
|
954
955
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
const
|
|
956
|
+
// Sign-in notices and daily tips post in the topic or not at all: the DM choice was withdrawn (hmsg-d1809f2a,
|
|
957
|
+
// hmsg-3b8ac2af), and an old "dm" reads as on.
|
|
958
|
+
const noticeMode = (v) => v !== "false" && v !== false;
|
|
959
|
+
const noticeChoices = () => [{ value: "true", label: tn("noticeTopic") }, { value: "false", label: tn("noticeOff") }];
|
|
960
|
+
const noticeLabel = (v) => tn(v ? "noticeTopic" : "noticeOff");
|
|
959
961
|
|
|
960
962
|
/** General's form, from the saved defaults (`d`) and ClassicBot's (`c`). */
|
|
961
963
|
function generalForm(d, c, fleet = {}, schema = {}) {
|
|
@@ -971,7 +973,7 @@ function generalForm(d, c, fleet = {}, schema = {}) {
|
|
|
971
973
|
// #2347 §10 review: unset startup concurrency is automatic (deriveSpawnConcurrency: 2–10 by free memory and cores),
|
|
972
974
|
// not 10 — an empty field, and a save that leaves it unset (the defaults merge replaces `startup` whole).
|
|
973
975
|
concurrency: startup.concurrency == null ? "" : String(startup.concurrency), stagger: String(startup.stagger_delay_ms ?? 500),
|
|
974
|
-
tips: String(
|
|
976
|
+
tips: String(noticeMode(d.tips)),
|
|
975
977
|
replyTriggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default),
|
|
976
978
|
classicReplyTriggers: Boolean(c.reply_triggers ?? d.reply_triggers ?? schema.reply_triggers_default), classicReplyTriggersInherit: !hasOwn(c, "reply_triggers"),
|
|
977
979
|
classicBackend: c.backend || d.backend || "claude-code",
|
|
@@ -1029,7 +1031,7 @@ function General({ ctx, part = "all" }) {
|
|
|
1029
1031
|
}, {
|
|
1030
1032
|
backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
|
|
1031
1033
|
agent_mode: d.agent_mode || "mcp", tool_set: TOOL_SETS.includes(d.tool_set) ? d.tool_set : null, tool_progress: d.tool_progress ?? "off",
|
|
1032
|
-
tips:
|
|
1034
|
+
tips: d.tips ?? true, cross_instance_visibility: visibilityDefault(d), reply_triggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
|
|
1033
1035
|
dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, dashboardDefault(d, k)])),
|
|
1034
1036
|
office_access: dashboardDefault(d, "office"),
|
|
1035
1037
|
hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
|
|
@@ -1190,7 +1192,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1190
1192
|
// serves their reads there).
|
|
1191
1193
|
const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
|
|
1192
1194
|
const original = useMemo(() => ({
|
|
1193
|
-
notify:
|
|
1195
|
+
notify: noticeMode(web.notify_login),
|
|
1194
1196
|
echo: web.echo_to_channel ?? true,
|
|
1195
1197
|
log: web.log_tab === true, // #1218: off by default
|
|
1196
1198
|
usagePub: web.usage_report_on_public_link === true, // #2122: off by default
|
|
@@ -1353,7 +1355,9 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1353
1355
|
${named || f.autoOpen ? html`<label class=${fc("check", "autoOpen")}><input id="publicAutoOpen" type="checkbox" checked=${f.autoOpen} onChange=${(e) => set("autoOpen")(e.target.checked)} /><span>${tn("publicAutoOpen")}</span><${Impact} schema=${schema} field="web.public_link.auto_open" /></label>
|
|
1354
1356
|
<p class="note">${tn("publicAutoOpenHint")}</p><${Feedback} error=${errors.autoOpen} />` : null}
|
|
1355
1357
|
</section>`) : null}
|
|
1356
|
-
${has("access") && !overPublicLink ? html`<${PublicTunnelToken}
|
|
1358
|
+
${has("access") && !overPublicLink ? html`<${PublicTunnelToken} />` : null}
|
|
1359
|
+
${has("access") ? html`<${OfficeSharesCard} />` : null}
|
|
1360
|
+
${has("access") && !overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
|
|
1357
1361
|
${has("chat") ? lock(html`<section class="card"><h3>${tn("webChat")}</h3>
|
|
1358
1362
|
<label class=${fc("check", "echo")}><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
|
|
1359
1363
|
<p class="note">${tn("webEchoHint")}</p>
|
|
@@ -33,8 +33,8 @@ const ROWS = [
|
|
|
33
33
|
// Fleet · Agents
|
|
34
34
|
["agents.new", "agents", "#newAgentBtn", "settings.newAgent", null, ["new agent", "create", "add agent", "instance"], ["新增 agent", "建立", "新 agent"]],
|
|
35
35
|
["agents.provider-keys", "agents", ".s-provider-keys", "settings.providerKeys", "web.provider_secrets", ["api key", "provider", "openai", "anthropic", "token", "secret"], ["API 金鑰", "金鑰", "供應商", "token"]],
|
|
36
|
-
["defaults.tips", "agent-defaults", "#g-tips", "settings.dailyTips", "defaults.tips", ["daily tips", "
|
|
37
|
-
["web.notify-login", "access", "#webNotifyLogin", "settings.webNotifyLogin", "web.notify_login", ["sign in", "login", "
|
|
36
|
+
["defaults.tips", "agent-defaults", "#g-tips", "settings.dailyTips", "defaults.tips", ["daily tips", "notices"], ["每日提示"]],
|
|
37
|
+
["web.notify-login", "access", "#webNotifyLogin", "settings.webNotifyLogin", "web.notify_login", ["sign in", "login", "notices"], ["登入通知"]],
|
|
38
38
|
// Fleet · Agent defaults
|
|
39
39
|
["defaults.locale", "agent-defaults", "#g-locale", "settings.language", "defaults.locale", ["language", "bot language", "reply language", "locale"], ["語言", "回覆語言", "bot 語言"]],
|
|
40
40
|
["defaults.backend", "agent-defaults", "#g-be", "settings.defaultBackend", "defaults.backend", ["backend", "cli", "claude", "codex", "kiro"], ["後端", "CLI"]],
|
|
@@ -4,10 +4,10 @@ import { register } from "/assets/app-i18n.js";
|
|
|
4
4
|
|
|
5
5
|
register("settings", {
|
|
6
6
|
en: {
|
|
7
|
-
noticeTopic: "Topic",
|
|
7
|
+
noticeTopic: "Topic", noticeOff: "Off",
|
|
8
8
|
webNotifyLogin: "Web sign-in notices", dailyTips: "Daily tips",
|
|
9
|
-
webNotifyLoginHint: "
|
|
10
|
-
dailyTipsHint: "
|
|
9
|
+
webNotifyLoginHint: "Posted in the topic. Off suppresses operator notices only; guest and public-link security notices still post. Default: Topic.",
|
|
10
|
+
dailyTipsHint: "Posted in the General topic. Default: Topic. /tips still posts where invoked.",
|
|
11
11
|
title: "Settings",
|
|
12
12
|
myAgents: "My Agents",
|
|
13
13
|
bots: "Bots",
|
|
@@ -679,10 +679,10 @@ register("settings", {
|
|
|
679
679
|
se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。",
|
|
680
680
|
se_unavailable: "無法使用",
|
|
681
681
|
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。進度前綴不是反應,而是進度訊息開頭的 emoji。",
|
|
682
|
-
noticeTopic: "頻道",
|
|
682
|
+
noticeTopic: "頻道", noticeOff: "關閉",
|
|
683
683
|
webNotifyLogin: "網頁登入通知", dailyTips: "每日提示",
|
|
684
|
-
webNotifyLoginHint: "
|
|
685
|
-
dailyTipsHint: "
|
|
684
|
+
webNotifyLoginHint: "發在頻道。關閉只停用管理者登入通知;訪客與公開連結安全通知仍會發出。預設:頻道。",
|
|
685
|
+
dailyTipsHint: "發在 General 頻道。預設:頻道。/tips 仍送至呼叫的聊天室。",
|
|
686
686
|
title: "設定",
|
|
687
687
|
myAgents: "我的 Agent",
|
|
688
688
|
bots: "機器人",
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -2504,3 +2504,31 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2504
2504
|
.msg.user .msg-reactions { justify-content: flex-end; }
|
|
2505
2505
|
.msg-reactions .reaction { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 24px; padding: 0 6px;
|
|
2506
2506
|
border: 1px solid var(--border); border-radius: var(--r-full); background: var(--surface-2); font-family: var(--font-emoji); font-size: 15px; line-height: 1; }
|
|
2507
|
+
|
|
2508
|
+
/* #2636: an Office share link's page (office-share-page.js) — the Office alone, filling the window under a one-line head. */
|
|
2509
|
+
.share-page { display: flex; flex-direction: column; min-height: 100dvh; background: var(--bg); color: var(--text); }
|
|
2510
|
+
.share-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-1) var(--s-3); padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--border); }
|
|
2511
|
+
.share-head .share-note { margin-left: auto; }
|
|
2512
|
+
.share-stage { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; padding: var(--s-2); }
|
|
2513
|
+
.share-loading, .share-page > .note { padding: var(--s-4); }
|
|
2514
|
+
.share-ended { align-items: center; justify-content: center; padding: var(--s-6); text-align: center; }
|
|
2515
|
+
@media (max-width: 640px) { .share-head .share-note { margin-left: 0; flex-basis: 100%; } }
|
|
2516
|
+
|
|
2517
|
+
/* #2636: Office share links — the Share… dialog and the open links (office-share-dialog.js; Settings → Access & security). */
|
|
2518
|
+
.os { display: flex; flex-direction: column; gap: var(--s-3); }
|
|
2519
|
+
.os > p { margin: 0; }
|
|
2520
|
+
.os-form { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-3); align-items: end; }
|
|
2521
|
+
.os-form .check, .os-form .note { grid-column: 1 / -1; margin: 0; }
|
|
2522
|
+
.os-made { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
|
|
2523
|
+
.os-url { flex: 1 1 260px; min-width: 0; min-height: 40px; padding: var(--s-2) var(--s-3); border: 1px solid var(--border-strong); border-radius: var(--r-sm);
|
|
2524
|
+
background: var(--bg); color: var(--text); font-size: var(--fs-sm); outline: none; }
|
|
2525
|
+
.os-url:focus { border-color: var(--focus); box-shadow: 0 0 0 3px var(--accent-dim); }
|
|
2526
|
+
.os-made .note { flex-basis: 100%; margin: 0; }
|
|
2527
|
+
.os-head { margin: var(--s-2) 0 0; font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--text-2); }
|
|
2528
|
+
.os-rows { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-2); }
|
|
2529
|
+
.os-row { display: flex; align-items: center; gap: var(--s-3); padding: var(--s-2) var(--s-3); border: 1px solid var(--border); border-radius: var(--r-sm); }
|
|
2530
|
+
.os-what { flex: 1; min-width: 0; overflow-wrap: anywhere; font-size: var(--fs-sm); }
|
|
2531
|
+
.os-all { margin-top: var(--s-2); }
|
|
2532
|
+
.os-none { margin: 0; }
|
|
2533
|
+
.os-form .os-err { grid-column: 1 / -1; margin: 0; padding: var(--s-2) var(--s-3); border: 1px solid var(--danger); border-radius: var(--r-sm); }
|
|
2534
|
+
@media (max-width: 480px) { .os-form { grid-template-columns: 1fr; } }
|
package/dist/web-api.d.ts
CHANGED
|
@@ -200,6 +200,20 @@ export interface WebApiContext {
|
|
|
200
200
|
readonly officeWrites?: OfficeWriteLimiter;
|
|
201
201
|
/** #2578: the agents' live acts in the Office view (office_set); absent: /ui/office-acts is not offered. */
|
|
202
202
|
readonly officeActs?: Pick<OfficeActs, "live">;
|
|
203
|
+
/** #2636: Office share links (office-share-service.ts): the open ones, making and ending one. */
|
|
204
|
+
readonly officeShares?: {
|
|
205
|
+
enabled(): boolean;
|
|
206
|
+
list(): import("./office-share.js").OfficeShare[];
|
|
207
|
+
create(opts: {
|
|
208
|
+
office: string;
|
|
209
|
+
minutes?: number;
|
|
210
|
+
text?: boolean;
|
|
211
|
+
owner: string;
|
|
212
|
+
}): Promise<import("./office-share-service.js").OfficeShareMade>;
|
|
213
|
+
revoke(id: string): number;
|
|
214
|
+
};
|
|
215
|
+
/** #2636: a share was made or ended from the page — said in General (who, which office, until when). */
|
|
216
|
+
officeShareNotice?(event: "created" | "revoked", who: string, share: import("./office-share.js").OfficeShare | null, count: number): void;
|
|
203
217
|
/** #2513: the Office view's uploaded skin packs (web-office-skins/); absent: /ui/office-skins is not offered. */
|
|
204
218
|
readonly officeSkins?: OfficeSkinStore;
|
|
205
219
|
/** How often a person may upload or delete a skin pack (absent: no limit). */
|