lcode-agent 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lcode/settings.py ADDED
@@ -0,0 +1,229 @@
1
+ """应用配置:所有配置统一在 ~/.lcode/config.json,不放进仓库。
2
+
3
+ 用户只改这一个文件:url / model / models / apiKey / maxTokens / pricing /
4
+ permissionMode 全在里面,dev 和打包后行为一致。缺的键启动时自动按出厂
5
+ 默认补全,打开文件就能看到所有可配置项。
6
+ 出厂默认值随包分发(src/lcode/default_config.json),不含密钥。
7
+ 旧的 secrets.json / settings.json 迁移完成后删除。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+
16
+ HOME_DIR = Path.home() / ".lcode"
17
+ SECRETS_PATH = HOME_DIR / "secrets.json"
18
+ SETTINGS_PATH = HOME_DIR / "settings.json"
19
+ # 出厂默认值:随包分发,dev 和安装后都读这一份
20
+ PROJECT_CONFIG = Path(__file__).with_name("default_config.json")
21
+ # 唯一的用户配置点:模型、密钥都改这里,dev/打包一致
22
+ USER_CONFIG = HOME_DIR / "config.json"
23
+
24
+ _runtime: dict | None = None
25
+
26
+
27
+ def _read_json(path: Path) -> dict:
28
+ try:
29
+ data = json.loads(path.read_text(encoding="utf-8"))
30
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
31
+ return {}
32
+ return data if isinstance(data, dict) else {}
33
+
34
+
35
+ def _write_json(path: Path, data: dict) -> None:
36
+ path.parent.mkdir(parents=True, exist_ok=True)
37
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
38
+
39
+
40
+ def _migrate_key_from_project(project: dict, secrets: dict) -> tuple[dict, dict, bool]:
41
+ leaked = str(project.get("apiKey") or "").strip()
42
+ if not leaked:
43
+ if "apiKey" in project:
44
+ project = dict(project)
45
+ project.pop("apiKey", None)
46
+ return project, secrets, True
47
+ return project, secrets, False
48
+ secrets = dict(secrets)
49
+ if not str(secrets.get("apiKey") or "").strip():
50
+ secrets["apiKey"] = leaked
51
+ _write_json(SECRETS_PATH, secrets)
52
+ project = dict(project)
53
+ project.pop("apiKey", None)
54
+ return project, secrets, True
55
+
56
+
57
+ def _migrate_key_from_git(secrets: dict) -> dict:
58
+ """旧提交里若有密钥,搬到 ~/.lcode/secrets.json,不写回项目文件。
59
+
60
+ 兼容注意:git show 出来的历史文件是 UTF-8,Windows 默认按 GBK 解码会炸
61
+ (中文注释一炸,check_output 在读线程里吞异常返回 None),所以拿 bytes
62
+ 手动解码,全程兜异常——这是尽力而为的遗留迁移,绝不能挂掉启动。
63
+ """
64
+ import subprocess
65
+
66
+ root = Path.cwd()
67
+ try:
68
+ log = subprocess.check_output(
69
+ ["git", "log", "--format=%H", "--", "config.json"],
70
+ cwd=str(root),
71
+ timeout=15,
72
+ stderr=subprocess.DEVNULL,
73
+ ).decode("utf-8", errors="replace")
74
+ except Exception:
75
+ return secrets
76
+ for commit in log.split():
77
+ try:
78
+ raw = subprocess.check_output(
79
+ ["git", "show", f"{commit}:config.json"],
80
+ cwd=str(root),
81
+ timeout=15,
82
+ stderr=subprocess.DEVNULL,
83
+ )
84
+ except Exception:
85
+ continue
86
+ if not raw:
87
+ continue
88
+ try:
89
+ data = json.loads(raw.decode("utf-8", errors="replace"))
90
+ except (ValueError, UnicodeDecodeError):
91
+ continue
92
+ if not isinstance(data, dict):
93
+ continue
94
+ key = str(data.get("apiKey") or "").strip()
95
+ if not key:
96
+ continue
97
+ secrets = dict(secrets)
98
+ secrets["apiKey"] = key
99
+ _write_json(SECRETS_PATH, secrets)
100
+ return secrets
101
+ return secrets
102
+
103
+
104
+ def _backfill_user_config(project: dict, user: dict) -> dict:
105
+ """用户 config.json 缺的键按出厂默认补全,让文件本身就是完整模板。
106
+
107
+ 用户已设置过的键不动;写回成功返回补全后的 dict,失败返回原样。
108
+ """
109
+ filled = dict(user)
110
+ changed = False
111
+ for key, value in project.items():
112
+ if key == "apiKey":
113
+ continue
114
+ if key not in filled:
115
+ filled[key] = value
116
+ changed = True
117
+ if changed:
118
+ try:
119
+ _write_json(USER_CONFIG, filled)
120
+ except OSError:
121
+ return user
122
+ return filled
123
+
124
+
125
+ def load_runtime() -> dict:
126
+ """合并出厂默认 + 用户 config.json + 密钥 + 权限设置。apiKey 只留在内存里。
127
+
128
+ 用户只改 ~/.lcode/config.json:url / model / models / apiKey / maxTokens /
129
+ permissionMode 全在这一个文件,缺的键自动补成完整模板,dev 和打包一致。
130
+ 旧的 secrets.json / settings.json 迁移完成后删除。
131
+ """
132
+ global _runtime
133
+ project = _read_json(PROJECT_CONFIG)
134
+ secrets = _read_json(SECRETS_PATH)
135
+ project, secrets, stripped = _migrate_key_from_project(project, secrets)
136
+ user = _backfill_user_config(project, _read_json(USER_CONFIG))
137
+ # 环境变量 / 用户 config / 旧 secrets 都没有 key,才去翻 git 历史;
138
+ # 用户已经配好 key 的话,这个遗留迁移一次都不该跑
139
+ if not str(
140
+ os.environ.get("LCODE_API_KEY")
141
+ or user.get("apiKey")
142
+ or secrets.get("apiKey")
143
+ or ""
144
+ ).strip():
145
+ try:
146
+ secrets = _migrate_key_from_git(secrets)
147
+ except Exception:
148
+ secrets = dict(secrets)
149
+ if stripped:
150
+ _write_json(PROJECT_CONFIG, project)
151
+ merged = {**project, **user}
152
+
153
+ # 密钥:环境变量 > 用户 config.json > 旧 secrets.json;
154
+ # 用户 config 里确立了 key 之后,旧 secrets.json 使命完成,删掉
155
+ key = str(
156
+ os.environ.get("LCODE_API_KEY")
157
+ or user.get("apiKey")
158
+ or secrets.get("apiKey")
159
+ or ""
160
+ ).strip()
161
+ if (
162
+ key
163
+ and not str(user.get("apiKey") or "").strip()
164
+ and not os.environ.get("LCODE_API_KEY")
165
+ ):
166
+ user["apiKey"] = key
167
+ _write_json(USER_CONFIG, user)
168
+ if str(user.get("apiKey") or "").strip() and SECRETS_PATH.exists() and SECRETS_PATH != USER_CONFIG:
169
+ try:
170
+ SECRETS_PATH.unlink(missing_ok=True)
171
+ except OSError:
172
+ pass
173
+
174
+ # 权限模式:config.json 优先;但 settings.json 是旧位置,迁移期间旧值优先,
175
+ # 迁进 config 并删掉旧文件后就只剩一个来源
176
+ legacy_mode = str(_read_json(SETTINGS_PATH).get("permissionMode") or "").strip().lower()
177
+ mode = str(legacy_mode or user.get("permissionMode") or "ask").strip().lower()
178
+ if mode not in ("ask", "pass"):
179
+ mode = "ask"
180
+ if user.get("permissionMode") != mode:
181
+ user["permissionMode"] = mode
182
+ _write_json(USER_CONFIG, user)
183
+ if SETTINGS_PATH.exists() and SETTINGS_PATH != USER_CONFIG:
184
+ try:
185
+ SETTINGS_PATH.unlink(missing_ok=True)
186
+ except OSError:
187
+ pass
188
+
189
+ out = dict(merged)
190
+ out["apiKey"] = key
191
+ out["permissionMode"] = mode
192
+ _runtime = out
193
+ return out
194
+
195
+
196
+ def get_runtime() -> dict:
197
+ if _runtime is None:
198
+ return load_runtime()
199
+ return _runtime
200
+
201
+
202
+ def api_key() -> str:
203
+ return str(get_runtime().get("apiKey") or "")
204
+
205
+
206
+ def get_permission_mode() -> str:
207
+ mode = str(get_runtime().get("permissionMode") or "ask").strip().lower()
208
+ return mode if mode in ("ask", "pass") else "ask"
209
+
210
+
211
+ def save_permission_mode(mode: str) -> str:
212
+ mode = "pass" if str(mode).strip().lower() == "pass" else "ask"
213
+ save_project_config({"permissionMode": mode})
214
+ runtime = get_runtime()
215
+ runtime["permissionMode"] = mode
216
+ return mode
217
+
218
+
219
+ def save_project_config(fields: dict) -> dict:
220
+ """把模型等用户配置写进 ~/.lcode/config.json,并同步进运行时。
221
+
222
+ dev 和打包后都在这里改,用户始终只看这一个文件。
223
+ """
224
+ base = _read_json(USER_CONFIG)
225
+ base.update(fields)
226
+ _write_json(USER_CONFIG, base)
227
+ runtime = get_runtime()
228
+ runtime.update(fields)
229
+ return runtime