From 26328751ee78894a6a360cd47f63c02b34d43b96 Mon Sep 17 00:00:00 2001 From: moranzhi Date: Mon, 14 Sep 2026 01:15:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=A7=92=E8=89=B2=E7=AE=A1=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E9=A2=84=E8=AE=BE=E9=87=87=E6=A0=B7=E7=BC=96=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 并把指令目录接口一并挂上,供输入框补全拉取。 Co-authored-by: Cursor --- src/server/settings-handlers.ts | 128 +++++++++++- web/settings.css | 119 ++++++++++++ web/settings.html | 41 +++- web/settings.js | 335 +++++++++++++++++++++++++++++++- 4 files changed, 609 insertions(+), 14 deletions(-) diff --git a/src/server/settings-handlers.ts b/src/server/settings-handlers.ts index bf91932..f0e79f9 100644 --- a/src/server/settings-handlers.ts +++ b/src/server/settings-handlers.ts @@ -26,6 +26,7 @@ import { patchPresetEntries, type PresetEntryPatch, } from "../preset/entries.js"; +import { patchPresetGeneration } from "../preset/generation.js"; import { runPresetProbe } from "../preset/probe.js"; import { deletePreset, @@ -33,10 +34,21 @@ import { importAndSavePreset, listPresets, } from "../preset/store.js"; +import type { GenerationParameters } from "../types/preset.js"; import { createLlmForPreset, hasRealLlmConfig, } from "../runtime/llm-factory.js"; +import { + createPersona, + deletePersona, + getActivePersona, + getCreationDefaultPersona, + listPersonas, + setActivePersonaId, + updatePersona, +} from "../persona/store.js"; +import { listDirectiveCatalog } from "../directives/expand.js"; import { sessionManager } from "./session-manager.js"; async function readBody(req: IncomingMessage): Promise { @@ -52,6 +64,16 @@ function json(res: ServerResponse, status: number, data: unknown): void { res.end(JSON.stringify(data)); } +function personasPayload(extra: Record = {}) { + const creation = getCreationDefaultPersona(); + return { + personas: listPersonas(), + activeId: getActivePersona()?.id ?? null, + creationDefaultId: creation?.id ?? null, + ...extra, + }; +} + export async function handleSettingsApi( req: IncomingMessage, res: ServerResponse, @@ -332,18 +354,25 @@ export async function handleSettingsApi( const body = JSON.parse(await readBody(req)) as { entries?: PresetEntryPatch[]; entry?: PresetEntryPatch; + generation?: GenerationParameters | Record; }; const patches = body.entries?.length ? body.entries : body.entry ? [body.entry] : []; - if (!patches.length) { - json(res, 400, { error: "缺少 entries 或 entry" }); + const hasGeneration = Object.prototype.hasOwnProperty.call( + body, + "generation", + ); + if (!patches.length && !hasGeneration) { + json(res, 400, { error: "缺少 entries、entry 或 generation" }); return true; } try { - const next = patchPresetEntries(preset, patches); + let next = preset; + if (patches.length) next = patchPresetEntries(next, patches); + if (hasGeneration) next = patchPresetGeneration(next, body.generation); const entries = listAllPresetEntries(next); const settings = loadAppSettings(); let reloadedSessions = 0; @@ -412,5 +441,98 @@ export async function handleSettingsApi( } } + if (pathname === "/api/directives/catalog" && req.method === "GET") { + json(res, 200, { items: listDirectiveCatalog() }); + return true; + } + + if (pathname === "/api/personas" && req.method === "GET") { + const active = getActivePersona(); + json(res, 200, { + ...personasPayload(), + active, + creationDefault: getCreationDefaultPersona(), + }); + return true; + } + + if (pathname === "/api/personas" && req.method === "POST") { + try { + const body = JSON.parse(await readBody(req)) as { + name?: string; + description?: string; + activate?: boolean; + creationDefault?: boolean; + }; + const persona = createPersona({ + name: body.name ?? "", + description: body.description, + activate: body.activate, + creationDefault: body.creationDefault, + }); + json(res, 201, { + ...personasPayload({ persona }), + }); + } catch (e) { + json(res, 400, { + error: e instanceof Error ? e.message : String(e), + }); + } + return true; + } + + const personaMatch = pathname.match( + /^\/api\/personas\/([^/]+)(\/activate)?$/, + ); + if (personaMatch) { + const id = decodeURIComponent(personaMatch[1]); + const activate = Boolean(personaMatch[2]); + + if (activate && req.method === "POST") { + try { + const persona = setActivePersonaId(id); + json(res, 200, { + ...personasPayload({ persona, activeId: persona.id }), + }); + } catch (e) { + json(res, 404, { + error: e instanceof Error ? e.message : String(e), + }); + } + return true; + } + + if (req.method === "PUT") { + try { + const body = JSON.parse(await readBody(req)) as { + name?: string; + description?: string; + creationDefault?: boolean; + }; + const persona = updatePersona(id, body); + json(res, 200, { + ...personasPayload({ persona }), + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + json(res, msg.includes("不存在") ? 404 : 400, { error: msg }); + } + return true; + } + + if (req.method === "DELETE") { + try { + deletePersona(id); + json(res, 200, { + ...personasPayload({ ok: true }), + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + json(res, msg.includes("不存在") ? 404 : 400, { error: msg }); + } + return true; + } + } + return false; } diff --git a/web/settings.css b/web/settings.css index f6240d3..2a4f97f 100644 --- a/web/settings.css +++ b/web/settings.css @@ -521,6 +521,73 @@ code { line-height: 1.5; } +.preset-gen-editor { + margin-bottom: 12px; + padding: 10px 12px; + background: var(--st-surface); + border: 1px solid var(--st-border); + border-radius: 6px; +} + +.preset-gen-head { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + margin-bottom: 10px; + font-size: 12px; + color: var(--st-text); +} + +.preset-gen-hint { + color: var(--st-faint); + font-weight: 400; +} + +.preset-gen-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px 10px; +} + +.preset-gen-grid .gen-field { + margin: 0; +} + +.preset-gen-grid .gen-field-check { + flex-direction: row; + align-items: center; + gap: 8px; + padding-top: 18px; +} + +.preset-gen-grid .gen-field-check input { + width: auto; + margin: 0; +} + +.preset-gen-actions { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +@media (max-width: 900px) { + .preset-gen-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .preset-gen-grid { + grid-template-columns: 1fr; + } + + .preset-entries-panel { + padding-left: 0; + } +} + .preset-entries-summary { font-size: 12px; color: var(--st-faint); @@ -1102,6 +1169,58 @@ code { line-height: 1.35; } +.profile-dialog label.check-label { + flex-direction: row; + align-items: flex-start; + gap: 8px; + color: var(--st-text); + font-size: 13px; + font-weight: 500; +} + +.profile-dialog label.check-label input { + width: auto; + margin-top: 3px; + flex-shrink: 0; +} + +.profile-dialog label.check-label .field-hint { + display: block; + margin-top: 4px; +} + +.persona-card-check { + display: inline-flex; + align-items: center; + gap: 6px; + margin-right: auto; + font-size: 12px; + color: var(--st-muted); + font-weight: 500; + cursor: pointer; + user-select: none; +} + +.persona-card-check input { + width: auto; + margin: 0; + accent-color: var(--st-accent); +} + +.persona-card-check:hover { + color: var(--st-text); +} + +.config-card.is-active { + border-color: #93c5fd; + box-shadow: inset 3px 0 0 var(--st-accent); +} + +.persona-config-card .config-card-actions { + padding-left: 0; + align-items: center; +} + .dialog-actions { display: flex; justify-content: flex-end; diff --git a/web/settings.html b/web/settings.html index 810b301..13ecf96 100644 --- a/web/settings.html +++ b/web/settings.html @@ -22,6 +22,7 @@