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 CHANGED
@@ -2,12 +2,21 @@
2
2
 
3
3
  一个用于查看 iFlow CLI 会话轨迹和历史会话的 Web 应用程序。
4
4
 
5
+ [![npm version](https://badge.fury.io/js/iflow-run.svg)](https://www.npmjs.com/package/iflow-run)
6
+ [![GitHub stars](https://img.shields.io/github/stars/KeWen-Du/iflow-run?style=social)](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 # Express 服务器
89
- ├── package.json # 项目配置
90
- ├── public/ # 前端静态文件
91
- ├── index.html # 主页面
92
- ├── app.js # 前端逻辑
93
- │ ├── styles.css # 样式文件
94
- └── test.html # 测试页面
95
- └── test_screenshot.py # 自动化测试脚本
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
- ![项目列表](https://via.placeholder.com/400x300/1a1a24/ffffff?text=Projects+List)
170
- ![会话列表](https://via.placeholder.com/400x300/1a1a24/ffffff?text=Sessions+List)
171
- ![会话详情](https://via.placeholder.com/400x300/1a1a24/ffffff?text=Session+Detail)
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
- require('../server.js');
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.0", "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"}}
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
- app.listen(PORT, () => {
251
- console.log(`iflow-run server running at http://localhost:${PORT}`);
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
+ })();