一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。
二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。
三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
167 lines
5.2 KiB
Python
167 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
"""保存并读取 Oil Motion 的本地配置。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import getpass
|
||
import json
|
||
import os
|
||
import stat
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
import secure_credentials
|
||
|
||
|
||
CONFIG_FILE_ENV = "OIL_MOTION_CONFIG_FILE"
|
||
API_KEY_ENV = "ZENMUX_API_KEY"
|
||
CREDENTIAL_REF = 'oil-motion/zenmux/default'
|
||
|
||
|
||
def config_path() -> Path:
|
||
override = os.environ.get(CONFIG_FILE_ENV, "").strip()
|
||
if override:
|
||
return Path(override).expanduser().resolve()
|
||
config_home = os.environ.get("XDG_CONFIG_HOME", "").strip()
|
||
root = Path(config_home).expanduser() if config_home else Path.home() / ".config"
|
||
return root / "oil-motion" / "config.json"
|
||
|
||
|
||
def read_config(path: Path | None = None) -> dict[str, Any]:
|
||
target = path or config_path()
|
||
if not target.exists():
|
||
return {}
|
||
try:
|
||
value = json.loads(target.read_text(encoding="utf-8"))
|
||
except json.JSONDecodeError as error:
|
||
raise RuntimeError(f"配置文件不是有效的 JSON:{target}") from error
|
||
if not isinstance(value, dict):
|
||
raise RuntimeError(f"配置文件的根节点必须是对象:{target}")
|
||
return value
|
||
|
||
|
||
def configured_api_key(path: Path | None = None) -> tuple[str, str]:
|
||
environment_key = os.environ.get(API_KEY_ENV, "").strip()
|
||
if environment_key:
|
||
return environment_key, API_KEY_ENV
|
||
config = read_config(path)
|
||
zenmux = config.get("zenmux")
|
||
if isinstance(zenmux, dict):
|
||
if zenmux.get('credential_ref'):
|
||
key = secure_credentials.read(zenmux['credential_ref'])
|
||
return (key, '系统凭据库') if key else ('', '')
|
||
stored_key = zenmux.get("api_key")
|
||
if isinstance(stored_key, str) and stored_key.strip():
|
||
return stored_key.strip(), str(path or config_path())
|
||
return "", ""
|
||
|
||
|
||
def require_api_key(path: Path | None = None) -> str:
|
||
api_key, _ = configured_api_key(path)
|
||
if api_key:
|
||
return api_key
|
||
raise RuntimeError(
|
||
"读取不到 ZenMux API Key。先运行 `node scripts/credential-ui/src/profile.ts status default`;"
|
||
"未配置时用 `setup default` 打开本机页面由用户填写,"
|
||
"再通过 `profile.ts run default -- python3 <生成脚本>` 运行生成命令。"
|
||
)
|
||
|
||
|
||
def write_config(config: dict[str, Any], path: Path | None = None) -> Path:
|
||
target = path or config_path()
|
||
target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||
try:
|
||
target.parent.chmod(0o700)
|
||
except OSError:
|
||
pass
|
||
temporary = target.with_suffix(f"{target.suffix}.tmp")
|
||
temporary.write_text(
|
||
json.dumps(config, ensure_ascii=False, indent=2) + "\n",
|
||
encoding="utf-8",
|
||
)
|
||
try:
|
||
temporary.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||
except OSError:
|
||
pass
|
||
temporary.replace(target)
|
||
return target
|
||
|
||
|
||
def set_api_key(path: Path | None = None) -> int:
|
||
key = getpass.getpass("ZenMux API Key:").strip()
|
||
if not key:
|
||
raise RuntimeError("API Key 不能为空")
|
||
config = read_config(path)
|
||
zenmux = config.get("zenmux")
|
||
if not isinstance(zenmux, dict):
|
||
zenmux = {}
|
||
secure_credentials.save(CREDENTIAL_REF, key)
|
||
zenmux.pop('api_key', None)
|
||
zenmux['credential_ref'] = CREDENTIAL_REF
|
||
config["zenmux"] = zenmux
|
||
target = write_config(config, path)
|
||
print(f"已保存:{target}")
|
||
return 0
|
||
|
||
|
||
def clear_api_key(path: Path | None = None) -> int:
|
||
target = path or config_path()
|
||
config = read_config(target)
|
||
zenmux = config.get("zenmux")
|
||
if isinstance(zenmux, dict):
|
||
reference = zenmux.get('credential_ref')
|
||
if reference:
|
||
secure_credentials.delete(reference)
|
||
zenmux.pop('credential_ref', None)
|
||
zenmux.pop("api_key", None)
|
||
if zenmux:
|
||
config["zenmux"] = zenmux
|
||
else:
|
||
config.pop("zenmux", None)
|
||
if config:
|
||
write_config(config, target)
|
||
elif target.exists():
|
||
target.unlink()
|
||
print("已清除 ZenMux API Key")
|
||
return 0
|
||
|
||
|
||
def show_status(path: Path | None = None) -> int:
|
||
_, source = configured_api_key(path)
|
||
if source:
|
||
print(f"ZenMux API Key 已配置(来源:{source})")
|
||
return 0
|
||
print("ZenMux API Key 尚未配置")
|
||
return 1
|
||
|
||
|
||
def parser() -> argparse.ArgumentParser:
|
||
result = argparse.ArgumentParser(description="管理 Oil Motion 的本地配置")
|
||
subparsers = result.add_subparsers(dest="command", required=True)
|
||
subparsers.add_parser("set", help="在隐藏输入框中保存 ZenMux API Key")
|
||
subparsers.add_parser("status", help="检查 API Key 是否已经配置")
|
||
subparsers.add_parser("clear", help="清除已经保存的 API Key")
|
||
subparsers.add_parser("path", help="显示配置文件路径")
|
||
return result
|
||
|
||
|
||
def main() -> int:
|
||
args = parser().parse_args()
|
||
if args.command == "set":
|
||
return set_api_key()
|
||
if args.command == "status":
|
||
return show_status()
|
||
if args.command == "clear":
|
||
return clear_api_key()
|
||
print(config_path())
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
raise SystemExit(main())
|
||
except RuntimeError as error:
|
||
print(f"错误:{error}", file=sys.stderr)
|
||
raise SystemExit(1) from error
|