iflow-run 1.0.0 → 1.0.2
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.
- package/README.md +68 -12
- package/bin/iflow-run.js +190 -3
- package/package.json +1 -1
- package/server.js +58 -3
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
一个用于查看 iFlow CLI 会话轨迹和历史会话的 Web 应用程序。
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/iflow-run)
|
|
6
|
+
[](https://github.com/KeWen-Du/iflow-run)
|
|
7
|
+
|
|
5
8
|
## 功能特性
|
|
6
9
|
|
|
7
10
|
- 📁 **项目管理** - 浏览和查看 iFlow CLI 创建的所有项目
|
|
8
11
|
- 💬 **会话浏览** - 查看每个项目下的所有会话历史
|
|
9
12
|
- 🔍 **消息详情** - 查看完整的对话消息,包括用户消息、助手响应、工具调用和工具结果
|
|
10
13
|
- 👁️ **预览功能** - 快速预览会话的第一条消息内容
|
|
14
|
+
- 📊 **会话上下文** - 显示工作目录、Git 分支、版本信息等环境上下文
|
|
15
|
+
- 🔎 **消息筛选** - 支持按类型筛选消息(用户/助手/工具调用)和内容搜索
|
|
16
|
+
- 💰 **Token 统计** - 显示模型名称、Token 消耗、执行时间和预估成本
|
|
17
|
+
- 🔄 **环境追踪** - 检测并显示工作目录和 Git 分支的变更
|
|
18
|
+
- 📥 **导出功能** - 支持导出会话为 Markdown 或 JSON 格式
|
|
19
|
+
- 📋 **消息目录** - 快速导航到用户消息
|
|
11
20
|
- 🎨 **现代 UI** - 采用暗色主题和玻璃拟态设计,提供优雅的用户体验
|
|
12
21
|
- 📱 **响应式设计** - 支持桌面端和移动端访问
|
|
13
22
|
|
|
@@ -85,14 +94,16 @@ npx iflow-run
|
|
|
85
94
|
|
|
86
95
|
```
|
|
87
96
|
iflow-run/
|
|
88
|
-
├── server.js
|
|
89
|
-
├── package.json
|
|
90
|
-
├──
|
|
91
|
-
│
|
|
92
|
-
|
|
93
|
-
│ ├──
|
|
94
|
-
│
|
|
95
|
-
|
|
97
|
+
├── server.js # Express 服务器
|
|
98
|
+
├── package.json # 项目配置
|
|
99
|
+
├── bin/ # 全局可执行文件
|
|
100
|
+
│ └── iflow-run.js # CLI 入口文件
|
|
101
|
+
├── public/ # 前端静态文件
|
|
102
|
+
│ ├── index.html # 主页面
|
|
103
|
+
│ ├── app.js # 前端逻辑
|
|
104
|
+
│ ├── styles.css # 样式文件
|
|
105
|
+
│ └── test.html # 测试页面
|
|
106
|
+
└── test_screenshot.py # 自动化测试脚本
|
|
96
107
|
```
|
|
97
108
|
|
|
98
109
|
## API 接口
|
|
@@ -113,6 +124,41 @@ GET /api/sessions/:projectId/:sessionId
|
|
|
113
124
|
|
|
114
125
|
返回指定会话的完整消息记录。
|
|
115
126
|
|
|
127
|
+
### 搜索会话
|
|
128
|
+
|
|
129
|
+
```http
|
|
130
|
+
GET /api/search?q=关键词&page=1&limit=20&type=all
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
查询参数:
|
|
134
|
+
- `q` (string): 搜索关键词
|
|
135
|
+
- `page` (number): 页码,默认 1
|
|
136
|
+
- `limit` (number): 每页结果数,默认 20
|
|
137
|
+
- `type` (string): 消息类型筛选,可选值:`all`、`user`、`assistant`,默认 `all`
|
|
138
|
+
- `startDate` (number): 开始时间戳(可选)
|
|
139
|
+
- `endDate` (number): 结束时间戳(可选)
|
|
140
|
+
|
|
141
|
+
响应示例:
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"results": [
|
|
145
|
+
{
|
|
146
|
+
"projectId": "project-id",
|
|
147
|
+
"projectName": "项目名称",
|
|
148
|
+
"sessionId": "session-1234567890",
|
|
149
|
+
"content": "消息内容预览...",
|
|
150
|
+
"type": "user",
|
|
151
|
+
"timestamp": 1704110400000,
|
|
152
|
+
"uuid": "message-uuid"
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
"total": 100,
|
|
156
|
+
"page": 1,
|
|
157
|
+
"limit": 20,
|
|
158
|
+
"totalPages": 5
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
116
162
|
## 配置
|
|
117
163
|
|
|
118
164
|
### 通过命令行参数配置
|
|
@@ -164,11 +210,7 @@ python test_screenshot.py
|
|
|
164
210
|
3. 点击第一个会话并截图会话详情
|
|
165
211
|
4. 测试返回按钮功能
|
|
166
212
|
|
|
167
|
-
## 截图
|
|
168
213
|
|
|
169
|
-

|
|
170
|
-

|
|
171
|
-

|
|
172
214
|
|
|
173
215
|
## 开发
|
|
174
216
|
|
|
@@ -198,6 +240,7 @@ python test_screenshot.py
|
|
|
198
240
|
1. `.iflow` 目录路径是否正确
|
|
199
241
|
2. 目录下是否有 `projects` 子目录
|
|
200
242
|
3. 项目目录中是否有 `session-*.jsonl` 文件
|
|
243
|
+
4. 尝试使用 `--dir` 参数指定正确的 iflow 目录
|
|
201
244
|
|
|
202
245
|
### 消息显示为空?
|
|
203
246
|
|
|
@@ -205,6 +248,19 @@ python test_screenshot.py
|
|
|
205
248
|
- 会话文件格式不正确
|
|
206
249
|
- 消息内容不包含可显示的文本
|
|
207
250
|
- 消息格式不符合预期
|
|
251
|
+
- 使用了消息筛选功能,当前筛选条件下没有匹配的消息
|
|
252
|
+
|
|
253
|
+
### 工具结果显示不完整?
|
|
254
|
+
|
|
255
|
+
工具结果默认折叠显示,点击工具结果的标题栏可以展开查看完整内容。长结果会自动截断,可以通过复制按钮获取完整内容。
|
|
256
|
+
|
|
257
|
+
### Token 统计不准确?
|
|
258
|
+
|
|
259
|
+
Token 统计依赖于会话消息中的 `usage` 字段。如果模型未返回该信息,则无法显示 Token 统计。
|
|
260
|
+
|
|
261
|
+
### 环境变更提示没有显示?
|
|
262
|
+
|
|
263
|
+
环境变更提示需要消息中包含 `cwd` 或 `gitBranch` 字段。只有当这些字段发生变化时才会显示提示。
|
|
208
264
|
|
|
209
265
|
## 贡献
|
|
210
266
|
|
package/bin/iflow-run.js
CHANGED
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const { spawn, exec } = require('child_process');
|
|
5
7
|
|
|
6
8
|
// 解析命令行参数
|
|
7
9
|
const args = process.argv.slice(2);
|
|
8
10
|
|
|
11
|
+
// PID 文件路径
|
|
12
|
+
const homeDir = os.homedir();
|
|
13
|
+
const iflowRunDir = path.join(homeDir, '.iflow-run');
|
|
14
|
+
const pidFile = path.join(iflowRunDir, 'iflow-run.pid');
|
|
15
|
+
|
|
16
|
+
// 确保 .iflow-run 目录存在
|
|
17
|
+
if (!fs.existsSync(iflowRunDir)) {
|
|
18
|
+
fs.mkdirSync(iflowRunDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
// 显示帮助信息
|
|
10
22
|
function showHelp() {
|
|
11
23
|
console.log(`
|
|
@@ -17,6 +29,8 @@ iflow-run - iFlow CLI 会话轨迹查看器
|
|
|
17
29
|
选项:
|
|
18
30
|
--port=<端口> 指定服务器端口 (默认: 3000)
|
|
19
31
|
--dir=<目录> 指定 iflow 数据目录 (默认: ~/.iflow)
|
|
32
|
+
-d, --daemon 后台运行
|
|
33
|
+
--stop 停止后台运行的服务
|
|
20
34
|
-h, --help 显示帮助信息
|
|
21
35
|
-v, --version 显示版本号
|
|
22
36
|
|
|
@@ -25,7 +39,9 @@ iflow-run - iFlow CLI 会话轨迹查看器
|
|
|
25
39
|
IFLOW_RUN_DIR 指定 iflow 数据目录
|
|
26
40
|
|
|
27
41
|
示例:
|
|
28
|
-
iflow-run #
|
|
42
|
+
iflow-run # 使用默认配置启动(前台)
|
|
43
|
+
iflow-run --daemon # 后台运行
|
|
44
|
+
iflow-run --stop # 停止后台服务
|
|
29
45
|
iflow-run --port=8080 # 指定端口 8080
|
|
30
46
|
iflow-run --dir=/path/to/iflow # 指定 iflow 目录
|
|
31
47
|
IFLOW_RUN_PORT=8080 iflow-run # 使用环境变量指定端口
|
|
@@ -42,7 +58,157 @@ function showVersion() {
|
|
|
42
58
|
console.log(`iflow-run v${pkg.version}`);
|
|
43
59
|
}
|
|
44
60
|
|
|
61
|
+
// 获取后台进程 PID
|
|
62
|
+
function getDaemonPid() {
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(pidFile)) {
|
|
65
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
|
|
66
|
+
return pid;
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('读取 PID 文件失败:', err.message);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 检查进程是否运行
|
|
75
|
+
function isProcessRunning(pid) {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 0); // 发送信号 0 检查进程是否存在
|
|
78
|
+
return true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 停止后台服务
|
|
85
|
+
function stopDaemon() {
|
|
86
|
+
const pid = getDaemonPid();
|
|
87
|
+
|
|
88
|
+
if (!pid) {
|
|
89
|
+
console.log('没有找到后台运行的服务');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isProcessRunning(pid)) {
|
|
94
|
+
try {
|
|
95
|
+
if (process.platform === 'win32') {
|
|
96
|
+
exec(`taskkill /F /PID ${pid}`, (error) => {
|
|
97
|
+
if (error) {
|
|
98
|
+
console.error('停止服务失败:', error.message);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
console.log('后台服务已停止');
|
|
102
|
+
fs.unlinkSync(pidFile);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
process.kill(pid, 'SIGTERM');
|
|
107
|
+
console.log('后台服务已停止');
|
|
108
|
+
fs.unlinkSync(pidFile);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('停止服务失败:', err.message);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
console.log('后台服务未运行');
|
|
117
|
+
// 清理过期的 PID 文件
|
|
118
|
+
if (fs.existsSync(pidFile)) {
|
|
119
|
+
fs.unlinkSync(pidFile);
|
|
120
|
+
}
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 后台运行
|
|
126
|
+
function startDaemon(port, dir) {
|
|
127
|
+
const pid = getDaemonPid();
|
|
128
|
+
|
|
129
|
+
if (pid && isProcessRunning(pid)) {
|
|
130
|
+
console.log('服务已经在后台运行');
|
|
131
|
+
console.log('如需重启,请先使用: iflow-run --stop');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('正在启动后台服务...');
|
|
136
|
+
|
|
137
|
+
if (process.platform === 'win32') {
|
|
138
|
+
// Windows 使用 start 命令启动新窗口
|
|
139
|
+
const nodePath = process.execPath;
|
|
140
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
141
|
+
const portArg = port ? `IFLOW_RUN_PORT=${port} ` : '';
|
|
142
|
+
const dirArg = dir ? `IFLOW_RUN_DIR=${dir} ` : '';
|
|
143
|
+
|
|
144
|
+
const command = `start /B cmd /C "${portArg}${dirArg}"${nodePath}" "${serverPath}"`;
|
|
145
|
+
|
|
146
|
+
exec(command, (error) => {
|
|
147
|
+
if (error) {
|
|
148
|
+
console.error('启动失败:', error.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 等待一段时间,然后查找新启动的进程
|
|
153
|
+
setTimeout(() => {
|
|
154
|
+
// 使用 tasklist 查找 iflow-run 相关的 node 进程
|
|
155
|
+
exec('tasklist /FI "IMAGENAME eq node.exe" /FO CSV', (err, stdout) => {
|
|
156
|
+
if (err) {
|
|
157
|
+
console.error('无法获取进程信息');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 解析输出,找到最新的 node 进程
|
|
162
|
+
const lines = stdout.split('\n').slice(1);
|
|
163
|
+
const pids = lines
|
|
164
|
+
.filter(line => line.includes('node.exe'))
|
|
165
|
+
.map(line => {
|
|
166
|
+
const parts = line.split(',');
|
|
167
|
+
return parseInt(parts[1].replace(/"/g, '').trim());
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (pids.length > 0) {
|
|
171
|
+
// 取最大的 PID(最新启动的)
|
|
172
|
+
const newPid = Math.max(...pids);
|
|
173
|
+
fs.writeFileSync(pidFile, newPid.toString());
|
|
174
|
+
console.log('后台服务启动成功');
|
|
175
|
+
console.log('使用 "iflow-run --stop" 停止服务');
|
|
176
|
+
} else {
|
|
177
|
+
console.error('无法获取进程 PID');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}, 1000);
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
// Linux/Mac 使用 detached 模式
|
|
185
|
+
const nodePath = process.execPath;
|
|
186
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
187
|
+
|
|
188
|
+
const env = { ...process.env };
|
|
189
|
+
if (port) env.IFLOW_RUN_PORT = port;
|
|
190
|
+
if (dir) env.IFLOW_RUN_DIR = dir;
|
|
191
|
+
|
|
192
|
+
const child = spawn(nodePath, [serverPath], {
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: 'ignore',
|
|
195
|
+
env
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
child.unref();
|
|
199
|
+
fs.writeFileSync(pidFile, child.pid.toString());
|
|
200
|
+
console.log('后台服务启动成功');
|
|
201
|
+
console.log('使用 "iflow-run --stop" 停止服务');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
45
206
|
// 处理参数
|
|
207
|
+
let port = null;
|
|
208
|
+
let dir = null;
|
|
209
|
+
let daemonMode = false;
|
|
210
|
+
let stopCommand = false;
|
|
211
|
+
|
|
46
212
|
for (const arg of args) {
|
|
47
213
|
if (arg === '-h' || arg === '--help') {
|
|
48
214
|
showHelp();
|
|
@@ -52,7 +218,28 @@ for (const arg of args) {
|
|
|
52
218
|
showVersion();
|
|
53
219
|
process.exit(0);
|
|
54
220
|
}
|
|
221
|
+
if (arg === '-d' || arg === '--daemon') {
|
|
222
|
+
daemonMode = true;
|
|
223
|
+
}
|
|
224
|
+
if (arg === '--stop') {
|
|
225
|
+
stopCommand = true;
|
|
226
|
+
}
|
|
227
|
+
if (arg.startsWith('--port=')) {
|
|
228
|
+
port = arg.split('=')[1];
|
|
229
|
+
}
|
|
230
|
+
if (arg.startsWith('--dir=')) {
|
|
231
|
+
dir = arg.split('=')[1];
|
|
232
|
+
}
|
|
55
233
|
}
|
|
56
234
|
|
|
57
|
-
//
|
|
58
|
-
|
|
235
|
+
// 执行命令
|
|
236
|
+
if (stopCommand) {
|
|
237
|
+
stopDaemon();
|
|
238
|
+
} else if (daemonMode) {
|
|
239
|
+
startDaemon(port, dir);
|
|
240
|
+
} else {
|
|
241
|
+
// 前台运行
|
|
242
|
+
if (port) process.env.IFLOW_RUN_PORT = port;
|
|
243
|
+
if (dir) process.env.IFLOW_RUN_DIR = dir;
|
|
244
|
+
require('../server.js');
|
|
245
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "iflow-run", "version": "1.0.
|
|
1
|
+
{"name": "iflow-run", "version": "1.0.2", "description": "查看 iflow-cli 会话轨迹和历史会话的 Web 应用", "main": "server.js", "bin": {"iflow-run": "./bin/iflow-run.js"}, "scripts": {"start": "node server.js", "dev": "node server.js"}, "keywords": ["iflow", "session", "history", "cli", "viewer", "dashboard"], "author": "dukewen <dukewen666@gmail.com>", "license": "MIT", "files": ["bin/", "public/", "server.js", "package.json", "README.md"], "dependencies": {"express": "^4.18.2", "cors": "^2.8.5"}, "engines": {"node": ">=14.0.0"}}
|
package/server.js
CHANGED
|
@@ -247,6 +247,61 @@ function extractContent(msg) {
|
|
|
247
247
|
return JSON.stringify(content);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
// 检测端口是否可用
|
|
251
|
+
function isPortAvailable(port) {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
const net = require('net');
|
|
254
|
+
const server = net.createServer();
|
|
255
|
+
|
|
256
|
+
server.once('error', (err) => {
|
|
257
|
+
if (err.code === 'EADDRINUSE') {
|
|
258
|
+
resolve(false);
|
|
259
|
+
} else {
|
|
260
|
+
resolve(true);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
server.once('listening', () => {
|
|
265
|
+
server.close();
|
|
266
|
+
resolve(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
server.listen(port);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 自动查找可用端口
|
|
274
|
+
async function findAvailablePort(startPort) {
|
|
275
|
+
let port = startPort;
|
|
276
|
+
let attempts = 0;
|
|
277
|
+
const maxAttempts = 100;
|
|
278
|
+
|
|
279
|
+
while (attempts < maxAttempts) {
|
|
280
|
+
const available = await isPortAvailable(port);
|
|
281
|
+
if (available) {
|
|
282
|
+
return port;
|
|
283
|
+
}
|
|
284
|
+
port++;
|
|
285
|
+
attempts++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw new Error(`Unable to find available port after ${maxAttempts} attempts`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 启动服务器
|
|
292
|
+
(async () => {
|
|
293
|
+
try {
|
|
294
|
+
const availablePort = await findAvailablePort(PORT);
|
|
295
|
+
|
|
296
|
+
if (availablePort !== PORT) {
|
|
297
|
+
console.log(`Port ${PORT} is occupied, using port ${availablePort} instead`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
app.listen(availablePort, () => {
|
|
301
|
+
console.log(`iflow-run server running at http://localhost:${availablePort}`);
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('Failed to start server:', error.message);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
})();
|