refactor(config): 统一配置存储为 ~/.mytoolkit/config.json (keys.*)
- Config 类改用 config.json + keys.* 格式,废弃 env.json (自动迁移) - 新增 resolve_key() / write_key_to_mytoolkit() 共享工具函数 - env CLI 文本更新 (vars → keys) - 更新 CLAUDE.md / README.md 配置描述
This commit is contained in:
@@ -43,16 +43,25 @@ skill 或其他脚本应通过上述命令定位资源,避免硬编码 `~/work
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 语音合成 (TTS)
|
## 2. 配置管理
|
||||||
|
|
||||||
|
通过 `mytoolkit env` 管理,存储于 `~/.mytoolkit/config.json`(`keys.*` 格式)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mytoolkit env set apikey_ark <key>
|
||||||
|
mytoolkit env set volc_appid <appid>
|
||||||
|
mytoolkit env set volc_access_token <token>
|
||||||
|
mytoolkit env list # 查看所有 key
|
||||||
|
mytoolkit env export # 导出为 shell export 语句
|
||||||
|
```
|
||||||
|
|
||||||
|
API key 读取顺序(所有 skill 脚本统一):环境变量 → `~/.mytoolkit/config.json`。共享调用为 `mytoolkit.config.resolve_key()`。
|
||||||
|
|
||||||
|
## 3. 语音合成 (TTS)
|
||||||
|
|
||||||
通过火山引擎(豆包)WebSocket API 实现,命令为 `mytoolkit voice tts`。
|
通过火山引擎(豆包)WebSocket API 实现,命令为 `mytoolkit voice tts`。
|
||||||
|
|
||||||
### 配置
|
配置方式见上方 §2 配置管理。
|
||||||
|
|
||||||
```bash
|
|
||||||
mytoolkit env set volc_appid <appid>
|
|
||||||
mytoolkit env set volc_access_token <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 用法
|
### 用法
|
||||||
|
|
||||||
@@ -70,7 +79,7 @@ mytoolkit voice tts "文本" -v zh_male_wennuanahu_moon_bigtts --format mp3 --sp
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. LaTeX 论文脚手架与投稿模板
|
## 4. LaTeX 论文脚手架与投稿模板
|
||||||
|
|
||||||
### 新建论文项目
|
### 新建论文项目
|
||||||
|
|
||||||
@@ -108,7 +117,7 @@ mytoolkit init journal springer mydir
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 依赖管理
|
## 5. 依赖管理
|
||||||
|
|
||||||
项目使用 `uv` 管理依赖。修改 `pyproject.toml` 后运行 `uv sync` 同步。
|
项目使用 `uv` 管理依赖。修改 `pyproject.toml` 后运行 `uv sync` 同步。
|
||||||
|
|
||||||
@@ -118,7 +127,7 @@ uv sync
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 命名规范
|
## 6. 命名规范
|
||||||
|
|
||||||
- **文件与文件夹**:英文统一使用 kebab-case,如 `review-comments.docx`
|
- **文件与文件夹**:英文统一使用 kebab-case,如 `review-comments.docx`
|
||||||
- **代码中的函数与变量**:统一使用 snake_case,如 `review_comments()`
|
- **代码中的函数与变量**:统一使用 snake_case,如 `review_comments()`
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ All user-level config lives under `~/.mytoolkit/` (override with `$MYTOOLKIT_HOM
|
|||||||
|
|
||||||
| File | Owner | Purpose |
|
| File | Owner | Purpose |
|
||||||
|------|-------|---------|
|
|------|-------|---------|
|
||||||
| `env.json` | `mytoolkit env` | API keys, paths, feishu credentials (flat key/value) |
|
| `config.json` | `mytoolkit env` | API keys, paths, feishu credentials (keys.* namespace) |
|
||||||
| `mail.json` | `mytoolkit mail` | IMAP/SMTP server settings |
|
| `mail.json` | `mytoolkit mail` | IMAP/SMTP server settings |
|
||||||
| `templates.json` | `mytoolkit templates` | Pointer to external template root |
|
| `templates.json` | `mytoolkit templates` | Pointer to external template root |
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ mytoolkit env export # print MYCLI_FOO=... export statements
|
|||||||
eval "$(mytoolkit env export)" # load into current shell
|
eval "$(mytoolkit env export)" # load into current shell
|
||||||
```
|
```
|
||||||
|
|
||||||
Legacy `bin/config.json` (in the source dir) is auto-migrated to `~/.mytoolkit/env.json` on first run after upgrade.
|
Legacy `~/.mytoolkit/env.json` is auto-migrated into `~/.mytoolkit/config.json` (keys.* format) on first load after upgrade.
|
||||||
|
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ def env_cmd():
|
|||||||
|
|
||||||
@env_cmd.command("list")
|
@env_cmd.command("list")
|
||||||
def env_list():
|
def env_list():
|
||||||
"""List all vars."""
|
"""List all keys."""
|
||||||
click.echo(f"Storage: {CONFIG_PATH}")
|
click.echo(f"Storage: {CONFIG_PATH}")
|
||||||
vars = config.get_all()
|
keys = config.get_all()
|
||||||
if not vars:
|
if not keys:
|
||||||
click.echo("No vars configured.")
|
click.echo("No keys configured.")
|
||||||
return
|
return
|
||||||
click.echo()
|
click.echo()
|
||||||
for name, value in sorted(vars.items()):
|
for name, value in sorted(keys.items()):
|
||||||
click.echo(f"{name:20} = {value}")
|
click.echo(f"{name:20} = {value}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+137
-50
@@ -1,94 +1,181 @@
|
|||||||
"""Configuration management for bin."""
|
"""Configuration management for mytoolkit.
|
||||||
|
|
||||||
|
Unified config file: ~/.mytoolkit/config.json (keys.* namespace)
|
||||||
|
Legacy: ~/.mytoolkit/env.json (migrated on first load)
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
_MYTOOLKIT_HOME = Path(os.environ.get("MYTOOLKIT_HOME", Path.home() / ".mytoolkit"))
|
_MYTOOLKIT_HOME = Path(os.environ.get("MYTOOLKIT_HOME", Path.home() / ".mytoolkit"))
|
||||||
CONFIG_PATH = _MYTOOLKIT_HOME / "env.json"
|
CONFIG_PATH = _MYTOOLKIT_HOME / "config.json"
|
||||||
_LEGACY_PATH = Path(__file__).parent / "config.json"
|
_LEGACY_MODULE_PATH = Path(__file__).parent / "config.json"
|
||||||
|
_LEGACY_ENV_PATH = _MYTOOLKIT_HOME / "env.json"
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Simple config manager for environment variables."""
|
"""Simple config manager for mytoolkit.
|
||||||
|
|
||||||
|
Stores in ~/.mytoolkit/config.json under the "keys" key::
|
||||||
|
|
||||||
|
{"keys": {"apikey_ark": "...", "volc_appid": "...", "path_study": "...", ...}}
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._config_path = CONFIG_PATH
|
self._config_path = CONFIG_PATH
|
||||||
self._migrate_legacy()
|
self._migrate_legacy()
|
||||||
self._data = self._load()
|
self._data = self._load()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Migration
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _migrate_legacy(self) -> None:
|
def _migrate_legacy(self) -> None:
|
||||||
"""One-shot move of legacy config files into env.json."""
|
"""One-shot: merge legacy env.json into config.json (keys.* format)."""
|
||||||
if self._config_path.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
migrated = False
|
|
||||||
|
|
||||||
# 1) Legacy bundled config.json next to this module.
|
|
||||||
if _LEGACY_PATH.exists():
|
|
||||||
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
_LEGACY_PATH.rename(self._config_path)
|
|
||||||
migrated = True
|
|
||||||
|
|
||||||
# 2) Old ~/.mytoolkit/config.json (sectioned layout) -> flat env.json vars.
|
# Load any existing config.json
|
||||||
legacy_user = self._config_path.parent / "config.json"
|
existing: dict = {}
|
||||||
if not migrated and legacy_user.exists():
|
if self._config_path.exists():
|
||||||
try:
|
try:
|
||||||
with open(legacy_user, encoding="utf-8") as f:
|
existing = json.loads(self._config_path.read_text())
|
||||||
data = json.load(f)
|
|
||||||
except (json.JSONDecodeError, OSError):
|
except (json.JSONDecodeError, OSError):
|
||||||
data = {}
|
existing = {}
|
||||||
flat_vars: dict[str, str] = {}
|
|
||||||
for section in ("paths", "keys", "tokens", "other"):
|
keys = existing.get("keys", {})
|
||||||
section_data = data.get(section)
|
|
||||||
if isinstance(section_data, dict):
|
# 1) Legacy ~/.mytoolkit/env.json (vars.* → keys.*)
|
||||||
flat_vars.update(section_data)
|
migrated_env = False
|
||||||
if flat_vars:
|
if _LEGACY_ENV_PATH.exists():
|
||||||
self._data = {"vars": flat_vars}
|
try:
|
||||||
self.save()
|
legacy = json.loads(_LEGACY_ENV_PATH.read_text())
|
||||||
legacy_user.unlink()
|
legacy_vars = legacy.get("vars", {})
|
||||||
migrated = True
|
if legacy_vars:
|
||||||
|
keys.update(legacy_vars)
|
||||||
|
migrated_env = True
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if migrated_env:
|
||||||
|
existing["keys"] = dict(sorted(keys.items()))
|
||||||
|
self._config_path.write_text(
|
||||||
|
json.dumps(existing, indent=2, ensure_ascii=False) + "\n"
|
||||||
|
)
|
||||||
|
# Rename env.json out of the way so we don't re-migrate
|
||||||
|
backup = _LEGACY_ENV_PATH.with_name("env.json.migrated")
|
||||||
|
if not backup.exists():
|
||||||
|
_LEGACY_ENV_PATH.rename(backup)
|
||||||
|
|
||||||
|
# 2) Very old bundled config.json next to this module (early mytoolkit)
|
||||||
|
if not self._config_path.exists() and _LEGACY_MODULE_PATH.exists():
|
||||||
|
try:
|
||||||
|
_LEGACY_MODULE_PATH.rename(self._config_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Load / Save
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _load(self) -> dict:
|
def _load(self) -> dict:
|
||||||
if self._config_path.exists():
|
if self._config_path.exists():
|
||||||
with open(self._config_path, encoding="utf-8") as f:
|
try:
|
||||||
return json.load(f)
|
return json.loads(self._config_path.read_text())
|
||||||
return {"vars": {}}
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
return {"keys": {}}
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save config to file."""
|
|
||||||
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(self._config_path, "w", encoding="utf-8") as f:
|
self._config_path.write_text(
|
||||||
json.dump(self._data, f, indent=2, ensure_ascii=False)
|
json.dumps(self._data, indent=2, ensure_ascii=False) + "\n"
|
||||||
f.write("\n")
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Accessors
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def get(self, name: str) -> str | None:
|
def get(self, name: str) -> str | None:
|
||||||
"""Get var by name."""
|
"""Get a key by name."""
|
||||||
return self._data.get("vars", {}).get(name)
|
return self._data.get("keys", {}).get(name)
|
||||||
|
|
||||||
def get_all(self) -> dict[str, str]:
|
def get_all(self) -> dict[str, str]:
|
||||||
"""Get all vars."""
|
"""Return all keys as a flat dict."""
|
||||||
return self._data.get("vars", {}).copy()
|
return self._data.get("keys", {}).copy()
|
||||||
|
|
||||||
def set(self, name: str, value: str):
|
def set(self, name: str, value: str):
|
||||||
"""Set a var."""
|
"""Set a key and persist."""
|
||||||
if "vars" not in self._data:
|
if "keys" not in self._data:
|
||||||
self._data["vars"] = {}
|
self._data["keys"] = {}
|
||||||
self._data["vars"][name] = value
|
self._data["keys"][name] = value
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def remove(self, name: str) -> bool:
|
def remove(self, name: str) -> bool:
|
||||||
"""Remove a var. Returns True if existed."""
|
"""Remove a key. Returns True if existed."""
|
||||||
if name in self._data.get("vars", {}):
|
if name in self._data.get("keys", {}):
|
||||||
del self._data["vars"][name]
|
del self._data["keys"][name]
|
||||||
self.save()
|
self.save()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def export(self) -> dict[str, str]:
|
def export(self) -> dict[str, str]:
|
||||||
"""Get all vars with env-compatible names."""
|
"""Return all keys with MYCLI_ prefix for shell eval."""
|
||||||
return {f"MYCLI_{k.upper()}": v for k, v in self.get_all().items()}
|
return {f"MYCLI_{k.upper()}": v for k, v in self.get_all().items()}
|
||||||
|
|
||||||
|
|
||||||
# Global instance
|
# Global singleton
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Shared utility: 2-layer key resolution for skill scripts
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def resolve_key(name: str) -> str:
|
||||||
|
"""Two-layer key resolution.
|
||||||
|
|
||||||
|
Priority:
|
||||||
|
1. Environment variable (checks ``MYCLI_<NAME>`` first, then ``<NAME>``)
|
||||||
|
2. ``~/.mytoolkit/config.json`` → ``keys.<name>``
|
||||||
|
|
||||||
|
Returns the key value, or ``""`` if not found.
|
||||||
|
"""
|
||||||
|
# 1. Env var (MYCLI_ prefix first, then bare)
|
||||||
|
env_name = name.upper()
|
||||||
|
for var in (f"MYCLI_{env_name}", env_name):
|
||||||
|
val = os.environ.get(var, "")
|
||||||
|
if val:
|
||||||
|
return val
|
||||||
|
|
||||||
|
# 2. ~/.mytoolkit/config.json
|
||||||
|
try:
|
||||||
|
cfg_path = Path.home() / ".mytoolkit" / "config.json"
|
||||||
|
if cfg_path.exists():
|
||||||
|
data = json.loads(cfg_path.read_text())
|
||||||
|
return data.get("keys", {}).get(name, "")
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def write_key_to_mytoolkit(name: str, value: str) -> None:
|
||||||
|
"""Write a key into ~/.mytoolkit/config.json (persistent).
|
||||||
|
|
||||||
|
Called by skills when a key is missing and the user provides it.
|
||||||
|
"""
|
||||||
|
cfg_path = Path.home() / ".mytoolkit" / "config.json"
|
||||||
|
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
if cfg_path.exists():
|
||||||
|
data = json.loads(cfg_path.read_text())
|
||||||
|
else:
|
||||||
|
data = {}
|
||||||
|
if "keys" not in data:
|
||||||
|
data["keys"] = {}
|
||||||
|
data["keys"][name] = value
|
||||||
|
cfg_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
data = {"keys": {name: value}}
|
||||||
|
cfg_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user