nexforge-cli 0.2.6__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.
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/PKG-INFO +1 -1
- nexforge_cli-0.2.7/STATS_API.md +209 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/pyproject.toml +1 -1
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/config.py +2 -0
- nexforge_cli-0.2.7/src/nexforge/stats/heartbeat.py +58 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/store/session_store.py +47 -16
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/screens/chat.py +41 -3
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/messages.py +2 -1
- nexforge_cli-0.2.7/src/nexforge/ui/widgets/mode_warning.py +120 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/statusbar.py +6 -0
- nexforge_cli-0.2.7/src/nexforge/web/__init__.py +0 -0
- nexforge_cli-0.2.6/src/nexforge/ui/widgets/mode_warning.py +0 -57
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/.gitignore +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/= +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/INSTALL.md +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/README.md +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/agent.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/cost.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/edit.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/err.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/file.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/model.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/ok.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/read.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/running.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/search.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/shell.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/thinking.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/web.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/assets/icons/write.svg +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/config.example.toml +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/devlog/2026-07-07.md +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//344/274/232/350/257/235/347/256/241/347/220/206.md" +0 -0
- {nexforge_cli-0.2.6 → 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" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//345/256/211/350/243/205/344/270/216/351/205/215/347/275/256.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//345/267/245/345/205/267/345/217/202/350/200/203.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//346/200/273/350/247/210.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//351/253/230/347/272/247/345/212/237/350/203/275.md" +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/scripts/publish.sh +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/scripts/smoke_provider.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/scripts/smoke_tui.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/__main__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/app.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/cli.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/agent.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/jobs.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/permissions.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/router.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/session.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/core/types.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/icons.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/mcp/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/provider/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/provider/balance.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/provider/deepseek.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/skills/loader.py +0 -0
- {nexforge_cli-0.2.6/src/nexforge/web → nexforge_cli-0.2.7/src/nexforge/stats}/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/store/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/store/plan_store.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/theme.tcss +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/_paths.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/background.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/base.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/edit_file.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/fs_ops.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/git.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/list_dir.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/multi_edit.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/read_file.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/registry.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/search_content.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/search_files.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/shell.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/web.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/tools/write_file.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/screens/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/__init__.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/apikey_modal.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/banner.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/confirm_modal.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/diff_modal.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/multiline_modal.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/multiline_prompt.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/plan_sidebar.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/ui/widgets/sidebar.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/web/server.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/src/nexforge/web/static/index.html +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_approval.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_config.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_icons.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_m2_tools.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_sidebar.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_sidebar_actions.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_tools.py +0 -0
- {nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/tests/test_tui.py +0 -0
|
@@ -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`
|
|
@@ -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
|
|
|
@@ -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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
f">
|
|
96
|
-
f""
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
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
|
|
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.6 → nexforge_cli-0.2.7}/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.6 → nexforge_cli-0.2.7}/doc//345/257/271/346/257/224/345/210/206/346/236/220.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/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.6 → nexforge_cli-0.2.7}/doc//346/235/203/351/231/220/347/263/273/347/273/237.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/doc//350/256/241/345/210/222/346/250/241/345/274/217.md"
RENAMED
|
File without changes
|
{nexforge_cli-0.2.6 → nexforge_cli-0.2.7}/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
|