commit 227cb96a1d1dd65f6888bed6caf307bf555d1d8c Author: v6ole Date: Thu Jun 18 15:01:01 2026 +0800 v1: three independent AI Monitor plugins (Codex + Claude + opencode) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea49ff7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +*.log +build/ +*.bak +debug.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..768d46f --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# StreamDock AI Monitor + +在 StreamDock N4 硬件外接屏上实时显示 AI Agent 工作流状态。 + +## 插件列表 + +| 插件 | UUID | 监控目标 | +|------|------|---------| +| AI Monitor - Codex Desktop | `com.streamdock.ai-monitor.codex` | `~/.codex/sessions/*.jsonl` | +| AI Monitor - Claude Desktop | `com.streamdock.ai-monitor.claude` | `~/.claude/projects/**/*.jsonl` | +| AI Monitor - opencode | `com.streamdock.ai-monitor.opencode` | `~/.opencode/sessions/*.jsonl` | + +## 安装 + +```powershell +$agents = @("codex", "claude", "opencode") +foreach ($a in $agents) { + Copy-Item "$a\com.streamdock.ai-monitor.$a.sdPlugin" "$env:APPDATA\HotSpot\StreamDock\plugins\com.streamdock.ai-monitor.$a.sdPlugin" -Recurse -Force +} +# 重启 StreamDock +``` diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/.gitignore b/claude/com.streamdock.ai-monitor.claude.sdPlugin/.gitignore new file mode 100644 index 0000000..ca5cc30 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +build/ \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/en.json b/claude/com.streamdock.ai-monitor.claude.sdPlugin/en.json new file mode 100644 index 0000000..57d72b0 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/en.json @@ -0,0 +1,4 @@ +{ + "Name": "Claude Desktop Monitor", + "Description": "Monitor Claude Desktop AI workflow status" +} \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/manifest.json b/claude/com.streamdock.ai-monitor.claude.sdPlugin/manifest.json new file mode 100644 index 0000000..793c7de --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/manifest.json @@ -0,0 +1,53 @@ +{ + "Actions": [ + { + "Icon": "static/img/claude-icon.svg", + "Name": "Claude Desktop", + "States": [ + { + "Image": "static/img/claude-icon.svg", + "TitleAlignment": "center", + "FontSize": "11" + } + ], + "Settings": { + "pollInterval": 1000 + }, + "Controllers": [ + "Keypad", + "Information" + ], + "UserTitleEnabled": true, + "SupportedInMultiActions": true, + "Tooltip": "Claude Desktop 工作流状态监控", + "UUID": "com.streamdock.ai-monitor.claude" + } + ], + "SDKVersion": 1, + "Author": "StreamDock", + "Name": "AI Monitor - Claude Desktop", + "Icon": "static/img/claude-icon.svg", + "Category": "AI Monitor", + "CategoryIcon": "static/img/claude-icon.svg", + "CodePathWin": "plugin/index.js", + "CodePathMac": "plugin/index.js", + "Description": "监控 Claude Desktop AI 工作流状态,在 N4 按钮上实时显示", + "Version": "1.0.0", + "URL": "http://10.10.10.14:18003/v6ole/streamdock-ai-monitor", + "OS": [ + { + "Platform": "windows", + "MinimumVersion": "7" + }, + { + "Platform": "mac", + "MinimumVersion": "10.11" + } + ], + "Software": { + "MinimumVersion": "3.10.188.226" + }, + "Nodejs": { + "Version": "20" + } +} \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/autofile.js b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/autofile.js new file mode 100644 index 0000000..38c45b6 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/autofile.js @@ -0,0 +1,47 @@ +const path = require('path'); +const fs = require('fs-extra'); + +console.log('开始执行自动化构建...'); + +const currentDir = __dirname; + +// 获取父文件夹的路径 +const parentDir = path.join(currentDir, '..'); +// 获取父文件夹的名称 +const PluginName = path.basename(parentDir); + + +const PluginPath = path.join(process.env.APPDATA, 'HotSpot/StreamDock/plugins', PluginName); + +try { + // 删除旧的插件目录 + fs.removeSync(PluginPath); + + // 确保目标目录存在 + fs.ensureDirSync(path.dirname(PluginPath)); + + // 复制当前目录到目标路径,排除 node_modules + fs.copySync(path.resolve(__dirname, '..'), PluginPath, { + filter: (src) => { + const relativePath = path.relative(path.resolve(__dirname, '..'), src); + // 排除 'node_modules' 和 '.git' 目录及其子文件 + return !relativePath.startsWith('plugin\\node_modules') + &&!relativePath.startsWith('plugin\\index.js') + &&!relativePath.startsWith('plugin\\package.json') + &&!relativePath.startsWith('plugin\\package-lock.json') + &&!relativePath.startsWith('plugin\\pnpm-lock.yaml') + &&!relativePath.startsWith('plugin\\yarn.lock') + &&!relativePath.startsWith('plugin\\build') + &&!relativePath.startsWith('plugin\\log') + &&!relativePath.startsWith('.git') + &&!relativePath.startsWith('.vscode'); + } + }); + + fs.copySync( path.join(__dirname, "build"), path.join(PluginPath,'plugin')) + + console.log(`插件 "${PluginName}" 已成功复制到 "${PluginPath}"`); + console.log('构建成功-------------'); +} catch (err) { + console.error(`复制出错 "${PluginName}":`, err); +} \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/index.js b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/index.js new file mode 100644 index 0000000..6f044b0 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/index.js @@ -0,0 +1,153 @@ +const { Plugins, Actions, log } = require('./utils/plugin'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const plugin = new Plugins('ai-monitor-claude'); +const timers = {}; +const sessionOffsets = {}; + +const HOME = os.homedir(); +const SCAN_DIR = path.join(HOME, '.claude', 'projects'); +const POLL_INTERVAL = 1000; +const IDLE_TIMEOUT_MS = 3000; + +// Load logo +var logoUri = ''; +try { + var logoPath = path.join(__dirname, '..', 'static', 'img', 'claude-icon.svg'); + logoUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(fs.readFileSync(logoPath, 'utf-8')); +} catch (_) {} + +var STATUS = { + idle: { title: 'Ready', fill: '#22c55e', blink: false, breathe: false }, + prompt: { title: 'Prompt', fill: '#3b82f6', blink: false, breathe: false }, + thinking: { title: 'Thinking', fill: '#f59e0b', blink: false, breathe: true }, + working: { title: 'Working', fill: '#f59e0b', blink: true, breathe: false }, + typing: { title: 'Typing', fill: '#f59e0b', blink: false, breathe: true }, + done: { title: 'Done', fill: '#22c55e', blink: true, breathe: false }, + error: { title: 'Error', fill: '#ef4444', blink: false, breathe: false }, +}; + +var PRIORITY = ['error', 'working', 'typing', 'thinking', 'prompt', 'done', 'idle']; +var currentCtx = null, currentState = 'idle', lastActivityTime = 0; +var animationHandle = null; + +function makeSvg(fill, label, blink, breathe) { + var anim = ''; + if (blink) anim = ''; + else if (breathe) anim = ''; + return '' + anim + '' + label + ''; +} + +function setDisplay(ctx, state) { + var d = STATUS[state] || STATUS['idle']; + plugin.setTitle(ctx, d.title); + if (state === 'idle' && logoUri) plugin.setImage(ctx, logoUri); + else plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); +} + +function animateTick() { + if (!currentCtx) return; + var d = STATUS[currentState] || STATUS['idle']; + if (d.blink || d.breathe) { + var now = Date.now(), period = d.blink ? 800 : 2000, phase = (now % period) / period; + var alpha = d.blink ? (phase < 0.5 ? 1 : 0.2) : 0.4 + 0.6 * Math.sin(phase * Math.PI); + var r = parseInt(d.fill.slice(1,3),16), g = parseInt(d.fill.slice(3,5),16), b = parseInt(d.fill.slice(5,7),16); + var color = 'rgb(' + Math.round(16+(r-16)*alpha) + ',' + Math.round(16+(g-16)*alpha) + ',' + Math.round(16+(b-16)*alpha) + ')'; + var svg = '' + d.title + ''; + plugin.setImage(currentCtx, 'data:image/svg+xml;charset=utf-8,' + svg); + } +} + +function setState(ctx, state) { + if (state === currentState) return; + if (state === 'done') { setDisplay(ctx, 'done'); currentState = 'done'; setTimeout(function() { if (currentState === 'done') { currentState = 'idle'; setDisplay(ctx, 'idle'); } }, 800); } + else { currentState = state; setDisplay(ctx, state); } +} + +function normalize(obj) { + var t = obj.type || ''; + if (t === 'user') return 'prompt'; + if (t === 'assistant') { + var content = (obj.message || {}).content || []; + for (var i = 0; i < content.length; i++) { + var ct = content[i].type || ''; + if (ct === 'tool_use') return 'working'; + if (ct === 'thinking') return 'thinking'; + } + for (var j = 0; j < content.length; j++) { + if ((content[j].type || '') === 'text') return 'typing'; + } + return 'typing'; + } + if (t === 'system') { if (obj.subtype === 'turn_duration') return 'done'; return null; } + return null; +} + +function findFiles(rootDir, suffix) { + var results = []; + if (!fs.existsSync(rootDir)) return results; + (function walk(dir, depth) { + if (depth > 16) return; + var names; try { names = fs.readdirSync(dir); } catch (_) { return; } + for (var i = 0; i < names.length; i++) { + var full = path.join(dir, names[i]); + var st; try { st = fs.statSync(full); } catch (_) { continue; } + if (st.isDirectory()) walk(full, depth + 1); + else if (st.isFile() && names[i].endsWith(suffix)) results.push(full); + } + })(rootDir, 0); + return results; +} + +function tailFile(filePath, ctx) { + var detected = false; + try { + var stat = fs.statSync(filePath); + var off = sessionOffsets[filePath] || 0; + if (stat.size < off) { sessionOffsets[filePath] = 0; return false; } + if (stat.size === off) return false; + var fd = fs.openSync(filePath, 'r'); + var grow = Math.min(stat.size - off, 65536); + var buf = Buffer.alloc(grow); + fs.readSync(fd, buf, 0, grow, off); + fs.closeSync(fd); + sessionOffsets[filePath] = stat.size; + var lines = buf.toString('utf-8').split('\n').filter(function(l) { return l.trim(); }); + var bestPri = PRIORITY.length, bestState = null; + for (var i = 0; i < lines.length; i++) { + try { var obj = JSON.parse(lines[i]); var s = normalize(obj); if (s) { var pri = PRIORITY.indexOf(s); if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } } } catch (_) {} + } + if (bestState) { setState(ctx, bestState); detected = true; } + } catch (_) { delete sessionOffsets[filePath]; } + return detected; +} + +function poll(ctx) { + var detected = false; + var files = findFiles(SCAN_DIR, '.jsonl'); + files.sort(function(a,b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); + for (var i = 0; i < Math.min(files.length, 3); i++) { if (tailFile(files[i], ctx)) detected = true; } + if (detected) lastActivityTime = Date.now(); + if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) { setState(ctx, 'idle'); } +} + +plugin.claude = new Actions({ + default: { pollInterval: POLL_INTERVAL }, + _willAppear: function(data) { + var ctx = data.context; + currentCtx = ctx; currentState = 'idle'; lastActivityTime = Date.now(); + setDisplay(ctx, 'idle'); + poll(ctx); + timers[ctx] = setInterval(function() { poll(ctx); }, POLL_INTERVAL); + if (!animationHandle) animationHandle = setInterval(animateTick, 200); + }, + _willDisappear: function(data) { + var ctx = data.context; + if (timers[ctx]) { clearInterval(timers[ctx]); delete timers[ctx]; } + if (currentCtx === ctx) { currentCtx = null; currentState = 'idle'; } + if (Object.keys(timers).length === 0 && animationHandle) { clearInterval(animationHandle); animationHandle = null; } + }, + keyUp: function(data) { poll(data.context); } +}); \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package-lock.json b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package-lock.json new file mode 100644 index 0000000..a1679d1 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package-lock.json @@ -0,0 +1,186 @@ +{ + "name": "ai-monitor-claude", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-monitor-claude", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package.json b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package.json new file mode 100644 index 0000000..122c141 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "ai-monitor-claude", + "version": "1.0.0", + "author": "StreamDock", + "main": "index.js", + "description": "Claude Desktop AI workflow status monitor", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } +} \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/utils/plugin.js b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/utils/plugin.js new file mode 100644 index 0000000..673de5c --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/plugin/utils/plugin.js @@ -0,0 +1,213 @@ +// 配置日志文件 +const now = new Date(); +const log = require('log4js').configure({ + appenders: { + file: { type: 'file', filename: `./log/${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}.log` } + }, + categories: { + default: { appenders: ['file'], level: 'info' } + } +}).getLogger(); + +//################################################## +//##################全局异常捕获##################### +process.on('uncaughtException', (error) => { + log.error('Uncaught Exception:', error); +}); +process.on('unhandledRejection', (reason) => { + log.error('Unhandled Rejection:', reason); +}); +//################################################## +//################################################## + + +// 插件类 +const ws = require('ws'); +class Plugins { + static language = JSON.parse(process.argv[9]).application.language; + static globalSettings = {}; + getGlobalSettingsFlag = true; + constructor() { + if (Plugins.instance) { + return Plugins.instance; + } + // log.info("process.argv", process.argv); + this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); + this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); + this.ws.on('close', process.exit); + this.ws.on('message', e => { + if (this.getGlobalSettingsFlag) { + // 只获取一次 + this.getGlobalSettingsFlag = false; + this.getGlobalSettings(); + } + const data = JSON.parse(e.toString()); + const action = data.action?.split('.').pop(); + this[action]?.[data.event]?.(data); + if (data.event === 'didReceiveGlobalSettings') { + Plugins.globalSettings = data.payload.settings; + } + this[data.event]?.(data); + }); + Plugins.instance = this; + } + + setGlobalSettings(payload) { + Plugins.globalSettings = payload; + this.ws.send(JSON.stringify({ + event: "setGlobalSettings", + context: process.argv[5], payload + })); + } + + getGlobalSettings() { + this.ws.send(JSON.stringify({ + event: "getGlobalSettings", + context: process.argv[5], + })); + } + // 设置标题 + setTitle(context, str, row = 0, num = 6) { + let newStr = null; + if (row && str) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.ws.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + '' + } + })); + } + // 设置背景 + setImage(context, url) { + this.ws.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })); + } + // 设置状态 + setState(context, state) { + this.ws.send(JSON.stringify({ + event: "setState", + context, payload: { state } + })); + } + // 保存持久化数据 + setSettings(context, payload) { + this.ws.send(JSON.stringify({ + event: "setSettings", + context, payload + })); + } + + // 在按键上展示警告 + showAlert(context) { + this.ws.send(JSON.stringify({ + event: "showAlert", + context + })); + } + + // 在按键上展示成功 + showOk(context) { + this.ws.send(JSON.stringify({ + event: "showOk", + context + })); + } + // 发送给属性检测器 + sendToPropertyInspector(payload) { + this.ws.send(JSON.stringify({ + action: Actions.currentAction, + context: Actions.currentContext, + payload, event: "sendToPropertyInspector" + })); + } + // 用默认浏览器打开网页 + openUrl(url) { + this.ws.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); + } +}; + +// 操作类 +class Actions { + constructor(data) { + this.data = {}; + this.default = {}; + Object.assign(this, data); + } + // 属性检查器显示时 + static currentAction = null; + static currentContext = null; + static actions = {}; + propertyInspectorDidAppear(data) { + Actions.currentAction = data.action; + Actions.currentContext = data.context; + this._propertyInspectorDidAppear?.(data); + } + // 初始化数据 + willAppear(data) { + Plugins.globalContext = data.context; + Actions.actions[data.context] = data.action + const { context, payload: { settings } } = data; + this.data[context] = Object.assign({ ...this.default }, settings); + this._willAppear?.(data); + } + + didReceiveSettings(data) { + this.data[data.context] = data.payload.settings; + this._didReceiveSettings?.(data); + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data); + delete this.data[data.context]; + } +} + +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + +module.exports = { + log, + Plugins, + Actions, + EventEmitter +}; \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/static/img/claude-icon.svg b/claude/com.streamdock.ai-monitor.claude.sdPlugin/static/img/claude-icon.svg new file mode 100644 index 0000000..5d8d746 --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/static/img/claude-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/claude/com.streamdock.ai-monitor.claude.sdPlugin/zh_CN.json b/claude/com.streamdock.ai-monitor.claude.sdPlugin/zh_CN.json new file mode 100644 index 0000000..f63422c --- /dev/null +++ b/claude/com.streamdock.ai-monitor.claude.sdPlugin/zh_CN.json @@ -0,0 +1,4 @@ +{ + "Name": "Claude Desktop 监控", + "Description": "监控 Claude Desktop AI 工作流状态" +} \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/.gitignore b/codex/com.streamdock.ai-monitor.codex.sdPlugin/.gitignore new file mode 100644 index 0000000..ca5cc30 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +build/ \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/en.json b/codex/com.streamdock.ai-monitor.codex.sdPlugin/en.json new file mode 100644 index 0000000..6d220c6 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/en.json @@ -0,0 +1,4 @@ +{ + "Name": "Codex Desktop Monitor", + "Description": "Monitor Codex Desktop AI workflow status" +} \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/manifest.json b/codex/com.streamdock.ai-monitor.codex.sdPlugin/manifest.json new file mode 100644 index 0000000..79526e9 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/manifest.json @@ -0,0 +1,53 @@ +{ + "Actions": [ + { + "Icon": "static/img/codex-icon.svg", + "Name": "Codex Desktop", + "States": [ + { + "Image": "static/img/codex-icon.svg", + "TitleAlignment": "center", + "FontSize": "11" + } + ], + "Settings": { + "pollInterval": 1000 + }, + "Controllers": [ + "Keypad", + "Information" + ], + "UserTitleEnabled": true, + "SupportedInMultiActions": true, + "Tooltip": "Codex Desktop 工作流状态监控", + "UUID": "com.streamdock.ai-monitor.codex" + } + ], + "SDKVersion": 1, + "Author": "StreamDock", + "Name": "AI Monitor - Codex Desktop", + "Icon": "static/img/codex-icon.svg", + "Category": "AI Monitor", + "CategoryIcon": "static/img/codex-icon.svg", + "CodePathWin": "plugin/index.js", + "CodePathMac": "plugin/index.js", + "Description": "监控 Codex Desktop AI 工作流状态,在 N4 按钮上实时显示", + "Version": "1.0.0", + "URL": "http://10.10.10.14:18003/v6ole/streamdock-ai-monitor", + "OS": [ + { + "Platform": "windows", + "MinimumVersion": "7" + }, + { + "Platform": "mac", + "MinimumVersion": "10.11" + } + ], + "Software": { + "MinimumVersion": "3.10.188.226" + }, + "Nodejs": { + "Version": "20" + } +} \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/autofile.js b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/autofile.js new file mode 100644 index 0000000..38c45b6 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/autofile.js @@ -0,0 +1,47 @@ +const path = require('path'); +const fs = require('fs-extra'); + +console.log('开始执行自动化构建...'); + +const currentDir = __dirname; + +// 获取父文件夹的路径 +const parentDir = path.join(currentDir, '..'); +// 获取父文件夹的名称 +const PluginName = path.basename(parentDir); + + +const PluginPath = path.join(process.env.APPDATA, 'HotSpot/StreamDock/plugins', PluginName); + +try { + // 删除旧的插件目录 + fs.removeSync(PluginPath); + + // 确保目标目录存在 + fs.ensureDirSync(path.dirname(PluginPath)); + + // 复制当前目录到目标路径,排除 node_modules + fs.copySync(path.resolve(__dirname, '..'), PluginPath, { + filter: (src) => { + const relativePath = path.relative(path.resolve(__dirname, '..'), src); + // 排除 'node_modules' 和 '.git' 目录及其子文件 + return !relativePath.startsWith('plugin\\node_modules') + &&!relativePath.startsWith('plugin\\index.js') + &&!relativePath.startsWith('plugin\\package.json') + &&!relativePath.startsWith('plugin\\package-lock.json') + &&!relativePath.startsWith('plugin\\pnpm-lock.yaml') + &&!relativePath.startsWith('plugin\\yarn.lock') + &&!relativePath.startsWith('plugin\\build') + &&!relativePath.startsWith('plugin\\log') + &&!relativePath.startsWith('.git') + &&!relativePath.startsWith('.vscode'); + } + }); + + fs.copySync( path.join(__dirname, "build"), path.join(PluginPath,'plugin')) + + console.log(`插件 "${PluginName}" 已成功复制到 "${PluginPath}"`); + console.log('构建成功-------------'); +} catch (err) { + console.error(`复制出错 "${PluginName}":`, err); +} \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/index.js b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/index.js new file mode 100644 index 0000000..8b168cf --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/index.js @@ -0,0 +1,159 @@ +const { Plugins, Actions, log } = require('./utils/plugin'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const plugin = new Plugins('ai-monitor-codex'); +const timers = {}; +const sessionOffsets = {}; + +const HOME = os.homedir(); +const SCAN_DIR = path.join(HOME, '.codex', 'sessions'); +const POLL_INTERVAL = 1000; +const IDLE_TIMEOUT_MS = 3000; + +// Load logo +var logoUri = ''; +try { + var logoPath = path.join(__dirname, '..', 'static', 'img', 'codex-icon.svg'); + logoUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(fs.readFileSync(logoPath, 'utf-8')); +} catch (_) {} + +var STATUS = { + idle: { title: 'Ready', fill: '#22c55e', blink: false, breathe: false }, + prompt: { title: 'Prompt', fill: '#3b82f6', blink: false, breathe: false }, + thinking: { title: 'Thinking', fill: '#f59e0b', blink: false, breathe: true }, + working: { title: 'Working', fill: '#f59e0b', blink: true, breathe: false }, + typing: { title: 'Typing', fill: '#f59e0b', blink: false, breathe: true }, + done: { title: 'Done', fill: '#22c55e', blink: true, breathe: false }, + error: { title: 'Error', fill: '#ef4444', blink: false, breathe: false }, +}; + +var PRIORITY = ['error', 'working', 'typing', 'thinking', 'prompt', 'done', 'idle']; +var currentCtx = null, currentState = 'idle', lastActivityTime = 0; +var animationHandle = null; + +function makeSvg(fill, label, blink, breathe) { + var anim = ''; + if (blink) anim = ''; + else if (breathe) anim = ''; + return '' + anim + '' + label + ''; +} + +function setDisplay(ctx, state) { + var d = STATUS[state] || STATUS['idle']; + plugin.setTitle(ctx, d.title); + if (state === 'idle' && logoUri) plugin.setImage(ctx, logoUri); + else plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); +} + +function animateTick() { + if (!currentCtx) return; + var d = STATUS[currentState] || STATUS['idle']; + if (d.blink || d.breathe) { + var now = Date.now(), period = d.blink ? 800 : 2000, phase = (now % period) / period; + var alpha = d.blink ? (phase < 0.5 ? 1 : 0.2) : 0.4 + 0.6 * Math.sin(phase * Math.PI); + var r = parseInt(d.fill.slice(1,3),16), g = parseInt(d.fill.slice(3,5),16), b = parseInt(d.fill.slice(5,7),16); + var color = 'rgb(' + Math.round(16+(r-16)*alpha) + ',' + Math.round(16+(g-16)*alpha) + ',' + Math.round(16+(b-16)*alpha) + ')'; + var svg = '' + d.title + ''; + plugin.setImage(currentCtx, 'data:image/svg+xml;charset=utf-8,' + svg); + } +} + +function setState(ctx, state) { + if (state === currentState) return; + if (state === 'done') { setDisplay(ctx, 'done'); currentState = 'done'; setTimeout(function() { if (currentState === 'done') { currentState = 'idle'; setDisplay(ctx, 'idle'); } }, 800); } + else { currentState = state; setDisplay(ctx, state); } +} + +function normalize(obj) { + var t = obj.type || ''; + var pt = (obj.payload || {}).type || ''; + var it = ((obj.payload || {}).item || {}).type || ''; + if (t === 'event_msg') { + if (pt === 'task_started') return 'thinking'; + if (pt === 'task_complete') return 'done'; + if (pt === 'turn_aborted') return 'error'; + if (pt === 'agent_message') return 'typing'; + if (pt === 'user_message') return 'prompt'; + if (pt === 'web_search_end') return 'working'; + if (pt === 'agent_reasoning') return 'thinking'; + return null; + } + if (t === 'response_item') { + var nt = pt || it; + if (nt === 'function_call' || nt === 'custom_tool_call' || nt === 'function_call_output' || nt === 'custom_tool_call_output') return 'working'; + if (nt === 'web_search_call') return 'working'; + if (nt === 'reasoning') return 'thinking'; + if (nt === 'message') return 'typing'; + return null; + } + return null; +} + +function findFiles(rootDir, suffix) { + var results = []; + if (!fs.existsSync(rootDir)) return results; + (function walk(dir, depth) { + if (depth > 16) return; + var names; try { names = fs.readdirSync(dir); } catch (_) { return; } + for (var i = 0; i < names.length; i++) { + var full = path.join(dir, names[i]); + var st; try { st = fs.statSync(full); } catch (_) { continue; } + if (st.isDirectory()) walk(full, depth + 1); + else if (st.isFile() && names[i].endsWith(suffix)) results.push(full); + } + })(rootDir, 0); + return results; +} + +function tailFile(filePath, ctx) { + var detected = false; + try { + var stat = fs.statSync(filePath); + var off = sessionOffsets[filePath] || 0; + if (stat.size < off) { sessionOffsets[filePath] = 0; return false; } + if (stat.size === off) return false; + var fd = fs.openSync(filePath, 'r'); + var grow = Math.min(stat.size - off, 65536); + var buf = Buffer.alloc(grow); + fs.readSync(fd, buf, 0, grow, off); + fs.closeSync(fd); + sessionOffsets[filePath] = stat.size; + var lines = buf.toString('utf-8').split('\n').filter(function(l) { return l.trim(); }); + var bestPri = PRIORITY.length, bestState = null; + for (var i = 0; i < lines.length; i++) { + try { var obj = JSON.parse(lines[i]); var s = normalize(obj); if (s) { var pri = PRIORITY.indexOf(s); if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } } } catch (_) {} + } + if (bestState) { setState(ctx, bestState); detected = true; } + } catch (_) { delete sessionOffsets[filePath]; } + return detected; +} + +function poll(ctx) { + var detected = false; + var files = findFiles(SCAN_DIR, '.jsonl'); + files.sort(function(a,b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); + for (var i = 0; i < Math.min(files.length, 3); i++) { if (tailFile(files[i], ctx)) detected = true; } + if (detected) lastActivityTime = Date.now(); + if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) { setState(ctx, 'idle'); } +} + +plugin.codex = new Actions({ + default: { pollInterval: POLL_INTERVAL }, + _willAppear: function(data) { + var ctx = data.context; + currentCtx = ctx; currentState = 'idle'; lastActivityTime = Date.now(); + setDisplay(ctx, 'idle'); + poll(ctx); + timers[ctx] = setInterval(function() { poll(ctx); }, POLL_INTERVAL); + if (!animationHandle) animationHandle = setInterval(animateTick, 200); + }, + _willDisappear: function(data) { + var ctx = data.context; + if (timers[ctx]) { clearInterval(timers[ctx]); delete timers[ctx]; } + if (currentCtx === ctx) { currentCtx = null; currentState = 'idle'; } + if (Object.keys(timers).length === 0 && animationHandle) { clearInterval(animationHandle); animationHandle = null; } + }, + keyUp: function(data) { poll(data.context); } +}); \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package-lock.json b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package-lock.json new file mode 100644 index 0000000..882a5a9 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package-lock.json @@ -0,0 +1,186 @@ +{ + "name": "ai-monitor-codex", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-monitor-codex", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package.json b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package.json new file mode 100644 index 0000000..0853b88 --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "ai-monitor-codex", + "version": "1.0.0", + "author": "StreamDock", + "main": "index.js", + "description": "Codex Desktop AI workflow status monitor", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } +} \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/utils/plugin.js b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/utils/plugin.js new file mode 100644 index 0000000..673de5c --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/plugin/utils/plugin.js @@ -0,0 +1,213 @@ +// 配置日志文件 +const now = new Date(); +const log = require('log4js').configure({ + appenders: { + file: { type: 'file', filename: `./log/${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}.log` } + }, + categories: { + default: { appenders: ['file'], level: 'info' } + } +}).getLogger(); + +//################################################## +//##################全局异常捕获##################### +process.on('uncaughtException', (error) => { + log.error('Uncaught Exception:', error); +}); +process.on('unhandledRejection', (reason) => { + log.error('Unhandled Rejection:', reason); +}); +//################################################## +//################################################## + + +// 插件类 +const ws = require('ws'); +class Plugins { + static language = JSON.parse(process.argv[9]).application.language; + static globalSettings = {}; + getGlobalSettingsFlag = true; + constructor() { + if (Plugins.instance) { + return Plugins.instance; + } + // log.info("process.argv", process.argv); + this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); + this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); + this.ws.on('close', process.exit); + this.ws.on('message', e => { + if (this.getGlobalSettingsFlag) { + // 只获取一次 + this.getGlobalSettingsFlag = false; + this.getGlobalSettings(); + } + const data = JSON.parse(e.toString()); + const action = data.action?.split('.').pop(); + this[action]?.[data.event]?.(data); + if (data.event === 'didReceiveGlobalSettings') { + Plugins.globalSettings = data.payload.settings; + } + this[data.event]?.(data); + }); + Plugins.instance = this; + } + + setGlobalSettings(payload) { + Plugins.globalSettings = payload; + this.ws.send(JSON.stringify({ + event: "setGlobalSettings", + context: process.argv[5], payload + })); + } + + getGlobalSettings() { + this.ws.send(JSON.stringify({ + event: "getGlobalSettings", + context: process.argv[5], + })); + } + // 设置标题 + setTitle(context, str, row = 0, num = 6) { + let newStr = null; + if (row && str) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.ws.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + '' + } + })); + } + // 设置背景 + setImage(context, url) { + this.ws.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })); + } + // 设置状态 + setState(context, state) { + this.ws.send(JSON.stringify({ + event: "setState", + context, payload: { state } + })); + } + // 保存持久化数据 + setSettings(context, payload) { + this.ws.send(JSON.stringify({ + event: "setSettings", + context, payload + })); + } + + // 在按键上展示警告 + showAlert(context) { + this.ws.send(JSON.stringify({ + event: "showAlert", + context + })); + } + + // 在按键上展示成功 + showOk(context) { + this.ws.send(JSON.stringify({ + event: "showOk", + context + })); + } + // 发送给属性检测器 + sendToPropertyInspector(payload) { + this.ws.send(JSON.stringify({ + action: Actions.currentAction, + context: Actions.currentContext, + payload, event: "sendToPropertyInspector" + })); + } + // 用默认浏览器打开网页 + openUrl(url) { + this.ws.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); + } +}; + +// 操作类 +class Actions { + constructor(data) { + this.data = {}; + this.default = {}; + Object.assign(this, data); + } + // 属性检查器显示时 + static currentAction = null; + static currentContext = null; + static actions = {}; + propertyInspectorDidAppear(data) { + Actions.currentAction = data.action; + Actions.currentContext = data.context; + this._propertyInspectorDidAppear?.(data); + } + // 初始化数据 + willAppear(data) { + Plugins.globalContext = data.context; + Actions.actions[data.context] = data.action + const { context, payload: { settings } } = data; + this.data[context] = Object.assign({ ...this.default }, settings); + this._willAppear?.(data); + } + + didReceiveSettings(data) { + this.data[data.context] = data.payload.settings; + this._didReceiveSettings?.(data); + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data); + delete this.data[data.context]; + } +} + +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + +module.exports = { + log, + Plugins, + Actions, + EventEmitter +}; \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/static/img/codex-icon.svg b/codex/com.streamdock.ai-monitor.codex.sdPlugin/static/img/codex-icon.svg new file mode 100644 index 0000000..c77ccfd --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/static/img/codex-icon.svg @@ -0,0 +1 @@ +Codex \ No newline at end of file diff --git a/codex/com.streamdock.ai-monitor.codex.sdPlugin/zh_CN.json b/codex/com.streamdock.ai-monitor.codex.sdPlugin/zh_CN.json new file mode 100644 index 0000000..298279d --- /dev/null +++ b/codex/com.streamdock.ai-monitor.codex.sdPlugin/zh_CN.json @@ -0,0 +1,4 @@ +{ + "Name": "Codex Desktop 监控", + "Description": "监控 Codex Desktop AI 工作流状态" +} \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/.gitignore b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/.gitignore new file mode 100644 index 0000000..ca5cc30 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +build/ \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/en.json b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/en.json new file mode 100644 index 0000000..274858d --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/en.json @@ -0,0 +1,4 @@ +{ + "Name": "opencode Monitor", + "Description": "Monitor opencode AI workflow status" +} \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/manifest.json b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/manifest.json new file mode 100644 index 0000000..8ccc5ec --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/manifest.json @@ -0,0 +1,53 @@ +{ + "Actions": [ + { + "Icon": "static/img/opencode-icon.svg", + "Name": "opencode", + "States": [ + { + "Image": "static/img/opencode-icon.svg", + "TitleAlignment": "center", + "FontSize": "11" + } + ], + "Settings": { + "pollInterval": 1000 + }, + "Controllers": [ + "Keypad", + "Information" + ], + "UserTitleEnabled": true, + "SupportedInMultiActions": true, + "Tooltip": "opencode 工作流状态监控", + "UUID": "com.streamdock.ai-monitor.opencode" + } + ], + "SDKVersion": 1, + "Author": "StreamDock", + "Name": "AI Monitor - opencode", + "Icon": "static/img/opencode-icon.svg", + "Category": "AI Monitor", + "CategoryIcon": "static/img/opencode-icon.svg", + "CodePathWin": "plugin/index.js", + "CodePathMac": "plugin/index.js", + "Description": "监控 opencode AI 工作流状态,在 N4 按钮上实时显示", + "Version": "1.0.0", + "URL": "http://10.10.10.14:18003/v6ole/streamdock-ai-monitor", + "OS": [ + { + "Platform": "windows", + "MinimumVersion": "7" + }, + { + "Platform": "mac", + "MinimumVersion": "10.11" + } + ], + "Software": { + "MinimumVersion": "3.10.188.226" + }, + "Nodejs": { + "Version": "20" + } +} \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/autofile.js b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/autofile.js new file mode 100644 index 0000000..38c45b6 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/autofile.js @@ -0,0 +1,47 @@ +const path = require('path'); +const fs = require('fs-extra'); + +console.log('开始执行自动化构建...'); + +const currentDir = __dirname; + +// 获取父文件夹的路径 +const parentDir = path.join(currentDir, '..'); +// 获取父文件夹的名称 +const PluginName = path.basename(parentDir); + + +const PluginPath = path.join(process.env.APPDATA, 'HotSpot/StreamDock/plugins', PluginName); + +try { + // 删除旧的插件目录 + fs.removeSync(PluginPath); + + // 确保目标目录存在 + fs.ensureDirSync(path.dirname(PluginPath)); + + // 复制当前目录到目标路径,排除 node_modules + fs.copySync(path.resolve(__dirname, '..'), PluginPath, { + filter: (src) => { + const relativePath = path.relative(path.resolve(__dirname, '..'), src); + // 排除 'node_modules' 和 '.git' 目录及其子文件 + return !relativePath.startsWith('plugin\\node_modules') + &&!relativePath.startsWith('plugin\\index.js') + &&!relativePath.startsWith('plugin\\package.json') + &&!relativePath.startsWith('plugin\\package-lock.json') + &&!relativePath.startsWith('plugin\\pnpm-lock.yaml') + &&!relativePath.startsWith('plugin\\yarn.lock') + &&!relativePath.startsWith('plugin\\build') + &&!relativePath.startsWith('plugin\\log') + &&!relativePath.startsWith('.git') + &&!relativePath.startsWith('.vscode'); + } + }); + + fs.copySync( path.join(__dirname, "build"), path.join(PluginPath,'plugin')) + + console.log(`插件 "${PluginName}" 已成功复制到 "${PluginPath}"`); + console.log('构建成功-------------'); +} catch (err) { + console.error(`复制出错 "${PluginName}":`, err); +} \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/index.js b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/index.js new file mode 100644 index 0000000..7f2b554 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/index.js @@ -0,0 +1,145 @@ +const { Plugins, Actions, log } = require('./utils/plugin'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const plugin = new Plugins('ai-monitor-opencode'); +const timers = {}; +const sessionOffsets = {}; + +const HOME = os.homedir(); +const SCAN_DIR = path.join(HOME, '.opencode', 'sessions'); +const POLL_INTERVAL = 1000; +const IDLE_TIMEOUT_MS = 3000; + +// Load logo +var logoUri = ''; +try { + var logoPath = path.join(__dirname, '..', 'static', 'img', 'opencode-icon.svg'); + logoUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(fs.readFileSync(logoPath, 'utf-8')); +} catch (_) {} + +var STATUS = { + idle: { title: 'Ready', fill: '#22c55e', blink: false, breathe: false }, + prompt: { title: 'Prompt', fill: '#3b82f6', blink: false, breathe: false }, + thinking: { title: 'Thinking', fill: '#f59e0b', blink: false, breathe: true }, + working: { title: 'Working', fill: '#f59e0b', blink: true, breathe: false }, + typing: { title: 'Typing', fill: '#f59e0b', blink: false, breathe: true }, + done: { title: 'Done', fill: '#22c55e', blink: true, breathe: false }, + error: { title: 'Error', fill: '#ef4444', blink: false, breathe: false }, +}; + +var PRIORITY = ['error', 'working', 'typing', 'thinking', 'prompt', 'done', 'idle']; +var currentCtx = null, currentState = 'idle', lastActivityTime = 0; +var animationHandle = null; + +function makeSvg(fill, label, blink, breathe) { + var anim = ''; + if (blink) anim = ''; + else if (breathe) anim = ''; + return '' + anim + '' + label + ''; +} + +function setDisplay(ctx, state) { + var d = STATUS[state] || STATUS['idle']; + plugin.setTitle(ctx, d.title); + if (state === 'idle' && logoUri) plugin.setImage(ctx, logoUri); + else plugin.setImage(ctx, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe)); +} + +function animateTick() { + if (!currentCtx) return; + var d = STATUS[currentState] || STATUS['idle']; + if (d.blink || d.breathe) { + var now = Date.now(), period = d.blink ? 800 : 2000, phase = (now % period) / period; + var alpha = d.blink ? (phase < 0.5 ? 1 : 0.2) : 0.4 + 0.6 * Math.sin(phase * Math.PI); + var r = parseInt(d.fill.slice(1,3),16), g = parseInt(d.fill.slice(3,5),16), b = parseInt(d.fill.slice(5,7),16); + var color = 'rgb(' + Math.round(16+(r-16)*alpha) + ',' + Math.round(16+(g-16)*alpha) + ',' + Math.round(16+(b-16)*alpha) + ')'; + var svg = '' + d.title + ''; + plugin.setImage(currentCtx, 'data:image/svg+xml;charset=utf-8,' + svg); + } +} + +function setState(ctx, state) { + if (state === currentState) return; + if (state === 'done') { setDisplay(ctx, 'done'); currentState = 'done'; setTimeout(function() { if (currentState === 'done') { currentState = 'idle'; setDisplay(ctx, 'idle'); } }, 800); } + else { currentState = state; setDisplay(ctx, state); } +} + +function normalize(obj) { + var t = obj.type || ''; + if (t === 'user') return 'prompt'; + if (t === 'assistant') return 'typing'; + if (t === 'tool' || t === 'tool_call') return 'working'; + if (t === 'thinking' || t === 'reasoning') return 'thinking'; + if (t === 'done' || t === 'complete') return 'done'; + if (t === 'error') return 'error'; + return null; +} + +function findFiles(rootDir, suffix) { + var results = []; + if (!fs.existsSync(rootDir)) return results; + (function walk(dir, depth) { + if (depth > 16) return; + var names; try { names = fs.readdirSync(dir); } catch (_) { return; } + for (var i = 0; i < names.length; i++) { + var full = path.join(dir, names[i]); + var st; try { st = fs.statSync(full); } catch (_) { continue; } + if (st.isDirectory()) walk(full, depth + 1); + else if (st.isFile() && names[i].endsWith(suffix)) results.push(full); + } + })(rootDir, 0); + return results; +} + +function tailFile(filePath, ctx) { + var detected = false; + try { + var stat = fs.statSync(filePath); + var off = sessionOffsets[filePath] || 0; + if (stat.size < off) { sessionOffsets[filePath] = 0; return false; } + if (stat.size === off) return false; + var fd = fs.openSync(filePath, 'r'); + var grow = Math.min(stat.size - off, 65536); + var buf = Buffer.alloc(grow); + fs.readSync(fd, buf, 0, grow, off); + fs.closeSync(fd); + sessionOffsets[filePath] = stat.size; + var lines = buf.toString('utf-8').split('\n').filter(function(l) { return l.trim(); }); + var bestPri = PRIORITY.length, bestState = null; + for (var i = 0; i < lines.length; i++) { + try { var obj = JSON.parse(lines[i]); var s = normalize(obj); if (s) { var pri = PRIORITY.indexOf(s); if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; } } } catch (_) {} + } + if (bestState) { setState(ctx, bestState); detected = true; } + } catch (_) { delete sessionOffsets[filePath]; } + return detected; +} + +function poll(ctx) { + var detected = false; + var files = findFiles(SCAN_DIR, '.jsonl'); + files.sort(function(a,b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; }); + for (var i = 0; i < Math.min(files.length, 3); i++) { if (tailFile(files[i], ctx)) detected = true; } + if (detected) lastActivityTime = Date.now(); + if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) { setState(ctx, 'idle'); } +} + +plugin.opencode = new Actions({ + default: { pollInterval: POLL_INTERVAL }, + _willAppear: function(data) { + var ctx = data.context; + currentCtx = ctx; currentState = 'idle'; lastActivityTime = Date.now(); + setDisplay(ctx, 'idle'); + poll(ctx); + timers[ctx] = setInterval(function() { poll(ctx); }, POLL_INTERVAL); + if (!animationHandle) animationHandle = setInterval(animateTick, 200); + }, + _willDisappear: function(data) { + var ctx = data.context; + if (timers[ctx]) { clearInterval(timers[ctx]); delete timers[ctx]; } + if (currentCtx === ctx) { currentCtx = null; currentState = 'idle'; } + if (Object.keys(timers).length === 0 && animationHandle) { clearInterval(animationHandle); animationHandle = null; } + }, + keyUp: function(data) { poll(data.context); } +}); \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package-lock.json b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package-lock.json new file mode 100644 index 0000000..b58c86f --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package-lock.json @@ -0,0 +1,186 @@ +{ + "name": "ai-monitor-opencode", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-monitor-opencode", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package.json b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package.json new file mode 100644 index 0000000..0f022e0 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "ai-monitor-opencode", + "version": "1.0.0", + "author": "StreamDock", + "main": "index.js", + "description": "opencode AI workflow status monitor", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.3.0", + "log4js": "^6.9.1", + "ws": "^8.14.2" + } +} \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/utils/plugin.js b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/utils/plugin.js new file mode 100644 index 0000000..673de5c --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/plugin/utils/plugin.js @@ -0,0 +1,213 @@ +// 配置日志文件 +const now = new Date(); +const log = require('log4js').configure({ + appenders: { + file: { type: 'file', filename: `./log/${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}.log` } + }, + categories: { + default: { appenders: ['file'], level: 'info' } + } +}).getLogger(); + +//################################################## +//##################全局异常捕获##################### +process.on('uncaughtException', (error) => { + log.error('Uncaught Exception:', error); +}); +process.on('unhandledRejection', (reason) => { + log.error('Unhandled Rejection:', reason); +}); +//################################################## +//################################################## + + +// 插件类 +const ws = require('ws'); +class Plugins { + static language = JSON.parse(process.argv[9]).application.language; + static globalSettings = {}; + getGlobalSettingsFlag = true; + constructor() { + if (Plugins.instance) { + return Plugins.instance; + } + // log.info("process.argv", process.argv); + this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); + this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); + this.ws.on('close', process.exit); + this.ws.on('message', e => { + if (this.getGlobalSettingsFlag) { + // 只获取一次 + this.getGlobalSettingsFlag = false; + this.getGlobalSettings(); + } + const data = JSON.parse(e.toString()); + const action = data.action?.split('.').pop(); + this[action]?.[data.event]?.(data); + if (data.event === 'didReceiveGlobalSettings') { + Plugins.globalSettings = data.payload.settings; + } + this[data.event]?.(data); + }); + Plugins.instance = this; + } + + setGlobalSettings(payload) { + Plugins.globalSettings = payload; + this.ws.send(JSON.stringify({ + event: "setGlobalSettings", + context: process.argv[5], payload + })); + } + + getGlobalSettings() { + this.ws.send(JSON.stringify({ + event: "getGlobalSettings", + context: process.argv[5], + })); + } + // 设置标题 + setTitle(context, str, row = 0, num = 6) { + let newStr = null; + if (row && str) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.ws.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + '' + } + })); + } + // 设置背景 + setImage(context, url) { + this.ws.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })); + } + // 设置状态 + setState(context, state) { + this.ws.send(JSON.stringify({ + event: "setState", + context, payload: { state } + })); + } + // 保存持久化数据 + setSettings(context, payload) { + this.ws.send(JSON.stringify({ + event: "setSettings", + context, payload + })); + } + + // 在按键上展示警告 + showAlert(context) { + this.ws.send(JSON.stringify({ + event: "showAlert", + context + })); + } + + // 在按键上展示成功 + showOk(context) { + this.ws.send(JSON.stringify({ + event: "showOk", + context + })); + } + // 发送给属性检测器 + sendToPropertyInspector(payload) { + this.ws.send(JSON.stringify({ + action: Actions.currentAction, + context: Actions.currentContext, + payload, event: "sendToPropertyInspector" + })); + } + // 用默认浏览器打开网页 + openUrl(url) { + this.ws.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); + } +}; + +// 操作类 +class Actions { + constructor(data) { + this.data = {}; + this.default = {}; + Object.assign(this, data); + } + // 属性检查器显示时 + static currentAction = null; + static currentContext = null; + static actions = {}; + propertyInspectorDidAppear(data) { + Actions.currentAction = data.action; + Actions.currentContext = data.context; + this._propertyInspectorDidAppear?.(data); + } + // 初始化数据 + willAppear(data) { + Plugins.globalContext = data.context; + Actions.actions[data.context] = data.action + const { context, payload: { settings } } = data; + this.data[context] = Object.assign({ ...this.default }, settings); + this._willAppear?.(data); + } + + didReceiveSettings(data) { + this.data[data.context] = data.payload.settings; + this._didReceiveSettings?.(data); + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data); + delete this.data[data.context]; + } +} + +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + +module.exports = { + log, + Plugins, + Actions, + EventEmitter +}; \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/static/img/opencode-icon.svg b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/static/img/opencode-icon.svg new file mode 100644 index 0000000..9c4cb89 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/static/img/opencode-icon.svg @@ -0,0 +1 @@ +opencode \ No newline at end of file diff --git a/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/zh_CN.json b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/zh_CN.json new file mode 100644 index 0000000..f10ed77 --- /dev/null +++ b/opencode/com.streamdock.ai-monitor.opencode.sdPlugin/zh_CN.json @@ -0,0 +1,4 @@ +{ + "Name": "opencode 监控", + "Description": "监控 opencode AI 工作流状态" +} \ No newline at end of file diff --git a/shared/plugin.js b/shared/plugin.js new file mode 100644 index 0000000..673de5c --- /dev/null +++ b/shared/plugin.js @@ -0,0 +1,213 @@ +// 配置日志文件 +const now = new Date(); +const log = require('log4js').configure({ + appenders: { + file: { type: 'file', filename: `./log/${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}.log` } + }, + categories: { + default: { appenders: ['file'], level: 'info' } + } +}).getLogger(); + +//################################################## +//##################全局异常捕获##################### +process.on('uncaughtException', (error) => { + log.error('Uncaught Exception:', error); +}); +process.on('unhandledRejection', (reason) => { + log.error('Unhandled Rejection:', reason); +}); +//################################################## +//################################################## + + +// 插件类 +const ws = require('ws'); +class Plugins { + static language = JSON.parse(process.argv[9]).application.language; + static globalSettings = {}; + getGlobalSettingsFlag = true; + constructor() { + if (Plugins.instance) { + return Plugins.instance; + } + // log.info("process.argv", process.argv); + this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); + this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); + this.ws.on('close', process.exit); + this.ws.on('message', e => { + if (this.getGlobalSettingsFlag) { + // 只获取一次 + this.getGlobalSettingsFlag = false; + this.getGlobalSettings(); + } + const data = JSON.parse(e.toString()); + const action = data.action?.split('.').pop(); + this[action]?.[data.event]?.(data); + if (data.event === 'didReceiveGlobalSettings') { + Plugins.globalSettings = data.payload.settings; + } + this[data.event]?.(data); + }); + Plugins.instance = this; + } + + setGlobalSettings(payload) { + Plugins.globalSettings = payload; + this.ws.send(JSON.stringify({ + event: "setGlobalSettings", + context: process.argv[5], payload + })); + } + + getGlobalSettings() { + this.ws.send(JSON.stringify({ + event: "getGlobalSettings", + context: process.argv[5], + })); + } + // 设置标题 + setTitle(context, str, row = 0, num = 6) { + let newStr = null; + if (row && str) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.ws.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + '' + } + })); + } + // 设置背景 + setImage(context, url) { + this.ws.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })); + } + // 设置状态 + setState(context, state) { + this.ws.send(JSON.stringify({ + event: "setState", + context, payload: { state } + })); + } + // 保存持久化数据 + setSettings(context, payload) { + this.ws.send(JSON.stringify({ + event: "setSettings", + context, payload + })); + } + + // 在按键上展示警告 + showAlert(context) { + this.ws.send(JSON.stringify({ + event: "showAlert", + context + })); + } + + // 在按键上展示成功 + showOk(context) { + this.ws.send(JSON.stringify({ + event: "showOk", + context + })); + } + // 发送给属性检测器 + sendToPropertyInspector(payload) { + this.ws.send(JSON.stringify({ + action: Actions.currentAction, + context: Actions.currentContext, + payload, event: "sendToPropertyInspector" + })); + } + // 用默认浏览器打开网页 + openUrl(url) { + this.ws.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); + } +}; + +// 操作类 +class Actions { + constructor(data) { + this.data = {}; + this.default = {}; + Object.assign(this, data); + } + // 属性检查器显示时 + static currentAction = null; + static currentContext = null; + static actions = {}; + propertyInspectorDidAppear(data) { + Actions.currentAction = data.action; + Actions.currentContext = data.context; + this._propertyInspectorDidAppear?.(data); + } + // 初始化数据 + willAppear(data) { + Plugins.globalContext = data.context; + Actions.actions[data.context] = data.action + const { context, payload: { settings } } = data; + this.data[context] = Object.assign({ ...this.default }, settings); + this._willAppear?.(data); + } + + didReceiveSettings(data) { + this.data[data.context] = data.payload.settings; + this._didReceiveSettings?.(data); + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data); + delete this.data[data.context]; + } +} + +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + +module.exports = { + log, + Plugins, + Actions, + EventEmitter +}; \ No newline at end of file