Skip to content

Commit 8afc45e

Browse files
committed
refactor: 统一工具名称与上游 gemini-cli 对齐
- 工具名称: Bash→run_shell_command, Read→read_file, Write→write_file, Edit→replace - Hook 系统: 更新为 4 层配置 (Project→User→System→Extensions) - MessageBus: 添加 TOOL_POLICY_REJECTION, UPDATE_POLICY 类型 - Policy: auto-saved.toml 持久化路径 - Shell: inactivityTimeout 机制 - Extension: Skills/Hooks 安全披露 (Consent) - ToolDeveloperGuide: messageBus 构造函数签名
1 parent 8eb42c3 commit 8afc45e

34 files changed

Lines changed: 802 additions & 716 deletions

src/pages/AgentFramework.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,10 @@ export class LocalAgentExecutor<TOutput> {
314314
onActivity?: ActivityCallback,
315315
): Promise<LocalAgentExecutor<TOutput>> {
316316
// 创建隔离的工具注册表
317-
const agentToolRegistry = new ToolRegistry(runtimeContext);
317+
const agentToolRegistry = new ToolRegistry(
318+
runtimeContext,
319+
runtimeContext.getMessageBus(),
320+
);
318321
// ... 注册 Agent 可用的工具
319322
return new LocalAgentExecutor(definition, runtimeContext, agentToolRegistry);
320323
}
@@ -473,7 +476,7 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<DelegateParams, Too
473476
constructor(
474477
private readonly registry: AgentRegistry,
475478
private readonly config: Config,
476-
messageBus?: MessageBus,
479+
messageBus: MessageBus,
477480
) {
478481
const definitions = registry.getAllDefinitions();
479482
@@ -497,6 +500,9 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<DelegateParams, Too
497500
registry.getToolDescription(), // 动态描述包含所有可用 Agent
498501
Kind.Think,
499502
zodToJsonSchema(schema),
503+
messageBus,
504+
true, // isOutputMarkdown
505+
true, // canUpdateOutput(子代理执行过程可流式输出)
500506
);
501507
}
502508
}
@@ -507,7 +513,7 @@ class DelegateInvocation extends BaseToolInvocation<DelegateParams, ToolResult>
507513
const definition = this.registry.getDefinition(this.params.agent_name);
508514
509515
// 使用 SubagentToolWrapper 创建实际执行
510-
const wrapper = new SubagentToolWrapper(definition, this.config);
516+
const wrapper = new SubagentToolWrapper(definition, this.config, this.messageBus);
511517
const { agent_name, ...agentArgs } = this.params;
512518
const invocation = wrapper.build(agentArgs);
513519

src/pages/AgentLoopAnimation.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,19 @@ export class LocalAgentExecutor<TOutput> {
6565
onActivity?: ActivityCallback,
6666
): Promise<LocalAgentExecutor<TOutput>> {
6767
// 创建隔离的工具注册表
68-
const agentToolRegistry = new ToolRegistry(runtimeContext);
68+
const agentToolRegistry = new ToolRegistry(
69+
runtimeContext,
70+
runtimeContext.getMessageBus(),
71+
);
6972
7073
// 只注册 Agent 定义中声明的工具
7174
for (const toolName of definition.toolConfig?.tools ?? []) {
7275
const tool = getToolByName(toolName);
73-
if (tool) agentToolRegistry.register(tool);
76+
if (tool) agentToolRegistry.registerTool(tool);
7477
}
7578
7679
// 注入 complete_task 工具(必须)
77-
agentToolRegistry.register(
80+
agentToolRegistry.registerTool(
7881
createCompleteTaskTool(definition.outputConfig)
7982
);
8083

src/pages/AgentSkills.tsx

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export function AgentSkills() {
4343

4444
<Layer title="技能发现(Discovery)" icon="🗂️">
4545
<p className="text-gray-300 mb-4">
46-
skills 启用后,<code>SkillManager</code> 会从用户目录和项目目录扫描 <code>*/SKILL.md</code>
47-
项目级 skill 会覆盖同名用户级 skill(同名以 YAML frontmatter 的 <code>name</code> 为准)。
46+
skills 启用后,<code>SkillManager</code> 会扫描 <code>*/SKILL.md</code> 并汇总为“可用技能清单”。
47+
覆盖优先级为:<strong>Extension(最低) → User → Project(最高)</strong>(同名以 YAML frontmatter 的 <code>name</code> 为准)。
4848
</p>
4949

5050
<CodeBlock
@@ -61,14 +61,26 @@ getProjectSkillsDir(): string {
6161

6262
<CodeBlock
6363
title="发现顺序与覆盖(skillManager.ts)"
64-
code={`// packages/core/src/services/skillManager.ts
65-
// 1) User skills first
66-
const userSkills = await this.discoverSkillsInternal([Storage.getUserSkillsDir()]);
67-
this.addSkillsWithPrecedence(userSkills);
68-
69-
// 2) Project skills second (overwrites user skills with same name)
70-
const projectSkills = await this.discoverSkillsInternal([storage.getProjectSkillsDir()]);
71-
this.addSkillsWithPrecedence(projectSkills);`}
64+
code={`// packages/core/src/skills/skillManager.ts
65+
// Precedence: Extensions (lowest) -> User -> Project (highest).
66+
async discoverSkills(storage: Storage, extensions: GeminiCLIExtension[] = []) {
67+
this.clearSkills();
68+
69+
// 1) Extension skills (lowest)
70+
for (const extension of extensions) {
71+
if (extension.isActive && extension.skills) {
72+
this.addSkillsWithPrecedence(extension.skills);
73+
}
74+
}
75+
76+
// 2) User skills
77+
const userSkills = await loadSkillsFromDir(Storage.getUserSkillsDir());
78+
this.addSkillsWithPrecedence(userSkills);
79+
80+
// 3) Project skills (highest, overrides)
81+
const projectSkills = await loadSkillsFromDir(storage.getProjectSkillsDir());
82+
this.addSkillsWithPrecedence(projectSkills);
83+
}`}
7284
/>
7385

7486
<CodeBlock
@@ -114,6 +126,26 @@ return {
114126
</div>
115127
</Layer>
116128

129+
<Layer title="Schema 收敛:把 name 变成 enum" icon="🧷">
130+
<p className="text-gray-300 mb-4">
131+
技能列表发现完成后,CLI 会<strong>重新注册一次</strong> <code>ActivateSkillTool</code>,让参数 <code>name</code>
132+
<code>string</code> 收敛为 <code>enum(availableSkillNames)</code>,从而减少模型“瞎猜技能名”的概率。
133+
</p>
134+
<CodeBlock
135+
title="config.ts:发现技能后重注册工具"
136+
code={`// packages/core/src/config/config.ts (节选)
137+
if (this.skillsSupport) {
138+
await this.getSkillManager().discoverSkills(this.storage, this.getExtensions());
139+
this.getSkillManager().setDisabledSkills(this.disabledSkills);
140+
141+
// Re-register ActivateSkillTool to update its schema with discovered skill enums
142+
if (this.getSkillManager().getSkills().length > 0) {
143+
this.getToolRegistry().registerTool(new ActivateSkillTool(this, this.messageBus));
144+
}
145+
}`}
146+
/>
147+
</Layer>
148+
117149
<Layer title="System Prompt 注入" icon="🧱">
118150
<p className="text-gray-300 mb-4">
119151
当存在可用 skills 时,System Prompt 会追加一个 <code>Available Agent Skills</code> 段落,列出技能元信息,并要求模型:
@@ -151,8 +183,31 @@ skills.disabled: string[] # List of disabled skills (restart required)`}
151183
/>
152184
</Layer>
153185

186+
<Layer title="扩展技能与安全披露" icon="🛡️">
187+
<p className="text-gray-300 mb-4">
188+
Extension 可以携带 <code>skills/</code> 目录(例如 <code>skills/my-skill/SKILL.md</code>)。在安装/更新扩展时,CLI 会在 consent
189+
文本中明确提示:<strong>Agent skills 会把指令注入 system prompt</strong>,并展示每个 skill 的名称、描述与文件位置,要求用户确认后才继续安装。
190+
</p>
191+
<CodeBlock
192+
title="extensions/consent.ts:skills 风险提示(节选)"
193+
code={`// packages/cli/src/config/extensions/consent.ts
194+
export const SKILLS_WARNING_MESSAGE = chalk.yellow(
195+
"Agent skills inject specialized instructions and domain-specific knowledge into the agent's system prompt..."
196+
);
197+
198+
if (skills.length > 0) {
199+
output.push("Agent Skills:");
200+
output.push(SKILLS_WARNING_MESSAGE);
201+
output.push("This extension will install the following agent skills:");
202+
for (const skill of skills) {
203+
output.push(\` * \${skill.name}: \${skill.description}\`);
204+
output.push(\` (Location: \${skill.location})\`);
205+
}
206+
}`}
207+
/>
208+
</Layer>
209+
154210
<RelatedPages pages={relatedPages} />
155211
</div>
156212
);
157213
}
158-

src/pages/ChatCompressionAnimation.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ const SAMPLE_CONTENTS: ContentBlock[] = [
6363
{ id: 0, role: 'user', tokens: 150, preview: '请帮我分析这段代码...', isSafeBoundary: false },
6464
{ id: 1, role: 'model', tokens: 800, preview: '好的,我来分析这段代码...', isSafeBoundary: true },
6565
{ id: 2, role: 'user', tokens: 50, preview: '请执行这个命令', isSafeBoundary: false },
66-
{ id: 3, role: 'tool_use', tokens: 100, preview: 'Bash: npm run build', isSafeBoundary: false },
66+
{ id: 3, role: 'tool_use', tokens: 100, preview: 'run_shell_command: npm run build', isSafeBoundary: false },
6767
{ id: 4, role: 'tool_result', tokens: 500, preview: 'Build completed successfully...', isSafeBoundary: true },
6868
{ id: 5, role: 'model', tokens: 400, preview: '构建成功,让我解释结果...', isSafeBoundary: true },
6969
{ id: 6, role: 'user', tokens: 80, preview: '读取配置文件', isSafeBoundary: false },
70-
{ id: 7, role: 'tool_use', tokens: 50, preview: 'Read: config.json', isSafeBoundary: false },
70+
{ id: 7, role: 'tool_use', tokens: 50, preview: 'read_file: config.json', isSafeBoundary: false },
7171
{ id: 8, role: 'tool_result', tokens: 200, preview: '{ "name": "project"...', isSafeBoundary: true },
7272
{ id: 9, role: 'model', tokens: 350, preview: '配置文件内容如下...', isSafeBoundary: true },
7373
{ id: 10, role: 'user', tokens: 120, preview: '请修改这个函数...', isSafeBoundary: false },

src/pages/ChatRecordingAnimation.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,13 @@ export default function ChatRecordingAnimation() {
175175
// AI starts thinking
176176
recordThought('用户想要读取一个 JSON 配置文件');
177177
await sleep(300);
178-
recordThought('我需要使用 Read 工具来读取文件内容');
178+
recordThought('我需要使用 read_file 工具来读取文件内容');
179179
await sleep(300);
180180

181181
// AI calls tool
182182
recordToolCalls([{
183183
id: 'call_' + generateId(),
184-
name: 'Read',
184+
name: 'read_file',
185185
status: 'completed',
186186
}]);
187187
await sleep(500);
@@ -306,7 +306,7 @@ export default function ChatRecordingAnimation() {
306306
💭 添加思考
307307
</button>
308308
<button
309-
onClick={() => recordToolCalls([{ id: 'call_' + generateId(), name: 'Read', status: 'completed' }])}
309+
onClick={() => recordToolCalls([{ id: 'call_' + generateId(), name: 'read_file', status: 'completed' }])}
310310
disabled={isSimulating}
311311
className="px-3 py-2 bg-green-500/20 text-green-400 border border-green-500/30 rounded-lg text-sm hover:bg-green-500/30 disabled:opacity-50"
312312
>

src/pages/CommandInjectionDetectionAnimation.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,6 @@ export default function CommandInjectionDetectionAnimation() {
7272

7373
// 模拟安全检测
7474
const analyzeCommand = useCallback((cmd: string): CommandAnalysis => {
75-
const segments = cmd
76-
.split(/\s*(?:&&|\|\||;|\|)\s*/)
77-
.map((s) => s.trim())
78-
.filter(Boolean);
79-
const rootCommand = segments[0]?.split(/\s+/)[0] || '';
8075
const checks: SecurityCheck[] = [];
8176

8277
const hasBalancedQuotes = (value: string) => {
@@ -85,6 +80,18 @@ export default function CommandInjectionDetectionAnimation() {
8580
return doubleQuotes % 2 === 0 && singleQuotes % 2 === 0;
8681
};
8782

83+
// 1) 解析是否可控(上游:parseCommandDetails()/splitCommands();这里用“引号平衡”做近似演示)
84+
// - shell-permissions.checkCommandPermissions(): 解析失败 => Hard DENY(无法安全拆分命令)
85+
// - PolicyEngine.checkShellCommand(): 解析失败 => ASK_USER(交互模式给用户最后决定权;non-interactive 会转为 DENY)
86+
const parseOk = hasBalancedQuotes(cmd);
87+
const segments = parseOk
88+
? cmd
89+
.split(/\s*(?:&&|\|\||;|\|)\s*/)
90+
.map((s) => s.trim())
91+
.filter(Boolean)
92+
: [cmd.trim()].filter(Boolean);
93+
const rootCommand = segments[0]?.split(/\s+/)[0] || '';
94+
8895
const parsePattern = (pattern: string): { tool: string; arg?: string } | null => {
8996
const openParen = pattern.indexOf('(');
9097
if (openParen === -1) {
@@ -110,18 +117,27 @@ export default function CommandInjectionDetectionAnimation() {
110117
return false;
111118
};
112119

113-
// 1) 解析是否可控(上游用 splitCommands()/parseCommandDetails;这里用“引号平衡”做近似演示)
114-
const parseOk = hasBalancedQuotes(cmd);
115120
checks.push({
116121
name: 'parseCommandDetails()',
117122
passed: parseOk,
118123
detail: parseOk
119124
? 'Parsed (simulated) OK'
120-
: 'Parse failed (simulated): unbalanced quotes → 上游会回退为 ASK_USER(更保守)',
121-
// 上游 parse 失败不会直接 DENY,而是回退为 ASK_USER(需要确认)
122-
severity: parseOk ? 'safe' : 'warning',
125+
: 'Parse failed (simulated): unbalanced quotes → shell-permissions 直接 Hard DENY(无法安全拆分命令)',
126+
severity: parseOk ? 'safe' : 'blocked',
123127
});
124128

129+
// 解析失败:上游 checkCommandPermissions() 会直接 hard deny,后续规则匹配没有意义
130+
if (!parseOk) {
131+
return {
132+
command: cmd,
133+
rootCommand,
134+
segments,
135+
checks,
136+
isAllowed: false,
137+
requiresPermission: false,
138+
};
139+
}
140+
125141
// 2) tools.exclude(blocklist,优先级最高)
126142
const isWildcardBlocked = EXAMPLE_TOOLS_EXCLUDE.some(
127143
(p) => p === 'run_shell_command' || p === 'ShellTool',

src/pages/ContentGenerationPipelineAnimation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const CONVERSION_STEPS: ConversionStep[] = [
5151
const SAMPLE_CHUNKS: StreamChunk[] = [
5252
{ id: 'c1', type: 'content', content: 'I will help you ' },
5353
{ id: 'c2', type: 'content', content: 'read the file.' },
54-
{ id: 'c3', type: 'tool_call', toolName: 'Read', content: '{"file_path": "/src/app.ts"}' },
54+
{ id: 'c3', type: 'tool_call', toolName: 'read_file', content: '{"file_path": "src/app.ts"}' },
5555
{ id: 'c4', type: 'finish', finishReason: 'tool_calls' },
5656
{ id: 'c5', type: 'usage', usage: { input: 1250, output: 89 } },
5757
];

src/pages/CoreCode.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,8 +445,15 @@ export async function createContentGenerator(
445445
class ToolRegistry {
446446
private allKnownTools = new Map<string, AnyDeclarativeTool>();
447447
448+
constructor(
449+
private readonly config: Config,
450+
private readonly messageBus: MessageBus,
451+
) {}
452+
448453
// 注册工具
449454
registerTool(tool: AnyDeclarativeTool) {
455+
// 同名冲突:MCP 工具会升级为 fully-qualified(<server>__<tool>)
456+
// 其他情况默认覆盖并 warn
450457
this.allKnownTools.set(tool.name, tool);
451458
}
452459
@@ -456,15 +463,14 @@ class ToolRegistry {
456463
}
457464
458465
// 获取所有工具定义(发送给模型,@google/genai FunctionDeclaration)
459-
getAllToolDefinitions(): FunctionDeclaration[] {
466+
getFunctionDeclarations(): FunctionDeclaration[] {
460467
return Array.from(this.allKnownTools.values()).map(tool => tool.schema);
461468
}
462469
}
463470
464471
// 上游入口:Config.createToolRegistry()
465472
async function createToolRegistry(config: Config) {
466-
const registry = new ToolRegistry(config);
467-
registry.setMessageBus(config.getMessageBus());
473+
const registry = new ToolRegistry(config, config.getMessageBus());
468474
469475
// 文件操作工具
470476
registry.registerTool(new ReadFileTool(config, config.getMessageBus())); // read_file
@@ -534,6 +540,9 @@ export abstract class DeclarativeTool<TParams extends object, TResult extends To
534540
readonly description: string,
535541
readonly kind: Kind,
536542
readonly parameterSchema: unknown,
543+
readonly messageBus: MessageBus,
544+
readonly isOutputMarkdown: boolean = true,
545+
readonly canUpdateOutput: boolean = false,
537546
) {}
538547
539548
get schema(): FunctionDeclaration {
@@ -553,7 +562,7 @@ export abstract class BaseDeclarativeTool<TParams extends object, TResult extend
553562
554563
protected abstract createInvocation(
555564
params: TParams,
556-
messageBus?: MessageBus,
565+
messageBus: MessageBus,
557566
_toolName?: string,
558567
_toolDisplayName?: string,
559568
): ToolInvocation<TParams, TResult>;

src/pages/ErrorHandling.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,43 +1083,43 @@ function attemptJSONFix(
10831083
<tr className="border-b border-gray-700/50">
10841084
<td className="py-3 px-2">
10851085
<span className="px-2 py-1 bg-blue-900/30 text-blue-400 rounded text-xs">TOOL</span>
1086-
<div className="text-xs text-gray-500 mt-1">Bash 失败</div>
1086+
<div className="text-xs text-gray-500 mt-1">run_shell_command 失败</div>
10871087
</td>
10881088
<td className="py-3 px-2 text-xs">命令执行返回非零</td>
10891089
<td className="py-3 px-2 text-xs">命令不存在、权限不足、语法错误</td>
10901090
<td className="py-3 px-2 text-xs">
10911091
<code className="text-green-400">AI 自动处理</code>
10921092
错误返回给模型重新决策
10931093
</td>
1094-
<td className="py-3 px-2 text-xs font-mono text-cyan-400">core/src/tools/bash.ts</td>
1094+
<td className="py-3 px-2 text-xs font-mono text-cyan-400">packages/core/src/tools/shell.ts</td>
10951095
</tr>
10961096

10971097
<tr className="border-b border-gray-700/50">
10981098
<td className="py-3 px-2">
10991099
<span className="px-2 py-1 bg-blue-900/30 text-blue-400 rounded text-xs">TOOL</span>
1100-
<div className="text-xs text-gray-500 mt-1">Read 失败</div>
1100+
<div className="text-xs text-gray-500 mt-1">read_file 失败</div>
11011101
</td>
11021102
<td className="py-3 px-2 text-xs">无法读取文件</td>
11031103
<td className="py-3 px-2 text-xs">文件不存在、权限不足、路径错误</td>
11041104
<td className="py-3 px-2 text-xs">
11051105
<code className="text-green-400">AI 自动处理</code>
11061106
提示文件不存在
11071107
</td>
1108-
<td className="py-3 px-2 text-xs font-mono text-cyan-400">core/src/tools/read.ts</td>
1108+
<td className="py-3 px-2 text-xs font-mono text-cyan-400">packages/core/src/tools/read-file.ts</td>
11091109
</tr>
11101110

11111111
<tr className="border-b border-gray-700/50">
11121112
<td className="py-3 px-2">
11131113
<span className="px-2 py-1 bg-blue-900/30 text-blue-400 rounded text-xs">TOOL</span>
1114-
<div className="text-xs text-gray-500 mt-1">Edit 失败</div>
1114+
<div className="text-xs text-gray-500 mt-1">replace 失败</div>
11151115
</td>
11161116
<td className="py-3 px-2 text-xs">old_string 未找到</td>
11171117
<td className="py-3 px-2 text-xs">文件已被修改、匹配字符串错误、编码问题</td>
11181118
<td className="py-3 px-2 text-xs">
11191119
<code className="text-green-400">AI 自动处理</code>
11201120
重新读取文件后重试
11211121
</td>
1122-
<td className="py-3 px-2 text-xs font-mono text-cyan-400">core/src/tools/edit.ts</td>
1122+
<td className="py-3 px-2 text-xs font-mono text-cyan-400">packages/core/src/tools/smart-edit.ts</td>
11231123
</tr>
11241124

11251125
{/* 配置错误 */}

0 commit comments

Comments
 (0)