chore: Phase 3 收尾——平台设置页+全页冒烟+文档

Task 9(Phase 3 最后任务):
- 平台设置页实装(settings/platform):推荐键表单(ai.* 三键 /
  smtp.* 四键,键名为前端约定——PlatformSetting 是自由 KV 表)+
  存储总览表格;保存 = 逐键 PUT 只提交脏键(与 config 组整组替换
  不同);密钥键哨兵交互与凭据页同款(已设置占位 / 清除 / 恢复
  保持 / 端口 1-65535 校验);页头注记 resolver 回退链未接线、
  现阶段仅存储。
- api/settings.ts:fetchPlatformSettings / putPlatformSetting 实装
  (GET items 列表 / PUT 单键 upsert 形态钉在 docstring)。
- vitest +14(model 10 + api 4):掩码判定后端镜像、归一/脏检测/
  提交值/校验/展示全覆盖;前端基线 139 → 153。
- 全页面浏览器冒烟通过(登录→强制改密→仪表盘双视角→租户管理→
  会话与线索→配置中心全组→统计分析→平台设置→退出→租户管理员
  视角菜单收敛)。
- CLAUDE.md:新增 Phase 3 完成状态段(基线 595/153、本地起法、
  admin 能力一览、前端约定);Phase 3 清理单结项 7/11/12(掩码
  哨兵 / 站点权限 / push enabled 显式化);Phase 4 清理单增补四项
  (components.dts 行尾漂移 / 推送探测 env 回退真址 / 生产反代
  /api/public / VITE_TENANT_PREVIEW_HOST 单域名)。README 现状
  刷新至 Phase 3。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 23:29:31 +08:00
parent 230eb2d86c
commit 65aad53e12
7 changed files with 933 additions and 24 deletions
+78
View File
@@ -0,0 +1,78 @@
/**
* 平台设置 API 单元测试(Phase 3 Task 9)
*
* 钉住 API 层契约(事实源 app/platform/routes/config_routes.py 的
* settings_router):GET 列表 {items:[{key,value}]} / PUT 单键 upsert
* {key,value}——与 config 组的"整组替换"不同,KV 表逐键写。
* HTTP 层行为已在 client.test.ts 覆盖,此处 mock http 只断言调用形态。
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/api/client', () => ({
http: { get: vi.fn(), put: vi.fn() }
}))
import { http } from '@/api/client'
import {
SETTINGS_ENDPOINTS,
fetchPlatformSettings,
putPlatformSetting
} from '@/api/settings'
describe('平台设置 API(src/api/settings.ts)', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('fetchPlatformSettings:GET /api/platform/settings(超管端点)', async () => {
const body = {
items: [
{ key: 'ai.api_key', value: '__SET__' },
{ key: 'ai.model', value: 'glm-4-flash' }
]
}
vi.mocked(http.get).mockResolvedValue(body)
const res = await fetchPlatformSettings()
expect(res).toEqual(body)
expect(http.get).toHaveBeenCalledWith(SETTINGS_ENDPOINTS.list, undefined)
})
it('fetchPlatformSettings:透传 RequestOptions(内联错误展示)', async () => {
vi.mocked(http.get).mockResolvedValue({ items: [] })
await fetchPlatformSettings({ showErrorMessage: false })
expect(http.get).toHaveBeenCalledWith(SETTINGS_ENDPOINTS.list, {
showErrorMessage: false
})
})
it('putPlatformSetting:PUT 单键 upsert,请求体 {key, value}', async () => {
const response = { key: 'smtp.host', value: 'smtp.exmail.qq.com' }
vi.mocked(http.put).mockResolvedValue(response)
const res = await putPlatformSetting(
{ key: 'smtp.host', value: 'smtp.exmail.qq.com' },
{ showErrorMessage: false }
)
expect(res).toEqual(response)
expect(http.put).toHaveBeenCalledWith(
SETTINGS_ENDPOINTS.put,
{ key: 'smtp.host', value: 'smtp.exmail.qq.com' },
{ showErrorMessage: false }
)
})
it('putPlatformSetting:密钥哨兵原样提交(后端还原库值)', async () => {
vi.mocked(http.put).mockResolvedValue({ key: 'ai.api_key', value: '__SET__' })
await putPlatformSetting({ key: 'ai.api_key', value: '__SET__' })
expect(http.put).toHaveBeenCalledWith(
SETTINGS_ENDPOINTS.put,
{ key: 'ai.api_key', value: '__SET__' },
undefined
)
})
})
+40 -4
View File
@@ -1,15 +1,26 @@
/**
* 平台设置 API 骨架(Phase 3 Task 3)
* 平台设置 API(Phase 3 Task 3 骨架;Task 9 实装调用函数)
*
* 端点事实源:app/platform/routes/config_routes.py 的 settings_router
* (/api/platform/settings,仅超管:集团 AI 凭据 / 默认 SMTP 等)。
* 本文件只建类型与端点常量——调用函数在 Task 9(平台设置)实装。
*
* 密钥掩码:key 名含 key/password/secret 的非空字符串值在列表响应中
* 掩码为 '__SET__'(嵌套 dict 递归两层);PUT 提交 '__SET__' = 保持库值。
* 形态(GET 列表 / PUT 单键 upsert——与 config 组的"整组替换"不同,
* 平台设置是 KV 表,逐键写):
* - GET → {items: [{key, value}]}(按 key 升序;value 任意 JSON——
* 后端 Text 列 json.dumps 直存,读回还原类型)
* - PUT → body {key, value}:同 key 再写 = 整值替换;响应 {key, value}
* (掩码视图)
*
* 密钥掩码:key 名含 key/password/secret 的非空字符串值在响应中
* 掩码为 '__SET__'(嵌套 dict 递归);PUT 提交 '__SET__' = 保持库值。
* 表单语义转换见 src/api/mask.ts。
*
* 注意(CLAUDE.md Phase 3 取舍):resolver 的平台默认回退链**尚未**接入
* platform_settings——本端点现阶段只做存储与展示,引擎不读取这些值。
*/
import { http, type RequestOptions } from './client'
/** 平台设置项(列表响应行;value 可能是掩码哨兵或任意 JSON) */
export interface PlatformSettingItem {
key: string
@@ -32,3 +43,28 @@ export const SETTINGS_ENDPOINTS = {
list: '/api/platform/settings',
put: '/api/platform/settings'
} as const
/**
* 平台设置列表(GET /api/platform/settings,仅超管):按 key 升序的
* 全量 KV。value 为掩码视图——密钥键(key 名含 key/password/secret)
* 的非空字符串值已是 '__SET__' 哨兵,调用方经 model 层的 normalize
* 转表单态、经 formatSettingValue 做展示,绝不久存原文。
*/
export function fetchPlatformSettings(
config?: RequestOptions
): Promise<PlatformSettingsResponse> {
return http.get<PlatformSettingsResponse>(SETTINGS_ENDPOINTS.list, config)
}
/**
* upsert 单个设置项(PUT /api/platform/settings):同 key 再写 = 整值
* 替换(无部分合并)。密钥键提交 '__SET__' 哨兵 = 保持库值(后端还原,
* 哨兵字面量绝不落库)。响应 {key, value} 为掩码视图。
* 本页按"仅提交脏键"调用——未改动 的键不发请求。
*/
export function putPlatformSetting(
body: PutPlatformSettingRequest,
config?: RequestOptions
): Promise<PlatformSettingItem> {
return http.put<PlatformSettingItem>(SETTINGS_ENDPOINTS.put, body, config)
}