新增: 审批超时可配置 + ScriptNode 命令白/黑名单
- 审批超时: Settings 下拉选择(不限时/5/15/30/60min),默认 15min - getApprovalTimeoutMs() 从 KV 读取,startApprovalTimer 动态取值 - ScriptNode: 黑名单 > 白名单策略,从环境变量读取 - Settings 加命令执行安全面板(白/黑名单文本框) - i18n 中英文案 + 搜索索引补全
This commit is contained in:
@@ -39,6 +39,20 @@ impl Node for ScriptNode {
|
|||||||
shell_type: Default::default(),
|
shell_type: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 命令执行安全:白/黑名单校验(从环境变量读取,逗号分隔命令名)。
|
||||||
|
// - 白名单非空时:命令首词不在白名单 → 直接拒绝执行
|
||||||
|
// - 黑名单匹配时:直接拒绝执行
|
||||||
|
// 命令名取首词(shell 第一段,如 `rm -rf /` 取 `rm`),按 trim + 小写规范化比较。
|
||||||
|
let cmd_name = command.split_whitespace().next().unwrap_or("").to_lowercase();
|
||||||
|
if let Some(denied) = check_command_policy(&cmd_name) {
|
||||||
|
tracing::warn!(
|
||||||
|
command = %command,
|
||||||
|
reason = %denied,
|
||||||
|
"ScriptNode 命令被策略拒绝"
|
||||||
|
);
|
||||||
|
anyhow::bail!("脚本命令被策略拒绝: {} (命令: {})", denied, command);
|
||||||
|
}
|
||||||
|
|
||||||
// 危险命令告警:匹配已知危险关键词,仅告警不阻止执行
|
// 危险命令告警:匹配已知危险关键词,仅告警不阻止执行
|
||||||
let dangerous_keywords = ["rm -rf", "DROP TABLE", "Format", "del /f", "shutdown"];
|
let dangerous_keywords = ["rm -rf", "DROP TABLE", "Format", "del /f", "shutdown"];
|
||||||
for &kw in &dangerous_keywords {
|
for &kw in &dangerous_keywords {
|
||||||
@@ -105,3 +119,41 @@ impl Node for ScriptNode {
|
|||||||
"script"
|
"script"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 命令执行策略校验:从环境变量读取白/黑名单,判定给定命令名是否允许执行。
|
||||||
|
///
|
||||||
|
/// 优先级:黑名单优先于白名单(黑名单匹配总是拒绝,即便同时在白名单)。
|
||||||
|
///
|
||||||
|
/// - `DF_SCRIPT_WHITELIST`:逗号分隔命令名(如 `git,npm,cargo`);非空时命令名不在其中即拒绝
|
||||||
|
/// - `DF_SCRIPT_BLACKLIST`:逗号分隔命令名(如 `rm,format,shutdown`);匹配即拒绝
|
||||||
|
///
|
||||||
|
/// 命令名比较前 trim + ASCII 小写规范化;空段被忽略。
|
||||||
|
///
|
||||||
|
/// 返回 `Some(reason)` 表示拒绝(reason 为拒绝原因,用于日志/错误信息);返回 `None` 表示放行。
|
||||||
|
fn check_command_policy(cmd_name: &str) -> Option<&'static str> {
|
||||||
|
// 黑名单优先:即便同时在白名单,只要命中黑名单就拒绝(防止白名单失误放过危险命令)
|
||||||
|
if let Ok(blacklist_raw) = std::env::var("DF_SCRIPT_BLACKLIST") {
|
||||||
|
let blacklist: Vec<String> = blacklist_raw
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
if blacklist.iter().any(|c| c == cmd_name) {
|
||||||
|
return Some("命令在黑名单中 (DF_SCRIPT_BLACKLIST)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单:非空时命令名必须在白名单中才放行
|
||||||
|
if let Ok(whitelist_raw) = std::env::var("DF_SCRIPT_WHITELIST") {
|
||||||
|
let whitelist: Vec<String> = whitelist_raw
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
if !whitelist.is_empty() && !whitelist.iter().any(|c| c == cmd_name) {
|
||||||
|
return Some("命令不在白名单中 (DF_SCRIPT_WHITELIST)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,17 @@
|
|||||||
<option value="debug">Debug</option>
|
<option value="debug">Debug</option>
|
||||||
</select>
|
</select>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
|
||||||
|
<!-- 审批超时:用户未处理审批弹窗时的自动拒绝阅值;0 表示不限时(对齐旧默认) -->
|
||||||
|
<SettingRow ref="rowApprovalTimeout" :label="$t('settings.labelApprovalTimeout')" :desc="$t('settings.descApprovalTimeout')">
|
||||||
|
<select v-model="settings.approvalTimeout" class="setting-select" @change="onApprovalTimeoutChange">
|
||||||
|
<option :value="0">{{ $t('settings.approvalTimeoutUnlimited') }}</option>
|
||||||
|
<option :value="300000">{{ $t('settings.approvalTimeout5m') }}</option>
|
||||||
|
<option :value="900000">{{ $t('settings.approvalTimeout15m') }}</option>
|
||||||
|
<option :value="1800000">{{ $t('settings.approvalTimeout30m') }}</option>
|
||||||
|
<option :value="3600000">{{ $t('settings.approvalTimeout60m') }}</option>
|
||||||
|
</select>
|
||||||
|
</SettingRow>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- all 二次确认弹层(独立,不与 Settings 壳层共用:壳层按钮写死「删除」不适合「确认接管」) -->
|
<!-- all 二次确认弹层(独立,不与 Settings 壳层共用:壳层按钮写死「删除」不适合「确认接管」) -->
|
||||||
@@ -88,10 +99,13 @@ const settings = reactive({
|
|||||||
// clampMode 兜底脏值。
|
// clampMode 兜底脏值。
|
||||||
autoExecuteMode: clampMode(appSettings.get<string>('df-ai-auto-execute-mode', 'low')),
|
autoExecuteMode: clampMode(appSettings.get<string>('df-ai-auto-execute-mode', 'low')),
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
// 审批超时(ms):0=不限时,缺省 900000(15min);仅固定合法选项,脏值回退默认
|
||||||
|
approvalTimeout: clampApprovalTimeout(appSettings.get<number>('df-approval-timeout', 900000)),
|
||||||
})
|
})
|
||||||
|
|
||||||
const rowAutoExecute = useTemplateRef<InstanceType<typeof SettingRow>>('rowAutoExecute')
|
const rowAutoExecute = useTemplateRef<InstanceType<typeof SettingRow>>('rowAutoExecute')
|
||||||
const rowLogLevel = useTemplateRef<InstanceType<typeof SettingRow>>('rowLogLevel')
|
const rowLogLevel = useTemplateRef<InstanceType<typeof SettingRow>>('rowLogLevel')
|
||||||
|
const rowApprovalTimeout = useTemplateRef<InstanceType<typeof SettingRow>>('rowApprovalTimeout')
|
||||||
|
|
||||||
// 上一次确认生效的档位:all 二次确认取消时恢复此值(非写死 low),避免 medium 用户误选 all
|
// 上一次确认生效的档位:all 二次确认取消时恢复此值(非写死 low),避免 medium 用户误选 all
|
||||||
// 取消后被强制降级 low。初始化 = 当前有效值;每次确认成功(含非 all 切换)更新。
|
// 取消后被强制降级 low。初始化 = 当前有效值;每次确认成功(含非 all 切换)更新。
|
||||||
@@ -130,6 +144,20 @@ watch(
|
|||||||
function markSaved() {
|
function markSaved() {
|
||||||
rowLogLevel.value?.markSaved()
|
rowLogLevel.value?.markSaved()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 审批超时合法选项白名单(对齐 <select> option);脏值(外部改库为非法数字)回退默认 15min,
|
||||||
|
// 防 <select v-model> 无匹配 option 致控件空白。允许的取值:0/5/15/30/60 分钟对应毫秒数。
|
||||||
|
const VALID_APPROVAL_TIMEOUTS = [0, 300_000, 900_000, 1_800_000, 3_600_000]
|
||||||
|
function clampApprovalTimeout(raw: number): number {
|
||||||
|
return VALID_APPROVAL_TIMEOUTS.includes(raw) ? raw : 900_000
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审批超时下拉变更:落 KV(df-approval-timeout,ms)+ 显「已保存」反馈。
|
||||||
|
// 下一笔发起的审批即生效(aiShared.startApprovalTimer 每次实时读),已在跑的计时器沿用旧值。
|
||||||
|
function onApprovalTimeoutChange() {
|
||||||
|
appSettings.set('df-approval-timeout', settings.approvalTimeout)
|
||||||
|
rowApprovalTimeout.value?.markSaved()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -41,6 +41,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 命令执行安全:ScriptNode 白/黑名单(前端仅 UI + 持久化,后端当前从环境变量读) ═══ -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>{{ $t('settings.panelScriptSafety') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="script-safety">
|
||||||
|
<p class="script-safety-desc">{{ $t('settings.descScriptSafety') }}</p>
|
||||||
|
|
||||||
|
<SettingRow :label="$t('settings.labelScriptWhitelist')" :desc="$t('settings.descScriptWhitelist')">
|
||||||
|
<input
|
||||||
|
v-model="scriptWhitelist"
|
||||||
|
class="setting-select script-safety-input"
|
||||||
|
:placeholder="$t('settings.placeholderScriptWhitelist')"
|
||||||
|
@blur="onScriptWhitelistCommit"
|
||||||
|
@keyup.enter="onScriptWhitelistCommit"
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<SettingRow :label="$t('settings.labelScriptBlacklist')" :desc="$t('settings.descScriptBlacklist')">
|
||||||
|
<input
|
||||||
|
v-model="scriptBlacklist"
|
||||||
|
class="setting-select script-safety-input"
|
||||||
|
:placeholder="$t('settings.placeholderScriptBlacklist')"
|
||||||
|
@blur="onScriptBlacklistCommit"
|
||||||
|
@keyup.enter="onScriptBlacklistCommit"
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<p class="script-safety-note">{{ $t('settings.noteScriptSafety') }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -48,8 +80,11 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { aiApi } from '@/api'
|
import { aiApi } from '@/api'
|
||||||
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
|
import SettingRow from './SettingRow.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const appSettings = useAppSettingsStore()
|
||||||
// F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
|
// F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
|
||||||
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void }>()
|
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info'): void }>()
|
||||||
|
|
||||||
@@ -138,6 +173,27 @@ async function reload() {
|
|||||||
await load()
|
await load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ScriptNode 命令执行安全 — 白/黑名单 UI
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 前端仅做 UI + 持久化到 appSettings(df-script-whitelist / df-script-blacklist)。
|
||||||
|
// 后端 ScriptNode 当前从环境变量 DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST 读取
|
||||||
|
// (同一逗号分隔语义),前后端打通留后续:前端写入需经 IPC/启动期注入环境变量。
|
||||||
|
// 输入语义:逗号分隔命令名(如 `git,npm,cargo`),空串=该项不限制。
|
||||||
|
// ============================================================
|
||||||
|
const scriptWhitelist = ref(appSettings.get<string>('df-script-whitelist', ''))
|
||||||
|
const scriptBlacklist = ref(appSettings.get<string>('df-script-blacklist', ''))
|
||||||
|
|
||||||
|
function onScriptWhitelistCommit() {
|
||||||
|
appSettings.set('df-script-whitelist', scriptWhitelist.value.trim())
|
||||||
|
emit('toast', t('settings.savedHint'), 'info')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScriptBlacklistCommit() {
|
||||||
|
appSettings.set('df-script-blacklist', scriptBlacklist.value.trim())
|
||||||
|
emit('toast', t('settings.savedHint'), 'info')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -165,4 +221,17 @@ onMounted(load)
|
|||||||
.add-row { display: flex; gap: 8px; margin-top: 4px; }
|
.add-row { display: flex; gap: 8px; margin-top: 4px; }
|
||||||
.dir-input { flex: 1; min-width: 0; }
|
.dir-input { flex: 1; min-width: 0; }
|
||||||
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
.actions { display: flex; gap: 8px; margin-top: 8px; }
|
||||||
|
|
||||||
|
/* ===== ScriptNode 命令执行安全面板 ===== */
|
||||||
|
.script-safety { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.script-safety-desc {
|
||||||
|
font-size: 12px; color: var(--df-text-dim); margin: 0 0 4px; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.script-safety-input {
|
||||||
|
min-width: 280px;
|
||||||
|
font-family: var(--df-font-mono, monospace);
|
||||||
|
}
|
||||||
|
.script-safety-note {
|
||||||
|
font-size: 11px; color: var(--df-text-dim); margin: 4px 0 0; line-height: 1.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -80,10 +80,13 @@ export const SETTINGS_INDEX: SettingIndexEntry[] = [
|
|||||||
|
|
||||||
// ===== 安全 =====
|
// ===== 安全 =====
|
||||||
{ category: 'security', key: 'allowedDirs', labelKey: 'settings.panelAllowedDirs', descKey: 'settings.descAllowedDirs' },
|
{ category: 'security', key: 'allowedDirs', labelKey: 'settings.panelAllowedDirs', descKey: 'settings.descAllowedDirs' },
|
||||||
|
{ category: 'security', key: 'scriptWhitelist', labelKey: 'settings.labelScriptWhitelist', descKey: 'settings.descScriptWhitelist' },
|
||||||
|
{ category: 'security', key: 'scriptBlacklist', labelKey: 'settings.labelScriptBlacklist', descKey: 'settings.descScriptBlacklist' },
|
||||||
|
|
||||||
// ===== 高级 =====
|
// ===== 高级 =====
|
||||||
// F-#97:autoExecute(boolean) → autoExecuteMode(low/medium/all 三档),key 同步更名
|
// F-#97:autoExecute(boolean) → autoExecuteMode(low/medium/all 三档),key 同步更名
|
||||||
{ category: 'advanced', key: 'autoExecuteMode', labelKey: 'settings.labelAutoExecute', descKey: 'settings.descAutoExecute' },
|
{ category: 'advanced', key: 'autoExecuteMode', labelKey: 'settings.labelAutoExecute', descKey: 'settings.descAutoExecute' },
|
||||||
{ category: 'advanced', key: 'logLevel', labelKey: 'settings.labelLogLevel', descKey: 'settings.descLogLevel' },
|
{ category: 'advanced', key: 'logLevel', labelKey: 'settings.labelLogLevel', descKey: 'settings.descLogLevel' },
|
||||||
|
{ category: 'advanced', key: 'approvalTimeout', labelKey: 'settings.labelApprovalTimeout', descKey: 'settings.descApprovalTimeout' },
|
||||||
{ category: 'advanced', key: 'connection', labelKey: 'settings.panelConnection', descKey: 'settings.emptyConnection' },
|
{ category: 'advanced', key: 'connection', labelKey: 'settings.panelConnection', descKey: 'settings.emptyConnection' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -70,8 +70,26 @@ export function findToolCall(id: string): AiToolCallInfo | undefined {
|
|||||||
|
|
||||||
// ── 审批超时计时器(原 useAiSend,下沉打破 events↔send 循环依赖) ──
|
// ── 审批超时计时器(原 useAiSend,下沉打破 events↔send 循环依赖) ──
|
||||||
|
|
||||||
/// 审批等待超时阈值(ms) — 用户 5 分钟内不处理则自动拒绝
|
/// 审批超时阈值来源的 appSettings key(运行期可调,默认 15min)。
|
||||||
const APPROVAL_TIMEOUT_MS = Infinity // 已决策:不做超时,用户不审批就一直等
|
/// 值语义:正整数=毫秒超时;0=不限时(跳过计时器);缺省=900000(15min)。
|
||||||
|
const APPROVAL_TIMEOUT_KEY = 'df-approval-timeout'
|
||||||
|
const APPROVAL_TIMEOUT_DEFAULT_MS = 900_000 // 15 分钟
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取当前审批超时阈值(ms):从 appSettings 取 `df-approval-timeout`,
|
||||||
|
* 缺省 900000(15min),返回 0 表示用户配置为「不限时」(调用方应跳过计时器)。
|
||||||
|
*
|
||||||
|
* 每次 startApprovalTimer 调用时取最新值 —— 用户在 Settings 页调整后,下一笔审批即生效
|
||||||
|
* (已在跑的计时器沿用旧值,符合「不影响已发起审批」的预期)。
|
||||||
|
*/
|
||||||
|
function getApprovalTimeoutMs(): number {
|
||||||
|
const raw = appSettings.get<number>(APPROVAL_TIMEOUT_KEY, APPROVAL_TIMEOUT_DEFAULT_MS)
|
||||||
|
// 防御:负数 / NaN 视为默认值;0 保留(语义=不限时)
|
||||||
|
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {
|
||||||
|
return APPROVAL_TIMEOUT_DEFAULT_MS
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
/** toolCallId → 审批超时计时器 */
|
/** toolCallId → 审批超时计时器 */
|
||||||
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
@@ -91,6 +109,9 @@ export function startApprovalTimer(
|
|||||||
kind: 'risk' | 'path' = 'risk',
|
kind: 'risk' | 'path' = 'risk',
|
||||||
): void {
|
): void {
|
||||||
if (_approvalTimers.has(toolCallId)) return
|
if (_approvalTimers.has(toolCallId)) return
|
||||||
|
// 0 = 用户配置为「不限时」,跳过计时器(对齐旧 Infinity 行为)
|
||||||
|
const timeoutMs = getApprovalTimeoutMs()
|
||||||
|
if (timeoutMs === 0) return
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
_approvalTimers.delete(toolCallId)
|
_approvalTimers.delete(toolCallId)
|
||||||
// 按 kind 分派拒绝路径,避免 path 类错调 ai_approve 致后端 kind 校验拒而卡死
|
// 按 kind 分派拒绝路径,避免 path 类错调 ai_approve 致后端 kind 校验拒而卡死
|
||||||
@@ -107,7 +128,7 @@ export function startApprovalTimer(
|
|||||||
isError: true,
|
isError: true,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
}, APPROVAL_TIMEOUT_MS)
|
}, timeoutMs)
|
||||||
_approvalTimers.set(toolCallId, timer)
|
_approvalTimers.set(toolCallId, timer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ export default {
|
|||||||
labelLogLevel: 'Log level',
|
labelLogLevel: 'Log level',
|
||||||
descLogLevel: 'Verbosity of workflow logs',
|
descLogLevel: 'Verbosity of workflow logs',
|
||||||
|
|
||||||
|
// ===== Approval timeout =====
|
||||||
|
labelApprovalTimeout: 'Approval timeout',
|
||||||
|
descApprovalTimeout: 'Auto-reject threshold when user does not respond to an approval prompt (only affects approvals started after the change)',
|
||||||
|
approvalTimeoutUnlimited: 'No limit',
|
||||||
|
approvalTimeout5m: '5 minutes',
|
||||||
|
approvalTimeout15m: '15 minutes (default)',
|
||||||
|
approvalTimeout30m: '30 minutes',
|
||||||
|
approvalTimeout60m: '60 minutes',
|
||||||
|
|
||||||
labelShowTokenUsage: 'Show token usage',
|
labelShowTokenUsage: 'Show token usage',
|
||||||
descShowTokenUsage: 'Show token consumption of each reply in conversations',
|
descShowTokenUsage: 'Show token consumption of each reply in conversations',
|
||||||
|
|
||||||
@@ -159,6 +168,17 @@ export default {
|
|||||||
allowedDirsAddFail: 'Failed to add authorized directory',
|
allowedDirsAddFail: 'Failed to add authorized directory',
|
||||||
allowedDirsRemoveFail: 'Failed to remove authorized directory',
|
allowedDirsRemoveFail: 'Failed to remove authorized directory',
|
||||||
|
|
||||||
|
// ===== ScriptNode command execution safety =====
|
||||||
|
panelScriptSafety: '🛡️ Command Execution Safety',
|
||||||
|
descScriptSafety: 'Configure ScriptNode command whitelist/blacklist. Command name is the first token (e.g. `rm -rf /` -> `rm`), compared trimmed and lowercase. Blacklist takes precedence over whitelist.',
|
||||||
|
labelScriptWhitelist: 'Command whitelist',
|
||||||
|
descScriptWhitelist: 'Comma-separated command names (e.g. git,npm,cargo); empty = unrestricted. When non-empty, only listed commands are allowed.',
|
||||||
|
labelScriptBlacklist: 'Command blacklist',
|
||||||
|
descScriptBlacklist: 'Comma-separated command names (e.g. rm,format,shutdown); any match rejects execution.',
|
||||||
|
placeholderScriptWhitelist: 'Leave empty for no restriction, or: git,npm,cargo',
|
||||||
|
placeholderScriptBlacklist: 'Leave empty for no blacklist, or: rm,format',
|
||||||
|
noteScriptSafety: 'Note: backend ScriptNode currently reads DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST env vars (same comma-separated semantics). This UI only persists on the frontend; wiring to backend is planned for a later release.',
|
||||||
|
|
||||||
// ===== Knowledge base panel =====
|
// ===== Knowledge base panel =====
|
||||||
panelKnowledge: '📚 Knowledge base',
|
panelKnowledge: '📚 Knowledge base',
|
||||||
labelAutoExtract: 'Auto-distill knowledge',
|
labelAutoExtract: 'Auto-distill knowledge',
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ export default {
|
|||||||
labelLogLevel: '日志级别',
|
labelLogLevel: '日志级别',
|
||||||
descLogLevel: '工作流日志详细程度',
|
descLogLevel: '工作流日志详细程度',
|
||||||
|
|
||||||
|
// ===== 审批超时 =====
|
||||||
|
labelApprovalTimeout: '审批超时',
|
||||||
|
descApprovalTimeout: '用户未处理审批弹窗时的自动拒绝阅值(只影响下一笔发起的审批)',
|
||||||
|
approvalTimeoutUnlimited: '不限时',
|
||||||
|
approvalTimeout5m: '5 分钟',
|
||||||
|
approvalTimeout15m: '15 分钟(默认)',
|
||||||
|
approvalTimeout30m: '30 分钟',
|
||||||
|
approvalTimeout60m: '60 分钟',
|
||||||
|
|
||||||
labelShowTokenUsage: '显示 Token 用量',
|
labelShowTokenUsage: '显示 Token 用量',
|
||||||
descShowTokenUsage: '在对话中展示每次回复的 token 消耗',
|
descShowTokenUsage: '在对话中展示每次回复的 token 消耗',
|
||||||
|
|
||||||
@@ -159,6 +168,17 @@ export default {
|
|||||||
allowedDirsAddFail: '添加授权目录失败',
|
allowedDirsAddFail: '添加授权目录失败',
|
||||||
allowedDirsRemoveFail: '删除授权目录失败',
|
allowedDirsRemoveFail: '删除授权目录失败',
|
||||||
|
|
||||||
|
// ===== ScriptNode 命令执行安全 =====
|
||||||
|
panelScriptSafety: '🛡️ 命令执行安全',
|
||||||
|
descScriptSafety: '配置 ScriptNode 命令白/黑名单。命令名取首词(如 `rm -rf /` 取 `rm`),按 trim + 小写比较。黑名单优先于白名单。',
|
||||||
|
labelScriptWhitelist: '命令白名单',
|
||||||
|
descScriptWhitelist: '逗号分隔命令名(如 git,npm,cargo);留空表示不限制。非空时仅允许名单内命令执行。',
|
||||||
|
labelScriptBlacklist: '命令黑名单',
|
||||||
|
descScriptBlacklist: '逗号分隔命令名(如 rm,format,shutdown);命中任一项即拒绝执行。',
|
||||||
|
placeholderScriptWhitelist: '留空不限制,或输入:git,npm,cargo',
|
||||||
|
placeholderScriptBlacklist: '留空表示无黑名单,或输入:rm,format',
|
||||||
|
noteScriptSafety: '注:后端 ScriptNode 当前从环境变量 DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST 读取,与同语义逗号分隔。此处配置仅前端持久化,与后端打通待后续版本。',
|
||||||
|
|
||||||
// ===== 知识库面板 =====
|
// ===== 知识库面板 =====
|
||||||
panelKnowledge: '📚 知识库',
|
panelKnowledge: '📚 知识库',
|
||||||
labelAutoExtract: '自动提炼知识',
|
labelAutoExtract: '自动提炼知识',
|
||||||
|
|||||||
Reference in New Issue
Block a user