MenuPilot 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.
- menupilot/__init__.py +3 -0
- menupilot/__main__.py +4 -0
- menupilot/agent/__init__.py +0 -0
- menupilot/agent/agent_loop.py +414 -0
- menupilot/agent/matching_engine.py +974 -0
- menupilot/agent/option_expander.py +490 -0
- menupilot/agent/orchestration.py +570 -0
- menupilot/agent/rule_engine.py +509 -0
- menupilot/agent/sandbox.py +216 -0
- menupilot/agent/schema_analyzer.py +1026 -0
- menupilot/agent/template_preprocessor.py +293 -0
- menupilot/agent/token_classifier.py +816 -0
- menupilot/agent/tools.py +365 -0
- menupilot/agent/workflow.py +1072 -0
- menupilot/cli/human_review.py +191 -0
- menupilot/cli/repl.py +821 -0
- menupilot/config.py +113 -0
- menupilot/data/__init__.py +0 -0
- menupilot/data/canonical_schema.py +135 -0
- menupilot/data/mapping_rules.yaml +387 -0
- menupilot/data/memory.py +674 -0
- menupilot/data/token_dict.py +275 -0
- menupilot/excel_io/__init__.py +0 -0
- menupilot/excel_io/excel_reader.py +552 -0
- menupilot/excel_io/excel_writer.py +413 -0
- menupilot/main.py +322 -0
- menupilot/wizard.py +86 -0
- menupilot-0.1.0.dist-info/METADATA +397 -0
- menupilot-0.1.0.dist-info/RECORD +33 -0
- menupilot-0.1.0.dist-info/WHEEL +5 -0
- menupilot-0.1.0.dist-info/entry_points.txt +2 -0
- menupilot-0.1.0.dist-info/licenses/LICENSE +21 -0
- menupilot-0.1.0.dist-info/top_level.txt +1 -0
menupilot/main.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI 入口 — MenuPilot 智能 POS 模板映射助手。
|
|
3
|
+
一条命令完成 SOP 字段自动映射。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
menupilot --master 主数据表.xlsx --template POS模板.xlsx --output 填充结果.xlsx
|
|
7
|
+
menupilot expand -m 主数据表.xlsx -t 选项模板.xlsx -o 输出.xlsx
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
# Windows 终端默认 GBK,中文容易乱码。切换为 UTF-8 全局输出。
|
|
16
|
+
if sys.platform == "win32":
|
|
17
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
18
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
"""构建 SOP 匹配管线命令行参数解析器。"""
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="menupilot",
|
|
25
|
+
description="MenuPilot — 智能 POS 模板映射助手",
|
|
26
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
27
|
+
epilog="""
|
|
28
|
+
示例:
|
|
29
|
+
menupilot --master 主数据表.xlsx --template POS模板.xlsx --output 结果.xlsx
|
|
30
|
+
menupilot -m 主数据表.xlsx -t POS模板.xlsx -o 结果.xlsx --target-col 配料
|
|
31
|
+
menupilot -m 主数据表.xlsx -t POS模板.xlsx -o 结果.xlsx -r 报告.txt
|
|
32
|
+
|
|
33
|
+
--sheet 参数(位置语义):
|
|
34
|
+
-t template.xlsx --sheet 1 → 模板表读取第 2 个 Sheet
|
|
35
|
+
-m master.xlsx --sheet 2 → 主数据表读取第 3 个 Sheet
|
|
36
|
+
""",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument("-m", "--master", required=True, help="主数据表 Excel 文件路径")
|
|
39
|
+
parser.add_argument("-t", "--template", required=True, help="POS 模板 Excel 文件路径")
|
|
40
|
+
parser.add_argument("-o", "--output", required=True, help="输出 Excel 文件路径")
|
|
41
|
+
parser.add_argument("--target-col", default="配料", help="模板中需要填充 SOP 的目标列名")
|
|
42
|
+
parser.add_argument("-r", "--report", default=None, help="校验报告输出路径")
|
|
43
|
+
parser.add_argument("--langgraph", action="store_true", default=True, help="使用 LangGraph 编排管线")
|
|
44
|
+
parser.add_argument("--no-langgraph", action="store_false", dest="langgraph", help="禁用 LangGraph")
|
|
45
|
+
return parser
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_expand_parser() -> argparse.ArgumentParser:
|
|
49
|
+
"""构建 expand 子命令参数解析器。"""
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog="menupilot expand",
|
|
52
|
+
description="选项规格模板展开器 — 将主数据表的选项值展开为空白模板行",
|
|
53
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
54
|
+
epilog="示例:\n menupilot expand -m 选项主数据.xlsx -t 选项模板.xlsx -o 输出.xlsx",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument("-m", "--master", required=True, help="选项规格主数据表 Excel 路径")
|
|
57
|
+
parser.add_argument("-t", "--template", required=True, help="空白选项模板 Excel 路径")
|
|
58
|
+
parser.add_argument("-o", "--output", required=True, help="输出 Excel 文件路径")
|
|
59
|
+
parser.add_argument("--sheet", type=int, default=0, help="Sheet 序号(默认 0)")
|
|
60
|
+
parser.add_argument("--template-sheet", type=int, default=None, help="模板表 Sheet 序号")
|
|
61
|
+
parser.add_argument("--master-sheet", type=int, default=None, help="主数据表 Sheet 序号")
|
|
62
|
+
parser.add_argument("--header-row", type=int, default=2, help="模板表头行号(默认 2)")
|
|
63
|
+
return parser
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
67
|
+
# 入口(委托给 orchestration 层)
|
|
68
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
def run(args: Optional[list] = None) -> int:
|
|
71
|
+
"""执行 SOP 匹配管线(委托给 agent.orchestration.run_sop_pipeline)。"""
|
|
72
|
+
from menupilot.agent.orchestration import run_sop_pipeline
|
|
73
|
+
return run_sop_pipeline(args)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_expand(args: Optional[list] = None) -> int:
|
|
77
|
+
"""执行选项展开管线(委托给 agent.orchestration.run_expand_pipeline)。"""
|
|
78
|
+
from menupilot.agent.orchestration import run_expand_pipeline
|
|
79
|
+
return run_expand_pipeline(args)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
83
|
+
# API Key 检查
|
|
84
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
85
|
+
|
|
86
|
+
def _ensure_api_key():
|
|
87
|
+
"""确保已配置 DeepSeek API Key,否则启动配置向导。"""
|
|
88
|
+
from menupilot import config
|
|
89
|
+
if not config.DEEPSEEK_API_KEY:
|
|
90
|
+
from menupilot.wizard import run_wizard
|
|
91
|
+
run_wizard()
|
|
92
|
+
import importlib
|
|
93
|
+
importlib.reload(config)
|
|
94
|
+
if not config.DEEPSEEK_API_KEY:
|
|
95
|
+
print("\n❌ 未配置 API Key,无法启动。"
|
|
96
|
+
"请设置环境变量 DEEPSEEK_API_KEY 或运行 menupilot 重新配置。")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
101
|
+
# 入口调度
|
|
102
|
+
# ═══════════════════════════════════════════════════════════════════
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
"""MenuPilot CLI 主入口。"""
|
|
106
|
+
# expand 子命令路由(纯规则引擎,无需 API Key)
|
|
107
|
+
if len(sys.argv) > 1 and sys.argv[1] == "expand":
|
|
108
|
+
sys.exit(run_expand(sys.argv[2:]))
|
|
109
|
+
|
|
110
|
+
# 自测模式
|
|
111
|
+
if "--self-test" in sys.argv:
|
|
112
|
+
sys.argv.remove("--self-test")
|
|
113
|
+
import tempfile, shutil
|
|
114
|
+
import pandas as pd
|
|
115
|
+
|
|
116
|
+
# ── 备份真实 memory.json(防止自测清空长期记忆)──
|
|
117
|
+
_mem_path = os.path.expanduser("~/.menupilot/memory.json")
|
|
118
|
+
_mem_backup = None
|
|
119
|
+
if os.path.exists(_mem_path):
|
|
120
|
+
_mem_backup_path = _mem_path + ".self_test_backup"
|
|
121
|
+
shutil.copy(_mem_path, _mem_backup_path)
|
|
122
|
+
_mem_backup = _mem_backup_path
|
|
123
|
+
|
|
124
|
+
from menupilot.agent.orchestration import (
|
|
125
|
+
set_batch_mode, set_column_prompt_hook,
|
|
126
|
+
_batch_mode,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
os.environ["USE_MOCK_LLM"] = "1"
|
|
130
|
+
import importlib
|
|
131
|
+
from menupilot import config as _cfg
|
|
132
|
+
importlib.reload(_cfg)
|
|
133
|
+
|
|
134
|
+
passed = 0
|
|
135
|
+
failed = 0
|
|
136
|
+
|
|
137
|
+
def check(condition, msg):
|
|
138
|
+
nonlocal passed, failed
|
|
139
|
+
if condition:
|
|
140
|
+
passed += 1
|
|
141
|
+
print(f" PASS {msg}")
|
|
142
|
+
else:
|
|
143
|
+
failed += 1
|
|
144
|
+
print(f" FAIL {msg}")
|
|
145
|
+
|
|
146
|
+
print("=== main.py CLI 自测(Mock 模式)===\n")
|
|
147
|
+
|
|
148
|
+
tmpdir = tempfile.mkdtemp()
|
|
149
|
+
master_path = os.path.join(tmpdir, "master.xlsx")
|
|
150
|
+
template_path = os.path.join(tmpdir, "template.xlsx")
|
|
151
|
+
output_path = os.path.join(tmpdir, "output.xlsx")
|
|
152
|
+
|
|
153
|
+
pd.DataFrame({
|
|
154
|
+
"品名": ["浅浅清茶", "珍珠奶茶"],
|
|
155
|
+
"杯型": ["中杯", "中杯"],
|
|
156
|
+
"奶底": ["牛奶", "椰乳"],
|
|
157
|
+
"做法": ["少冰", "热"],
|
|
158
|
+
"糖": ["七分糖", "无糖"],
|
|
159
|
+
"SOP": ["T240", "T180"],
|
|
160
|
+
}).to_excel(master_path, index=False)
|
|
161
|
+
|
|
162
|
+
pd.DataFrame({
|
|
163
|
+
"菜品名称": ["浅浅清茶", "珍珠奶茶"],
|
|
164
|
+
"规格": ["中杯", "中杯"],
|
|
165
|
+
"口味做法组合": ["牛奶, 少冰, 七分糖", "椰乳, 热, 无糖"],
|
|
166
|
+
"配料": ["", ""],
|
|
167
|
+
}).to_excel(template_path, index=False)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# ── 1. 基本 CLI 运行 ──
|
|
171
|
+
print("1. 基本 CLI 运行(--master --template --output)")
|
|
172
|
+
exit_code = run([
|
|
173
|
+
"--master", master_path,
|
|
174
|
+
"--template", template_path,
|
|
175
|
+
"--output", output_path,
|
|
176
|
+
])
|
|
177
|
+
check(exit_code == 0, f"exit_code=0(实际 {exit_code})")
|
|
178
|
+
check(os.path.exists(output_path), "输出文件已生成")
|
|
179
|
+
report_path = output_path.replace(".xlsx", "_report.txt")
|
|
180
|
+
check(os.path.exists(report_path), "报告文件已生成")
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
# ── 2. 自定义参数 ──
|
|
184
|
+
print("2. 自定义 --target-col 和 --report")
|
|
185
|
+
custom_report = os.path.join(tmpdir, "custom_report.txt")
|
|
186
|
+
exit_code2 = run([
|
|
187
|
+
"-m", master_path, "-t", template_path, "-o", output_path,
|
|
188
|
+
"--target-col", "配料", "-r", custom_report,
|
|
189
|
+
])
|
|
190
|
+
check(exit_code2 == 0, f"exit_code=0(实际 {exit_code2})")
|
|
191
|
+
check(os.path.exists(custom_report), "自定义报告路径生效")
|
|
192
|
+
print()
|
|
193
|
+
|
|
194
|
+
# ── 3. 缺失文件错误 ──
|
|
195
|
+
print("3. 缺失文件错误处理")
|
|
196
|
+
exit_code3 = run(["-m", "不存在的文件.xlsx", "-t", template_path, "-o", output_path])
|
|
197
|
+
check(exit_code3 == 1, f"exit_code=1(实际 {exit_code3})")
|
|
198
|
+
print()
|
|
199
|
+
|
|
200
|
+
# ── 4. --help ──
|
|
201
|
+
print("4. --help 参数解析")
|
|
202
|
+
try:
|
|
203
|
+
help_text = build_parser().format_help()
|
|
204
|
+
check("--master" in help_text, "--master 出现在 help 中")
|
|
205
|
+
check("--target-col" in help_text, "--target-col 出现在 help 中")
|
|
206
|
+
except SystemExit:
|
|
207
|
+
check(False, "--help 不应触发 SystemExit")
|
|
208
|
+
print()
|
|
209
|
+
|
|
210
|
+
# ── 5. 简写参数 ──
|
|
211
|
+
print("5. 简写参数 -m -t -o")
|
|
212
|
+
exit_code5 = run(["-m", master_path, "-t", template_path, "-o", output_path])
|
|
213
|
+
check(exit_code5 == 0, "简写参数正常执行")
|
|
214
|
+
print()
|
|
215
|
+
|
|
216
|
+
# ── 6. --sheet 位置语义 ──
|
|
217
|
+
print("6. --sheet 位置语义")
|
|
218
|
+
master_multi = os.path.join(tmpdir, "master_multi.xlsx")
|
|
219
|
+
with pd.ExcelWriter(master_multi, engine="openpyxl") as writer:
|
|
220
|
+
pd.DataFrame({
|
|
221
|
+
"品名": ["浅浅清茶"], "杯型": ["中杯"], "奶底": ["牛奶"],
|
|
222
|
+
"做法": ["少冰"], "糖": ["七分糖"], "SOP": ["SHEET0_WRONG"],
|
|
223
|
+
}).to_excel(writer, sheet_name="Sheet0", index=False)
|
|
224
|
+
pd.DataFrame({
|
|
225
|
+
"品名": ["浅浅清茶"], "杯型": ["中杯"], "奶底": ["牛奶"],
|
|
226
|
+
"做法": ["少冰"], "糖": ["七分糖"], "SOP": ["T240_CORRECT"],
|
|
227
|
+
}).to_excel(writer, sheet_name="Sheet1", index=False)
|
|
228
|
+
|
|
229
|
+
template_multi = os.path.join(tmpdir, "template_multi.xlsx")
|
|
230
|
+
with pd.ExcelWriter(template_multi, engine="openpyxl") as writer:
|
|
231
|
+
pd.DataFrame({
|
|
232
|
+
"菜品名称": ["不相干商品"], "规格": ["大杯"],
|
|
233
|
+
"口味做法组合": ["红茶, 全糖, 正常冰"], "配料": [""],
|
|
234
|
+
}).to_excel(writer, sheet_name="Sheet0", index=False)
|
|
235
|
+
pd.DataFrame({
|
|
236
|
+
"菜品名称": ["浅浅清茶"], "规格": ["中杯"],
|
|
237
|
+
"口味做法组合": ["牛奶, 少冰, 七分糖"], "配料": [""],
|
|
238
|
+
}).to_excel(writer, sheet_name="Sheet1", index=False)
|
|
239
|
+
|
|
240
|
+
print(" 6a: --sheet 1 在 -t 后 → 模板 Sheet 1")
|
|
241
|
+
exit_code6a = run(["-m", master_multi, "-t", template_multi, "--sheet", "1", "-o", output_path])
|
|
242
|
+
check(exit_code6a == 0, f"exit_code=0(实际 {exit_code6a})")
|
|
243
|
+
df_6a = pd.read_excel(output_path)
|
|
244
|
+
check(df_6a.iloc[0]["配料"] == "SHEET0_WRONG",
|
|
245
|
+
f"主数据 Sheet 0,SOP=SHEET0_WRONG(实际 {df_6a.iloc[0]['配料']})")
|
|
246
|
+
print()
|
|
247
|
+
|
|
248
|
+
# ── 7. 交互式列分类(Mock hook)──
|
|
249
|
+
print("7. 交互式列分类(_interactive_classify_columns)")
|
|
250
|
+
from menupilot.data.memory import reset_memory as mem_reset, get_column_alias as mem_get_col
|
|
251
|
+
mem_reset()
|
|
252
|
+
|
|
253
|
+
df_interactive = pd.DataFrame({
|
|
254
|
+
"菜品名称": ["测试商品"], "原料类型": ["红茶"],
|
|
255
|
+
"规格": ["中杯"], "配料": [""], "备注": [""],
|
|
256
|
+
})
|
|
257
|
+
interactive_path = os.path.join(tmpdir, "interactive.xlsx")
|
|
258
|
+
df_interactive.to_excel(interactive_path, index=False)
|
|
259
|
+
interactive_out = os.path.join(tmpdir, "interactive_out.xlsx")
|
|
260
|
+
|
|
261
|
+
hook_calls = []
|
|
262
|
+
def mock_column_hook(col, sample):
|
|
263
|
+
hook_calls.append((col, sample))
|
|
264
|
+
mapping = {"原料类型": "tea_base", "备注": "ignore"}
|
|
265
|
+
return mapping.get(col, "ignore")
|
|
266
|
+
|
|
267
|
+
set_column_prompt_hook(mock_column_hook)
|
|
268
|
+
from menupilot import config as cfg_inner
|
|
269
|
+
orig_schema = dict(cfg_inner.MOCK_SCHEMA_RESPONSE)
|
|
270
|
+
cfg_inner.MOCK_SCHEMA_RESPONSE = {
|
|
271
|
+
"field_mapping": {"菜品名称": "product_name", "规格": "size"},
|
|
272
|
+
"composite_col": None, "target_col": "配料", "irrelevant_cols": [],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
exit_code8 = run(["-m", master_path, "-t", interactive_path, "-o", interactive_out])
|
|
276
|
+
check(exit_code8 == 0, f"交互分类后正常执行(实际 {exit_code8})")
|
|
277
|
+
called_cols = {c for c, _ in hook_calls}
|
|
278
|
+
check("原料类型" in called_cols, "「原料类型」触发交互")
|
|
279
|
+
check(mem_get_col("原料类型") == "tea_base", "别名已持久化")
|
|
280
|
+
cfg_inner.MOCK_SCHEMA_RESPONSE = orig_schema
|
|
281
|
+
set_column_prompt_hook(None)
|
|
282
|
+
mem_reset()
|
|
283
|
+
print()
|
|
284
|
+
|
|
285
|
+
finally:
|
|
286
|
+
for f in [master_path, template_path, output_path,
|
|
287
|
+
output_path.replace(".xlsx", "_report.txt"),
|
|
288
|
+
os.path.join(tmpdir, "custom_report.txt"),
|
|
289
|
+
os.path.join(tmpdir, "master_multi.xlsx"),
|
|
290
|
+
os.path.join(tmpdir, "template_multi.xlsx"),
|
|
291
|
+
os.path.join(tmpdir, "interactive.xlsx"),
|
|
292
|
+
os.path.join(tmpdir, "interactive_out.xlsx"),
|
|
293
|
+
os.path.join(tmpdir, "interactive_out_report.txt")]:
|
|
294
|
+
if os.path.exists(f):
|
|
295
|
+
os.remove(f)
|
|
296
|
+
os.rmdir(tmpdir)
|
|
297
|
+
|
|
298
|
+
from menupilot.agent.token_classifier import reset_cache
|
|
299
|
+
reset_cache()
|
|
300
|
+
|
|
301
|
+
# ── 还原真实 memory.json ──
|
|
302
|
+
if _mem_backup:
|
|
303
|
+
from menupilot.data.memory import reload as mem_reload
|
|
304
|
+
shutil.move(_mem_backup, _mem_path)
|
|
305
|
+
mem_reload()
|
|
306
|
+
|
|
307
|
+
print(f"=== 结果: {passed} passed, {failed} failed ===")
|
|
308
|
+
|
|
309
|
+
elif len(sys.argv) <= 1:
|
|
310
|
+
# 无参数:进入交互 REPL 模式
|
|
311
|
+
_ensure_api_key()
|
|
312
|
+
from menupilot.cli.repl import repl_loop
|
|
313
|
+
repl_loop()
|
|
314
|
+
else:
|
|
315
|
+
# --help / -h 无需 API Key,直接放行
|
|
316
|
+
if not (set(sys.argv) & {"--help", "-h"}):
|
|
317
|
+
_ensure_api_key()
|
|
318
|
+
sys.exit(run())
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
main()
|
menupilot/wizard.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
首次运行配置向导 — 引导用户设置 DeepSeek API Key。
|
|
3
|
+
|
|
4
|
+
仅在 ~/.menupilot/config.json 不存在或缺少 API Key 时触发。
|
|
5
|
+
配置写入 ~/.menupilot/config.json,后续运行自动读取。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
_CONFIG_DIR = os.path.expanduser("~/.menupilot")
|
|
13
|
+
_CONFIG_PATH = os.path.join(_CONFIG_DIR, "config.json")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_wizard() -> str:
|
|
17
|
+
"""启动中文配置向导,返回用户输入的 API Key。
|
|
18
|
+
|
|
19
|
+
如果 config.json 已存在,保留其中的其他配置项(BASE_URL、MODEL 等)。
|
|
20
|
+
"""
|
|
21
|
+
print()
|
|
22
|
+
print("=" * 60)
|
|
23
|
+
print(" \U0001f375 欢迎使用 MenuPilot — 智能 POS 模板映射助手")
|
|
24
|
+
print("=" * 60)
|
|
25
|
+
print()
|
|
26
|
+
print(" 首次使用需要配置 DeepSeek API Key。")
|
|
27
|
+
print(" 申请地址: https://platform.deepseek.com/api_keys")
|
|
28
|
+
print()
|
|
29
|
+
print(" ⚠️ 你的 API Key 只会保存在本地,不会上传到任何服务器。")
|
|
30
|
+
print()
|
|
31
|
+
|
|
32
|
+
# ── 交互式输入 ──
|
|
33
|
+
while True:
|
|
34
|
+
try:
|
|
35
|
+
api_key = input(" 请输入你的 DeepSeek API Key: ").strip()
|
|
36
|
+
except (EOFError, KeyboardInterrupt):
|
|
37
|
+
print()
|
|
38
|
+
print(" ❌ 已取消配置。可设置环境变量 DEEPSEEK_API_KEY 后重试。")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
if api_key:
|
|
42
|
+
break
|
|
43
|
+
print(" ⚠️ API Key 不能为空,请重新输入。")
|
|
44
|
+
print()
|
|
45
|
+
|
|
46
|
+
# ── 写入配置文件(保留已有配置) ──
|
|
47
|
+
os.makedirs(_CONFIG_DIR, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
existing = {}
|
|
50
|
+
if os.path.exists(_CONFIG_PATH):
|
|
51
|
+
try:
|
|
52
|
+
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
53
|
+
existing = json.load(f)
|
|
54
|
+
except (json.JSONDecodeError, IOError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
existing["DEEPSEEK_API_KEY"] = api_key
|
|
58
|
+
existing.setdefault("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
|
|
59
|
+
existing.setdefault("DEEPSEEK_MODEL", "deepseek-chat")
|
|
60
|
+
|
|
61
|
+
with open(_CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
62
|
+
json.dump(existing, f, indent=2, ensure_ascii=False)
|
|
63
|
+
|
|
64
|
+
print()
|
|
65
|
+
print(f" ✅ 配置已保存到: {_CONFIG_PATH}")
|
|
66
|
+
print(" 如需修改,可直接编辑该文件,或删除后重新运行 menupilot。")
|
|
67
|
+
print("=" * 60)
|
|
68
|
+
print()
|
|
69
|
+
|
|
70
|
+
return api_key
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_saved_api_key() -> str:
|
|
74
|
+
"""静默读取已保存的 API Key(不触发向导)。"""
|
|
75
|
+
if os.path.exists(_CONFIG_PATH):
|
|
76
|
+
try:
|
|
77
|
+
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
78
|
+
data = json.load(f)
|
|
79
|
+
return data.get("DEEPSEEK_API_KEY", "")
|
|
80
|
+
except (json.JSONDecodeError, IOError):
|
|
81
|
+
pass
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
run_wizard()
|