stella-coder 5.1.1 → 5.2.0
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/capture.ps1 +14 -0
- package/package.json +1 -1
- package/stella-cli/index.mjs +253 -3
- package/stella-cli/screen-monitor.mjs +334 -0
- package/test_screen.png +0 -0
package/capture.ps1
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
2
|
+
Add-Type -AssemblyName System.Drawing
|
|
3
|
+
try {
|
|
4
|
+
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
5
|
+
$bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
6
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
7
|
+
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
8
|
+
$bitmap.Save("C:\Users\user\Downloads\stella-coder-3-9\test_screen.png")
|
|
9
|
+
$graphics.Dispose()
|
|
10
|
+
$bitmap.Dispose()
|
|
11
|
+
Write-Output "OK"
|
|
12
|
+
} catch {
|
|
13
|
+
Write-Output "FAIL:$($_.Exception.Message)"
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stella-coder",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
|
|
6
6
|
"main": "stella-cli/index.mjs",
|
package/stella-cli/index.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { runSubagent, listSubagents, parseAgentCommand, SUBAGENTS } from "./suba
|
|
|
22
22
|
import { mcp, MCP_COMMANDS } from "./mcp.mjs"
|
|
23
23
|
import { generatePresentation, createPresentationFromTopic, AVAILABLE_THEMES, exportToPDF } from "./presentations.mjs"
|
|
24
24
|
import { AutonomousAgent } from "./autonomous-agent.mjs"
|
|
25
|
+
import { ScreenMonitor } from "./screen-monitor.mjs"
|
|
25
26
|
import {
|
|
26
27
|
buildRepoMap, buildProjectContext, compressContext,
|
|
27
28
|
loadSpec, generateSpecTemplate,
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
generateAdminCode, listAuthorizedUsers,
|
|
42
43
|
} from "./telegram-bot.mjs"
|
|
43
44
|
|
|
44
|
-
const VERSION = "5.
|
|
45
|
+
const VERSION = "5.2.0"
|
|
45
46
|
const CONFIG_DIR = path.join(os.homedir(), ".stella")
|
|
46
47
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json")
|
|
47
48
|
const HISTORY_PATH = path.join(CONFIG_DIR, "history.json")
|
|
@@ -650,6 +651,13 @@ const COMMANDS = [
|
|
|
650
651
|
["/auto-dashboard", "создать dashboard с результатами"],
|
|
651
652
|
["/auto-history", "история выполненных задач"],
|
|
652
653
|
|
|
654
|
+
// Computer Vision Monitor
|
|
655
|
+
["/monitor", "запустить мониторинг экрана (AI vision)"],
|
|
656
|
+
["/monitor-stop", "остановить мониторинг"],
|
|
657
|
+
["/monitor-status", "статус мониторинга"],
|
|
658
|
+
["/monitor-alerts", "показать обнаруженные проблемы"],
|
|
659
|
+
["/monitor-screenshot", "сделать скриншот и проанализировать"],
|
|
660
|
+
|
|
653
661
|
// Управление компьютером
|
|
654
662
|
["/open", "открыть приложение/файл/URL"],
|
|
655
663
|
["/app", "запустить приложение"],
|
|
@@ -775,6 +783,7 @@ async function handleCommand(line) {
|
|
|
775
783
|
["📱 Telegram", ["/tg", "/tg-stop", "/tg-notify", "/tg-sessions", "/tg-verify", "/tg-code", "/tg-users"]],
|
|
776
784
|
["🔀 Git Экосистема", ["/git-eco", "/git-pr", "/git-merge-auto"]],
|
|
777
785
|
["🤖 Autonomous Agent", ["/auto", "/auto-stop", "/auto-status", "/auto-dashboard", "/auto-history"]],
|
|
786
|
+
["👁️ Computer Vision", ["/monitor", "/monitor-stop", "/monitor-status", "/monitor-alerts", "/monitor-screenshot"]],
|
|
778
787
|
["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
|
|
779
788
|
]
|
|
780
789
|
for (const [cat, cmds] of categories) {
|
|
@@ -3444,8 +3453,13 @@ async function handleCommand(line) {
|
|
|
3444
3453
|
console.log()
|
|
3445
3454
|
|
|
3446
3455
|
const apiCall = async (prompt) => {
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3456
|
+
const model = getModel(state.model)
|
|
3457
|
+
const { text } = await generateText({
|
|
3458
|
+
model,
|
|
3459
|
+
messages: [{ role: "user", content: prompt }],
|
|
3460
|
+
maxTokens: 16000,
|
|
3461
|
+
})
|
|
3462
|
+
return text
|
|
3449
3463
|
}
|
|
3450
3464
|
|
|
3451
3465
|
console.log(dim(" Запускаю автономный режим...\n"))
|
|
@@ -3594,6 +3608,242 @@ async function handleCommand(line) {
|
|
|
3594
3608
|
return
|
|
3595
3609
|
}
|
|
3596
3610
|
|
|
3611
|
+
// ═══════════════════════════════════════════════════
|
|
3612
|
+
// COMPUTER VISION MONITOR
|
|
3613
|
+
// ═══════════════════════════════════════════════════
|
|
3614
|
+
case "/monitor": {
|
|
3615
|
+
console.log()
|
|
3616
|
+
const monitor = new ScreenMonitor()
|
|
3617
|
+
|
|
3618
|
+
if (monitor.state.running) {
|
|
3619
|
+
console.log(yellow(" Мониторинг уже запущен! Используй /monitor-stop для остановки.\n"))
|
|
3620
|
+
return
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
const intervalSec = arg ? parseInt(arg) || 2 : 2
|
|
3624
|
+
const intervalMs = intervalSec * 1000
|
|
3625
|
+
|
|
3626
|
+
console.log(box([
|
|
3627
|
+
violet("👁️ COMPUTER VISION MONITOR"),
|
|
3628
|
+
"",
|
|
3629
|
+
white("Интервал: ") + cyan(`${intervalSec} секунды`),
|
|
3630
|
+
white("Режим: ") + cyan("автоматический скриншот + AI анализ"),
|
|
3631
|
+
"",
|
|
3632
|
+
dim("Агент будет делать скриншот каждые") + white(`${intervalSec}с`),
|
|
3633
|
+
dim("и анализировать через AI на наличие ошибок, крашей, алертов."),
|
|
3634
|
+
dim("Все обнаруженные проблемы будут сохранены в лог."),
|
|
3635
|
+
], { color: violet, padding: 2 }))
|
|
3636
|
+
console.log()
|
|
3637
|
+
|
|
3638
|
+
const confirmMonitor = await question(" " + green("Запустить мониторинг? (y/n) › "))
|
|
3639
|
+
if (confirmMonitor.trim().toLowerCase() !== "y" && confirmMonitor.trim().toLowerCase() !== "д") {
|
|
3640
|
+
console.log(dim(" Отменено\n"))
|
|
3641
|
+
return
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
const visionFn = async (imageBase64, recentContext) => {
|
|
3645
|
+
const contextStr = recentContext?.length > 0
|
|
3646
|
+
? `\nRecent history:\n${recentContext.map(r => `- ${r.time}: ${r.summary} (${r.alerts} alerts)`).join("\n")}`
|
|
3647
|
+
: ""
|
|
3648
|
+
|
|
3649
|
+
const prompt = `Analyze this screenshot. Detect:
|
|
3650
|
+
1. Error messages, crash dialogs, exception windows
|
|
3651
|
+
2. Alert popups, warning notifications
|
|
3652
|
+
3. Application crashes or "not responding" dialogs
|
|
3653
|
+
4. System errors, BSOD, kernel panics
|
|
3654
|
+
5. Any other issues or anomalies
|
|
3655
|
+
|
|
3656
|
+
${contextStr}
|
|
3657
|
+
|
|
3658
|
+
Return JSON:
|
|
3659
|
+
{
|
|
3660
|
+
"summary": "one-line description of what's on screen",
|
|
3661
|
+
"text": "detailed analysis of what you see",
|
|
3662
|
+
"issues": ["list of issues found"],
|
|
3663
|
+
"severity": "none|info|warning|critical",
|
|
3664
|
+
"recommendations": ["what to do about issues"]
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
If everything looks normal, set severity to "none" and issues to [].`
|
|
3668
|
+
|
|
3669
|
+
const model = getModel(state.model)
|
|
3670
|
+
const { text } = await generateText({
|
|
3671
|
+
model,
|
|
3672
|
+
messages: [{
|
|
3673
|
+
role: "user",
|
|
3674
|
+
content: [
|
|
3675
|
+
{ type: "text", text: prompt },
|
|
3676
|
+
{ type: "image", image: `data:image/png;base64,${imageBase64}` }
|
|
3677
|
+
]
|
|
3678
|
+
}],
|
|
3679
|
+
maxTokens: 2000,
|
|
3680
|
+
})
|
|
3681
|
+
|
|
3682
|
+
try {
|
|
3683
|
+
let clean = text.trim()
|
|
3684
|
+
if (clean.startsWith("```")) clean = clean.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "")
|
|
3685
|
+
return JSON.parse(clean)
|
|
3686
|
+
} catch {
|
|
3687
|
+
return { summary: text.slice(0, 200), text, issues: [], severity: "none", recommendations: [] }
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
console.log()
|
|
3692
|
+
startSpinner("Запускаю мониторинг")
|
|
3693
|
+
const result = monitor.start(intervalMs, visionFn)
|
|
3694
|
+
stopSpinner()
|
|
3695
|
+
|
|
3696
|
+
if (result.success) {
|
|
3697
|
+
console.log(green(` ✓ Мониторинг запущен! Интервал: ${intervalSec}с`))
|
|
3698
|
+
console.log(dim(" Скриншоты сохраняются в ~/.stella/monitor/screenshots/"))
|
|
3699
|
+
console.log(dim(" Используй /monitor-stop для остановки"))
|
|
3700
|
+
console.log(dim(" Используй /monitor-status для проверки"))
|
|
3701
|
+
console.log(dim(" Используй /monitor-alerts для просмотра проблем\n"))
|
|
3702
|
+
|
|
3703
|
+
// Show first capture
|
|
3704
|
+
console.log(dim(" Делаю первый скриншот..."))
|
|
3705
|
+
const firstCapture = await monitor.captureAndAnalyze(visionFn)
|
|
3706
|
+
if (firstCapture.success) {
|
|
3707
|
+
console.log(green(` ✓ Скриншот: ${firstCapture.capture.sizeKB}KB`))
|
|
3708
|
+
if (firstCapture.capture.analysis?.summary) {
|
|
3709
|
+
console.log(dim(" Анализ: ") + white(firstCapture.capture.analysis.summary))
|
|
3710
|
+
}
|
|
3711
|
+
if (firstCapture.capture.alerts?.length > 0) {
|
|
3712
|
+
console.log(yellow(` ⚠️ Обнаружено проблем: ${firstCapture.capture.alerts.length}`))
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
console.log()
|
|
3716
|
+
} else {
|
|
3717
|
+
console.log(red(` ✗ ${result.error}\n`))
|
|
3718
|
+
}
|
|
3719
|
+
return
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
case "/monitor-stop": {
|
|
3723
|
+
console.log()
|
|
3724
|
+
const monitor = new ScreenMonitor()
|
|
3725
|
+
if (!monitor.state.running) {
|
|
3726
|
+
console.log(dim(" Мониторинг не запущен\n"))
|
|
3727
|
+
return
|
|
3728
|
+
}
|
|
3729
|
+
monitor.stop()
|
|
3730
|
+
console.log(green(" ✓ Мониторинг остановлен"))
|
|
3731
|
+
console.log(dim(` Всего скриншотов: ${monitor.state.totalCaptures}`))
|
|
3732
|
+
console.log(dim(` Всего алертов: ${monitor.state.totalAlerts}`))
|
|
3733
|
+
console.log()
|
|
3734
|
+
return
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
case "/monitor-status": {
|
|
3738
|
+
console.log()
|
|
3739
|
+
const monitor = new ScreenMonitor()
|
|
3740
|
+
const status = monitor.getStatus()
|
|
3741
|
+
|
|
3742
|
+
if (!status.running && status.totalCaptures === 0) {
|
|
3743
|
+
console.log(dim(" Мониторинг не запущен. Используй /monitor для запуска.\n"))
|
|
3744
|
+
return
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
const items = [
|
|
3748
|
+
dim("Статус: ") + (status.running ? green("● RUNNING") : red("○ STOPPED")),
|
|
3749
|
+
dim("Интервал: ") + cyan(`every ${status.interval / 1000}s`),
|
|
3750
|
+
dim("Начато: ") + white(status.startedAt ? new Date(status.startedAt).toLocaleString() : "—"),
|
|
3751
|
+
dim("Скриншотов: ") + cyan(String(status.totalCaptures)),
|
|
3752
|
+
dim("Алертов: ") + (status.totalAlerts > 0 ? red(String(status.totalAlerts)) : green("0")),
|
|
3753
|
+
dim("PID: ") + white(String(status.pid || "—")),
|
|
3754
|
+
]
|
|
3755
|
+
|
|
3756
|
+
if (status.recentAlerts.length > 0) {
|
|
3757
|
+
items.push("", violet("Последние алерты:"))
|
|
3758
|
+
for (const a of status.recentAlerts.slice(-5)) {
|
|
3759
|
+
const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
|
|
3760
|
+
items.push(` ${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`)
|
|
3761
|
+
if (a.description) items.push(dim(` ${a.description.slice(0, 100)}`))
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
if (status.recentCaptures.length > 0) {
|
|
3766
|
+
items.push("", dim("Последние скриншоты:"))
|
|
3767
|
+
for (const c of status.recentCaptures) {
|
|
3768
|
+
const icon = c.alertCount > 0 ? yellow("⚠️") : green("✅")
|
|
3769
|
+
items.push(` ${icon} [${new Date(c.time).toLocaleTimeString()}] ${c.summary}`)
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
console.log(box(items, { title: "👁️ Monitor Status", color: violet, padding: 2 }))
|
|
3774
|
+
console.log()
|
|
3775
|
+
return
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
case "/monitor-alerts": {
|
|
3779
|
+
console.log()
|
|
3780
|
+
const monitor = new ScreenMonitor()
|
|
3781
|
+
const alerts = monitor.getAlerts()
|
|
3782
|
+
|
|
3783
|
+
if (alerts.length === 0) {
|
|
3784
|
+
console.log(dim(" Нет обнаруженных проблем. Всё чисто! ✅\n"))
|
|
3785
|
+
return
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
console.log(box([
|
|
3789
|
+
violet(`Всего алертов: ${alerts.length}`),
|
|
3790
|
+
"",
|
|
3791
|
+
...alerts.slice(-15).map(a => {
|
|
3792
|
+
const icon = a.severity === "critical" ? red("🔴") : yellow("🟡")
|
|
3793
|
+
return `${icon} [${new Date(a.timestamp).toLocaleTimeString()}] ${a.type}`
|
|
3794
|
+
}),
|
|
3795
|
+
], { title: "⚠️ Monitor Alerts", color: yellow, padding: 2 }))
|
|
3796
|
+
console.log()
|
|
3797
|
+
return
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
case "/monitor-screenshot": {
|
|
3801
|
+
console.log()
|
|
3802
|
+
startSpinner("Делаю скриншот")
|
|
3803
|
+
const monitor = new ScreenMonitor()
|
|
3804
|
+
|
|
3805
|
+
const visionFn = async (imageBase64) => {
|
|
3806
|
+
const prompt = `Analyze this screenshot in detail. What's on screen? Any errors, issues, or anomalies? Return JSON with "summary", "text", "issues", "severity".`
|
|
3807
|
+
const model = getModel(state.model)
|
|
3808
|
+
const { text } = await generateText({
|
|
3809
|
+
model,
|
|
3810
|
+
messages: [{
|
|
3811
|
+
role: "user",
|
|
3812
|
+
content: [
|
|
3813
|
+
{ type: "text", text: prompt },
|
|
3814
|
+
{ type: "image", image: `data:image/png;base64,${imageBase64}` }
|
|
3815
|
+
]
|
|
3816
|
+
}],
|
|
3817
|
+
maxTokens: 2000,
|
|
3818
|
+
})
|
|
3819
|
+
try { return JSON.parse(text) } catch { return { summary: text.slice(0, 200), text } }
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
const capture = await monitor.captureAndAnalyze(visionFn)
|
|
3823
|
+
stopSpinner()
|
|
3824
|
+
|
|
3825
|
+
if (capture.success) {
|
|
3826
|
+
console.log(box([
|
|
3827
|
+
green("✓ Скриншот сделан!"),
|
|
3828
|
+
"",
|
|
3829
|
+
dim("Файл: ") + cyan(capture.capture.path),
|
|
3830
|
+
dim("Размер: ") + white(`${capture.capture.sizeKB}KB`),
|
|
3831
|
+
"",
|
|
3832
|
+
violet("Анализ:"),
|
|
3833
|
+
white(capture.capture.analysis?.summary || "analyzing..."),
|
|
3834
|
+
capture.capture.analysis?.text ? dim(capture.capture.analysis.text.slice(0, 500)) : "",
|
|
3835
|
+
"",
|
|
3836
|
+
capture.capture.alerts?.length > 0
|
|
3837
|
+
? yellow(`⚠️ Обнаружено проблем: ${capture.capture.alerts.length}`)
|
|
3838
|
+
: green("✅ Проблем не обнаружено"),
|
|
3839
|
+
], { title: "📸 Screenshot Analysis", color: cyan, padding: 2 }))
|
|
3840
|
+
} else {
|
|
3841
|
+
console.log(red(` ✗ ${capture.error}\n`))
|
|
3842
|
+
}
|
|
3843
|
+
console.log()
|
|
3844
|
+
return
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3597
3847
|
default:
|
|
3598
3848
|
console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
|
|
3599
3849
|
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, statSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
import { execSync } from "child_process"
|
|
5
|
+
|
|
6
|
+
const STELLA_DIR = join(homedir(), ".stella")
|
|
7
|
+
const MONITOR_DIR = join(STELLA_DIR, "monitor")
|
|
8
|
+
const HISTORY_FILE = join(MONITOR_DIR, "history.json")
|
|
9
|
+
const ALERTS_FILE = join(MONITOR_DIR, "alerts.json")
|
|
10
|
+
const SCREENSHOTS_DIR = join(MONITOR_DIR, "screenshots")
|
|
11
|
+
const MONITOR_STATE = join(MONITOR_DIR, "state.json")
|
|
12
|
+
|
|
13
|
+
function ensureDir() {
|
|
14
|
+
if (!existsSync(MONITOR_DIR)) mkdirSync(MONITOR_DIR, { recursive: true })
|
|
15
|
+
if (!existsSync(SCREENSHOTS_DIR)) mkdirSync(SCREENSHOTS_DIR, { recursive: true })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loadJSON(file, fallback) {
|
|
19
|
+
try { return JSON.parse(readFileSync(file, "utf-8")) } catch { return fallback }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveJSON(file, data) {
|
|
23
|
+
ensureDir()
|
|
24
|
+
writeFileSync(file, JSON.stringify(data, null, 2))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function captureScreenSync() {
|
|
28
|
+
const ts = Date.now()
|
|
29
|
+
const outPath = join(SCREENSHOTS_DIR, `screen_${ts}.png`)
|
|
30
|
+
const tempPath = join(SCREENSHOTS_DIR, `_temp.png`)
|
|
31
|
+
const psScriptPath = join(SCREENSHOTS_DIR, `_capture.ps1`)
|
|
32
|
+
|
|
33
|
+
// Write PowerShell script to file (avoids escaping issues)
|
|
34
|
+
const psScript = `
|
|
35
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
36
|
+
Add-Type -AssemblyName System.Drawing
|
|
37
|
+
try {
|
|
38
|
+
$bounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
|
|
39
|
+
$bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
|
40
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
41
|
+
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
|
|
42
|
+
$bitmap.Save("${tempPath.replace(/\\/g, "\\\\")}")
|
|
43
|
+
$graphics.Dispose()
|
|
44
|
+
$bitmap.Dispose()
|
|
45
|
+
Write-Output "OK"
|
|
46
|
+
} catch {
|
|
47
|
+
Write-Output "FAIL:$($_.Exception.Message)"
|
|
48
|
+
}
|
|
49
|
+
`.trim()
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(psScriptPath, psScript, "utf-8")
|
|
53
|
+
const result = execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${psScriptPath}"`, {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
timeout: 10000,
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
}).trim()
|
|
58
|
+
|
|
59
|
+
if (result.startsWith("OK") && existsSync(tempPath)) {
|
|
60
|
+
const data = readFileSync(tempPath)
|
|
61
|
+
writeFileSync(outPath, data)
|
|
62
|
+
try { unlinkSync(tempPath) } catch {}
|
|
63
|
+
try { unlinkSync(psScriptPath) } catch {}
|
|
64
|
+
return outPath
|
|
65
|
+
}
|
|
66
|
+
try { unlinkSync(psScriptPath) } catch {}
|
|
67
|
+
return null
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function compressImage(imagePath, maxWidth = 800) {
|
|
74
|
+
const compressedPath = imagePath.replace(".png", "_small.png")
|
|
75
|
+
const psScriptPath = join(SCREENSHOTS_DIR, `_compress.ps1`)
|
|
76
|
+
|
|
77
|
+
const psScript = `
|
|
78
|
+
Add-Type -AssemblyName System.Drawing
|
|
79
|
+
try {
|
|
80
|
+
$img = [System.Drawing.Image]::FromFile("${imagePath.replace(/\\/g, "\\\\")}")
|
|
81
|
+
$ratio = ${maxWidth} / $img.Width
|
|
82
|
+
if ($ratio -ge 1) { $ratio = 1 }
|
|
83
|
+
$newW = [int]($img.Width * $ratio)
|
|
84
|
+
$newH = [int]($img.Height * $ratio)
|
|
85
|
+
$bmp = New-Object System.Drawing.Bitmap($newW, $newH)
|
|
86
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
87
|
+
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
88
|
+
$g.DrawImage($img, 0, 0, $newW, $newH)
|
|
89
|
+
$bmp.Save("${compressedPath.replace(/\\/g, "\\\\")}")
|
|
90
|
+
$g.Dispose()
|
|
91
|
+
$bmp.Dispose()
|
|
92
|
+
$img.Dispose()
|
|
93
|
+
Write-Output "OK"
|
|
94
|
+
} catch {
|
|
95
|
+
Write-Output "FAIL"
|
|
96
|
+
}
|
|
97
|
+
`.trim()
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(psScriptPath, psScript, "utf-8")
|
|
101
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -File "${psScriptPath}"`, {
|
|
102
|
+
encoding: "utf-8",
|
|
103
|
+
timeout: 10000,
|
|
104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
105
|
+
})
|
|
106
|
+
try { unlinkSync(psScriptPath) } catch {}
|
|
107
|
+
if (existsSync(compressedPath)) {
|
|
108
|
+
return compressedPath
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
return imagePath
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class ScreenMonitor {
|
|
115
|
+
constructor() {
|
|
116
|
+
ensureDir()
|
|
117
|
+
this.state = loadJSON(MONITOR_STATE, {
|
|
118
|
+
running: false,
|
|
119
|
+
interval: 2000,
|
|
120
|
+
startedAt: null,
|
|
121
|
+
totalCaptures: 0,
|
|
122
|
+
totalAlerts: 0,
|
|
123
|
+
pid: null,
|
|
124
|
+
})
|
|
125
|
+
this.history = loadJSON(HISTORY_FILE, [])
|
|
126
|
+
this.alerts = loadJSON(ALERTS_FILE, [])
|
|
127
|
+
this.intervalId = null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
save() {
|
|
131
|
+
saveJSON(MONITOR_STATE, this.state)
|
|
132
|
+
saveJSON(HISTORY_FILE, this.history.slice(-500))
|
|
133
|
+
saveJSON(ALERTS_FILE, this.alerts.slice(-200))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async captureAndAnalyze(visionFn) {
|
|
137
|
+
const imagePath = captureScreenSync()
|
|
138
|
+
if (!imagePath) {
|
|
139
|
+
return { success: false, error: "Failed to capture screen" }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const stats = statSync(imagePath)
|
|
143
|
+
const sizeKB = Math.round(stats.size / 1024)
|
|
144
|
+
|
|
145
|
+
// Compress for AI analysis
|
|
146
|
+
const compressedPath = compressImage(imagePath, 600)
|
|
147
|
+
let imageBase64
|
|
148
|
+
try {
|
|
149
|
+
imageBase64 = readFileSync(compressedPath).toString("base64")
|
|
150
|
+
} catch {
|
|
151
|
+
imageBase64 = readFileSync(imagePath).toString("base64")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ts = new Date().toISOString()
|
|
155
|
+
const capture = {
|
|
156
|
+
id: Date.now().toString(36),
|
|
157
|
+
timestamp: ts,
|
|
158
|
+
path: imagePath,
|
|
159
|
+
sizeKB,
|
|
160
|
+
analysis: null,
|
|
161
|
+
alerts: [],
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Analyze with vision AI
|
|
165
|
+
if (visionFn) {
|
|
166
|
+
try {
|
|
167
|
+
const analysis = await visionFn(imageBase64, this.getRecentContext())
|
|
168
|
+
capture.analysis = analysis
|
|
169
|
+
|
|
170
|
+
// Detect alerts
|
|
171
|
+
const alertKeywords = [
|
|
172
|
+
"error", "crash", "exception", "fatal", "alert", "warning", "failed",
|
|
173
|
+
"ошибка", "краш", "исключение", "фатальная", "предупреждение", "сбой",
|
|
174
|
+
"not responding", "has stopped", "dead", "kill", "terminate",
|
|
175
|
+
"не отвечает", "остановлен", "завершить",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
const textLower = (analysis.text || "").toLowerCase()
|
|
179
|
+
const detected = alertKeywords.filter(k => textLower.includes(k))
|
|
180
|
+
|
|
181
|
+
if (detected.length > 0) {
|
|
182
|
+
const alert = {
|
|
183
|
+
id: capture.id,
|
|
184
|
+
timestamp: ts,
|
|
185
|
+
type: detected.join(", "),
|
|
186
|
+
severity: detected.some(k => ["fatal", "crash", "exception", "фатальная", "краш", "исключение"].includes(k)) ? "critical" : "warning",
|
|
187
|
+
description: analysis.text?.slice(0, 500),
|
|
188
|
+
screenshot: imagePath,
|
|
189
|
+
}
|
|
190
|
+
capture.alerts.push(alert)
|
|
191
|
+
this.alerts.push(alert)
|
|
192
|
+
this.state.totalAlerts++
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
capture.analysis = { error: err.message }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.history.push(capture)
|
|
200
|
+
this.state.totalCaptures++
|
|
201
|
+
this.save()
|
|
202
|
+
|
|
203
|
+
// Clean old screenshots (keep last 50)
|
|
204
|
+
this.cleanOldScreenshots(50)
|
|
205
|
+
|
|
206
|
+
return { success: true, capture }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getRecentContext() {
|
|
210
|
+
const recent = this.history.slice(-5)
|
|
211
|
+
return recent.map(h => ({
|
|
212
|
+
time: h.timestamp,
|
|
213
|
+
analysis: h.analysis?.summary || h.analysis?.text?.slice(0, 200) || "no analysis",
|
|
214
|
+
alerts: h.alerts?.length || 0,
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
cleanOldScreenshots(keep = 50) {
|
|
219
|
+
try {
|
|
220
|
+
const files = readdirSync(SCREENSHOTS_DIR)
|
|
221
|
+
.filter(f => f.startsWith("screen_") && f.endsWith(".png"))
|
|
222
|
+
.sort()
|
|
223
|
+
|
|
224
|
+
if (files.length > keep) {
|
|
225
|
+
const toDelete = files.slice(0, files.length - keep)
|
|
226
|
+
for (const f of toDelete) {
|
|
227
|
+
try { unlinkSync(join(SCREENSHOTS_DIR, f)) } catch {}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
start(intervalMs, visionFn) {
|
|
234
|
+
if (this.state.running) {
|
|
235
|
+
return { success: false, error: "Monitor already running" }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.state.running = true
|
|
239
|
+
this.state.startedAt = new Date().toISOString()
|
|
240
|
+
this.state.interval = intervalMs || 2000
|
|
241
|
+
this.state.pid = process.pid
|
|
242
|
+
this.save()
|
|
243
|
+
|
|
244
|
+
this.intervalId = setInterval(async () => {
|
|
245
|
+
if (!this.state.running) {
|
|
246
|
+
this.stop()
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
await this.captureAndAnalyze(visionFn)
|
|
250
|
+
}, this.state.interval)
|
|
251
|
+
|
|
252
|
+
return { success: true, interval: this.state.interval }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
stop() {
|
|
256
|
+
if (this.intervalId) {
|
|
257
|
+
clearInterval(this.intervalId)
|
|
258
|
+
this.intervalId = null
|
|
259
|
+
}
|
|
260
|
+
this.state.running = false
|
|
261
|
+
this.state.pid = null
|
|
262
|
+
this.save()
|
|
263
|
+
return { success: true }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getStatus() {
|
|
267
|
+
const recentAlerts = this.alerts.slice(-10)
|
|
268
|
+
const recentCaptures = this.history.slice(-5)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
running: this.state.running,
|
|
272
|
+
startedAt: this.state.startedAt,
|
|
273
|
+
interval: this.state.interval,
|
|
274
|
+
totalCaptures: this.state.totalCaptures,
|
|
275
|
+
totalAlerts: this.state.totalAlerts,
|
|
276
|
+
pid: this.state.pid,
|
|
277
|
+
recentAlerts,
|
|
278
|
+
recentCaptures: recentCaptures.map(c => ({
|
|
279
|
+
time: c.timestamp,
|
|
280
|
+
size: c.sizeKB + "KB",
|
|
281
|
+
summary: c.analysis?.summary || c.analysis?.text?.slice(0, 100) || "analyzing...",
|
|
282
|
+
alertCount: c.alerts?.length || 0,
|
|
283
|
+
})),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getAlerts(since) {
|
|
288
|
+
if (since) {
|
|
289
|
+
return this.alerts.filter(a => new Date(a.timestamp) > new Date(since))
|
|
290
|
+
}
|
|
291
|
+
return this.alerts
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getScreenshotPath() {
|
|
295
|
+
const files = readdirSync(SCREENSHOTS_DIR)
|
|
296
|
+
.filter(f => f.startsWith("screen_") && f.endsWith(".png"))
|
|
297
|
+
.sort()
|
|
298
|
+
return files.length > 0 ? join(SCREENSHOTS_DIR, files[files.length - 1]) : null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
generateReport() {
|
|
302
|
+
const status = this.getStatus()
|
|
303
|
+
const alertsByType = {}
|
|
304
|
+
for (const a of this.alerts) {
|
|
305
|
+
alertsByType[a.type] = (alertsByType[a.type] || 0) + 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const report = `
|
|
309
|
+
═══════════════════════════════════════════
|
|
310
|
+
👁️ SCREEN MONITOR REPORT
|
|
311
|
+
═══════════════════════════════════════════
|
|
312
|
+
|
|
313
|
+
Status: ${status.running ? "🟢 RUNNING" : "🔴 STOPPED"}
|
|
314
|
+
Started: ${status.startedAt ? new Date(status.startedAt).toLocaleString() : "Never"}
|
|
315
|
+
Interval: every ${status.interval / 1000}s
|
|
316
|
+
Total captures: ${status.totalCaptures}
|
|
317
|
+
Total alerts: ${status.totalAlerts}
|
|
318
|
+
|
|
319
|
+
${Object.keys(alertsByType).length > 0 ? `
|
|
320
|
+
Alert breakdown:
|
|
321
|
+
${Object.entries(alertsByType).map(([type, count]) => ` ⚠️ ${type}: ${count}`).join("\n")}
|
|
322
|
+
` : "No alerts detected."}
|
|
323
|
+
|
|
324
|
+
Recent captures:
|
|
325
|
+
${status.recentCaptures.map(c => ` [${new Date(c.time).toLocaleTimeString()}] ${c.summary} ${c.alertCount > 0 ? `⚠️ ${c.alertCount} alerts` : "✅"}`).join("\n")}
|
|
326
|
+
|
|
327
|
+
═══════════════════════════════════════════
|
|
328
|
+
`.trim()
|
|
329
|
+
|
|
330
|
+
return report
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default ScreenMonitor
|
package/test_screen.png
ADDED
|
Binary file
|