@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.3
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 +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +599 -127
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +82 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +45 -83
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +8 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +13 -5
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +38 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +50 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
|
@@ -10,6 +10,9 @@ import { Icon } from "/assets/ui-icons.js";
|
|
|
10
10
|
import { toast } from "/assets/ui-toast.js";
|
|
11
11
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
12
12
|
import { api, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
13
|
+
import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
14
|
+
import { DiscordServerPicker } from "./settings-discord.js";
|
|
15
|
+
import { startOperation } from "./settings-apply.js";
|
|
13
16
|
import {
|
|
14
17
|
ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
|
|
15
18
|
VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
|
|
@@ -341,15 +344,31 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
341
344
|
const [bindBusy, setBindBusy] = useState(false);
|
|
342
345
|
const [token, setToken] = useState("");
|
|
343
346
|
const [tokenNote, setTokenNote] = useState(null);
|
|
347
|
+
// Replace token (#1519 P1): the field opens on Replace, and a token is staged only once the platform has named its bot.
|
|
348
|
+
const [replacing, setReplacing] = useState(false);
|
|
349
|
+
const [tokenId, setTokenId] = useState(null);
|
|
350
|
+
const [tokenBusy, setTokenBusy] = useState(false);
|
|
351
|
+
const status = (ctx.connections || []).find((c) => c && c.id === id) || {};
|
|
344
352
|
const [emojis, setEmojis] = useState({ value: null, baseline: null });
|
|
345
353
|
const lease = useLease("bot-dialog");
|
|
346
354
|
const accessWarn = mode === "locked" && users.length === 0 ? "accessLockedEmpty" : "";
|
|
355
|
+
// The token's revision (#1529 review): typing moves it, and a Verify for an older value is dropped when it lands.
|
|
356
|
+
const tokenRev = useRef(0);
|
|
357
|
+
const verifyToken = async () => {
|
|
358
|
+
setTokenBusy(true);
|
|
359
|
+
const mine = epoch.current, at = tokenRev.current;
|
|
360
|
+
const identity = await verifyBotToken(type, token);
|
|
361
|
+
if (!lease.current() || epoch.current !== mine) return;
|
|
362
|
+
setTokenBusy(false);
|
|
363
|
+
if (tokenRev.current === at) setTokenId(identity);
|
|
364
|
+
};
|
|
347
365
|
const stageToken = () => {
|
|
348
366
|
if (!ch) return;
|
|
349
367
|
if (!token) { setTokenNote({ error: tn("tokenRequired") }); return; }
|
|
368
|
+
if (!tokenId || !tokenId.valid) { setTokenNote({ error: tn("wizardNeedVerify") }); return; }
|
|
350
369
|
// The token goes into the staged change and nowhere else: not a label, not the URL, not storage.
|
|
351
370
|
ctx.stage(`secret:${id}`, { label: tn("rotateToken", type), impact: schema.order[0], connectionSecret: { id, secret: token } });
|
|
352
|
-
setToken("");
|
|
371
|
+
setToken(""); setTokenId(null); setReplacing(false);
|
|
353
372
|
setTokenNote({ text: tn("tokenQueued") });
|
|
354
373
|
};
|
|
355
374
|
const stage = () => {
|
|
@@ -419,45 +438,96 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
419
438
|
previewBody=${(map) => ({ channel_id: id, platform: type, channel_config: map })} onChange=${setEmojis} />
|
|
420
439
|
</${Drawer}>
|
|
421
440
|
<${Drawer} title=${tn("advancedSection")}>
|
|
422
|
-
<div class="field"><span>${tn("
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
441
|
+
<div class="field"><span>${tn("tokenStatus")}</span>
|
|
442
|
+
<div class="dlg-inline-actions"><span class="token-status">${status.token_present === false ? tn("tokenMissing")
|
|
443
|
+
: status.identity && status.identity.username ? tn("tokenSetAs", botHandle(status.identity.username)) : tn("tokenSet")}</span>
|
|
444
|
+
${replacing ? null : html`<button type="button" class="btn btn-sm" onClick=${() => { setReplacing(true); setTokenNote(null); }}>${tn("tokenReplace")}</button>`}</div></div>
|
|
445
|
+
${replacing ? html`<${TokenField} id="bot-token" platform=${type} value=${token} identity=${tokenId} busy=${tokenBusy}
|
|
446
|
+
onInput=${(v) => { tokenRev.current++; setToken(v); setTokenId(null); }} onVerify=${verifyToken} hint=${tn("tokenHint")} />
|
|
447
|
+
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${!tokenId || !tokenId.valid} onClick=${stageToken}>${tn("tokenButton")}</button>
|
|
448
|
+
<button type="button" class="btn btn-sm btn-ghost" onClick=${() => { tokenRev.current++; setReplacing(false); setToken(""); setTokenId(null); }}>${tn("cancel")}</button></div>` : null}
|
|
449
|
+
${tokenNote ? html`<p class=${`feedback${tokenNote.error ? " error" : ""}`} role="status">${tokenNote.error || tokenNote.text}</p>` : null}
|
|
450
|
+
<${TokenEnvNote} name=${ch.bot_token_env} />
|
|
428
451
|
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm danger" onClick=${remove}><${Icon} name="trash" size=${14} />${tn("deleteBot")}</button></div>
|
|
429
452
|
</${Drawer}>
|
|
430
453
|
</${EditDialog}>`;
|
|
431
454
|
}
|
|
432
455
|
|
|
433
|
-
/**
|
|
456
|
+
/**
|
|
457
|
+
* A new connection (#1519 P1): the bot token, checked with the platform, then the server-side plan — a free id and a
|
|
458
|
+
* generated token env (shown under Advanced, read-only) — then the same write the setup wizard makes, without an agent:
|
|
459
|
+
* one operation (the confirmation an admin may have to give, then Apply, which offers the restart a new connection
|
|
460
|
+
* needs). Never a replacement: the server refuses an id or token env another connection holds.
|
|
461
|
+
*/
|
|
434
462
|
export function NewBotDialog({ ctx, onClose }) {
|
|
435
463
|
const lease = useLease("new-bot");
|
|
436
|
-
const [f, setF] = useState({ type: "discord", id: "",
|
|
464
|
+
const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "" });
|
|
465
|
+
const [plan, setPlan] = useState(null);
|
|
437
466
|
const [busy, setBusy] = useState(false);
|
|
438
467
|
const [err, setErr] = useState("");
|
|
468
|
+
// The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
|
|
469
|
+
// that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
|
|
470
|
+
const rev = useRef(0);
|
|
471
|
+
const body = (x) => ({ platform: x.type, connection_only: true, ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
|
|
472
|
+
...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
|
|
473
|
+
...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
|
|
474
|
+
const planFor = async (x) => {
|
|
475
|
+
const at = rev.current;
|
|
476
|
+
const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(body(x)) }).catch(() => ({ ok: false, body: {} }));
|
|
477
|
+
if (!lease.current() || rev.current !== at) return null;
|
|
478
|
+
if (!res.ok) { setErr((res.body && res.body.error) || tn("failed")); setPlan(null); return null; }
|
|
479
|
+
setErr(""); setPlan(res.body); return res.body;
|
|
480
|
+
};
|
|
481
|
+
const verify = async () => {
|
|
482
|
+
const at = rev.current, x = f;
|
|
483
|
+
const live = () => lease.current() && rev.current === at;
|
|
484
|
+
setBusy(true); setErr("");
|
|
485
|
+
const identity = await verifyBotToken(x.type, x.token);
|
|
486
|
+
if (!live()) { if (lease.current()) setBusy(false); return; }
|
|
487
|
+
let guilds = [];
|
|
488
|
+
if (identity.valid && x.type === "discord") {
|
|
489
|
+
const g = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token: x.token }) }).catch(() => null);
|
|
490
|
+
if (!live()) { if (lease.current()) setBusy(false); return; }
|
|
491
|
+
guilds = (g && g.body && g.body.guilds) || [];
|
|
492
|
+
}
|
|
493
|
+
// Onto the form as it is now (same revision, so the same token): never a copy of the form from before the await.
|
|
494
|
+
setF((cur) => ({ ...cur, identity, guilds })); setBusy(false);
|
|
495
|
+
if (identity.valid) await planFor({ ...x, identity, guilds });
|
|
496
|
+
};
|
|
439
497
|
const save = async () => {
|
|
440
|
-
if (!f.
|
|
441
|
-
const
|
|
498
|
+
if (!f.identity || !f.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
|
|
499
|
+
const x = f;
|
|
442
500
|
setBusy(true);
|
|
443
|
-
const
|
|
444
|
-
if (!
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
501
|
+
const p = await planFor(x);
|
|
502
|
+
if (!p) { if (lease.current()) setBusy(false); return; }
|
|
503
|
+
const handed = startOperation([{ label: tn("addBotLabel", p.channel_id), impact: impactOf(ctx.schema, "fleet.channels"),
|
|
504
|
+
request: { method: "POST", url: "/api/settings/quickstart/commit",
|
|
505
|
+
body: { ...body(x), channel_id: p.channel_id, token_env: p.token_env, token_env_generated: true, token: x.token }, sensitive: true } }]);
|
|
506
|
+
if (!handed) { setErr(tn("applyBusyLocal")); setBusy(false); return; }
|
|
507
|
+
setF((x) => ({ ...x, token: "" }));
|
|
508
|
+
onClose();
|
|
448
509
|
};
|
|
449
|
-
|
|
510
|
+
// A change of platform or id is another connection: what was verified and planned no longer applies. The server or
|
|
511
|
+
// group does not change the id or the token's name, so the plan (and its name under Advanced) stays.
|
|
512
|
+
const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
|
|
450
513
|
return html`<${Dialog} title=${tn("newBot")} onClose=${onClose} busy=${busy}
|
|
451
514
|
actions=${html`<button type="button" class="btn" disabled=${busy} onClick=${onClose}>${tn("cancel")}</button>
|
|
452
|
-
<button type="button" class="btn btn-primary" disabled=${busy} onClick=${save}>${tn("save")}</button>`}>
|
|
515
|
+
<button type="button" class="btn btn-primary" disabled=${busy || !f.identity || !f.identity.valid} onClick=${save}>${tn("save")}</button>`}>
|
|
453
516
|
<div class="form">
|
|
454
|
-
<div class="
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
517
|
+
<div class="field"><label for="nb-type">${tn("type")}</label><${Select} id="nb-type" value=${f.type} onChange=${set("type")} options=${CH_TYPES} /></div>
|
|
518
|
+
<${TokenField} id="nb-token" platform=${f.type} value=${f.token} identity=${f.identity} busy=${busy}
|
|
519
|
+
onInput=${(v) => { rev.current++; setF((x) => ({ ...x, token: v, identity: null, guilds: [] })); setPlan(null); }} onVerify=${verify} hint=${tn("newBotTokenHint")} />
|
|
520
|
+
${f.type === "discord"
|
|
521
|
+
? html`<${DiscordServerPicker} idPrefix="nb" token=${f.identity && f.identity.valid ? f.token : ""} bot=${f.identity && botHandle(f.identity.username)}
|
|
522
|
+
invite=${f.identity && f.identity.invite} portal=${f.identity && f.identity.portal} guilds=${f.guilds} onGuilds=${(g) => setF((x) => ({ ...x, guilds: g }))}
|
|
523
|
+
guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
|
|
524
|
+
: html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
|
|
525
|
+
<input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value.trim())} /></div>`}
|
|
526
|
+
<${Drawer} title=${tn("advancedSection")}>
|
|
527
|
+
<div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
|
|
528
|
+
value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
|
|
529
|
+
<${TokenEnvNote} name=${plan && plan.token_env} />
|
|
530
|
+
</${Drawer}>
|
|
461
531
|
${err ? html`<p class="feedback error" role="alert">${err}</p>` : null}
|
|
462
532
|
</div></${Dialog}>`;
|
|
463
533
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// #1519 P3 (docs/design/ux-onboarding-walkthrough.md §5.3): after a Discord token verifies — the invite (one permission
|
|
2
|
+
// set, never Administrator), the Message Content note with the portal's Bot page, the server the bot joins found by
|
|
3
|
+
// asking Discord every 2 s for up to 2 minutes, and the server's text channels as a list ("general" first choice). The
|
|
4
|
+
// setup wizard and New connection use it.
|
|
5
|
+
//
|
|
6
|
+
// Every read is an operation of its own (#1533 review): numbered when it starts, and its answer is taken only while it is
|
|
7
|
+
// still the current one of its kind and this picker is still on the page. A new token, another server, a manual choice,
|
|
8
|
+
// Stop, the 2-minute limit or leaving the page each retire what was asked before — an answer about A never lands after
|
|
9
|
+
// B, nor after A again (an older A), nor once the page is gone.
|
|
10
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
11
|
+
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { api } from "./settings-confirm.js";
|
|
13
|
+
import { Select } from "./settings-dialogs.js";
|
|
14
|
+
|
|
15
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
16
|
+
export const POLL_MS = 2000, POLL_FOR_MS = 120_000;
|
|
17
|
+
const clock = () => (typeof performance !== "undefined" ? performance.now() : Date.now()); // elapsed time: monotonic
|
|
18
|
+
const probe = (body) => api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify(body) }).catch(() => ({ ok: false, body: {} }));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* props: idPrefix, token, bot (@name), invite, portal, guilds, onGuilds(list), guild, onGuild(id), channel, onChannel(id).
|
|
22
|
+
* The parent owns the values; this owns the asking.
|
|
23
|
+
*/
|
|
24
|
+
export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guilds, onGuilds, guild, onGuild, channel, onChannel }) {
|
|
25
|
+
const [polling, setPolling] = useState(null); // { timedOut } while waiting for the bot to join
|
|
26
|
+
const [channels, setChannels] = useState(null); // null: not asked; { list } | { error }
|
|
27
|
+
const live = useRef(true);
|
|
28
|
+
const now = useRef({ token, guild, channel });
|
|
29
|
+
now.current = { token, guild, channel };
|
|
30
|
+
// The current operation of each kind; an answer is taken only while its number is still the one here.
|
|
31
|
+
// `choice`: the one generation of the server choice that polls and Check again both write (#1533 review r2) — a read
|
|
32
|
+
// started before any later pick, Check again or Stop cannot pick.
|
|
33
|
+
const op = useRef({ poll: 0, check: 0, channels: 0, choice: 0 });
|
|
34
|
+
const poll = useRef({ timer: null, until: 0, known: new Set(), asking: false });
|
|
35
|
+
const chosen = useRef(false); // the person picked a General channel since this server's list was asked
|
|
36
|
+
|
|
37
|
+
const stopPolling = (timedOut = false) => {
|
|
38
|
+
op.current.poll++; op.current.choice++; // retires the poll's reads still on their way
|
|
39
|
+
if (poll.current.timer) { clearInterval(poll.current.timer); poll.current.timer = null; }
|
|
40
|
+
setPolling(timedOut ? { timedOut: true } : null);
|
|
41
|
+
};
|
|
42
|
+
useEffect(() => () => { live.current = false; if (poll.current.timer) clearInterval(poll.current.timer); }, []);
|
|
43
|
+
// A new token is another bot: what was being waited for, or listed, was about the old one — and so was its General.
|
|
44
|
+
const lastToken = useRef(token);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
op.current.check++; op.current.channels++; stopPolling(); setChannels(null);
|
|
47
|
+
if (lastToken.current !== token && now.current.channel) onChannel("");
|
|
48
|
+
lastToken.current = token;
|
|
49
|
+
}, [token]);
|
|
50
|
+
|
|
51
|
+
/** One read of the bot's servers, for operation `kind` number `seq`; joined: pick it (unless the person chose meanwhile). */
|
|
52
|
+
const readGuilds = async (kind, seq, known, deadline) => {
|
|
53
|
+
const asked = token, choice = op.current.choice;
|
|
54
|
+
const res = await probe({ action: "guilds", token: asked });
|
|
55
|
+
const current = () => live.current && op.current[kind] === seq && op.current.choice === choice && now.current.token === asked
|
|
56
|
+
&& (deadline == null || clock() <= deadline);
|
|
57
|
+
if (!current()) return false;
|
|
58
|
+
const list = (res.ok && res.body && Array.isArray(res.body.guilds)) ? res.body.guilds : null;
|
|
59
|
+
if (!list) return false;
|
|
60
|
+
onGuilds(list);
|
|
61
|
+
const joined = list.filter((g) => !known.has(g.id));
|
|
62
|
+
if (!joined.length) return false;
|
|
63
|
+
if (joined.length === 1 || !now.current.guild) pickGuild(joined[0].id, false);
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
const startPolling = () => {
|
|
67
|
+
stopPolling();
|
|
68
|
+
const seq = op.current.poll, known = new Set((guilds || []).map((g) => g.id)), until = clock() + POLL_FOR_MS;
|
|
69
|
+
poll.current = { timer: null, until, known, asking: false };
|
|
70
|
+
setPolling({ timedOut: false });
|
|
71
|
+
const record = poll.current;
|
|
72
|
+
record.timer = setInterval(async () => {
|
|
73
|
+
if (op.current.poll !== seq) return;
|
|
74
|
+
if (clock() > until) { stopPolling(true); return; }
|
|
75
|
+
if (record.asking) return; // one read at a time
|
|
76
|
+
record.asking = true;
|
|
77
|
+
// Its own record only: a later poll's in-flight flag is not this read's to clear.
|
|
78
|
+
await readGuilds("poll", seq, known, until).finally(() => { record.asking = false; });
|
|
79
|
+
}, POLL_MS);
|
|
80
|
+
};
|
|
81
|
+
// Check again: a new, explicit read of its own (it does not revive a stopped poll).
|
|
82
|
+
// A newer, explicit read: it supersedes any read still on its way (theirs no longer picks).
|
|
83
|
+
const checkAgain = () => { op.current.choice++; const seq = ++op.current.check; void readGuilds("check", seq, poll.current.known, null); };
|
|
84
|
+
|
|
85
|
+
/** A server is picked — by the person (`manual`) or because the bot joined it. Either way the old General goes. */
|
|
86
|
+
const pickGuild = (id, manual) => {
|
|
87
|
+
// Any pick — the person's, or a read's — ends the waiting and retires every other read still on its way.
|
|
88
|
+
stopPolling(); op.current.check++;
|
|
89
|
+
if (id !== now.current.guild && now.current.channel) onChannel("");
|
|
90
|
+
onGuild(id);
|
|
91
|
+
};
|
|
92
|
+
const pickChannel = (id) => { chosen.current = true; onChannel(id); };
|
|
93
|
+
|
|
94
|
+
// The chosen server's text channels; "general" when there is one and none is chosen yet.
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const seq = ++op.current.channels;
|
|
97
|
+
chosen.current = false;
|
|
98
|
+
setChannels(null);
|
|
99
|
+
if (!guild || !token) return;
|
|
100
|
+
const askedToken = token, askedGuild = guild;
|
|
101
|
+
(async () => {
|
|
102
|
+
const res = await probe({ action: "channels", token: askedToken, guild_id: askedGuild });
|
|
103
|
+
if (!live.current || op.current.channels !== seq || now.current.token !== askedToken || now.current.guild !== askedGuild) return;
|
|
104
|
+
if (!res.ok || !res.body || !Array.isArray(res.body.channels)) { setChannels({ error: (res.body && res.body.error) || tn("failed") }); return; }
|
|
105
|
+
setChannels({ list: res.body.channels });
|
|
106
|
+
// The first choice only fills an empty choice the person has not made.
|
|
107
|
+
if (!chosen.current && !now.current.channel) {
|
|
108
|
+
const general = res.body.channels.find((c) => c.name.toLowerCase() === "general");
|
|
109
|
+
if (general) onChannel(general.id);
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
}, [guild, token]);
|
|
113
|
+
|
|
114
|
+
return html`<div class="discord-setup">
|
|
115
|
+
${invite ? html`<div class="dlg-inline-actions">
|
|
116
|
+
<a class="btn btn-sm btn-primary" href=${invite} target="_blank" rel="noopener noreferrer" onClick=${startPolling}>${tn("discordInvite", bot || tn("discordTheBot"))}</a>
|
|
117
|
+
${polling && !polling.timedOut ? html`<span class="note" role="status">${tn("discordWaitingJoin")}</span>` : null}
|
|
118
|
+
${polling ? html`<button type="button" class="btn btn-sm btn-ghost" onClick=${checkAgain}>${tn("discordCheckAgain")}</button>` : null}</div>
|
|
119
|
+
<p class="note">${tn("discordIntentNote")} ${portal ? html`<a href=${portal} target="_blank" rel="noopener noreferrer">${tn("discordPortalBot")}</a>` : null}</p>` : null}
|
|
120
|
+
<div class="field"><label for=${`${idPrefix}-guild`}>${tn("guildIdField")}</label>
|
|
121
|
+
${guilds && guilds.length
|
|
122
|
+
? html`<${Select} id=${`${idPrefix}-guild`} value=${guild} onChange=${(v) => pickGuild(v, true)} options=${["", ...guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} />`
|
|
123
|
+
: html`<input id=${`${idPrefix}-guild`} type="text" value=${guild} placeholder=${tn("discordNoServerYet")} onInput=${(e) => pickGuild(e.target.value.trim(), true)} />`}</div>
|
|
124
|
+
<div class="field"><label for=${`${idPrefix}-gen`}>${tn("wizardGeneralChannel")}</label>
|
|
125
|
+
${channels && channels.list && channels.list.length
|
|
126
|
+
? html`<${Select} id=${`${idPrefix}-gen`} value=${channel} onChange=${pickChannel} options=${["", ...channels.list.map((c) => ({ value: c.id, label: `#${c.name}` }))]} />`
|
|
127
|
+
: html`<input id=${`${idPrefix}-gen`} type="text" value=${channel} onInput=${(e) => pickChannel(e.target.value.trim())} />`}
|
|
128
|
+
${channels && channels.error ? html`<p class="feedback warning">${channels.error}</p>` : null}</div>
|
|
129
|
+
</div>`;
|
|
130
|
+
}
|
|
@@ -275,3 +275,24 @@ export function fullModelRequests(model, current) {
|
|
|
275
275
|
}
|
|
276
276
|
return out;
|
|
277
277
|
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* #1519 P6 (design §5.6): one connection's own state, from its own metadata (GET /api/settings/connections) — not the
|
|
281
|
+
* fleet's. { key, cls, bot }: key names the settings string, cls the dot ("ok" | "warn" | "bad" | "off"), bot its @name.
|
|
282
|
+
* In order: no fleet answering, no token, a named problem, then the adapter's own status.
|
|
283
|
+
*/
|
|
284
|
+
export function connectionState(meta, fleetUp) {
|
|
285
|
+
const m = meta || {};
|
|
286
|
+
if (!fleetUp) return { key: "problem", cls: "bad" };
|
|
287
|
+
if (m.token_present === false) return { key: "connTokenMissing", cls: "bad" };
|
|
288
|
+
if (m.problem === "missing_intent") return { key: "discordMissingIntent", cls: "bad" };
|
|
289
|
+
if (m.problem === "rejected") return { key: "connRejected", cls: "bad" };
|
|
290
|
+
const bot = m.identity && m.identity.username ? `@${String(m.identity.username).replace(/^@/, "")}` : "";
|
|
291
|
+
switch (m.status) {
|
|
292
|
+
case "connected": return { key: bot ? "connConnectedAs" : "connected", cls: "ok", bot };
|
|
293
|
+
case "retrying": return { key: "connReconnecting", cls: "warn" };
|
|
294
|
+
case "starting": return { key: "connStarting", cls: "warn" };
|
|
295
|
+
case "failed": return { key: "connFailed", cls: "bad" };
|
|
296
|
+
default: return { key: "connNotRunning", cls: "off" };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -100,7 +100,18 @@ register("settings", {
|
|
|
100
100
|
tokenEnv: "Bot token env var",
|
|
101
101
|
groupId: "Group / guild id",
|
|
102
102
|
nameField: "Name",
|
|
103
|
-
|
|
103
|
+
tokenShow: "Show the token", tokenHide: "Hide the token", tokenIsBot: "This is {0}.", tokenStatus: "Bot token",
|
|
104
|
+
tokenSet: "Token set", tokenSetAs: "Token set · {0}", tokenMissing: "Token missing", tokenReplace: "Replace",
|
|
105
|
+
tokenStoredAs: "The token is stored on this computer in ~/.agend/.env under this name — generated, and only needed if you manage .env yourself.",
|
|
106
|
+
tokenSavedRestart: "Token saved. This connection is not running, so it starts with the new token when AgEnD restarts (on the host: agend restart).",
|
|
107
|
+
newBotTokenHint: "Paste the bot's token. It is checked with the platform, then stored on this computer — never in the browser.",
|
|
108
|
+
connectionIdOptional: "Connection id (optional)",
|
|
109
|
+
connConnectedAs: "Connected · {0}", connReconnecting: "Reconnecting…", connStarting: "Starting…", connFailed: "Could not connect",
|
|
110
|
+
connNotRunning: "Not running", connTokenMissing: "Token missing", connRejected: "Token rejected — replace it",
|
|
111
|
+
discordInvite: "Invite {0} to your server", discordTheBot: "the bot", discordWaitingJoin: "Waiting for the bot to join a server…",
|
|
112
|
+
discordCheckAgain: "Check again", discordNoServerYet: "Invite the bot first, or type the server id",
|
|
113
|
+
discordIntentNote: "Turn on Message Content Intent for this bot in the Discord developer portal (it cannot be set from here):", discordPortalBot: "Developer portal → Bot",
|
|
114
|
+
discordMissingIntent: "Turn on Message Content Intent in the Discord developer portal → Bot, then restart AgEnD.",
|
|
104
115
|
noAgents: "No agents yet — click + New.",
|
|
105
116
|
noBots: "No bots configured.",
|
|
106
117
|
currentVersion: "Current version",
|
|
@@ -123,7 +134,7 @@ register("settings", {
|
|
|
123
134
|
wizardButton: "Setup wizard",
|
|
124
135
|
wizardTitle: "Set up AgEnD",
|
|
125
136
|
wizardStep: "Step {0} of {1}",
|
|
126
|
-
wizardRerun: "A fleet is already configured — finishing
|
|
137
|
+
wizardRerun: "A fleet is already configured — finishing adds a new connection; nothing already there is replaced.",
|
|
127
138
|
wizardBack: "Back",
|
|
128
139
|
wizardNext: "Next",
|
|
129
140
|
wizardFinish: "Create and start",
|
|
@@ -131,6 +142,14 @@ register("settings", {
|
|
|
131
142
|
wizardBackendFound: "Found on this host: {0}",
|
|
132
143
|
wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
133
144
|
wizardAgentName: "First agent name",
|
|
145
|
+
wizardAgentWhich: "Which agent answers on this bot",
|
|
146
|
+
wizardAgentNew: "A new agent",
|
|
147
|
+
wizardAgentExisting: "An agent you already have",
|
|
148
|
+
wizardAgentPick: "Agent",
|
|
149
|
+
wizardAgentExistingNote: "It keeps its directory, backend and settings; only its chat connection becomes this bot (if it has one now, it moves here).",
|
|
150
|
+
wizardAgentNewName: "New agent name",
|
|
151
|
+
wizardTopicNotCarried: "Its topic ({0}) is in the chat of its old connection, so it is not carried over: in the new chat it has no topic until one is bound.",
|
|
152
|
+
wizardTopicGeneral: "It is the General agent: in the new chat it takes that chat's General topic.",
|
|
134
153
|
wizardNeedDir: "Enter an absolute working directory.",
|
|
135
154
|
wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
|
|
136
155
|
wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
|
|
@@ -165,7 +184,7 @@ register("settings", {
|
|
|
165
184
|
restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
166
185
|
restartFleetStarted: "Restarting AgEnD — this page keeps checking.",
|
|
167
186
|
restartFleetBusy: "Restarting…",
|
|
168
|
-
restartFleetTitle: "Restarting AgEnD…",
|
|
187
|
+
restartFleetTitle: "Restarting AgEnD…", connStartingNow: "Starting the new connection — no restart needed…",
|
|
169
188
|
restartFleetWaiting: "AgEnD is restarting. This page keeps checking and will say when it is back — there is nothing to press.",
|
|
170
189
|
working: "Working…",
|
|
171
190
|
restartFleetRefused: "Restart refused: {0}",
|
|
@@ -310,6 +329,7 @@ register("settings", {
|
|
|
310
329
|
editMode: "Edit",
|
|
311
330
|
viewMode: "View",
|
|
312
331
|
copy: "Copy",
|
|
332
|
+
devDownload: "Download fleet.yaml",
|
|
313
333
|
devEditor: "fleet.yaml editor",
|
|
314
334
|
devApply: "Apply & save",
|
|
315
335
|
devApplyHint: "Writes these settings and applies them, like Apply changes.",
|
|
@@ -424,7 +444,18 @@ register("settings", {
|
|
|
424
444
|
tokenEnv: "Bot token 環境變數",
|
|
425
445
|
groupId: "群組 / guild id",
|
|
426
446
|
nameField: "名稱",
|
|
427
|
-
|
|
447
|
+
tokenShow: "顯示 token", tokenHide: "隱藏 token", tokenIsBot: "這是 {0}。", tokenStatus: "Bot token",
|
|
448
|
+
tokenSet: "已設定 token", tokenSetAs: "已設定 token · {0}", tokenMissing: "缺少 token", tokenReplace: "更換",
|
|
449
|
+
tokenStoredAs: "token 存在這台電腦的 ~/.agend/.env,用這個名稱——自動產生,只有自行管理 .env 時才需要。",
|
|
450
|
+
tokenSavedRestart: "已儲存 token。這個連線目前沒有在執行,AgEnD 重新啟動時會用新的 token 啟動(在主機上:agend restart)。",
|
|
451
|
+
newBotTokenHint: "貼上 bot 的 token。會先向平台確認,再存到這台電腦——絕不存在瀏覽器。",
|
|
452
|
+
connectionIdOptional: "連線 id(選填)",
|
|
453
|
+
connConnectedAs: "已連線 · {0}", connReconnecting: "重新連線中…", connStarting: "啟動中…", connFailed: "無法連線",
|
|
454
|
+
connNotRunning: "未執行", connTokenMissing: "缺少 token", connRejected: "token 被拒——請更換",
|
|
455
|
+
discordInvite: "邀請 {0} 加入你的伺服器", discordTheBot: "bot", discordWaitingJoin: "等待 bot 加入伺服器…",
|
|
456
|
+
discordCheckAgain: "再檢查一次", discordNoServerYet: "請先邀請 bot,或輸入伺服器 id",
|
|
457
|
+
discordIntentNote: "請在 Discord 開發者入口網站為這個 bot 開啟 Message Content Intent(這裡無法代為設定):", discordPortalBot: "開發者入口網站 → Bot",
|
|
458
|
+
discordMissingIntent: "請到 Discord 開發者入口網站 → Bot 開啟 Message Content Intent,然後重新啟動 AgEnD。",
|
|
428
459
|
noAgents: "還沒有 agent — 點 + 新增。",
|
|
429
460
|
noBots: "尚未設定機器人。",
|
|
430
461
|
currentVersion: "目前版本",
|
|
@@ -447,7 +478,7 @@ register("settings", {
|
|
|
447
478
|
wizardButton: "設定精靈",
|
|
448
479
|
wizardTitle: "設定 AgEnD",
|
|
449
480
|
wizardStep: "第 {0} 步,共 {1} 步",
|
|
450
|
-
wizardRerun: "已經有設定好的 fleet ——
|
|
481
|
+
wizardRerun: "已經有設定好的 fleet —— 完成後會新增一個連線,不會取代任何既有的設定。",
|
|
451
482
|
wizardBack: "上一步",
|
|
452
483
|
wizardNext: "下一步",
|
|
453
484
|
wizardFinish: "建立並啟動",
|
|
@@ -455,6 +486,14 @@ register("settings", {
|
|
|
455
486
|
wizardBackendFound: "在這台主機上找到:{0}",
|
|
456
487
|
wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
457
488
|
wizardAgentName: "第一個 agent 名稱",
|
|
489
|
+
wizardAgentWhich: "由哪個 agent 在這個機器人上回覆",
|
|
490
|
+
wizardAgentNew: "新的 agent",
|
|
491
|
+
wizardAgentExisting: "既有的 agent",
|
|
492
|
+
wizardAgentPick: "Agent",
|
|
493
|
+
wizardAgentExistingNote: "它的目錄、backend 和設定都不變;只有聊天連線改成這個機器人(如果它目前已有連線,會移到這裡)。",
|
|
494
|
+
wizardAgentNewName: "新 agent 名稱",
|
|
495
|
+
wizardTopicNotCarried: "它的 topic({0})屬於舊連線的聊天室,不會帶過去:在新的聊天室裡,綁定新的 topic 之前它沒有 topic。",
|
|
496
|
+
wizardTopicGeneral: "它是 General agent:在新的聊天室裡會改用那裡的 General topic。",
|
|
458
497
|
wizardNeedDir: "請輸入絕對路徑的工作目錄。",
|
|
459
498
|
wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
|
|
460
499
|
wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
|
|
@@ -489,7 +528,7 @@ register("settings", {
|
|
|
489
528
|
restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
490
529
|
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。",
|
|
491
530
|
restartFleetBusy: "重啟中…",
|
|
492
|
-
restartFleetTitle: "AgEnD 重新啟動中…",
|
|
531
|
+
restartFleetTitle: "AgEnD 重新啟動中…", connStartingNow: "正在啟動新連線——不需要重新啟動…",
|
|
493
532
|
restartFleetWaiting: "AgEnD 正在重新啟動。此頁面會持續確認,恢復後會告訴你 —— 不需要再按任何按鈕。",
|
|
494
533
|
working: "執行中…",
|
|
495
534
|
restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
@@ -634,6 +673,7 @@ register("settings", {
|
|
|
634
673
|
editMode: "編輯",
|
|
635
674
|
viewMode: "檢視",
|
|
636
675
|
copy: "複製",
|
|
676
|
+
devDownload: "下載 fleet.yaml",
|
|
637
677
|
devEditor: "fleet.yaml 編輯器",
|
|
638
678
|
devApply: "套用並儲存",
|
|
639
679
|
devApplyHint: "寫入這些設定並套用,和「套用變更」相同。",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// #1519 P1 (docs/design/ux-onboarding-walkthrough.md §5.2): the one bot-token field — the setup wizard, "New
|
|
2
|
+
// connection" and a connection's "Replace token" use it. A password field (show/hide), Verify asks the platform who the
|
|
3
|
+
// token belongs to (Telegram getMe, Discord /users/@me) and shows the bot's name before anything is written. The token
|
|
4
|
+
// lives in this field's state only: never in storage, a URL, a label or the page's other state, and the server never
|
|
5
|
+
// hands it back — afterwards a connection shows "Token set · @bot".
|
|
6
|
+
import { html, useState } from "/assets/app-html.js";
|
|
7
|
+
import { t } from "/assets/app-i18n.js";
|
|
8
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
9
|
+
import { api } from "./settings-confirm.js";
|
|
10
|
+
|
|
11
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
12
|
+
|
|
13
|
+
/** Who this token is: { valid, username?, reason? } — one outbound check, the token sent once and not kept. */
|
|
14
|
+
export async function verifyBotToken(platform, token) {
|
|
15
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform, token }) })
|
|
16
|
+
.catch(() => ({ ok: false, body: {} }));
|
|
17
|
+
// A Discord bot's invite and portal page come with it (#1519 P3).
|
|
18
|
+
if (res.ok && res.body && res.body.identity) return { ...res.body.identity, invite: res.body.invite_url || null, portal: res.body.portal_url || null };
|
|
19
|
+
return { valid: false, reason: (res.body && res.body.error) || tn("verifyFailed") };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A bot's name as it is addressed: @name, once. */
|
|
23
|
+
export const botHandle = (username) => (username ? `@${String(username).replace(/^@/, "")}` : "");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The field. props: id, platform, value, onInput(token) (the parent drops its verified identity on a change and moves
|
|
27
|
+
* its credential revision, so a Verify still on its way for the old value cannot land — #1529 review),
|
|
28
|
+
* identity ({ valid, username, reason } | null), busy (the field is locked meanwhile), onVerify(), hint.
|
|
29
|
+
*/
|
|
30
|
+
export function TokenField({ id, platform, value, onInput, identity, busy, onVerify, hint }) {
|
|
31
|
+
const [shown, setShown] = useState(false);
|
|
32
|
+
return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
|
|
33
|
+
<div class="token-row">
|
|
34
|
+
<input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck="false" disabled=${!!busy}
|
|
35
|
+
placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
|
|
36
|
+
<button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
|
|
37
|
+
aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
|
|
38
|
+
</div>
|
|
39
|
+
<p class="note">${hint || (platform === "telegram" ? tn("wizardTokenTelegram") : tn("wizardTokenDiscord"))}</p>
|
|
40
|
+
<div class="dlg-inline-actions"><button type="button" class="btn btn-sm" disabled=${busy || !value} onClick=${onVerify}>${tn("wizardVerify")}</button>
|
|
41
|
+
${identity ? html`<span class=${`feedback${identity.valid ? " ok" : " error"}`} role="status">${identity.valid
|
|
42
|
+
? tn("tokenIsBot", botHandle(identity.username) || tn("verified")) : (identity.reason || tn("verifyFailed"))}</span>` : null}</div>
|
|
43
|
+
</div>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The generated env var a token is stored under, read-only with a copy button (the "Advanced" view). */
|
|
47
|
+
export function TokenEnvNote({ name }) {
|
|
48
|
+
const [copied, setCopied] = useState(false);
|
|
49
|
+
if (!name) return null;
|
|
50
|
+
const copy = async () => { try { await navigator.clipboard.writeText(name); setCopied(true); } catch { /* the name is on screen */ } };
|
|
51
|
+
return html`<div class="field"><span>${tn("tokenEnv")}</span>
|
|
52
|
+
<div class="dlg-inline-actions"><code>${name}</code><button type="button" class="btn btn-sm btn-ghost" onClick=${copy}>${copied ? tn("copied") : tn("copy")}</button></div>
|
|
53
|
+
<p class="note">${tn("tokenStoredAs")}</p></div>`;
|
|
54
|
+
}
|