import type { Config, SettingGroup } from "../config"; import { SETTINGS_GROUPS } from "../config"; import { THEME_CSS, escapeHtml, escapeAttr } from "../render/layout"; import type { UserRow, CachedModel } from "../store"; function fieldInput(group: SettingGroup, cfg: Config, models: CachedModel[]): string { return group.fields .map((f) => { const cur = String(cfg[f.key] ?? ""); if (f.type === "backend") { const opts = cfg.ttsBackends .map( (b) => `` ) .join(""); return ``; } if (f.type === "model") { // If the cache is empty (opencode not yet polled), fall back to a free // text input so the user can still type a known provider/model id. if (models.length === 0) { return ``; } // No empty "use opencode default" option: empty default lets env // vars pollute the model slot (see M-1 in index.ts refresh handler). // If cur is empty, the browser auto-selects the first option. const opts = models .map( (m) => `` ) .join(""); return ``; } const inp = f.type === "number" ? "number" : "text"; return ``; }) .join(""); } function buildDbSection(viewer: UserRow): string { if (viewer.is_admin !== 1) return ""; const driver = process.env.WORK_DB_DRIVER || "sqlite"; const isMysql = driver === "mysql"; const currentTarget = isMysql ? `${process.env.WORK_DB_HOST ?? "?"}:${process.env.WORK_DB_PORT ?? "3306"}/${process.env.WORK_DB_NAME ?? "?"}` : `SQLite · ${process.env.WORK_DB_PATH ?? "默认路径"}`; return `

数据库后端

当前后端: ${escapeHtml(driver.toUpperCase())} · ${escapeHtml(currentTarget)}

⚠ 切换到 MySQL 后 Web 进程会重启并以 MySQL 为存储。若 MySQL 不可达,进程无法启动——需手动编辑 .envWORK_DB_DRIVER 改回 sqlite 才能恢复。流程:先 ① 测试连接、再 ② 迁移数据、最后 ③ 启用。若你的 MySQL 账号无 CREATE 权限,先点 ⑤ 生成建表 SQL,用有权限的账号跑一遍,再回来 ② 迁移。
${isMysql ? `
` : ""}
`; } function buildDaemonSection(viewer: UserRow): string { if (viewer.is_admin !== 1) return ""; return `

Daemon 集群

运行中的 daemon 实例。新 daemon 通过 ework-aio add-daemon 启动;停止只标记为 drained,需手动结束进程。

加载中…
📡 SSH 远程部署
`; } export function buildSettingsPage(cfg: Config, saved: boolean, viewer: UserRow, models: CachedModel[], globalDispatchOff: boolean): { html: string } { const groups = SETTINGS_GROUPS.map( (g) => `

${escapeHtml(g.title)}

${fieldInput(g, cfg, models)}
` ).join(""); const banner = saved ? `
✓ 已保存,立即生效
` : ""; const ttsLink = viewer.is_admin === 1 ? `

要增删朗读后端(kokoro / cosyvoice3 等),去 朗读后端管理

` : ""; const modelRefreshForm = `

opencode 模型列表

opencode models 拉取可用模型并刷新上方下拉列表。刷新后自动选定一个具体模型作为默认(不会留空)。

`; const dispatchToggle = viewer.is_admin === 1 ? `

全局 AI 自动接单

关闭后,所有项目新建 issue 都不会自动派给 AI;评论仍可显式召唤。

` : ""; const html = ` ework-web · 设置

后台配置

改完保存立即生效,无需重启。密钥/启动项(token、端口等)仍在 .env,不在此处。

${banner}
${groups}
返回
${modelRefreshForm} ${dispatchToggle} ${ttsLink} ${buildDbSection(viewer)} ${buildDaemonSection(viewer)} ${viewer.is_admin === 1 ? buildGroupsSection() : ""}
`; return { html }; } function buildGroupsSection(): string { return `

Daemon 分组与路由策略

配置 daemon 分组和 webhook 派发策略。Router 自动摘除心跳超时的节点(120s)。

路由策略
ID节点状态分组

Repo → 分组绑定

分组工作目录与脚本

为每个分组配置工作目录模板和生命周期脚本。模板变量:{owner} {repo} {issue} {session}。脚本在 daemon 上以工作目录为 cwd 执行。

`; }