@kevin5251984/guild 0.2.17 → 0.2.19

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="/style.css" />
11
+ <link rel="stylesheet" href="/style.css?v=aux-align" />
12
12
  <script src="/i18n.js"></script>
13
13
  </head>
14
14
  <body class="app settings-page" data-i18n-page="settings" data-section="main">
@@ -46,6 +46,7 @@
46
46
  </header>
47
47
  <div class="page-body">
48
48
  <p id="flash" class="muted"></p>
49
+ <p class="sub" data-i18n="settings.lede">第一次:先連訂閱或填 API key,再套用主模型。沒有模型就不能用。訂閱走 OAuth;API key 寫入 ~/.guild/models.json。</p>
49
50
 
50
51
  <section class="panel" data-sec-panel="main">
51
52
  <h2 data-i18n="settings.main">主模型</h2>
@@ -76,7 +77,7 @@
76
77
  <h2 data-i18n="settings.aux">輔助模型</h2>
77
78
  <button type="button" class="linkish" id="reset-aux" data-i18n="settings.auxReset">全部改回主模型</button>
78
79
  </div>
79
- <p class="sub" data-i18n="settings.auxHint">Vision、Web extract、Compression 等每一列都寫出實際模型。跟主模型時仍顯示名稱,不是空白。</p>
80
+ <p class="sub" data-i18n="settings.auxHint">只有 Vision、Web extract、SubAgent 可以另選模型。其餘用途一律跟主模型。</p>
80
81
  <div id="aux"></div>
81
82
  </section>
82
83
 
@@ -144,6 +145,18 @@
144
145
  let auxRole = null;
145
146
  let keyTab = 0;
146
147
  const PRESETS = {
148
+ "opencode-free": {
149
+ id: "opencode-free",
150
+ name: "OpenCode Free",
151
+ baseUrl: "https://opencode.ai/zen/v1",
152
+ api: "openai-completions",
153
+ apiKey: "",
154
+ models: [
155
+ { id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free" },
156
+ { id: "laguna-s-2.1-free", name: "Laguna S 2.1 Free" },
157
+ { id: "mimo-v2.5-free", name: "Mimo V2.5 Free" },
158
+ ],
159
+ },
147
160
  openai: {
148
161
  id: "openai",
149
162
  name: "OpenAI",
@@ -260,7 +273,7 @@
260
273
  '<option value="' +
261
274
  escapeHtml(m.id) +
262
275
  '">' +
263
- escapeHtml(m.name || m.id) +
276
+ escapeHtml(prettyModelName(m.id, m.name)) +
264
277
  "</option>",
265
278
  )
266
279
  .join("");
@@ -275,11 +288,27 @@
275
288
  function groupReady(id) {
276
289
  return Boolean((data.picker || []).find((p) => p.id === id && p.ready));
277
290
  }
291
+ function prettyModelName(id, given) {
292
+ const raw = String(id || "");
293
+ const label = String(given || "").trim();
294
+ if (label && label !== raw) return label;
295
+ const core = (raw.split("/").pop() || raw).trim();
296
+ const spaced = core
297
+ .replace(/[:_]+/g, "-")
298
+ .replace(/([A-Za-z])(\d)/g, "$1 $2")
299
+ .replace(/-/g, " ")
300
+ .replace(/\s+/g, " ")
301
+ .trim();
302
+ if (!spaced) return raw;
303
+ return spaced.replace(/\b([a-z])/g, function (ch) {
304
+ return ch.toUpperCase();
305
+ });
306
+ }
278
307
  function modelLabel(ref) {
279
308
  if (!ref || !ref.provider || !ref.model) return t("settings.noDefault");
280
309
  const group = pickerGroups().find((p) => p.id === ref.provider);
281
310
  const hit = ((group && group.models) || []).find((m) => m.id === ref.model);
282
- const name = (hit && hit.name) || ref.model;
311
+ const name = prettyModelName(ref.model, hit && hit.name);
283
312
  const vendor = (group && group.name) || ref.provider;
284
313
  return name + " · " + vendor;
285
314
  }
@@ -388,12 +417,46 @@
388
417
  if (key.length <= 10) return key;
389
418
  return key.slice(0, 5) + "…" + key.slice(-5);
390
419
  }
420
+ function isKeylessProvider(id) {
421
+ return id === "opencode-free" || id === "free" || id === "opencode_free";
422
+ }
391
423
  function storedBadge(p) {
424
+ if (isKeylessProvider(p.id)) {
425
+ return '<span class="badge">' + t("settings.keyless") + "</span>";
426
+ }
392
427
  if (p.stored === "env") return '<span class="badge">env</span>';
393
428
  if (p.stored === "literal") return '<span class="badge">' + t("settings.filled") + "</span>";
394
429
  return '<span class="badge empty">' + t("settings.emptyKey") + "</span>";
395
430
  }
431
+ function probeFor(id) {
432
+ return ((data.probe || []).find((row) => row.id === id) || null);
433
+ }
434
+ function probeBadge(id) {
435
+ const row = probeFor(id);
436
+ if (!row) return "";
437
+ if (row.ok && row.status === 429) {
438
+ return '<span class="badge wait probe-flag">' + t("settings.probeBusy") + "</span>";
439
+ }
440
+ if (row.ok) return '<span class="badge probe-flag">' + t("settings.probeOk") + "</span>";
441
+ return '<span class="badge empty probe-flag">' + t("settings.probeFail") + "</span>";
442
+ }
443
+ function probeHint() {
444
+ const probe = data.probe || [];
445
+ if (!probe.length) return "";
446
+ const skip = probe.filter((row) => !row.ok);
447
+ if (!skip.length) return "";
448
+ return (
449
+ '<p class="hint">' +
450
+ t("settings.syncedSkip", {
451
+ ok: probe.filter((row) => row.ok).length,
452
+ names: skip.map((row) => row.id).join("、"),
453
+ }) +
454
+ "</p>"
455
+ );
456
+ }
396
457
  function cardHtml(p, index) {
458
+ const lock = isKeylessProvider(p.id);
459
+ const ro = lock ? " readonly" : "";
397
460
  const models = (p.models || [])
398
461
  .map((m, i) => {
399
462
  return (
@@ -401,7 +464,9 @@
401
464
  '" data-f="id" value="' + escapeHtml(m.id || "") +
402
465
  '" placeholder="model id" /><input data-p="' + index + '" data-m="' + i +
403
466
  '" data-f="name" value="' + escapeHtml(m.name || "") +
404
- '" placeholder="' + t("settings.displayName") + '" /><button type="button" class="secondary" data-del-model="' +
467
+ '" placeholder="' + t("settings.displayName") + '" />' +
468
+ probeBadge(m.id) +
469
+ '<button type="button" class="secondary" data-del-model="' +
405
470
  index + ":" + i + '">×</button></div>'
406
471
  );
407
472
  })
@@ -409,19 +474,38 @@
409
474
  return (
410
475
  '<div class="provider-card" data-index="' + index + '">' +
411
476
  '<div class="card-head"><h3>' + escapeHtml(p.name || p.id) + storedBadge(p) +
412
- '</h3><button type="button" class="secondary" data-del="' + index + '">' + t("settings.delete") + "</button></div>" +
413
- "<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '" />' +
414
- "<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '" />' +
415
- "<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '" />' +
416
- '<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api">' +
477
+ "</h3>" +
478
+ (lock
479
+ ? ""
480
+ : '<button type="button" class="secondary" data-del-provider="' + index + '">' + t("settings.delete") + "</button>") +
481
+ "</div>" +
482
+ "<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '"' + ro + " />" +
483
+ "<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '"' + ro + " />" +
484
+ "<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '"' + ro + " />" +
485
+ '<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api"' +
486
+ (lock ? " disabled" : "") + ">" +
417
487
  '<option value="openai-completions"' + (p.api !== "anthropic-messages" && p.api !== "openai-responses" ? " selected" : "") + ">openai-completions</option>" +
418
488
  '<option value="anthropic-messages"' + (p.api === "anthropic-messages" ? " selected" : "") + ">anthropic-messages</option>" +
419
489
  '<option value="openai-responses"' + (p.api === "openai-responses" ? " selected" : "") + ">openai-responses</option></select></div>" +
420
- '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
421
- escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR sk-…" /></div></div>' +
422
- "<label>" + t("model") + "</label>" + models +
490
+ (isKeylessProvider(p.id)
491
+ ? '<div><label>API key</label><p class="hint">' + t("settings.keyless") + "</p></div>"
492
+ : '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
493
+ escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR 或 sk-…" /></div>') +
494
+ "</div>" +
495
+ (isKeylessProvider(p.id)
496
+ ? '<p class="hint">' + t("settings.opencodeFreeHint") + "</p>"
497
+ : "") +
498
+ '<div class="model-head"><label>' + t("model") + "</label>" +
499
+ (isKeylessProvider(p.id)
500
+ ? '<button type="button" class="sync-free" data-sync-free="1"><span class="sync-ico" aria-hidden="true">↻</span>' + t("settings.sync") + "</button>"
501
+ : "") +
502
+ "</div>" +
503
+ models +
504
+ (lock ? probeHint() : "") +
423
505
  '<button type="button" class="secondary" data-add-model="' + index + '" style="margin-top:0.55rem">' + t("settings.addModel") + "</button>" +
424
- '<div class="card-foot"><p class="hint">' + t("settings.saveKeyHint") + "</p>" +
506
+ '<div class="card-foot"><p class="hint">' +
507
+ t(isKeylessProvider(p.id) ? "settings.saveKeylessHint" : "settings.saveKeyHint") +
508
+ "</p>" +
425
509
  '<button type="button" data-save-keys="' + index + '">' +
426
510
  t("settings.saveKey") +
427
511
  "</button></div></div>"
@@ -434,11 +518,19 @@
434
518
  const p = (data.providers || [])[index];
435
519
  if (!p) return;
436
520
  const id = card.querySelector('[data-f="id"]').value.trim();
437
- if (id) p.id = id;
438
- p.name = card.querySelector('[data-f="name"]').value.trim();
439
- p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
440
- p.api = card.querySelector('[data-f="api"]').value;
441
- const key = card.querySelector('[data-f="apiKey"]').value.trim();
521
+ if (isKeylessProvider(p.id) || isKeylessProvider(id)) {
522
+ p.id = "opencode-free";
523
+ p.name = "OpenCode Free";
524
+ p.baseUrl = "https://opencode.ai/zen/v1";
525
+ p.api = "openai-completions";
526
+ } else {
527
+ if (id) p.id = id;
528
+ p.name = card.querySelector('[data-f="name"]').value.trim();
529
+ p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
530
+ p.api = card.querySelector('[data-f="api"]').value;
531
+ }
532
+ const keyEl = card.querySelector('[data-f="apiKey"]');
533
+ const key = keyEl ? keyEl.value.trim() : "";
442
534
  if (key && key !== (p.apiKeyPreview || "")) {
443
535
  p.apiKey = key;
444
536
  p.apiKeyPreview = maskApiKey(key);
@@ -454,8 +546,7 @@
454
546
  });
455
547
  p.models = models;
456
548
  }
457
- function collectKeys() {
458
- flushVisibleCard();
549
+ function snapshotProviders() {
459
550
  const providers = {};
460
551
  (data.providers || []).forEach((p) => {
461
552
  if (!p.id) return;
@@ -469,6 +560,10 @@
469
560
  });
470
561
  return providers;
471
562
  }
563
+ function collectKeys() {
564
+ flushVisibleCard();
565
+ return snapshotProviders();
566
+ }
472
567
  function presetLabel(key, preset) {
473
568
  return key === "custom" ? t("settings.custom") : (preset && preset.name) || key;
474
569
  }
@@ -730,6 +825,68 @@
730
825
  document.getElementById("add-provider-cancel").addEventListener("click", () => {
731
826
  document.getElementById("add-provider-dialog").close();
732
827
  });
828
+ async function syncOpenCodeFree() {
829
+ const btn = document.querySelector("[data-sync-free]");
830
+ if (btn) btn.disabled = true;
831
+ flash("", t("settings.syncing"));
832
+ try {
833
+ const res = await fetch("/settings/models/opencode-free/sync", {
834
+ method: "POST",
835
+ });
836
+ const body = await res.json().catch(() => ({}));
837
+ if (!res.ok) {
838
+ flash("error", body.error || t("settings.syncFailed"));
839
+ return;
840
+ }
841
+ data = body;
842
+ const probe = Array.isArray(body.probe) ? body.probe : [];
843
+ const ok = probe.filter((row) => row.ok).length;
844
+ const skip = probe.filter((row) => !row.ok);
845
+ render();
846
+ if (probe.length && !ok) {
847
+ flash("error", t("settings.syncedNone"));
848
+ } else if (skip.length) {
849
+ flash(
850
+ "ok",
851
+ t("settings.syncedSkip", {
852
+ ok: ok,
853
+ names: skip.map((row) => row.id).join("、"),
854
+ }),
855
+ );
856
+ } else {
857
+ const n =
858
+ ok ||
859
+ (((data.picker || []).find((p) => p.id === "opencode-free") || {}).models || [])
860
+ .length;
861
+ flash("ok", t("settings.synced", { n: n }));
862
+ }
863
+ } catch (err) {
864
+ flash("error", err.message || t("settings.syncFailed"));
865
+ } finally {
866
+ if (btn) btn.disabled = false;
867
+ }
868
+ }
869
+ async function deleteProvider(index) {
870
+ const doomed = (data.providers || [])[index];
871
+ if (!doomed || isKeylessProvider(doomed.id)) return;
872
+ const name = doomed.name || doomed.id;
873
+ data.providers.splice(index, 1);
874
+ if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
875
+ const res = await fetch("/settings/models", {
876
+ method: "PUT",
877
+ headers: { "content-type": "application/json" },
878
+ body: JSON.stringify({ providers: snapshotProviders() }),
879
+ });
880
+ const body = await res.json().catch(() => ({}));
881
+ if (!res.ok) {
882
+ flash("error", body.error || t("deleteFailed"));
883
+ await load();
884
+ return;
885
+ }
886
+ data = body;
887
+ render();
888
+ flash("health", t("settings.deletedProvider", { name: name }));
889
+ }
733
890
  async function saveProviderCard(index) {
734
891
  const card = document.querySelector('.provider-card[data-index="' + index + '"]');
735
892
  const label = card
@@ -762,17 +919,21 @@
762
919
  if (p) input.value = p.apiKeyPreview || "";
763
920
  });
764
921
  document.getElementById("providers").addEventListener("click", (event) => {
765
- const el = event.target;
922
+ const el = event.target.closest ? event.target : event.target.parentElement;
923
+ if (!el || !el.closest) return;
766
924
  if (el.closest("[data-save-keys]")) {
767
925
  const i = Number(el.closest("[data-save-keys]").getAttribute("data-save-keys"));
768
926
  saveProviderCard(i);
769
927
  return;
770
928
  }
771
- if (el.closest("[data-del]")) {
772
- const i = Number(el.closest("[data-del]").getAttribute("data-del"));
773
- data.providers.splice(i, 1);
774
- if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
775
- render();
929
+ if (el.closest("[data-sync-free]")) {
930
+ syncOpenCodeFree();
931
+ return;
932
+ }
933
+ if (el.closest("[data-del-provider]")) {
934
+ const i = Number(el.closest("[data-del-provider]").getAttribute("data-del-provider"));
935
+ deleteProvider(i);
936
+ return;
776
937
  }
777
938
  if (el.closest("[data-add-model]")) {
778
939
  flushVisibleCard();
@@ -95,7 +95,12 @@
95
95
  return;
96
96
  }
97
97
  const button = document.getElementById("ai-generate");
98
+ if (button.disabled || button.getAttribute("aria-busy") === "true") return;
98
99
  button.disabled = true;
100
+ button.setAttribute("aria-busy", "true");
101
+ button.classList.add("is-busy");
102
+ const idle = button.textContent;
103
+ button.textContent = t("studio.generating");
99
104
  try {
100
105
  const response = await fetch("/generate", {
101
106
  method: "POST",
@@ -115,6 +120,9 @@
115
120
  );
116
121
  } finally {
117
122
  button.disabled = false;
123
+ button.removeAttribute("aria-busy");
124
+ button.classList.remove("is-busy");
125
+ button.textContent = idle;
118
126
  }
119
127
  });
120
128