codex-partner 0.0.1__py3-none-any.whl
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.
- app.py +5530 -0
- codex_partner/__init__.py +4 -0
- codex_partner/__main__.py +6 -0
- codex_partner/app_server.py +150 -0
- codex_partner/cli.py +35 -0
- codex_partner/commands.py +77 -0
- codex_partner/database.py +206 -0
- codex_partner/discovery.py +136 -0
- codex_partner/schemas.py +133 -0
- codex_partner-0.0.1.dist-info/METADATA +101 -0
- codex_partner-0.0.1.dist-info/RECORD +244 -0
- codex_partner-0.0.1.dist-info/WHEEL +5 -0
- codex_partner-0.0.1.dist-info/entry_points.txt +2 -0
- codex_partner-0.0.1.dist-info/top_level.txt +3 -0
- static/__init__.py +1 -0
- static/app.js +505 -0
- static/chat-worker.js +157 -0
- static/conversation.js +894 -0
- static/core.js +880 -0
- static/editor.js +142 -0
- static/index.html +71 -0
- static/mascot-dance.css +100 -0
- static/mascot-dance.js +46 -0
- static/settings.js +335 -0
- static/styles.css +690 -0
- static/vendor/ace/LICENSE +24 -0
- static/vendor/ace/ace.js +23 -0
- static/vendor/ace/ext-modelist.js +8 -0
- static/vendor/ace/mode-abap.js +8 -0
- static/vendor/ace/mode-abc.js +8 -0
- static/vendor/ace/mode-actionscript.js +8 -0
- static/vendor/ace/mode-ada.js +8 -0
- static/vendor/ace/mode-alda.js +8 -0
- static/vendor/ace/mode-apache_conf.js +8 -0
- static/vendor/ace/mode-apex.js +8 -0
- static/vendor/ace/mode-applescript.js +8 -0
- static/vendor/ace/mode-aql.js +8 -0
- static/vendor/ace/mode-asciidoc.js +8 -0
- static/vendor/ace/mode-asl.js +8 -0
- static/vendor/ace/mode-assembly_arm32.js +8 -0
- static/vendor/ace/mode-assembly_x86.js +8 -0
- static/vendor/ace/mode-astro.js +8 -0
- static/vendor/ace/mode-autohotkey.js +8 -0
- static/vendor/ace/mode-basic.js +8 -0
- static/vendor/ace/mode-batchfile.js +8 -0
- static/vendor/ace/mode-bibtex.js +8 -0
- static/vendor/ace/mode-c9search.js +8 -0
- static/vendor/ace/mode-c_cpp.js +8 -0
- static/vendor/ace/mode-cedar.js +8 -0
- static/vendor/ace/mode-cedarschema.js +8 -0
- static/vendor/ace/mode-cirru.js +8 -0
- static/vendor/ace/mode-clojure.js +8 -0
- static/vendor/ace/mode-clue.js +8 -0
- static/vendor/ace/mode-cobol.js +8 -0
- static/vendor/ace/mode-coffee.js +8 -0
- static/vendor/ace/mode-coldfusion.js +8 -0
- static/vendor/ace/mode-crystal.js +8 -0
- static/vendor/ace/mode-csharp.js +8 -0
- static/vendor/ace/mode-csound_document.js +8 -0
- static/vendor/ace/mode-csound_orchestra.js +8 -0
- static/vendor/ace/mode-csound_score.js +8 -0
- static/vendor/ace/mode-csp.js +8 -0
- static/vendor/ace/mode-css.js +8 -0
- static/vendor/ace/mode-csv.js +8 -0
- static/vendor/ace/mode-curly.js +8 -0
- static/vendor/ace/mode-cuttlefish.js +8 -0
- static/vendor/ace/mode-d.js +8 -0
- static/vendor/ace/mode-dart.js +8 -0
- static/vendor/ace/mode-diff.js +8 -0
- static/vendor/ace/mode-django.js +8 -0
- static/vendor/ace/mode-dockerfile.js +8 -0
- static/vendor/ace/mode-dot.js +8 -0
- static/vendor/ace/mode-drools.js +8 -0
- static/vendor/ace/mode-edifact.js +8 -0
- static/vendor/ace/mode-eiffel.js +8 -0
- static/vendor/ace/mode-ejs.js +8 -0
- static/vendor/ace/mode-elixir.js +8 -0
- static/vendor/ace/mode-elm.js +8 -0
- static/vendor/ace/mode-erlang.js +8 -0
- static/vendor/ace/mode-flix.js +8 -0
- static/vendor/ace/mode-forth.js +8 -0
- static/vendor/ace/mode-fortran.js +8 -0
- static/vendor/ace/mode-fsharp.js +8 -0
- static/vendor/ace/mode-fsl.js +8 -0
- static/vendor/ace/mode-ftl.js +8 -0
- static/vendor/ace/mode-gcode.js +8 -0
- static/vendor/ace/mode-gherkin.js +8 -0
- static/vendor/ace/mode-gitignore.js +8 -0
- static/vendor/ace/mode-glsl.js +8 -0
- static/vendor/ace/mode-gobstones.js +8 -0
- static/vendor/ace/mode-golang.js +8 -0
- static/vendor/ace/mode-graphqlschema.js +8 -0
- static/vendor/ace/mode-groovy.js +8 -0
- static/vendor/ace/mode-groq.js +8 -0
- static/vendor/ace/mode-haml.js +8 -0
- static/vendor/ace/mode-handlebars.js +8 -0
- static/vendor/ace/mode-haskell.js +8 -0
- static/vendor/ace/mode-haskell_cabal.js +8 -0
- static/vendor/ace/mode-haxe.js +8 -0
- static/vendor/ace/mode-hjson.js +8 -0
- static/vendor/ace/mode-html.js +8 -0
- static/vendor/ace/mode-html_elixir.js +8 -0
- static/vendor/ace/mode-html_ruby.js +8 -0
- static/vendor/ace/mode-ini.js +8 -0
- static/vendor/ace/mode-io.js +8 -0
- static/vendor/ace/mode-ion.js +8 -0
- static/vendor/ace/mode-jack.js +8 -0
- static/vendor/ace/mode-jade.js +8 -0
- static/vendor/ace/mode-java.js +8 -0
- static/vendor/ace/mode-javascript.js +8 -0
- static/vendor/ace/mode-jexl.js +8 -0
- static/vendor/ace/mode-json.js +8 -0
- static/vendor/ace/mode-json5.js +8 -0
- static/vendor/ace/mode-jsoniq.js +8 -0
- static/vendor/ace/mode-jsp.js +8 -0
- static/vendor/ace/mode-jssm.js +8 -0
- static/vendor/ace/mode-jsx.js +8 -0
- static/vendor/ace/mode-julia.js +8 -0
- static/vendor/ace/mode-kotlin.js +8 -0
- static/vendor/ace/mode-latex.js +8 -0
- static/vendor/ace/mode-latte.js +8 -0
- static/vendor/ace/mode-less.js +8 -0
- static/vendor/ace/mode-liquid.js +8 -0
- static/vendor/ace/mode-lisp.js +8 -0
- static/vendor/ace/mode-livescript.js +8 -0
- static/vendor/ace/mode-logiql.js +8 -0
- static/vendor/ace/mode-logtalk.js +8 -0
- static/vendor/ace/mode-lsl.js +8 -0
- static/vendor/ace/mode-lua.js +8 -0
- static/vendor/ace/mode-luapage.js +8 -0
- static/vendor/ace/mode-lucene.js +8 -0
- static/vendor/ace/mode-makefile.js +8 -0
- static/vendor/ace/mode-mariadb.js +8 -0
- static/vendor/ace/mode-markdown.js +8 -0
- static/vendor/ace/mode-mask.js +8 -0
- static/vendor/ace/mode-matlab.js +8 -0
- static/vendor/ace/mode-maze.js +8 -0
- static/vendor/ace/mode-mediawiki.js +8 -0
- static/vendor/ace/mode-mel.js +8 -0
- static/vendor/ace/mode-mips.js +8 -0
- static/vendor/ace/mode-mixal.js +8 -0
- static/vendor/ace/mode-mushcode.js +8 -0
- static/vendor/ace/mode-mysql.js +8 -0
- static/vendor/ace/mode-nasal.js +8 -0
- static/vendor/ace/mode-nginx.js +8 -0
- static/vendor/ace/mode-nim.js +8 -0
- static/vendor/ace/mode-nix.js +8 -0
- static/vendor/ace/mode-nsis.js +8 -0
- static/vendor/ace/mode-nunjucks.js +8 -0
- static/vendor/ace/mode-objectivec.js +8 -0
- static/vendor/ace/mode-ocaml.js +8 -0
- static/vendor/ace/mode-odin.js +8 -0
- static/vendor/ace/mode-partiql.js +8 -0
- static/vendor/ace/mode-pascal.js +8 -0
- static/vendor/ace/mode-perl.js +8 -0
- static/vendor/ace/mode-pgsql.js +8 -0
- static/vendor/ace/mode-php.js +8 -0
- static/vendor/ace/mode-php_laravel_blade.js +8 -0
- static/vendor/ace/mode-pig.js +8 -0
- static/vendor/ace/mode-plain_text.js +8 -0
- static/vendor/ace/mode-plsql.js +8 -0
- static/vendor/ace/mode-powershell.js +8 -0
- static/vendor/ace/mode-praat.js +8 -0
- static/vendor/ace/mode-prisma.js +8 -0
- static/vendor/ace/mode-prolog.js +8 -0
- static/vendor/ace/mode-properties.js +8 -0
- static/vendor/ace/mode-protobuf.js +8 -0
- static/vendor/ace/mode-prql.js +8 -0
- static/vendor/ace/mode-puppet.js +8 -0
- static/vendor/ace/mode-python.js +8 -0
- static/vendor/ace/mode-qml.js +8 -0
- static/vendor/ace/mode-r.js +8 -0
- static/vendor/ace/mode-raku.js +8 -0
- static/vendor/ace/mode-razor.js +8 -0
- static/vendor/ace/mode-rdoc.js +8 -0
- static/vendor/ace/mode-red.js +8 -0
- static/vendor/ace/mode-redshift.js +8 -0
- static/vendor/ace/mode-rhtml.js +8 -0
- static/vendor/ace/mode-robot.js +8 -0
- static/vendor/ace/mode-rst.js +8 -0
- static/vendor/ace/mode-ruby.js +8 -0
- static/vendor/ace/mode-rust.js +8 -0
- static/vendor/ace/mode-sac.js +8 -0
- static/vendor/ace/mode-sass.js +8 -0
- static/vendor/ace/mode-scad.js +8 -0
- static/vendor/ace/mode-scala.js +8 -0
- static/vendor/ace/mode-scheme.js +8 -0
- static/vendor/ace/mode-scrypt.js +8 -0
- static/vendor/ace/mode-scss.js +8 -0
- static/vendor/ace/mode-sh.js +8 -0
- static/vendor/ace/mode-sjs.js +8 -0
- static/vendor/ace/mode-slim.js +8 -0
- static/vendor/ace/mode-smarty.js +8 -0
- static/vendor/ace/mode-smithy.js +8 -0
- static/vendor/ace/mode-snippets.js +8 -0
- static/vendor/ace/mode-soy_template.js +8 -0
- static/vendor/ace/mode-space.js +8 -0
- static/vendor/ace/mode-sparql.js +8 -0
- static/vendor/ace/mode-sql.js +8 -0
- static/vendor/ace/mode-sqlserver.js +8 -0
- static/vendor/ace/mode-stylus.js +8 -0
- static/vendor/ace/mode-svg.js +8 -0
- static/vendor/ace/mode-swift.js +8 -0
- static/vendor/ace/mode-tcl.js +8 -0
- static/vendor/ace/mode-terraform.js +8 -0
- static/vendor/ace/mode-tex.js +8 -0
- static/vendor/ace/mode-text.js +8 -0
- static/vendor/ace/mode-textile.js +8 -0
- static/vendor/ace/mode-toml.js +8 -0
- static/vendor/ace/mode-tsv.js +8 -0
- static/vendor/ace/mode-tsx.js +8 -0
- static/vendor/ace/mode-turtle.js +8 -0
- static/vendor/ace/mode-twig.js +8 -0
- static/vendor/ace/mode-typescript.js +8 -0
- static/vendor/ace/mode-vala.js +8 -0
- static/vendor/ace/mode-vbscript.js +8 -0
- static/vendor/ace/mode-velocity.js +8 -0
- static/vendor/ace/mode-verilog.js +8 -0
- static/vendor/ace/mode-vhdl.js +8 -0
- static/vendor/ace/mode-visualforce.js +8 -0
- static/vendor/ace/mode-vue.js +8 -0
- static/vendor/ace/mode-wollok.js +8 -0
- static/vendor/ace/mode-xml.js +8 -0
- static/vendor/ace/mode-xquery.js +8 -0
- static/vendor/ace/mode-yaml.js +8 -0
- static/vendor/ace/mode-zeek.js +8 -0
- static/vendor/ace/mode-zig.js +8 -0
- static/vendor/ace/theme-tomorrow_night_eighties.js +8 -0
- static/vendor/i18next/LICENSE +21 -0
- static/vendor/i18next/i18next.min.js +1 -0
- static/vendor/toastui-editor/LICENSE +21 -0
- static/vendor/toastui-editor/toastui-editor-dark.css +474 -0
- static/vendor/toastui-editor/toastui-editor.css +1597 -0
- static/vendor/toastui-editor/toastui-editor.js +24 -0
- static/vendor/toastui-editor/zh-cn.js +7 -0
- static/vendor/xterm/LICENSE +21 -0
- static/vendor/xterm/addons/fit.LICENSE +19 -0
- static/vendor/xterm/addons/fit.js +2 -0
- static/vendor/xterm/addons/unicode11.LICENSE +19 -0
- static/vendor/xterm/addons/unicode11.js +2 -0
- static/vendor/xterm/addons/web-links.LICENSE +19 -0
- static/vendor/xterm/addons/web-links.js +2 -0
- static/vendor/xterm/xterm.css +285 -0
- static/vendor/xterm/xterm.js +2 -0
static/settings.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Drawers and management panels for server, providers, skills, and memories.
|
|
2
|
+
function openPanel(type) {
|
|
3
|
+
const panel = $("#panel");
|
|
4
|
+
const normalized = type === "providers" ? "server" : type;
|
|
5
|
+
panel.dataset.panelType = normalized;
|
|
6
|
+
panel.classList.add("open");
|
|
7
|
+
panel.setAttribute("aria-hidden", "false");
|
|
8
|
+
$("#connection-status")?.classList.add("panel-open");
|
|
9
|
+
if (normalized === "server") renderServerPanel(type === "providers");
|
|
10
|
+
if (normalized === "memories") renderMemoryPanel();
|
|
11
|
+
if (normalized === "skills") renderSkillPanel();
|
|
12
|
+
if (normalized === "ssh") renderSSHPanel(true);
|
|
13
|
+
if (normalized === "trash") renderTrashPanel();
|
|
14
|
+
}
|
|
15
|
+
function closePanel() { $("#panel").classList.remove("open"); $("#panel").setAttribute("aria-hidden", "true"); if (!$("#drawer")?.classList.contains("open")) $("#connection-status")?.classList.remove("panel-open"); }
|
|
16
|
+
function refreshOpenPanel() {
|
|
17
|
+
const panel = $("#panel");
|
|
18
|
+
if (!panel?.classList.contains("open")) return;
|
|
19
|
+
if (panel.dataset.panelType === "server") renderServerPanel();
|
|
20
|
+
if (panel.dataset.panelType === "memories") renderMemoryPanel($("#memory-search")?.value || "");
|
|
21
|
+
if (panel.dataset.panelType === "skills") renderSkillPanel();
|
|
22
|
+
if (panel.dataset.panelType === "trash") renderTrashPanel();
|
|
23
|
+
if (panel.dataset.panelType === "ssh") renderSSHPanel(false);
|
|
24
|
+
}
|
|
25
|
+
function providerConnectionRows(current) {
|
|
26
|
+
if (!state.providers.length) return `<div class="empty connection-empty">${uiLabel("noProviders")}</div>`;
|
|
27
|
+
return state.providers.map(item => {
|
|
28
|
+
const health = providerHealth(item);
|
|
29
|
+
const selected = current?.id === item.id;
|
|
30
|
+
const inUse = Number(item.in_use_count || 0) > 0;
|
|
31
|
+
const latency = item.health_latency_ms == null ? "--" : `${Number(item.health_latency_ms)}ms`;
|
|
32
|
+
const useButton = selected ? `<button class="provider-use current" disabled>${uiLabel("current")}</button>` : item.enabled === false ? `<button class="provider-use" disabled>${uiLabel("unavailable")}</button>` : `<button data-provider-use="${esc(item.id)}" class="provider-use">${uiLabel("use")}</button>`;
|
|
33
|
+
const badge = selected ? uiLabel("current") : inUse ? t("running") : item.is_default ? uiLabel("default") : uiLabel("priority", { value: Number(item.priority || 0) });
|
|
34
|
+
const defaultAddress = uiLabel("codexDefaultAddress");
|
|
35
|
+
return `<article class="connection-provider-item ${selected ? "current" : ""} ${item.enabled === false ? "disabled" : ""}"><div class="connection-provider-head"><div class="connection-provider-name"><strong>${esc(item.name)}</strong><span>${badge}</span></div><span class="provider-health ${esc(health.kind)}"><i></i>${esc(health.label)}</span></div><div class="connection-provider-route"><strong>${esc(item.model || item.model_provider || uiLabel("providerDefaultModel"))}</strong><span title="${esc(item.base_url || defaultAddress)}">${esc(item.base_url || defaultAddress)}</span></div><div class="connection-provider-meta"><span>${item.enabled === false ? uiLabel("excludedFromFailover") : uiLabel("priority", { value: Number(item.priority || 0) })}</span><span>${esc(item.credential_source || uiLabel("apiKeyUnset"))}</span><span>${uiLabel("latency", { value: latency })}</span><span>${uiLabel("successFailure", { success: Number(item.success_count || 0), failure: Number(item.failure_count || 0) })}</span></div><div class="connection-provider-actions">${useButton}<button data-provider-check="${esc(item.id)}">${uiLabel("check")}</button><button data-provider-edit="${esc(item.id)}">${uiLabel("edit")}</button><button class="danger-text" data-provider-delete="${esc(item.id)}">${uiLabel("delete")}</button></div></article>`;
|
|
36
|
+
}).join("");
|
|
37
|
+
}
|
|
38
|
+
async function renderCodexManagement() {
|
|
39
|
+
const section = $("#codex-management");
|
|
40
|
+
if (!section) return;
|
|
41
|
+
try {
|
|
42
|
+
const info = await api("/codex/manage");
|
|
43
|
+
state.codexInfo = info;
|
|
44
|
+
const plan = info.update_plan || {};
|
|
45
|
+
section.innerHTML = `<div class="connection-section-head"><div><span class="panel-section-label">CODEX</span><h3>${uiLabel("codexManagement")}</h3></div><button id="update-codex" class="secondary" ${plan.supported ? "" : "disabled"}>${uiLabel("updateCodex")}</button></div><div class="codex-management-grid"><div><small>${uiLabel("codexVersion")}</small><strong>${esc(info.version || uiLabel("codexVersionUnknown"))}</strong></div><div><small>${uiLabel("codexSource")}</small><strong>${esc(info.source || "missing")}</strong></div><div class="codex-management-wide"><small>${uiLabel("codexPath")}</small><code title="${esc(info.path || "")}">${esc(info.path || uiLabel("codexMissing"))}</code></div><div class="codex-management-wide"><small>${uiLabel("codexWorkingDirectory")}</small><code title="${esc(info.working_directory || "")}">${esc(info.working_directory || "")}</code></div></div>${info.error ? `<p class="connection-inline-error">${esc(info.error)}</p>` : ""}`;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
section.innerHTML = `<div class="connection-section-head"><div><span class="panel-section-label">CODEX</span><h3>${uiLabel("codexManagement")}</h3></div></div><p class="connection-inline-error">${esc(error.message)}</p>`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function renderServerPanel(focusProviders = false) {
|
|
51
|
+
const info = state.serverInfo || {};
|
|
52
|
+
const host = location.hostname || info.hostname || "网页服务器";
|
|
53
|
+
const port = location.port || info.bind_port || 8787;
|
|
54
|
+
const current = currentProvider();
|
|
55
|
+
const enabled = state.providers.filter(item => item.enabled !== false);
|
|
56
|
+
const usable = enabled.filter(providerUsable);
|
|
57
|
+
const metrics = state.runtimeMetrics || {};
|
|
58
|
+
const realtime = realtimeChannels.overview === "live" ? uiLabel("liveConnected") : realtimeChannels.overview === "reconnecting" ? uiLabel("reconnecting") : uiLabel("connecting");
|
|
59
|
+
$("#panel-content").innerHTML = `<div class="connection-panel"><div class="panel-title connection-panel-title"><span class="eyebrow">CONNECTION</span><h2>${uiLabel("connectionAndProviders")}</h2><p>${uiLabel("connectionDescription")}</p></div><section class="connection-overview" aria-label="${uiLabel("webService")}"><div class="connection-server"><span class="connection-server-mark" aria-hidden="true"><i></i></span><div><strong>${esc(info.user || uiLabel("serviceUser"))}@${esc(host)}</strong><small>HTTP ${esc(host)}:${esc(port)} · ${esc(info.hostname || host)}</small></div><span class="provider-health healthy"><i></i>${realtime}</span></div><div class="connection-facts"><div><small>${uiLabel("currentProvider")}</small><strong>${esc(current?.name || uiLabel("automatic"))}</strong></div><div><small>${uiLabel("usableRoutes")}</small><strong>${usable.length} / ${enabled.length}</strong></div><div><small>TTFT</small><strong>${formatMetricMs(metrics.ttftMs)}</strong></div><div><small>TPOT</small><strong>${formatMetricMs(metrics.tpotMs)}</strong></div></div><div class="connection-server-meta"><span>${state.codexAvailable ? uiLabel("codexReady") : uiLabel("codexMissing")}</span><span>${uiLabel("runningTasks", { count: Number(info.running || 0) })}</span><span title="${esc(info.default_workspace || "")}">${esc(info.default_workspace || uiLabel("workspaceUnset"))}</span></div></section><section id="codex-management" class="codex-management"></section><section id="connection-providers" class="connection-providers"><div class="connection-section-head"><div><span class="panel-section-label">PROVIDER ROUTING</span><h3>Provider</h3><p>${uiLabel("providerRoutingDescription")}</p></div><div class="connection-section-actions"><button id="sync-providers" class="secondary">${uiLabel("sync")}</button><button id="check-providers" class="secondary">${uiLabel("checkAll")}</button><button id="new-provider" class="primary">${uiLabel("add")}</button></div></div><div class="connection-provider-list">${providerConnectionRows(current)}</div></section></div>`;
|
|
60
|
+
renderCodexManagement();
|
|
61
|
+
if (focusProviders) requestAnimationFrame(() => $("#connection-providers")?.scrollIntoView({ block: "start" }));
|
|
62
|
+
}
|
|
63
|
+
async function renderTrashPanel() {
|
|
64
|
+
const content = $("#panel-content");
|
|
65
|
+
content.innerHTML = `<div class="panel-title"><span class="eyebrow">RECYCLE BIN</span><h2>${t("trash")}</h2><p>${uiLabel("recycleBinDescription")}</p></div><div class="panel-list"><div class="empty">${uiLabel("readingTrash")}</div></div>`;
|
|
66
|
+
try {
|
|
67
|
+
const rows = await api("/trash");
|
|
68
|
+
const list = rows.map(task => `<article class="panel-item trash-item"><div class="panel-item-head"><strong>${esc(task.name || "Codex")}</strong><span class="source-badge readonly">${uiLabel("deleted")}</span></div><small>${esc(task.trashed_at ? shortDate(task.trashed_at) : shortDate(task.updated_at))} · ${esc(task.workspace || "")}</small><p>${esc(task.goal || task.prompt || uiLabel("emptySession"))}</p><div class="panel-item-actions"><button data-trash-restore="${esc(task.id)}">${uiLabel("restore")}</button><button class="danger" data-trash-delete="${esc(task.id)}">${uiLabel("permanentDelete")}</button></div></article>`).join("");
|
|
69
|
+
content.querySelector(".panel-list").innerHTML = list || `<div class="empty">${uiLabel("emptyTrash")}</div>`;
|
|
70
|
+
} catch (error) { content.querySelector(".panel-list").innerHTML = `<p class="inspector-error">${esc(error.message)}</p>`; }
|
|
71
|
+
}
|
|
72
|
+
function renderMemoryPanel(query = "") {
|
|
73
|
+
const needle = query.trim().toLowerCase();
|
|
74
|
+
const files = state.memories.filter(item => !needle || `${item.name} ${item.preview || ""}`.toLowerCase().includes(needle));
|
|
75
|
+
const generated = state.generatedMemories.filter(item => !needle || `${item.thread_id} ${item.slug || ""} ${item.preview || ""}`.toLowerCase().includes(needle));
|
|
76
|
+
$("#panel-content").innerHTML = `<div class="panel-title"><span class="eyebrow">NATIVE MEMORY</span><h2>${uiLabel("codexMemory")}</h2><p>${uiLabel("memorySummary", { markdown: state.memories.length, generated: state.generatedMemories.length })}</p></div><div class="panel-toolbar"><input id="memory-search" value="${esc(query)}" placeholder="${uiLabel("searchMemory")}"/><button id="new-memory" class="primary">${uiLabel("newItem")}</button></div><span class="panel-section-label">MARKDOWN</span><div class="panel-list">${files.slice(0, 80).map(item => `<article class="panel-item"><div class="panel-item-head"><strong>${esc(item.name)}</strong><span class="source-badge">${uiLabel("editable")}</span></div><small>${formatBytes(item.size)} · ${esc(shortDate(item.updated_at))}</small><p>${esc(item.preview || "")}</p><div class="panel-item-actions"><button data-memory-view="${esc(item.name)}">${uiLabel("view")}</button><button data-memory-edit="${esc(item.name)}">${uiLabel("edit")}</button><button data-memory-delete="${esc(item.name)}">${uiLabel("delete")}</button></div></article>`).join("") || `<div class="empty">${uiLabel("noMarkdownMemories")}</div>`}</div><span class="panel-section-label">${uiLabel("generated")}</span><div class="panel-list">${generated.slice(0, 40).map(item => `<article class="panel-item"><div class="panel-item-head"><strong>${esc(item.slug || item.thread_id)}</strong><span class="source-badge readonly">${uiLabel("readOnly")}</span></div><small>${esc(shortDate(item.generated_at))} · ${uiLabel("memoryUsage", { count: item.usage_count || 0 })}</small><p>${esc(item.preview || "")}</p><div class="panel-item-actions"><button data-generated-memory="${esc(item.thread_id)}">${uiLabel("view")}</button></div></article>`).join("") || `<div class="empty">${uiLabel("noGeneratedMemories")}</div>`}</div><button id="reset-generated-memories" class="danger full-button">${uiLabel("rebuildGeneratedMemories")}</button>`;
|
|
77
|
+
const search = $("#memory-search");
|
|
78
|
+
search.oninput = event => { const position = event.target.selectionStart; renderMemoryPanel(event.target.value); const next = $("#memory-search"); next.focus(); next.setSelectionRange(position, position); };
|
|
79
|
+
}
|
|
80
|
+
function renderSkillPanel() {
|
|
81
|
+
const installed = state.skills.filter(item => item.installed).length;
|
|
82
|
+
$("#panel-content").innerHTML = `<div class="panel-title"><span class="eyebrow">INSTALLED CONTEXT</span><h2>${t("skills")}</h2><p>${uiLabel("installedSkillsSummary", { count: installed })}</p></div><button id="new-skill" class="primary full-button">${uiLabel("newPersonalSkill")}</button><div class="panel-list panel-list-spaced">${state.skills.map(item => `<article class="panel-item"><div class="panel-item-head"><strong>${esc(item.name)}</strong><span class="source-badge ${item.editable ? "" : "readonly"}">${esc(item.source || "Dashboard")}</span></div><small>${item.editable ? uiLabel("editable") : uiLabel("readOnly")}${item.path ? ` · ${esc(item.path)}` : ` · ${item.enabled ? uiLabel("enabled") : uiLabel("disabled")}`}</small><p>${esc(item.description || item.content || "")}</p><div class="panel-item-actions"><button data-skill-edit="${item.id}">${item.editable ? uiLabel("edit") : uiLabel("view")}</button>${item.deletable ? `<button data-skill-delete="${item.id}">${uiLabel("delete")}</button>` : ""}</div></article>`).join("") || `<div class="empty">${uiLabel("noSkills")}</div>`}</div>`;
|
|
83
|
+
}
|
|
84
|
+
function providerHealth(item) {
|
|
85
|
+
if (item.enabled === false) return { label: uiLabel("disabled"), kind: "disabled" };
|
|
86
|
+
const labels = { healthy: "healthHealthy", reachable: "healthReachable", configured: "healthConfigured", needs_key: "healthNeedsKey", auth_error: "healthAuthError", degraded: "healthDegraded", unavailable: "healthUnreachable", invalid: "healthInvalid", error: "healthError", unchecked: "healthUnchecked" };
|
|
87
|
+
return { label: labels[item.health_status] ? uiLabel(labels[item.health_status]) : item.health_status || uiLabel("healthUnchecked"), kind: item.health_status || "unchecked" };
|
|
88
|
+
}
|
|
89
|
+
function renderProviderPanel() { renderServerPanel(true); }
|
|
90
|
+
function sshStatus(item) {
|
|
91
|
+
const labels = { connected: "connected", connected_no_codex: "codexUnavailable", needs_password: "needsPassword", failed: "connectionFailed", disconnected: "notConnected" };
|
|
92
|
+
return { label: labels[item.status] ? uiLabel(labels[item.status]) : item.status || uiLabel("notConnected"), kind: item.connected ? (item.codex_bin ? "healthy" : "needs_key") : item.status === "needs_password" ? "needs_key" : item.status === "failed" ? "unavailable" : "unchecked" };
|
|
93
|
+
}
|
|
94
|
+
function drawSSHPanel() {
|
|
95
|
+
const content = $("#panel-content"); if (!content) return;
|
|
96
|
+
const localActive = !state.activeSSHHost;
|
|
97
|
+
const localRow = `<article class="panel-item ssh-item ${localActive ? "active" : ""}"><div class="panel-item-head"><strong>${uiLabel("local")}${localActive ? `<span class="provider-flag live">${uiLabel("default")}</span>` : ""}</strong><span class="provider-health healthy"><i></i>${uiLabel("readyStatus")}</span></div><small>${esc(state.defaultWorkspace || "")}</small><p>Local Codex</p><div class="panel-item-actions"><button data-ssh-use="">${uiLabel("useLocal")}</button></div></article>`;
|
|
98
|
+
const rows = state.sshHosts.slice(0, 1).map(item => { const health = sshStatus(item); const active = state.activeSSHHost === item.alias; const identity = (item.identity_files || []).find(path => !path.includes("_ecdsa") && !path.includes("_ed25519")) || (item.identity_files || [])[0] || "~/.ssh/config"; return `<article class="panel-item ssh-item ${active ? "active" : ""}"><div class="panel-item-head"><strong>${esc(item.alias)}${active ? `<span class="provider-flag live">${uiLabel("default")}</span>` : ""}</strong><span class="provider-health ${esc(health.kind)}"><i></i>${esc(health.label)}</span></div><small>${esc(item.user || uiLabel("defaultUser"))}@${esc(item.hostname || item.alias)}:${item.port || 22}</small><p title="${esc(item.last_error || "")}">${esc(identity)}${item.proxy_jump ? `<br>ProxyJump ${esc(item.proxy_jump)}` : ""}${item.remote_home ? `<br>${esc(item.remote_home)}` : ""}${item.codex_bin ? `<br>${esc(item.codex_bin)}` : ""}</p><div class="panel-item-actions">${item.connected ? `<button data-ssh-use="${esc(item.alias)}">${uiLabel("use")}</button>${item.codex_bin ? "" : `<button data-ssh-install="${esc(item.alias)}">${uiLabel("installCodex")}</button>`}<button data-ssh-disconnect="${esc(item.alias)}">${uiLabel("disconnect")}</button>` : `<button data-ssh-connect="${esc(item.alias)}">${item.status === "needs_password" ? uiLabel("login") : uiLabel("retry")}</button>`}</div></article>`; }).join("") || `<div class="empty">${uiLabel("noFixedServer")}</div>`;
|
|
99
|
+
const fixed = state.sshHosts[0];
|
|
100
|
+
const connectForm = !fixed ? `<form id="ssh-host-form" class="form"><label>${uiLabel("serverAddress")}<input name="host" required autocomplete="off" spellcheck="false" placeholder="192.168.1.20"/></label><label>${uiLabel("sshUsername")}<input name="username" required autocomplete="username" placeholder="root"/></label><label>${uiLabel("port")}<input name="port" type="number" min="1" max="65535" value="22" required/></label><label>${uiLabel("passwordOrPassphrase")}<input name="password" type="password" autocomplete="current-password"/></label><button class="primary">${uiLabel("connectServer")}</button></form>` : "";
|
|
101
|
+
content.innerHTML = `<div class="panel-title"><span class="eyebrow">FIXED CODEX SERVER</span><h2>${uiLabel("serverConnection")}</h2><p>${esc(fixed ? `${fixed.hostname || fixed.alias}:${fixed.port || 22}` : (state.sshConfig || "~/.ssh/config"))} · ~/.ssh / ssh-agent</p></div>${connectForm}<div class="panel-list">${fixed ? rows : ""}</div>`;
|
|
102
|
+
if (!fixed) $("#ssh-host-form").onsubmit = async event => { event.preventDefault(); const form = new FormData(event.target); try { const row = await api("/ssh/connect", { method: "POST", body: JSON.stringify({ host: form.get("host"), username: form.get("username"), port: Number(form.get("port") || 22), password: form.get("password") || null }) }); updateSSHHost(row); state.activeSSHHost = row.alias; localStorage.setItem("codex-dashboard-ssh-host", row.alias); drawSSHPanel(); if (!row.connected && row.status === "needs_password") return openSSHPasswordForm(row); if (!row.connected) toast(row.last_error || "SSH 连接失败"); else toast("Codex 服务器已连接"); } catch (error) { toast(`SSH 连接失败:${error.message}`); } };
|
|
103
|
+
}
|
|
104
|
+
async function loadSSHHosts(probe = false) {
|
|
105
|
+
const data = await api(`/ssh/hosts?probe=${probe ? "true" : "false"}`);
|
|
106
|
+
state.sshHosts = data.hosts || []; state.sshConfig = data.config || "~/.ssh/config";
|
|
107
|
+
renderConnectionStatus();
|
|
108
|
+
if (state.activeSSHHost && !state.sshHosts.some(item => item.alias === state.activeSSHHost)) { state.activeSSHHost = ""; localStorage.removeItem("codex-dashboard-ssh-host"); }
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
async function renderSSHPanel(probe = false) {
|
|
112
|
+
$("#panel-content").innerHTML = `<div class="panel-title"><span class="eyebrow">FIXED CODEX SERVER</span><h2>${uiLabel("serverConnection")}</h2><p>${uiLabel("sshConnectionDescription")}</p></div>`;
|
|
113
|
+
try { await loadSSHHosts(probe); drawSSHPanel(); } catch (error) { $("#panel-content").insertAdjacentHTML("beforeend", `<p class="inspector-error">${esc(error.message)}</p>`); }
|
|
114
|
+
}
|
|
115
|
+
function updateSSHHost(row) {
|
|
116
|
+
const index = state.sshHosts.findIndex(item => item.alias === row.alias);
|
|
117
|
+
if (index >= 0) state.sshHosts[index] = row; else state.sshHosts.push(row);
|
|
118
|
+
renderConnectionStatus();
|
|
119
|
+
}
|
|
120
|
+
async function connectSSHHost(host, password = "") {
|
|
121
|
+
if (!host) return;
|
|
122
|
+
try {
|
|
123
|
+
const row = await api("/ssh/connect", { method: "POST", body: JSON.stringify({ host, password: password || null }) });
|
|
124
|
+
updateSSHHost(row);
|
|
125
|
+
if (!row.connected) { drawSSHPanel(); if (row.status === "needs_password") return openSSHPasswordForm(row); return toast(row.last_error || "SSH 主机不可达"); }
|
|
126
|
+
state.activeSSHHost = row.alias; localStorage.setItem("codex-dashboard-ssh-host", row.alias); drawSSHPanel(); toast(`${row.alias} 已连接`);
|
|
127
|
+
} catch (error) { toast(`SSH 连接失败:${error.message}`); }
|
|
128
|
+
}
|
|
129
|
+
function openSSHPasswordForm(item) {
|
|
130
|
+
openDrawer(`<h2>连接 Codex 服务器</h2><p class="drawer-meta">${esc(item.hostname || item.alias)}:${item.port || 22}</p><form id="ssh-password-form" class="form"><label>SSH 用户名<input name="username" value="${esc(item.user || "")}" required autocomplete="username" autofocus/></label><label>SSH 密码或私钥口令<input name="password" type="password" required autocomplete="current-password"/></label><label>端口<input name="port" type="number" min="1" max="65535" value="${item.port || 22}" required/></label><div class="form-actions"><button type="button" class="secondary" id="cancel">取消</button><button class="primary">连接</button></div></form>`);
|
|
131
|
+
$("#cancel").onclick = closeDrawer;
|
|
132
|
+
$("#ssh-password-form").onsubmit = async event => { event.preventDefault(); const restore = setFormBusy(event.target, "连接中…"); const form = new FormData(event.target); const password = form.get("password"); try { const row = await api("/ssh/connect", { method: "POST", body: JSON.stringify({ host: item.hostname || item.alias, username: form.get("username"), port: Number(form.get("port") || 22), password }) }); updateSSHHost(row); if (row.connected) { state.activeSSHHost = row.alias; localStorage.setItem("codex-dashboard-ssh-host", row.alias); closeDrawer(); openPanel("ssh"); } else restore(); } catch (error) { restore(); toast(`SSH 连接失败:${error.message}`); } };
|
|
133
|
+
}
|
|
134
|
+
async function checkAllProviders() {
|
|
135
|
+
const button = $("#check-providers");
|
|
136
|
+
if (button) { button.disabled = true; button.textContent = "检查中…"; }
|
|
137
|
+
try {
|
|
138
|
+
state.providers = await api("/providers/check-all", { method: "POST" });
|
|
139
|
+
renderServerPanel(true);
|
|
140
|
+
toast("Provider 检查完成");
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (button) { button.disabled = false; button.textContent = "全部检查"; }
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function checkOneProvider(providerId) {
|
|
147
|
+
const button = $$('[data-provider-check]').find(node => node.dataset.providerCheck === providerId);
|
|
148
|
+
if (button) { button.disabled = true; button.textContent = "检查中…"; }
|
|
149
|
+
try {
|
|
150
|
+
const checked = await api(`/providers/${providerId}/check`, { method: "POST" });
|
|
151
|
+
state.providers = state.providers.map(item => item.id === providerId ? checked : item);
|
|
152
|
+
renderServerPanel(true);
|
|
153
|
+
toast(`${checked.name} 检查完成`);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (button) { button.disabled = false; button.textContent = "检查"; }
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function destroyMarkdownComponents() {
|
|
160
|
+
for (const component of markdownComponents) {
|
|
161
|
+
try { component.destroy(); } catch (_) {}
|
|
162
|
+
}
|
|
163
|
+
markdownComponents = [];
|
|
164
|
+
}
|
|
165
|
+
function markdownOptions() { return document.documentElement.dataset.theme === "light" ? {} : { theme: "dark" }; }
|
|
166
|
+
function openDrawer(html, options = {}) { destroyMarkdownComponents(); $("#drawer-content").innerHTML = html; $("#drawer").classList.toggle("editor-drawer", Boolean(options.editor)); $("#drawer").classList.add("open"); $("#drawer").setAttribute("aria-hidden", "false"); $("#connection-status")?.classList.add("panel-open"); }
|
|
167
|
+
function closeDrawer() { destroyMarkdownComponents(); $("#drawer").classList.remove("open", "editor-drawer"); $("#drawer").setAttribute("aria-hidden", "true"); if (!$("#panel")?.classList.contains("open")) $("#connection-status")?.classList.remove("panel-open"); }
|
|
168
|
+
async function createQuickSession() {
|
|
169
|
+
try {
|
|
170
|
+
// New sessions run on the Codex Partner service host. SSH is opt-in for
|
|
171
|
+
// legacy remote sessions and must never change the webpage connection.
|
|
172
|
+
const created = await api("/tasks/quick", { method: "POST", body: JSON.stringify({ workspace: "", ssh_host: "" }) });
|
|
173
|
+
await refresh(false);
|
|
174
|
+
await selectSession(created.id);
|
|
175
|
+
$("#message-input").focus();
|
|
176
|
+
toast("新会话已创建 · YOLO 已开启");
|
|
177
|
+
} catch (error) { toast(error.message); }
|
|
178
|
+
}
|
|
179
|
+
function setFormBusy(form, label) {
|
|
180
|
+
const submit = form.querySelector('[type="submit"], button:not([type])');
|
|
181
|
+
if (!submit) return () => {};
|
|
182
|
+
const previous = submit.textContent; submit.disabled = true; submit.textContent = label;
|
|
183
|
+
return () => { submit.disabled = false; submit.textContent = previous; };
|
|
184
|
+
}
|
|
185
|
+
async function memoryForm(name = "") {
|
|
186
|
+
openDrawer(`<h2>${name ? uiLabel("editMemory") : uiLabel("newMemory")}</h2><p class="drawer-status">${name ? uiLabel("reading") : uiLabel("createMarkdownMemory")}</p>`, { editor: true });
|
|
187
|
+
try {
|
|
188
|
+
const content = name ? (await api(`/memories/${name.split("/").map(encodeURIComponent).join("/")}`)).content || "" : "";
|
|
189
|
+
$("#drawer-content").innerHTML = `<h2>${name ? uiLabel("editMemory") : uiLabel("newMemory")}</h2><form id="memory-form" class="form unified-editor-form"><label>${uiLabel("fileName")}<input name="name" required value="${esc(name)}" ${name ? "readonly" : ""} placeholder="project-notes.md"/></label><div class="editor-field"><span class="form-label">${uiLabel("content")}</span>${textEditorMarkup("memory-editor")}</div><div class="form-actions"><button type="button" class="secondary" id="cancel">${uiLabel("cancel")}</button><button type="submit" class="primary">${uiLabel("save")} <kbd>Ctrl S</kbd></button></div></form>`;
|
|
190
|
+
const editor = mountTextEditor("#memory-editor", { filename: name || "project-notes.md", value: content, placeholder: uiLabel("memoryEditorPlaceholder") });
|
|
191
|
+
$("#cancel").onclick = closeDrawer;
|
|
192
|
+
$("#memory-form").onsubmit = async event => {
|
|
193
|
+
event.preventDefault(); const restore = setFormBusy(event.target, uiLabel("saving")); const form = new FormData(event.target);
|
|
194
|
+
let file = String(form.get("name") || "").trim(); if (!/\.(md|markdown)$/i.test(file)) file += ".md";
|
|
195
|
+
try {
|
|
196
|
+
await api(`/memories/${file.split("/").map(encodeURIComponent).join("/")}`, { method: "PUT", body: JSON.stringify({ name: file, content: editor.getValue() }) });
|
|
197
|
+
closeDrawer(); await refresh(false); openPanel("memories"); toast(uiLabel("memorySaved"));
|
|
198
|
+
} catch (error) { restore(); toast(uiLabel("saveFailed", { message: error.message })); }
|
|
199
|
+
};
|
|
200
|
+
} catch (error) { $("#drawer-content").insertAdjacentHTML("beforeend", `<p class="inspector-error">${esc(error.message)}</p>`); }
|
|
201
|
+
}
|
|
202
|
+
async function memoryView(name) {
|
|
203
|
+
openDrawer(`<h2>${esc(name)}</h2><p class="drawer-status">${uiLabel("reading")}</p>`);
|
|
204
|
+
try {
|
|
205
|
+
const item = await api(`/memories/${name.split("/").map(encodeURIComponent).join("/")}`);
|
|
206
|
+
$("#drawer-content").innerHTML = `<div class="drawer-heading"><div><h2>${esc(name)}</h2><p class="drawer-meta">TOAST UI Markdown Viewer</p></div><button class="primary" data-memory-edit="${esc(name)}">${uiLabel("edit")}</button></div><div id="memory-viewer" class="memory-viewer"></div>`;
|
|
207
|
+
const viewer = toastui.Editor.factory({ el: $("#memory-viewer"), viewer: true, initialValue: item.content || "", usageStatistics: false, ...markdownOptions() });
|
|
208
|
+
markdownComponents.push(viewer);
|
|
209
|
+
} catch (error) { $("#drawer-content").insertAdjacentHTML("beforeend", `<p class="inspector-error">${esc(error.message)}</p>`); }
|
|
210
|
+
}
|
|
211
|
+
async function generatedMemoryView(threadId) {
|
|
212
|
+
openDrawer(`<h2>${uiLabel("generated")}</h2><p class="drawer-status">${uiLabel("reading")}</p>`);
|
|
213
|
+
try {
|
|
214
|
+
const item = await api(`/memories/generated/${encodeURIComponent(threadId)}`);
|
|
215
|
+
$("#drawer-content").innerHTML = `<h2>${esc(item.slug || item.thread_id)}</h2><p class="drawer-meta">${esc(item.thread_id)} · ${uiLabel("memoryUsage", { count: item.usage_count || 0 })}</p><div class="readonly-document"><h3>${uiLabel("summary")}</h3><div id="generated-summary" class="memory-viewer"></div><h3>${uiLabel("rawMemory")}</h3><div id="generated-raw" class="memory-viewer"></div></div><div class="form-actions"><button type="button" class="secondary" id="cancel">${uiLabel("close")}</button></div>`;
|
|
216
|
+
const summaryViewer = toastui.Editor.factory({ el: $("#generated-summary"), viewer: true, initialValue: item.rollout_summary || "", usageStatistics: false, ...markdownOptions() });
|
|
217
|
+
const rawViewer = toastui.Editor.factory({ el: $("#generated-raw"), viewer: true, initialValue: item.raw_memory || "", usageStatistics: false, ...markdownOptions() });
|
|
218
|
+
markdownComponents.push(summaryViewer, rawViewer);
|
|
219
|
+
$("#cancel").onclick = closeDrawer;
|
|
220
|
+
} catch (error) { $("#drawer-content").insertAdjacentHTML("beforeend", `<p class="inspector-error">${esc(error.message)}</p>`); }
|
|
221
|
+
}
|
|
222
|
+
function providerNameFromUrl(value) {
|
|
223
|
+
try { return new URL(value).hostname || "Custom Provider"; } catch (_) { return "Custom Provider"; }
|
|
224
|
+
}
|
|
225
|
+
function providerEndpointKey(value) {
|
|
226
|
+
try {
|
|
227
|
+
const url = new URL(String(value || "").trim());
|
|
228
|
+
url.hostname = url.hostname.toLowerCase();
|
|
229
|
+
if ((url.protocol === "https:" && url.port === "443") || (url.protocol === "http:" && url.port === "80")) url.port = "";
|
|
230
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
231
|
+
url.hash = "";
|
|
232
|
+
return url.toString().replace(/\/$/, "");
|
|
233
|
+
} catch (_) { return String(value || "").trim().replace(/\/+$/, "").toLowerCase(); }
|
|
234
|
+
}
|
|
235
|
+
function providerEditForm(provider) {
|
|
236
|
+
const keyHint = provider.has_saved_key ? "已保存;留空不修改" : (provider.native ? "留空则沿用 Codex 配置" : "输入 API Key");
|
|
237
|
+
const currentModel = provider.model || "";
|
|
238
|
+
const currentOption = currentModel ? `<option value="${esc(currentModel)}" selected>${esc(currentModel)}(当前)</option>` : "";
|
|
239
|
+
openDrawer(`<h2>编辑 Provider</h2>${provider.native ? `<p class="drawer-meta">来自 Codex config.toml;网页保存的 API Key 不会被同步覆盖。</p>` : ""}<form id="provider-form" class="form"><label>名称<input name="name" required value="${esc(provider.name || "")}"/></label><label>模型<select name="model" id="provider-edit-model"><option value="">Provider 默认模型</option>${currentOption}</select><small id="provider-edit-model-status" class="field-hint">正在读取模型列表…</small></label><label>modelProvider ID<input name="model_provider" value="${esc(provider.model_provider || "")}" placeholder="config.toml 中的 provider ID"/></label><label>API Key<input name="api_key" type="password" autocomplete="new-password" placeholder="${esc(keyHint)}"/></label>${provider.has_saved_key ? `<label class="toggle danger-toggle"><input name="clear_api_key" type="checkbox"/>清除已保存的 API Key</label>` : ""}<label>Base URL<input name="base_url" type="url" value="${esc(provider.base_url || "")}" placeholder="https://api.example.com/v1"/></label><label>优先级<input name="priority" type="number" value="${provider.priority ?? 100}"/></label><label class="toggle"><input name="enabled" type="checkbox" ${provider.enabled !== false ? "checked" : ""}/>启用并参与自动切换</label><div class="form-actions"><button type="button" class="secondary" id="cancel">取消</button><button type="submit" class="primary">保存并检查</button></div></form>`);
|
|
240
|
+
const modelSelect = $("#provider-edit-model");
|
|
241
|
+
const modelStatus = $("#provider-edit-model-status");
|
|
242
|
+
const selectedTask = state.selectedTask?.provider_id === provider.id ? state.selectedTask : null;
|
|
243
|
+
if (selectedTask) {
|
|
244
|
+
api(`/tasks/${encodeURIComponent(selectedTask.id)}/models`).then(result => {
|
|
245
|
+
const models = normalizeModelItems(result.models);
|
|
246
|
+
const options = models.map(item => `<option value="${esc(item.id)}">${esc(item.label)}</option>`).join("");
|
|
247
|
+
modelSelect.innerHTML = `<option value="">Provider 默认模型</option>${options}${currentModel && !models.some(item => item.id === currentModel) ? currentOption : ""}`;
|
|
248
|
+
modelSelect.value = currentModel;
|
|
249
|
+
modelStatus.textContent = models.length ? `可选模型:${models.length} 个` : "Provider 未返回模型,将使用默认模型";
|
|
250
|
+
}).catch(error => { modelStatus.textContent = `模型列表读取失败:${error.message}`; });
|
|
251
|
+
} else {
|
|
252
|
+
modelStatus.textContent = "保存后可在会话工具栏选择该 Provider 的模型";
|
|
253
|
+
}
|
|
254
|
+
$("#cancel").onclick = closeDrawer;
|
|
255
|
+
$("#provider-form").onsubmit = async event => {
|
|
256
|
+
event.preventDefault(); const restore = setFormBusy(event.target, "保存并检查…"); const form = new FormData(event.target);
|
|
257
|
+
const body = { name: form.get("name"), kind: "codex", model: form.get("model"), model_provider: form.get("model_provider"), profile: "", api_key: form.get("api_key") || null, clear_api_key: form.has("clear_api_key"), base_url: form.get("base_url"), priority: Number(form.get("priority")), enabled: form.has("enabled") };
|
|
258
|
+
try {
|
|
259
|
+
await api(provider.id ? `/providers/${provider.id}` : "/providers", { method: provider.id ? "PUT" : "POST", body: JSON.stringify(body) });
|
|
260
|
+
closeDrawer(); await refresh(false); openPanel("server"); toast("Provider 已保存并完成检查");
|
|
261
|
+
} catch (error) { restore(); toast(`保存失败:${error.message}`); }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function providerForm(provider = {}) {
|
|
265
|
+
if (provider.id) return providerEditForm(provider);
|
|
266
|
+
openDrawer(`<h2>新增 Provider</h2><p class="drawer-meta">先填写连接地址和 API Key。验证成功后读取模型列表,选择默认模型再完成添加。相同连接地址不能重复添加。</p><form id="provider-form" class="form provider-create-form"><label>连接地址<input name="base_url" type="url" required autocomplete="url" placeholder="https://api.example.com/v1"/></label><label>API Key<input name="api_key" type="password" required autocomplete="new-password" placeholder="输入 API Key"/></label><p id="provider-verify-status" class="provider-verify-status">验证后选择默认模型</p><div id="provider-model-row" class="provider-model-picker" hidden><div class="provider-model-picker-head"><strong id="provider-model-count">模型列表</strong><input id="provider-model-search" type="search" autocomplete="off" placeholder="筛选模型"/></div><label class="provider-model-select-label">默认模型<select name="verified_model" id="provider-verified-model" aria-label="选择默认模型"><option value="">请先验证连接</option></select></label></div><div class="form-actions"><button type="button" class="secondary" id="cancel">取消</button><button type="button" class="secondary" id="provider-verify">验证连接</button><button type="submit" class="primary" id="provider-submit" hidden>添加 Provider</button></div></form>`);
|
|
267
|
+
const form = $("#provider-form");
|
|
268
|
+
const verifyButton = $("#provider-verify");
|
|
269
|
+
const submitButton = $("#provider-submit");
|
|
270
|
+
const modelRow = $("#provider-model-row");
|
|
271
|
+
const modelSelect = $("#provider-verified-model");
|
|
272
|
+
const modelSearch = $("#provider-model-search");
|
|
273
|
+
const modelCount = $("#provider-model-count");
|
|
274
|
+
const status = $("#provider-verify-status");
|
|
275
|
+
let verifiedSignature = "";
|
|
276
|
+
const signature = () => `${String(form.elements.base_url.value || "").trim()}\n${String(form.elements.api_key.value || "")}`;
|
|
277
|
+
const resetVerification = () => { verifiedSignature = ""; modelRow.hidden = true; submitButton.hidden = true; verifyButton.hidden = false; status.className = "provider-verify-status"; status.textContent = "验证后选择默认模型"; };
|
|
278
|
+
[form.elements.base_url, form.elements.api_key].forEach(input => input.addEventListener("input", resetVerification));
|
|
279
|
+
$("#cancel").onclick = closeDrawer;
|
|
280
|
+
verifyButton.onclick = async () => {
|
|
281
|
+
const baseUrl = String(form.elements.base_url.value || "").trim();
|
|
282
|
+
const apiKey = String(form.elements.api_key.value || "");
|
|
283
|
+
if (!baseUrl || !apiKey) { status.className = "provider-verify-status error"; status.textContent = "连接地址和 API Key 都必须填写"; return; }
|
|
284
|
+
const duplicate = state.providers.find(item => providerEndpointKey(item.base_url) === providerEndpointKey(baseUrl));
|
|
285
|
+
if (duplicate) { status.className = "provider-verify-status error"; status.textContent = `连接已存在:${duplicate.name}`; return; }
|
|
286
|
+
const previous = verifyButton.textContent; verifyButton.disabled = true; verifyButton.textContent = "验证中…";
|
|
287
|
+
try {
|
|
288
|
+
const result = await api("/providers/verify", { method: "POST", body: JSON.stringify({ base_url: baseUrl, api_key: apiKey }) });
|
|
289
|
+
if (!result.ok) { status.className = "provider-verify-status error"; status.textContent = `验证失败:${result.detail || result.health_status}`; return; }
|
|
290
|
+
const models = Array.isArray(result.models) ? result.models.slice() : [];
|
|
291
|
+
if (!models.length) { status.className = "provider-verify-status error"; status.textContent = "连接成功,但没有返回模型列表,无法完成添加"; return; }
|
|
292
|
+
const normalizedModels = normalizeModelItems(models);
|
|
293
|
+
const renderModels = (query = "") => {
|
|
294
|
+
const needle = query.trim().toLowerCase();
|
|
295
|
+
const visible = normalizedModels.filter(item => !needle || `${item.id} ${item.label}`.toLowerCase().includes(needle));
|
|
296
|
+
const selected = modelSelect.value;
|
|
297
|
+
modelSelect.innerHTML = visible.map(item => `<option value="${esc(item.id)}">${esc(item.label)}</option>`).join("") || `<option value="">没有匹配的模型</option>`;
|
|
298
|
+
if (visible.some(item => item.id === selected)) modelSelect.value = selected;
|
|
299
|
+
modelCount.textContent = `模型列表 · ${visible.length}/${normalizedModels.length}`;
|
|
300
|
+
};
|
|
301
|
+
modelSelect.value = normalizedModels[0].id;
|
|
302
|
+
modelSearch.oninput = event => renderModels(event.target.value);
|
|
303
|
+
renderModels();
|
|
304
|
+
status.className = "provider-verify-status ok"; status.textContent = `连接正常 · ${normalizedModels.length} 个模型 · ${Number(result.latency_ms || 0)}ms`;
|
|
305
|
+
} catch (error) { status.className = "provider-verify-status error"; status.textContent = `验证失败:${error.message}`; }
|
|
306
|
+
finally { verifyButton.disabled = false; verifyButton.textContent = previous; }
|
|
307
|
+
};
|
|
308
|
+
form.onsubmit = async event => {
|
|
309
|
+
event.preventDefault();
|
|
310
|
+
if (!verifiedSignature || verifiedSignature !== signature()) { status.className = "provider-verify-status error"; status.textContent = "连接或 API Key 已变化,请重新验证"; return; }
|
|
311
|
+
if (!form.elements.verified_model.value) { status.className = "provider-verify-status error"; status.textContent = "请选择默认模型"; return; }
|
|
312
|
+
const restore = setFormBusy(form, "添加中…");
|
|
313
|
+
const body = { name: providerNameFromUrl(form.elements.base_url.value), kind: "codex", model: form.elements.verified_model.value, model_provider: "", profile: "", api_key: form.elements.api_key.value, clear_api_key: false, base_url: form.elements.base_url.value, priority: 100, enabled: true };
|
|
314
|
+
try { await api("/providers", { method: "POST", body: JSON.stringify(body) }); closeDrawer(); await refresh(false); openPanel("server"); toast("Provider 已添加并设为可用路由"); }
|
|
315
|
+
catch (error) { restore(); status.className = "provider-verify-status error"; status.textContent = `添加失败:${error.message}`; }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function skillForm(skill = {}) {
|
|
319
|
+
const readOnly = Boolean(skill.id && !skill.editable);
|
|
320
|
+
const installed = Boolean(skill.installed);
|
|
321
|
+
const title = readOnly ? uiLabel("viewSkill") : skill.id ? uiLabel("editSkill") : uiLabel("newPersonalSkill");
|
|
322
|
+
const initialContent = skill.content || (!skill.id ? uiLabel("skillTemplate") : "");
|
|
323
|
+
openDrawer(`<h2>${title}</h2>${skill.path ? `<p class="drawer-meta">${esc(skill.source)} · ${esc(skill.path)}</p>` : ""}<form id="skill-form" class="form unified-editor-form"><div class="editor-form-fields"><label>${uiLabel("name")}<input name="name" required value="${esc(skill.name || "")}" ${readOnly ? "readonly" : ""} placeholder="my-skill"/></label><label>${uiLabel("description")}<input name="description" value="${esc(skill.description || "")}" ${readOnly ? "readonly" : ""}/></label></div><div class="editor-field"><span class="form-label">${uiLabel("content")}</span>${textEditorMarkup("skill-editor")}</div>${installed ? "" : `<label class="toggle"><input name="enabled" type="checkbox" ${skill.enabled !== false ? "checked" : ""}/>${uiLabel("enable")}</label>`}<div class="form-actions"><button type="button" class="secondary" id="cancel">${readOnly ? uiLabel("close") : uiLabel("cancel")}</button>${readOnly ? "" : `<button type="submit" class="primary">${uiLabel("save")} <kbd>Ctrl S</kbd></button>`}</div></form>`, { editor: true });
|
|
324
|
+
const editor = mountTextEditor("#skill-editor", { filename: skill.path || "SKILL.md", value: initialContent, readOnly });
|
|
325
|
+
$("#cancel").onclick = closeDrawer;
|
|
326
|
+
if (readOnly) return;
|
|
327
|
+
$("#skill-form").onsubmit = async event => {
|
|
328
|
+
event.preventDefault(); const restore = setFormBusy(event.target, uiLabel("saving")); const form = new FormData(event.target);
|
|
329
|
+
const body = { name: form.get("name"), description: form.get("description"), content: editor.getValue(), enabled: installed ? true : form.has("enabled") };
|
|
330
|
+
try {
|
|
331
|
+
await api(skill.id ? `/skills/${skill.id}` : "/skills", { method: skill.id ? "PUT" : "POST", body: JSON.stringify(body) });
|
|
332
|
+
closeDrawer(); await refresh(false); openPanel("skills"); toast(uiLabel("skillSaved"));
|
|
333
|
+
} catch (error) { restore(); toast(uiLabel("saveFailed", { message: error.message })); }
|
|
334
|
+
};
|
|
335
|
+
}
|