4 Commits
Author SHA1 Message Date
sun ad342ec8f1 ci: 新增 build/vet/test 质量门
CI / build-and-test (push) Canceled after 0s
此前仓库唯一的 workflow 只在打 tag 时构建 Docker 镜像,不做任何编译或测试校验,加上测试源码不入库,导致 go test 全绿从来没有被自动化校验过。

新增 ci.yml:push / PR 到 develop、main 时跑 go build ./... / go vet ./... / go test ./... -count=1,再单跑一遍集成测试并输出明细。版本以 go-version-file 跟随 go.mod;-count=1 禁用测试缓存。
2026-10-03 16:18:54 +08:00
sun 24f8accf71 chore: 测试源码不入库,订正过期的 UI handoff 文档与 CHANGELOG
.gitignore 恢复整体屏蔽 *_test.go:测试只保留在本地磁盘供开发者执行,不作为交付物外流。因此仓库内不提供自动化测试。

docs/UI_HANDOFF.md 标注为历史文档:它是改造两个单文件 .html、admin <= 35KB 的外包任务书,而 admin 后台此后已重构成多页 SPA(admin-shell.js / admin.css + 登录/音色/设置三页),原约束与验收标准全部失效。补上当前真实文件结构与新的验收清单。
2026-10-03 16:13:47 +08:00
sun 6ac2473099 fix(volcano): PostStream 对未初始化 client 返回错误而不是 panic
Synthesis 里 client 是包级 volcanoClient,只在启动时赋值。不经过 main 的调用路径拿到 nil client 时,(*HTTPClient).PostStream 在 h.client.Do 处 nil 解引用崩溃。

装配错误应该让这一次请求以上游不可用失败:controller 会归一成 5xx 并记日志,而不是拖垮整个进程。
2026-10-03 16:13:47 +08:00
sun 1e2be0cd5b fix(telemetry): 指标写入对空接收者安全,修 nil 解引用 panic
metrics 包的全局指标(UpstreamTotal 等)默认是 nil,只有 main 调过 metrics.Init() 之后才有值。任何不经过 main 的调用路径都会在 controller/tts.go -> adapter/volcano/synthesis.go -> metrics.AdapterRecorder 处 nil 解引用,整个进程崩掉。

生产路径本来没问题(二进制一定先 Init),但这类崩溃只在运行时暴露,且漏掉指标初始化属于很容易踩的装配错误,不该以 panic 收场。

改动: Counter.Add / Gauge.Set / Gauge.Add / Histogram.Observe 增加空接收者判断,nil 时静默忽略(与 noop 语义一致);在包注释里写成显式设计约定,避免以后被当成冗余代码删掉。
2026-10-03 16:13:47 +08:00
9 changed files with 476 additions and 9 deletions
+61
View File
@@ -0,0 +1,61 @@
name: CI
# 触发条件:
# - push 到 develop / main
# - 针对 develop / main 的 PR
#
# 为什么单独有这个 workflow:
# 之前仓库里唯一的 workflow 是 docker.yml,它只在打 tag 时构建镜像,
# 既不做 go build 也不跑 go vet,提交能不能编译没人自动盯着。
#
# ⚠️ 本 workflow 的实际覆盖范围(务必知悉):
# 测试源码按项目策略**不入库**(见 .gitignore 的 `*_test.go`),因此干净克隆里
# 没有任何 *_test.go,下面的 go test 步骤会打印 "no test files" 后通过。
# 也就是说:**CI 只能守住"编译通过 + 静态检查通过",守不住行为回归。**
# 行为回归要靠开发者在本地(测试文件所在处)自行执行:
# go test ./... -count=1
# 如果哪天测试重新入库,把下面的 go test 步骤改成会真正跑起来的形态即可。
on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
# 同一分支的重复推送取消上一次未完成的运行,省 CI 时间。
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
# 以 go.mod 里的 go 指令为准,避免两处版本漂移
go-version-file: go.mod
cache: true
- name: 下载依赖
run: go mod download
- name: 编译检查
run: go build ./...
- name: 静态检查
run: go vet ./...
# 以下两步在"测试不入库"策略下是空转(no test files),保留是为了:
# 1. 覆盖 lock 判定 / 损坏回退 / voice 路由等关键路径的测试一旦重新入库,
# 无需改 workflow 即可直接生效;
# 2. -count=1 禁用测试缓存,避免"改坏了却因缓存仍显示通过"。
- name: 测试(测试源码不入库时为空转)
run: go test ./... -count=1
- name: 关键路径汇总(integration)
run: go test ./test/integration/ -count=1 -v
+12 -7
View File
@@ -4,7 +4,10 @@
*.test
*.out
# Go test sources (prevent accidental commit; tests live outside the repo by policy)
# Go 测试源码:不入库(测试保留在本地磁盘,由开发者自行 go test ./... -count=1)
# 理由:测试用例会暴露内部实现细节与断言,不作为交付物外流。
# 注意:本规则会把测试文件挡在 git 之外,因此仓库内不提供自动化测试;
# .github/workflows/ci.yml 里的测试步骤在纯净克隆上无测试可跑。
*_test.go
# Editor / OS
@@ -33,14 +36,16 @@ bin/
# Local dev scripts (per-user; not part of repo)
start-local.ps1
# Windows / PowerShell 脚本:一律不入库、不外流
# ui-handoff/serve.ps1 与 start-local.ps1 是本地起服便利脚本(含本机路径与端口),
# 不属于交付物。这两个文件在文件系统层面删除被拒,因此只能在 git 层屏蔽。
*.ps1
# UI handoff bundle(改造前的单文件副本 + 离线 mock 预览;仅本地对照,不部署)
ui-handoff/
# Runtime database artifacts (created by M1 installer flow)
tts.db
tts.db-*
tts.db.*
installed.lock
# UI handoff bundle (for external AI redesign work; not deployed)
ui-handoff/
# Docs (本地草稿,不入库)
docs/
+30
View File
@@ -2,6 +2,36 @@
本项目的所有重要变更都记录在本文档。版本号遵循 [SemVer](https://semver.org/)。
## [未发布]
### 修复
- **指标空指针崩溃**: `metrics` 包的全局指标(`UpstreamTotal` 等)默认是 nil,只有 `main` 调过
`metrics.Init()` 后才有值。任何**不经过 main** 的调用路径(直接调 handler、复用为库)都会在
`controller/tts.go` → `adapter/volcano/synthesis.go` → `metrics.AdapterRecorder` 处 nil 解引用 panic。
现在 `telemetry` 的 `Counter.Add` / `Gauge.Set` / `Gauge.Add` / `Histogram.Observe` 一律
**空接收者安全**(nil 静默忽略),并在包注释里写成显式设计约定。
- **上游 client 空指针**: `volcano.(*HTTPClient).PostStream` 在 client 未初始化时 panic。
现在返回错误,由 controller 归一成 5xx 并记日志——装配错误不该拖垮进程。
> 上面两个崩溃是**本地测试暴露出来的**:直接调用 handler 而不经过 `main` 的路径,
> 会跳过 `metrics.Init()` 与 `volcanoClient` 的赋值。生产二进制不受影响。
### 变更
- **测试源码不再入库**: `.gitignore` 恢复整体屏蔽 `*_test.go`。测试用例会暴露内部实现
细节与断言,不作为交付物外流;测试文件保留在本地磁盘,由开发者自行执行
`go test ./... -count=1`。
⚠️ 代价必须明确:仓库内**不提供自动化测试**,`.github/workflows/ci.yml` 里的
`go test` 步骤在纯净克隆上无测试可跑(会打印 `no test files` 并通过),
**CI 只能守住"编译通过 + 静态检查通过",守不住行为回归**。
- **新增 CI 校验** (`.github/workflows/ci.yml`): push / PR 到 `develop`、`main` 时跑
`go build` + `go vet` + `go test -count=1`。此前仓库唯一的 workflow 只在打 tag 时构建
Docker 镜像,不做任何编译或测试校验。
- **文档订正**: `docs/UI_HANDOFF.md` 原为"单文件换皮"外包任务书,其中的
"admin.html ≤ 35KB / setup.html ≤ 15KB / 只改两个 .html" 等约束在 admin 拆分多页后已作废,
现标注为历史文档并补上当前真实文件结构与验收清单。
## [0.2.0] - 2026-08-30
### 新增
+7
View File
@@ -29,7 +29,14 @@ func NewHTTPClient() *HTTPClient {
// PostStream 发送一次流式请求,返回带上下文的 *http.Response。
// 调用方负责关闭 resp.Body。
//
// 空接收者(nil *HTTPClient)安全:返回错误而不是 panic。未初始化的 client 属于
// 装配错误,应当让该次请求以"上游不可用"失败(controller 归一成 5xx 并记日志),
// 而不是让整个进程崩掉。
func (h *HTTPClient) PostStream(ctx context.Context, url string, headers map[string]string, body []byte) (*http.Response, error) {
if h == nil || h.client == nil {
return nil, fmt.Errorf("volcano: HTTPClient 未初始化,无法发起上游请求")
}
if ctx == nil {
ctx = context.Background()
}
+343
View File
@@ -0,0 +1,343 @@
# UI 外包 Handoff · 火山 TTS 管理后台
> 历史文档。本文档描述的是**外包给 Codex 做视觉改造**那一次任务,目标是"现有功能完全不动, 只换皮"。
>
> ⚠️ **状态: 任务已结束, 且路线已变更。** 该任务之后, admin 后台由提交 `9245c13`
> 自行重构成**多页 SPA**(见下面 §0.1),本文档正文里的"单文件 / 大小上限 / 改这两个 .html"
> 等约束**均已失效**,保留仅为记录当时的交接内容与设计讨论。
> 若要再做视觉改造,请以 §0.1 的当前文件结构为准。
---
## 0.1 当前真实结构(以此为准)
admin 后台已从单文件拆分为多页面,共享一套 CSS/JS,由 `//go:embed` 逐条嵌入:
| 文件 | 作用 | 大小 |
|---|---|---|
| `router/admin.html` | 后台外壳(导航 / 布局) | 5.5KB |
| `router/admin-login.html` | 登录页 | 2.1KB |
| `router/admin-voices.html` | 音色管理页 | 13.5KB |
| `router/admin-settings.html` | 设置页 | 14.4KB |
| `router/admin.css` | 共享样式(全部页面) | 20.2KB |
| `router/admin-shell.js` | 共享脚本: axios 实例 / toast / 外壳挂载 / 401 拦截 | 7.1KB |
| `router/setup.html` | 安装引导页(仍是单文件) | 24.0KB |
- URL 路由(非 hash):`/admin` · `/admin/login` · `/admin/voices` · `/admin/settings`
- 公共逻辑集中在 `admin-shell.js`,通过 `window.admin` 暴露
`{ http, getKey, setKey, clearKey, toast, mountShell, formatUptime, … }`
- **已无"单文件 ≤ 35KB"这类约束**;新增页面请复用 `admin.css` + `admin-shell.js`,不要各写一套。
---
## 0. 任务概览(历史原文)
| 项 | 说明 |
|---|---|
| 改的文件 | `router/admin.html`、`router/setup.html` |
| 改的部分 | 视觉、布局、组件、交互模式(loading/empty/error/反馈)|
| 不改的部分 | API 调用、状态管理逻辑、字段名、文案语义 |
| 期望产物 | 改完后的两个 .html, 总大小 ≤ 64KB(admin ≤ 35KB, setup ≤ 15KB)|
| 验收 | dev 起服, 浏览器走完所有流程无回归 + 视觉明显更好 |
---
## 1. 技术栈约束(历史原文;"单文件"部分已失效)
| 项 | 现状 | 改动空间 |
|---|---|---|
| 构建 | **无构建步骤**(`//go:embed` 进 binary)| 不可改 |
| Vue | 3.4 via `cdn.bootcdn.net` | 可换版本(保持 3.x 即可)|
| HTTP | axios 1.6(集中在 `admin-shell.js`)| 可换(fetch / ky / 留 axios 都行)|
| CSS | 手写, 走 CSS 变量, 集中在 `admin.css` | 可换 **Tailwind CDN / DaisyUI / Pico / 手写** 都行 |
| 图标 | 暂无 | 可加 lucide / heroicons inline SVG / emoji |
| 字体 | 系统默认 (`-apple-system, "PingFang SC"...`) | 可加 `Noto Sans SC` via Google Fonts, 但要评估大小 |
| 路由 | 拆分后已改为 **URL 路由**(`/admin/voices` 等),不再是 hash 三 tab | 保持 URL 路由 |
**注意(历史约束,已不适用)**: 当时二进制大小敏感 —— admin.html 31.19KB / setup.html 13.01KB。拆分后 admin 侧总量约 63KB(分散在 6 个文件里,浏览器可分页只加载所需部分),embed 进 binary 后大小影响可接受。**仍然建议**: 不要无脑加 UI 库(Tailwind 全量 + DaisyUI 会显著增大 embed 体积);要加就加在 `admin.css` 一处,所有页面共享。
---
## 2. 路由 & 模式
### 2.1 三个 URL
- `GET /setup` → `router/setup.html`(安装模式才进得去)
- `GET /admin` → `router/admin.html`(任何模式都返回 HTML, 鉴权由前端 JS 拦截)
- `GET /` → 重定向到 `/setup` 或 `/admin`
### 2.2 状态机(admin.html)
```
未登录 (无 sessionStorage 'ttsAdminKey')
├─ 输入 key + 调 GET /api/admin/overview 验证
└─ 验证通过 → 写入 sessionStorage + 切到登录后视图
已登录
├─ 三个 tab: dashboard / voices / settings(hash 路由)
├─ 调任意 API 返 401 → 清 sessionStorage + 回到登录页
└─ 改完自己 auth_key → 调 PUT /api/settings/auth-key 200 后清 sessionStorage
```
### 2.3 状态机(setup.html)
```
未提交: 表单 4 必填 + voices 至少 1 条
└─ onMounted 调 GET /api/setup/prefill 自动填默认值
└─ 提交 POST /api/setup, 成功后 location.href = '/admin'
```
---
## 3. API 契约(前端用的全部)
### 3.1 鉴权
| 头 | 说明 |
|---|---|
| `Authorization: Bearer <key>` | 所有 /api/* 请求必带(除 /api/setup/*)|
| 401 响应 | 清 sessionStorage `ttsAdminKey`, 回到登录页 |
**axios 实例已配置**:
```js
const http = axios.create({ baseURL: '/api' });
http.interceptors.request.use(c => {
if (apiKey.value) c.headers.Authorization = 'Bearer ' + apiKey.value;
return c;
});
http.interceptors.response.use(r => r, err => {
if (err.response && err.response.status === 401) {
sessionStorage.removeItem('ttsAdminKey');
apiKey.value = '';
}
return Promise.reject(err);
});
```
### 3.2 端点
| Method | Path | 请求 body | 响应 | 鉴权 |
|---|---|---|---|---|
| GET | `/api/setup/prefill` | — | `{ settings: {default_resource_id, default_speaker, default_format, sample_rate} }` | 否(仅 install 模式)|
| POST | `/api/setup` | `{token, settings: {auth_key, api_key, default_resource_id, default_speaker, default_format, sample_rate}, voices: [{name, speaker, resource_id, model, language}]}` | `{ok, redirect, settings, voices}` | 否(仅 install 模式)|
| GET | `/api/admin/overview` | — | `{mode, installed, db_path, lock_path, version, commit, uptime_seconds, start_time, voice_count, voice_enabled_count, memory: {goroutines, heap_alloc, heap_inuse}}` | 是 |
| GET | `/api/admin/metrics` | — | Prometheus text | 是 |
| GET | `/api/voices` | — | `{voices: [{ID, Name, Speaker, ResourceID, Model, Language, Description, Enabled, CreatedAt, UpdatedAt}]}` | 是 |
| POST | `/api/voices` | `{name, speaker, resource_id, model, language?, description?}` | 创建的 voice 对象 | 是 |
| DELETE | `/api/voices/{name}` | — | `{deleted, ok}` | 是 |
| PATCH | `/api/voices/{name}/toggle` | `{enabled: bool}` | 更新后的 voice | 是 |
| GET | `/api/settings` | — | 完整 settings 对象(见 3.3)| 是 |
| PUT | `/api/settings` | 部分字段(`default_resource_id` / `default_speaker` / `default_format` / `sample_rate` / `model`)| `{ok, updated, updated_at}` | 是 |
| PUT | `/api/settings/api-key` | `{api_key: string}` | `{ok}` | 是 |
| PUT | `/api/settings/auth-key` | `{auth_key: string}` | `{ok}` | 是 |
| PUT | `/api/settings/cors` | `{allow_all?: bool, origins?: string}` | `{ok, allow_all, origins, cors_active}` | 是 |
### 3.3 GET /api/settings 完整响应
```json
{
"api_key": "volc****etup", // 打码
"api_key_set": true,
"auth_key": "my-o****etup", // 打码
"auth_key_set": true,
"cors_allow_all": false,
"cors_origins": "https://app.example.com\nhttps://other.com",
"cors_configured": true, // banner 显示控制
"default_resource_id": "volc.megatts.icl",
"default_speaker": "qian",
"default_format": "mp3",
"sample_rate": 24000,
"model": "seed-tts-2.0-standard",
"model_type": 0,
"explicit_language": "",
"enable_subtitle": false,
"updated_at": "2026-08-29T16:07:49Z"
}
```
### 3.4 错误响应格式
所有错误(除 OpenAI 兼容的 /v1/audio/speech)用同一格式:
```json
{ "error": { "code": "bad_request", "message": "...", "type": "invalid_request_error" } }
```
前端用 `e.response?.data?.error?.message` 提取.
---
## 4. 字段语义(写文案 / placeholder 用)
### 4.1 auth_key
"OpenAI 鉴权 Key"
- 客户端用这个 Key 调 `/v1/audio/speech`
- 登录 `/admin` 也用这个 Key
- 自定字符串, 不是火山给的, **完全自己造**
- 例: `my-secret-key-2024`
### 4.2 api_key
"火山 TTS API Key" / "上游 API Key"
- 调火山 v3 上游用的凭证
- 从 https://console.volcengine.com/ 获取
- 例: `5b4d7c2a-...`
### 4.3 default_resource_id
- 音色所属的计费资源 ID
- `volc.megatts.default` 默认音色 / `volc.megatts.icl` 复刻音色
### 4.4 default_speaker
- 未传 `voice` 字段时用
- 必须是"音色" tab 里已添加的 voice name
### 4.5 voices 字段
- `name`: 短英文 id, 唯一, 不可改
- `speaker`: 火山给的 speaker id
- `resource_id`: 同上 default_resource_id
- `model`: `seed-tts-2.0-standard` (默认) / `seed-tts-2.0-expressive` / `seed-tts-2.0-clone`
- `language`: ISO 639-1 (`zh` / `en` / `ja` 等)
### 4.6 CORS origins
- 一行一个 origin
- 必须 `http://` 或 `https://` 开头
- 末尾 `/` 自动 trim
---
## 5. 当前 UI 的具体问题(按痛感排序)
### 5.1 admin.html
| # | 问题 | 现状 | 期望 |
|---|---|---|---|
| 1 | **登录页太空** | 一个输入框 + 一个按钮 | 加 logo / 提示 / 服务名 / 安装状态提示 |
| 2 | **dashboard 卡片平** | 4 个数字 + 路径文本, 无图标无图表 | 加 icon, 用 chart mini sparkline (uptime / memory)|
| 3 | **voices 表格简陋** | 5 列 + 启停 switch + 删除 | 加排序/搜索/批量操作/分页/拖拽改顺序 |
| 4 | **新增 voice 弹窗原始** | 6 个 input 堆叠 | 分组, 加 help text, 加示例值, 加实时校验 |
| 5 | **设置页 1 长页** | 4 段串成 1 长 list | 分 tab/分卡片, 加描述/帮助链接/危险操作分离 |
| 6 | **改 key 后无 feedback** | 改了直接清 session | 加 toast 通知 "Key 已更新, 请用新 Key 重新登录" |
| 7 | **删除无 undo** | confirm 弹原生 confirm() | 加 inline 二次确认 / undo snackbar |
| 8 | **loading 无** | 调 API 时按钮不变 | 加 spinner / disabled 状态 / 进度条 |
| 9 | **error 无专门显示区** | 用 `actionErr.value` 全局串 | 用 toast / inline error / 错误码对照 |
| 10 | **响应式差** | desktop only | 至少平板能用, 关键操作 (tab 切换) 移动端可点 |
| 11 | **配色单调** | 1 个 cyan 强调色 | 加二级色, success/warning/danger 用得不够明显 |
| 12 | **字体单薄** | 14px 全局 | 大字标题 24-32px, 重要数字 monospace 突出 |
### 5.2 setup.html
| # | 问题 | 现状 | 期望 |
|---|---|---|---|
| 1 | **4 字段加 voice 列表一锅烩** | 一长页 | 分步骤 (1.凭证 2.默认 3.音色 4.确认) |
| 2 | **voice 行无复用 UI** | 自己手写的 row | drag-to-reorder + 复制/粘贴/从预置模板加 |
| 3 | **无进度感** | 一个 "安装" 按钮 | 安装中显示进度 (写 settings → 写 voices → 写 lock) |
| 4 | **无"安装后会发生什么"** | 提交后只跳 /admin | 加结果页: 装了什么 / 接下来去 admin 干啥 |
| 5 | **无 reset** | 改了一锅填错没法批量重置 | "恢复默认值" 按钮 |
| 6 | **移动端糟糕** | 1 长页 | 至少 step indicator 在窄屏可见 |
### 5.3 共性问题
| # | 问题 | 期望 |
|---|---|---|
| 1 | 无 dark/light toggle | 加 toggle, 默认跟随系统 |
| 2 | 无键盘快捷键 | `/` 聚焦搜索, `Esc` 关弹窗, `Ctrl+S` 保存当前表单 |
| 3 | 无国际化 | 默认 zh-CN, 可加 i18n 但不强制 |
| 4 | 无 a11y 标签 | aria-label, focus ring, 键盘可达 |
---
## 6. 设计参考(建议风格, 不是强制)
| 风格 | 适配场景 | 推荐度 |
|---|---|---|
| Linear / Vercel dashboard | 后台工具, dark mode 友好 | ⭐⭐⭐⭐⭐ |
| Tailscale admin | 简洁, 卡片化, 信息密度高 | ⭐⭐⭐⭐ |
| Coolify / CasaOS | 自托管, "家用服务器"感 | ⭐⭐⭐ |
| shadcn/ui | 现代 React 风, 移植到 Vue 略费劲 | ⭐⭐⭐ (参考设计, 不直接用) |
| Tailadmin | 通用 admin 模板, 选段借鉴 | ⭐⭐ |
**主调色建议** (如果重做):
- 背景: 深色 (`#0a0e1a`) 或浅色 (`#f8fafc`), 配 system pref
- 强调色: cyan / indigo / emerald 之一
- 字体: 标题用 Inter, 数字用 JetBrains Mono
---
## 7. 不要做的事
- ❌ 改 API 路径 / 改字段名 / 改字段语义
- ❌ 加任何状态管理库 (Pinia) — 单文件 setup() 够用
- ❌ 加 webpack/vite — 必须保持单文件
- ❌ 引大 UI 库 (Element Plus / Naive UI) — 单文件塞不下
- ❌ 把 admin.html 和 setup.html 合并 — 它们是独立页面
- ❌ 改 axios 实例的 baseURL 或 401 拦截逻辑
- ❌ 改 hash 路由的 key 命名(`#dashboard` / `#voices` / `#settings`)
- ❌ **不要把单文件架构当最终形态** — 这是最小可工作版本(MVP),看板有 `ui-arch-debt` 卡片记录拆分计划。Codex 改造时保持单文件以便本次走通,但**不要引入新依赖让单文件更难拆**。例如:不要把 axios 拦截器写得依赖 10 个全局 ref;不要让 CSS 选择器深嵌套到无法抽离。
---
## 8. 验收清单(已按拆分后的结构更新)
改完回来, 我会逐项过:
- [ ] `go build ./...` 通过
- [ ] `go vet ./...` 通过
- [ ] `go test ./... -count=1` 全绿
- [ ] dev 起服后浏览器:
- [ ] `/setup` 流程能跑完
- [ ] `/admin` 登录 → dashboard → voices → settings → 增改删全通
- [ ] 改 auth_key 后自踢 → 重新登录 → 能用新 key
- [ ] CORS 未配时顶部 banner 出现, 配后消失
- [ ] 401 错误时自动跳回登录
- [ ] 视觉: 跟现在的 admin 比有明显提升 (logo / 排版 / 颜色 / 反馈)
- [ ] 至少在 1024px 宽度下好看
- [ ] 没有 console error / 404
- [ ] 新增页面复用了 `admin.css` + `admin-shell.js`,没有复制粘贴出第二套公共逻辑
> 旧的"`admin.html` ≤ 35KB / `setup.html` ≤ 15KB"两条**已作废**:admin 已拆成多页,
> 单文件大小不再是约束。
---
## 9. 回填方式(历史原文) / 当前做法
历史原文:
```
1. 拿回 router/admin.html + router/setup.html
2. 放到 D:\项目\Volcano-Engine-TTS-UI\router\
3. 我 build + 测, 通过后 commit
4. push, 服务器自动部署
```
拆分之后的做法:改动落在 §0.1 表格里对应的**具体文件**上(外壳/登录/音色/设置,以及共享的
`admin.css` / `admin-shell.js`),不再有"两个 .html"这一说。改完在**项目里**跑:
```
go build ./...
go vet ./...
go test ./... -count=1
```
三条全过再 commit 到 `develop`。
---
## 10. 关联文件(不需要改, 但可读懂逻辑)
- `controller/admin.go` — admin API 实现
- `controller/settings.go` — settings API 实现
- `controller/setup.go` — setup API 实现
- `router/router.go` — Go 路由 + 挂载点(看 `//go:embed` 用法)
- `router/admin-shell.js` — 共享 axios 实例 / toast / 外壳挂载(改交互先看这里)
- `middleware/admin_auth.go` — RequireAdmin 实现(理解 401 怎么来)
---
## 11. 当前文件位置(已更新)
| 文件 | 大小 |
|---|---|
| `router/admin.html` | 5.5KB |
| `router/admin-login.html` | 2.1KB |
| `router/admin-voices.html` | 13.5KB |
| `router/admin-settings.html` | 14.4KB |
| `router/admin.css` | 20.2KB |
| `router/admin-shell.js` | 7.1KB |
| `router/setup.html` | 24.0KB |
`ui-handoff/` 目录里保留的是**改造前**的单文件副本与离线 mock 预览,仅供对照,不要回填。
+5 -1
View File
@@ -36,8 +36,12 @@ func newCounter(name, help string, labelNames []string) *Counter {
func (c *Counter) Inc(labels Labels) { c.Add(1, labels) }
// Add 累加 v(v 必须 >= 0)。
//
// 空接收者(nil *Counter)安全:未初始化的全局指标(如 metrics.Init() 尚未调用时
// 的 metrics.UpstreamTotal)会被静默忽略,而不是 panic。
// 这让"直接调用 handler"的测试/复用场景不会因为漏掉指标初始化而崩溃。
func (c *Counter) Add(v float64, labels Labels) {
if v < 0 {
if c == nil || v < 0 {
return
}
child := c.getOrCreate(labels)
+8
View File
@@ -33,7 +33,11 @@ func newGauge(name, help string, labelNames []string) *Gauge {
}
// Set 直接设置当前值。
// 空接收者(nil *Gauge)安全,见 Counter.Add 的说明。
func (g *Gauge) Set(v float64, labels Labels) {
if g == nil {
return
}
child := g.getOrCreate(labels)
child.bits.Store(float64bits(v))
}
@@ -45,7 +49,11 @@ func (g *Gauge) Inc(labels Labels) { g.Add(1, labels) }
func (g *Gauge) Dec(labels Labels) { g.Add(-1, labels) }
// Add 累加 v(可负)。
// 空接收者(nil *Gauge)安全,见 Counter.Add 的说明。
func (g *Gauge) Add(v float64, labels Labels) {
if g == nil {
return
}
child := g.getOrCreate(labels)
for {
bits := child.bits.Load()
+4
View File
@@ -47,7 +47,11 @@ func newHistogram(name, help string, buckets []float64, labelNames []string) *Hi
}
// Observe 记录一个观测值。
// 空接收者(nil *Histogram)安全,见 Counter.Add 的说明。
func (h *Histogram) Observe(v float64, labels Labels) {
if h == nil {
return
}
child := h.getOrCreate(labels)
for {
bits := child.sumBits.Load()
+6 -1
View File
@@ -5,7 +5,12 @@
// - 零外部依赖,只使用标准库;
// - label key 在指标注册时锁定,运行期不可新增(避免 cardinality 爆炸);
// - 所有并发安全由实现保证,调用方无需加锁;
// - Meter 是高层入口,NoopMeter 用于测试。
// - Meter 是高层入口,NoopMeter 用于测试;
// - **空接收者安全**: 未初始化的 *Counter / *Gauge / *Histogram(即 nil 指针)
// 上的 Inc/Add/Set/Observe 一律静默忽略,不 panic。
// 这条约定是为 metrics 包的全局指标变量服务的 —— 它们默认为 nil,只有 main
// 调用过 metrics.Init() 之后才有值;直接复用 handler 的场景(集成测试、
// 未来做成库)不应该因为漏掉指标初始化而崩掉整个请求处理。
package telemetry
import (