diff --git a/README.md b/README.md index 9535f09e76..d7df6a1943 100644 --- a/README.md +++ b/README.md @@ -1437,6 +1437,8 @@ Custom skill history preserves Unicode line separators inside saved content and The bundled `github-deep-research` API client also works without `requests`, using Python's standard-library HTTP client. Its fallback preserves spaces, Unicode, and reserved characters in search queries and label filters. +File reads accept raw repository-relative paths and preserve spaces, Unicode, +`#`, `?`, and literal percent signs with either HTTP client. Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`. diff --git a/skills/public/github-deep-research/scripts/github_api.py b/skills/public/github-deep-research/scripts/github_api.py index 61f2501f34..2e9b4dfdf9 100644 --- a/skills/public/github-deep-research/scripts/github_api.py +++ b/skills/public/github-deep-research/scripts/github_api.py @@ -8,6 +8,7 @@ import os import json import sys from typing import Any, Dict, List, Optional +from urllib.parse import quote try: import requests @@ -113,10 +114,10 @@ class GitHubAPI: raise def get_file_content(self, owner: str, repo: str, path: str) -> str: - """Get content of a specific file.""" + """Get content of a file using its raw repository-relative path.""" try: return self._get( - f"/repos/{owner}/{repo}/contents/{path}", + f"/repos/{owner}/{repo}/contents/{quote(path, safe='/')}", accept="application/vnd.github.raw", ) except Exception as e: diff --git a/tests/skills/test_github_deep_research.py b/tests/skills/test_github_deep_research.py index d175f3d9e8..2168e12443 100644 --- a/tests/skills/test_github_deep_research.py +++ b/tests/skills/test_github_deep_research.py @@ -1,4 +1,4 @@ -"""Exercise the GitHub research client's dependency-free HTTP fallback.""" +"""Exercise the GitHub research client's HTTP transports without network IO.""" import http.client import io @@ -37,6 +37,54 @@ def fallback_client(monkeypatch): return module["GitHubAPI"](), captured +@pytest.fixture(params=["requests", "urllib"]) +def file_content_client(request, monkeypatch): + if request.param == "urllib": + return request.getfixturevalue("fallback_client") + + requests = pytest.importorskip("requests") + script = ( + Path(__file__).resolve().parents[2] + / "skills/public/github-deep-research/scripts/github_api.py" + ) + module = runpy.run_path(str(script)) + captured = {} + + def fake_send(session, prepared_request, **kwargs): + captured["url"] = prepared_request.url + response = requests.Response() + response.status_code = 200 + response._content = b'{"ok": true}' + return response + + monkeypatch.setattr(requests.Session, "send", fake_send) + return module["GitHubAPI"](), captured + + +@pytest.mark.parametrize( + ("path", "encoded_path"), + [ + ("docs/C# guide.md", "docs/C%23%20guide.md"), + ("docs/query?draft.md", "docs/query%3Fdraft.md"), + ("docs/literal%20name.md", "docs/literal%2520name.md"), + ("文档/入门 guide.md", "%E6%96%87%E6%A1%A3/%E5%85%A5%E9%97%A8%20guide.md"), + ("docs/file name.md", "docs/file%20name.md"), + ("src/nested/README.md", "src/nested/README.md"), + ], +) +def test_file_content_preserves_repository_path( + file_content_client, path, encoded_path +): + client, captured = file_content_client + + assert client.get_file_content("owner", "repo", path) == '{"ok": true}' + + url = urlsplit(captured["url"]) + assert url.path == f"/repos/owner/repo/contents/{encoded_path}" + assert url.query == "" + assert url.fragment == "" + + @pytest.mark.parametrize("query", ["bug fix", "C++ & retry#fragment", "模型请求"]) def test_fallback_search_preserves_query(fallback_client, query): client, captured = fallback_client