mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 07:12:09 +08:00
feat(connections): configure domain and process rules
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
||||
- 新增从连接详情快捷设置域名或进程规则,支持子域匹配与修改已有规则
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -5,8 +5,15 @@ import { useCallback, useImperativeHandle, useState, type Ref } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import parseTraffic from '@/utils/parse-traffic'
|
||||
|
||||
import { getConnectionRuleTargets } from './connection-rule'
|
||||
import {
|
||||
ConnectionRuleDialog,
|
||||
type ConnectionRuleTarget,
|
||||
} from './connection-rule-dialog'
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem, closed: boolean) => void
|
||||
close: () => void
|
||||
@@ -17,6 +24,9 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
||||
const [detail, setDetail] = useState<IConnectionsItem | null>(null)
|
||||
const [closed, setClosed] = useState(false)
|
||||
const theme = useTheme()
|
||||
const [ruleTarget, setRuleTarget] = useState<ConnectionRuleTarget | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setOpen(false)
|
||||
@@ -35,32 +45,44 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
||||
}))
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
'.MuiSnackbarContent-root': {
|
||||
width: '520px',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '480px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'.MuiSnackbarContent-message': { width: '100%', minWidth: 0 },
|
||||
}}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail
|
||||
data={detail}
|
||||
closed={closed}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{
|
||||
'.MuiSnackbarContent-root': {
|
||||
width: '520px',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '480px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'.MuiSnackbarContent-message': { width: '100%', minWidth: 0 },
|
||||
}}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail
|
||||
data={detail}
|
||||
closed={closed}
|
||||
onClose={onClose}
|
||||
onAddRule={(target) => {
|
||||
setRuleTarget(target)
|
||||
onClose()
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{ruleTarget && (
|
||||
<ConnectionRuleDialog
|
||||
target={ruleTarget}
|
||||
onClose={() => setRuleTarget(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,11 +90,24 @@ interface InnerProps {
|
||||
data: IConnectionsItem
|
||||
closed: boolean
|
||||
onClose?: () => void
|
||||
onAddRule: (target: ConnectionRuleTarget) => void
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
const InnerConnectionDetail = ({
|
||||
data,
|
||||
closed,
|
||||
onClose,
|
||||
onAddRule,
|
||||
}: InnerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { current } = useProfiles()
|
||||
const { metadata, rulePayload } = data
|
||||
const targets = getConnectionRuleTargets(metadata)
|
||||
const canAddRule = Boolean(
|
||||
current?.uid &&
|
||||
current.option?.rules &&
|
||||
(targets.domain || targets.process),
|
||||
)
|
||||
const theme = useTheme()
|
||||
const chains = [...data.chains].reverse().join(' / ')
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
|
||||
@@ -108,7 +143,7 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
{ label: t('connections.components.fields.rule'), value: rule },
|
||||
{
|
||||
label: t('connections.components.fields.process'),
|
||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||
value: `${metadata.process ?? ''}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||
},
|
||||
{
|
||||
label: t('connections.components.fields.time'),
|
||||
@@ -157,16 +192,31 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!closed && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
mt: 1,
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!canAddRule}
|
||||
onClick={() => {
|
||||
if (!current?.uid || !current.option?.rules) return
|
||||
onAddRule({
|
||||
metadata: { ...metadata },
|
||||
profileUid: current.uid,
|
||||
profileName: current.name || current.uid,
|
||||
rulesUid: current.option.rules,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('connections.ruleDialog.entry')}
|
||||
</Button>
|
||||
{!closed && (
|
||||
<Button
|
||||
variant="contained"
|
||||
title={t('connections.components.actions.closeConnection')}
|
||||
@@ -177,6 +227,15 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
||||
>
|
||||
{t('connections.components.actions.closeConnection')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{!canAddRule && (
|
||||
<Box sx={{ mt: 1, typography: 'caption' }}>
|
||||
{t(
|
||||
targets.domain || targets.process
|
||||
? 'connections.ruleDialog.noProfile'
|
||||
: 'connections.ruleDialog.noMetadata',
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
import {
|
||||
Alert,
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
MenuItem,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
useClashConfigData,
|
||||
useProxiesData,
|
||||
} from '@/providers/app-data-context'
|
||||
import { getProfiles, readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import {
|
||||
getConnectionRuleTargets,
|
||||
findConnectionRule,
|
||||
saveConnectionRule,
|
||||
} from './connection-rule'
|
||||
|
||||
export interface ConnectionRuleTarget {
|
||||
metadata: IConnectionsItem['metadata']
|
||||
profileUid: string
|
||||
profileName: string
|
||||
rulesUid: string
|
||||
}
|
||||
|
||||
type ConnectionRuleKind = 'domain' | 'process'
|
||||
|
||||
export function ConnectionRuleDialog({
|
||||
target,
|
||||
onClose,
|
||||
}: {
|
||||
target: ConnectionRuleTarget
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { proxyView } = useProxiesData()
|
||||
const { clashConfig } = useClashConfigData()
|
||||
const targets = getConnectionRuleTargets(target.metadata)
|
||||
const [kind, setKind] = useState<ConnectionRuleKind>(
|
||||
targets.domain ? 'domain' : 'process',
|
||||
)
|
||||
const [policy, setPolicy] = useState<string | null>(null)
|
||||
const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
|
||||
const [existing, setExisting] = useState<Record<
|
||||
ConnectionRuleKind | 'suffix',
|
||||
ReturnType<typeof findConnectionRule>
|
||||
> | null>(null)
|
||||
const [loadAttempt, setLoadAttempt] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const selected = targets[kind]
|
||||
const modifying = Boolean(
|
||||
existing?.[
|
||||
kind === 'domain' && scope === 'DOMAIN-SUFFIX' ? 'suffix' : kind
|
||||
],
|
||||
)
|
||||
const groups = proxyView
|
||||
? proxyView.groups.length || !proxyView.global
|
||||
? proxyView.groups
|
||||
: [proxyView.global]
|
||||
: []
|
||||
const policies = [
|
||||
...new Set(['DIRECT', 'REJECT', ...groups.map((group) => group.name)]),
|
||||
]
|
||||
|
||||
const kindLabels = {
|
||||
domain: t('connections.ruleDialog.domain'),
|
||||
process: t('connections.components.fields.process'),
|
||||
}
|
||||
|
||||
const unavailablePolicy = Boolean(policy && !policies.includes(policy))
|
||||
const blocked = loading || saving || !existing
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
readProfileFile(target.rulesUid)
|
||||
.then((content) => {
|
||||
const ruleTargets = getConnectionRuleTargets(target.metadata)
|
||||
const rules = {
|
||||
domain: ruleTargets.domain
|
||||
? findConnectionRule(content, ruleTargets.domain)
|
||||
: undefined,
|
||||
suffix: ruleTargets.domain
|
||||
? findConnectionRule(content, {
|
||||
...ruleTargets.domain,
|
||||
type: 'DOMAIN-SUFFIX',
|
||||
})
|
||||
: undefined,
|
||||
process: ruleTargets.process
|
||||
? findConnectionRule(content, ruleTargets.process)
|
||||
: undefined,
|
||||
}
|
||||
if (cancelled) return
|
||||
const initialKind = ruleTargets.domain ? 'domain' : 'process'
|
||||
const initial =
|
||||
initialKind === 'domain'
|
||||
? (rules.domain ?? rules.suffix)
|
||||
: rules[initialKind]
|
||||
setExisting(rules)
|
||||
setPolicy(initial?.policy ?? null)
|
||||
setScope(initial?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN')
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled)
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [target, loadAttempt])
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
if (blocked || !selected || !policy || !policies.includes(policy)) return
|
||||
if (/[\r\n,]/u.test(selected.value) || /[\r\n,]/u.test(policy)) {
|
||||
setError(t('connections.ruleDialog.unsupportedValue'))
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError('')
|
||||
try {
|
||||
const result = await mutate(
|
||||
async () => {
|
||||
const checkProfile = async () => {
|
||||
const profiles = await getProfiles()
|
||||
const current = profiles.items?.find(
|
||||
(item) => item.uid === profiles.current,
|
||||
)
|
||||
if (
|
||||
profiles.current !== target.profileUid ||
|
||||
current?.option?.rules !== target.rulesUid
|
||||
) {
|
||||
throw new Error(t('connections.ruleDialog.profileChanged'))
|
||||
}
|
||||
}
|
||||
await checkProfile()
|
||||
const content = await readProfileFile(target.rulesUid)
|
||||
const updated = saveConnectionRule(
|
||||
content,
|
||||
{ ...selected, type: kind === 'domain' ? scope : selected.type },
|
||||
policy,
|
||||
)
|
||||
await checkProfile()
|
||||
return saveProfileFile(target.rulesUid, updated)
|
||||
},
|
||||
{ id: `save-profile-file:${target.rulesUid}`, errorNotice: false },
|
||||
)
|
||||
if (!result.ok || !result.value) {
|
||||
setError(t('connections.ruleDialog.applyFailed'))
|
||||
return
|
||||
}
|
||||
showNotice.success('connections.ruleDialog.saved')
|
||||
onClose()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog open onClose={saving ? undefined : onClose} fullWidth maxWidth="sm">
|
||||
<DialogTitle>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.modifyTitle'
|
||||
: 'connections.ruleDialog.title',
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
size="small"
|
||||
label={t('connections.ruleDialog.matchBy')}
|
||||
value={kind}
|
||||
disabled={blocked}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as ConnectionRuleKind
|
||||
const rule =
|
||||
next === 'domain'
|
||||
? (existing?.domain ?? existing?.suffix)
|
||||
: existing?.[next]
|
||||
setKind(next)
|
||||
setPolicy(rule?.policy ?? null)
|
||||
setScope(
|
||||
rule?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN',
|
||||
)
|
||||
}}
|
||||
>
|
||||
{(['domain', 'process'] as const).map((option) => (
|
||||
<MenuItem key={option} value={option} disabled={!targets[option]}>
|
||||
{kindLabels[option]}
|
||||
{!targets[option] &&
|
||||
` · ${t('connections.ruleDialog.unavailable')}`}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{kind === 'process' ? (
|
||||
<>
|
||||
{target.metadata.process?.trim() && (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={kindLabels.process}
|
||||
value={target.metadata.process}
|
||||
slotProps={{
|
||||
input: { readOnly: true },
|
||||
htmlInput: { title: target.metadata.process },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selected?.type === 'PROCESS-PATH' && (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={t('connections.ruleDialog.fullPath')}
|
||||
value={selected.value}
|
||||
slotProps={{
|
||||
input: { readOnly: true },
|
||||
htmlInput: { title: selected.value },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label={kindLabels[kind]}
|
||||
value={selected?.value ?? ''}
|
||||
slotProps={{
|
||||
input: { readOnly: true },
|
||||
htmlInput: { title: selected?.value },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{kind === 'domain' && (
|
||||
<RadioGroup
|
||||
row
|
||||
aria-label={t('connections.ruleDialog.domainScope')}
|
||||
value={scope}
|
||||
onChange={(_, value) => {
|
||||
setScope(value as 'DOMAIN' | 'DOMAIN-SUFFIX')
|
||||
setPolicy(
|
||||
existing?.[value === 'DOMAIN-SUFFIX' ? 'suffix' : 'domain']
|
||||
?.policy ?? null,
|
||||
)
|
||||
}}
|
||||
sx={{ mt: 0.5 }}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="DOMAIN"
|
||||
control={<Radio size="small" />}
|
||||
disabled={blocked}
|
||||
label={t('connections.ruleDialog.exactDomain')}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="DOMAIN-SUFFIX"
|
||||
control={<Radio size="small" />}
|
||||
disabled={blocked}
|
||||
label={t('connections.ruleDialog.allSubdomains')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
{kind === 'process' && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{selected?.type === 'PROCESS-PATH'
|
||||
? t('connections.ruleDialog.processPathHint')
|
||||
: t('connections.ruleDialog.processNameHint')}
|
||||
</Typography>
|
||||
)}
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={
|
||||
unavailablePolicy && policy ? [policy, ...policies] : policies
|
||||
}
|
||||
getOptionLabel={(value) =>
|
||||
value === 'DIRECT'
|
||||
? t('proxies.components.enums.policies.DIRECT')
|
||||
: value === 'REJECT'
|
||||
? t('proxies.components.enums.policies.REJECT')
|
||||
: value
|
||||
}
|
||||
getOptionDisabled={(value) => !policies.includes(value)}
|
||||
value={policy}
|
||||
onChange={(_, value) => setPolicy(value)}
|
||||
disabled={blocked}
|
||||
loading={loading}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('rules.modals.editor.form.labels.proxyPolicy')}
|
||||
margin="normal"
|
||||
error={unavailablePolicy}
|
||||
helperText={
|
||||
unavailablePolicy
|
||||
? t('connections.ruleDialog.policyUnavailable')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{clashConfig && clashConfig.mode !== 'rule' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
{t('connections.ruleDialog.ruleModeHint')}
|
||||
</Alert>
|
||||
)}
|
||||
{error && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="error">
|
||||
{error}
|
||||
{!existing && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setLoadAttempt((attempt) => attempt + 1)
|
||||
}}
|
||||
>
|
||||
{t('shared.actions.retry')}
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 2, overflowWrap: 'anywhere' }}
|
||||
>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.editDestination'
|
||||
: 'connections.ruleDialog.destination',
|
||||
{
|
||||
name: target.profileName,
|
||||
},
|
||||
)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block' }}
|
||||
>
|
||||
{t('connections.ruleDialog.newConnections')}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={saving} onClick={onClose}>
|
||||
{t('shared.actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
loading={saving}
|
||||
disabled={
|
||||
blocked || !selected || !policy || !policies.includes(policy)
|
||||
}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t(
|
||||
modifying
|
||||
? 'connections.ruleDialog.saveAndApply'
|
||||
: 'connections.ruleDialog.addAndApply',
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { dump, load } from 'js-yaml'
|
||||
import isFQDN from 'validator/es/lib/isFQDN'
|
||||
import isIP from 'validator/es/lib/isIP'
|
||||
|
||||
export function getConnectionRuleTargets(
|
||||
metadata: Pick<
|
||||
IConnectionsItem['metadata'],
|
||||
'host' | 'process' | 'processPath'
|
||||
>,
|
||||
) {
|
||||
const host = metadata.host?.trim() ?? ''
|
||||
const path = metadata.processPath
|
||||
const name = metadata.process
|
||||
const domain =
|
||||
host && !isIP(host) && isFQDN(host, { require_tld: false })
|
||||
? { type: 'DOMAIN', value: host }
|
||||
: undefined
|
||||
const process = path?.trim()
|
||||
? { type: 'PROCESS-PATH', value: path }
|
||||
: name?.trim()
|
||||
? { type: 'PROCESS-NAME', value: name }
|
||||
: undefined
|
||||
return { domain, process }
|
||||
}
|
||||
|
||||
function parseRuleExtension(content: string) {
|
||||
const parsed: unknown = load(content)
|
||||
if (
|
||||
parsed !== undefined &&
|
||||
parsed !== null &&
|
||||
(typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed) ||
|
||||
Object.getPrototypeOf(parsed) !== Object.prototype)
|
||||
) {
|
||||
throw new Error('The rule extension must be a YAML mapping.')
|
||||
}
|
||||
const config = (parsed ?? {}) as Record<string, unknown>
|
||||
for (const key of ['prepend', 'append', 'delete']) {
|
||||
const sequence = config[key]
|
||||
if (
|
||||
sequence != null &&
|
||||
(!Array.isArray(sequence) ||
|
||||
sequence.some((item) => typeof item !== 'string'))
|
||||
) {
|
||||
throw new Error('The rule extension contains an invalid rule list.')
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
interface RuleCondition {
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
function matchesConnectionRule(raw: string, target: RuleCondition) {
|
||||
const parts = raw.split(',').map((part) => part.trim())
|
||||
if (parts.length !== 3) return false
|
||||
if (target.type === 'DOMAIN' || target.type === 'DOMAIN-SUFFIX') {
|
||||
return (
|
||||
parts[0] === target.type &&
|
||||
parts[1].toLowerCase() === target.value.toLowerCase()
|
||||
)
|
||||
}
|
||||
return parts[0] === target.type && parts[1] === target.value
|
||||
}
|
||||
|
||||
export function findConnectionRule(content: string, target: RuleCondition) {
|
||||
const config = parseRuleExtension(content)
|
||||
for (const key of ['prepend', 'append']) {
|
||||
for (const raw of (config[key] ?? []) as string[]) {
|
||||
if (matchesConnectionRule(raw, target)) {
|
||||
const [type, , policy] = raw.split(',').map((part) => part.trim())
|
||||
return { type, policy }
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function saveConnectionRule(
|
||||
content: string,
|
||||
target: RuleCondition,
|
||||
policy: string,
|
||||
): string {
|
||||
const config = parseRuleExtension(content)
|
||||
const rule = `${target.type},${target.value},${policy}`
|
||||
let replaced = false
|
||||
for (const key of ['prepend', 'append']) {
|
||||
if (!Array.isArray(config[key])) continue
|
||||
config[key] = (config[key] as string[]).flatMap((raw) => {
|
||||
if (!matchesConnectionRule(raw, target)) return [raw]
|
||||
if (replaced) return []
|
||||
replaced = true
|
||||
return [rule]
|
||||
})
|
||||
}
|
||||
if (!replaced) {
|
||||
config.prepend = [rule, ...((config.prepend ?? []) as string[])]
|
||||
}
|
||||
return dump(config, { forceQuotes: true })
|
||||
}
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "الأعمدة",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Spalten",
|
||||
"dragHandle": "Ziehpunkt"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Columns",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Columnas",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "ستونها",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Kolom",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "列",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "열",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Colunas",
|
||||
"dragHandle": "Alça de arrastar"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Столбцы",
|
||||
"dragHandle": "Маркер перетаскивания"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Sütunlar",
|
||||
"dragHandle": "Sürükleme tutamacı"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "Баганалар",
|
||||
"dragHandle": "Drag handle"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "列设置",
|
||||
"dragHandle": "拖拽控件"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "添加规则…",
|
||||
"matchBy": "匹配对象",
|
||||
"domain": "域名",
|
||||
"unavailable": "未获取",
|
||||
"destination": "添加到「{{name}}」的前置规则。",
|
||||
"newConnections": "新建立的连接将按更新后的配置匹配。",
|
||||
"processPathHint": "匹配此路径对应进程访问的所有目标。",
|
||||
"processNameHint": "匹配所有同名进程访问的所有目标。",
|
||||
"ruleModeHint": "规则分流在规则模式下生效。",
|
||||
"addAndApply": "添加并应用",
|
||||
"profileChanged": "当前订阅已切换,请关闭弹窗后重新添加规则。",
|
||||
"applyFailed": "规则未能应用,已保留当前选择。",
|
||||
"unsupportedValue": "此内容包含快捷规则不支持的字符。",
|
||||
"saved": "规则已保存,配置已应用。",
|
||||
"noMetadata": "此连接没有可用于添加规则的域名或进程信息。",
|
||||
"noProfile": "当前没有可编辑规则扩展的活动订阅。",
|
||||
"entry": "设置规则…",
|
||||
"modifyTitle": "修改规则",
|
||||
"domainScope": "域名匹配范围",
|
||||
"exactDomain": "仅此域名",
|
||||
"allSubdomains": "此域名及所有子域名",
|
||||
"policyUnavailable": "此策略已不可用,请选择其他策略。",
|
||||
"editDestination": "修改「{{name}}」中的现有规则,保留排序。",
|
||||
"saveAndApply": "保存并应用",
|
||||
"fullPath": "完整路径"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,5 +30,32 @@
|
||||
"title": "欄位設定",
|
||||
"dragHandle": "拖曳以移動"
|
||||
}
|
||||
},
|
||||
"ruleDialog": {
|
||||
"title": "Add rule…",
|
||||
"matchBy": "Match by",
|
||||
"domain": "Domain",
|
||||
"unavailable": "Not available",
|
||||
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||
"newConnections": "New connections will match the updated configuration.",
|
||||
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||
"addAndApply": "Add and apply",
|
||||
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||
"applyFailed": "The rule could not be applied. Your selection has been kept.",
|
||||
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
|
||||
"saved": "Rule saved and configuration applied.",
|
||||
"noMetadata": "This connection has no domain or process information for adding a rule.",
|
||||
"noProfile": "No active profile with an editable rule extension is available.",
|
||||
"entry": "Configure rule…",
|
||||
"modifyTitle": "Modify rule",
|
||||
"domainScope": "Domain scope",
|
||||
"exactDomain": "Only this domain",
|
||||
"allSubdomains": "This domain and all its subdomains",
|
||||
"policyUnavailable": "This policy is no longer available. Choose another policy.",
|
||||
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
|
||||
"saveAndApply": "Save and apply",
|
||||
"fullPath": "Full path"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,31 @@ export const translationKeys = [
|
||||
'connections.components.actions.closeConnection',
|
||||
'connections.components.columnManager.title',
|
||||
'connections.components.columnManager.dragHandle',
|
||||
'connections.ruleDialog.title',
|
||||
'connections.ruleDialog.matchBy',
|
||||
'connections.ruleDialog.domain',
|
||||
'connections.ruleDialog.unavailable',
|
||||
'connections.ruleDialog.destination',
|
||||
'connections.ruleDialog.newConnections',
|
||||
'connections.ruleDialog.processPathHint',
|
||||
'connections.ruleDialog.processNameHint',
|
||||
'connections.ruleDialog.ruleModeHint',
|
||||
'connections.ruleDialog.addAndApply',
|
||||
'connections.ruleDialog.profileChanged',
|
||||
'connections.ruleDialog.applyFailed',
|
||||
'connections.ruleDialog.unsupportedValue',
|
||||
'connections.ruleDialog.saved',
|
||||
'connections.ruleDialog.noMetadata',
|
||||
'connections.ruleDialog.noProfile',
|
||||
'connections.ruleDialog.entry',
|
||||
'connections.ruleDialog.modifyTitle',
|
||||
'connections.ruleDialog.domainScope',
|
||||
'connections.ruleDialog.exactDomain',
|
||||
'connections.ruleDialog.allSubdomains',
|
||||
'connections.ruleDialog.policyUnavailable',
|
||||
'connections.ruleDialog.editDestination',
|
||||
'connections.ruleDialog.saveAndApply',
|
||||
'connections.ruleDialog.fullPath',
|
||||
'home.page.tooltips.lightweightMode',
|
||||
'home.page.tooltips.manual',
|
||||
'home.page.tooltips.settings',
|
||||
|
||||
@@ -36,6 +36,33 @@ export interface TranslationResources {
|
||||
page: {
|
||||
title: string
|
||||
}
|
||||
ruleDialog: {
|
||||
addAndApply: string
|
||||
allSubdomains: string
|
||||
applyFailed: string
|
||||
destination: string
|
||||
domain: string
|
||||
domainScope: string
|
||||
editDestination: string
|
||||
entry: string
|
||||
exactDomain: string
|
||||
fullPath: string
|
||||
matchBy: string
|
||||
modifyTitle: string
|
||||
newConnections: string
|
||||
noMetadata: string
|
||||
noProfile: string
|
||||
policyUnavailable: string
|
||||
processNameHint: string
|
||||
processPathHint: string
|
||||
profileChanged: string
|
||||
ruleModeHint: string
|
||||
saveAndApply: string
|
||||
saved: string
|
||||
title: string
|
||||
unavailable: string
|
||||
unsupportedValue: string
|
||||
}
|
||||
}
|
||||
home: {
|
||||
components: {
|
||||
|
||||
Reference in New Issue
Block a user