@yeaft/webchat-agent 0.0.11 → 0.0.13

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/claude.js CHANGED
@@ -34,6 +34,7 @@ export async function startClaudeQuery(conversationId, workDir, resumeSessionId)
34
34
  createdAt: Date.now(),
35
35
  abortController,
36
36
  turnActive: false, // 是否有 turn 正在处理中
37
+ turnResultReceived: false, // 当前 turn 是否已收到 result(用于抑制重复 result)
37
38
  // Metadata from system init message
38
39
  tools: [],
39
40
  slashCommands: [],
@@ -324,6 +325,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
324
325
 
325
326
  // 捕获 result 消息中的 usage 信息
326
327
  if (message.type === 'result') {
328
+ // 累计 usage(无论是否重复,始终统计)
327
329
  if (message.usage) {
328
330
  state.usage.inputTokens += message.usage.input_tokens || 0;
329
331
  state.usage.outputTokens += message.usage.output_tokens || 0;
@@ -333,9 +335,17 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
333
335
  state.usage.totalCostUsd += message.total_cost_usd || 0;
334
336
  console.log(`[SDK] Query completed for ${conversationId}, cost: $${state.usage.totalCostUsd.toFixed(4)}`);
335
337
 
338
+ // ★ Guard:当前 turn 已收到过 result,抑制 SDK 发出的重复 result
339
+ // (长任务场景下 SDK 可能先发 result/success 再发 result/error_during_execution)
340
+ if (state.turnResultReceived) {
341
+ console.warn(`[SDK] Suppressing duplicate result for ${conversationId} (subtype: ${message.subtype || 'unknown'})`);
342
+ continue;
343
+ }
344
+
336
345
  // ★ Turn 完成:发送 turn_completed,进程继续运行等待下一条消息
337
346
  // stream-json 模式下 Claude 进程是持久运行的,for-await 在 result 后继续等待
338
347
  // 不清空 state.query 和 state.inputStream,下次用户消息直接通过同一个 inputStream 发送
348
+ state.turnResultReceived = true;
339
349
  sendOutput(conversationId, message);
340
350
 
341
351
  resultHandled = true;
package/conversation.js CHANGED
@@ -360,6 +360,7 @@ export async function handleUserInput(msg) {
360
360
 
361
361
  console.log(`[${conversationId}] Sending: ${prompt.substring(0, 100)}...`);
362
362
  state.turnActive = true;
363
+ state.turnResultReceived = false; // 重置 per-turn 去重标志
363
364
  sendConversationList(); // 在 turnActive=true 后通知 server,确保 processing 状态正确
364
365
  state.inputStream.enqueue(userMessage);
365
366
  }
package/history.js CHANGED
@@ -168,7 +168,7 @@ export function loadSessionHistory(workDir, claudeSessionId, limit = 500) {
168
168
  }
169
169
 
170
170
  export async function handleListHistorySessions(msg) {
171
- const { workDir, requestId } = msg;
171
+ const { workDir, requestId, _requestClientId } = msg;
172
172
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
173
173
 
174
174
  console.log(`Listing history sessions for: ${effectiveWorkDir}`);
@@ -178,6 +178,7 @@ export async function handleListHistorySessions(msg) {
178
178
  ctx.sendToServer({
179
179
  type: 'history_sessions_list',
180
180
  requestId,
181
+ _requestClientId,
181
182
  workDir: effectiveWorkDir,
182
183
  sessions
183
184
  });
@@ -186,6 +187,7 @@ export async function handleListHistorySessions(msg) {
186
187
  ctx.sendToServer({
187
188
  type: 'history_sessions_list',
188
189
  requestId,
190
+ _requestClientId,
189
191
  workDir: effectiveWorkDir,
190
192
  sessions: [],
191
193
  error: e.message
@@ -195,7 +197,7 @@ export async function handleListHistorySessions(msg) {
195
197
 
196
198
  // 列出 Claude projects 目录下的所有 folder (工作目录)
197
199
  export async function handleListFolders(msg) {
198
- const { requestId } = msg;
200
+ const { requestId, _requestClientId } = msg;
199
201
  const projectsDir = getClaudeProjectsDir();
200
202
 
201
203
  console.log(`Listing folders from: ${projectsDir}`);
@@ -268,6 +270,7 @@ export async function handleListFolders(msg) {
268
270
  ctx.sendToServer({
269
271
  type: 'folders_list',
270
272
  requestId,
273
+ _requestClientId,
271
274
  folders
272
275
  });
273
276
  console.log(`folders_list sent with ${folders.length} folders`);
@@ -276,6 +279,7 @@ export async function handleListFolders(msg) {
276
279
  ctx.sendToServer({
277
280
  type: 'folders_list',
278
281
  requestId,
282
+ _requestClientId,
279
283
  folders: [],
280
284
  error: e.message
281
285
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -44,7 +44,8 @@
44
44
  "workbench.js",
45
45
  "history.js",
46
46
  "encryption.js",
47
- "sdk/"
47
+ "sdk/",
48
+ "scripts/agent-tray.ps1"
48
49
  ],
49
50
  "license": "MIT",
50
51
  "author": "Yeaft",
@@ -0,0 +1,134 @@
1
+ # Claude Agent 托盘管理器
2
+ # 右键托盘图标可以查看日志、重启、停止等
3
+
4
+ Add-Type -AssemblyName System.Windows.Forms
5
+ Add-Type -AssemblyName System.Drawing
6
+
7
+ $PM2AppName = "yeaft-agent"
8
+
9
+ # 创建托盘图标
10
+ $notifyIcon = New-Object System.Windows.Forms.NotifyIcon
11
+ $notifyIcon.Text = "Yeaft Agent"
12
+ $notifyIcon.Visible = $true
13
+
14
+ # 创建 Claude 风格图标(橙色圆形 + 白色 C)
15
+ function Create-ClaudeIcon {
16
+ $size = 32
17
+ $bitmap = New-Object System.Drawing.Bitmap($size, $size)
18
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
19
+ $graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
20
+ $graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::AntiAlias
21
+
22
+ # 橙色背景圆形 (Claude 的品牌色)
23
+ $orangeBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(255, 204, 102, 51))
24
+ $graphics.FillEllipse($orangeBrush, 1, 1, $size - 2, $size - 2)
25
+
26
+ # 白色 "C" 字母
27
+ $whiteBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
28
+ $font = New-Object System.Drawing.Font("Arial", 16, [System.Drawing.FontStyle]::Bold)
29
+ # 测量文字大小并精确居中
30
+ $textSize = $graphics.MeasureString("C", $font)
31
+ $x = ($size - $textSize.Width) / 2
32
+ $y = ($size - $textSize.Height) / 2
33
+ $graphics.DrawString("C", $font, $whiteBrush, $x, $y)
34
+
35
+ $graphics.Dispose()
36
+
37
+ # 转换为图标
38
+ $hIcon = $bitmap.GetHicon()
39
+ $icon = [System.Drawing.Icon]::FromHandle($hIcon)
40
+ return $icon
41
+ }
42
+
43
+ $notifyIcon.Icon = Create-ClaudeIcon
44
+
45
+ # 创建右键菜单
46
+ $contextMenu = New-Object System.Windows.Forms.ContextMenuStrip
47
+
48
+ # 查看状态
49
+ $menuStatus = New-Object System.Windows.Forms.ToolStripMenuItem
50
+ $menuStatus.Text = "View Status"
51
+ $menuStatus.Add_Click({
52
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 status`""
53
+ })
54
+ $contextMenu.Items.Add($menuStatus)
55
+
56
+ # 查看日志
57
+ $menuLogs = New-Object System.Windows.Forms.ToolStripMenuItem
58
+ $menuLogs.Text = "View Logs"
59
+ $menuLogs.Add_Click({
60
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 logs $PM2AppName --lines 100`""
61
+ })
62
+ $contextMenu.Items.Add($menuLogs)
63
+
64
+ # 分隔线
65
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
66
+
67
+ # 重启
68
+ $menuRestart = New-Object System.Windows.Forms.ToolStripMenuItem
69
+ $menuRestart.Text = "Restart Agent"
70
+ $menuRestart.Add_Click({
71
+ pm2 restart $PM2AppName
72
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent restarted", [System.Windows.Forms.ToolTipIcon]::Info)
73
+ })
74
+ $contextMenu.Items.Add($menuRestart)
75
+
76
+ # 停止
77
+ $menuStop = New-Object System.Windows.Forms.ToolStripMenuItem
78
+ $menuStop.Text = "Stop Agent"
79
+ $menuStop.Add_Click({
80
+ pm2 stop $PM2AppName
81
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent stopped", [System.Windows.Forms.ToolTipIcon]::Warning)
82
+ })
83
+ $contextMenu.Items.Add($menuStop)
84
+
85
+ # 启动
86
+ $menuStart = New-Object System.Windows.Forms.ToolStripMenuItem
87
+ $menuStart.Text = "Start Agent"
88
+ $menuStart.Add_Click({
89
+ pm2 start $PM2AppName
90
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Agent started", [System.Windows.Forms.ToolTipIcon]::Info)
91
+ })
92
+ $contextMenu.Items.Add($menuStart)
93
+
94
+ # 分隔线
95
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
96
+
97
+ # 打开日志文件夹
98
+ $menuOpenLogs = New-Object System.Windows.Forms.ToolStripMenuItem
99
+ $menuOpenLogs.Text = "Open Logs Folder"
100
+ $menuOpenLogs.Add_Click({
101
+ $logDir = Join-Path $env:APPDATA "yeaft-agent\logs"
102
+ if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null }
103
+ Start-Process "explorer" -ArgumentList $logDir
104
+ })
105
+ $contextMenu.Items.Add($menuOpenLogs)
106
+
107
+ # 分隔线
108
+ $contextMenu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
109
+
110
+ # 退出(停止 agent + 关闭托盘)
111
+ $menuExit = New-Object System.Windows.Forms.ToolStripMenuItem
112
+ $menuExit.Text = "Exit"
113
+ $menuExit.Add_Click({
114
+ pm2 stop $PM2AppName 2>$null
115
+ $notifyIcon.ShowBalloonTip(1000, "Yeaft Agent", "Agent stopped", [System.Windows.Forms.ToolTipIcon]::Info)
116
+ Start-Sleep -Milliseconds 500
117
+ $notifyIcon.Visible = $false
118
+ $notifyIcon.Dispose()
119
+ [System.Windows.Forms.Application]::Exit()
120
+ })
121
+ $contextMenu.Items.Add($menuExit)
122
+
123
+ $notifyIcon.ContextMenuStrip = $contextMenu
124
+
125
+ # 双击打开日志
126
+ $notifyIcon.Add_DoubleClick({
127
+ Start-Process "powershell" -ArgumentList "-NoExit -Command `"pm2 logs $PM2AppName`""
128
+ })
129
+
130
+ # 显示启动提示
131
+ $notifyIcon.ShowBalloonTip(2000, "Yeaft Agent", "Tray manager started. Right-click for options.", [System.Windows.Forms.ToolTipIcon]::Info)
132
+
133
+ # 保持运行
134
+ [System.Windows.Forms.Application]::Run()
package/service.js CHANGED
@@ -443,12 +443,23 @@ function winInstall(config) {
443
443
 
444
444
  // Setup auto-start: create startup script in Windows Startup folder
445
445
  // pm2-startup doesn't work well on Windows, use Startup folder approach
446
+ const trayScript = join(__dirname, 'scripts', 'agent-tray.ps1');
446
447
  const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
447
448
  const startupBat = join(startupDir, `${PM2_APP_NAME}.bat`);
448
- // pm2 is in PATH (ensured by ensurePm2), so just call it directly
449
- const batContent = `@echo off\r\npm2 resurrect\r\n`;
449
+ // Resurrect pm2 processes + launch tray icon
450
+ let batContent = `@echo off\r\npm2 resurrect\r\n`;
451
+ if (existsSync(trayScript)) {
452
+ batContent += `start "" powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "${trayScript}"\r\n`;
453
+ }
450
454
  writeFileSync(startupBat, batContent);
451
455
 
456
+ // Launch tray now
457
+ if (existsSync(trayScript)) {
458
+ spawn('powershell', ['-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', trayScript], {
459
+ detached: true, stdio: 'ignore'
460
+ }).unref();
461
+ }
462
+
452
463
  console.log(`\nService installed and started.`);
453
464
  console.log(` Ecosystem: ${ecoPath}`);
454
465
  console.log(` Startup: ${startupBat}`);