nexforge-cli 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/PKG-INFO +1 -1
- nexforge_cli-0.2.2/doc//345/257/271/346/257/224/345/210/206/346/236/220.md +76 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/pyproject.toml +1 -1
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/screens/chat.py +10 -7
- nexforge_cli-0.2.2/src/nexforge/ui/widgets/apikey_modal.py +84 -0
- nexforge_cli-0.2.1/src/nexforge/ui/widgets/apikey_modal.py +0 -65
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/.gitignore +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/= +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/INSTALL.md +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/README.md +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/config.example.toml +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//345/221/275/344/273/244/344/270/216/345/277/253/346/215/267/351/224/256.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//346/200/273/350/247/210.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md" +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/config.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/store/session_store.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/messages.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/mode_warning.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/ui/widgets/statusbar.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/web/__init__.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/tests/test_tui.py +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# NexForgeCLI vs 其他 AI 编码智能体横向对比
|
|
2
|
+
|
|
3
|
+
## 总览
|
|
4
|
+
|
|
5
|
+
| | **NexForgeCLI** | **Claude Code** | **Codex CLI** | **Cursor** | **Windsurf** | **Aider** |
|
|
6
|
+
|---|---|---|---|---|---|---|
|
|
7
|
+
| **界面** | TUI(Textual)+CLI | 终端 TUI | 终端 CLI(Node) | VS Code 分支 | VS Code 扩展 | 终端 TUI+CLI |
|
|
8
|
+
| **模型** | DeepSeek V4 | Claude 3.5/4 | GPT-4o/o-series | 多模型 | 多模型 | 多模型 |
|
|
9
|
+
| **安装** | `pip install` | `npm install -g` | `npx` | 安装包(~400MB) | VS Code 市场 | `pip install` |
|
|
10
|
+
| **开源** | ✅ MIT | ❌ | ✅ Apache | ❌ | ❌ | ✅ Apache |
|
|
11
|
+
| **跨平台** | Win/Mac/Linux | Mac/Linux(WSL) | Mac/Linux(?) | 全平台 | 全平台 | 全平台 |
|
|
12
|
+
| **大小** | ~95KB | ~50MB npm | ~? | ~400MB | ~500MB | ~3MB |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 工具能力
|
|
17
|
+
|
|
18
|
+
| | **NexForgeCLI** | **Claude Code** | **Codex CLI** | **Cursor** | **Windsurf** | **Aider** |
|
|
19
|
+
|---|---|---|---|---|---|---|
|
|
20
|
+
| **文件读取** | ✅ 行范围 | ✅ | ✅ | ✅ IDE原生 | ✅ IDE原生 | ✅ |
|
|
21
|
+
| **文件编辑** | ✅ SEARCH/REPLACE | ✅ SEARCH/REPLACE | ✅ | ✅ IDE直接 | ✅ IDE直接 | ✅ SEARCH/REPLACE |
|
|
22
|
+
| **批量编辑** | ✅ multi_edit | — | — | ✅ 多光标 | ✅ 多光标 | — |
|
|
23
|
+
| **Shell** | ✅ 超时+危险拦截 | ✅ | ✅ | ✅ 集成终端 | ✅ 集成终端 | ✅ |
|
|
24
|
+
| **后台任务** | ✅ run_background | — | — | ✅ 终端多标签 | ✅ 终端多标签 | — |
|
|
25
|
+
| **Web 抓取** | ✅ web_fetch | — | — | — | — | ❌(靠 curl) |
|
|
26
|
+
| **文件搜索** | ✅ grep+文件名 | ✅ | ✅ | ✅ VS搜索 | ✅ VS搜索 | ❌(靠 shell) |
|
|
27
|
+
| **Git** | ✅ 三状态+可选commit | ✅ 完整 | ✅ 完整 | ✅ 内建VCS | ✅ 内建VCS | ✅ 完整 |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 权限与安全
|
|
32
|
+
|
|
33
|
+
| | **NexForgeCLI** | **Claude Code** | **Codex CLI** | **Cursor** | **Aider** |
|
|
34
|
+
|---|---|---|---|---|---|
|
|
35
|
+
| **权限分级** | 🛡safe/⚡supervised/🔥auto | 逐命令审批(Y/n) | 工具级审批 | 审批确认 | `--yes`全跳过 |
|
|
36
|
+
| **危险命令拦截** | ✅ 内置黑名单(rm -rf /等) | ⚠ Shell警告 | — | ✅ IDE沙箱 | ❌ |
|
|
37
|
+
| **死循环检测** | ✅ 连续3次同工具失败→注入提示 | — | — | — | — |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 会话与上下文
|
|
42
|
+
|
|
43
|
+
| | **NexForgeCLI** | **Claude Code** | **Codex CLI** | **Aider** |
|
|
44
|
+
|---|---|---|---|---|
|
|
45
|
+
| **会话保存** | ✅ 自动保存MD文件 | ✅ .claude/sessions/ | ✅ .codex/sessions/ | ✅ .md文件 |
|
|
46
|
+
| **会话管理** | ✅ 侧边栏:新建/切/删/改名 | ✅ ls/resume命令 | — | `--restore` |
|
|
47
|
+
| **会话恢复** | ✅ 启动自动恢复最近 | ✅ 可恢复 | ✅ 可恢复 | ✅ 可恢复 |
|
|
48
|
+
| **上下文窗口** | **1M tokens**(DeepSeek V4) | 200K(Claude) | 128K(GPT-4o) | 模型决定 |
|
|
49
|
+
| **计划模式** | ✅ /plan只读讨论 | — | — | — |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 模型路由与优化
|
|
54
|
+
|
|
55
|
+
| | **NexForgeCLI** | **Claude Code** | **Codex CLI** | **Cursor** | **Aider** |
|
|
56
|
+
|---|---|---|---|---|---|
|
|
57
|
+
| **自动路由** | ✅ Flash→Pro(关键词+上下文+失败次数) | ❌ | ❌ | ❌ | ❌ |
|
|
58
|
+
| **思考模式** | ✅ thinking流式+可折叠面板 | ✅ 扩展思考 | ✅ o-series推理 | ❌ | ❌ |
|
|
59
|
+
| **KV Cache计费** | ✅ 缓存命中/未命中拆分 | — | — | — | — |
|
|
60
|
+
| **采样参数** | ✅ thinking=off时可用 | — | — | — | — |
|
|
61
|
+
| **JSON模式** | ✅ /json切换 | — | — | — | — |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 独有亮点
|
|
66
|
+
|
|
67
|
+
1. **后台任务管理** —— 启动/监控/杀后台进程(`run_background`等5个工具)
|
|
68
|
+
2. **自动路由** —— 客户端智能判断,小任务用 Flash 省钱,大任务自动升 Pro
|
|
69
|
+
3. **死循环保护** —— 同一工具连续失败3次自动停止+提示
|
|
70
|
+
4. **三级权限** —— safe/supervised/auto 允许用户按场景自由控制
|
|
71
|
+
5. **DeepSeek KV Cache 精确计费** —— 区分缓存命中/未命中,费用更准确
|
|
72
|
+
6. **自建 Web 面板** —— 零外部依赖,浏览器管理会话/计划/配置
|
|
73
|
+
7. **计划模式** —— `/plan` 只读讨论,先规划再执行
|
|
74
|
+
8. **Markdown 会话** —— 人类可读的会话文件,可直接编辑
|
|
75
|
+
9. **跨 Agent 协作** —— `/import` 读取其他智能体的项目数据
|
|
76
|
+
10. **轻量级** —— pip 包仅 95KB,无 Node.js 依赖
|
|
@@ -79,6 +79,7 @@ class ChatScreen(Screen):
|
|
|
79
79
|
("f2", "multiline_input", "多行"),
|
|
80
80
|
("f4", "copy_last", "复制"),
|
|
81
81
|
("f3", "external_edit", "编辑器"),
|
|
82
|
+
("ctrl+c", "copy_last", "复制"),
|
|
82
83
|
]
|
|
83
84
|
|
|
84
85
|
def __init__(
|
|
@@ -128,16 +129,18 @@ class ChatScreen(Screen):
|
|
|
128
129
|
# 加载技能
|
|
129
130
|
self._skills = discover_skills(Path.cwd())
|
|
130
131
|
if not self.config.resolve_api_key():
|
|
131
|
-
# 弹出 API Key 输入面板
|
|
132
|
-
|
|
133
|
-
key = await self.app.push_screen_wait(ApiKeyModal())
|
|
132
|
+
# 弹出 API Key 输入面板(延迟到屏幕完挂载后)
|
|
133
|
+
def _on_key(key: str | None) -> None:
|
|
134
134
|
if key:
|
|
135
135
|
from nexforge.config import save_api_key
|
|
136
136
|
save_api_key(key)
|
|
137
137
|
self.config.provider.api_key = key
|
|
138
138
|
self._init_provider(key)
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
self._refresh_status()
|
|
140
|
+
self._note("✅ API Key 已保存并验证通过!", scroll=True)
|
|
141
|
+
else:
|
|
142
|
+
self._note("⚠ 跳过 API Key 配置,稍后可用 /key 设置。", scroll=True)
|
|
143
|
+
self.set_timer(0.1, lambda: self.app.push_screen(ApiKeyModal(), callback=_on_key))
|
|
141
144
|
else:
|
|
142
145
|
self._init_provider()
|
|
143
146
|
self._refresh_status()
|
|
@@ -540,8 +543,8 @@ class ChatScreen(Screen):
|
|
|
540
543
|
self.agent = AgentLoop(
|
|
541
544
|
self.config, self.provider, self.session, approve=self._ask_approve
|
|
542
545
|
)
|
|
543
|
-
except
|
|
544
|
-
self._note(
|
|
546
|
+
except Exception as exc:
|
|
547
|
+
self._note(f"初始化失败:{exc}", error=True)
|
|
545
548
|
|
|
546
549
|
def _auto_fetch_balance(self) -> None:
|
|
547
550
|
"""启动时自动查询余额(不阻塞)。"""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""API Key 输入面板 —— 首次使用或 /key 无参时弹出。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import Button, Input, Label, Static
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiKeyModal(ModalScreen[str | None]):
|
|
12
|
+
DEFAULT_CSS = """
|
|
13
|
+
ApiKeyModal { align: center middle; }
|
|
14
|
+
ApiKeyModal > Vertical {
|
|
15
|
+
width: 55; background: #1a1a24;
|
|
16
|
+
border: round #6D5EF5; padding: 1 2;
|
|
17
|
+
}
|
|
18
|
+
#apikey-title { color: #22D3EE; text-style: bold; margin-bottom: 1; }
|
|
19
|
+
#apikey-body { height: auto; margin-bottom: 1; }
|
|
20
|
+
#apikey-input { margin-bottom: 1; }
|
|
21
|
+
#apikey-status { height: auto; margin-bottom: 1; }
|
|
22
|
+
#apikey-buttons { height: auto; align: center bottom; }
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self._verified = False
|
|
28
|
+
self._key = ""
|
|
29
|
+
|
|
30
|
+
def compose(self) -> ComposeResult:
|
|
31
|
+
with Vertical():
|
|
32
|
+
yield Label("配置 DeepSeek API Key", id="apikey-title")
|
|
33
|
+
yield Label(
|
|
34
|
+
"请输入你的 DeepSeek API Key(以 sk- 开头)。\n"
|
|
35
|
+
"Key 将安全保存在本地凭据文件中,不会上传。\n\n"
|
|
36
|
+
"[dim]获取地址: https://platform.deepseek.com/api_keys[/]",
|
|
37
|
+
id="apikey-body",
|
|
38
|
+
)
|
|
39
|
+
yield Input(placeholder="sk-…", id="apikey-input", password=True)
|
|
40
|
+
yield Static("", id="apikey-status")
|
|
41
|
+
with Vertical(id="apikey-buttons"):
|
|
42
|
+
yield Button("验证 Key", variant="primary", id="btn-apikey-verify")
|
|
43
|
+
yield Button("保存并开始", variant="success", id="btn-apikey-save", disabled=True)
|
|
44
|
+
yield Button("跳过(稍后配置)", id="btn-apikey-skip")
|
|
45
|
+
|
|
46
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
47
|
+
bid = event.button.id
|
|
48
|
+
if bid == "btn-apikey-verify":
|
|
49
|
+
key = self.query_one("#apikey-input", Input).value.strip()
|
|
50
|
+
status = self.query_one("#apikey-status", Static)
|
|
51
|
+
if not key.startswith("sk-"):
|
|
52
|
+
status.update("[red]❌ Key 必须以 sk- 开头。[/]")
|
|
53
|
+
return
|
|
54
|
+
status.update("[yellow]⏳ 正在验证 Key…[/]")
|
|
55
|
+
import asyncio
|
|
56
|
+
asyncio.create_task(self._do_verify(key, status))
|
|
57
|
+
elif bid == "btn-apikey-save":
|
|
58
|
+
if self._verified and self._key:
|
|
59
|
+
self.dismiss(self._key)
|
|
60
|
+
else:
|
|
61
|
+
self.query_one("#apikey-status", Static).update(
|
|
62
|
+
"[red]请先点击\"验证 Key\"通过验证。[/]"
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
self.dismiss(None)
|
|
66
|
+
|
|
67
|
+
async def _do_verify(self, key: str, status: Static) -> None:
|
|
68
|
+
try:
|
|
69
|
+
from openai import AsyncOpenAI
|
|
70
|
+
client = AsyncOpenAI(api_key=key, base_url="https://api.deepseek.com", timeout=10)
|
|
71
|
+
resp = await client.models.list()
|
|
72
|
+
await client.close()
|
|
73
|
+
ok = len(resp.data) > 0
|
|
74
|
+
except Exception:
|
|
75
|
+
ok = False
|
|
76
|
+
if ok:
|
|
77
|
+
self._verified = True
|
|
78
|
+
self._key = key
|
|
79
|
+
status.update("[green]✅ Key 验证通过!点击\"保存并开始\"即可。[/]")
|
|
80
|
+
self.query_one("#btn-apikey-save", Button).disabled = False
|
|
81
|
+
else:
|
|
82
|
+
self._verified = False
|
|
83
|
+
status.update("[red]❌ Key 验证失败!请检查 Key 和网络。[/]")
|
|
84
|
+
self.query_one("#btn-apikey-save", Button).disabled = True
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""API Key 输入面板 —— 首次使用或 /key 无参时弹出。"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from textual.app import ComposeResult
|
|
6
|
-
from textual.containers import Vertical
|
|
7
|
-
from textual.screen import ModalScreen
|
|
8
|
-
from textual.widgets import Button, Input, Label
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ApiKeyModal(ModalScreen[str | None]):
|
|
12
|
-
DEFAULT_CSS = """
|
|
13
|
-
ApiKeyModal {
|
|
14
|
-
align: center middle;
|
|
15
|
-
}
|
|
16
|
-
ApiKeyModal > Vertical {
|
|
17
|
-
width: 55;
|
|
18
|
-
background: #1a1a24;
|
|
19
|
-
border: round #6D5EF5;
|
|
20
|
-
padding: 1 2;
|
|
21
|
-
}
|
|
22
|
-
#apikey-title {
|
|
23
|
-
color: #22D3EE;
|
|
24
|
-
text-style: bold;
|
|
25
|
-
margin-bottom: 1;
|
|
26
|
-
}
|
|
27
|
-
#apikey-body {
|
|
28
|
-
height: auto;
|
|
29
|
-
margin-bottom: 1;
|
|
30
|
-
}
|
|
31
|
-
#apikey-input {
|
|
32
|
-
margin-bottom: 1;
|
|
33
|
-
}
|
|
34
|
-
#apikey-buttons {
|
|
35
|
-
height: auto;
|
|
36
|
-
align: center bottom;
|
|
37
|
-
}
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def compose(self) -> ComposeResult:
|
|
41
|
-
with Vertical():
|
|
42
|
-
yield Label("配置 DeepSeek API Key", id="apikey-title")
|
|
43
|
-
yield Label(
|
|
44
|
-
"请输入你的 DeepSeek API Key(以 sk- 开头)。\n"
|
|
45
|
-
"Key 将安全保存在本地凭据文件中,不会上传。\n\n"
|
|
46
|
-
"[dim]获取地址: https://platform.deepseek.com/api_keys[/]",
|
|
47
|
-
id="apikey-body",
|
|
48
|
-
)
|
|
49
|
-
yield Input(placeholder="sk-…", id="apikey-input", password=True)
|
|
50
|
-
with Vertical(id="apikey-buttons"):
|
|
51
|
-
yield Button("保存并开始", variant="primary", id="btn-apikey-save")
|
|
52
|
-
yield Button("跳过(稍后配置)", id="btn-apikey-skip")
|
|
53
|
-
|
|
54
|
-
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
55
|
-
if event.button.id == "btn-apikey-save":
|
|
56
|
-
key = self.query_one("#apikey-input", Input).value.strip()
|
|
57
|
-
if key.startswith("sk-"):
|
|
58
|
-
self.dismiss(key)
|
|
59
|
-
else:
|
|
60
|
-
self.query_one("#apikey-body", Label).update(
|
|
61
|
-
"[red]Key 必须以 sk- 开头,请检查后重试。[/]\n\n"
|
|
62
|
-
"[dim]获取地址: https://platform.deepseek.com/api_keys[/]"
|
|
63
|
-
)
|
|
64
|
-
else:
|
|
65
|
-
self.dismiss(None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md"
RENAMED
|
File without changes
|
|
File without changes
|
{nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.1 → nexforge_cli-0.2.2}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md"
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|