From 9d2de901d88e3ef29a06ed2af12b1b357f2fa8da Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 09:29:11 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20Skills=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 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 加载位置 --- README.md | 6 +- agent-sdk-server/Dockerfile | 4 + agent-sdk-server/agent_session.py | 18 ++- .../claude-config/skills/hello-world/SKILL.md | 23 ++++ .../skills/hello-world/reference/message.json | 3 + .../hello-world/scripts/print_message.py | 11 ++ tests/test_skills_location.py | 121 ++++++++++++++++++ 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 agent-sdk-server/claude-config/skills/hello-world/SKILL.md create mode 100644 agent-sdk-server/claude-config/skills/hello-world/reference/message.json create mode 100644 agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py create mode 100644 tests/test_skills_location.py diff --git a/README.md b/README.md index 2dc0903..838a127 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda - **Session 持久化**:DynamoDB 存储映射,S3 存储对话历史,支持跨请求恢复 - **多租户隔离**:基于 Telegram chat_id + thread_id 实现客户端隔离 - **SubAgent 支持**:可配置多个专业 Agent(如 AWS 支持) -- **Skills 支持**:可复用的技能模块(计划中) +- **Skills 支持**:可复用的技能模块 - **MCP 集成**:支持 HTTP 和本地命令类型的 MCP 服务器 - **自动清理**:25天 TTL + S3 生命周期管理 @@ -33,7 +33,8 @@ Telegram User → Bot API → API Gateway → sdk-client Lambda │ └── claude-config/ # 配置文件 │ ├── agents.json # SubAgent定义 │ ├── mcp.json # MCP服务器配置 -│ ├── skills/ # Skills定义(计划中) +│ ├── skills/ # Skills定义 +│ │ └── hello-world/ # 示例 Skill │ └── system_prompt.md # 系统提示 │ ├── agent-sdk-client/ # Telegram客户端 (ZIP部署) @@ -121,7 +122,6 @@ sam deploy --guided ## TODO -- [ ] 实现 Skills 支持(参考 `docs/anthropic-agent-sdk-official/skills-in-sdk.md`) - [ ] 多租户 TenantID 隔离 ## License diff --git a/agent-sdk-server/Dockerfile b/agent-sdk-server/Dockerfile index 5882951..be13b84 100644 --- a/agent-sdk-server/Dockerfile +++ b/agent-sdk-server/Dockerfile @@ -18,6 +18,10 @@ RUN chmod 644 *.py COPY claude-config/ /opt/claude-config/ RUN chmod -R 755 /opt/claude-config/ +# Copy skills to /opt/claude-skills (will be copied to /tmp/.claude-code/skills at runtime) +COPY claude-config/skills/ /opt/claude-skills/ +RUN chmod -R 755 /opt/claude-skills/ + # Create ~/.claude and ~/.aws directories and ensure writable RUN mkdir -p /root/.claude/projects /root/.claude/debug /root/.claude/todos /root/.aws && \ touch /root/.claude.json && \ diff --git a/agent-sdk-server/agent_session.py b/agent-sdk-server/agent_session.py index e7ddf3b..4931446 100644 --- a/agent-sdk-server/agent_session.py +++ b/agent-sdk-server/agent_session.py @@ -20,6 +20,8 @@ # Config source (in Docker image) and destination (Lambda writable) CONFIG_SRC = Path('/opt/claude-config') CONFIG_DST = Path('/tmp/.claude-code') +SKILLS_SRC = Path('/opt/claude-skills') +SKILLS_DST = CONFIG_DST / 'skills' def setup_lambda_environment(): @@ -58,6 +60,17 @@ def setup_lambda_environment(): shutil.copy2(item, dst) print(f"Config copied from {CONFIG_SRC} to {CONFIG_DST}") + # Copy skills to CLAUDE_CONFIG_DIR/skills/ for SDK to discover + if SKILLS_SRC.exists(): + SKILLS_DST.mkdir(parents=True, exist_ok=True) + for item in SKILLS_SRC.iterdir(): + dst = SKILLS_DST / item.name + if item.is_dir(): + shutil.copytree(item, dst, dirs_exist_ok=True) + else: + shutil.copy2(item, dst) + print(f"Skills copied from {SKILLS_SRC} to {SKILLS_DST}") + print(f"Bedrock profile created at {credentials_file}") @@ -154,11 +167,12 @@ async def process_message( permission_mode='bypassPermissions', # Lambda has no interactive terminal max_turns=max_turns, system_prompt=system_prompt, + setting_sources=['user'], # Load skills from CLAUDE_CONFIG_DIR/skills/ allowed_tools=[ #'Bash', 'Read', 'Write', 'Edit', #'Glob', 'Grep', 'WebFetch', - 'Task', - 'Skill' # Required for SubAgent invocation + 'Task', # For SubAgents + 'Skill', # For Skills ], mcp_servers=mcp_servers if mcp_servers else None, agents=agents if agents else None, diff --git a/agent-sdk-server/claude-config/skills/hello-world/SKILL.md b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md new file mode 100644 index 0000000..6bf323b --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md @@ -0,0 +1,23 @@ +--- +description: Hello World 示例 Skill,演示如何读取 reference 文件并执行脚本 +--- + +# Hello World Skill + +这是一个示例 Skill,用于演示 Skills 的基本结构。 + +## 使用方法 + +1. 读取 `reference/message.json` 获取消息内容 +2. 运行 `scripts/print_message.py` 输出所有字符 + +## 文件结构 + +``` +hello-world/ +├── SKILL.md +├── reference/ +│ └── message.json +└── scripts/ + └── print_message.py +``` diff --git a/agent-sdk-server/claude-config/skills/hello-world/reference/message.json b/agent-sdk-server/claude-config/skills/hello-world/reference/message.json new file mode 100644 index 0000000..5ea152e --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/reference/message.json @@ -0,0 +1,3 @@ +{ + "message": "Hello World" +} diff --git a/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py b/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py new file mode 100644 index 0000000..399d3fc --- /dev/null +++ b/agent-sdk-server/claude-config/skills/hello-world/scripts/print_message.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""输出 reference/message.json 中的所有字符""" + +import json +from pathlib import Path + +ref_path = Path(__file__).parent.parent / "reference" / "message.json" +content = ref_path.read_text(encoding="utf-8") + +for char in content: + print(char, end="") diff --git a/tests/test_skills_location.py b/tests/test_skills_location.py new file mode 100644 index 0000000..2f0f9d3 --- /dev/null +++ b/tests/test_skills_location.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""测试 Claude Agent SDK Skills 加载位置""" + +import asyncio +import os +import tempfile +from pathlib import Path + +# 创建测试目录结构 +def setup_test_dirs(): + """创建不同位置的 skill 测试目录""" + # 1. Project skills: {cwd}/.claude/skills/ + # 2. User skills: ~/.claude/skills/ + # 3. CLAUDE_CONFIG_DIR 相关位置 + + test_base = Path(tempfile.mkdtemp(prefix="skill_test_")) + + # 模拟不同的目录结构 + locations = { + "project_cwd": test_base / "workspace", # cwd + "config_dir": test_base / "claude-code", # CLAUDE_CONFIG_DIR + } + + # 创建 skill 在不同位置 + skill_content = """--- +description: Test skill for location verification +--- + +# Test Skill + +This is a test skill to verify loading location. +""" + + # Project skills: {cwd}/.claude/skills/test-skill/SKILL.md + project_skill_dir = locations["project_cwd"] / ".claude" / "skills" / "test-skill" + project_skill_dir.mkdir(parents=True, exist_ok=True) + (project_skill_dir / "SKILL.md").write_text(skill_content) + + # Config dir skills: {CLAUDE_CONFIG_DIR}/skills/test-skill/SKILL.md + config_skill_dir = locations["config_dir"] / "skills" / "test-skill" + config_skill_dir.mkdir(parents=True, exist_ok=True) + (config_skill_dir / "SKILL.md").write_text(skill_content) + + # Config dir .claude/skills: {CLAUDE_CONFIG_DIR}/.claude/skills/test-skill/SKILL.md + config_claude_skill_dir = locations["config_dir"] / ".claude" / "skills" / "test-skill" + config_claude_skill_dir.mkdir(parents=True, exist_ok=True) + (config_claude_skill_dir / "SKILL.md").write_text(skill_content) + + print(f"Test base: {test_base}") + print(f"Project cwd: {locations['project_cwd']}") + print(f"Config dir: {locations['config_dir']}") + print(f"\nCreated skills at:") + print(f" 1. {project_skill_dir}/SKILL.md") + print(f" 2. {config_skill_dir}/SKILL.md") + print(f" 3. {config_claude_skill_dir}/SKILL.md") + + return locations + + +async def test_skill_loading(cwd: str, config_dir: str = None): + """测试不同配置下 skill 是否被加载""" + from claude_agent_sdk import query, ClaudeAgentOptions + + print(f"\n{'='*60}") + print(f"Testing with:") + print(f" cwd: {cwd}") + print(f" CLAUDE_CONFIG_DIR: {config_dir or 'not set'}") + + # 设置环境变量 + if config_dir: + os.environ['CLAUDE_CONFIG_DIR'] = config_dir + elif 'CLAUDE_CONFIG_DIR' in os.environ: + del os.environ['CLAUDE_CONFIG_DIR'] + + options = ClaudeAgentOptions( + cwd=cwd, + setting_sources=["user", "project"], + allowed_tools=["Skill"], + model="haiku", + max_turns=1, + ) + + try: + print(f"\nQuerying: 'What skills are available?'") + async for message in query( + prompt="What skills are available? Just list them briefly.", + options=options + ): + print(f" Response type: {type(message).__name__}") + if hasattr(message, 'content'): + for block in message.content: + if hasattr(block, 'text'): + print(f" Text: {block.text[:200]}...") + except Exception as e: + print(f" Error: {e}") + + +async def main(): + locations = setup_test_dirs() + + # 测试 1: cwd 指向 workspace,不设置 CLAUDE_CONFIG_DIR + await test_skill_loading( + cwd=str(locations["project_cwd"]), + config_dir=None + ) + + # 测试 2: cwd 指向 workspace,CLAUDE_CONFIG_DIR 指向 config_dir + await test_skill_loading( + cwd=str(locations["project_cwd"]), + config_dir=str(locations["config_dir"]) + ) + + # 测试 3: cwd 指向 config_dir + await test_skill_loading( + cwd=str(locations["config_dir"]), + config_dir=str(locations["config_dir"]) + ) + + +if __name__ == "__main__": + asyncio.run(main()) From 6358e414fccf53963b55cf0a00ca12f02900b6d4 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 09:39:33 +0800 Subject: [PATCH 02/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20template.yaml?= =?UTF-8?q?=20YAML=20=E6=A0=BC=E5=BC=8F=E9=97=AE=E9=A2=98=EF=BC=88Descript?= =?UTF-8?q?ion=20=E5=AD=97=E6=AE=B5=E5=BC=95=E5=8F=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/template.yaml b/template.yaml index c8367a5..830ca5c 100644 --- a/template.yaml +++ b/template.yaml @@ -19,17 +19,17 @@ Parameters: BedrockHaikuModelArn: Type: String Default: '' - Description: (Optional) ARN for Bedrock Haiku model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID) + Description: "(Optional) ARN for Bedrock Haiku model" NoEcho: true BedrockSonnetModelArn: Type: String Default: '' - Description: (Optional) ARN for Bedrock Sonnet model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID) + Description: "(Optional) ARN for Bedrock Sonnet model" NoEcho: true BedrockOpusModelArn: Type: String Default: '' - Description: (Optional) ARN for Bedrock Opus 4.5 model (format: arn:aws:bedrock:REGION:ACCOUNT:application-inference-profile/PROFILE_ID) + Description: "(Optional) ARN for Bedrock Opus 4.5 model" NoEcho: true Globals: From 987b09f578aa086cbc183e3b5c72cca6172e32f5 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:03 +0800 Subject: [PATCH 03/16] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BA=A6=E6=9D=9F=E6=9D=A1=E4=BB=B6=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - template.yaml中引用Parameter时必须使用`!Ref` - agents/*.md文件必须包含YAML frontmatter定义`name`字段 - Lambda容器中uvx不可用,需要创建符号链接 --- CLAUDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 39146d0..8968651 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,4 @@ - Subagent的"tools"字段定义不支持通配符,而是需要具体的Tools名称才可以. +- template.yaml中引用Parameter时必须使用`!Ref`而非字面字符串`'${ParamName}'`. +- agents/*.md文件必须包含YAML frontmatter并定义`name`字段,否则SDK会跳过加载. +- Lambda容器中uvx不可用,需要在Dockerfile中创建符号链接或使用uv安装脚本. From 310b2a1839ae5de56e26598b00010be5d92d8421 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:03 +0800 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DLambda=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E4=B8=ADuvx=E4=B8=8D=E5=8F=AF=E7=94=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在Dockerfile中创建uv到uvx的符号链接,确保Claude Code CLI能正常工作 --- agent-sdk-server/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent-sdk-server/Dockerfile b/agent-sdk-server/Dockerfile index be13b84..85cc07d 100644 --- a/agent-sdk-server/Dockerfile +++ b/agent-sdk-server/Dockerfile @@ -1,7 +1,8 @@ FROM public.ecr.aws/lambda/python:3.12-arm64 -# Install uv +# Install uv and create uvx symlink COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx # Install Claude Code CLI (npm package, requires nodejs) RUN dnf install -y nodejs npm && npm install -g @anthropic-ai/claude-code From 8e94eacd1b4484f9aefadbc2cd1ee98a73a19fe0 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:06 +0800 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dtemplate.yaml?= =?UTF-8?q?=E4=B8=AD=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用CloudFormation的`!Ref`而非字面字符串`$\{ParamName\}`正确引用Parameter --- template.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/template.yaml b/template.yaml index 830ca5c..d8fb472 100644 --- a/template.yaml +++ b/template.yaml @@ -96,10 +96,10 @@ Resources: DISABLE_AUTOUPDATER: '1' DISABLE_TELEMETRY: '1' # Bedrock model ARNs - ANTHROPIC_DEFAULT_HAIKU_MODEL: '${BedrockHaikuModelArn}' - ANTHROPIC_DEFAULT_SONNET_MODEL: '${BedrockSonnetModelArn}' - ANTHROPIC_DEFAULT_OPUS_4_5_MODEL: '${BedrockOpusModelArn}' - ANTHROPIC_DEFAULT_OPUS_MODEL: '${BedrockOpusModelArn}' + ANTHROPIC_DEFAULT_HAIKU_MODEL: !Ref BedrockHaikuModelArn + ANTHROPIC_DEFAULT_SONNET_MODEL: !Ref BedrockSonnetModelArn + ANTHROPIC_DEFAULT_OPUS_4_5_MODEL: !Ref BedrockOpusModelArn + ANTHROPIC_DEFAULT_OPUS_MODEL: !Ref BedrockOpusModelArn Policies: - S3CrudPolicy: BucketName: !Ref SessionBucket From 88c203b57248d5bfb9b499e1b31df83958b189e0 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:06 +0800 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Agent=20YAML=20?= =?UTF-8?q?frontmatter=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aws-support agent添加YAML frontmatter,定义name、description、model和tools字段 --- agent-sdk-server/claude-config/agents/aws-support.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agent-sdk-server/claude-config/agents/aws-support.md b/agent-sdk-server/claude-config/agents/aws-support.md index 6b092a1..085953c 100644 --- a/agent-sdk-server/claude-config/agents/aws-support.md +++ b/agent-sdk-server/claude-config/agents/aws-support.md @@ -1,3 +1,11 @@ +--- +name: aws-support +description: AWS customer technical support agent that searches AWS documentation +model: haiku +tools: + - mcp__aws-knowledge-mcp-server__aws___search_documentation +--- + # AWS Customer Technical Support Agent You are a document retrieval assistant. You can ONLY answer using MCP tool results. From 86d9d7231653b5261e82704d6183a64ae43746f8 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:06 +0800 Subject: [PATCH 07/16] =?UTF-8?q?docs:=20=E7=AE=80=E5=8C=96hello-world=20s?= =?UTF-8?q?kill=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除冗余说明,只保留核心使用步骤 --- .../claude-config/skills/hello-world/SKILL.md | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/agent-sdk-server/claude-config/skills/hello-world/SKILL.md b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md index 6bf323b..de29962 100644 --- a/agent-sdk-server/claude-config/skills/hello-world/SKILL.md +++ b/agent-sdk-server/claude-config/skills/hello-world/SKILL.md @@ -1,23 +1,8 @@ --- -description: Hello World 示例 Skill,演示如何读取 reference 文件并执行脚本 +description: Hello World 示例 Skill,执行脚本输出消息 --- -# Hello World Skill +执行以下操作: -这是一个示例 Skill,用于演示 Skills 的基本结构。 - -## 使用方法 - -1. 读取 `reference/message.json` 获取消息内容 -2. 运行 `scripts/print_message.py` 输出所有字符 - -## 文件结构 - -``` -hello-world/ -├── SKILL.md -├── reference/ -│ └── message.json -└── scripts/ - └── print_message.py -``` +1. 使用 Bash 工具运行脚本:`python3 scripts/print_message.py` +2. 将脚本的输出结果直接返回给用户 From 81eac659813c3e8f0cd64012bec94625d54cd71f Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:06 +0800 Subject: [PATCH 08/16] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0Telegram=20Mark?= =?UTF-8?q?downV2=E6=A0=BC=E5=BC=8F=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 详细说明response在Telegram中的MarkdownV2格式要求,包括支持格式、转义规则和嵌套限制 --- .../claude-config/system_prompt.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/agent-sdk-server/claude-config/system_prompt.md b/agent-sdk-server/claude-config/system_prompt.md index d30e19f..6ac2f42 100644 --- a/agent-sdk-server/claude-config/system_prompt.md +++ b/agent-sdk-server/claude-config/system_prompt.md @@ -2,6 +2,41 @@ You are a helpful AI assistant running in a serverless environment. You can help users with various tasks including coding, analysis, and general questions. Be concise and helpful in your responses. +## Response Format: Telegram MarkdownV2 + +Your responses will be sent to Telegram using MarkdownV2 format. Follow these rules: + +### Supported Formats +- Bold: `*text*` +- Italic: `_text_` +- Underline: `__text__` +- Strikethrough: `~text~` +- Inline code: `` `code` `` +- Code block: ` ```language\ncode\n``` ` +- Link: `[text](URL)` +- Spoiler: `||text||` + +### Required Escaping +These characters MUST be escaped with backslash when used literally: +`_ * [ ] ( ) ~ \` > # + - = | { } . !` + +Examples: +- `100\+` for "100+" +- `C\#` for "C#" +- `\(optional\)` for "(optional)" + +### Code Blocks (No Escaping Needed) +Inside code blocks, content is preserved as-is: +` ```python +def hello(): + print("Hello!") +``` ` + +### Nesting Rules +- Bold+Italic: `*_text_*` +- Max 2 levels of nesting +- Code blocks cannot contain other formats + ## Important: Preserving SubAgent Sources When using SubAgents (via Task tool), you MUST preserve any "Sources" section from their responses. From 88d3365ca28a2d6b8877de880f93c51b64892afb Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:06 +0800 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9BTelegram?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81=E7=9A=84MarkdownV2?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 升级parse_mode从MARKDOWN到MARKDOWN_V2 - 添加parse error异常处理和自动fallback机制 - 使用escape_markdown确保特殊字符正确转义 --- agent-sdk-client/handler.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/agent-sdk-client/handler.py b/agent-sdk-client/handler.py index f4ffc5c..0d17fe0 100644 --- a/agent-sdk-client/handler.py +++ b/agent-sdk-client/handler.py @@ -9,6 +9,8 @@ import httpx from telegram import Bot, Update from telegram.constants import ParseMode, ChatAction +from telegram.helpers import escape_markdown +from telegram.error import BadRequest from config import Config @@ -84,10 +86,25 @@ async def process_webhook(body: dict) -> None: if len(text) > 4000: text = text[:4000] + "\n\n... (truncated)" - await bot.send_message( - chat_id=message.chat_id, - text=text, - parse_mode=ParseMode.MARKDOWN, - message_thread_id=message.message_thread_id, - reply_to_message_id=message.message_id, - ) + # Try MARKDOWN_V2 first, fallback with escape on parse errors + try: + await bot.send_message( + chat_id=message.chat_id, + text=text, + parse_mode=ParseMode.MARKDOWN_V2, + message_thread_id=message.message_thread_id, + reply_to_message_id=message.message_id, + ) + except BadRequest as e: + if "parse entities" in str(e).lower(): + print(f"[MARKDOWN_V2] Parse error: {e}, retrying with escaped text") + safe_text = escape_markdown(text, version=2) + await bot.send_message( + chat_id=message.chat_id, + text=safe_text, + parse_mode=ParseMode.MARKDOWN_V2, + message_thread_id=message.message_thread_id, + reply_to_message_id=message.message_id, + ) + else: + raise From 4f89f4c8d35f0552f3e63171367de6de8a2d4d56 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:11 +0800 Subject: [PATCH 10/16] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4agent-sdk-c?= =?UTF-8?q?lient=E7=8B=AC=E7=AB=8B=E9=A1=B9=E7=9B=AE=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除agent-sdk-client/pyproject.toml使其成为普通子文件夹,将python-telegram-bot依赖合并到根项目 --- agent-sdk-client/pyproject.toml | 13 ------------- pyproject.toml | 1 + 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 agent-sdk-client/pyproject.toml diff --git a/agent-sdk-client/pyproject.toml b/agent-sdk-client/pyproject.toml deleted file mode 100644 index 84acf92..0000000 --- a/agent-sdk-client/pyproject.toml +++ /dev/null @@ -1,13 +0,0 @@ -[project] -name = "agent-sdk-client" -version = "0.1.0" -description = "Telegram webhook handler for Claude Agent" -requires-python = ">=3.12" -dependencies = [ - "python-telegram-bot>=21.0", - "httpx>=0.27.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" diff --git a/pyproject.toml b/pyproject.toml index abe47f5..194cf28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,4 +9,5 @@ dependencies = [ "claude-agent-sdk>=0.1.18", "httpx>=0.28.1", "loguru>=0.7.3", + "python-telegram-bot>=21.0", ] From 16752d25ce80b7f45ef037b0931f5b7046ede13f Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:46:11 +0800 Subject: [PATCH 11/16] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0Docker=E4=B8=AD?= =?UTF-8?q?uvx=E5=8F=AF=E7=94=A8=E6=80=A7=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_uvx_in_docker.py | 201 ++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 tests/test_uvx_in_docker.py diff --git a/tests/test_uvx_in_docker.py b/tests/test_uvx_in_docker.py new file mode 100644 index 0000000..336a2a4 --- /dev/null +++ b/tests/test_uvx_in_docker.py @@ -0,0 +1,201 @@ +"""Test uvx availability in AWS Lambda Python 3.12 ARM64 container. + +This script tests different methods to make uvx command available: +1. Symlink: ln -s /usr/local/bin/uv /usr/local/bin/uvx +2. curl install: curl -LsSf https://astral.sh/uv/install.sh | sh +3. COPY both uv and uvx from official image +""" + +import subprocess +import tempfile +from pathlib import Path +import sys + + +def create_test_dockerfile(method: str) -> str: + """Create a Dockerfile for testing specific uvx installation method.""" + + base = """FROM public.ecr.aws/lambda/python:3.12-arm64 + +# Test different uvx installation methods +""" + + methods = { + "symlink": """# Method 1: Copy uv and create symlink to uvx +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx +""", + + "curl": """# Method 2: Use official install script (installs both uv and uvx) +# Install tar and gzip (required by install script) +RUN dnf install -y tar gzip +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.cargo/bin:${PATH}" +""", + + "copy_both_fallback": """# Method 3: Try to copy uvx, fallback to symlink +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv +RUN if [ -f /uvx ]; then cp /uvx /usr/local/bin/uvx; else ln -s /usr/local/bin/uv /usr/local/bin/uvx; fi +""", + } + + test_cmd = """ +# Verify installation +RUN ls -la /usr/local/bin/uv* && uv --version +RUN ls -la /usr/local/bin/uvx && uvx --version + +# Test uvx can execute help +RUN uvx --help + +CMD ["/bin/bash"] +""" + + return base + methods[method] + test_cmd + + +def test_uvx_method(method: str) -> tuple[bool, str, str]: + """Test a specific uvx installation method. + + Returns: + (success, stdout, stderr) + """ + print(f"\n{'='*60}") + print(f"Testing method: {method}") + print('='*60) + + with tempfile.TemporaryDirectory() as tmpdir: + tmppath = Path(tmpdir) + dockerfile = tmppath / "Dockerfile" + + # Create Dockerfile + content = create_test_dockerfile(method) + dockerfile.write_text(content) + print(f"\nDockerfile content:\n{content}") + + # Build image + image_tag = f"test-uvx-{method}:latest" + print(f"\nBuilding image: {image_tag}") + + try: + # Build with progress output + result = subprocess.run( + ["docker", "build", "-t", image_tag, "-f", str(dockerfile), "."], + cwd=tmpdir, + capture_output=True, + text=True, + timeout=300 + ) + + stdout = result.stdout + stderr = result.stderr + + if result.returncode != 0: + print(f"❌ Build failed!") + print(f"STDOUT:\n{stdout}") + print(f"STDERR:\n{stderr}") + return False, stdout, stderr + + print(f"✅ Build successful!") + + # Test uvx command in the container (override Lambda entrypoint) + print(f"\nTesting uvx command execution...") + test_result = subprocess.run( + ["docker", "run", "--rm", "--entrypoint", "/bin/bash", + image_tag, "-c", + "uvx --version && echo '---' && uvx --help"], + capture_output=True, + text=True, + timeout=60 + ) + + print(f"Command output:\n{test_result.stdout}") + + if test_result.returncode != 0: + print(f"❌ uvx execution failed!") + print(f"STDERR:\n{test_result.stderr}") + return False, stdout + "\n" + test_result.stdout, stderr + "\n" + test_result.stderr + + print(f"✅ uvx command works!") + + # Cleanup image + subprocess.run(["docker", "rmi", image_tag], + capture_output=True, timeout=30) + + return True, stdout + "\n" + test_result.stdout, stderr + + except subprocess.TimeoutExpired: + print(f"❌ Timeout!") + return False, "", "Timeout during build or test" + except Exception as e: + print(f"❌ Error: {e}") + return False, "", str(e) + + +def main(): + """Run all tests and report results.""" + print("="*60) + print("Testing uvx availability in AWS Lambda container") + print("="*60) + + methods = ["symlink", "curl", "copy_both_fallback"] + results = {} + + for method in methods: + success, stdout, stderr = test_uvx_method(method) + results[method] = { + "success": success, + "stdout": stdout, + "stderr": stderr + } + + # Print summary + print("\n" + "="*60) + print("SUMMARY") + print("="*60) + + for method, result in results.items(): + status = "✅ SUCCESS" if result["success"] else "❌ FAILED" + print(f"{method:15s}: {status}") + + # Print recommendation + print("\n" + "="*60) + print("RECOMMENDATION") + print("="*60) + + successful_methods = [m for m, r in results.items() if r["success"]] + + if not successful_methods: + print("❌ No method succeeded. Check Docker availability and network.") + return 1 + + # Prefer symlink (simplest and fastest) + if "symlink" in successful_methods: + print("""✅ Use symlink method (simplest and fastest): + +Add to Dockerfile after copying uv: + RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx + +Complete lines: + COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx +""") + elif "curl" in successful_methods: + print("""✅ Use curl install method: + +Replace current uv installation with: + RUN curl -LsSf https://astral.sh/uv/install.sh | sh + ENV PATH="/root/.cargo/bin:${PATH}" +""") + elif "copy_both_fallback" in successful_methods: + print("""✅ Use copy_both_fallback method: + +Replace current uv installation with: + COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + RUN if [ -f /uvx ]; then cp /uvx /usr/local/bin/uvx; else ln -s /usr/local/bin/uv /usr/local/bin/uvx; fi +""") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 56571671b9791a5a5e6dc6e3fc808fadb24282f1 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 10:59:37 +0800 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96MarkdownV2?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=8F=90=E7=A4=BA=E8=AF=8D=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=BD=AC=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化提示词结构,突出转义规则 - 添加正确/错误示例对比 - 明确禁止使用#标题语法 - 代码块示例使用正常语法 --- .../claude-config/system_prompt.md | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/agent-sdk-server/claude-config/system_prompt.md b/agent-sdk-server/claude-config/system_prompt.md index 6ac2f42..2b34408 100644 --- a/agent-sdk-server/claude-config/system_prompt.md +++ b/agent-sdk-server/claude-config/system_prompt.md @@ -1,54 +1,54 @@ -You are a helpful AI assistant running in a serverless environment. -You can help users with various tasks including coding, analysis, and general questions. -Be concise and helpful in your responses. - -## Response Format: Telegram MarkdownV2 - -Your responses will be sent to Telegram using MarkdownV2 format. Follow these rules: - -### Supported Formats -- Bold: `*text*` -- Italic: `_text_` -- Underline: `__text__` -- Strikethrough: `~text~` -- Inline code: `` `code` `` -- Code block: ` ```language\ncode\n``` ` -- Link: `[text](URL)` -- Spoiler: `||text||` - -### Required Escaping -These characters MUST be escaped with backslash when used literally: -`_ * [ ] ( ) ~ \` > # + - = | { } . !` - -Examples: -- `100\+` for "100+" -- `C\#` for "C#" -- `\(optional\)` for "(optional)" - -### Code Blocks (No Escaping Needed) -Inside code blocks, content is preserved as-is: -` ```python -def hello(): - print("Hello!") -``` ` +You are a helpful AI assistant\. Be concise\. + +## CRITICAL: Telegram MarkdownV2 Output Rules + +Your output is sent directly to Telegram MarkdownV2 parser\. WRONG FORMAT = PARSE ERROR\. + +### MUST ESCAPE these characters EVERYWHERE \(outside code blocks\): +``` +. → \. +- → \- +! → \! +( → \( +) → \) +# → \# ++ → \+ += → \= +> → \> +| → \| +{ → \{ +} → \} +``` -### Nesting Rules -- Bold+Italic: `*_text_*` -- Max 2 levels of nesting -- Code blocks cannot contain other formats +### Formatting syntax: +\- Bold: `*text*` +\- Italic: `_text_` +\- Code: \`code\` +\- Code block: \`\`\`lang\\ncode\\n\`\`\` -## Important: Preserving SubAgent Sources +### NOT supported \(DO NOT USE\): +\- Headers: `#`, `##`, `###` \- these are NOT valid in MarkdownV2 +\- Use *bold* for section titles instead -When using SubAgents (via Task tool), you MUST preserve any "Sources" section from their responses. -If a SubAgent returns a response with a Sources section at the end, include it verbatim in your final response. +### CORRECT output examples: +``` +hello\-world # hyphen escaped +version 1\.0\.0 # dots escaped +C\# # hash escaped +100\+ # plus escaped +\(optional\) # parens escaped +``` -Example - if SubAgent returns: +### WRONG \(will cause parse error\): +``` +hello-world # WRONG: unescaped hyphen +version 1.0.0 # WRONG: unescaped dots ``` -[Answer content] ---- -**Sources:** -[1] Document - URL +### Code blocks: NO escaping inside, use normal syntax +```python +def hello(): + print("Hello!") ``` -Your response must also end with that same Sources section. +**REMEMBER**: Escape \- \. \! \( \) \# \+ \= \> \| \{ \} OUTSIDE code blocks\! From 6e6d72d001b2958407c596250caebc881401eb98 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 11:00:02 +0800 Subject: [PATCH 13/16] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0python-telegra?= =?UTF-8?q?m-bot=E6=B5=8B=E8=AF=95=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/uv.lock b/uv.lock index f706190..05f5f90 100644 --- a/uv.lock +++ b/uv.lock @@ -358,6 +358,7 @@ dependencies = [ { name = "claude-agent-sdk" }, { name = "httpx" }, { name = "loguru" }, + { name = "python-telegram-bot" }, ] [package.metadata] @@ -366,6 +367,7 @@ requires-dist = [ { name = "claude-agent-sdk", specifier = ">=0.1.18" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "python-telegram-bot", specifier = ">=21.0" }, ] [[package]] @@ -521,6 +523,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, ] +[[package]] +name = "python-telegram-bot" +version = "22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1c519a3ca8ad0babc650ec63dbfbd1b73babf625ed54/python_telegram_bot-22.5.tar.gz", hash = "sha256:82d4efd891d04132f308f0369f5b5929e0b96957901f58bcef43911c5f6f92f8", size = 1488269, upload-time = "2025-09-27T13:50:27.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/340c7520095a8c79455fcf699cbb207225e5b36490d2b9ee557c16a7b21b/python_telegram_bot-22.5-py3-none-any.whl", hash = "sha256:4b7cd365344a7dce54312cc4520d7fa898b44d1a0e5f8c74b5bd9b540d035d16", size = 730976, upload-time = "2025-09-27T13:50:25.93Z" }, +] + [[package]] name = "pywin32" version = "311" From d70fc08159e5aba23811ff699bf6ee2eb50916d5 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 11:19:37 +0800 Subject: [PATCH 14/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DCI=E4=B8=ADARM64?= =?UTF-8?q?=20Docker=E9=95=9C=E5=83=8F=E6=9E=84=E5=BB=BA=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用QEMU模拟ARM64架构 - 使用docker/build-push-action替代原生docker build - 添加hadolint进行Dockerfile语法检查 - 启用GitHub Actions缓存加速构建 - 扩大触发路径到整个agent-sdk-server目录 --- .github/workflows/docker-validate.yml | 46 ++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/.github/workflows/docker-validate.yml b/.github/workflows/docker-validate.yml index 62b66c0..314283e 100644 --- a/.github/workflows/docker-validate.yml +++ b/.github/workflows/docker-validate.yml @@ -3,37 +3,39 @@ name: Docker Build Validation on: push: paths: - - 'agent-sdk-server/Dockerfile' + - 'agent-sdk-server/**' pull_request: paths: - - 'agent-sdk-server/Dockerfile' + - 'agent-sdk-server/**' jobs: - validate-docker: + lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Build and validate file permissions - run: | - cd agent-sdk-server - docker build -t agent-sdk-server:test . + - name: Lint Dockerfile + uses: hadolint/hadolint-action@v3 + with: + dockerfile: agent-sdk-server/Dockerfile - # Simulate Lambda init - copy from /opt to /tmp - docker run --rm agent-sdk-server:test python -c " - import shutil - from pathlib import Path + build: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 - src = Path('/opt/claude-config') - dst = Path('/tmp/.claude-code') - dst.mkdir(exist_ok=True) + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - for item in src.iterdir(): - target = dst / item.name - if item.is_dir(): - shutil.copytree(item, target, dirs_exist_ok=True) - else: - shutil.copy2(item, target) + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - print('Lambda init simulation: OK') - " + - name: Build ARM64 image + uses: docker/build-push-action@v6 + with: + context: ./agent-sdk-server + platforms: linux/arm64 + push: false + cache-from: type=gha + cache-to: type=gha,mode=max From 75a5e19d463ca1ab8e1f8028acfba3b9e15114e7 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 11:21:54 +0800 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3hadolint-action?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-validate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-validate.yml b/.github/workflows/docker-validate.yml index 314283e..4aef80d 100644 --- a/.github/workflows/docker-validate.yml +++ b/.github/workflows/docker-validate.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - name: Lint Dockerfile - uses: hadolint/hadolint-action@v3 + uses: hadolint/hadolint-action@v3.1.0 with: dockerfile: agent-sdk-server/Dockerfile From 247c18cebbad183865152db6e1f29da4447b31c7 Mon Sep 17 00:00:00 2001 From: BukeLy Date: Sun, 4 Jan 2026 11:24:38 +0800 Subject: [PATCH 16/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DDockerfile=20had?= =?UTF-8?q?olint=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加WORKDIR设置(DL3045) - 添加dnf clean all清理缓存(DL3040) - 使用./*.py避免glob被解释为选项(SC2035) - 忽略版本固定警告(DL3016,DL3041)以支持自动更新 --- agent-sdk-server/Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/agent-sdk-server/Dockerfile b/agent-sdk-server/Dockerfile index 85cc07d..43ccd94 100644 --- a/agent-sdk-server/Dockerfile +++ b/agent-sdk-server/Dockerfile @@ -1,18 +1,24 @@ FROM public.ecr.aws/lambda/python:3.12-arm64 +# Set working directory (Lambda default) +WORKDIR ${LAMBDA_TASK_ROOT} + # Install uv and create uvx symlink COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv RUN ln -s /usr/local/bin/uv /usr/local/bin/uvx # Install Claude Code CLI (npm package, requires nodejs) -RUN dnf install -y nodejs npm && npm install -g @anthropic-ai/claude-code +# hadolint ignore=DL3016,DL3041 +RUN dnf install -y nodejs npm && \ + dnf clean all && \ + npm install -g @anthropic-ai/claude-code # Install Python dependencies RUN uv pip install --system boto3 claude-agent-sdk # Copy Lambda code and ensure readable -COPY *.py ./ -RUN chmod 644 *.py +COPY ./*.py ./ +RUN chmod 644 ./*.py # Copy config files (MCP servers, SubAgents) to /opt (read-only at runtime) # setup_lambda_environment() will copy to /tmp/.claude-code at runtime