From 5dc80afaa76b7324cb21dc2f5db23524db9fe274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B8=AD=E9=B8=AD=E3=80=8C=E3=82=AB=E3=83=A2=E3=80=8D?= <89643991+DuckDuckStudio@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:00:06 +0800 Subject: [PATCH] =?UTF-8?q?feat!(config):=20=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=20Sundry=20=E8=87=AA=E5=B7=B1=E7=AE=A1=E7=90=86=20Tok?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit !: 配置项 github.token 的默认值从 glm 改为 keyring --- src/function/github/token.py | 39 ----- src/function/maintain/config.py | 8 +- src/tools/ignore.py | 2 +- src/tools/logsAnalyse.py | 2 +- src/tools/maintain/config.py | 13 +- src/tools/maintain/token.py | 135 ++++++++++++++++++ src/tools/modify.py | 2 +- src/tools/remove.py | 2 +- src/tools/verify.py | 2 +- .../github => tools/maintain}/test_token.py | 41 +++--- 10 files changed, 179 insertions(+), 67 deletions(-) delete mode 100644 src/function/github/token.py create mode 100644 src/tools/maintain/token.py rename tests/{function/github => tools/maintain}/test_token.py (54%) diff --git a/src/function/github/token.py b/src/function/github/token.py deleted file mode 100644 index 22222f88..00000000 --- a/src/function/github/token.py +++ /dev/null @@ -1,39 +0,0 @@ -import keyring -from catfood.exceptions.operation import OperationFailed -from catfood.functions.print import MSHead -from colorama import Fore -from keyring.errors import KeyringError - -from function.maintain.config import 读取配置 - - -def read_token(silent: bool = False) -> str | None: - """尝试从配置文件中指定的源读取 Token,读取失败返回 None""" - - try: - source = 读取配置("github.token") - if not isinstance(source, str): - raise OperationFailed("未能从配置文件中获取读取源") - - if source == "env": - import os - if token := os.getenv("GITHUB_TOKEN"): - return token - else: - raise OperationFailed("没有读取到 Token,请确保您设置了 GITHUB_TOKEN 环境变量") - else: - if source == "glm": - service_name, username = ("github-access-token.glm", "github-access-token") - elif source == "komac": - service_name, username = ("github-access-token.komac", "github-access-token") - else: - raise OperationFailed(f"未知的读取源 {source}") - - if token := keyring.get_password(service_name, username): - return token - else: - raise OperationFailed(f"没有读取到 Token,请确保您设置了 {source} 的 Token ({service_name}, {username})") - except (OperationFailed, KeyringError) as e: - if not silent: - print(f"{MSHead.Error} 读取 Token 失败: {Fore.RED}{e}{Fore.RESET}") - return None diff --git a/src/function/maintain/config.py b/src/function/maintain/config.py index dbd5ed51..f854a61d 100644 --- a/src/function/maintain/config.py +++ b/src/function/maintain/config.py @@ -52,7 +52,7 @@ class 配置信息: "maintainer_can_modify": False, "mention_self_when_reviewer": False }, - "token": "glm", + "token": "keyring", }, "tools": { "autoremove": { @@ -145,7 +145,7 @@ def 验证配置(配置项: 所有配置项 | str, 配置值: str | bool | int) elif (配置项 == "i18n.lang") and (配置值 not in ["zh-cn", "en-us"]): return f"不支持的语言 {Fore.BLUE}{配置值}{Fore.RESET}" - elif (配置项 == "github.token") and (配置值 not in ["glm", "komac", "env"]): + elif (配置项 == "github.token") and (配置值 not in ["keyring", "glm", "komac", "env"]): return "未知的 Token 读取源" elif (配置项 == "git.retry_interval") and (not isinstance(配置值, int)): @@ -266,7 +266,7 @@ def 获取配置schema(版本: str) -> dict[str, Any] | None: # NOTE 这里的导入不要放顶级,会出现循环导入 from catfood.functions.github.api import get_github_file_content - from function.github.token import read_token + from tools.maintain.token import read_token print(f"{MSHead.Information} 尝试从 GitHub API 获取配置文件 schema ...") schema文件 = get_github_file_content("DuckDuckStudio/yazicbs.github.io", f"Tools/Sundry/config/schema/{版本}.json", read_token(silent=True)) @@ -321,7 +321,7 @@ def 转换配置值(配置项: 所有配置项 | str, 配置值: str) -> str | b if not 配置值: # 使用默认配置值 if 配置项 == "github.token": - 配置值 = "glm" + 配置值 = "keyring" elif 配置项 == "i18n.lang": 配置值 = "zh-cn" else: diff --git a/src/tools/ignore.py b/src/tools/ignore.py index 04ddd2d8..7505390e 100644 --- a/src/tools/ignore.py +++ b/src/tools/ignore.py @@ -11,8 +11,8 @@ from function.constant.general import REQUEST_TIMEOUT, RETRY_INTERVAL from function.git.format import branchName -from function.github.token import read_token from function.maintain.config import 读取配置 +from tools.maintain.token import read_token def 追加忽略字段(检测程序路径: str, 忽略字段: str, 理由: str) -> bool | str: diff --git a/src/tools/logsAnalyse.py b/src/tools/logsAnalyse.py index b1381b57..f5ec4c4e 100644 --- a/src/tools/logsAnalyse.py +++ b/src/tools/logsAnalyse.py @@ -25,8 +25,8 @@ ) from function.constant.paths import VALIDATION_LOGS_DIR, VALIDATION_LOGS_ZIP_DIR from function.constant.regexes import LOGS_ZIP_DOWNLOAD_URL -from function.github.token import read_token from function.maintain.config import 读取配置 +from tools.maintain.token import read_token def main(args: list[str]) -> Literal[0, 1]: diff --git a/src/tools/maintain/config.py b/src/tools/maintain/config.py index ca9eb818..af5e686a 100644 --- a/src/tools/maintain/config.py +++ b/src/tools/maintain/config.py @@ -14,6 +14,7 @@ from pygments.formatters.terminal import TerminalFormatter from pygments.lexers.data import JsonLexer # pyright: ignore[reportMissingTypeStubs] +import tools.maintain.token from function.maintain.config import ( 获取配置schema, 读取配置, @@ -76,7 +77,15 @@ def 获取用户输入(配置项: str) -> str | bool | int: while True: 配置值 = input(提示消息).strip() try: - return 转换配置值(配置项, 配置值) + 配置值 = 转换配置值(配置项, 配置值) + if ( + (配置项 == "github.token") and (配置值 == "keyring") and + (tools.maintain.token.read_token() is None) and + tools.maintain.token.set_token() + ): + continue + + return 配置值 except OperationFailed as e: print(f"{MSHead.Error} 无法转换配置值: {Fore.RED}{e}{Fore.RESET}") @@ -317,6 +326,8 @@ def main(args: list[str]) -> int: print(f"{MSHead.Information} 配置文件 config.json 位于 {配置信息.所在位置}") print(f"{MSHead.Information} 尝试打开配置文件 config.json ...") return open_file(配置信息.所在位置) + elif args[0] == "token": + return tools.maintain.token.main(args[1] if len(args) > 1 else "") elif len(args) == 2: return 修改配置项(args[0], args[1]) else: diff --git a/src/tools/maintain/token.py b/src/tools/maintain/token.py new file mode 100644 index 00000000..30081af5 --- /dev/null +++ b/src/tools/maintain/token.py @@ -0,0 +1,135 @@ +""" +有关 GitHub Token 管理的函数。 +""" + +import os +from getpass import getpass +from typing import Literal + +import keyring +import keyring.errors +from catfood.exceptions.operation import OperationFailed +from catfood.functions.print import MSHead +from colorama import Fore + +from function.maintain.config import 读取配置 + + +def main(operation: str) -> Literal[0, 1]: + """ + ``sundry config token `` + + 此函数本质上就是一个中转站,如果你知道具体的操作,可以直接调用 ``set_token()`` 或 ``remove_token()``。 + + :param operation: "set" 设置 Token,"remove" 移除设置的 Token。 + :type operation: str + :return: 退出代码 + :rtype: Literal[0, 1] + """ + + match operation: + case "set": + return set_token() + case "remove": + return remove_token() + case "": + print(f"{MSHead.Error} 请告诉我你要做什么") + return 1 + case _: + print(f"{MSHead.Error} 未知的操作") + return 1 + +def remove_token() -> Literal[0, 1]: + """ + ``sundry config token remove`` 的实现。 + + :return: 退出代码 + :rtype: Literal[0, 1] + """ + + try: + keyring.delete_password( + service_name="DuckStudio.Sundry-GitHubToken", + username="GitHubToken" + ) + print(f"{MSHead.Success} 成功移除设置的 Token") + return 0 + except keyring.errors.PasswordDeleteError: + print(f"{MSHead.Warning} 没有找到已设置的 Token") + return 0 + except keyring.errors.KeyringError as e: + print(f"{MSHead.Error} 移除 Token 时发生异常: ({type(e)}) {e}") + return 1 + +def set_token() -> Literal[0, 1]: + """ + ``sundry config token set`` 的实现。 + + :return: 退出代码 + :rtype: Literal[0, 1] + """ + + token = getpass("请粘贴 Token: ") + if not token.startswith(("ghp_", "github_pat_")): + print(f"{MSHead.Error} Token 格式不正确,GitHub Token 应以 \"ghp_\" 或 \"github_pat_\" 开头。") + return 1 + + try: + keyring.set_password( + service_name="DuckStudio.Sundry-GitHubToken", + username="GitHubToken", + password=token + ) + del token + print(f"{MSHead.Success} 成功设置 Token") + return 0 + except keyring.errors.KeyringError as e: + print(f"{MSHead.Error} 设置 Token 时发生异常: ({type(e)}) {e}") + return 1 + +def read_token(silent: bool = False) -> str | None: + """ + 尝试从配置文件中指定的源读取 GitHub Token,读取失败返回 ``None``。 + + :pram silent: 是否在读取失败时保持沉默(不输出错误信息) + :type silent: bool + :return: 读取到的 Token,或 ``None``(如果读取失败) + :rtype: str | None + """ + + try: + source = 读取配置("github.token") + if not isinstance(source, str): + raise OperationFailed("未能从配置文件中获取 GitHub Token 读取源") + + if source == "env": + if token := os.getenv("GITHUB_TOKEN"): + return token + else: + raise OperationFailed("未能从环境变量 GITHUB_TOKEN 中读取 GitHub Token") + else: + token: str | None + match source: + case "keyring": + token = keyring.get_password( + "DuckStudio.Sundry-GitHubToken", "GitHubToken" + ) + case "komac": + token = keyring.get_password( + "github-access-token.komac", "github-access-token" + ) + case "glm": + token = keyring.get_password( + "github-access-token.glm", "github-access-token" + ) + case _: + raise OperationFailed(f"未知的 GitHub Token 读取源: {source}") + + if token: + return token + else: + raise OperationFailed(f"未能从 {source} 源中读取 GitHub Token") + except (OperationFailed, keyring.errors.KeyringError) as e: + if not silent: + print(f"{MSHead.Error} 读取 GitHub Token 失败: {Fore.RED}{e}{Fore.RESET}") + return None diff --git a/src/tools/modify.py b/src/tools/modify.py index 9c8b6dc5..f7c556fc 100644 --- a/src/tools/modify.py +++ b/src/tools/modify.py @@ -19,8 +19,8 @@ from function.constant.general import PR_TOOL_NOTE, REQUEST_TIMEOUT, RETRY_INTERVAL from function.files.manifest import FormatManifest, 获取清单目录 from function.git.format import branchName -from function.github.token import read_token from function.maintain.config import 读取配置 +from tools.maintain.token import read_token def main(args: list[str]) -> Literal[1] | Literal[0]: diff --git a/src/tools/remove.py b/src/tools/remove.py index f68fa39f..5f7dbbb0 100644 --- a/src/tools/remove.py +++ b/src/tools/remove.py @@ -19,9 +19,9 @@ from function.constant.general import PR_TOOL_NOTE, REQUEST_TIMEOUT, RETRY_INTERVAL from function.files.manifest import 获取清单目录, 获取现有包版本 from function.git.format import branchName -from function.github.token import read_token from function.maintain.config import 读取配置 from tools import cat, sync +from tools.maintain.token import read_token # 创建拉取请求 diff --git a/src/tools/verify.py b/src/tools/verify.py index 61a82232..29eae869 100644 --- a/src/tools/verify.py +++ b/src/tools/verify.py @@ -16,8 +16,8 @@ from function.constant.paths import VERIFY_TEMP_DIR from function.files.manifest import 获取PR清单, 获取清单目录 -from function.github.token import read_token from function.maintain.config import 读取配置 +from tools.maintain.token import read_token def main(args: list[str]) -> int: diff --git a/tests/function/github/test_token.py b/tests/tools/maintain/test_token.py similarity index 54% rename from tests/function/github/test_token.py rename to tests/tools/maintain/test_token.py index 2989682d..7aeaab36 100644 --- a/tests/function/github/test_token.py +++ b/tests/tools/maintain/test_token.py @@ -3,11 +3,11 @@ import keyring import pytest -from function.github.token import read_token +from tools.maintain.token import read_token def test_read_token_returns_token_from_environment(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr("function.github.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] monkeypatch.setenv("GITHUB_TOKEN", "environment-token") assert read_token() == "environment-token" @@ -17,26 +17,28 @@ def test_read_token_returns_none_when_environment_token_is_missing( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] monkeypatch.delenv("GITHUB_TOKEN", raising=False) assert read_token() is None - assert "没有读取到 Token" in capsys.readouterr().out + assert "未能从环境变量 GITHUB_TOKEN 中读取 GitHub Token" in capsys.readouterr().out @pytest.mark.parametrize( - ("source", "service_name"), + ("source", "service_name", "username"), [ - ("glm", "github-access-token.glm"), - ("komac", "github-access-token.komac"), + ("keyring", "DuckStudio.Sundry-GitHubToken", "GitHubToken"), + ("glm", "github-access-token.glm", "github-access-token"), + ("komac", "github-access-token.komac", "github-access-token"), ], ) def test_read_token_returns_token_from_keyring_source( source: str, service_name: str, + username: str, monkeypatch: pytest.MonkeyPatch, ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: source) # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: source) # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] calls: list[tuple[str, str]] = [] def get_password(service: str, username: str) -> str: @@ -46,38 +48,38 @@ def get_password(service: str, username: str) -> str: monkeypatch.setattr(keyring, "get_password", get_password) assert read_token() == "keyring-token" - assert calls == [(service_name, "github-access-token")] + assert calls == [(service_name, username)] def test_read_token_returns_none_for_invalid_source( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: "unknown") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "unknown") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] assert read_token() is None - assert "未知的读取源 unknown" in capsys.readouterr().out + assert "未知的 GitHub Token 读取源: unknown" in capsys.readouterr().out def test_read_token_returns_none_for_no_configed_source( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: None) # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: None) # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] assert read_token() is None - assert "未能从配置文件中获取读取源" in capsys.readouterr().out + assert "未能从配置文件中获取 GitHub Token 读取源" in capsys.readouterr().out def test_read_token_returns_none_when_keyring_token_is_missing( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: "komac") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "komac") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] monkeypatch.setattr(keyring, "get_password", lambda service, username: None) # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] assert read_token() is None - assert "没有读取到 Token" in capsys.readouterr().out + assert "未能从 komac 源中读取 GitHub Token" in capsys.readouterr().out def test_read_token_return_none_when_keyring_raise_error( @@ -90,18 +92,21 @@ def raise_keyring_error(service_name: str, username: str) -> NoReturn: raise KeyringError("123456") - monkeypatch.setattr("function.github.token.读取配置", lambda name: "komac") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "komac") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] monkeypatch.setattr(keyring, "get_password", raise_keyring_error) assert read_token() is None - assert "123456" in capsys.readouterr().out + + output = capsys.readouterr().out + assert "读取 GitHub Token 失败:" in output + assert "123456" in output def test_read_token_silent_failure_does_not_print( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ): - monkeypatch.setattr("function.github.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] + monkeypatch.setattr("tools.maintain.token.读取配置", lambda name: "env") # pyright: ignore[reportUnknownLambdaType, reportUnknownArgumentType] monkeypatch.delenv("GITHUB_TOKEN", raising=False) assert read_token(silent=True) is None