Skip to content

refactor(skills): use native workspace symlinks instead of prompt injection#1246

Merged
kaizhou-lab merged 3 commits intoiOfficeAI:mainfrom
audichuang:feat/skills-native-symlinks
Mar 19, 2026
Merged

refactor(skills): use native workspace symlinks instead of prompt injection#1246
kaizhou-lab merged 3 commits intoiOfficeAI:mainfrom
audichuang:feat/skills-native-symlinks

Conversation

@audichuang
Copy link
Copy Markdown
Contributor

@audichuang audichuang commented Mar 11, 2026

背景與問題

目前 AionUi 的 Skills 機制存在一個根本性缺陷:系統只是單純把 SKILL.md 的文字內容注入到 system prompt / 首條訊息中(prompt injection),讓 AI「知道」有哪些技能可用。但這種做法有三個嚴重問題:

問題 1:Skills 腳本完全無法執行

pptx 技能為例,其目錄結構包含實際的 Python/JS 腳本:

skills/pptx/
├── SKILL.md                  ← 技能說明文件(522 行)
├── html2pptx.md              ← 參考文件
├── ooxml/scripts/unpack.py   ← 實際腳本
├── scripts/html2pptx.js      ← 實際腳本
├── scripts/thumbnail.py      ← 實際腳本
└── scripts/replace.py        ← 實際腳本

SKILL.md 中寫的指令是 python ooxml/scripts/unpack.py(相對路徑),但因為這些腳本根本不存在於 AI 的工作空間中,所以 AI 嘗試執行時會直接報錯找不到檔案。同理,SKILL.md 中引用的 html2pptx.md 等參考文件也無法被讀取。

結論:目前的 Skills 功能只能當「文字提示」用,包含腳本或參考文件的技能實質上是壞的。

問題 2:System Prompt Token 浪費

現有機制透過 buildSystemInstructionsWithSkillsIndex()(Gemini)和 prepareFirstMessageWithSkillsIndex()(ACP),將所有啟用的 Skills 的 SKILL.md 索引文字注入到 system prompt 或首條訊息中。每次對話都會消耗大量 token 在這些靜態文字上,即使 AI 在當輪對話中根本不需要使用任何技能。

問題 3:助手設定中技能移除 UI Bug

  • 「從助理移除技能」的確認對話框顯示原始的 {{name}},沒有正確插值替換為技能名稱。
  • 點擊「移除」後技能沒有從清單上消失,因為清單渲染來源是 availableSkills(系統所有自訂技能),而移除操作只更新了 customSkills state。

解決方案

核心改動:以 Workspace Symlink 取代 Prompt Injection

在建立對話時,將使用者目錄中啟用的 skills 以 symbolic link 方式放入 CLI 原生的 skills 目錄,讓各 CLI 的 SkillManager 自動發現和載入技能。

使用者目錄                                  臨時工作空間
~/Library/.../skills/pptx/   ──symlink──→  .gemini/skills/pptx/
~/Library/.../skills/pdf/    ──symlink──→  .gemini/skills/pdf/

每個 agent 只 symlink 到自己的原生 skills 目錄:

Agent 類型 Symlink 目標目錄
Gemini CLI .gemini/skills/{name}/
Claude Code / CodeBuddy .claude/skills/{name}/
其他 (Codex, OpenCode, Qwen…) .agents/skills/{name}/ (通用 fallback)

安全性考量:只在臨時 workspace(非使用者指定的自訂 workspace)中建立 symlink,避免污染使用者專案目錄。

重要行為變更:System Prompt 不再包含 SKILL 資訊

本次重構後,system prompt / 首條訊息中不再注入任何 Skills 索引或 SKILL.md 內容

變更前

  • GeminiAgentManager:透過 buildSystemInstructionsWithSkillsIndex() 將 skills 索引注入 presetRules → 作為 system prompt 的一部分
  • AcpAgentManager:透過 prepareFirstMessageWithSkillsIndex() 將 skills 索引注入首條使用者訊息

變更後

  • GeminiAgentManagerpresetRules 只包含助手的人格規則(如「你是專業運動教練」),不再包含 Skills 索引。Skills 由 CLI 的原生 SkillManager 透過掃描 .gemini/skills/ 目錄自動發現,AI 使用 activate_skill 工具按需載入
  • AcpAgentManager:首條訊息只注入 presetContext(人格規則),不再附加 skills 索引。Skills 同樣透過 workspace 中的 symlink 目錄被 CLI 原生發現

好處:

  • ✅ AI 只在需要時才載入 SKILL.md(按需),降低 token 消耗
  • ✅ 腳本和參考文件都可透過相對路徑正確存取
  • ✅ System prompt 更精簡,留更多上下文空間給實際對話

實際效果截圖

Gemini CLI — workspace 中只有 .gemini/skills/

Electron 2026-03-11 09 35 27

Claude Code — workspace 中正確建立 .claude/skills/

CleanShot 2026-03-11 at 09 38 53@2x

各檔案變更說明

src/process/initAgent.ts (+121)

  • 新增 setupAssistantWorkspace() 函式:根據 agent 類型將啟用的 skills symlink 到對應的原生目錄
  • 新增 AGENT_SKILLS_DIRS 映射表:只列出有專屬目錄的 CLI,其餘走 DEFAULT_SKILLS_DIRS
  • 在 5 個 agent 建構函式中加入 setupAssistantWorkspace() 呼叫

src/agent/gemini/cli/config.ts (+4 / −3)

  • skillsSupport: false → true:啟用 Gemini CLI 原生的 SkillManager

src/process/bridge/fsBridge.ts (+8 / −4)

  • skills 目錄掃描時加上 entry.isSymbolicLink() 判斷
  • skill 匯入時已存在則回傳 success: true(跳過複製)

src/process/task/AcpAgentManager.ts (+4 / −8)

  • 移除 prepareFirstMessageWithSkillsIndex() 呼叫
  • 保留 presetContext(助手人格規則)的注入

src/process/task/AcpSkillManager.ts (+2 / −2)

  • 技能目錄掃描時加上 entry.isSymbolicLink() 判斷

src/process/task/GeminiAgentManager.ts (+8 / −12)

  • 移除 buildSystemInstructionsWithSkillsIndex() 呼叫
  • presetRules 直接傳遞原始的助手人格規則

src/renderer/pages/settings/AssistantManagement.tsx (+7 / −5)

  • 修復 i18n 插值:加入 name 參數
  • 自訂技能清單改為只渲染屬於此助手的技能,移除後立即反映在 UI 上

測試驗證

  • bun run lint — 通過,無錯誤
  • bun run test — 452 passed, 2 failed(configureChromium.test.ts 上游既有問題,與本 PR 無關)
  • 手動測試:Gemini 助手確認 .gemini/skills/ 目錄中正確建立 symlink
  • 手動測試:Gemini CLI 透過原生 activate_skill 工具成功發現並載入技能
  • 手動測試:技能移除確認框正確顯示技能名稱,移除後立即從清單消失
  • 手動測試:自訂 workspace 模式下不建立 symlink

…ection

- Add setupAssistantWorkspace() to symlink enabled skills into CLI-native
  skill directories (.gemini/skills/, .claude/skills/, .agents/skills/)
- Enable Gemini CLI native SkillManager (skillsSupport: true)
- Remove skills index injection from GeminiAgentManager and AcpAgentManager
- Support symbolic links in fsBridge and AcpSkillManager file discovery
- Treat existing skills as success (skip copy) during skill import
@kaizhou-lab kaizhou-lab self-assigned this Mar 17, 2026
@kaizhou-lab
Copy link
Copy Markdown
Collaborator

kaizhou-lab commented Mar 17, 2026

关于 PR 描述中提到的问题

我仔细看了 main 分支上 skill 注入的实际逻辑,有几个地方和 PR 描述的说法存在出入:

1. "Skills 脚本完全无法执行" — ACP 侧不成立

PR 描述说"系统只是单纯把 SKILL.md 的文字内容注入到 system prompt",但实际 prepareFirstMessageWithSkillsIndex() 注入的内容是:

  • Skills INDEX(name + description),不是全文
  • 完整的路径信息
[Skills Location]
Skills are stored in two locations:
- Builtin skills: ${builtinSkillsDir}/{skill-name}/SKILL.md
- Optional skills: ${skillsDir}/{skill-name}/SKILL.md

Each skill has a SKILL.md file containing detailed instructions.
To use a skill, read its SKILL.md file when needed.

ACP agent(Claude Code / OpenCode 等)拿到完整路径后,用 Read 工具按需读取 SKILL.md,然后可以通过路径组合解析 SKILL.md 中的相对路径引用(如 ${skillsDir}/pptx/ooxml/scripts/unpack.py)。ACP agents 并不存在"找不到路径"的问题。

2. "System Prompt Token 浪费" — 程度被夸大

注入的是轻量级的 INDEX(每个 skill 只有一行 name + description),不是 SKILL.md 全文。5 个 skills 的索引可能只有几百 token,称不上"大量消耗"。

3. Gemini 侧确实存在路径缺失问题

Gemini 的 [LOAD_SKILL] 机制中,buildSkillContentText() 返回的内容是:

[Skill: pptx]
<SKILL.md body>

没有包含路径信息。当 SKILL.md body 中引用了相对路径的脚本(如 python ooxml/scripts/unpack.py),Gemini 确实不知道这些文件在哪。这是一个真实的问题。

4. UI Bug — 确认存在

i18n 插值缺失和 custom skills 列表渲染源错误这两个修复是正确的。


建议:用更小的改动解决 Gemini 路径问题

既然问题只存在于 Gemini 的 [LOAD_SKILL] 响应中缺少路径信息,只需修改 buildSkillContentText() 加入 skill 目录路径即可:

// Before
export function buildSkillContentText(skills: SkillDefinition[]): string {
  return skills.map((s) => `[Skill: ${s.name}]\n${s.body}`).join('\n\n');
}

// After — 加入 skill 目录路径,让 Gemini 能解析相对路径引用
export function buildSkillContentText(skills: SkillDefinition[]): string {
  return skills.map((s) => {
    const skillDir = path.dirname(s.location);
    return `[Skill: ${s.name}]\n[Skill Directory: ${skillDir}]\n${s.body}`;
  }).join('\n\n');
}

这样 Gemini 收到的内容变为:

[Skill: pptx]
[Skill Directory: /Users/.../skills/pptx]
<SKILL.md body>

Gemini 就能知道脚本文件在哪,用绝对路径执行。

相比 symlink 方案的优势:

  • 改动量极小(只改一个函数),而非 7 个文件 +158/-36 行
  • 不需要改变 skillsSupport 配置、不需要改 initAgent.ts、不需要改 ACP/Gemini AgentManager 的注入逻辑
  • 不引入 symlink 的平台兼容性问题
  • ACP 侧完全不需要改动(本来就能工作)

建议拆分为两个 PR:

  1. fix(skills): 修改 buildSkillContentText() 加入路径信息(解决 Gemini 路径问题)
  2. fix(settings): 修复助手设置页面的 i18n 插值 + custom skills 列表渲染

…symlinks

# Conflicts:
#	src/process/initAgent.ts
#	src/renderer/pages/settings/AssistantManagement.tsx
@sentry
Copy link
Copy Markdown

sentry bot commented Mar 19, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kaizhou-lab kaizhou-lab merged commit 69585ee into iOfficeAI:main Mar 19, 2026
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants