Skip to content

Commit 9d2de90

Browse files
committed
feat: 实现 Skills 支持
- 添加 hello-world 示例 Skill(SKILL.md + reference + scripts) - Dockerfile: 复制 skills 到 /opt/claude-skills - agent_session.py: 运行时复制到 /tmp/.claude-code/skills/,配置 setting_sources=['user'] - 更新 README.md 移除 Skills TODO - 添加 tests/test_skills_location.py 验证 SDK Skills 加载位置
1 parent 5f8cc06 commit 9d2de90

7 files changed

Lines changed: 181 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda
1919
- **Session 持久化**:DynamoDB 存储映射,S3 存储对话历史,支持跨请求恢复
2020
- **多租户隔离**:基于 Telegram chat_id + thread_id 实现客户端隔离
2121
- **SubAgent 支持**:可配置多个专业 Agent(如 AWS 支持)
22-
- **Skills 支持**:可复用的技能模块(计划中)
22+
- **Skills 支持**:可复用的技能模块
2323
- **MCP 集成**:支持 HTTP 和本地命令类型的 MCP 服务器
2424
- **自动清理**:25天 TTL + S3 生命周期管理
2525

@@ -33,7 +33,8 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda
3333
│ └── claude-config/ # 配置文件
3434
│ ├── agents.json # SubAgent定义
3535
│ ├── mcp.json # MCP服务器配置
36-
│ ├── skills/ # Skills定义(计划中)
36+
│ ├── skills/ # Skills定义
37+
│ │ └── hello-world/ # 示例 Skill
3738
│ └── system_prompt.md # 系统提示
3839
3940
├── agent-sdk-client/ # Telegram客户端 (ZIP部署)
@@ -121,7 +122,6 @@ sam deploy --guided
121122

122123
## TODO
123124

124-
- [ ] 实现 Skills 支持(参考 `docs/anthropic-agent-sdk-official/skills-in-sdk.md`
125125
- [ ] 多租户 TenantID 隔离
126126

127127
## License

agent-sdk-server/Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ RUN chmod 644 *.py
1818
COPY claude-config/ /opt/claude-config/
1919
RUN chmod -R 755 /opt/claude-config/
2020

21+
# Copy skills to /opt/claude-skills (will be copied to /tmp/.claude-code/skills at runtime)
22+
COPY claude-config/skills/ /opt/claude-skills/
23+
RUN chmod -R 755 /opt/claude-skills/
24+
2125
# Create ~/.claude and ~/.aws directories and ensure writable
2226
RUN mkdir -p /root/.claude/projects /root/.claude/debug /root/.claude/todos /root/.aws && \
2327
touch /root/.claude.json && \

agent-sdk-server/agent_session.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
# Config source (in Docker image) and destination (Lambda writable)
2121
CONFIG_SRC = Path('/opt/claude-config')
2222
CONFIG_DST = Path('/tmp/.claude-code')
23+
SKILLS_SRC = Path('/opt/claude-skills')
24+
SKILLS_DST = CONFIG_DST / 'skills'
2325

2426

2527
def setup_lambda_environment():
@@ -58,6 +60,17 @@ def setup_lambda_environment():
5860
shutil.copy2(item, dst)
5961
print(f"Config copied from {CONFIG_SRC} to {CONFIG_DST}")
6062

63+
# Copy skills to CLAUDE_CONFIG_DIR/skills/ for SDK to discover
64+
if SKILLS_SRC.exists():
65+
SKILLS_DST.mkdir(parents=True, exist_ok=True)
66+
for item in SKILLS_SRC.iterdir():
67+
dst = SKILLS_DST / item.name
68+
if item.is_dir():
69+
shutil.copytree(item, dst, dirs_exist_ok=True)
70+
else:
71+
shutil.copy2(item, dst)
72+
print(f"Skills copied from {SKILLS_SRC} to {SKILLS_DST}")
73+
6174
print(f"Bedrock profile created at {credentials_file}")
6275

6376

@@ -154,11 +167,12 @@ async def process_message(
154167
permission_mode='bypassPermissions', # Lambda has no interactive terminal
155168
max_turns=max_turns,
156169
system_prompt=system_prompt,
170+
setting_sources=['user'], # Load skills from CLAUDE_CONFIG_DIR/skills/
157171
allowed_tools=[
158172
#'Bash', 'Read', 'Write', 'Edit',
159173
#'Glob', 'Grep', 'WebFetch',
160-
'Task',
161-
'Skill' # Required for SubAgent invocation
174+
'Task', # For SubAgents
175+
'Skill', # For Skills
162176
],
163177
mcp_servers=mcp_servers if mcp_servers else None,
164178
agents=agents if agents else None,
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
description: Hello World 示例 Skill,演示如何读取 reference 文件并执行脚本
3+
---
4+
5+
# Hello World Skill
6+
7+
这是一个示例 Skill,用于演示 Skills 的基本结构。
8+
9+
## 使用方法
10+
11+
1. 读取 `reference/message.json` 获取消息内容
12+
2. 运行 `scripts/print_message.py` 输出所有字符
13+
14+
## 文件结构
15+
16+
```
17+
hello-world/
18+
├── SKILL.md
19+
├── reference/
20+
│ └── message.json
21+
└── scripts/
22+
└── print_message.py
23+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"message": "Hello World"
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python3
2+
"""输出 reference/message.json 中的所有字符"""
3+
4+
import json
5+
from pathlib import Path
6+
7+
ref_path = Path(__file__).parent.parent / "reference" / "message.json"
8+
content = ref_path.read_text(encoding="utf-8")
9+
10+
for char in content:
11+
print(char, end="")

tests/test_skills_location.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""测试 Claude Agent SDK Skills 加载位置"""
3+
4+
import asyncio
5+
import os
6+
import tempfile
7+
from pathlib import Path
8+
9+
# 创建测试目录结构
10+
def setup_test_dirs():
11+
"""创建不同位置的 skill 测试目录"""
12+
# 1. Project skills: {cwd}/.claude/skills/
13+
# 2. User skills: ~/.claude/skills/
14+
# 3. CLAUDE_CONFIG_DIR 相关位置
15+
16+
test_base = Path(tempfile.mkdtemp(prefix="skill_test_"))
17+
18+
# 模拟不同的目录结构
19+
locations = {
20+
"project_cwd": test_base / "workspace", # cwd
21+
"config_dir": test_base / "claude-code", # CLAUDE_CONFIG_DIR
22+
}
23+
24+
# 创建 skill 在不同位置
25+
skill_content = """---
26+
description: Test skill for location verification
27+
---
28+
29+
# Test Skill
30+
31+
This is a test skill to verify loading location.
32+
"""
33+
34+
# Project skills: {cwd}/.claude/skills/test-skill/SKILL.md
35+
project_skill_dir = locations["project_cwd"] / ".claude" / "skills" / "test-skill"
36+
project_skill_dir.mkdir(parents=True, exist_ok=True)
37+
(project_skill_dir / "SKILL.md").write_text(skill_content)
38+
39+
# Config dir skills: {CLAUDE_CONFIG_DIR}/skills/test-skill/SKILL.md
40+
config_skill_dir = locations["config_dir"] / "skills" / "test-skill"
41+
config_skill_dir.mkdir(parents=True, exist_ok=True)
42+
(config_skill_dir / "SKILL.md").write_text(skill_content)
43+
44+
# Config dir .claude/skills: {CLAUDE_CONFIG_DIR}/.claude/skills/test-skill/SKILL.md
45+
config_claude_skill_dir = locations["config_dir"] / ".claude" / "skills" / "test-skill"
46+
config_claude_skill_dir.mkdir(parents=True, exist_ok=True)
47+
(config_claude_skill_dir / "SKILL.md").write_text(skill_content)
48+
49+
print(f"Test base: {test_base}")
50+
print(f"Project cwd: {locations['project_cwd']}")
51+
print(f"Config dir: {locations['config_dir']}")
52+
print(f"\nCreated skills at:")
53+
print(f" 1. {project_skill_dir}/SKILL.md")
54+
print(f" 2. {config_skill_dir}/SKILL.md")
55+
print(f" 3. {config_claude_skill_dir}/SKILL.md")
56+
57+
return locations
58+
59+
60+
async def test_skill_loading(cwd: str, config_dir: str = None):
61+
"""测试不同配置下 skill 是否被加载"""
62+
from claude_agent_sdk import query, ClaudeAgentOptions
63+
64+
print(f"\n{'='*60}")
65+
print(f"Testing with:")
66+
print(f" cwd: {cwd}")
67+
print(f" CLAUDE_CONFIG_DIR: {config_dir or 'not set'}")
68+
69+
# 设置环境变量
70+
if config_dir:
71+
os.environ['CLAUDE_CONFIG_DIR'] = config_dir
72+
elif 'CLAUDE_CONFIG_DIR' in os.environ:
73+
del os.environ['CLAUDE_CONFIG_DIR']
74+
75+
options = ClaudeAgentOptions(
76+
cwd=cwd,
77+
setting_sources=["user", "project"],
78+
allowed_tools=["Skill"],
79+
model="haiku",
80+
max_turns=1,
81+
)
82+
83+
try:
84+
print(f"\nQuerying: 'What skills are available?'")
85+
async for message in query(
86+
prompt="What skills are available? Just list them briefly.",
87+
options=options
88+
):
89+
print(f" Response type: {type(message).__name__}")
90+
if hasattr(message, 'content'):
91+
for block in message.content:
92+
if hasattr(block, 'text'):
93+
print(f" Text: {block.text[:200]}...")
94+
except Exception as e:
95+
print(f" Error: {e}")
96+
97+
98+
async def main():
99+
locations = setup_test_dirs()
100+
101+
# 测试 1: cwd 指向 workspace,不设置 CLAUDE_CONFIG_DIR
102+
await test_skill_loading(
103+
cwd=str(locations["project_cwd"]),
104+
config_dir=None
105+
)
106+
107+
# 测试 2: cwd 指向 workspace,CLAUDE_CONFIG_DIR 指向 config_dir
108+
await test_skill_loading(
109+
cwd=str(locations["project_cwd"]),
110+
config_dir=str(locations["config_dir"])
111+
)
112+
113+
# 测试 3: cwd 指向 config_dir
114+
await test_skill_loading(
115+
cwd=str(locations["config_dir"]),
116+
config_dir=str(locations["config_dir"])
117+
)
118+
119+
120+
if __name__ == "__main__":
121+
asyncio.run(main())

0 commit comments

Comments
 (0)