v1: three independent AI Monitor plugins (Codex + Claude + opencode)

This commit is contained in:
2026-06-18 15:01:01 +08:00
commit 227cb96a1d
33 changed files with 2268 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
*.log
build/
*.bak
debug.log
+21
View File
@@ -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
```
@@ -0,0 +1,3 @@
node_modules/
*.log
build/
@@ -0,0 +1,4 @@
{
"Name": "Claude Desktop Monitor",
"Description": "Monitor Claude Desktop AI workflow status"
}
@@ -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"
}
}
@@ -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);
}
@@ -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 = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>';
else if (breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>';
return '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + fill + '">' + anim + '</circle><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + label + '</text></svg>';
}
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 = '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + color + '"/><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + d.title + '</text></svg>';
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); }
});
@@ -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
}
}
}
}
}
@@ -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"
}
}
@@ -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
};
@@ -0,0 +1 @@
<svg fill="none" height="2500" viewBox="0 -.01 39.5 39.53" width="2500" xmlns="http://www.w3.org/2000/svg"><path d="m7.75 26.27 7.77-4.36.13-.38-.13-.21h-.38l-1.3-.08-4.44-.12-3.85-.16-3.73-.2-.94-.2-.88-1.16.09-.58.79-.53 1.13.1 2.5.17 3.75.26 2.72.16 4.03.42h.64l.09-.26-.22-.16-.17-.16-3.88-2.63-4.2-2.78-2.2-1.6-1.19-.81-.6-.76-.26-1.66 1.08-1.19 1.45.1.37.1 1.47 1.13 3.14 2.43 4.1 3.02.6.5.24-.17.03-.12-.27-.45-2.23-4.03-2.38-4.1-1.06-1.7-.28-1.02c-.1-.42-.17-.77-.17-1.2l1.23-1.67.68-.22 1.64.22.69.6 1.02 2.33 1.65 3.67 2.56 4.99.75 1.48.4 1.37.15.42h.26v-.24l.21-2.81.39-3.45.38-4.44.13-1.25.62-1.5 1.23-.81.96.46.79 1.13-.11.73-.47 3.05-.92 4.78-.6 3.2h.35l.4-.4 1.62-2.15 2.72-3.4 1.2-1.35 1.4-1.49.9-.71h1.7l1.25 1.86-.56 1.92-1.75 2.22-1.45 1.88-2.08 2.8-1.3 2.24.12.18.31-.03 4.7-1 2.54-.46 3.03-.52 1.37.64.15.65-.54 1.33-3.24.8-3.8.76-5.66 1.34-.07.05.08.1 2.55.24 1.09.06h2.67l4.97.37 1.3.86.78 1.05-.13.8-2 1.02-2.7-.64-6.3-1.5-2.16-.54h-.3v.18l1.8 1.76 3.3 2.98 4.13 3.84.21.95-.53.75-.56-.08-3.63-2.73-1.4-1.23-3.17-2.67h-.21v.28l.73 1.07 3.86 5.8.2 1.78-.28.58-1 .35-1.1-.2-2.26-3.17-2.33-3.57-1.88-3.2-.23.13-1.11 11.95-.52.61-1.2.46-1-.76-.53-1.23.53-2.43.64-3.17.52-2.52.47-3.13.28-1.04-.02-.07-.23.03-2.36 3.24-3.59 4.85-2.84 3.04-.68.27-1.18-.61.11-1.09.66-.97 3.93-5 2.37-3.1 1.53-1.79-.01-.26h-.09l-10.44 6.78-1.86.24-.8-.75.1-1.23.38-.4 3.14-2.16z" fill="#d97757"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,4 @@
{
"Name": "Claude Desktop 监控",
"Description": "监控 Claude Desktop AI 工作流状态"
}
@@ -0,0 +1,3 @@
node_modules/
*.log
build/
@@ -0,0 +1,4 @@
{
"Name": "Codex Desktop Monitor",
"Description": "Monitor Codex Desktop AI workflow status"
}
@@ -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"
}
}
@@ -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);
}
@@ -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 = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>';
else if (breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>';
return '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + fill + '">' + anim + '</circle><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + label + '</text></svg>';
}
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 = '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + color + '"/><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + d.title + '</text></svg>';
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); }
});
@@ -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
}
}
}
}
}
@@ -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"
}
}
@@ -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
};
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path d="M19.503 0H4.496A4.496 4.496 0 000 4.496v15.007A4.496 4.496 0 004.496 24h15.007A4.496 4.496 0 0024 19.503V4.496A4.496 4.496 0 0019.503 0z" fill="#fff"></path><path d="M9.064 3.344a4.578 4.578 0 012.285-.312c1 .115 1.891.54 2.673 1.275.01.01.024.017.037.021a.09.09 0 00.043 0 4.55 4.55 0 013.046.275l.047.022.116.057a4.581 4.581 0 012.188 2.399c.209.51.313 1.041.315 1.595a4.24 4.24 0 01-.134 1.223.123.123 0 00.03.115c.594.607.988 1.33 1.183 2.17.289 1.425-.007 2.71-.887 3.854l-.136.166a4.548 4.548 0 01-2.201 1.388.123.123 0 00-.081.076c-.191.551-.383 1.023-.74 1.494-.9 1.187-2.222 1.846-3.711 1.838-1.187-.006-2.239-.44-3.157-1.302a.107.107 0 00-.105-.024c-.388.125-.78.143-1.204.138a4.441 4.441 0 01-1.945-.466 4.544 4.544 0 01-1.61-1.335c-.152-.202-.303-.392-.414-.617a5.81 5.81 0 01-.37-.961 4.582 4.582 0 01-.014-2.298.124.124 0 00.006-.056.085.085 0 00-.027-.048 4.467 4.467 0 01-1.034-1.651 3.896 3.896 0 01-.251-1.192 5.189 5.189 0 01.141-1.6c.337-1.112.982-1.985 1.933-2.618.212-.141.413-.251.601-.33.215-.089.43-.164.646-.227a.098.098 0 00.065-.066 4.51 4.51 0 01.829-1.615 4.535 4.535 0 011.837-1.388zm3.482 10.565a.637.637 0 000 1.272h3.636a.637.637 0 100-1.272h-3.636zM8.462 9.23a.637.637 0 00-1.106.631l1.272 2.224-1.266 2.136a.636.636 0 101.095.649l1.454-2.455a.636.636 0 00.005-.64L8.462 9.23z" fill="url(#lobe-icons-codex-_R_0_)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-codex-_R_0_" x1="12" x2="12" y1="3" y2="21"><stop stop-color="#B1A7FF"></stop><stop offset=".5" stop-color="#7A9DFF"></stop><stop offset="1" stop-color="#3941FF"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,4 @@
{
"Name": "Codex Desktop 监控",
"Description": "监控 Codex Desktop AI 工作流状态"
}
@@ -0,0 +1,3 @@
node_modules/
*.log
build/
@@ -0,0 +1,4 @@
{
"Name": "opencode Monitor",
"Description": "Monitor opencode AI workflow status"
}
@@ -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"
}
}
@@ -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);
}
@@ -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 = '<animate attributeName="opacity" values="1;0.2;1" dur="0.8s" repeatCount="indefinite"/>';
else if (breathe) anim = '<animate attributeName="opacity" values="1;0.4;1" dur="2s" repeatCount="indefinite"/>';
return '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + fill + '">' + anim + '</circle><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + label + '</text></svg>';
}
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 = '<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="none"/><circle cx="72" cy="58" r="32" fill="' + color + '"/><text x="72" y="118" font-family="Arial" font-weight="bold" font-size="13" fill="white" text-anchor="middle">' + d.title + '</text></svg>';
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); }
});
@@ -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
}
}
}
}
}
@@ -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"
}
}
@@ -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
};
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>opencode</title><path d="M16 6H8v12h8V6zm4 16H4V2h16v20z"></path></svg>

After

Width:  |  Height:  |  Size: 235 B

@@ -0,0 +1,4 @@
{
"Name": "opencode 监控",
"Description": "监控 opencode AI 工作流状态"
}
+213
View File
@@ -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
};