Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5aa48cf9 | ||
|
|
21624f6c55 | ||
|
|
c4a799c99c | ||
|
|
62ed5cc1ce | ||
|
|
048a6ec969 | ||
|
|
cb3015a5b1 | ||
|
|
245d38d471 | ||
|
|
88dcebc3fb | ||
|
|
8091fbb89c |
@@ -2,28 +2,14 @@ package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func ValidatePassword(username, password string) error {
|
||||
func ValidatePassword(_ string, password string) error {
|
||||
if len([]rune(password)) < 6 {
|
||||
return fmt.Errorf("密码至少需要 6 个字符")
|
||||
}
|
||||
var upper, lower, digit bool
|
||||
for _, value := range password {
|
||||
upper = upper || unicode.IsUpper(value)
|
||||
lower = lower || unicode.IsLower(value)
|
||||
digit = digit || unicode.IsDigit(value)
|
||||
}
|
||||
if !upper || !lower || !digit {
|
||||
return fmt.Errorf("密码必须同时包含大写字母、小写字母和数字")
|
||||
}
|
||||
if username != "" && strings.Contains(strings.ToLower(password), strings.ToLower(username)) {
|
||||
return fmt.Errorf("密码不能包含用户名")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@ func TestValidatePasswordLengthAndComplexity(t *testing.T) {
|
||||
password string
|
||||
want string
|
||||
}{
|
||||
{name: "six characters", username: "operator", password: "Abc123"},
|
||||
{name: "six lowercase characters", username: "operator", password: "abcdef"},
|
||||
{name: "contains username", username: "admin", password: "admin123"},
|
||||
{name: "six unicode characters", username: "operator", password: "密码密码密码"},
|
||||
{name: "five characters", username: "operator", password: "Ab123", want: "至少需要 6 个字符"},
|
||||
{name: "missing uppercase", username: "operator", password: "abc123", want: "大写字母、小写字母和数字"},
|
||||
{name: "missing lowercase", username: "operator", password: "ABC123", want: "大写字母、小写字母和数字"},
|
||||
{name: "missing digit", username: "operator", password: "Abcdef", want: "大写字母、小写字母和数字"},
|
||||
{name: "contains username", username: "admin", password: "MyAdmin1", want: "不能包含用户名"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "eslint \"src/**/*.{js,vue}\""
|
||||
"lint": "eslint \"src/**/*.{js,vue}\"",
|
||||
"test:navigation": "node --test tests/navigation.test.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.2",
|
||||
@@ -50,4 +51,3 @@
|
||||
"not dead"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -58,22 +58,14 @@ import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
import { buildNavigation } from './navigation'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
const collapsed = ref(false)
|
||||
const navigation = computed(() => {
|
||||
const root = router.getRoutes().find((route) => route.path === '/')
|
||||
const permissions = store.getters['sense-identity/permissions'] || []
|
||||
return (root?.children || [])
|
||||
.filter((route) => route.meta?.title && !route.meta?.hidden)
|
||||
.filter((route) => !route.meta?.permission || permissions.includes(route.meta.permission))
|
||||
.map((route) => ({
|
||||
path: route.path ? `/${route.path}`.replace('//', '/') : '/',
|
||||
title: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
order: route.meta.order || 0
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order)
|
||||
return buildNavigation(router.getRoutes(), permissions)
|
||||
})
|
||||
|
||||
async function logout() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
function buildNavigation(routes, permissions = []) {
|
||||
const layout = routes
|
||||
.filter((route) => route.path === '/' && Array.isArray(route.children) && route.children.length > 0)
|
||||
.sort((left, right) => right.children.length - left.children.length)[0]
|
||||
|
||||
return (layout?.children || [])
|
||||
.filter((route) => route.meta?.title && !route.meta?.hidden)
|
||||
.filter((route) => !route.meta?.permission || permissions.includes(route.meta.permission))
|
||||
.map((route) => ({
|
||||
path: route.path ? `/${route.path}`.replace('//', '/') : '/',
|
||||
title: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
order: route.meta.order || 0
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order)
|
||||
}
|
||||
|
||||
module.exports = { buildNavigation }
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-form-item label="用户名" prop="username"><el-input v-model="form.username" :disabled="Boolean(editingID)" /></el-form-item>
|
||||
<el-form-item label="姓名" prop="display_name"><el-input v-model="form.display_name" /></el-form-item>
|
||||
<el-form-item label="角色" prop="role"><el-select v-model="form.role" style="width: 100%"><el-option v-for="role in roles" :key="role.value" :label="role.label" :value="role.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="!editingID" label="初始密码" prop="password"><el-input v-model="form.password" type="password" show-password /><div class="field-hint">至少 6 位,包含大小写字母和数字,且不含用户名。</div></el-form-item>
|
||||
<el-form-item v-if="!editingID" label="初始密码" prop="password"><el-input v-model="form.password" type="password" show-password /><div class="field-hint">至少 6 位字符。</div></el-form-item>
|
||||
<el-form-item v-else label="状态"><el-switch v-model="form.enabled" active-text="启用" inactive-text="停用" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const test = require('node:test')
|
||||
|
||||
const { buildNavigation } = require('../src/layout/navigation.js')
|
||||
|
||||
test('selects the layout record when normalized routes contain duplicate root paths', () => {
|
||||
const routes = [
|
||||
{ path: '/', name: 'sense-dashboard', children: [], meta: { title: '工作台' } },
|
||||
{ path: '/devices', name: 'sense-devices', children: [], meta: { title: '设备管理' } },
|
||||
{
|
||||
path: '/',
|
||||
children: [
|
||||
{ path: 'devices', meta: { title: '设备管理', permission: 'device.read', order: 10 } },
|
||||
{ path: '', meta: { title: '工作台', order: 0 } },
|
||||
{ path: 'users', meta: { title: '用户与权限', permission: 'identity.users.read', order: 90 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes, ['device.read']), [
|
||||
{ path: '/', title: '工作台', icon: undefined, order: 0 },
|
||||
{ path: '/devices', title: '设备管理', icon: undefined, order: 10 }
|
||||
])
|
||||
})
|
||||
|
||||
test('returns no unauthorized or hidden entries', () => {
|
||||
const routes = [{
|
||||
path: '/',
|
||||
children: [
|
||||
{ path: '', meta: { title: '工作台' } },
|
||||
{ path: 'users', meta: { title: '用户与权限', permission: 'identity.users.read' } },
|
||||
{ path: 'internal', meta: { title: '内部页', hidden: true } }
|
||||
]
|
||||
}]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes), [
|
||||
{ path: '/', title: '工作台', icon: undefined, order: 0 }
|
||||
])
|
||||
})
|
||||
|
||||
test('administrator receives every Sense MVP navigation entry', () => {
|
||||
const entries = [
|
||||
['', '工作台', null, 0],
|
||||
['devices', '设备管理', 'device.read', 10],
|
||||
['admission', '视频接入', 'device.write', 20],
|
||||
['media', '视频服务', 'media.read', 30],
|
||||
['liveview', '实时监看', 'media.read', 40],
|
||||
['areas', '区域与警戒线', 'area.read', 50],
|
||||
['users', '用户与权限', 'identity.users.read', 90],
|
||||
['audit', '操作记录', 'identity.audit.read', 91]
|
||||
]
|
||||
const routes = [{ path: '/', children: entries.map(([path, title, permission, order]) => ({
|
||||
path,
|
||||
meta: { title, permission, order }
|
||||
})) }]
|
||||
const permissions = [
|
||||
'device.read', 'device.write', 'media.read', 'area.read',
|
||||
'identity.users.read', 'identity.audit.read'
|
||||
]
|
||||
|
||||
assert.deepEqual(buildNavigation(routes, permissions).map((item) => item.title), [
|
||||
'工作台', '设备管理', '视频接入', '视频服务', '实时监看',
|
||||
'区域与警戒线', '用户与权限', '操作记录'
|
||||
])
|
||||
})
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: fbb85952429813bb3da7e3cd8526ec03bc0dac0d
|
||||
synchronized_at: 2026-08-13T00:52:39Z
|
||||
wiki_revision: c9d47108e45f66875726e3eef79eedb2a5942514
|
||||
synchronized_at: 2026-08-13T01:15:01Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -71,7 +71,7 @@ synchronized_at: 2026-08-13T00:52:39Z
|
||||
## Sense MVP 业务与安全规则
|
||||
|
||||
- **Sense 账户**:只登录 Sense;不得接受 Bell JWT、Cookie 或用户数据。角色为系统管理员、实施/运维、站点管理员和只读用户,后端权限是最终边界。
|
||||
- **安全初始化**:系统不提供默认账户、默认密码或默认签名密钥;首次管理员由仓库外一次性令牌创建。所有模式的密码至少 6 个字符,必须同时包含大写字母、小写字母和数字,且不得包含用户名。该最短长度由负责人在已知生产安全风险后明确确认。
|
||||
- **安全初始化**:系统不提供默认账户、默认密码或默认签名密钥;首次管理员由仓库外一次性令牌创建。所有模式的密码仅要求至少 6 个字符,不限制字符种类并允许包含用户名。负责人已明确接受该生产密码策略的字典猜测与凭据填充风险。
|
||||
- **Device**:设备不可变逻辑 ID 是后续 Profile、媒体和区域的内部引用。非视频适配器未实现时必须显示 `adapter_not_ready`。
|
||||
- **摄像机凭据**:只写不读,使用仓库外 32 字节密钥加密;不得进入 URL、日志、审计、工单或响应。
|
||||
- **受控发现**:ONVIF Discovery 默认关闭,只有显式设置获准本机 IP 才能发送发现;不得扫描未授权网段。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Troubleshooting
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Troubleshooting
|
||||
wiki_revision: a076590915040eae0d165be6254c6302f308fc5a
|
||||
synchronized_at: 2026-08-12T10:22:09Z
|
||||
wiki_revision: b8e778ae4255ba3fc36a1cd6bcb735e29bd19eee
|
||||
synchronized_at: 2026-08-13T01:26:22Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 故障排查
|
||||
@@ -47,6 +47,7 @@ synchronized_at: 2026-08-12T10:22:09Z
|
||||
|---|---|
|
||||
| 启动提示缺少 `SENSE_DATABASE_URL` | 正式模式必须提供独立 PostgreSQL URL;只有测试/smoke 可显式使用 `memory`。 |
|
||||
| 登录统一提示用户名或密码错误 | 为避免枚举账户,失败响应不区分用户不存在、密码错误或账户停用;由管理员查看审计。 |
|
||||
| 管理员登录后侧栏没有模块 | 先确认 /api/v1/identity/me 返回角色与权限;若权限正常,检查前端是否从具有 children 的应用布局路由派生菜单,不得依赖重复 / 路由记录顺序。 |
|
||||
| `adapter_not_ready` | 当前设备类型尚无适配器,不代表网络故障;首期完整支持 video。 |
|
||||
| `discovery_unavailable` | 未设置获准的 `SENSE_ONVIF_DISCOVERY_IP`,或该 IP 不属于本机网卡。可改用手工 ONVIF 地址。 |
|
||||
| `authentication_failed` | 在设备管理中重新写入凭据后再次执行接入检查;不要把凭据写进地址。 |
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Delivery-Documentation-Guide
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Delivery-Documentation-Guide.-
|
||||
wiki_revision: 2cf0a0f2f261c002866c86c4e70b5f6ed89f9c1c
|
||||
synchronized_at: 2026-08-13T00:53:04Z
|
||||
wiki_revision: 04d9a7f0916262c3578e7934a2e98327dae55c7a
|
||||
synchronized_at: 2026-08-13T01:15:15Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 交付文档指南
|
||||
@@ -84,7 +84,7 @@ synchronized_at: 2026-08-13T00:53:04Z
|
||||
|
||||
Sense 面向网管、实施人员和非技术现场人员,菜单按日常任务组织:工作台 → 设备管理 → 视频接入 → 视频服务 → 实时监看 → 区域与警戒线。普通操作优先展示中文状态与下一步,不要求用户理解 ONVIF、RTSP 或 MediaMTX 内部模型。
|
||||
|
||||
- 系统管理员:完成一次性安全初始化,维护 Sense 用户与角色,查看操作记录。首次管理员和新用户密码至少 6 个字符,必须同时包含大写字母、小写字母和数字,且不得包含用户名。
|
||||
- 系统管理员:完成一次性安全初始化,维护 Sense 用户与角色,查看操作记录。首次管理员和新用户密码仅要求至少 6 个字符,不限制字符种类并允许包含用户名;仍应由管理员选择难猜且不复用的密码。
|
||||
- 实施/运维:添加设备、只写更新凭据、在获准网卡发现或手工接入、检查主辅码流、处理媒体进程与对账。
|
||||
- 站点管理员:查看/维护站点设备,实时监看,绘制和发布区域版本;无权创建系统管理员。
|
||||
- 只读用户:查看设备、媒体状态、实时画面和区域,不能修改。
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-42-Sense密码策略仅限制最短6位
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-42-Sense%E5%AF%86%E7%A0%81%E7%AD%96%E7%95%A5%E4%BB%85%E9%99%90%E5%88%B6%E6%9C%80%E7%9F%AD6%E4%BD%8D.-
|
||||
wiki_revision: 53d52a17ae17072e1335b8bdff30fa19ad50f20f
|
||||
synchronized_at: 2026-08-13T01:20:24Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 42 Sense密码策略仅限制最短6位
|
||||
|
||||
- 类型:需求变更
|
||||
- 所属 Epic:#7
|
||||
- 所属 MVP / 版本:#8
|
||||
- 状态:待验收
|
||||
- 日期:2026-08-13
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/42
|
||||
- Wiki 页面:Task-42-Sense密码策略仅限制最短6位
|
||||
- Wiki revision:见本地镜像头
|
||||
|
||||
## 背景与目标
|
||||
|
||||
负责人确认 Sense demo 与生产密码仅要求至少 6 个字符,取消字符种类和用户名包含限制,以支持用户指定的简单管理员凭据。实际账号和密码属于运行时秘密,不进入本归档。
|
||||
|
||||
## 最终方案
|
||||
|
||||
- 后端 `ValidatePassword` 只按 Unicode 字符数检查至少 6 个字符;bootstrap 与创建用户继续共享该入口。
|
||||
- 测试覆盖 6 位全小写、包含用户名和 6 个 Unicode 字符通过,以及 5 字符拒绝。
|
||||
- 前端新用户表单只提示“至少 6 位字符”。
|
||||
- Wiki 记录全模式规则和明确接受的风险,交付说明仍建议选择难猜且不复用的密码。
|
||||
- 重新生成 Windows 包,使用运行时注入的脱敏凭据完成 demo 管理员初始化和登录,服务保持运行。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `Sense/server/app/sense/identity/password.go`
|
||||
- `Sense/server/app/sense/identity/password_test.go`
|
||||
- `Sense/ui/src/views/sense/identity/Users.vue`
|
||||
- `docs/03-business-rules-and-glossary.md`
|
||||
- `docs/delivery/README.md`
|
||||
- `wiki-docs.json`
|
||||
- `docs/task/42-Sense密码策略仅限制最短6位.md`
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| 6 位全小写密码通过,5 位拒绝 | 通过 |
|
||||
| 包含用户名密码通过 | 通过 |
|
||||
| bootstrap 与创建用户共享规则 | 通过 |
|
||||
| 前端只提示至少 6 位 | 通过 |
|
||||
| Windows ZIP 重建并完成管理员初始化/登录 | 通过;bootstrap 201,login 200 |
|
||||
| 凭据不进入仓库、Wiki、工单或日志 | 通过;工单需求已脱敏 |
|
||||
|
||||
## 测试
|
||||
|
||||
- `go test ./app/sense/identity/...`、`go test ./...`:通过。
|
||||
- `corepack pnpm@9.15.1 lint`:0 error,存在既有格式 warning。
|
||||
- `cmd /c Sense\scripts\package-windows.bat`:通过;前端仅有既有 vendor bundle 体积警告。
|
||||
- ZIP:7,280,519 bytes;SHA-256 `7A1DC3378ECAD90DC7521F1E49C7A279E4C31B38D6EBC8CC30E123528BECDEE4`。
|
||||
- 包内 demo:管理员初始化 201、登录 200;服务监听 `127.0.0.1:18080`。
|
||||
- Harness:31 项测试、strict 和 Wiki 镜像检查通过。
|
||||
- **未验证部分**:未在真实 PostgreSQL 环境验证;未由目标管理员人工页面登录验收。生产弱密码风险为负责人明确接受的产品决策。
|
||||
|
||||
## 遗留问题
|
||||
|
||||
- 本策略显著增加字典猜测和凭据填充风险;当前未增加 MFA、登录限速或外部身份提供者。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `8091fbb` feat: simplify Sense password policy (#42)
|
||||
- `88dcebc` docs: record length-only Sense password policy (#42)
|
||||
- `245d38d` docs: create task 42 acceptance archive (#42)
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Task-44-Sense管理员侧栏模块为空修复
|
||||
wiki_url: https://git.ilapage.cn/ila/yovision/wiki/Task-44-Sense%E7%AE%A1%E7%90%86%E5%91%98%E4%BE%A7%E6%A0%8F%E6%A8%A1%E5%9D%97%E4%B8%BA%E7%A9%BA%E4%BF%AE%E5%A4%8D.-
|
||||
wiki_revision: 9647fffe7d8b0ede13c53746e8966fffb3555820
|
||||
synchronized_at: 2026-08-13T01:31:35Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 44 Sense管理员侧栏模块为空修复
|
||||
|
||||
- 类型:缺陷
|
||||
- 所属 Epic:#7
|
||||
- 所属 MVP / 版本:#8
|
||||
- 状态:待验收
|
||||
- 日期:2026-08-13
|
||||
- Gitea 工单:https://git.ilapage.cn/ila/yovision/issues/44
|
||||
- Wiki 页面:Task-44-Sense管理员侧栏模块为空修复
|
||||
- Wiki revision:见本地镜像头
|
||||
|
||||
## 背景与目标
|
||||
|
||||
最新 Windows 包中管理员可登录,但侧栏不显示任何模块。目标是在不放宽 RBAC 的前提下恢复 Sense MVP 导航,并防止 Vue Router 规范化记录顺序再次导致空菜单。
|
||||
|
||||
## 根因与最终方案
|
||||
|
||||
后端管理员会话正确返回 9 项权限。Vue Router `getRoutes()` 会同时返回父布局 `/` 和路径也为 `/` 的工作台子记录;原代码用首个 `path === '/'` 记录派生菜单,误选无 children 的工作台记录。
|
||||
|
||||
修复后只选择路径为 `/` 且具有 children 的布局记录,并在多个候选中优先 children 最完整者。权限过滤、隐藏入口过滤和排序逻辑保持不变,并提取为独立可测试函数。
|
||||
|
||||
## 修改文件
|
||||
|
||||
- `Sense/ui/src/layout/AppLayout.vue`:调用稳定的菜单派生函数。
|
||||
- `Sense/ui/src/layout/navigation.js`:定位布局、按权限过滤并排序。
|
||||
- `Sense/ui/tests/navigation.test.cjs`:重复根路径、权限/隐藏过滤和管理员八项导航测试。
|
||||
- `Sense/ui/package.json`:增加 `test:navigation`。
|
||||
- `docs/06-troubleshooting.md`:Wiki-first 排错说明。
|
||||
- `wiki-docs.json`、`docs/task/44-Sense管理员侧栏模块为空修复.md`:任务归档。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| 管理员显示八项 Sense MVP 导航 | 自动回归通过 |
|
||||
| 不依赖重复 `/` 记录顺序 | 通过 |
|
||||
| 权限不足和隐藏入口仍被过滤 | 通过 |
|
||||
| 前端 test/lint/build | 通过;lint 0 error,存在既有 warning |
|
||||
| Windows ZIP 重建并以管理员登录 | 通过;角色 administrator,9 项权限,新前端资源已加载 |
|
||||
|
||||
## 测试
|
||||
|
||||
- `corepack pnpm@9.15.1 test:navigation`:3 项通过。
|
||||
- Vue Router 实际规范化记录探针:正确派生工作台和设备管理。
|
||||
- `corepack pnpm@9.15.1 lint`:0 error,既有 warning。
|
||||
- Windows 打包:通过;前端仅有既有 vendor bundle 体积警告。
|
||||
- 包内运行:登录角色 administrator、权限数 9,首页引用新脚本 `js/index.25411ed5.js`。
|
||||
- Harness:31 项、strict 与 Wiki 镜像检查通过。
|
||||
- ZIP:7,280,825 bytes;SHA-256 `0703B397D20EAAE54BCB6B5A83170EFF6816246A8C163276495CD93EEEB72B4F`。
|
||||
- **未验证部分**:未通过自动化真实浏览器截图断言侧栏 DOM;已要求用户 Ctrl+F5 后人工查看。
|
||||
|
||||
## 遗留问题
|
||||
|
||||
- 浏览器可能缓存旧前端资源,更新包后需要强制刷新。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `048a6ec` fix: restore Sense permission navigation (#44)
|
||||
- `62ed5cc` docs: add Sense empty navigation troubleshooting (#44)
|
||||
- `c4a799c` test: cover Sense administrator navigation (#44)
|
||||
- `21624f6` docs: create task 44 acceptance archive (#44)
|
||||
@@ -119,6 +119,14 @@
|
||||
{
|
||||
"page": "Task-40-Sense全模式密码最短6位",
|
||||
"path": "docs/task/40-Sense全模式密码最短6位.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-42-Sense密码策略仅限制最短6位",
|
||||
"path": "docs/task/42-Sense密码策略仅限制最短6位.md"
|
||||
},
|
||||
{
|
||||
"page": "Task-44-Sense管理员侧栏模块为空修复",
|
||||
"path": "docs/task/44-Sense管理员侧栏模块为空修复.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user