mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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',
|
||||
note: 'Note',
|
||||
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: {
|
||||
|
||||
@ -4500,6 +4500,17 @@ export default {
|
||||
expireAt: '过期时间',
|
||||
note: '备注',
|
||||
password: '登录密码(敏感策略需要)',
|
||||
workspaceHint: '策略将创建在工作区 {name},只对该工作区内的会话生效。IM 渠道(企微/钉钉等)会话所属工作区由渠道配置决定,与控制台当前选择无关。',
|
||||
severityHint: '只自动放行严重度 ≤ 上限的命中;CRITICAL 永远需要人工审批。',
|
||||
severityLow: '仅放行低危命中',
|
||||
severityMedium: '放行低/中危命中',
|
||||
severityHigh: '放行低/中/高危命中',
|
||||
scopeIdPickAgent: '选择智能体…',
|
||||
scopeIdWorkspaceHint: '选择策略生效的工作区(必须与会话实际所属工作区一致)。',
|
||||
scopeIdAgentHint: '选择智能体后,该智能体在本工作区的所有会话均适用。',
|
||||
scopeIdUserHint: '只能为当前登录用户创建 USER 范围策略。',
|
||||
scopeIdConversationHint: '填写会话 ID,可在审计日志的"会话"列查看(如 wecom:xxx)。',
|
||||
scopeIdConversationPlaceholder: '如 wecom:xxx 或会话 ID',
|
||||
},
|
||||
},
|
||||
resolution: {
|
||||
|
||||
@ -181,6 +181,14 @@
|
||||
<el-icon :size="16"><WarningFilled /></el-icon>
|
||||
<span>{{ t('approval.grant.createWorkspaceWarning') }}</span>
|
||||
</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-group">
|
||||
<label>{{ t('approval.grant.form.scopeType') }} <span class="required">*</span></label>
|
||||
@ -197,14 +205,51 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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
|
||||
v-else-if="form.scopeType === 'USER'"
|
||||
:value="form.scopeId"
|
||||
type="text"
|
||||
class="form-input"
|
||||
disabled
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model.trim="form.scopeId"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="\d*"
|
||||
placeholder="snowflake id"
|
||||
class="form-input"
|
||||
class="form-input mono"
|
||||
:placeholder="t('approval.grant.form.scopeIdConversationPlaceholder')"
|
||||
/>
|
||||
<p v-if="scopeIdHint" class="field-hint">{{ scopeIdHint }}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ t('approval.grant.form.toolName') }}</label>
|
||||
@ -226,10 +271,11 @@
|
||||
<div class="form-group">
|
||||
<label>{{ t('approval.grant.form.maxSeverity') }}</label>
|
||||
<select v-model="form.maxSeverity" class="form-input">
|
||||
<option value="LOW">LOW</option>
|
||||
<option value="MEDIUM">MEDIUM</option>
|
||||
<option value="HIGH">HIGH</option>
|
||||
<option value="LOW">LOW — {{ t('approval.grant.form.severityLow') }}</option>
|
||||
<option value="MEDIUM">MEDIUM — {{ t('approval.grant.form.severityMedium') }}</option>
|
||||
<option value="HIGH">HIGH — {{ t('approval.grant.form.severityHigh') }}</option>
|
||||
</select>
|
||||
<p class="field-hint">{{ t('approval.grant.form.severityHint') }}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{ t('approval.grant.form.grantKind') }}</label>
|
||||
@ -293,7 +339,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 {
|
||||
Delete,
|
||||
@ -305,6 +351,8 @@ import {
|
||||
WarningFilled,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { approvalApi } from '@/api'
|
||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||
import { useAgentStore } from '@/stores/useAgentStore'
|
||||
import McPagination from '@/components/common/McPagination.vue'
|
||||
import { mcConfirm } from '@/components/common/useConfirm'
|
||||
import type {
|
||||
@ -317,6 +365,9 @@ import type {
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const agentStore = useAgentStore()
|
||||
|
||||
const rows = ref<ApprovalGrant[]>([])
|
||||
const total = ref(0)
|
||||
// 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')
|
||||
})
|
||||
|
||||
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) {
|
||||
currentPage.value = p
|
||||
loadGrants()
|
||||
@ -425,6 +514,10 @@ function openCreateDialog(workspaceWide: boolean) {
|
||||
dialogWorkspaceWide.value = false
|
||||
}
|
||||
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() {
|
||||
@ -783,6 +876,27 @@ onMounted(loadGrants)
|
||||
}
|
||||
.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-group--full breaks across both columns (note, password). */
|
||||
.form-grid {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user