Skip to content

Commit 06f8fbb

Browse files
committed
feat(permissions): 实现权限管理系统增强功能
- 新增权限模式(DEFAULT/AUTO_EDIT/YOLO/PLAN)支持 - 添加交互式权限管理器UI组件 - 支持会话级权限记忆功能 - 优化权限确认流程,增加范围选择(once/session) - 重构权限检查逻辑,支持模式覆盖规则 - 添加权限模式切换快捷键(Shift+Tab) - 完善相关测试用例和文档
1 parent 1184e77 commit 06f8fbb

40 files changed

Lines changed: 2154 additions & 248 deletions

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"Bash(done)",
4040
"Bash(npm test:*)",
4141
"Bash(npm run test:integration:*)",
42-
"Bash(blade mcp:*)"
42+
"Bash(blade mcp:*)",
43+
"Bash(test:*)"
4344
],
4445
"deny": [],
4546
"ask": [],

docs/development/architecture/confirmation-flow.md

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ interface ConfirmationResponse {
109109

110110
/** 拒绝原因(如果未批准) */
111111
reason?: string;
112+
113+
/**
114+
* 授权范围
115+
* - once: 仅本次执行
116+
* - session: 记住至项目本地配置(settings.local.json)
117+
*/
118+
scope?: 'once' | 'session';
112119
}
113120
```
114121

@@ -176,17 +183,53 @@ const useConfirmation = () => {
176183

177184
```typescript
178185
const ConfirmationPrompt: React.FC<Props> = ({ details, onResponse }) => {
179-
// 使用 Ink 的 useInput hook 处理键盘输入
186+
const { isFocused } = useFocus({ autoFocus: true });
187+
180188
useInput((input, key) => {
181-
if (input === 'y' || input === 'Y') {
182-
onResponse({ approved: true });
183-
} else if (input === 'n' || input === 'N') {
184-
onResponse({ approved: false, reason: '用户拒绝' });
185-
} else if (key.escape) {
189+
if (!isFocused) return;
190+
191+
if (key.escape) {
186192
onResponse({ approved: false, reason: '用户取消' });
193+
return;
194+
}
195+
196+
const normalized = input?.toLowerCase();
197+
if (normalized === 'y') {
198+
onResponse({ approved: true, scope: 'once' });
199+
} else if (normalized === 's' || (key.shift && key.tab)) {
200+
onResponse({ approved: true, scope: 'session' });
201+
} else if (normalized === 'n') {
202+
onResponse({ approved: false, reason: '用户拒绝' });
187203
}
188204
});
189205

206+
const ItemComponent: React.FC<{ label: string; isSelected?: boolean }> = ({
207+
label,
208+
isSelected,
209+
}) => <Text color={isSelected ? 'yellow' : undefined}>{label}</Text>;
210+
211+
const options = useMemo<
212+
Array<{ label: string; key: string; value: ConfirmationResponse }>
213+
>(() => {
214+
return [
215+
{
216+
key: 'approve-once',
217+
label: '[Y] Yes (once only)',
218+
value: { approved: true, scope: 'once' },
219+
},
220+
{
221+
key: 'approve-session',
222+
label: '[S] Yes, remember for this project (Shift+Tab)',
223+
value: { approved: true, scope: 'session' },
224+
},
225+
{
226+
key: 'reject',
227+
label: '[N] No',
228+
value: { approved: false, reason: '用户拒绝' },
229+
},
230+
];
231+
}, []);
232+
190233
return (
191234
<Box flexDirection="column" borderStyle="round" borderColor="yellow">
192235
<Text bold color="yellow">🔔 需要用户确认</Text>
@@ -202,10 +245,15 @@ const ConfirmationPrompt: React.FC<Props> = ({ details, onResponse }) => {
202245
</Box>
203246
)}
204247

205-
<Text>
206-
<Text color="green" bold>[Y]</Text> 批准 /
207-
<Text color="red" bold>[N]</Text> 拒绝
208-
</Text>
248+
<Box flexDirection="column">
249+
<Text color="gray">使用 ↑ ↓ 选择,回车确认(支持 Y / S / N 快捷键,ESC 取消)</Text>
250+
<SelectInput
251+
isFocused={isFocused}
252+
items={options}
253+
itemComponent={ItemComponent}
254+
onSelect={(item) => onResponse(item.value)}
255+
/>
256+
</Box>
209257
</Box>
210258
);
211259
};
@@ -317,9 +365,10 @@ await agent.chat('删除所有测试文件', context);
317365
// 4. ConfirmationPrompt 组件渲染
318366
// → 显示确认对话框
319367

320-
// 5. 用户按键响应
321-
// → 按 'Y': onResponse({ approved: true })
322-
// → 按 'N': onResponse({ approved: false, reason: '用户拒绝' })
368+
// 5. 用户选择确认选项
369+
// → 选择“仅此一次允许”: onResponse({ approved: true, scope: 'once' })
370+
// → 选择“本会话允许”: onResponse({ approved: true, scope: 'session' })
371+
// → 选择“拒绝”: onResponse({ approved: false, reason: '用户拒绝' })
323372

324373
// 6. handleResponse 调用 resolver
325374
// → Promise 被 resolve
@@ -329,6 +378,11 @@ await agent.chat('删除所有测试文件', context);
329378
// → 如果拒绝: 调用 execution.abort()
330379
```
331380

381+
## 授权记忆
382+
383+
- 当用户选择 `scope: 'session'` 时,权限阶段会缓存当前工具调用签名,并将规则追加到 `.blade/settings.local.json`,在当前项目中长期生效。
384+
- 命中缓存时,权限阶段会直接返回允许结果,并附带原因说明,便于日志审计。
385+
332386
## 最佳实践
333387

334388
### 工具开发者
@@ -368,8 +422,8 @@ await agent.chat('删除所有测试文件', context);
368422
- 易于理解的操作提示
369423

370424
2. **良好的键盘交互**
371-
- 使用 Ink 的 `useInput` 而非直接监听 stdin
372-
- 支持多种确认方式(Y/y, N/n, ESC)
425+
- 使用 Ink 的 `useInput` 捕获 ESC 等基础事件
426+
- 借助 `ink-select-input` 提供箭头选择 + Enter 的确认体验
373427
- 防止与其他输入冲突
374428

375429
3. **状态管理**
@@ -451,10 +505,12 @@ const deleteFilesTool: ToolConfig = {
451505
│ • debug.log
452506
...还有 12 个文件 │
453507
│ │
454-
│ [Y] 批准 / [N] 拒绝 │
508+
│ › [Y] Yes (once only) │
509+
│ [S] Yes, remember for this project
510+
│ [N] No
455511
└─────────────────────────────────┘
456-
用户按 Y: 继续删除
457-
用户按 N: 中止操作,向 AI 返回"用户拒绝执行"
512+
用户通过方向键选择对应项并按回车确认
513+
选择“记住至本项目”会立刻写入 settings.local.json
458514
```
459515

460516
### 场景 2: 网络请求

0 commit comments

Comments
 (0)