fix(skills): encode GitHub API fallback query parameters (#6661)

Co-authored-by: ranjianghong526-hash <228174768+ranjianghong526-hash@users.noreply.github.com>
This commit is contained in:
ranjianghong526-hash 2026-10-11 11:50:12 +08:00 committed by GitHub
parent 128dafd0ce
commit 04a45db836
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 1 deletions

View File

@ -225,6 +225,8 @@ These apply repo-wide; module guides own the module-specific detail.
- **Test-driven development** — features and bug fixes ship with tests. Backend tests live
in `backend/tests/` (TDD is mandatory there; see [backend/AGENTS.md](backend/AGENTS.md));
frontend tests live in `frontend/tests/`.
Public skill script regressions live in `tests/skills/`; exercise dependency fallbacks
with the optional import unavailable and mock HTTP transport to keep tests offline.
- **Format before pushing** — run `make format` (backend) / `pnpm check` (frontend). Backend
CI enforces `ruff format --check`, so formatting must be clean before a push.
- **Skill text encoding** — treat `SKILL.md` and other textual skill resources as UTF-8;

View File

@ -1299,6 +1299,10 @@ Skill Markdown and bundled text resources use UTF-8. Skill-creator CLI and revie
Custom skill history preserves Unicode line separators inside saved content and metadata, keeping those revisions readable for history inspection and rollback. Malformed JSON history records are still rejected.
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.
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`.
After an answer loads skills, its toolbar includes **Skills used**. Hover over or click the icon to see the skills and their sources; hovering a skill name underlines it. Select a skill to inspect its `SKILL.md` in the resizable side panel (a drawer on mobile). The view uses snapshots captured during successful configured read-tool loads or explicit slash activation, so later edits or removal of a skill do not rewrite its history. Repeated loads appear once per run, in first-load order. Range reads and bounded snapshots are labeled as partial; older conversations without captured evidence do not show this menu. Copy returns the captured Markdown, including YAML frontmatter. Package-relative links and images remain readable references rather than navigating away from the conversation. Skill loads performed through other tools, such as shell commands, are not inferred from answer text.

View File

@ -14,6 +14,7 @@ try:
except ImportError:
# Fallback to urllib if requests not available
import urllib.error
import urllib.parse
import urllib.request
class RequestsFallback:
@ -35,7 +36,7 @@ except ImportError:
@staticmethod
def get(url: str, headers: dict = None, params: dict = None, timeout: int = 30):
if params:
query = "&".join(f"{k}={v}" for k, v in params.items())
query = urllib.parse.urlencode(params)
url = f"{url}?{query}"
req = urllib.request.Request(url, headers=headers or {})

View File

@ -0,0 +1,84 @@
"""Exercise the GitHub research client's dependency-free HTTP fallback."""
import http.client
import io
import runpy
import sys
import urllib.request
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
@pytest.fixture
def fallback_client(monkeypatch):
monkeypatch.setitem(sys.modules, "requests", None)
script = (
Path(__file__).resolve().parents[2]
/ "skills/public/github-deep-research/scripts/github_api.py"
)
module = runpy.run_path(str(script))
captured = {}
def fake_urlopen(request, timeout):
# Validate the request target as the real HTTP transport does, without IO.
connection = http.client.HTTPSConnection("api.github.com")
try:
connection.putrequest("GET", request.selector)
finally:
connection.close()
captured.update(url=request.full_url, timeout=timeout, headers=request.headers)
response = io.BytesIO(b'{"ok": true}')
response.status = 200
return response
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
return module["GitHubAPI"](), captured
@pytest.mark.parametrize("query", ["bug fix", "C++ & retry#fragment", "模型请求"])
def test_fallback_search_preserves_query(fallback_client, query):
client, captured = fallback_client
assert client.search_issues("owner", "repo", query) == {"ok": True}
url = urlsplit(captured["url"])
assert url.path == "/search/issues"
assert url.fragment == ""
assert parse_qs(url.query) == {
"q": [f"repo:owner/repo {query}"],
"per_page": ["30"],
}
def test_fallback_issues_preserves_filters(fallback_client):
client, captured = fallback_client
assert client.get_issues(
"owner", "repo", state="open", limit=10, labels="bug,help wanted"
) == {"ok": True}
assert parse_qs(urlsplit(captured["url"]).query) == {
"state": ["open"],
"per_page": ["10"],
"labels": ["bug,help wanted"],
}
def test_fallback_ordinary_parameters(fallback_client):
client, captured = fallback_client
assert client.get_contributors("owner", "repo", limit=10) == {"ok": True}
assert parse_qs(urlsplit(captured["url"]).query) == {"per_page": ["10"]}
assert captured["timeout"] == 30
assert captured["headers"]["User-agent"] == "Deep-Research-Bot/1.0"
def test_fallback_without_parameters(fallback_client):
client, captured = fallback_client
assert client.get_repo_info("owner", "repo") == {"ok": True}
assert captured["url"] == "https://api.github.com/repos/owner/repo"