mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(ui): scope-typed pickers and severity-ceiling hints on the auto-approve grant form
This commit is contained in:
parent
1bb81234d0
commit
aa54058e31
@ -4408,6 +4408,17 @@ export default {
|
|||||||
expireAt: 'Expire at',
|
expireAt: 'Expire at',
|
||||||
note: 'Note',
|
note: 'Note',
|
||||||
password: 'Login password (required for sensitive rules)',
|
password: 'Login password (required for sensitive rules)',
|
||||||
|
workspaceHint: 'This grant is created in workspace {name} and only matches conversations belonging to it. IM-channel conversations (WeCom / DingTalk / …) follow the channel\'s workspace binding, not the console selection.',
|
||||||
|
severityHint: 'Only findings with severity ≤ the ceiling are auto-approved; CRITICAL always requires human approval.',
|
||||||
|
severityLow: 'auto-approve LOW findings only',
|
||||||
|
severityMedium: 'auto-approve LOW/MEDIUM findings',
|
||||||
|
severityHigh: 'auto-approve LOW/MEDIUM/HIGH findings',
|
||||||
|
scopeIdPickAgent: 'Pick an agent…',
|
||||||
|
scopeIdWorkspaceHint: 'Pick the workspace this grant applies to (must be the workspace the conversation actually belongs to).',
|
||||||
|
scopeIdAgentHint: 'Applies to every conversation of the selected agent within this workspace.',
|
||||||
|
scopeIdUserHint: 'USER-scope grants can only target the currently logged-in user.',
|
||||||
|
scopeIdConversationHint: 'Conversation id, as shown in the audit log "session" column (e.g. wecom:xxx).',
|
||||||
|
scopeIdConversationPlaceholder: 'e.g. wecom:xxx or a conversation id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolution: {
|
resolution: {
|
||||||
|
|||||||
@ -4500,6 +4500,17 @@ export default {
|
|||||||
expireAt: '过期时间',
|
expireAt: '过期时间',
|
||||||
note: '备注',
|
note: '备注',
|
||||||
password: '登录密码(敏感策略需要)',
|
password: '登录密码(敏感策略需要)',
|
||||||
|
workspaceHint: '策略将创建在工作区 {name},只对该工作区内的会话生效。IM 渠道(企微/钉钉等)会话所属工作区由渠道配置决定,与控制台当前选择无关。',
|
||||||
|
severityHint: '只自动放行严重度 ≤ 上限的命中;CRITICAL 永远需要人工审批。',
|
||||||
|
severityLow: '仅放行低危命中',
|
||||||
|
severityMedium: '放行低/中危命中',
|
||||||
|
severityHigh: '放行低/中/高危命中',
|
||||||
|
scopeIdPickAgent: '选择智能体…',
|
||||||
|
scopeIdWorkspaceHint: '选择策略生效的工作区(必须与会话实际所属工作区一致)。',
|
||||||
|
scopeIdAgentHint: '选择智能体后,该智能体在本工作区的所有会话均适用。',
|
||||||
|
scopeIdUserHint: '只能为当前登录用户创建 USER 范围策略。',
|
||||||
|
scopeIdConversationHint: '填写会话 ID,可在审计日志的"会话"列查看(如 wecom:xxx)。',
|
||||||
|
scopeIdConversationPlaceholder: '如 wecom:xxx 或会话 ID',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolution: {
|
resolution: {
|
||||||
|
|||||||
@ -181,6 +181,14 @@
|
|||||||
<el-icon :size="16"><WarningFilled /></el-icon>
|
<el-icon :size="16"><WarningFilled /></el-icon>
|
||||||
<span>{{ t('approval.grant.createWorkspaceWarning') }}</span>
|
<span>{{ t('approval.grant.createWorkspaceWarning') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- The grant is tenant-bound to the console's current workspace and
|
||||||
|
only matches conversations that actually live in it — IM-channel
|
||||||
|
conversations follow the channel's workspace binding, not the
|
||||||
|
console selection. Spelling this out up front prevents the
|
||||||
|
"configured but never fires" dead-grant trap. -->
|
||||||
|
<div class="info-banner">
|
||||||
|
{{ t('approval.grant.form.workspaceHint', { name: currentWorkspaceLabel }) }}
|
||||||
|
</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('approval.grant.form.scopeType') }} <span class="required">*</span></label>
|
<label>{{ t('approval.grant.form.scopeType') }} <span class="required">*</span></label>
|
||||||
@ -197,14 +205,51 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('approval.grant.form.scopeId') }} <span class="required">*</span></label>
|
<label>{{ t('approval.grant.form.scopeId') }} <span class="required">*</span></label>
|
||||||
|
<!-- Scope-typed pickers: free-text snowflake input made it far too
|
||||||
|
easy to paste the wrong kind of id (e.g. a workspace id into an
|
||||||
|
AGENT-scope grant), producing a grant that never matches. -->
|
||||||
|
<select
|
||||||
|
v-if="form.scopeType === 'WORKSPACE'"
|
||||||
|
v-model="form.scopeId"
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="ws in workspaceStore.workspaces"
|
||||||
|
:key="String(ws.id)"
|
||||||
|
:value="String(ws.id)"
|
||||||
|
>
|
||||||
|
{{ ws.name }} (#{{ ws.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-else-if="form.scopeType === 'AGENT'"
|
||||||
|
v-model="form.scopeId"
|
||||||
|
class="form-input"
|
||||||
|
>
|
||||||
|
<option value="" disabled>{{ t('approval.grant.form.scopeIdPickAgent') }}</option>
|
||||||
|
<option
|
||||||
|
v-for="a in agentStore.agents"
|
||||||
|
:key="String(a.id)"
|
||||||
|
:value="String(a.id)"
|
||||||
|
>
|
||||||
|
{{ a.name }} (#{{ a.id }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
<input
|
<input
|
||||||
|
v-else-if="form.scopeType === 'USER'"
|
||||||
|
:value="form.scopeId"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
v-model.trim="form.scopeId"
|
v-model.trim="form.scopeId"
|
||||||
type="text"
|
type="text"
|
||||||
inputmode="numeric"
|
class="form-input mono"
|
||||||
pattern="\d*"
|
:placeholder="t('approval.grant.form.scopeIdConversationPlaceholder')"
|
||||||
placeholder="snowflake id"
|
|
||||||
class="form-input"
|
|
||||||
/>
|
/>
|
||||||
|
<p v-if="scopeIdHint" class="field-hint">{{ scopeIdHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('approval.grant.form.toolName') }}</label>
|
<label>{{ t('approval.grant.form.toolName') }}</label>
|
||||||
@ -226,10 +271,11 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('approval.grant.form.maxSeverity') }}</label>
|
<label>{{ t('approval.grant.form.maxSeverity') }}</label>
|
||||||
<select v-model="form.maxSeverity" class="form-input">
|
<select v-model="form.maxSeverity" class="form-input">
|
||||||
<option value="LOW">LOW</option>
|
<option value="LOW">LOW — {{ t('approval.grant.form.severityLow') }}</option>
|
||||||
<option value="MEDIUM">MEDIUM</option>
|
<option value="MEDIUM">MEDIUM — {{ t('approval.grant.form.severityMedium') }}</option>
|
||||||
<option value="HIGH">HIGH</option>
|
<option value="HIGH">HIGH — {{ t('approval.grant.form.severityHigh') }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
<p class="field-hint">{{ t('approval.grant.form.severityHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{ t('approval.grant.form.grantKind') }}</label>
|
<label>{{ t('approval.grant.form.grantKind') }}</label>
|
||||||
@ -293,7 +339,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus/es/components/message/index'
|
import { ElMessage } from 'element-plus/es/components/message/index'
|
||||||
import { ref, computed, onMounted, reactive } from 'vue'
|
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import {
|
import {
|
||||||
Delete,
|
Delete,
|
||||||
@ -305,6 +351,8 @@ import {
|
|||||||
WarningFilled,
|
WarningFilled,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { approvalApi } from '@/api'
|
import { approvalApi } from '@/api'
|
||||||
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
import { useAgentStore } from '@/stores/useAgentStore'
|
||||||
import McPagination from '@/components/common/McPagination.vue'
|
import McPagination from '@/components/common/McPagination.vue'
|
||||||
import { mcConfirm } from '@/components/common/useConfirm'
|
import { mcConfirm } from '@/components/common/useConfirm'
|
||||||
import type {
|
import type {
|
||||||
@ -317,6 +365,9 @@ import type {
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const agentStore = useAgentStore()
|
||||||
|
|
||||||
const rows = ref<ApprovalGrant[]>([])
|
const rows = ref<ApprovalGrant[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
// Active grants count for the summary pill — separate from `total` because the
|
// Active grants count for the summary pill — separate from `total` because the
|
||||||
@ -364,6 +415,44 @@ const requiresPassword = computed(() => {
|
|||||||
return noTool && (form.scopeType === 'WORKSPACE' || form.scopeType === 'AGENT')
|
return noTool && (form.scopeType === 'WORKSPACE' || form.scopeType === 'AGENT')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentWorkspaceLabel = computed(() => {
|
||||||
|
const ws = workspaceStore.currentWorkspace
|
||||||
|
return ws ? `${ws.name} (#${ws.id})` : String(workspaceStore.currentWorkspaceId ?? '')
|
||||||
|
})
|
||||||
|
|
||||||
|
const scopeIdHint = computed(() => {
|
||||||
|
switch (form.scopeType) {
|
||||||
|
case 'WORKSPACE': return t('approval.grant.form.scopeIdWorkspaceHint')
|
||||||
|
case 'AGENT': return t('approval.grant.form.scopeIdAgentHint')
|
||||||
|
case 'USER': return t('approval.grant.form.scopeIdUserHint')
|
||||||
|
case 'CONVERSATION': return t('approval.grant.form.scopeIdConversationHint')
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefill scopeId whenever the scope type changes: WORKSPACE defaults to the
|
||||||
|
* console's current workspace, USER is locked to the requesting user (the
|
||||||
|
* backend rejects anything else), AGENT/CONVERSATION start empty for an
|
||||||
|
* explicit pick.
|
||||||
|
*/
|
||||||
|
function prefillScopeId() {
|
||||||
|
switch (form.scopeType) {
|
||||||
|
case 'WORKSPACE':
|
||||||
|
form.scopeId = String(workspaceStore.currentWorkspaceId ?? '')
|
||||||
|
break
|
||||||
|
case 'USER':
|
||||||
|
form.scopeId = localStorage.getItem('userId') || ''
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
form.scopeId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => form.scopeType, () => {
|
||||||
|
if (dialogOpen.value) prefillScopeId()
|
||||||
|
})
|
||||||
|
|
||||||
function onPageChange(p: number) {
|
function onPageChange(p: number) {
|
||||||
currentPage.value = p
|
currentPage.value = p
|
||||||
loadGrants()
|
loadGrants()
|
||||||
@ -425,6 +514,10 @@ function openCreateDialog(workspaceWide: boolean) {
|
|||||||
dialogWorkspaceWide.value = false
|
dialogWorkspaceWide.value = false
|
||||||
}
|
}
|
||||||
dialogOpen.value = true
|
dialogOpen.value = true
|
||||||
|
prefillScopeId()
|
||||||
|
// Lazy-load picker data sources; both are cheap and cached in their stores.
|
||||||
|
if (!workspaceStore.workspaces.length) workspaceStore.fetchWorkspaces()
|
||||||
|
if (!agentStore.agents.length) agentStore.fetchAgents()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitCreate() {
|
async function submitCreate() {
|
||||||
@ -783,6 +876,27 @@ onMounted(loadGrants)
|
|||||||
}
|
}
|
||||||
.danger-banner .el-icon { flex-shrink: 0; margin-top: 1px; }
|
.danger-banner .el-icon { flex-shrink: 0; margin-top: 1px; }
|
||||||
|
|
||||||
|
/* Neutral info banner — states which workspace the grant will be created in.
|
||||||
|
Deliberately calmer than .danger-banner: informational, not a warning. */
|
||||||
|
.info-banner {
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: var(--mc-surface-tertiary, #f1f5f9);
|
||||||
|
border: 1px solid var(--mc-border-light, #e5e7eb);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--mc-text-secondary, #475569);
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-field helper line under a control (scope-id semantics, severity ceiling). */
|
||||||
|
.field-hint {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--mc-text-tertiary, #94a3b8);
|
||||||
|
}
|
||||||
|
|
||||||
/* Form grid layout — two columns on wide modal, one column when narrow.
|
/* Form grid layout — two columns on wide modal, one column when narrow.
|
||||||
form-group--full breaks across both columns (note, password). */
|
form-group--full breaks across both columns (note, password). */
|
||||||
.form-grid {
|
.form-grid {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user