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:
@@ -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
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user