Files
writing-agent/web/settings.html
T
chenmoranzhi e2413b0677 主题与字号从页面顶栏搬进设置页的「外观」,顶部导航加上预设入口。
顶栏那套汉堡菜单在每页都要单独挂载、样式也重复,收进设置页后只剩一处改;顺手把「整组都没有曾用型号」的配置组整组收起,不再留空卡片。
2026-10-26 01:09:43 +08:00

424 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设置 · Writing Agent</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700&family=Noto+Serif+SC:wght@500;600;700&display=swap"
rel="stylesheet"
/>
<script src="/theme-init.js"></script>
<link rel="stylesheet" href="/settings.css" />
</head>
<body class="settings-page">
<header class="st-top">
<a class="st-brand" href="/">Writing Agent</a>
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/chat.html">直聊</a>
<a href="/modules.html">技能</a>
<a href="/libraries.html">库</a>
<a href="/stats.html">Token</a>
<a href="/presets.html">预设</a>
<a href="/settings.html" aria-current="page">设置</a>
</nav>
</header>
<div class="st-shell">
<aside class="st-rail" aria-label="设置分类">
<p class="st-rail-label">设置</p>
<button type="button" class="st-rail-item active" data-section="api">API</button>
<button type="button" class="st-rail-item" data-section="persona">用户角色</button>
<button type="button" class="st-rail-item" data-section="preference">用户偏好</button>
<button type="button" class="st-rail-item" data-section="preset">预设</button>
<button type="button" class="st-rail-item" data-section="storage">上下文</button>
<button type="button" class="st-rail-item" data-section="chat">直聊</button>
<button type="button" class="st-rail-item" data-section="appearance">外观</button>
<p class="st-rail-note">保存在本机 <code>~/.writing-agent/</code>,明文、不同步。</p>
</aside>
<main class="st-main">
<header class="st-main-head">
<div>
<h1 id="panel-title">API</h1>
<p class="st-sub" id="panel-subtitle">管理 LLM 连接</p>
</div>
<div class="st-main-actions" id="panel-actions"></div>
</header>
<div class="st-status" id="active-settings-bar"></div>
<div class="settings-toast" id="settings-toast" hidden></div>
<div class="st-body">
<section class="settings-section" id="section-api">
<div class="profiles-toolbar">
<label class="profiles-search">
<span class="visually-hidden">搜索 API 配置</span>
<input
type="search"
id="profile-search"
placeholder="搜索组名、模型、URL…"
autocomplete="off"
/>
</label>
</div>
<div id="profiles-list" class="card-list"></div>
</section>
<section class="settings-section hidden" id="section-persona">
<div class="settings-card storage-card">
<h3>用户角色</h3>
<p class="settings-help">
游玩时,对话里 <code>@玩家</code> / <code>{{user}}</code> 会换成当前选中角色的名字;
<code>@人设</code> 与预设的 persona 洞用其正文。可在输入框旁快速切换。
创作阶段不用当前选用,而用下方勾选的「创作默认」填入预设用户设定。
建议把创作默认的名称写成 <code>@玩家</code>:模型会按这个称呼写开场白,替换后仍是占位,进游玩再换成当前角色。
</p>
</div>
<div id="personas-list" class="card-list"></div>
</section>
<section class="settings-section hidden" id="section-preference">
<div class="settings-card storage-card">
<h3>用户偏好</h3>
<p class="settings-help">
跨局硬约束条目的增删与 LLM 提取已迁到顶栏
<a href="/libraries.html?tab=preferences">库 · 偏好库</a>。
创作期由「用户需求」节点从库选用写入本局,不再开玩时全量自动摊入。
</p>
<label class="field">
每隔多少游玩回合采集一次
<input
type="number"
id="preference-collect-every"
min="0"
max="100"
step="1"
value="10"
/>
</label>
<p class="settings-help muted">0 = 关闭自动采集。默认 10。候选经审核卡收下后写入偏好库。</p>
<div class="storage-actions">
<button type="button" class="btn-primary" id="btn-save-pref-every">保存间隔</button>
<a class="btn-secondary" href="/libraries.html?tab=preferences">打开偏好库</a>
</div>
</div>
</section>
<section class="settings-section hidden" id="section-preset">
<div class="settings-card storage-card">
<h3>发出前的消息整形</h3>
<p class="settings-help">
预设(如酒馆的 <code>squash_system_messages</code>)会要求把相邻同角色的消息合并。
这里默认「跟随预设」,也可以强制覆盖。
</p>
<div class="settings-form">
<div class="form-row">
<span class="form-label" id="preset-squash-label">合并模式</span>
<div class="form-control">
<select id="message-squash" aria-labelledby="preset-squash-label">
<option value="follow-preset">跟随预设</option>
<option value="none">不合并</option>
<option value="merge">合并相同角色连续的发言</option>
<option value="semi">半严格(强制对话角色交替)</option>
<option value="strict">严格(强制对话角色交替、用户最先)</option>
<option value="single-user-turn">单一用户消息(无工具)</option>
</select>
<p class="form-hint">
请求里带工具调用时,后三档会退化为「只合并相邻同角色」,避免拆散工具调用。
</p>
</div>
</div>
<div class="form-row">
<span class="form-label" id="preset-tool-label">预设工具协议</span>
<div class="form-control">
<select id="preset-tool-policy" aria-labelledby="preset-tool-label">
<option value="adapt">适配(把预设要求的工具折算到已有工具上)</option>
<option value="app">只用本应用工具(排除预设的伪工具条目)</option>
<option value="preset">只发预设要求的工具</option>
</select>
</div>
</div>
<div class="form-row">
<span class="form-label"><code>{{char}}</code> 取值</span>
<div class="form-control">
<p class="form-static">
优先用直聊上下文壳里的<strong>角色名</strong>;创作期取<strong>当前作品名</strong>
(对话落盘时给快照起的名字)。都没填时用「作品」。
</p>
</div>
</div>
</div>
<div class="storage-actions">
<button type="button" class="btn-primary" id="btn-save-preset-runtime">保存</button>
</div>
</div>
<div id="presets-list" class="card-list"></div>
<pre id="import-report" class="import-report" hidden></pre>
</section>
<section class="settings-section hidden" id="section-storage">
<div class="settings-card storage-card">
<h3>LLM 请求上下文</h3>
<p class="settings-help">
每次模型调用可保存完整 prompt。默认每个对话只保留最新若干条;更早消息仍在,但去掉上下文痕迹。可在对话里查看上下文。
</p>
<label class="field">
每个对话保留最新条数
<input
type="number"
id="context-trace-keep"
min="0"
max="50"
step="1"
value="5"
/>
</label>
<p class="settings-help muted">0 = 不保存;最大 50。保存后对新回复立即生效。</p>
<div class="storage-actions">
<button type="button" class="btn-primary" id="btn-save-context-keep">保存</button>
<button type="button" class="btn-secondary" id="btn-prune-context">修剪已打开会话</button>
<button type="button" class="btn-secondary danger-outline" id="btn-clear-context">清除全部痕迹</button>
</div>
</div>
<div class="settings-card storage-card">
<h3>对话落盘补写</h3>
<p class="settings-help">
同一句用户消息里,填写没通过时可以再自动补几枪。每一枪只改失败点:已经通过的产物不动,没通过的用上一份正文当底稿,已经填对的自评维度会带回去。次数用尽后仍标成失败待补,等下一句继续。
</p>
<label class="field">
同轮最多补写次数
<input
type="number"
id="dictate-fill-retries"
min="0"
max="12"
step="1"
value="0"
/>
</label>
<p class="settings-help muted">0 = 只填一次。最大 12。例如设为 7,就是第一次填写之外最多再试 7 次。</p>
<div class="storage-actions">
<button type="button" class="btn-primary" id="btn-save-fill-retries">保存次数</button>
</div>
</div>
</section>
<section class="settings-section hidden" id="section-chat">
<div class="settings-card storage-card">
<h3>直聊卡片拖动</h3>
<p class="settings-help">
在直聊里按住回复卡片拖动(左右切卡片、上下滚正文)时,指针移动 1px 实际滚动多少 px。
</p>
<label class="field">
拖动倍率
<input
type="number"
id="chat-drag-gain"
min="0.5"
max="3"
step="0.1"
value="1.5"
/>
</label>
<p class="settings-help muted">
默认 1.5(拖 100px 走 150px);1 = 跟手;范围 0.5–3。保存后回到直聊刷新即生效。
</p>
<div class="storage-actions">
<button type="button" class="btn-primary" id="btn-save-drag-gain">保存倍率</button>
</div>
</div>
</section>
<section class="settings-section hidden" id="section-appearance">
<div class="settings-card storage-card">
<h3>主题</h3>
<p class="settings-help">
只改这台浏览器看到的配色,跟着本机走,不写进作品。点一张即生效。
</p>
<div class="appearance-themes" id="appearance-themes" role="radiogroup" aria-label="主题"></div>
</div>
<div class="settings-card storage-card">
<h3>字号</h3>
<p class="settings-help">
整体缩放界面文字,100% 为默认;范围 80–150%,步进 5%。
</p>
<div class="appearance-scale" role="group" aria-label="字号">
<button type="button" class="btn-secondary" id="appearance-scale-minus" aria-label="缩小字号">−</button>
<output class="appearance-scale-value" id="appearance-scale-value" aria-live="polite">100%</output>
<button type="button" class="btn-secondary" id="appearance-scale-plus" aria-label="放大字号">+</button>
<button type="button" class="btn-secondary" id="appearance-scale-reset">恢复默认</button>
</div>
<p class="settings-help muted">
同一浏览器的其他标签会自己跟上;回到别的页面刷新即生效。
</p>
</div>
</section>
</div>
</main>
</div>
<dialog id="preset-probe-dialog" class="probe-dialog">
<header class="probe-head">
<div>
<h3>试跑预设</h3>
<p class="probe-sub" id="probe-preset-name"></p>
</div>
<button type="button" class="btn-secondary" id="probe-close">关闭</button>
</header>
<div class="probe-body">
<section class="probe-composer">
<label>
本轮输入
<textarea id="probe-message" rows="4" placeholder="发给模型的这一轮"></textarea>
</label>
<details class="probe-extras">
<summary>试跑用设定 / 历史</summary>
<label>
历史前(设定)
<textarea id="probe-lore-before" rows="3"></textarea>
</label>
<label>
对话历史
<textarea id="probe-history" rows="4"></textarea>
</label>
<label>
历史后(状态)
<textarea id="probe-lore-after" rows="3"></textarea>
</label>
<label>
本轮后(可选)
<textarea id="probe-post-turn" rows="2"></textarea>
</label>
</details>
<div class="probe-composer-actions">
<button type="button" class="btn-primary" id="probe-send">发送一轮</button>
<p class="probe-status" id="probe-status" hidden></p>
</div>
</section>
<section class="probe-inspect" aria-live="polite">
<h4>发出的上下文</h4>
<div id="probe-messages" class="probe-messages">
<p class="probe-empty">发送后显示实际拼进请求的消息。</p>
</div>
<div id="probe-reply-wrap" class="probe-reply" hidden>
<h4>模型回复</h4>
<pre id="probe-reply"></pre>
</div>
</section>
</div>
</dialog>
<dialog id="profile-dialog" class="profile-dialog">
<form id="profile-form" method="dialog">
<h3 id="profile-dialog-title">新增 API</h3>
<label>
名称
<input name="name" required placeholder="例如 DeepSeek" autocomplete="off" />
</label>
<label>
Base URL
<input name="baseUrl" required placeholder="https://api.deepseek.com/v1" />
</label>
<label>
API Key
<input name="apiKey" type="password" placeholder="sk-…" autocomplete="off" />
</label>
<div id="profile-model-lock" class="model-lock" hidden>
<span class="model-field-label">型号</span>
<p id="profile-model-lock-value" class="model-lock-value"></p>
<span class="field-hint">型号是这一行的身份,探测跟着它走。要换型号,在组里点「添加型号」。</span>
</div>
<div id="profile-model-editor" class="model-field">
<label>
型号
<input name="model" placeholder="deepseek-chat" autocomplete="off" />
</label>
<span class="field-hint">保存的是这一格里的完整型号。下面的筛选只用来查找,点一条才会把完整名字填进来。</span>
<div class="model-field-row">
<input id="profile-model-filter" type="search" placeholder="筛选嗅探结果,如 gemini" autocomplete="off" aria-label="筛选嗅探结果" />
<button type="button" class="btn-secondary" id="profile-sniff">嗅探</button>
</div>
<p class="field-hint" id="profile-sniff-status" hidden></p>
<div id="profile-model-pick" class="model-pick">
<div id="profile-model-list" class="model-pick-list" role="listbox" aria-label="嗅探结果"></div>
<p class="model-pick-empty" id="profile-model-empty" hidden></p>
</div>
</div>
<label>
思考强度
<select name="reasoningEffort">
<option value="">默认(不发送)</option>
<option value="none">none · 关闭思考(带 tools 时自动用)</option>
<option value="low">low · 轻量</option>
<option value="medium">medium</option>
<option value="high">high · 增强</option>
<option value="max">max · 深度</option>
</select>
<span class="field-hint">只在 API 配置里设置,预设不再带思考强度。GLM-5.3:仅 low/high/max(none 会映射为 low),并附带 thinking.enabled;旧 GLM 只用 thinking、不发 reasoning_effort。Gemini 3.8:仅 low/medium/high,none 与 minimal 都会映射为 low,并附带 reasoning.effort。探测始终用 low。带 function tools 时:多数型号强制 none;GLM-5.3 与 Gemini 3 保留合法档。</span>
</label>
<div class="dialog-actions">
<button type="button" id="profile-cancel" class="btn-secondary">取消</button>
<button type="button" id="profile-save" class="btn-secondary">仅保存</button>
<button type="submit" class="btn-primary">保存并选用</button>
</div>
</form>
</dialog>
<dialog id="add-model-dialog" class="profile-dialog">
<form id="add-model-form" method="dialog">
<h3>添加型号</h3>
<p class="field-hint" id="add-model-group"></p>
<div class="model-field-row">
<input id="add-model-filter" type="search" placeholder="筛选嗅探结果,如 gemini" autocomplete="off" aria-label="筛选嗅探结果" />
<button type="button" class="btn-secondary" id="add-model-sniff">嗅探</button>
</div>
<p class="field-hint" id="add-model-status" hidden></p>
<div id="add-model-pick" class="model-pick">
<div id="add-model-list" class="model-pick-list" role="listbox" aria-label="嗅探结果"></div>
<p class="model-pick-empty" id="add-model-empty" hidden></p>
</div>
<span class="field-hint">点一条会新建一行并选用。组里已有同名型号时,直接选用那一行,不改写别的型号。</span>
<div class="dialog-actions">
<button type="button" id="add-model-cancel" class="btn-secondary">关闭</button>
</div>
</form>
</dialog>
<dialog id="persona-dialog" class="profile-dialog">
<form id="persona-form" method="dialog">
<h3 id="persona-dialog-title">新增用户角色</h3>
<label>
名称
<input name="name" required placeholder="例如 林晚,或创作默认用 @玩家" autocomplete="off" />
</label>
<label>
人设
<textarea name="description" rows="6" placeholder="性格、外貌、背景……(可空)"></textarea>
</label>
<label class="check-label">
<input type="checkbox" name="creationDefault" />
<span>
作为创作默认用户设定
<span class="field-hint">创作阶段填入预设的用户设定洞。建议名称写成 @玩家,这样开场白会自然用该占位,进游玩再换成当前角色。</span>
</span>
</label>
<div class="dialog-actions">
<button type="button" id="persona-cancel" class="btn-secondary">取消</button>
<button type="submit" class="btn-primary">保存并选用</button>
</div>
</form>
</dialog>
<dialog id="preference-dialog" class="profile-dialog" hidden>
<!-- 偏好 CRUD 已迁至 /libraries.html -->
</dialog>
<input type="file" id="preset-file" accept=".json,application/json" hidden />
<script src="/settings.js" type="module"></script>
</body>
</html>