@kevin5251984/guild 0.2.21 → 0.2.23

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.
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
9
9
  <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
10
10
  <title>Guild — 大廳</title>
11
- <link rel="stylesheet" href="/chat.css?v=quest-nest" />
11
+ <link rel="stylesheet" href="/chat.css?v=nav-fold" />
12
12
  <script src="/i18n.js"></script>
13
13
  <script src="/md.js"></script>
14
14
  </head>
@@ -18,17 +18,38 @@
18
18
  <div class="side-top">
19
19
  <input class="search" id="filter" placeholder="搜尋" data-i18n-placeholder="search" />
20
20
  </div>
21
- <div class="section-h">
22
- <span data-i18n="channels">委託</span>
23
- <button type="button" class="add-ch" id="toggle-create" title="新委託" data-i18n-title="newChannel">+</button>
21
+ <div class="nav-sec" data-nav-sec="channels">
22
+ <div class="section-h">
23
+ <button type="button" class="sec-fold" data-fold="channels" aria-expanded="true" aria-controls="channels">
24
+ <span class="sec-chev" aria-hidden="true">▾</span>
25
+ <span data-i18n="channels">委託</span>
26
+ </button>
27
+ <button type="button" class="add-ch" id="toggle-create" title="新委託" data-i18n-title="newChannel">+</button>
28
+ </div>
29
+ <form class="create-row" id="create-row">
30
+ <input id="new-channel" placeholder="委託名稱" data-i18n-placeholder="newChannelName" />
31
+ <button type="submit" data-i18n="create">建立</button>
32
+ </form>
33
+ <ul class="navlist" id="channels"></ul>
34
+ </div>
35
+ <div class="nav-sec" data-nav-sec="dms">
36
+ <div class="section-h">
37
+ <button type="button" class="sec-fold" data-fold="dms" aria-expanded="true" aria-controls="dms">
38
+ <span class="sec-chev" aria-hidden="true">▾</span>
39
+ <span data-i18n="dms">密談</span>
40
+ </button>
41
+ </div>
42
+ <ul class="navlist" id="dms"></ul>
43
+ </div>
44
+ <div class="nav-sec" data-nav-sec="cron">
45
+ <div class="section-h">
46
+ <button type="button" class="sec-fold" data-fold="cron" aria-expanded="true" aria-controls="cron-nav">
47
+ <span class="sec-chev" aria-hidden="true">▾</span>
48
+ <span data-i18n="cron.title">排程</span>
49
+ </button>
50
+ </div>
51
+ <ul class="navlist" id="cron-nav"></ul>
24
52
  </div>
25
- <form class="create-row" id="create-row">
26
- <input id="new-channel" placeholder="委託名稱" data-i18n-placeholder="newChannelName" />
27
- <button type="submit" data-i18n="create">建立</button>
28
- </form>
29
- <ul class="navlist" id="channels"></ul>
30
- <div class="section-h" data-i18n="dms">密談</div>
31
- <ul class="navlist" id="dms"></ul>
32
53
  <div class="side-foot">
33
54
  <a href="/studio" data-i18n="nav.studio">編制</a>
34
55
  <a href="/library" data-i18n="nav.library">工坊</a>
@@ -116,6 +137,7 @@
116
137
  <button type="button" class="attach-item" data-attach="rules"><span class="attach-ico">☰</span><span data-i18n="attach.rules">規則</span></button>
117
138
  <button type="button" class="attach-item" data-attach="term"><span class="attach-ico">&gt;_</span><span data-i18n="attach.term">終端</span></button>
118
139
  <button type="button" class="attach-item" data-attach="tree"><span class="attach-ico">🗺</span><span data-i18n="attach.tree">工作區</span></button>
140
+ <button type="button" class="attach-item" data-attach="cron"><span class="attach-ico">⏱</span><span data-i18n="attach.cron">排程</span></button>
119
141
  <button type="button" class="attach-item" data-attach="slash"><span class="attach-ico">/</span><span data-i18n="attach.slash">指令</span></button>
120
142
  </div>
121
143
  </div>
@@ -230,6 +252,19 @@
230
252
  <button type="button" id="channel-md-save" data-i18n="save">儲存</button>
231
253
  </div>
232
254
  </dialog>
255
+ <dialog id="cron-dialog">
256
+ <h2 data-i18n="cron.title">排程</h2>
257
+ <p data-i18n="cron.help">新增會請這席呼叫 cronjob。時間用自然語言寫即可,例如「每10分鐘跟我說一次 Hi」。guildd 每 60 秒看一次;到點後這席開一輪新的 @handle,prompt 必須自包含。</p>
258
+ <div id="cron-list" class="cron-list"></div>
259
+ <label for="cron-bot" data-i18n="cron.seat">席</label>
260
+ <select id="cron-bot"></select>
261
+ <label for="cron-ask" data-i18n="cron.ask">要排什麼</label>
262
+ <textarea id="cron-ask" rows="4" placeholder="每10分鐘跟我說一次 Hi" data-i18n-placeholder="cron.askPh"></textarea>
263
+ <div class="dialog-actions">
264
+ <button type="button" class="ghost" id="cron-cancel" data-i18n="cancel">取消</button>
265
+ <button type="button" id="cron-add" data-i18n="cron.add">請他排</button>
266
+ </div>
267
+ </dialog>
233
268
  <dialog id="bot-memory">
234
269
  <h2 data-i18n="memory.bot">MEMORY.md</h2>
235
270
  <p data-i18n="memory.botHelp">這名冒險者學會的站立筆記。有用的回合後會自動更新。</p>
@@ -320,6 +355,19 @@
320
355
  return new Set();
321
356
  }
322
357
  })(),
358
+ cronJobs: [],
359
+ navFold: (function () {
360
+ try {
361
+ const raw = JSON.parse(localStorage.getItem("guild-nav-fold") || "{}");
362
+ return {
363
+ channels: Boolean(raw.channels),
364
+ dms: Boolean(raw.dms),
365
+ cron: Boolean(raw.cron),
366
+ };
367
+ } catch {
368
+ return { channels: false, dms: false, cron: false };
369
+ }
370
+ })(),
323
371
  };
324
372
 
325
373
  function hue(key) {
@@ -1422,6 +1470,65 @@
1422
1470
  const nb = Number.isFinite(tb) ? tb : 0;
1423
1471
  return nb - na;
1424
1472
  }
1473
+ function navSecFolded(sec) {
1474
+ const q = (document.getElementById("filter").value || "").trim();
1475
+ if (q) return false;
1476
+ return Boolean(state.navFold[sec]);
1477
+ }
1478
+ function applyNavFold() {
1479
+ ["channels", "dms", "cron"].forEach((sec) => {
1480
+ const el = document.querySelector('[data-nav-sec="' + sec + '"]');
1481
+ if (!el) return;
1482
+ const folded = navSecFolded(sec);
1483
+ el.classList.toggle("folded", folded);
1484
+ const btn = el.querySelector("[data-fold]");
1485
+ if (btn) btn.setAttribute("aria-expanded", folded ? "false" : "true");
1486
+ });
1487
+ }
1488
+ function saveNavFold() {
1489
+ try {
1490
+ localStorage.setItem("guild-nav-fold", JSON.stringify(state.navFold));
1491
+ } catch {
1492
+ /* ignore quota */
1493
+ }
1494
+ }
1495
+ function cronJobHref(job) {
1496
+ const id = String((job && job.roomId) || "");
1497
+ if ((state.channels || []).some((ch) => ch.id === id)) {
1498
+ return "#c/" + encodeURIComponent(id);
1499
+ }
1500
+ if (id.indexOf("dm-") === 0) {
1501
+ return "#d/" + encodeURIComponent(id.slice(3));
1502
+ }
1503
+ return "#c/" + encodeURIComponent(id);
1504
+ }
1505
+ function cronNavPreview(job) {
1506
+ const bot = botById(job.botId);
1507
+ const handle = bot ? bot.handle : "bot";
1508
+ return t("cron.navWhere", {
1509
+ handle: handle,
1510
+ schedule: job.schedule || "",
1511
+ });
1512
+ }
1513
+ async function loadNavCron() {
1514
+ try {
1515
+ const res = await fetch("/cron");
1516
+ const body = await res.json().catch(() => ({}));
1517
+ const jobs = Array.isArray(body.jobs) ? body.jobs : [];
1518
+ const sig = jobs
1519
+ .map((job) =>
1520
+ [job.id, job.nextRunAt, job.paused ? "1" : "0", job.running ? "1" : "0", job.name].join(":"),
1521
+ )
1522
+ .join("|");
1523
+ if (sig === state.cronSig) return;
1524
+ state.cronSig = sig;
1525
+ state.cronJobs = jobs;
1526
+ } catch {
1527
+ if (state.cronSig != null) return;
1528
+ state.cronJobs = [];
1529
+ }
1530
+ renderNav();
1531
+ }
1425
1532
  function renderNav() {
1426
1533
  const q = document.getElementById("filter").value.toLowerCase();
1427
1534
  const all = state.channels || [];
@@ -1544,6 +1651,53 @@
1544
1651
  });
1545
1652
  })
1546
1653
  .join("");
1654
+ const cronBox = document.getElementById("cron-nav");
1655
+ if (cronBox) {
1656
+ const jobs = (state.cronJobs || []).filter((job) => {
1657
+ if (!q) return true;
1658
+ const bot = botById(job.botId);
1659
+ const hay = [
1660
+ job.name,
1661
+ job.schedule,
1662
+ job.prompt,
1663
+ bot && bot.handle,
1664
+ bot && bot.name,
1665
+ ]
1666
+ .filter(Boolean)
1667
+ .join(" ")
1668
+ .toLowerCase();
1669
+ return hay.includes(q);
1670
+ });
1671
+ cronBox.innerHTML = jobs.length
1672
+ ? jobs
1673
+ .map((job) => {
1674
+ const bot = botById(job.botId);
1675
+ const onRoom =
1676
+ cronJobHref(job) ===
1677
+ (state.kind === "dm"
1678
+ ? "#d/" + encodeURIComponent(state.id)
1679
+ : "#c/" + encodeURIComponent(state.id));
1680
+ return navRowHtml({
1681
+ href: cronJobHref(job),
1682
+ on: onRoom,
1683
+ av: bot
1684
+ ? '<span class="avatar" style="background:' +
1685
+ hue(bot.handle) +
1686
+ '">' +
1687
+ avatarFace(bot) +
1688
+ "</span>"
1689
+ : "",
1690
+ name: job.name || t("cron.title"),
1691
+ updatedAt: job.nextRunAt,
1692
+ preview: cronNavPreview(job),
1693
+ busy: Boolean(job.running),
1694
+ chip: job.paused ? t("cron.pause") : "",
1695
+ });
1696
+ })
1697
+ .join("")
1698
+ : '<li class="nav-empty">' + escapeHtml(t("cron.navEmpty")) + "</li>";
1699
+ }
1700
+ applyNavFold();
1547
1701
  }
1548
1702
 
1549
1703
  function memberRowHtml(bot, action, locked, full) {
@@ -2087,6 +2241,7 @@
2087
2241
  syncThreadChrome();
2088
2242
  return;
2089
2243
  }
2244
+ const keptHtml = holdHtmlFrames(root);
2090
2245
  root.innerHTML =
2091
2246
  rows
2092
2247
  .map((msg, index) => {
@@ -2242,6 +2397,7 @@
2242
2397
  root.scrollTop = Math.max(0, root.scrollHeight - fromBottom);
2243
2398
  }
2244
2399
  syncThreadChrome();
2400
+ putHtmlFrames(root, keptHtml);
2245
2401
  hydrateHtmlPreviews(root);
2246
2402
  }
2247
2403
  function hydrateHtmlPreviews(root) {
@@ -2249,15 +2405,16 @@
2249
2405
  root.querySelectorAll(".md-html-preview").forEach((box) => {
2250
2406
  const src = box.querySelector(".md-html-src");
2251
2407
  const frame = box.querySelector(".md-html-frame");
2252
- if (!src || !frame || frame.dataset.ready) return;
2253
- frame.dataset.ready = "1";
2408
+ if (!src || !frame) return;
2254
2409
  const raw = src.value;
2255
2410
  const lang = (box.querySelector(".md-fence-lang") || {}).textContent || "";
2256
- frame.srcdoc = /svg/i.test(lang)
2257
- ? '<!doctype html><html><body style="margin:0;background:#fff">' +
2258
- raw +
2259
- "</body></html>"
2260
- : raw;
2411
+ const doc =
2412
+ typeof htmlPreviewSrcdoc === "function"
2413
+ ? htmlPreviewSrcdoc(raw, lang)
2414
+ : raw;
2415
+ if (frame.dataset.ready && frame.srcdoc === doc) return;
2416
+ frame.dataset.ready = "1";
2417
+ frame.srcdoc = doc;
2261
2418
  });
2262
2419
  }
2263
2420
  function openHtmlZoom(raw) {
@@ -2285,6 +2442,7 @@
2285
2442
  renderNav();
2286
2443
  renderHead();
2287
2444
  setHash();
2445
+ loadNavCron().catch(() => {});
2288
2446
  }
2289
2447
  async function deleteChannel(id, name) {
2290
2448
  if (!id || id === "channel-general") {
@@ -2593,8 +2751,11 @@
2593
2751
  const pending = (state.messages || []).filter(
2594
2752
  (msg) => String(msg.id).indexOf("pending-") === 0,
2595
2753
  );
2596
- if (!extras.length && !pending.length) return;
2597
2754
  const serverYou = incoming.filter((msg) => msg.author === "you");
2755
+ const dropPending = pending.filter((msg) =>
2756
+ serverYou.some((row) => row.body === msg.body),
2757
+ );
2758
+ if (!extras.length && !dropPending.length) return;
2598
2759
  state.messages = (state.messages || [])
2599
2760
  .filter((msg) => {
2600
2761
  if (String(msg.id).indexOf("pending-") === 0) {
@@ -3000,6 +3161,19 @@
3000
3161
  }
3001
3162
  });
3002
3163
  document.getElementById("filter").addEventListener("input", renderNav);
3164
+ document.getElementById("nav-drawer").addEventListener("click", (event) => {
3165
+ const btn = event.target.closest("[data-fold]");
3166
+ if (!btn || event.target.closest(".add-ch")) return;
3167
+ event.preventDefault();
3168
+ const sec = btn.getAttribute("data-fold");
3169
+ if (!sec) return;
3170
+ state.navFold[sec] = !state.navFold[sec];
3171
+ saveNavFold();
3172
+ applyNavFold();
3173
+ if (sec === "cron" && !state.navFold.cron) {
3174
+ loadNavCron().catch(() => {});
3175
+ }
3176
+ });
3003
3177
  document.getElementById("toggle-create").addEventListener("click", () => {
3004
3178
  document.getElementById("create-row").classList.toggle("open");
3005
3179
  document.getElementById("new-channel").focus();
@@ -3264,6 +3438,64 @@
3264
3438
  channelMdDialog.close();
3265
3439
  await loadWorkspace();
3266
3440
  });
3441
+ document.getElementById("cron-cancel").addEventListener("click", () => {
3442
+ document.getElementById("cron-dialog").close();
3443
+ });
3444
+ document.getElementById("cron-add").addEventListener("click", async () => {
3445
+ const botId = document.getElementById("cron-bot").value;
3446
+ const ask = document.getElementById("cron-ask").value.trim();
3447
+ const bot = botById(botId);
3448
+ if (!bot) {
3449
+ alert(t("cron.needSeat"));
3450
+ return;
3451
+ }
3452
+ if (!ask) {
3453
+ alert(t("cron.needAsk"));
3454
+ return;
3455
+ }
3456
+ const raw = "@" + bot.handle + " " + t("cron.askBody", { ask: ask });
3457
+ document.getElementById("cron-ask").value = "";
3458
+ document.getElementById("cron-dialog").close();
3459
+ const split = splitSendTargets([botId]);
3460
+ if (split.fresh.length) {
3461
+ await flushSend(raw, [], "", [botId], botId);
3462
+ } else {
3463
+ enqueuePending({
3464
+ raw: raw,
3465
+ packed: [],
3466
+ replyTo: "",
3467
+ botIds: [botId],
3468
+ assigneeId: botId,
3469
+ });
3470
+ }
3471
+ });
3472
+ document.getElementById("cron-list").addEventListener("click", async (event) => {
3473
+ const btn = event.target.closest("[data-cron-act]");
3474
+ const row = event.target.closest("[data-cron-id]");
3475
+ if (!btn || !row) return;
3476
+ const id = row.getAttribute("data-cron-id");
3477
+ const act = btn.getAttribute("data-cron-act");
3478
+ if (act === "remove") {
3479
+ const res = await fetch("/cron/" + encodeURIComponent(id), { method: "DELETE" });
3480
+ if (!res.ok) {
3481
+ const body = await res.json().catch(() => ({}));
3482
+ alert(body.error || "cron failed");
3483
+ return;
3484
+ }
3485
+ } else {
3486
+ const res = await fetch(
3487
+ "/cron/" + encodeURIComponent(id) + "/" + act,
3488
+ { method: "POST" },
3489
+ );
3490
+ const body = await res.json().catch(() => ({}));
3491
+ if (!res.ok) {
3492
+ alert(body.error || "cron failed");
3493
+ return;
3494
+ }
3495
+ }
3496
+ await renderCronList();
3497
+ loadNavCron().catch(() => {});
3498
+ });
3267
3499
  document.getElementById("bot-memory-cancel").addEventListener("click", () => {
3268
3500
  botMemoryDialog.close();
3269
3501
  });
@@ -4299,6 +4531,85 @@
4299
4531
  body: chunks.join("\n\n") || t("attach.noPersona"),
4300
4532
  });
4301
4533
  }
4534
+ function cronRoomId() {
4535
+ if (state.kind === "dm") return "dm-" + state.id;
4536
+ return state.id;
4537
+ }
4538
+ async function loadCronJobs() {
4539
+ const id = cronRoomId();
4540
+ if (!id) return [];
4541
+ const res = await fetch("/cron?room=" + encodeURIComponent(id));
4542
+ const body = await res.json().catch(() => ({}));
4543
+ return Array.isArray(body.jobs) ? body.jobs : [];
4544
+ }
4545
+ function cronWhen(iso) {
4546
+ const d = new Date(iso || "");
4547
+ if (!Number.isFinite(d.getTime())) return String(iso || "");
4548
+ return d.toLocaleString();
4549
+ }
4550
+ async function renderCronList() {
4551
+ const box = document.getElementById("cron-list");
4552
+ const jobs = await loadCronJobs();
4553
+ if (!jobs.length) {
4554
+ box.innerHTML = '<div class="cron-empty">' + t("cron.empty") + "</div>";
4555
+ return;
4556
+ }
4557
+ box.innerHTML = jobs
4558
+ .map((job) => {
4559
+ const pauseLabel = job.paused ? t("cron.resume") : t("cron.pause");
4560
+ const pauseAct = job.paused ? "resume" : "pause";
4561
+ const err =
4562
+ job.lastStatus === "failed" && job.lastError
4563
+ ? " · " + escapeHtml(job.lastError)
4564
+ : "";
4565
+ return (
4566
+ '<div class="cron-row" data-cron-id="' +
4567
+ escapeHtml(job.id) +
4568
+ '"><div><b>' +
4569
+ escapeHtml(job.name) +
4570
+ "</b><span>" +
4571
+ escapeHtml(job.schedule) +
4572
+ " · " +
4573
+ t("cron.next", { when: cronWhen(job.nextRunAt) }) +
4574
+ (job.paused ? " · paused" : "") +
4575
+ err +
4576
+ '</span></div><div class="cron-acts">' +
4577
+ '<button type="button" data-cron-act="' +
4578
+ pauseAct +
4579
+ '">' +
4580
+ pauseLabel +
4581
+ '</button><button type="button" data-cron-act="run">' +
4582
+ t("cron.run") +
4583
+ '</button><button type="button" class="ghost danger" data-cron-act="remove">' +
4584
+ t("cron.remove") +
4585
+ "</button></div></div>"
4586
+ );
4587
+ })
4588
+ .join("");
4589
+ }
4590
+ async function openCronDialog() {
4591
+ const dialog = document.getElementById("cron-dialog");
4592
+ const sel = document.getElementById("cron-bot");
4593
+ const ch = currentChannel();
4594
+ const members = ch ? ch.members || [] : [];
4595
+ const dmBot =
4596
+ state.kind === "dm" && state.id ? botById(state.id) : null;
4597
+ const bots = members.length ? members : dmBot ? [dmBot] : [];
4598
+ sel.innerHTML = bots
4599
+ .map(
4600
+ (bot) =>
4601
+ '<option value="' +
4602
+ escapeHtml(bot.id) +
4603
+ '">@' +
4604
+ escapeHtml(bot.handle) +
4605
+ "</option>",
4606
+ )
4607
+ .join("");
4608
+ await renderCronList();
4609
+ dialog.showModal();
4610
+ const ask = document.getElementById("cron-ask");
4611
+ if (ask) ask.focus();
4612
+ }
4302
4613
  function openSlashSheet() {
4303
4614
  const rows = [
4304
4615
  { id: "channel-md", label: t("slash.editMd"), hint: t("slash.editMdHint") },
@@ -4306,6 +4617,7 @@
4306
4617
  { id: "git", label: t("slash.git"), hint: "status + diff" },
4307
4618
  { id: "here", label: t("slash.here"), hint: t("slash.hereHint") },
4308
4619
  { id: "library", label: t("slash.library"), hint: "/library" },
4620
+ { id: "cron", label: t("slash.cron"), hint: t("slash.cronHint") },
4309
4621
  ];
4310
4622
  if (state.kind !== "channel") {
4311
4623
  rows.splice(0, 1);
@@ -4335,6 +4647,11 @@
4335
4647
  }
4336
4648
  if (id === "library") {
4337
4649
  window.location.href = "/library";
4650
+ return;
4651
+ }
4652
+ if (id === "cron") {
4653
+ hideAttach();
4654
+ openCronDialog();
4338
4655
  }
4339
4656
  }
4340
4657
  async function runAttach(kind) {
@@ -4356,6 +4673,10 @@
4356
4673
  renderAttachSheet();
4357
4674
  return;
4358
4675
  }
4676
+ if (kind === "cron") {
4677
+ hideAttach();
4678
+ return openCronDialog();
4679
+ }
4359
4680
  if (kind === "slash") return openSlashSheet();
4360
4681
  }
4361
4682
  function startLiveTimers() {
@@ -5167,6 +5488,15 @@
5167
5488
  hint: "/library",
5168
5489
  glyph: "/",
5169
5490
  },
5491
+ {
5492
+ kind: "cmd",
5493
+ section: "cmd",
5494
+ cmdId: "cron",
5495
+ insert: "",
5496
+ name: t("slash.cron"),
5497
+ hint: t("slash.cronHint"),
5498
+ glyph: "/",
5499
+ },
5170
5500
  ];
5171
5501
  if (state.kind !== "channel") {
5172
5502
  return rows.filter(
@@ -6064,6 +6394,11 @@
6064
6394
  setNavOpen(false);
6065
6395
  openFromHash().catch((err) => alert(err.message));
6066
6396
  });
6397
+ applyNavFold();
6398
+ setInterval(() => {
6399
+ if (document.hidden || navSecFolded("cron")) return;
6400
+ loadNavCron().catch(() => {});
6401
+ }, 15000);
6067
6402
  Promise.all([openFromHash(), loadModelPick()])
6068
6403
  .then(() => loadSlashCatalog().catch(() => {}))
6069
6404
  .catch((err) => {
@@ -31,6 +31,8 @@ var I18N_ROWS = [
31
31
  ["search.traj", "搜尋 system / tool / spawn / 子代理 / 結果", "Search system / tool / spawn / subagent / result"],
32
32
  ["channels", "委託", "Channels"],
33
33
  ["dms", "密談", "Whispers"],
34
+ ["cron.navEmpty", "還沒有排程", "No schedules"],
35
+ ["cron.navWhere", "@{handle} · {schedule}", "@{handle} · {schedule}"],
34
36
  ["newChannel", "新委託", "New channel"],
35
37
  ["newChannelName", "委託名稱", "Channel name"],
36
38
  ["create", "建立", "Create"],
@@ -156,6 +158,7 @@ var I18N_ROWS = [
156
158
  ["attach.rules", "規則", "Rules"],
157
159
  ["attach.term", "終端", "Terminal"],
158
160
  ["attach.tree", "工作區", "Workspace"],
161
+ ["attach.cron", "排程", "Schedule"],
159
162
  ["attach.slash", "指令", "Commands"],
160
163
  ["insert", "插入 {name}", "Insert {name}"],
161
164
  ["remove", "移除", "Remove"],
@@ -224,6 +227,23 @@ var I18N_ROWS = [
224
227
  ["slash.here", "叫所有人", "Ping everyone"],
225
228
  ["slash.hereHint", "通知這個據點上的所有冒險者。通常是錯的:出活仍點名一人。", "Notify every adventurer in this hall. Usually wrong: call one @handle."],
226
229
  ["slash.library", "開工坊", "Open workshop"],
230
+ ["slash.cron", "排程", "Schedule"],
231
+ ["slash.cronHint", "請這席用 cronjob 排", "Ask this seat to cronjob"],
232
+ ["cron.title", "排程", "Schedule"],
233
+ ["cron.help", "新增會請這席呼叫 cronjob。時間用自然語言寫即可,例如「每10分鐘跟我說一次 Hi」。guildd 每 60 秒看一次;到點後這席開一輪新的 @handle,prompt 必須自包含。", "Add asks this seat to call cronjob. Write the time in natural language, e.g. \"every 10 minutes say Hi\". guildd ticks every 60s. Due jobs start a fresh @handle turn with a self-contained prompt."],
234
+ ["cron.seat", "席", "Seat"],
235
+ ["cron.ask", "要排什麼", "What to schedule"],
236
+ ["cron.askPh", "每10分鐘跟我說一次 Hi", "every 10 minutes say Hi"],
237
+ ["cron.askBody", "請用 cronjob 排程:{ask}", "Create a cronjob for this: {ask}"],
238
+ ["cron.needAsk", "先寫要排什麼", "Write what to schedule first"],
239
+ ["cron.needSeat", "這個據點沒有席", "No seat in this hall"],
240
+ ["cron.add", "請他排", "Ask them"],
241
+ ["cron.pause", "暫停", "Pause"],
242
+ ["cron.resume", "繼續", "Resume"],
243
+ ["cron.run", "立刻跑", "Run now"],
244
+ ["cron.remove", "刪除", "Remove"],
245
+ ["cron.empty", "這個據點還沒有排程。", "No schedules in this hall."],
246
+ ["cron.next", "下次 {when}", "Next {when}"],
227
247
  ["slash.sec.skill", "技能", "Skills"],
228
248
  ["slash.sec.agent", "子代理", "Subagents"],
229
249
  ["slash.sec.cmd", "指令", "Commands"],