fix: Phase3 终审修复(env入库/通用配置组面板/锁屏通知摘除/记档修正)
- I-2: admin/.env(被根 .gitignore 误伤)删除,内容并入入库的 .env.development/.env.production(公共块两份都写,PORT 仅 dev) - I-1: 配置中心补 contact/engine/misc 三组通用 JSON 编辑面板 (JsonGroupPane:等宽 textarea 整组编辑 + JSON.parse 预检 + PUT 整组 + 重置出厂 + 顶部字段说明;挂进 GROUP_NAV 三入口); 修组键缺失租户的禁存 bug(父层快照加载后挂载,空组可建组) - I-3: config_routes.py docstring 改「接线归 Phase 4」; CLAUDE.md Phase 4 清理单补 resolver 回退链与 email/push test 限流 - M-1: 锁屏摘除入口(用户菜单项删 + 全局组件 enabled:false) - M-2: 顶栏通知铃铛隐藏(headerBar notification.enabled:false) - M-5/M-8: admin/README 换三行指针(删模板 README.zh-CN.md); quickLinks 删「登录」项 - M-3/M-4 不做记档入 Phase 4 清理单 - 测试基线:后端 pytest 595 / 前端 vitest 156(+3 通用 JSON 组) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -37,10 +37,8 @@
|
||||
<ArtSvgIcon icon="ri:key-2-line" />
|
||||
<span>{{ $t('topBar.user.changePassword') }}</span>
|
||||
</li>
|
||||
<li class="btn-item" @click="lockScreen()">
|
||||
<ArtSvgIcon icon="ri:lock-line" />
|
||||
<span>{{ $t('topBar.user.lockScreen') }}</span>
|
||||
</li>
|
||||
<!-- 锁屏入口已摘除(Phase 3 终审 M-1:AES 存 localStorage + 公开模板
|
||||
默认密钥,防御价值为零;组件代码保留,Phase 4 决定删否) -->
|
||||
<div class="w-full h-px my-2 bg-g-300/80"></div>
|
||||
<div class="log-out c-p" @click="loginOut">
|
||||
{{ $t('topBar.user.logout') }}
|
||||
@@ -57,7 +55,6 @@
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useAuthStore } from '@/store/modules/auth'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
|
||||
defineOptions({ name: 'ArtUserMenu' })
|
||||
|
||||
@@ -77,13 +74,6 @@
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开锁屏功能
|
||||
*/
|
||||
const lockScreen = (): void => {
|
||||
mittBus.emit('openLockScreen')
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出确认
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,10 @@ export const globalComponentsConfig: GlobalComponentConfig[] = [
|
||||
component: defineAsyncComponent(
|
||||
() => import('@/components/core/layouts/art-screen-lock/index.vue')
|
||||
),
|
||||
enabled: true
|
||||
// 入口已摘除(Phase 3 终审 M-1):锁屏把真实密码 AES 存 localStorage
|
||||
// 且密钥是公开模板默认值,防御价值为零——enabled: false 连全局挂载
|
||||
// 也不注册;组件代码保留,Phase 4 决定删否。
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
name: '聊天窗口',
|
||||
|
||||
@@ -46,18 +46,12 @@ const fastEnterConfig: FastEnterConfig = {
|
||||
routeName: 'StatsOverview'
|
||||
}
|
||||
],
|
||||
// 快速链接
|
||||
// 快速链接(登录项已删——终审 M-8:已登录态下无意义)
|
||||
quickLinks: [
|
||||
{
|
||||
name: '登录',
|
||||
enabled: true,
|
||||
order: 1,
|
||||
routeName: 'Login'
|
||||
},
|
||||
{
|
||||
name: '修改密码',
|
||||
enabled: true,
|
||||
order: 2,
|
||||
order: 1,
|
||||
routeName: 'ForceChangePassword'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -39,7 +39,9 @@ export const headerBarConfig: HeaderBarFeatureConfig = {
|
||||
description: '全屏切换功能'
|
||||
},
|
||||
notification: {
|
||||
enabled: true,
|
||||
// 入口已摘除(Phase 3 终审 M-2):模板通知中心是硬编码演示数据,
|
||||
// 无真实数据源——enabled: false 隐藏铃铛;组件保留,接真数据源时再开。
|
||||
enabled: false,
|
||||
description: '通知中心,显示系统通知和消息'
|
||||
},
|
||||
chat: {
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<!--
|
||||
通用 JSON 组编辑(Phase 3 终审 I-1:contact / engine / misc 三组)
|
||||
- spec §7.3 要求配置中心覆盖全部配置组——这三组字段少 / 无密钥 /
|
||||
结构低频变更,表单化收益低,统一走等宽 textarea 整组 JSON 编辑;
|
||||
- 保存前 JSON.parse 预检(语法 + 顶层须为对象),类型/边界交后端
|
||||
_check_contact/_check_engine/_check_misc 校验兜底(400 detail 带
|
||||
字段路径,内联展示);
|
||||
- 重置出厂接既有 reset.ts(确认框 → POST config/{group}/reset →
|
||||
emit saved 回写快照组 → watch 重置编辑器为出厂档案值)。
|
||||
-->
|
||||
<template>
|
||||
<ElCard shadow="never" class="form-card json-pane">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">
|
||||
{{ RESET_GROUP_LABELS[groupKey] }}({{ groupKey }} 组 · 整组 JSON 编辑)
|
||||
</span>
|
||||
<div class="header-side">
|
||||
<span class="card-sub">保存即写新版本并热生效</span>
|
||||
<ElButton
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:loading="resetting"
|
||||
@click="onReset"
|
||||
>
|
||||
重置出厂
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElAlert
|
||||
v-if="actionError"
|
||||
type="error"
|
||||
:closable="true"
|
||||
:title="actionError"
|
||||
class="pane-error"
|
||||
@close="actionError = ''"
|
||||
/>
|
||||
|
||||
<!-- 字段说明(事实源 app/platform/defaults.py 的 DEFAULT_CONFIG) -->
|
||||
<ul class="field-docs">
|
||||
<li v-for="field in JSON_GROUP_FIELDS[groupKey]" :key="field.key">
|
||||
<span class="mono field-key">{{ field.key }}</span>
|
||||
<span class="field-desc">{{ field.desc }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ElInput
|
||||
v-model="draft"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
spellcheck="false"
|
||||
class="json-editor"
|
||||
placeholder='{ "key": "value" }'
|
||||
/>
|
||||
|
||||
<div class="save-bar">
|
||||
<ElInput
|
||||
v-model="saveComment"
|
||||
maxlength="200"
|
||||
placeholder="保存备注(可选;写入版本历史 comment)"
|
||||
class="comment-input"
|
||||
/>
|
||||
<ElButton type="primary" :loading="saving" @click="onSave">
|
||||
保存(整组提交,热生效)
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
JSON_GROUP_FIELDS,
|
||||
RESET_GROUP_LABELS,
|
||||
jsonGroupDraft,
|
||||
parseJsonGroupDraft,
|
||||
type JsonGroupKey
|
||||
} from './model'
|
||||
import { putConfigGroup } from '@/api/config'
|
||||
import { confirmAndResetGroup } from './reset'
|
||||
|
||||
defineOptions({ name: 'JsonGroupPane' })
|
||||
|
||||
const props = defineProps<{
|
||||
/** 租户 slug(保存目标) */
|
||||
slug: string
|
||||
/** 组键(contact / engine / misc——通用 pane 的三元实例) */
|
||||
groupKey: JsonGroupKey
|
||||
/** GET config 回显的组(原样快照;undefined = 快照里无该组 = 未存储
|
||||
* 覆盖,编辑器从 {} 起步可建组——父层仅在快照加载完成后挂载本 pane) */
|
||||
group: Record<string, unknown> | null | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 保存/重置成功:父层更新版本号与快照组 */
|
||||
(
|
||||
e: 'saved',
|
||||
payload: { version: number; group: JsonGroupKey; config: Record<string, unknown> }
|
||||
): void
|
||||
}>()
|
||||
|
||||
// ---- 编辑态(group 引用变化 = 保存回写/重置/快照重载 → 重置编辑器;
|
||||
// 挂载契约见 props.group 注——父层快照加载后才挂载,无「未加载先存」窗口) ----------
|
||||
|
||||
const draft = ref('')
|
||||
const saveComment = ref('')
|
||||
const saving = ref(false)
|
||||
const resetting = ref(false)
|
||||
const actionError = ref('')
|
||||
|
||||
watch(
|
||||
() => props.group,
|
||||
(group) => {
|
||||
draft.value = jsonGroupDraft(group)
|
||||
saveComment.value = ''
|
||||
actionError.value = ''
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// ---- 保存(前端 JSON 预检 → PUT 整组;类型校验后端兜底) ----------------------------
|
||||
|
||||
async function onSave(): Promise<void> {
|
||||
if (saving.value) return
|
||||
actionError.value = ''
|
||||
const parsed = parseJsonGroupDraft(draft.value)
|
||||
if (!parsed.ok) {
|
||||
actionError.value = parsed.error
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const res = await putConfigGroup(
|
||||
props.slug,
|
||||
props.groupKey,
|
||||
parsed.value,
|
||||
saveComment.value.trim() || undefined,
|
||||
{ showErrorMessage: false }
|
||||
)
|
||||
saveComment.value = ''
|
||||
ElMessage.success(
|
||||
`${RESET_GROUP_LABELS[props.groupKey]}已保存(版本 v${res.version},热生效)`
|
||||
)
|
||||
emit('saved', { version: res.version, group: props.groupKey, config: res.config })
|
||||
} catch (error) {
|
||||
// 400 校验 detail(带字段路径,如 "engine.ws_conn_limit: 须为正整数")
|
||||
actionError.value = error instanceof Error ? error.message : '保存失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 重置出厂(确认框 → POST config/{group}/reset → emit saved) --------------------
|
||||
|
||||
async function onReset(): Promise<void> {
|
||||
if (resetting.value) return
|
||||
resetting.value = true
|
||||
try {
|
||||
const res = await confirmAndResetGroup(props.slug, props.groupKey)
|
||||
if (!res) return // 用户取消确认
|
||||
ElMessage.success(
|
||||
`${RESET_GROUP_LABELS[props.groupKey]}已重置出厂(版本 v${res.version},热生效)`
|
||||
)
|
||||
emit('saved', {
|
||||
version: res.version,
|
||||
group: props.groupKey,
|
||||
config: res.config as Record<string, unknown>
|
||||
})
|
||||
} catch (error) {
|
||||
actionError.value = error instanceof Error ? error.message : '重置失败'
|
||||
} finally {
|
||||
resetting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.json-pane {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.card-sub {
|
||||
font-size: 12px;
|
||||
color: var(--art-gray-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pane-error {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.field-docs {
|
||||
margin: 0 0 12px;
|
||||
padding: 10px 12px 10px 30px;
|
||||
list-style: disc;
|
||||
border-radius: 6px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
|
||||
.field-key {
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-desc {
|
||||
color: var(--art-gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
:deep(textarea) {
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.save-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.comment-input {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -38,11 +38,14 @@ import {
|
||||
formatKB,
|
||||
formatVersionTime,
|
||||
initWallEdit,
|
||||
JSON_GROUP_FIELDS,
|
||||
jsonGroupDraft,
|
||||
normalizeAiCredsGroup,
|
||||
normalizeEmailCredsGroup,
|
||||
normalizePromptsGroup,
|
||||
normalizePushCredsGroup,
|
||||
parsePatternEntry,
|
||||
parseJsonGroupDraft,
|
||||
promptByteInfo,
|
||||
promptsOverCap,
|
||||
rowToEntry,
|
||||
@@ -705,3 +708,40 @@ describe('scopedConfigJson / snapshotGroupKeys(两版对比的范围视图)'
|
||||
expect(snapshotGroupKeys(null)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('通用 JSON 组编辑(终审 I-1:contact/engine/misc)', () => {
|
||||
it('jsonGroupDraft:组值 → 2 空格缩进等宽稿;null/undefined → 空对象', () => {
|
||||
expect(jsonGroupDraft({ cc_completion: false })).toBe(
|
||||
'{\n "cc_completion": false\n}'
|
||||
)
|
||||
expect(jsonGroupDraft(null)).toBe('{}')
|
||||
expect(jsonGroupDraft(undefined)).toBe('{}')
|
||||
})
|
||||
|
||||
it('parseJsonGroupDraft:语法错误 / 非对象顶层 → 带原因拒绝;对象通过', () => {
|
||||
const bad = parseJsonGroupDraft('{ "a": ')
|
||||
expect(bad.ok).toBe(false)
|
||||
if (!bad.ok) expect(bad.error).toContain('JSON 语法错误')
|
||||
const notObject = parseJsonGroupDraft('[1, 2]')
|
||||
expect(notObject.ok).toBe(false)
|
||||
if (!notObject.ok) expect(notObject.error).toContain('顶层须为 JSON 对象')
|
||||
const ok = parseJsonGroupDraft('{"cc_completion": false}')
|
||||
expect(ok.ok).toBe(true)
|
||||
if (ok.ok) expect(ok.value).toEqual({ cc_completion: false })
|
||||
})
|
||||
|
||||
it('JSON_GROUP_FIELDS:三组字段说明齐备(键与 DEFAULT_CONFIG 同名)', () => {
|
||||
expect(Object.keys(JSON_GROUP_FIELDS).sort()).toEqual(['contact', 'engine', 'misc'])
|
||||
expect(JSON_GROUP_FIELDS.contact.map((f) => f.key)).toEqual([
|
||||
'cc_completion',
|
||||
'email_light_ask',
|
||||
'template_languages'
|
||||
])
|
||||
expect(JSON_GROUP_FIELDS.engine.map((f) => f.key)).toEqual([
|
||||
'rate_limit_per_hour',
|
||||
'ws_conn_limit',
|
||||
'context_seed_keys'
|
||||
])
|
||||
expect(JSON_GROUP_FIELDS.misc.map((f) => f.key)).toEqual(['name_to_region_extras'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -443,6 +443,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 通用 JSON 组 pane(终审 I-1:contact / engine / misc) -->
|
||||
<template v-else-if="JSON_GROUPS.has(activeGroup)">
|
||||
<div v-loading="configLoading" class="group-pane">
|
||||
<ElAlert
|
||||
v-if="configError"
|
||||
type="error"
|
||||
:closable="false"
|
||||
:title="configError"
|
||||
class="pane-error"
|
||||
>
|
||||
<ElButton size="small" :loading="configLoading" @click="reloadConfig">重试</ElButton>
|
||||
</ElAlert>
|
||||
<JsonGroupPane
|
||||
v-else-if="activeSlug && configLoaded"
|
||||
:slug="activeSlug"
|
||||
:group-key="activeJsonGroup"
|
||||
:group="snapshot?.[activeJsonGroup]"
|
||||
@saved="onPaneSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 版本 pane(Task 7d) -->
|
||||
<template v-else-if="activeGroup === 'versions'">
|
||||
<div class="group-pane">
|
||||
@@ -477,7 +499,9 @@
|
||||
import PromptsPane from './PromptsPane.vue'
|
||||
import WallPane from './WallPane.vue'
|
||||
import CredsPane from './CredsPane.vue'
|
||||
import JsonGroupPane from './JsonGroupPane.vue'
|
||||
import VersionsPane from './VersionsPane.vue'
|
||||
import type { JsonGroupKey } from './model'
|
||||
import { confirmAndResetGroup } from './reset'
|
||||
|
||||
defineOptions({ name: 'ConfigCenter' })
|
||||
@@ -495,7 +519,10 @@
|
||||
{ key: 'brand', label: '品牌' },
|
||||
{ key: 'prompts', label: '提示词' },
|
||||
{ key: 'wall', label: '安全词表' },
|
||||
{ key: 'contact', label: '联系方式' },
|
||||
{ key: 'creds', label: '凭据' },
|
||||
{ key: 'engine', label: '引擎' },
|
||||
{ key: 'misc', label: '杂项' },
|
||||
{ key: 'versions', label: '版本' }
|
||||
] as const
|
||||
|
||||
@@ -504,10 +531,23 @@
|
||||
'brand',
|
||||
'prompts',
|
||||
'wall',
|
||||
'contact',
|
||||
'creds',
|
||||
'engine',
|
||||
'misc',
|
||||
'versions'
|
||||
])
|
||||
|
||||
/** 通用 JSON 组(终审 I-1:contact/engine/misc 走 JsonGroupPane 一份实现) */
|
||||
const JSON_GROUPS = new Set<GroupKey>(['contact', 'engine', 'misc'])
|
||||
|
||||
/** 模板绑定用的收窄视图(JSON 组分支下恒为该三键之一) */
|
||||
const activeJsonGroup = computed<JsonGroupKey>(() =>
|
||||
JSON_GROUPS.has(activeGroup.value)
|
||||
? (activeGroup.value as JsonGroupKey)
|
||||
: 'contact'
|
||||
)
|
||||
|
||||
type GroupKey = (typeof GROUP_NAV)[number]['key']
|
||||
|
||||
const activeGroup = ref<GroupKey>('brand')
|
||||
|
||||
@@ -737,3 +737,65 @@ export function scopedConfigJson(
|
||||
scope === COMPARE_SCOPE_ALL ? config : { [scope]: config[scope] }
|
||||
return JSON.stringify(view, null, 2)
|
||||
}
|
||||
|
||||
// ---- 通用 JSON 组编辑(Phase 3 终审 I-1:contact / engine / misc) -------------------
|
||||
|
||||
/** 走通用 JSON pane 的组(无密钥字段、结构低频变更——表单化收益低) */
|
||||
export type JsonGroupKey = 'contact' | 'engine' | 'misc'
|
||||
|
||||
/** 组取值 → 编辑器初稿(null 组 = 空对象;2 空格缩进等宽展示) */
|
||||
export function jsonGroupDraft(
|
||||
group: Record<string, unknown> | null | undefined
|
||||
): string {
|
||||
return JSON.stringify(group ?? {}, null, 2)
|
||||
}
|
||||
|
||||
/** 编辑器解析结果:ok=false 时 error 带可直接展示的中文原因 */
|
||||
export type JsonDraftParse =
|
||||
| { ok: true; value: Record<string, unknown> }
|
||||
| { ok: false; error: string }
|
||||
|
||||
/** 编辑稿预检:JSON 语法 + 顶层须为对象(后端 _check_* 类型校验兜底) */
|
||||
export function parseJsonGroupDraft(text: string): JsonDraftParse {
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(text)
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
return { ok: false, error: `JSON 语法错误——${detail}` }
|
||||
}
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
return { ok: false, error: '顶层须为 JSON 对象({ ... })——整组替换语义' }
|
||||
}
|
||||
// typeof 收窄只到 object(无索引签名)——JSON.parse 产物必为普通对象,断言收形
|
||||
return { ok: true, value: value as Record<string, unknown> }
|
||||
}
|
||||
|
||||
/** 三组的字段说明(pane 顶部展示;事实源 app/platform/defaults.py) */
|
||||
export const JSON_GROUP_FIELDS: Record<
|
||||
JsonGroupKey,
|
||||
Array<{ key: string; desc: string }>
|
||||
> = {
|
||||
contact: [
|
||||
{ key: 'cc_completion', desc: 'bool——cc 区号补全判定树开关' },
|
||||
{ key: 'email_light_ask', desc: 'bool——dual-contact 分步双要的邮箱轻补' },
|
||||
{
|
||||
key: 'template_languages',
|
||||
desc: 'string[]——邮件模板语言清单(运行期接线归 Phase 4,清理单 #8)'
|
||||
}
|
||||
],
|
||||
engine: [
|
||||
{ key: 'rate_limit_per_hour', desc: '正整数——聊天限流(次/小时/IP)' },
|
||||
{ key: 'ws_conn_limit', desc: '正整数——每 IP WebSocket 连接上限' },
|
||||
{
|
||||
key: 'context_seed_keys',
|
||||
desc: '映射——产品字段 → 会话上下文种子键(如 product_name → app_product_name)'
|
||||
}
|
||||
],
|
||||
misc: [
|
||||
{
|
||||
key: 'name_to_region_extras',
|
||||
desc: '映射——国名 → ISO 区域码扩展(resolve_region_code 消费)'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user