nexforge-cli 0.2.5__tar.gz → 0.2.7__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.
Files changed (102) hide show
  1. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/PKG-INFO +2 -2
  2. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/README.md +1 -1
  3. nexforge_cli-0.2.7/STATS_API.md +209 -0
  4. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//345/221/275/344/273/244/344/270/216/345/277/253/346/215/267/351/224/256.md +1 -1
  5. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//346/200/273/350/247/210.md +1 -1
  6. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/pyproject.toml +1 -1
  7. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/config.py +2 -0
  8. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/session.py +11 -1
  9. nexforge_cli-0.2.7/src/nexforge/stats/heartbeat.py +58 -0
  10. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/store/session_store.py +47 -16
  11. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/shell.py +6 -0
  12. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/screens/chat.py +41 -3
  13. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/messages.py +2 -1
  14. nexforge_cli-0.2.7/src/nexforge/ui/widgets/mode_warning.py +120 -0
  15. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/statusbar.py +6 -0
  16. nexforge_cli-0.2.7/src/nexforge/web/__init__.py +0 -0
  17. nexforge_cli-0.2.5/src/nexforge/ui/widgets/mode_warning.py +0 -57
  18. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/.gitignore +0 -0
  19. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/= +0 -0
  20. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/INSTALL.md +0 -0
  21. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/agent.svg +0 -0
  22. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/cost.svg +0 -0
  23. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/edit.svg +0 -0
  24. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/err.svg +0 -0
  25. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/file.svg +0 -0
  26. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/model.svg +0 -0
  27. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/ok.svg +0 -0
  28. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/read.svg +0 -0
  29. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/running.svg +0 -0
  30. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/search.svg +0 -0
  31. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/shell.svg +0 -0
  32. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/thinking.svg +0 -0
  33. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/web.svg +0 -0
  34. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/assets/icons/write.svg +0 -0
  35. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/config.example.toml +0 -0
  36. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/devlog/2026-07-07.md +0 -0
  37. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md" +0 -0
  38. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md" +0 -0
  39. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
  40. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
  41. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md" +0 -0
  42. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md" +0 -0
  43. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md" +0 -0
  44. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/scripts/publish.sh +0 -0
  45. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/scripts/smoke_provider.py +0 -0
  46. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/scripts/smoke_tui.py +0 -0
  47. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/__init__.py +0 -0
  48. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/__main__.py +0 -0
  49. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/app.py +0 -0
  50. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/cli.py +0 -0
  51. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/__init__.py +0 -0
  52. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/agent.py +0 -0
  53. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/jobs.py +0 -0
  54. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/permissions.py +0 -0
  55. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/router.py +0 -0
  56. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/core/types.py +0 -0
  57. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/icons.py +0 -0
  58. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/mcp/__init__.py +0 -0
  59. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/provider/__init__.py +0 -0
  60. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/provider/balance.py +0 -0
  61. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/provider/deepseek.py +0 -0
  62. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/skills/loader.py +0 -0
  63. {nexforge_cli-0.2.5/src/nexforge/web → nexforge_cli-0.2.7/src/nexforge/stats}/__init__.py +0 -0
  64. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/store/__init__.py +0 -0
  65. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/store/plan_store.py +0 -0
  66. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/theme.tcss +0 -0
  67. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/__init__.py +0 -0
  68. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/_paths.py +0 -0
  69. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/background.py +0 -0
  70. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/base.py +0 -0
  71. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/edit_file.py +0 -0
  72. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/fs_ops.py +0 -0
  73. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/git.py +0 -0
  74. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/list_dir.py +0 -0
  75. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/multi_edit.py +0 -0
  76. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/read_file.py +0 -0
  77. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/registry.py +0 -0
  78. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/search_content.py +0 -0
  79. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/search_files.py +0 -0
  80. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/web.py +0 -0
  81. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/tools/write_file.py +0 -0
  82. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/__init__.py +0 -0
  83. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/screens/__init__.py +0 -0
  84. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/__init__.py +0 -0
  85. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
  86. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/banner.py +0 -0
  87. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
  88. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/diff_modal.py +0 -0
  89. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
  90. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
  91. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
  92. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/sidebar.py +0 -0
  93. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/web/server.py +0 -0
  94. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/src/nexforge/web/static/index.html +0 -0
  95. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_approval.py +0 -0
  96. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_config.py +0 -0
  97. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_icons.py +0 -0
  98. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_m2_tools.py +0 -0
  99. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_sidebar.py +0 -0
  100. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_sidebar_actions.py +0 -0
  101. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_tools.py +0 -0
  102. {nexforge_cli-0.2.5 → nexforge_cli-0.2.7}/tests/test_tui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexforge-cli
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)
5
5
  Project-URL: Homepage, https://gitee.com/Lighthorn/nexforgecli
6
6
  Author: NexForge
@@ -48,7 +48,7 @@ Description-Content-Type: text/markdown
48
48
 
49
49
  NexForgeCLI 是一个运行在终端里的智能体助手,专为 **DeepSeek V4** 打造。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
50
50
 
51
- > 当前版本:**v0.2.0** · PyPI: `pip install nexforge-cli`
51
+ > 当前版本:**v0.2.5** · PyPI: `pip install nexforge-cli`
52
52
 
53
53
  ---
54
54
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  NexForgeCLI 是一个运行在终端里的智能体助手,专为 **DeepSeek V4** 打造。既能作为全屏 TUI 交互式对话/编码,也能作为 one-shot 命令行工具嵌进脚本和管道。
10
10
 
11
- > 当前版本:**v0.2.0** · PyPI: `pip install nexforge-cli`
11
+ > 当前版本:**v0.2.5** · PyPI: `pip install nexforge-cli`
12
12
 
13
13
  ---
14
14
 
@@ -0,0 +1,209 @@
1
+ # NexTech 软件统计 API 文档
2
+
3
+ > 版本: 1.0
4
+ > 基础路径: `https://nextech.club/api/stats.php`
5
+
6
+ ---
7
+
8
+ ## 概述
9
+
10
+ 轻量级软件在线统计服务,用于采集和查询软件客户端的下载量、在线用户数等指标。采用 **SQLite** 存储 + **POST 心跳** 机制,支持万级并发。
11
+
12
+ ### 设计原则
13
+
14
+ - 最小资源消耗:单条 SQLite `INSERT OR REPLACE` 写入,无 MySQL 开销
15
+ - 自动过期清理:120 秒无心跳自动标记离线
16
+ - 批量清理:每 10 次心跳触发一次过期记录删除
17
+
18
+ ---
19
+
20
+ ## 接口列表
21
+
22
+ ### 1. 软件心跳上报
23
+
24
+ ```
25
+ POST /api/stats.php?action=heartbeat
26
+ Content-Type: application/json
27
+ ```
28
+
29
+ 上报软件客户端的在线状态,建议每 **60 秒** 调用一次。
30
+
31
+ #### 请求参数
32
+
33
+ | 字段 | 类型 | 必填 | 说明 |
34
+ |------|------|------|------|
35
+ | `software` | string | 是 | 软件名称(如 `NexAgent`、`NexClient`) |
36
+ | `client_id` | string | 是 | 客户端唯一标识(每台设备的 ID) |
37
+
38
+ #### 请求示例
39
+
40
+ ```bash
41
+ curl -X POST https://nextech.club/api/stats.php?action=heartbeat \
42
+ -H "Content-Type: application/json" \
43
+ -d '{"software":"NexAgent","client_id":"device-001"}'
44
+ ```
45
+
46
+ #### 响应示例
47
+
48
+ ```json
49
+ {
50
+ "ok": true,
51
+ "online_total": 42,
52
+ "cleaned": 3
53
+ }
54
+ ```
55
+
56
+ | 字段 | 类型 | 说明 |
57
+ |------|------|------|
58
+ | `ok` | bool | 是否成功 |
59
+ | `online_total` | int | 当前总在线数 |
60
+ | `cleaned` | int | 本次清理的过期记录数 |
61
+
62
+ #### 错误响应
63
+
64
+ ```json
65
+ {
66
+ "ok": false,
67
+ "error": "software 和 client_id 不能为空"
68
+ }
69
+ ```
70
+
71
+ HTTP 状态码: `400`
72
+
73
+ ---
74
+
75
+ ### 2. 获取统计
76
+
77
+ ```
78
+ GET /api/stats.php
79
+ ```
80
+
81
+ 返回当前在线用户数(按软件分组)和今日软件下载量。
82
+
83
+ #### 请求示例
84
+
85
+ ```bash
86
+ curl https://nextech.club/api/stats.php
87
+ ```
88
+
89
+ #### 响应示例
90
+
91
+ ```json
92
+ {
93
+ "ok": true,
94
+ "time": "2026-07-21 06:48:00",
95
+ "online": {
96
+ "total": 42,
97
+ "by_software": {
98
+ "NexAgent": 30,
99
+ "NexClient": 12
100
+ }
101
+ },
102
+ "downloads": [
103
+ {
104
+ "name": "nexhttp-1.0.0.zip",
105
+ "count": 4,
106
+ "size_mb": 23.37
107
+ }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ | 字段 | 类型 | 说明 |
113
+ |------|------|------|
114
+ | `online.total` | int | 总在线数(120 秒内有心跳) |
115
+ | `online.by_software` | object | 按软件名称分组的在线数 |
116
+ | `downloads[]` | array | 今日下载统计列表 |
117
+ | `downloads[].name` | string | 下载文件名 |
118
+ | `downloads[].count` | int | 今日下载次数 |
119
+ | `downloads[].size_mb` | float | 文件大小(MB) |
120
+
121
+ ---
122
+
123
+ ## 客户端接入示例
124
+
125
+ ### Python
126
+
127
+ ```python
128
+ import requests
129
+ import time
130
+ import uuid
131
+
132
+ CLIENT_ID = str(uuid.uuid4()) # 每台设备固定一个 ID
133
+
134
+ def heartbeat():
135
+ while True:
136
+ try:
137
+ r = requests.post(
138
+ "https://nextech.club/api/stats.php?action=heartbeat",
139
+ json={"software": "NexAgent", "client_id": CLIENT_ID},
140
+ timeout=5
141
+ )
142
+ print(f"心跳成功: {r.json()['online_total']} 在线")
143
+ except Exception as e:
144
+ print(f"心跳失败: {e}")
145
+ time.sleep(60) # 每 60 秒上报一次
146
+ ```
147
+
148
+ ### JavaScript / Node.js
149
+
150
+ ```javascript
151
+ const CLIENT_ID = 'device-' + Math.random().toString(36).substr(2, 9);
152
+
153
+ setInterval(async () => {
154
+ try {
155
+ const res = await fetch('https://nextech.club/api/stats.php?action=heartbeat', {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({ software: 'NexClient', client_id: CLIENT_ID })
159
+ });
160
+ const data = await res.json();
161
+ console.log(`心跳成功: ${data.online_total} 在线`);
162
+ } catch (e) {
163
+ console.error('心跳失败:', e);
164
+ }
165
+ }, 60000);
166
+ ```
167
+
168
+ ### C / C++(libcurl)
169
+
170
+ ```c
171
+ #include <curl/curl.h>
172
+
173
+ void heartbeat() {
174
+ CURL *curl = curl_easy_init();
175
+ if (!curl) return;
176
+
177
+ char json[256];
178
+ snprintf(json, sizeof(json),
179
+ "{\"software\":\"NexAgent\",\"client_id\":\"%s\"}", DEVICE_ID);
180
+
181
+ curl_easy_setopt(curl, CURLOPT_URL,
182
+ "https://nextech.club/api/stats.php?action=heartbeat");
183
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json);
184
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5);
185
+ curl_easy_perform(curl);
186
+ curl_easy_cleanup(curl);
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ## 性能参考
193
+
194
+ | 并发数 | 每次心跳 | SQLite 写入 | 服务器负载 |
195
+ |--------|----------|-------------|-----------|
196
+ | 100 | 60 秒 | 1.7 次/秒 | < 0.1% CPU |
197
+ | 1,000 | 60 秒 | 17 次/秒 | < 0.5% CPU |
198
+ | 10,000 | 60 秒 | 167 次/秒 | ~3% CPU |
199
+ | 50,000 | 60 秒 | 833 次/秒 | ~15% CPU |
200
+
201
+ ---
202
+
203
+ ## 技术细节
204
+
205
+ - **数据库**: SQLite3 `/tmp/software_online.db`
206
+ - **表结构**: `heartbeats(id, software, client_id, last_seen, ip)`
207
+ - **过期 TTL**: 120 秒
208
+ - **清理策略**: 每 10 次心跳批量 `DELETE WHERE last_seen < now - 120`
209
+ - **索引**: `idx_last_seen`、`idx_software`
@@ -12,7 +12,7 @@
12
12
  | `Ctrl+L` | 清屏 |
13
13
  | `Ctrl+S` | 打开/关闭会话侧边栏 |
14
14
  | `Ctrl+B` | 打开/关闭计划侧边栏 |
15
- | `Ctrl+Shift+C` | 复制最后一条 AI 回复到剪贴板 |
15
+ | `F4`/`Ctrl+C` | 复制最后一条 AI 回复 |
16
16
  | `PageUp/PageDown` | 上下翻页 |
17
17
  | `↑/↓` | 单行滚动 |
18
18
  | `End` | 滚回底部 |
@@ -34,7 +34,7 @@ API Key 获取:https://platform.deepseek.com/api_keys
34
34
 
35
35
  ## 版本
36
36
 
37
- 当前版本: **v0.2.0**
37
+ 当前版本: **v0.2.5**
38
38
 
39
39
  ## 项目地址
40
40
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nexforge-cli"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  description = "NexForgeCLI — 一个面向 DeepSeek V4 的终端智能体(TUI + CLI 双模式)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -83,6 +83,8 @@ class UIConfig(BaseModel):
83
83
  mode: Literal["review", "auto"] = "review"
84
84
  plan_mode: bool = False # /plan 切换
85
85
  json_mode: bool = False # /json 开关,强制 JSON 输出
86
+ goal_mode: bool = False # /goal 切换,目标驱动自动循环
87
+ ops_mode: bool = False # /ops 切换,运维模式
86
88
  accent: str = "#6D5EF5"
87
89
  accent2: str = "#22D3EE"
88
90
 
@@ -16,7 +16,17 @@ DEFAULT_SYSTEM = (
16
16
  "回答用简洁的中文;修改文件用 edit_file 工具;需要执行命令用 run_shell。\n"
17
17
  "完成任务后用一两句话总结你做了什么。\n"
18
18
  "不要用 emoji(📦🌤等),用纯文本或 Markdown 列表/表格表示信息。\n"
19
- "执行 Shell 命令前先用 run_shell 探测操作系统(Windows用cmd命令,Linux/macOS用bash命令)"
19
+ "执行 Shell 命令前先用 run_shell 探测操作系统(Windows用cmd命令,Linux/macOS用bash命令)。\n"
20
+ "\n"
21
+ "【UTF-8 编码安全】\n"
22
+ "在 Windows PowerShell 中,Set-Content/Out-File 默认输出 ANSI 编码会损坏中文。\n"
23
+ "必须遵守以下规则:\n"
24
+ "- 优先使用 Git Bash 执行命令: \"C:\\Program Files\\Git\\bin\\bash.exe\" -c \"命令\"\n"
25
+ "- 若必须用 PowerShell,写文件命令必须加 -Encoding UTF8\n"
26
+ " 例: ... | Set-Content path.txt -Encoding UTF8\n"
27
+ "- Python 写文件必须指定: open(path, 'w', encoding='utf-8')\n"
28
+ "- CMD 的 echo/type 无编码问题,可直接使用\n"
29
+ "- 避免在 PowerShell 中做文本替换后写回文件"
20
30
  )
21
31
 
22
32
 
@@ -0,0 +1,58 @@
1
+ """软件在线统计心跳:每60秒POST一次到云端API。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import threading
8
+ import time
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ import httpx
13
+
14
+ SOFTWARE_NAME = "NexForgeCLI"
15
+ STATS_URL = "https://nextech.club/api/stats.php?action=heartbeat"
16
+
17
+ _client_id: str | None = None
18
+
19
+
20
+ def _get_client_id() -> str:
21
+ global _client_id
22
+ if _client_id:
23
+ return _client_id
24
+ path = Path.home() / ".nexforge" / "client_id"
25
+ path.parent.mkdir(parents=True, exist_ok=True)
26
+ if path.exists():
27
+ _client_id = path.read_text().strip()
28
+ else:
29
+ _client_id = str(uuid.uuid4())
30
+ path.write_text(_client_id)
31
+ return _client_id
32
+
33
+
34
+ def start_heartbeat() -> None:
35
+ """启动后台心跳线程(不阻塞主流程)。"""
36
+ cid = _get_client_id()
37
+
38
+ def _loop() -> None:
39
+ while True:
40
+ try:
41
+ resp = httpx.post(
42
+ STATS_URL,
43
+ json={"software": SOFTWARE_NAME, "client_id": cid},
44
+ timeout=5,
45
+ )
46
+ if resp.status_code == 200:
47
+ data = resp.json()
48
+ if data.get("ok"):
49
+ total = data.get("online_total", "?")
50
+ # 静默成功,只在开发模式打印
51
+ if os.environ.get("NEXFORGE_DEV"):
52
+ print(f"[stats] 心跳成功: {total} 在线")
53
+ except Exception:
54
+ pass # 静默失败,不打扰用户
55
+ time.sleep(60)
56
+
57
+ t = threading.Thread(target=_loop, daemon=True)
58
+ t.start()
@@ -62,7 +62,7 @@ def _format_message(msg: dict) -> str:
62
62
  tool_calls = msg.get("tool_calls")
63
63
 
64
64
  if role == "system":
65
- return f"> ℹ {content}\n"
65
+ return "" # 系统消息不回显在会话中
66
66
  if role == "user":
67
67
  return f"### ❯ 用户\n\n{content}\n"
68
68
  if role == "assistant":
@@ -86,21 +86,52 @@ def _format_message(msg: dict) -> str:
86
86
 
87
87
 
88
88
  def save_session(session_id: str, name: str, messages: list[dict]) -> Path:
89
- """把消息列表写成 MD 文件,返回文件路径。"""
90
- target = _today_dir() / f"{session_id}.md"
91
- lines = [
92
- f"# {name}",
93
- f"",
94
- f"> 创建 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
95
- f"> 项目 `{_project_name()}`",
96
- f"",
97
- "---",
98
- "",
99
- ]
100
- for msg in messages:
101
- lines.append(_format_message(msg))
102
- target.write_text("\n".join(lines), encoding="utf-8")
103
- return target
89
+ """把消息列表写成 MD 文件(200行/文件,过滤系统消息),返回第一段路径。"""
90
+ # 过滤系统消息
91
+ visible = [m for m in messages if m.get("role") != "system"]
92
+ MAX_LINES = 200
93
+
94
+ def _write_segment(seq: int, msgs: list[dict]) -> Path:
95
+ suffix = f"-{seq}" if seq > 1 else ""
96
+ target = _today_dir() / f"{session_id}{suffix}.md"
97
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
98
+ hdr = [
99
+ f"# {name}",
100
+ f"",
101
+ f"> 创建 {ts}",
102
+ f"> 项目 `{_project_name()}`",
103
+ f"",
104
+ "---",
105
+ "",
106
+ ]
107
+ body: list[str] = []
108
+ for m in msgs:
109
+ body.append(_format_message(m))
110
+ # 按 MAX_LINES 切割:header算8行
111
+ header_lines = 8
112
+ all_lines = hdr + body
113
+ if len(all_lines) > MAX_LINES:
114
+ # 取前 MAX_LINES 行写入,剩余放到下一个 segment
115
+ chunk = all_lines[:MAX_LINES]
116
+ target.write_text("\n".join(chunk), encoding="utf-8")
117
+ else:
118
+ target.write_text("\n".join(all_lines), encoding="utf-8")
119
+ return target
120
+
121
+ # 估算每段能装多少条消息(保守估计:每条消息约4行)
122
+ per_segment = MAX_LINES // 4
123
+ first_path: Path | None = None
124
+ seq = 1
125
+ i = 0
126
+ while i < len(visible):
127
+ chunk = visible[i:i + per_segment]
128
+ path = _write_segment(seq, chunk)
129
+ if first_path is None:
130
+ first_path = path
131
+ seq += 1
132
+ i += per_segment
133
+
134
+ return first_path or (_today_dir() / f"{session_id}.md")
104
135
 
105
136
 
106
137
  def load_session(path: Path) -> list[dict] | None:
@@ -30,6 +30,12 @@ class ShellTool(BaseTool):
30
30
  return f"run_shell($ {cmd[:60]})"
31
31
 
32
32
  def run(self, command: str, timeout: int = 30) -> ToolResult:
33
+ # PowerShell UTF-8 防护:自动给写文件命令加 -Encoding UTF8
34
+ import sys
35
+ if sys.platform == "win32" and "powershell" in command.lower():
36
+ for cmd in ("Set-Content", "Out-File", "Export-Csv", "Export-Clixml"):
37
+ if cmd.lower() in command.lower() and "-Encoding" not in command:
38
+ command = command.replace(cmd, f"{cmd} -Encoding UTF8")
33
39
  low = command.strip().lower()
34
40
  if any(d in low for d in _DANGER):
35
41
  return ToolResult(ok=False, error="命令被安全策略拦截(高危操作)")
@@ -24,7 +24,7 @@ from nexforge.ui.widgets.banner import Banner
24
24
  from nexforge.ui.widgets.apikey_modal import ApiKeyModal
25
25
  from nexforge.ui.widgets.confirm_modal import ConfirmModal
26
26
  from nexforge.ui.widgets.diff_modal import DiffApprovalModal
27
- from nexforge.ui.widgets.mode_warning import ModeWarningModal
27
+ from nexforge.ui.widgets.mode_warning import ModeWarningModal, GoalModeModal, OpsModeModal
28
28
  from nexforge.ui.widgets.multiline_modal import MultilineInputModal
29
29
  from nexforge.ui.widgets.messages import (
30
30
  AssistantTurn,
@@ -61,6 +61,8 @@ HELP = """\
61
61
  /clear 清空当前会话
62
62
  /quit 退出
63
63
  /web start|stop Web 管理面板
64
+ /goal 开启目标模式(自动循环直到达成目标)
65
+ /ops 开启运维模式(记录工具、减少试错)
64
66
  快捷键:Esc 退出(确认) · Ctrl+L 清屏 · F1 帮助"""
65
67
 
66
68
 
@@ -153,8 +155,9 @@ class ChatScreen(Screen):
153
155
  self._auto_resume()
154
156
  # 自动查询余额
155
157
  self._auto_fetch_balance()
156
-
157
- # ---- 输入处理 ---------------------------------------------------------
158
+ # 在线统计心跳
159
+ from nexforge.stats.heartbeat import start_heartbeat
160
+ start_heartbeat()
158
161
 
159
162
  async def on_multiline_prompt_prompt_submitted(self, event: MultilinePrompt.PromptSubmitted) -> None:
160
163
  text = event.text.strip()
@@ -197,6 +200,28 @@ class ChatScreen(Screen):
197
200
  except ProviderError as exc:
198
201
  self._note(str(exc), error=True)
199
202
  return
203
+ elif cmd == "goal":
204
+ def _on_goal_confirm(result: bool) -> None:
205
+ if result:
206
+ self.config.ui.goal_mode = True
207
+ self._refresh_status()
208
+ self._note("🎯 目标模式已开启:AI 将自动循环执行直到目标达成。Esc 可停止。", scroll=True)
209
+ else:
210
+ self._note("已取消")
211
+ self.app.push_screen(GoalModeModal(), callback=_on_goal_confirm)
212
+ elif cmd == "ops":
213
+ def _on_ops_confirm(result: bool) -> None:
214
+ if result:
215
+ self.config.ui.ops_mode = True
216
+ self._refresh_status()
217
+ self.session.messages.insert(1, {"role": "system", "content": (
218
+ "[运维模式] 你在为系统/网站做运维。优先使用已成功的工具和命令;"
219
+ "对相似任务复用已验证的方案;记录每次操作的结果。"
220
+ )})
221
+ self._note("🔧 运维模式已开启:工具调用将记录日志,优先复用已验证方案。", scroll=True)
222
+ else:
223
+ self._note("已取消")
224
+ self.app.push_screen(OpsModeModal(), callback=_on_ops_confirm)
200
225
  elif cmd == "quit" or cmd == "exit":
201
226
  self.app.exit()
202
227
  elif cmd == "clear":
@@ -370,6 +395,9 @@ class ChatScreen(Screen):
370
395
  await self._mount(SystemNote("── 本轮结束 · F4 复制最后回复"))
371
396
  log = self._log
372
397
  self.set_timer(0.05, lambda: log.scroll_end(animate=False))
398
+ # 目标模式:自动发起下一轮
399
+ if self.config.ui.goal_mode and self.agent is not None:
400
+ self._run_agent("请继续执行目标,直到达成。如已达成请明确说明。")
373
401
  elif ev.kind is AgentEventKind.ERROR:
374
402
  await self._mount(SystemNote(ev.text, error=True))
375
403
  finally:
@@ -458,6 +486,8 @@ class ChatScreen(Screen):
458
486
  plan_mode=self.config.ui.plan_mode,
459
487
  perm=self.config.permissions.profile,
460
488
  mode=self.config.ui.mode,
489
+ goal_mode=self.config.ui.goal_mode,
490
+ ops_mode=self.config.ui.ops_mode,
461
491
  balance=getattr(self, "_balance", ""),
462
492
  working=self._agent_running,
463
493
  )
@@ -732,6 +762,10 @@ class ChatScreen(Screen):
732
762
  self._auto_save()
733
763
  self.session = Session()
734
764
  self.session.messages = msgs
765
+ # 如果加载的消息缺少 system prompt,补上
766
+ if not msgs or msgs[0].get("role") != "system":
767
+ from nexforge.core.session import DEFAULT_SYSTEM
768
+ msgs.insert(0, {"role": "system", "content": DEFAULT_SYSTEM})
735
769
  self._session_id = info.id
736
770
  self._session_name = info.name
737
771
  self._follow = True
@@ -810,6 +844,10 @@ class ChatScreen(Screen):
810
844
  self._session_name = latest.name
811
845
  return
812
846
  self.session.messages = msgs
847
+ # 如果加载的消息缺少 system prompt,补上
848
+ if not msgs or msgs[0].get("role") != "system":
849
+ from nexforge.core.session import DEFAULT_SYSTEM
850
+ msgs.insert(0, {"role": "system", "content": DEFAULT_SYSTEM})
813
851
  self._session_id = latest.id
814
852
  self._session_name = latest.name
815
853
  # 渲染历史
@@ -96,7 +96,8 @@ class AssistantTurn(Vertical):
96
96
  panel.display = True
97
97
  if self._body is None:
98
98
  self._body = self.query_one("#reasoning-body", Static)
99
- self._body.update(self._reasoning)
99
+ from rich.markup import escape as rich_escape
100
+ self._body.update(rich_escape(self._reasoning))
100
101
  # 思考面板的 Contents 有 overflow-y:auto,每次更新后滚到底
101
102
  try:
102
103
  contents = panel.query_one("Contents")
@@ -0,0 +1,120 @@
1
+ """警告模态:切换全自动模式前的确认弹窗。"""
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, Label
9
+
10
+
11
+ class ModeWarningModal(ModalScreen[bool]):
12
+ DEFAULT_CSS = """
13
+ ModeWarningModal {
14
+ align: center middle;
15
+ }
16
+ ModeWarningModal > Vertical {
17
+ width: 50;
18
+ background: #1a1a24;
19
+ border: round #E04040;
20
+ padding: 1 2;
21
+ }
22
+ #mw-title {
23
+ color: #E04040;
24
+ text-style: bold;
25
+ margin-bottom: 1;
26
+ }
27
+ #mw-body {
28
+ height: auto;
29
+ margin-bottom: 1;
30
+ }
31
+ #mw-buttons {
32
+ height: auto;
33
+ align: center bottom;
34
+ }
35
+ """
36
+
37
+ def compose(self) -> ComposeResult:
38
+ with Vertical():
39
+ yield Label("⚠ 全自动模式", id="mw-title")
40
+ yield Label(
41
+ "全自动模式下,所有写文件/执行命令/删除操作将 [bold #E04040]不经审批自动执行[/]。\n\n"
42
+ "这可能导致不可逆的文件修改或系统命令执行。\n\n"
43
+ "[dim]确认开启? (y/n)[/]",
44
+ id="mw-body",
45
+ )
46
+ with Vertical(id="mw-buttons"):
47
+ yield Button("确认开启 (y)", variant="error", id="btn-auto-yes")
48
+ yield Button("取消 (n)", variant="primary", id="btn-auto-no")
49
+
50
+ def on_button_pressed(self, event: Button.Pressed) -> None:
51
+ self.dismiss(event.button.id == "btn-auto-yes")
52
+
53
+ def on_key(self, event) -> None:
54
+ if event.key in ("y", "Y"):
55
+ self.dismiss(True)
56
+ elif event.key in ("n", "N", "escape"):
57
+ self.dismiss(False)
58
+
59
+
60
+ class GoalModeModal(ModalScreen[bool]):
61
+
62
+ DEFAULT_CSS = """
63
+ GoalModeModal { align: center middle; }
64
+ GoalModeModal > Vertical {
65
+ width: 60; background: #1a1a24; border: round #E63946;
66
+ padding: 1 2;
67
+ }
68
+ """
69
+
70
+ def compose(self) -> ComposeResult:
71
+ with Vertical():
72
+ yield Label(
73
+ "🎯 目标模式\n\n"
74
+ "该模式会让 AI 自主循环执行,直到判断目标达成或 token 耗尽。\n\n"
75
+ "[red]⚠ 风险提示:[/]\n"
76
+ "- AI 将自动连续执行多个工具调用,可能消耗大量 token\n"
77
+ "- 可能修改大量文件,请确保代码已提交 Git\n"
78
+ "- 中途可按 Esc 强制停止\n\n"
79
+ "确定要开启目标模式吗?",
80
+ )
81
+ with Vertical(id="goal-buttons"):
82
+ yield Button("确认开启(我已了解风险)", variant="error", id="btn-goal-yes")
83
+ yield Button("取消", id="btn-goal-no")
84
+
85
+ def on_button_pressed(self, event: Button.Pressed) -> None:
86
+ if event.button.id == "btn-goal-yes":
87
+ self.dismiss(True)
88
+ else:
89
+ self.dismiss(False)
90
+
91
+
92
+ class OpsModeModal(ModalScreen[bool]):
93
+
94
+ DEFAULT_CSS = """
95
+ OpsModeModal { align: center middle; }
96
+ OpsModeModal > Vertical {
97
+ width: 60; background: #1a1a24; border: round #22D3EE;
98
+ padding: 1 2;
99
+ }
100
+ """
101
+
102
+ def compose(self) -> ComposeResult:
103
+ with Vertical():
104
+ yield Label(
105
+ "🔧 运维模式\n\n"
106
+ "该模式为网站/系统运维优化:\n\n"
107
+ "- 记录所有工具调用和结果到运维日志\n"
108
+ "- 优先使用已验证的工具和命令,减少试错\n"
109
+ "- 自动记录常用操作模式,提升效率\n\n"
110
+ "开启后状态栏显示 🔧 运维模式",
111
+ )
112
+ with Vertical(id="ops-buttons"):
113
+ yield Button("开启运维模式", variant="primary", id="btn-ops-yes")
114
+ yield Button("取消", id="btn-ops-no")
115
+
116
+ def on_button_pressed(self, event: Button.Pressed) -> None:
117
+ if event.button.id == "btn-ops-yes":
118
+ self.dismiss(True)
119
+ else:
120
+ self.dismiss(False)
@@ -24,6 +24,8 @@ class StatusBar(Static):
24
24
  balance: str = "",
25
25
  working: bool = False,
26
26
  mode: str = "review",
27
+ goal_mode: bool = False,
28
+ ops_mode: bool = False,
27
29
  ) -> None:
28
30
  t = Text()
29
31
 
@@ -42,6 +44,10 @@ class StatusBar(Static):
42
44
  # 计划
43
45
  if plan_mode:
44
46
  t.append(" 📋 计划中", style="#22D3EE bold")
47
+ if goal_mode:
48
+ t.append(" 🎯 目标", style="#E63946 bold")
49
+ if ops_mode:
50
+ t.append(" 🔧 运维", style="#22D3EE bold")
45
51
 
46
52
  # 余额(人民币)
47
53
  if balance:
File without changes
@@ -1,57 +0,0 @@
1
- """警告模态:切换全自动模式前的确认弹窗。"""
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, Label
9
-
10
-
11
- class ModeWarningModal(ModalScreen[bool]):
12
- DEFAULT_CSS = """
13
- ModeWarningModal {
14
- align: center middle;
15
- }
16
- ModeWarningModal > Vertical {
17
- width: 50;
18
- background: #1a1a24;
19
- border: round #E04040;
20
- padding: 1 2;
21
- }
22
- #mw-title {
23
- color: #E04040;
24
- text-style: bold;
25
- margin-bottom: 1;
26
- }
27
- #mw-body {
28
- height: auto;
29
- margin-bottom: 1;
30
- }
31
- #mw-buttons {
32
- height: auto;
33
- align: center bottom;
34
- }
35
- """
36
-
37
- def compose(self) -> ComposeResult:
38
- with Vertical():
39
- yield Label("⚠ 全自动模式", id="mw-title")
40
- yield Label(
41
- "全自动模式下,所有写文件/执行命令/删除操作将 [bold #E04040]不经审批自动执行[/]。\n\n"
42
- "这可能导致不可逆的文件修改或系统命令执行。\n\n"
43
- "[dim]确认开启? (y/n)[/]",
44
- id="mw-body",
45
- )
46
- with Vertical(id="mw-buttons"):
47
- yield Button("确认开启 (y)", variant="error", id="btn-auto-yes")
48
- yield Button("取消 (n)", variant="primary", id="btn-auto-no")
49
-
50
- def on_button_pressed(self, event: Button.Pressed) -> None:
51
- self.dismiss(event.button.id == "btn-auto-yes")
52
-
53
- def on_key(self, event) -> None:
54
- if event.key in ("y", "Y"):
55
- self.dismiss(True)
56
- elif event.key in ("n", "N", "escape"):
57
- self.dismiss(False)
File without changes
File without changes
File without changes