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/__init__.py +3 -0
- lcode/__main__.py +3 -0
- lcode/app.py +4140 -0
- lcode/compress.py +80 -0
- lcode/default_config.json +10 -0
- lcode/protocol.py +800 -0
- lcode/settings.py +229 -0
- lcode/store.py +889 -0
- lcode/tools.py +1147 -0
- lcode_agent-0.1.0.dist-info/METADATA +123 -0
- lcode_agent-0.1.0.dist-info/RECORD +13 -0
- lcode_agent-0.1.0.dist-info/WHEEL +4 -0
- lcode_agent-0.1.0.dist-info/entry_points.txt +2 -0
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
|