mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-09-07 16:37:06 +08:00
add screenshot show
This commit is contained in:
@@ -1468,8 +1468,9 @@ export async function mainLoop (hooks) {
|
|||||||
height: 900 - 140,
|
height: 900 - 140,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
hooks.puppeteerLaunched?.call()
|
hooks.puppeteerLaunched?.call(browser)
|
||||||
page = (await browser.pages())[0]
|
page = (await browser.pages())[0]
|
||||||
|
hooks.pageGotten?.call(page)
|
||||||
//set cookies
|
//set cookies
|
||||||
hooks.cookieWillSet?.call(bossCookies)
|
hooks.cookieWillSet?.call(bossCookies)
|
||||||
for(let i = 0; i < bossCookies.length; i++){
|
for(let i = 0; i < bossCookies.length; i++){
|
||||||
|
|||||||
+23
-16
@@ -30,7 +30,7 @@ const path = require('path');
|
|||||||
const split2 = require('split2');
|
const split2 = require('split2');
|
||||||
|
|
||||||
const PORT = 12345;
|
const PORT = 12345;
|
||||||
const workers = new Map(); // workerId -> { process, status, restartCount, socket }
|
const workers = new Map(); // workerId -> { process, status, restartCount, socket, latestScreenshot, latestScreenshotAt }
|
||||||
const guiClients = new Set(); // GUI客户端连接集合
|
const guiClients = new Set(); // GUI客户端连接集合
|
||||||
const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件
|
const stoppedWorkers = new Set(); // 被用户主动停止的workerId集合,用于防止竞态条件
|
||||||
const pidToProcessInfoMap = new Map()
|
const pidToProcessInfoMap = new Map()
|
||||||
@@ -148,12 +148,11 @@ function handleMessage(socket, message) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具进程发送的消息(数据、心跳等)
|
// 工具进程发送的消息(数据、心跳、截图等)
|
||||||
if (message.type === 'worker-message' || message.type === 'worker-heartbeat' || message.type === 'worker-data') {
|
const workerId = message.workerId;
|
||||||
const workerId = message.workerId;
|
const workerInfo = workers.get(workerId);
|
||||||
const workerInfo = workers.get(workerId);
|
switch (message.type) {
|
||||||
|
case 'worker-message': {
|
||||||
if (workerInfo && workerInfo.socket === socket) {
|
|
||||||
// 转发工具进程消息到GUI客户端
|
// 转发工具进程消息到GUI客户端
|
||||||
broadcastToGUI({
|
broadcastToGUI({
|
||||||
type: 'worker-message',
|
type: 'worker-message',
|
||||||
@@ -161,15 +160,20 @@ function handleMessage(socket, message) {
|
|||||||
data: message.data || message,
|
data: message.data || message,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
break
|
||||||
// // 如果是心跳,更新最后心跳时间
|
}
|
||||||
// if (message.type === 'worker-heartbeat') {
|
case 'worker-screenshot': {
|
||||||
// workerInfo.lastHeartbeat = Date.now();
|
if (workerInfo && message.data && message.data.screenshot /* && workerInfo.socket === socket */) {
|
||||||
// }
|
// 如果携带截图信息,则在守护进程内缓存一份,供 get-status 使用
|
||||||
} else {
|
try {
|
||||||
sendResponse(socket, _callbackUuid, { error: '未注册的工具进程连接' });
|
workerInfo.latestScreenshot = message.data.screenshot;
|
||||||
|
workerInfo.latestScreenshotAt = Date.now();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('缓存 worker 截图信息失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GUI客户端的控制消息
|
// GUI客户端的控制消息
|
||||||
@@ -393,7 +397,10 @@ function getWorkersStatus() {
|
|||||||
// lastHeartbeat: workerInfo.lastHeartbeat,
|
// lastHeartbeat: workerInfo.lastHeartbeat,
|
||||||
command: workerInfo.command,
|
command: workerInfo.command,
|
||||||
args: workerInfo.args,
|
args: workerInfo.args,
|
||||||
pid: workerInfo.process?.pid
|
pid: workerInfo.process?.pid,
|
||||||
|
// 最新截图(通常是 data URL 或 base64 字符串),以及截图时间
|
||||||
|
screenshot: workerInfo.latestScreenshot ?? null,
|
||||||
|
screenshotAt: workerInfo.latestScreenshotAt ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ const main = async () => {
|
|||||||
}
|
}
|
||||||
const hooks = {
|
const hooks = {
|
||||||
daemonInitialized: new AsyncSeriesHook(),
|
daemonInitialized: new AsyncSeriesHook(),
|
||||||
puppeteerLaunched: new SyncHook(),
|
puppeteerLaunched: new SyncHook(['browser']),
|
||||||
|
pageGotten: new SyncHook(['page']),
|
||||||
pageLoaded: new SyncHook(),
|
pageLoaded: new SyncHook(),
|
||||||
cookieWillSet: new SyncHook(['cookies']),
|
cookieWillSet: new SyncHook(['cookies']),
|
||||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import SqlitePluginModule from '@geekgeekrun/sqlite-plugin'
|
|||||||
import gtag from '../../utils/gtag'
|
import gtag from '../../utils/gtag'
|
||||||
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
import GtagPlugin from '../../utils/gtag/GtagPlugin'
|
||||||
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||||
|
import { PeriodPushCurrentPageScreenshotPlugin } from '../../utils/screenshot'
|
||||||
const { default: SqlitePlugin } = SqlitePluginModule
|
const { default: SqlitePlugin } = SqlitePluginModule
|
||||||
|
|
||||||
const rerunInterval = (() => {
|
const rerunInterval = (() => {
|
||||||
@@ -34,6 +35,7 @@ const initPlugins = (hooks) => {
|
|||||||
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
|
new DingtalkPlugin(dingTalkAccessToken).apply(hooks)
|
||||||
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
|
new SqlitePlugin(getPublicDbFilePath()).apply(hooks)
|
||||||
new GtagPlugin().apply(hooks)
|
new GtagPlugin().apply(hooks)
|
||||||
|
new PeriodPushCurrentPageScreenshotPlugin().apply(hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkShouldExit () {
|
async function checkShouldExit () {
|
||||||
@@ -91,7 +93,8 @@ const runAutoChat = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hooks = {
|
const hooks = {
|
||||||
puppeteerLaunched: new SyncHook(),
|
puppeteerLaunched: new SyncHook(['browser']),
|
||||||
|
pageGotten: new SyncHook(['page']),
|
||||||
pageLoaded: new SyncHook(),
|
pageLoaded: new SyncHook(),
|
||||||
cookieWillSet: new SyncHook(['cookies']),
|
cookieWillSet: new SyncHook(['cookies']),
|
||||||
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
userInfoResponse: new AsyncSeriesHook(['userInfo']),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { JobHireStatus } from '@geekgeekrun/sqlite-plugin/dist/enums';
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import cheerio from 'cheerio'
|
import cheerio from 'cheerio'
|
||||||
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
import { connectToDaemon, sendToDaemon } from '../OPEN_SETTING_WINDOW/connect-to-daemon'
|
||||||
|
import { pushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS } from '../../utils/screenshot'
|
||||||
|
|
||||||
const throttleIntervalMinutes =
|
const throttleIntervalMinutes =
|
||||||
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
readConfigFile('boss.json').autoReminder?.throttleIntervalMinutes ?? 10
|
||||||
@@ -48,6 +49,16 @@ export const pageMapByName: {
|
|||||||
boss?: Page | null
|
boss?: Page | null
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
|
async function periodPushCurrentPageScreenshot () {
|
||||||
|
try {
|
||||||
|
await pushCurrentPageScreenshot(pageMapByName.boss)
|
||||||
|
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
periodPushCurrentPageScreenshot()
|
||||||
|
|
||||||
async function saveCurrentChatRecord(page) {
|
async function saveCurrentChatRecord(page) {
|
||||||
const userInfo = await page.evaluate(
|
const userInfo = await page.evaluate(
|
||||||
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
'document.querySelector(".main-wrap").__vue__.$store.state.userInfo'
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { sendToDaemon } from "../flow/OPEN_SETTING_WINDOW/connect-to-daemon"
|
||||||
|
|
||||||
|
export const SCREENSHOT_INTERVAL_MS = 2500
|
||||||
|
|
||||||
|
export async function pushCurrentPageScreenshot (page) {
|
||||||
|
try {
|
||||||
|
if (!page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 尝试截图当前页面(压缩为 jpeg + base64,避免文件写盘)
|
||||||
|
const screenshotBase64 = await page.screenshot({
|
||||||
|
type: 'jpeg',
|
||||||
|
quality: 60,
|
||||||
|
encoding: 'base64',
|
||||||
|
fullPage: false
|
||||||
|
})
|
||||||
|
const screenshotAt = Date.now()
|
||||||
|
await sendToDaemon({
|
||||||
|
type: 'worker-screenshot',
|
||||||
|
workerId: process.env.GEEKGEEKRUND_WORKER_ID,
|
||||||
|
data: {
|
||||||
|
screenshot: `data:image/jpeg;base64,${screenshotBase64}`,
|
||||||
|
screenshotAt,
|
||||||
|
pageUrl: page.url?.() ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
// 截图失败不应影响主流程
|
||||||
|
console.warn('[READ_NO_REPLY_AUTO_REMINDER] pushCurrentPageScreenshot error', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PeriodPushCurrentPageScreenshotPlugin {
|
||||||
|
apply(hooks) {
|
||||||
|
hooks.pageGotten.tap(
|
||||||
|
'PeriodPushCurrentPageScreenshotPlugin',
|
||||||
|
(page) => {
|
||||||
|
async function periodPushCurrentPageScreenshot () {
|
||||||
|
try {
|
||||||
|
if (!page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (page.isClosed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await pushCurrentPageScreenshot(page)
|
||||||
|
setTimeout(periodPushCurrentPageScreenshot, SCREENSHOT_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
periodPushCurrentPageScreenshot()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="task-item" flex>
|
<el-card class="task-item">
|
||||||
<div>
|
<div>
|
||||||
<img height="160" width="256" />
|
<div flex flex-col position-relative>
|
||||||
|
<div>
|
||||||
|
<el-button type="danger" size="small" @click="stopTask(task.workerId)">结束任务</el-button>
|
||||||
|
</div>
|
||||||
|
<img block :src="task.screenshot" height="190" width="360" />
|
||||||
|
<div position-absolute bottom-0 right-0 font-size-12px :style="{
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||||
|
color: '#fff',
|
||||||
|
padding: '2px 4px 2px 6px',
|
||||||
|
borderRadius: '8px 0 0 0'
|
||||||
|
}">{{ task.screenshotAt ? dayjs(task.screenshotAt).format('YYYY-MM-DD HH:mm:ss') : ' - ' }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ml-40px>
|
<div ml-30px>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>workerId</dt>
|
<dt>workerId</dt>
|
||||||
<dd>{{ task.workerId }}</dd>
|
<dd>{{ task.workerId }}</dd>
|
||||||
@@ -28,12 +39,12 @@
|
|||||||
<dt>PID</dt>
|
<dt>PID</dt>
|
||||||
<dd>{{ task.pid }}</dd>
|
<dd>{{ task.pid }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<el-button type="danger" @click="stopTask(task.workerId)">结束任务</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -50,8 +61,13 @@ const stopTask = async (workerId: string) => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.task-item {
|
.task-item {
|
||||||
|
width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
::v-deep(.el-card__body) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
dl {
|
dl {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -63,6 +79,7 @@ const stopTask = async (workerId: string) => {
|
|||||||
flex: 0 0 6em;
|
flex: 0 0 6em;
|
||||||
}
|
}
|
||||||
dd {
|
dd {
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user