zigmini 0.9.0-beta.2 → 1.0.0-beta.1
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.ko.md +35 -5
- package/README.md +35 -5
- package/assets/agent/gateway-rules.md +1 -1
- package/assets/android/zigmini.apk +0 -0
- package/assets/android/zigmini.apk.json +3 -3
- package/assets/build.json +1 -1
- package/docs/manual.ko.md +51 -11
- package/docs/manual.md +52 -12
- package/host/omo-bridge/agent-control.ts +274 -0
- package/host/omo-bridge/bridge.ts +1 -1
- package/host/omo-bridge/continuity-control.ts +1 -1
- package/host/omo-bridge/model-control.ts +15 -3
- package/host/omo-bridge/task-control.ts +26 -16
- package/host/omo-check/patches/omo-5.1.48-compaction-recovery.patch +912 -0
- package/host/omo-check/patches/omo-5.1.48-gateway-port.manifest.json +12 -11
- package/host/omo-check/patches/omo-5.1.51-compaction-long-context.patch +92 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-port.patch +2635 -0
- package/host/omo-check/patches/omo-5.1.51-compaction-recovery.patch +911 -0
- package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +363 -0
- package/host/omo-check/patches/omo-5.1.51-provider-failover.patch +50 -0
- package/host/omo-check/patches/omo-5.1.51-provider-port.patch +441 -0
- package/host/omo-check/patches/omo-5.1.51-rpc-port.patch +799 -0
- package/host/omo-check/patches/omo-5.1.51-runtime-compat.patch +31 -0
- package/package.json +3 -2
- package/src/agent/api.ts +215 -0
- package/src/agent/definition.ts +177 -0
- package/src/agent/fields.ts +42 -0
- package/src/agent/files.ts +192 -0
- package/src/agent/legacy-files.ts +46 -0
- package/src/agent/list.ts +21 -0
- package/src/agent-rules.ts +14 -5
- package/src/approvals.ts +6 -1
- package/src/cli/main.ts +33 -0
- package/src/cli/messages.ts +10 -2
- package/src/cli/rotate-main.ts +2 -1
- package/src/cli/scan.ts +414 -0
- package/src/cli/security-import.ts +155 -0
- package/src/compact.ts +22 -0
- package/src/decisions.ts +6 -4
- package/src/engine/compat.json +3 -3
- package/src/engine/index.ts +1 -0
- package/src/engine/schema-guard.ts +12 -4
- package/src/engine/sdk.ts +8 -0
- package/src/formal-verification.ts +366 -0
- package/src/gc.ts +37 -6
- package/src/hub.ts +525 -85
- package/src/main.ts +95 -9
- package/src/maintenance.ts +1 -1
- package/src/manual.ts +9 -2
- package/src/omo/client.ts +2 -0
- package/src/profile-prompt.ts +73 -0
- package/src/profile.ts +33 -10
- package/src/protocol.ts +11 -2
- package/src/question-notifications.ts +4 -1
- package/src/reply-notifications.ts +7 -1
- package/src/repo-security-setting.ts +33 -0
- package/src/runtime/paths.ts +16 -0
- package/src/security/baseline.ts +170 -0
- package/src/security/dex-strings.ts +242 -0
- package/src/security/repo-scan.ts +369 -0
- package/src/security/revision-probe.ts +144 -0
- package/src/security/terms.ts +97 -0
- package/src/security-allow.ts +1 -0
- package/src/security-baseline.ts +113 -0
- package/src/security-check.ts +91 -16
- package/src/server-text.ts +38 -0
- package/src/store.ts +142 -16
- package/src/transcript.ts +7 -1
- package/src/ui-language.ts +33 -0
- package/src/update/check.ts +7 -3
- package/src/update/handoff-system.ts +24 -16
- package/src/update/handoff.ts +23 -12
- package/src/update/helper.ts +72 -36
- package/src/update/request.ts +46 -30
- package/src/update/state.ts +21 -11
- package/src/update/strategy.ts +12 -5
- package/src/update/target.ts +32 -15
- package/src/usage.ts +43 -20
- package/src/version.ts +39 -16
- package/src/voice/agents.ts +8 -3
- package/src/voice/install.ts +29 -18
- package/src/work-watch.ts +40 -18
- package/web/agent-icon.js +15 -9
- package/web/agents.d.ts +201 -0
- package/web/agents.js +813 -0
- package/web/app-download.js +32 -19
- package/web/app.js +1407 -278
- package/web/autocopy.js +6 -1
- package/web/boot.js +27 -8
- package/web/commands.js +20 -11
- package/web/decisions.js +70 -49
- package/web/devlog.js +25 -19
- package/web/diagram.js +8 -3
- package/web/enroll.js +43 -22
- package/web/gc.js +32 -19
- package/web/i18n/agent-icon.js +24 -0
- package/web/i18n/agents.js +48 -0
- package/web/i18n/app-download.js +26 -0
- package/web/i18n/autocopy.js +5 -0
- package/web/i18n/boot.js +24 -0
- package/web/i18n/commands.js +34 -0
- package/web/i18n/decisions.js +24 -0
- package/web/i18n/devlog.js +60 -0
- package/web/i18n/diagram.js +12 -0
- package/web/i18n/enroll.js +22 -0
- package/web/i18n/gc.js +38 -0
- package/web/i18n/lastsent.js +10 -0
- package/web/i18n/main.d.ts +4 -0
- package/web/i18n/main.js +527 -0
- package/web/i18n/manual.d.ts +4 -0
- package/web/i18n/manual.js +14 -0
- package/web/i18n/memo.js +30 -0
- package/web/i18n/notifications.js +16 -0
- package/web/i18n/profile.js +13 -0
- package/web/i18n/reference.js +26 -0
- package/web/i18n/security-inbox.js +40 -0
- package/web/i18n/slash.js +38 -0
- package/web/i18n/turn-stats.js +5 -0
- package/web/i18n/update.js +42 -0
- package/web/i18n/voice.js +28 -0
- package/web/i18n/wake.js +16 -0
- package/web/i18n/workid.js +10 -0
- package/web/i18n/workstatus.js +20 -0
- package/web/i18n.d.ts +20 -0
- package/web/i18n.js +63 -0
- package/web/index.html +156 -96
- package/web/lastsent.js +9 -4
- package/web/manual.d.ts +1 -1
- package/web/manual.js +11 -5
- package/web/memo.js +18 -13
- package/web/notifications.js +12 -7
- package/web/profile.js +42 -17
- package/web/reference.js +20 -15
- package/web/security-inbox.d.ts +18 -1
- package/web/security-inbox.js +173 -4
- package/web/slash.js +23 -13
- package/web/style.css +132 -3
- package/web/tooltip.js +5 -2
- package/web/turn-stats.js +6 -2
- package/web/update.js +31 -23
- package/web/voice.js +18 -8
- package/web/wake.d.ts +4 -0
- package/web/wake.js +7 -3
- package/web/workid.js +12 -2
- package/web/workstatus.js +36 -32
package/web/autocopy.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import strings from "./i18n/autocopy.js";
|
|
2
|
+
import { registerStrings, t } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
registerStrings(strings);
|
|
5
|
+
|
|
1
6
|
const EDITABLE = "input, textarea, select, [contenteditable]:not([contenteditable='false'])";
|
|
2
7
|
|
|
3
8
|
function elementOf(node) {
|
|
@@ -50,7 +55,7 @@ export function setupAutoCopy(messages) {
|
|
|
50
55
|
document.body.append(toast);
|
|
51
56
|
let hideTimer = 0;
|
|
52
57
|
const show = (ok) => {
|
|
53
|
-
toast.textContent = ok ? "
|
|
58
|
+
toast.textContent = t(ok ? "autocopy.copied" : "autocopy.failed");
|
|
54
59
|
toast.classList.toggle("error", !ok);
|
|
55
60
|
toast.hidden = false;
|
|
56
61
|
clearTimeout(hideTimer);
|
package/web/boot.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import dict from "./i18n/boot.js";
|
|
2
|
+
import { applyI18n, isUiLanguage, registerStrings, setLanguage, t } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
registerStrings(dict);
|
|
5
|
+
|
|
1
6
|
const POLL_MS = 2000;
|
|
2
7
|
|
|
8
|
+
/** This device is not trusted yet, so the gateway may refuse the setting; the browser language is the fallback. */
|
|
9
|
+
async function pickLanguage() {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch("/api/settings/language", { cache: "no-store" });
|
|
12
|
+
const body = res.ok ? await res.json() : null;
|
|
13
|
+
if (isUiLanguage(body?.language)) return setLanguage(body.language);
|
|
14
|
+
} catch {
|
|
15
|
+
// Fall through to the browser language.
|
|
16
|
+
}
|
|
17
|
+
setLanguage(String(navigator.language).toLowerCase().startsWith("ko") ? "ko" : "en");
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
async function status() {
|
|
4
21
|
const res = await fetch("/api/device/status", { cache: "no-store" });
|
|
5
22
|
return res.json();
|
|
@@ -11,12 +28,13 @@ function screen() {
|
|
|
11
28
|
box.innerHTML = `
|
|
12
29
|
<div class="pairing-card">
|
|
13
30
|
<div class="pairing-logo">Z</div>
|
|
14
|
-
<h1
|
|
31
|
+
<h1 data-i18n="boot.title"></h1>
|
|
15
32
|
<p class="pairing-msg"></p>
|
|
16
33
|
<div class="pairing-code" hidden></div>
|
|
17
|
-
<button type="button" class="send pairing-btn" hidden
|
|
34
|
+
<button type="button" class="send pairing-btn" data-i18n="boot.request" hidden></button>
|
|
18
35
|
</div>`;
|
|
19
36
|
document.body.replaceChildren(box);
|
|
37
|
+
applyI18n(box);
|
|
20
38
|
return {
|
|
21
39
|
msg: box.querySelector(".pairing-msg"),
|
|
22
40
|
code: box.querySelector(".pairing-code"),
|
|
@@ -37,17 +55,17 @@ async function pair(first) {
|
|
|
37
55
|
const deadline = Date.now() + s.msLeft;
|
|
38
56
|
const tick = () => {
|
|
39
57
|
const left = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
|
|
40
|
-
ui.msg.textContent =
|
|
58
|
+
ui.msg.textContent = t("boot.pending", { seconds: left, name: s.name });
|
|
41
59
|
};
|
|
42
60
|
tick();
|
|
43
61
|
countdown = setInterval(tick, 1000);
|
|
44
62
|
ui.code.textContent = s.code;
|
|
45
63
|
ui.code.hidden = false;
|
|
46
64
|
} else if (s.locked) {
|
|
47
|
-
ui.msg.textContent = "
|
|
65
|
+
ui.msg.textContent = t("boot.locked");
|
|
48
66
|
} else {
|
|
49
|
-
const head = s.state === "denied" ? "
|
|
50
|
-
ui.msg.textContent =
|
|
67
|
+
const head = t(s.state === "denied" ? "boot.denied" : "boot.first");
|
|
68
|
+
ui.msg.textContent = t("boot.hint", { head, left: s.attemptsLeft });
|
|
51
69
|
ui.button.hidden = false;
|
|
52
70
|
}
|
|
53
71
|
};
|
|
@@ -56,11 +74,11 @@ async function pair(first) {
|
|
|
56
74
|
try {
|
|
57
75
|
const res = await fetch("/api/device/request", { method: "POST" });
|
|
58
76
|
const body = await res.json();
|
|
59
|
-
if (res.status === 429) throw new Error("
|
|
77
|
+
if (res.status === 429) throw new Error(t("boot.busy"));
|
|
60
78
|
if (!res.ok && res.status !== 423) throw new Error(body.error ?? String(res.status));
|
|
61
79
|
show(body);
|
|
62
80
|
} catch (err) {
|
|
63
|
-
ui.msg.textContent =
|
|
81
|
+
ui.msg.textContent = t("boot.failed", { reason: err instanceof Error ? err.message : err });
|
|
64
82
|
} finally {
|
|
65
83
|
ui.button.disabled = false;
|
|
66
84
|
}
|
|
@@ -69,6 +87,7 @@ async function pair(first) {
|
|
|
69
87
|
setInterval(() => status().then(show).catch(() => {}), POLL_MS);
|
|
70
88
|
}
|
|
71
89
|
|
|
90
|
+
await pickLanguage();
|
|
72
91
|
const first = await status().catch(() => ({ state: "none" }));
|
|
73
92
|
if (first.state === "trusted" || first.state === "approved") await import("./app.js");
|
|
74
93
|
else await pair(first);
|
package/web/commands.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import dict from "./i18n/commands.js";
|
|
2
|
+
|
|
3
|
+
const words = (key) => [...new Set([dict.en[key], dict.ko[key]].flatMap((value) => value.split(",")))];
|
|
4
|
+
const escaped = (list) => list.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
5
|
+
|
|
1
6
|
const TARGETS = [
|
|
2
|
-
{ option: "tts", names:
|
|
3
|
-
{ option: "wake", names:
|
|
4
|
-
{ option: "clap", names:
|
|
7
|
+
{ option: "tts", names: words("commands.tts.names") },
|
|
8
|
+
{ option: "wake", names: words("commands.wake.names") },
|
|
9
|
+
{ option: "clap", names: words("commands.clap.names") },
|
|
5
10
|
];
|
|
6
|
-
const ON = new Set(
|
|
7
|
-
const OFF = new Set(
|
|
8
|
-
const STATUS = new Set(
|
|
9
|
-
const READ_FULL = new Set(
|
|
10
|
-
const CLOSE_EARS = new Set(
|
|
11
|
+
const ON = new Set(words("commands.on"));
|
|
12
|
+
const OFF = new Set(words("commands.off"));
|
|
13
|
+
const STATUS = new Set(words("commands.status"));
|
|
14
|
+
const READ_FULL = new Set(words("commands.readFull"));
|
|
15
|
+
const CLOSE_EARS = new Set(words("commands.closeEars"));
|
|
16
|
+
const DEVICES = new Set(words("commands.devices"));
|
|
17
|
+
const DEVICE_DELETE = new RegExp(`^(?:${escaped(words("commands.devices.prefix"))})(?:${escaped(words("commands.devices.delete"))})(\\d+)$`);
|
|
18
|
+
const MODE = new RegExp(`^(?:${escaped(words("commands.mode"))})`);
|
|
19
|
+
const PARTICLE = new RegExp(`^(?:${escaped(words("commands.particle"))})`);
|
|
11
20
|
// Improvement 71: the gateway voice setting (the slash menu offers /voice on and /voice off).
|
|
12
21
|
const VOICE_NAMES = ["/voice", "voice"];
|
|
13
22
|
|
|
@@ -16,13 +25,13 @@ export function parseOptionCommand(text) {
|
|
|
16
25
|
if (STATUS.has(s)) return { kind: "status" };
|
|
17
26
|
if (READ_FULL.has(s)) return { kind: "readFull" };
|
|
18
27
|
if (CLOSE_EARS.has(s)) return { kind: "set", option: "wake", value: false };
|
|
19
|
-
if (s
|
|
20
|
-
const del =
|
|
28
|
+
if (DEVICES.has(s)) return { kind: "devices" };
|
|
29
|
+
const del = DEVICE_DELETE.exec(s);
|
|
21
30
|
if (del) return { kind: "deviceDelete", index: Number(del[1]) };
|
|
22
31
|
for (const { option, names } of TARGETS) {
|
|
23
32
|
const name = names.find((n) => s.startsWith(n));
|
|
24
33
|
if (!name) continue;
|
|
25
|
-
const rest = s.slice(name.length).replace(
|
|
34
|
+
const rest = s.slice(name.length).replace(MODE, "").replace(PARTICLE, "");
|
|
26
35
|
if (ON.has(rest)) return { kind: "set", option, value: true };
|
|
27
36
|
if (OFF.has(rest)) return { kind: "set", option, value: false };
|
|
28
37
|
}
|
package/web/decisions.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// 확인함(confirmation inbox) state, presentation rules and request builders.
|
|
2
2
|
// Pure: no DOM, no fetch. Rendering and transport live in app.js.
|
|
3
3
|
// allow: SIZE_OK — one pure reducer owns the indivisible canonical decision and answer-request state machine.
|
|
4
|
+
import strings from "./i18n/decisions.js";
|
|
5
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
6
|
+
|
|
7
|
+
registerStrings(strings);
|
|
4
8
|
|
|
5
9
|
const UNRESOLVED = new Set(["pending", "submitted", "needs_review"]);
|
|
6
10
|
const TERMINAL_WORK = new Set(["completed", "failed", "cancelled"]);
|
|
@@ -366,13 +370,17 @@ export function canDismiss(decision) {
|
|
|
366
370
|
return isUnresolved(decision) && decision.status !== "submitted";
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
export
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
export let DISMISS_TEXT = dismissText();
|
|
374
|
+
|
|
375
|
+
function dismissText() {
|
|
376
|
+
return {
|
|
377
|
+
hide: { prompt: t("decisions.dismiss.hide.prompt"), confirm: t("decisions.dismiss.hide.confirm") },
|
|
378
|
+
cancel: {
|
|
379
|
+
prompt: t("decisions.dismiss.cancel.prompt"),
|
|
380
|
+
confirm: t("decisions.dismiss.cancel.confirm"),
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
376
384
|
|
|
377
385
|
function record(value) {
|
|
378
386
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -380,7 +388,7 @@ function record(value) {
|
|
|
380
388
|
|
|
381
389
|
/** One question's line in the inbox: its header, text and the multi-select / optional hints. */
|
|
382
390
|
export function questionTitle(q) {
|
|
383
|
-
return `${q.header ? `[${q.header}] ` : ""}${typeof q.question === "string" ? q.question : ""}${q.multiSelect === true ? "
|
|
391
|
+
return `${q.header ? `[${q.header}] ` : ""}${typeof q.question === "string" ? q.question : ""}${q.multiSelect === true ? t("decisions.question.multi") : ""}${q.required === false ? t("decisions.question.optional") : ""}`;
|
|
384
392
|
}
|
|
385
393
|
|
|
386
394
|
/**
|
|
@@ -399,7 +407,7 @@ export function originalChoices(decision) {
|
|
|
399
407
|
return choices.length ? [{ title: "", options: choices }] : [];
|
|
400
408
|
}
|
|
401
409
|
case "confirm":
|
|
402
|
-
return [{ title: "", options: [{ label: "
|
|
410
|
+
return [{ title: "", options: [{ label: t("decisions.confirm.yes"), description: "" }, { label: t("decisions.confirm.no"), description: "" }] }];
|
|
403
411
|
case "question":
|
|
404
412
|
return (Array.isArray(payload.questions) ? payload.questions : []).filter(record)
|
|
405
413
|
.map((q) => ({ title: questionTitle(q), options: options(q.options) }));
|
|
@@ -477,7 +485,7 @@ export function interpretAnswerResponse(status, body) {
|
|
|
477
485
|
if (status === 404) return { kind: "gone", error };
|
|
478
486
|
if (status === 400) return { kind: "rejected", decision, error };
|
|
479
487
|
if (status === 422 && error === "security_check_blocked") {
|
|
480
|
-
return { kind: "blocked", error, held: record(body.held) ? body.held : null, message: typeof body.message === "string" ? body.message : "
|
|
488
|
+
return { kind: "blocked", error, held: record(body.held) ? body.held : null, message: typeof body.message === "string" ? body.message : t("decisions.blocked") };
|
|
481
489
|
}
|
|
482
490
|
return { kind: "unavailable", status, error };
|
|
483
491
|
}
|
|
@@ -518,39 +526,52 @@ export function speechTextForTurn(turn, lastText, textAt) {
|
|
|
518
526
|
return lastText ?? null;
|
|
519
527
|
}
|
|
520
528
|
|
|
521
|
-
export
|
|
522
|
-
export
|
|
529
|
+
export let STATUS_LABELS = statusLabels();
|
|
530
|
+
export let BINDING_LABELS = bindingLabels();
|
|
531
|
+
|
|
532
|
+
function statusLabels() {
|
|
533
|
+
return { pending: t("decisions.status.pending"), needs_review: t("decisions.status.pending"), submitted: t("decisions.status.submitted") };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function bindingLabels() {
|
|
537
|
+
return { live: t("decisions.binding.live"), expired: t("decisions.binding.expired"), closed: t("decisions.binding.closed"), unknown: t("decisions.binding.unknown") };
|
|
538
|
+
}
|
|
523
539
|
|
|
524
540
|
/** A closed question that still takes an answer (reported again, or kept open so main does not pause) waits for it. */
|
|
525
541
|
export function bindingLabel(decision) {
|
|
526
|
-
if (decision.bindingState === "closed" && (decision.reason === "report_only" || decision.reason === "main_released")) return "
|
|
542
|
+
if (decision.bindingState === "closed" && (decision.reason === "report_only" || decision.reason === "main_released")) return t("decisions.binding.answer");
|
|
527
543
|
return BINDING_LABELS[decision.bindingState] ?? decision.bindingState;
|
|
528
544
|
}
|
|
529
|
-
export
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
545
|
+
export let WORK_STATUS_LABELS = workStatusLabels();
|
|
546
|
+
export let ROUTE_LABELS = routeLabels();
|
|
547
|
+
|
|
548
|
+
function workStatusLabels() {
|
|
549
|
+
return {
|
|
550
|
+
submitted: t("decisions.work.submitted"), running: t("decisions.work.running"), waiting: t("decisions.work.waiting"), waiting_input: t("decisions.work.waiting_input"),
|
|
551
|
+
completed: t("decisions.work.completed"), failed: t("decisions.work.failed"), cancelled: t("decisions.work.cancelled"), unknown: t("decisions.work.unknown"),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function routeLabels() {
|
|
556
|
+
return { live_response: t("decisions.route.live_response"), follow_up_command: t("decisions.route.follow_up_command") };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// The exported texts follow the UI language (app.js reads them at render time).
|
|
560
|
+
onLanguageChange(() => {
|
|
561
|
+
DISMISS_TEXT = dismissText();
|
|
562
|
+
STATUS_LABELS = statusLabels();
|
|
563
|
+
BINDING_LABELS = bindingLabels();
|
|
564
|
+
WORK_STATUS_LABELS = workStatusLabels();
|
|
565
|
+
ROUTE_LABELS = routeLabels();
|
|
566
|
+
});
|
|
534
567
|
const REASON_LABELS = {
|
|
535
|
-
receipt_unknown: "
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
owner_disconnected: "담당 세션 연결이 끊겼습니다",
|
|
543
|
-
binding_unknown: "원래 요청 상태를 알 수 없습니다",
|
|
544
|
-
owner_unknown: "현재 담당 세션을 확인할 수 없습니다",
|
|
545
|
-
parked: "담당 세션이 잠시 멈춰 있습니다",
|
|
546
|
-
command_pending: "후속 지시를 보내는 중입니다",
|
|
547
|
-
timeout: "기한이 지났습니다",
|
|
548
|
-
report_only: "현재 담당이 다시 낸 질문입니다",
|
|
549
|
-
main_released: "답을 기다리는 질문입니다",
|
|
550
|
-
instructions_changed: "질문 이후 새 지시가 추가됐습니다",
|
|
551
|
-
context_changed: "질문 이후 새 지시가 추가됐습니다",
|
|
552
|
-
recheck_requested: "현재 상황 확인을 요청했습니다",
|
|
553
|
-
request_permission_requested: "새 승인 요청을 보냈습니다",
|
|
568
|
+
receipt_unknown: "decisions.reason.receipt_unknown", user_deferred: "decisions.reason.user_deferred", source_changed: "decisions.reason.source_changed",
|
|
569
|
+
recheck_incomplete: "decisions.reason.recheck_incomplete", engine_wait_closed: "decisions.reason.engine_wait_closed", engine_wait_cancelled: "decisions.reason.engine_wait_cancelled",
|
|
570
|
+
request_missing: "decisions.reason.request_missing", owner_disconnected: "decisions.reason.owner_disconnected", binding_unknown: "decisions.reason.binding_unknown",
|
|
571
|
+
owner_unknown: "decisions.reason.owner_unknown", parked: "decisions.reason.parked", command_pending: "decisions.reason.command_pending",
|
|
572
|
+
timeout: "decisions.reason.timeout", report_only: "decisions.reason.report_only", main_released: "decisions.reason.main_released",
|
|
573
|
+
instructions_changed: "decisions.reason.instructions_changed", context_changed: "decisions.reason.context_changed",
|
|
574
|
+
recheck_requested: "decisions.reason.recheck_requested", request_permission_requested: "decisions.reason.request_permission_requested",
|
|
554
575
|
};
|
|
555
576
|
|
|
556
577
|
/** Why the item still waits, in the user's words; unmapped server codes are shown as-is. */
|
|
@@ -558,30 +579,30 @@ export function waitReason(decision) {
|
|
|
558
579
|
const parts = [];
|
|
559
580
|
const work = decision.work;
|
|
560
581
|
if (work && TERMINAL_WORK.has(work.status)) {
|
|
561
|
-
parts.push(
|
|
582
|
+
parts.push(t("decisions.wait.work_ended", { status: WORK_STATUS_LABELS[work.status] ?? work.status }));
|
|
562
583
|
return parts.join(" ");
|
|
563
584
|
}
|
|
564
585
|
if (decision.status === "submitted") {
|
|
565
|
-
if (decision.reason?.endsWith("_requested")) parts.push(
|
|
586
|
+
if (decision.reason?.endsWith("_requested")) parts.push(t("decisions.wait.requested", { reason: t(REASON_LABELS[decision.reason] ?? decision.reason) }));
|
|
566
587
|
else {
|
|
567
|
-
parts.push("
|
|
568
|
-
if (decision.answerRoute) parts.push(`${ROUTE_LABELS[decision.answerRoute]}.`);
|
|
588
|
+
parts.push(t("decisions.wait.answer_sending"));
|
|
589
|
+
if (decision.answerRoute) parts.push(`${ROUTE_LABELS[decision.answerRoute] ?? decision.answerRoute}.`);
|
|
569
590
|
}
|
|
570
591
|
} else if (decision.status === "needs_review") {
|
|
571
|
-
parts.push(
|
|
592
|
+
parts.push(t("decisions.wait.recheck", { reason: t(REASON_LABELS[decision.reason ?? ""] ?? decision.reason ?? "decisions.wait.recheck") }));
|
|
572
593
|
} else if (decision.bindingState === "live") {
|
|
573
|
-
parts.push("
|
|
594
|
+
parts.push(t("decisions.wait.live"));
|
|
574
595
|
} else if (decision.bindingState === "expired") {
|
|
575
596
|
parts.push(decision.permission
|
|
576
|
-
? "
|
|
577
|
-
: "
|
|
597
|
+
? t("decisions.wait.permission_expired")
|
|
598
|
+
: t("decisions.wait.expired"));
|
|
578
599
|
} else if (decision.bindingState === "closed" && decision.reason === "report_only") {
|
|
579
|
-
parts.push("
|
|
600
|
+
parts.push(t("decisions.wait.report_only"));
|
|
580
601
|
} else if (decision.bindingState === "closed" && decision.reason === "main_released") {
|
|
581
|
-
parts.push("
|
|
602
|
+
parts.push(t("decisions.wait.main_released"));
|
|
582
603
|
} else {
|
|
583
|
-
parts.push(
|
|
604
|
+
parts.push(t("decisions.wait.unknown", { reason: t(REASON_LABELS[decision.reason ?? ""] ?? decision.reason ?? "decisions.wait.unknown") }));
|
|
584
605
|
}
|
|
585
|
-
if (work) parts.push(
|
|
606
|
+
if (work) parts.push(t("decisions.wait.work_status", { status: WORK_STATUS_LABELS[work.status] ?? work.status, reason: work.statusReason ? ` (${work.statusReason})` : "" }));
|
|
586
607
|
return parts.join(" ");
|
|
587
608
|
}
|
package/web/devlog.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { registerStrings, t } from "./i18n.js";
|
|
2
|
+
import strings from "./i18n/devlog.js";
|
|
3
|
+
|
|
4
|
+
registerStrings(strings);
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
* The chat line for a main turn end (turn.completed), or null. The
|
|
7
|
+
* The chat line for a main turn end (turn.completed), or null. The work status drawing already shows whether Zig is working,
|
|
3
8
|
* so a normal end appears only in this event log. An abnormal end ("interrupted") stays in the chat: a work report turn
|
|
4
9
|
* interrupted on reattach has no other notice.
|
|
5
10
|
*/
|
|
6
11
|
export function chatTurnEndLine(data) {
|
|
7
12
|
if (data.status === "completed") return null;
|
|
8
|
-
return
|
|
13
|
+
return t("devlog.turnEnd", { status: data.status, error: data.error ? ` (${JSON.stringify(data.error)})` : "" });
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
const CATEGORY = {
|
|
@@ -60,50 +65,50 @@ function summary(type, d = {}) {
|
|
|
60
65
|
return clip(d.title || d.method);
|
|
61
66
|
case "action.resolved": {
|
|
62
67
|
const a = d.answer ?? {};
|
|
63
|
-
const v = a.kind === "value" ? a.value : a.kind === "confirm" ? (a.confirmed ? "
|
|
68
|
+
const v = a.kind === "value" ? a.value : a.kind === "confirm" ? (a.confirmed ? t("devlog.yes") : t("devlog.no")) : t("devlog.cancel");
|
|
64
69
|
return clip(`${v} (${d.by})${a.reason ? ` · ${a.reason}` : ""}`);
|
|
65
70
|
}
|
|
66
71
|
case "turn.started":
|
|
67
|
-
return "
|
|
72
|
+
return t("devlog.turnStarted");
|
|
68
73
|
case "turn.completed":
|
|
69
|
-
return clip(
|
|
74
|
+
return clip(t("devlog.turnCompleted", { status: d.status }));
|
|
70
75
|
case "error":
|
|
71
76
|
case "notice":
|
|
72
77
|
return clip(d.message);
|
|
73
78
|
case "omo.status":
|
|
74
|
-
return clip(
|
|
79
|
+
return clip(t("devlog.engineStatus", { status: d.status, detail: d.detail ? ` · ${d.detail}` : "" }));
|
|
75
80
|
case "state.snapshot":
|
|
76
|
-
return
|
|
81
|
+
return t("devlog.snapshot", { conversations: d.conversations?.length ?? 0, approvals: d.approvals?.length ?? 0, engine: d.omo });
|
|
77
82
|
case "history":
|
|
78
|
-
return
|
|
83
|
+
return t("devlog.history", { conversation: d.conv, count: d.events?.length ?? 0 });
|
|
79
84
|
case "conversation.created":
|
|
80
|
-
return clip(
|
|
85
|
+
return clip(t("devlog.newConversation", { title: d.title }));
|
|
81
86
|
case "conversation.thread":
|
|
82
87
|
return clip(d.threadId);
|
|
83
88
|
case "profile.updated":
|
|
84
|
-
return clip(
|
|
89
|
+
return clip(t("devlog.profile", { nickname: d.nickname ?? "", agent: d.agentName ? ` · ${t("devlog.agent")} ${d.agentName}` : "" }));
|
|
85
90
|
case "voice.updated":
|
|
86
|
-
return d.enabled === false ? "
|
|
91
|
+
return d.enabled === false ? t("devlog.voiceOff") : t("devlog.voiceOn");
|
|
87
92
|
case "voice.install.progress":
|
|
88
|
-
return clip(
|
|
93
|
+
return clip(t("devlog.voiceInstall", { state: d.state, index: d.index, total: d.total, message: d.message ?? "" }));
|
|
89
94
|
case "version.available":
|
|
90
|
-
return clip(
|
|
95
|
+
return clip(t("devlog.versionAvailable", { latest: d.latest ?? "?", current: d.current ?? "?" }));
|
|
91
96
|
case "update.progress":
|
|
92
|
-
return clip(
|
|
97
|
+
return clip(t("devlog.updateProgress", { from: d.from ?? "?", to: d.to ?? "?", label: d.label ?? d.phase ?? "" }));
|
|
93
98
|
case "update.result":
|
|
94
|
-
return clip(
|
|
99
|
+
return clip(t("devlog.updateResult", { result: d.ok ? t("devlog.success") : d.rolledBack ? t("devlog.rollback") : t("devlog.failure"), from: d.from ?? "?", to: d.to ?? "?" }));
|
|
95
100
|
case "memo.updated":
|
|
96
|
-
return
|
|
101
|
+
return t("devlog.memo", { count: d.text?.length ?? 0, size: d.width ? ` · ${d.width}×${d.height}` : "" });
|
|
97
102
|
case "wake.heard":
|
|
98
|
-
return clip(
|
|
103
|
+
return clip(t("devlog.wakeHeard", { text: d.text ?? "", armed: d.armed ? ` · ${t("devlog.armed")}` : "", speaking: d.duringSpeech ? ` · ${t("devlog.speaking")}` : "" }));
|
|
99
104
|
case "clap.double":
|
|
100
|
-
return d.wakeWasOn ? "
|
|
105
|
+
return d.wakeWasOn ? t("devlog.clapWakeOff") : t("devlog.clapWakeOn");
|
|
101
106
|
case "tts.summary":
|
|
102
107
|
return clip(d.summary);
|
|
103
108
|
case "input.text":
|
|
104
109
|
return clip(d.text);
|
|
105
110
|
case "input.action":
|
|
106
|
-
return clip(`${d.actionId} → ${d.value ?? (d.confirmed !== undefined ? d.confirmed : "
|
|
111
|
+
return clip(`${d.actionId} → ${d.value ?? (d.confirmed !== undefined ? d.confirmed : t("devlog.cancel"))}`);
|
|
107
112
|
case "hello":
|
|
108
113
|
return clip(`${d.client} ${d.version}`);
|
|
109
114
|
default:
|
|
@@ -124,3 +129,4 @@ export function describeEvent(dir, obj) {
|
|
|
124
129
|
mergeKey: streaming ? `${type}:${data.itemId}` : null,
|
|
125
130
|
};
|
|
126
131
|
}
|
|
132
|
+
|
package/web/diagram.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import dict from "./i18n/diagram.js";
|
|
2
|
+
import { registerStrings, t } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
registerStrings(dict);
|
|
5
|
+
|
|
1
6
|
// Diagram input is data only. Markdown recognition stays at the actual lexer boundary.
|
|
2
7
|
const BODY_LIMIT = 16_384;
|
|
3
8
|
const ID = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
@@ -128,12 +133,12 @@ function element(doc, tag, className, text) {
|
|
|
128
133
|
|
|
129
134
|
export function createDiagram(doc, diagram, source) {
|
|
130
135
|
const figure = element(doc, "figure", "chat-diagram");
|
|
131
|
-
const title = diagram.title ?? "
|
|
136
|
+
const title = diagram.title ?? t("diagram.title.default");
|
|
132
137
|
figure.append(element(doc, "figcaption", "diagram-title", title));
|
|
133
138
|
const viewport = element(doc, "div", "diagram-viewport");
|
|
134
139
|
viewport.tabIndex = 0;
|
|
135
140
|
viewport.setAttribute("role", "region");
|
|
136
|
-
viewport.setAttribute("aria-label",
|
|
141
|
+
viewport.setAttribute("aria-label", t("diagram.region", { title }));
|
|
137
142
|
const stage = element(doc, "div", "diagram-stage");
|
|
138
143
|
const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
139
144
|
svg.setAttribute("class", "diagram-edges");
|
|
@@ -160,7 +165,7 @@ export function createDiagram(doc, diagram, source) {
|
|
|
160
165
|
relationships.append(element(doc, "li", "", `${labels.get(edge.from)} → ${labels.get(edge.to)}`));
|
|
161
166
|
}
|
|
162
167
|
const details = element(doc, "details", "diagram-source");
|
|
163
|
-
details.append(element(doc, "summary", "", "
|
|
168
|
+
details.append(element(doc, "summary", "", t("diagram.source")));
|
|
164
169
|
const pre = element(doc, "pre");
|
|
165
170
|
pre.append(element(doc, "code", "", source));
|
|
166
171
|
details.append(pre);
|
package/web/enroll.js
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
// Plan Todo 15 (improvement 89): the 기기 등록 area of the web 설정 panel. Only decisions live here, with no DOM, so
|
|
2
2
|
// test/unit/enroll-web.test.ts and the mutation run drive them directly; app.js paints the view it gets. The token
|
|
3
3
|
// exists only inside the QR payload: nothing here turns it into text for the screen or the console.
|
|
4
|
+
import strings from "./i18n/enroll.js";
|
|
5
|
+
import { onLanguageChange, registerStrings, t } from "./i18n.js";
|
|
6
|
+
|
|
7
|
+
registerStrings(strings);
|
|
4
8
|
|
|
5
9
|
export const ENROLL_TOKENS_PATH = "/api/enroll/tokens";
|
|
6
10
|
export const DEVICES_PATH = "/api/devices";
|
|
7
11
|
export const PAYLOAD_PREFIX = "zigmini://enroll?";
|
|
8
12
|
export const QR_MARGIN = 4;
|
|
9
13
|
export const REVOKE_ARM_MS = 5000;
|
|
10
|
-
export
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
export
|
|
15
|
-
export
|
|
16
|
-
export
|
|
17
|
-
export
|
|
14
|
+
export let RENEW_LABEL = t("enroll.button.renew");
|
|
15
|
+
export let REVOKE_LABEL = t("enroll.button.revoke");
|
|
16
|
+
export let REVOKE_ARMED_LABEL = t("enroll.button.revoke.armed");
|
|
17
|
+
export let HINT_LINE = t("enroll.hint");
|
|
18
|
+
export let LOADING_LINE = t("enroll.loading");
|
|
19
|
+
export let EXPIRED_LINE = t("enroll.expired");
|
|
20
|
+
export let NO_DEVICES_LINE = t("enroll.devices.empty");
|
|
21
|
+
export let NO_CONTACT_LINE = t("enroll.contact.none");
|
|
18
22
|
export const ERROR_LINES = Object.freeze({
|
|
19
|
-
access_url_unknown: "
|
|
20
|
-
qr_unavailable: "
|
|
21
|
-
network: "
|
|
23
|
+
access_url_unknown: "enroll.error.access_url_unknown",
|
|
24
|
+
qr_unavailable: "enroll.error.qr_unavailable",
|
|
25
|
+
network: "enroll.error.network",
|
|
26
|
+
});
|
|
27
|
+
export let DEVICE_STATUS = deviceStatus();
|
|
28
|
+
|
|
29
|
+
function deviceStatus() {
|
|
30
|
+
return Object.freeze({ approved: t("enroll.device.approved"), pending: t("enroll.device.pending"), denied: t("enroll.device.denied") });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The exported texts follow the UI language (app.js reads them at render time).
|
|
34
|
+
onLanguageChange(() => {
|
|
35
|
+
RENEW_LABEL = t("enroll.button.renew");
|
|
36
|
+
REVOKE_LABEL = t("enroll.button.revoke");
|
|
37
|
+
REVOKE_ARMED_LABEL = t("enroll.button.revoke.armed");
|
|
38
|
+
HINT_LINE = t("enroll.hint");
|
|
39
|
+
LOADING_LINE = t("enroll.loading");
|
|
40
|
+
EXPIRED_LINE = t("enroll.expired");
|
|
41
|
+
NO_DEVICES_LINE = t("enroll.devices.empty");
|
|
42
|
+
NO_CONTACT_LINE = t("enroll.contact.none");
|
|
43
|
+
DEVICE_STATUS = deviceStatus();
|
|
22
44
|
});
|
|
23
|
-
export const DEVICE_STATUS = Object.freeze({ approved: "승인됨", pending: "승인 대기", denied: "거절됨" });
|
|
24
45
|
|
|
25
46
|
/** A token response as the screen needs it: the payload and the expiry as a time number. Null for any other shape. */
|
|
26
47
|
export function parseEnrollment(body) {
|
|
@@ -42,7 +63,7 @@ export function formatLeft(seconds) {
|
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
export function errorLine(error) {
|
|
45
|
-
return ERROR_LINES[error] ??
|
|
66
|
+
return t(ERROR_LINES[error] ?? "enroll.error.generic", { error });
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
/**
|
|
@@ -50,12 +71,12 @@ export function errorLine(error) {
|
|
|
50
71
|
* 새로 만들기 button. The QR and the button never show together, and an expired or failed token leaves only the button.
|
|
51
72
|
*/
|
|
52
73
|
export function enrollView(state, now) {
|
|
53
|
-
if (state.status === "loading") return { qr: false, countdown: null, renew: false, expired: false, error: false, message:
|
|
74
|
+
if (state.status === "loading") return { qr: false, countdown: null, renew: false, expired: false, error: false, message: t("enroll.loading") };
|
|
54
75
|
if (state.status === "error") return { qr: false, countdown: null, renew: true, expired: false, error: true, message: errorLine(state.error) };
|
|
55
|
-
if (!state.enrollment) return { qr: false, countdown: null, renew: true, expired: false, error: false, message:
|
|
76
|
+
if (!state.enrollment) return { qr: false, countdown: null, renew: true, expired: false, error: false, message: t("enroll.hint") };
|
|
56
77
|
const left = secondsLeft(state.enrollment.expiresAt, now);
|
|
57
|
-
if (left === 0) return { qr: false, countdown: null, renew: true, expired: true, error: false, message:
|
|
58
|
-
return { qr: true, countdown:
|
|
78
|
+
if (left === 0) return { qr: false, countdown: null, renew: true, expired: true, error: false, message: t("enroll.expired") };
|
|
79
|
+
return { qr: true, countdown: t("enroll.countdown", { time: formatLeft(left) }), renew: false, expired: false, error: false, message: t("enroll.hint") };
|
|
59
80
|
}
|
|
60
81
|
|
|
61
82
|
/** The payload as a scalable SVG from the vendored qrcode-generator: black modules on white with a quiet zone. */
|
|
@@ -78,10 +99,10 @@ export function lastSeenLabel(iso, now) {
|
|
|
78
99
|
const at = typeof iso === "string" ? Date.parse(iso) : NaN;
|
|
79
100
|
if (!Number.isFinite(at)) return NO_CONTACT_LINE;
|
|
80
101
|
const seconds = Math.floor((now - at) / 1000);
|
|
81
|
-
if (seconds < 60) return "
|
|
82
|
-
if (seconds < 3600) return
|
|
83
|
-
if (seconds < 86400) return
|
|
84
|
-
return
|
|
102
|
+
if (seconds < 60) return t("enroll.contact.now");
|
|
103
|
+
if (seconds < 3600) return t("enroll.contact.minutes", { count: Math.floor(seconds / 60) });
|
|
104
|
+
if (seconds < 86400) return t("enroll.contact.hours", { count: Math.floor(seconds / 3600) });
|
|
105
|
+
return t("enroll.contact.days", { count: Math.floor(seconds / 86400) });
|
|
85
106
|
}
|
|
86
107
|
|
|
87
108
|
function contactTime(device) {
|
|
@@ -100,7 +121,7 @@ export function deviceRows(devices, now) {
|
|
|
100
121
|
.map((device) => ({
|
|
101
122
|
id: device.id,
|
|
102
123
|
name: device.name,
|
|
103
|
-
model: device.model ?? "
|
|
124
|
+
model: device.model ?? t("enroll.device.model.unknown"),
|
|
104
125
|
lastSeen: lastSeenLabel(device.lastSeen, now),
|
|
105
126
|
status: DEVICE_STATUS[device.status] ?? device.status,
|
|
106
127
|
approved: device.status === "approved",
|