first commit: StreamDock AI Monitor - Codex Desktop status display
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
*.bak
|
||||
debug.log
|
||||
log/
|
||||
@@ -0,0 +1,29 @@
|
||||
# StreamDock AI Monitor
|
||||
|
||||
在 StreamDock N4 硬件外接屏上实时显示 AI Agent 工作流状态。
|
||||
|
||||
## 支持的 Agent
|
||||
|
||||
| Agent | 状态 |
|
||||
|------|------|
|
||||
| Codex Desktop | ✅ 已支持 |
|
||||
| Claude Desktop | 🔜 计划中 |
|
||||
| opencode | 🔜 计划中 |
|
||||
|
||||
## 工作原理
|
||||
|
||||
插件启动后,以 1 秒间隔轮询 AI Agent 的本地日志/会话文件,将 Agent 活动映射为 N4 按钮上的图标和标题。
|
||||
|
||||
3 秒无活动自动回到 Ready 状态。
|
||||
|
||||
## 安装
|
||||
|
||||
```powershell
|
||||
$dst = "$env:APPDATA\HotSpot\StreamDock\plugins\com.streamdock.codex.monitor.sdPlugin"
|
||||
Remove-Item -Recurse -Force $dst -ErrorAction SilentlyContinue
|
||||
Copy-Item "com.streamdock.codex.monitor.sdPlugin" $dst -Recurse
|
||||
```
|
||||
|
||||
## 技术栈
|
||||
|
||||
Node.js V2 (StreamDock SDK)
|
||||
@@ -0,0 +1,4 @@
|
||||
plugin/node_modules/**
|
||||
plugin/log/
|
||||
plugin/build/
|
||||
plugin/data/
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Name": "Codex Monitor",
|
||||
"Description": "Monitor Codex Desktop AI workflow status on N4 buttons in real-time",
|
||||
"PollInterval": "Poll Interval (ms)",
|
||||
"ShowHealth": "Show Connection Health",
|
||||
"StatusIdle": "Ready",
|
||||
"StatusPrompt": "Prompt",
|
||||
"StatusThinking": "Thinking",
|
||||
"StatusWorking": "Working",
|
||||
"StatusTyping": "Typing",
|
||||
"StatusDone": "Done",
|
||||
"StatusError": "Error",
|
||||
"StatusWaiting": "Waiting",
|
||||
"StatusReconnecting": "Reconnecting",
|
||||
"StatusRecovered": "Recovered"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Actions": [
|
||||
{
|
||||
"Icon": "static/img/codex-idle.svg",
|
||||
"Name": "Codex Monitor",
|
||||
"DisableAutomaticStates": true,
|
||||
"States": [
|
||||
{
|
||||
"Image": "static/img/codex-idle.svg",
|
||||
"TitleAlignment": "center",
|
||||
"FontSize": "11"
|
||||
}
|
||||
],
|
||||
"Settings": {
|
||||
"pollInterval": 1000,
|
||||
"showHealth": true
|
||||
},
|
||||
"Controllers": ["Keypad", "Information"],
|
||||
"UserTitleEnabled": true,
|
||||
"SupportedInMultiActions": true,
|
||||
"Tooltip": "Codex Desktop 工作流状态监控",
|
||||
"UUID": "com.streamdock.codex.monitor",
|
||||
"PropertyInspectorPath": "propertyInspector/codex-monitor/index.html"
|
||||
}
|
||||
],
|
||||
"SDKVersion": 1,
|
||||
"Author": "StreamDock",
|
||||
"Name": "Codex Monitor",
|
||||
"Icon": "static/img/codex-icon.svg",
|
||||
"Category": "Codex",
|
||||
"CategoryIcon": "static/img/codex-icon.svg",
|
||||
"CodePathWin": "plugin/index.js",
|
||||
"CodePathMac": "plugin/index.js",
|
||||
"Description": "监控 Codex Desktop AI 工作流状态,在 N4 按钮上实时显示运行/思考/完成/错误等状态",
|
||||
"Version": "1.0.0",
|
||||
"URL": "https://github.com/openai/codex",
|
||||
"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,320 @@
|
||||
const { Plugins, Actions, log } = require('./utils/plugin');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const plugin = new Plugins('codex-monitor');
|
||||
const timers = {};
|
||||
const sessionOffsets = {};
|
||||
const healthOffsets = {};
|
||||
|
||||
// ─── 直接调试日志 ──────────────────────────────────────
|
||||
const DEBUG_LOG = path.join(__dirname, '..', 'debug.log');
|
||||
function dlog(msg) {
|
||||
try { fs.appendFileSync(DEBUG_LOG, '[' + new Date().toISOString() + '] ' + msg + '\n'); } catch (_) {}
|
||||
}
|
||||
dlog('=== Plugin started v6 ===');
|
||||
|
||||
// ─── 路径配置 ───────────────────────────────────────────
|
||||
const HOME = os.homedir();
|
||||
const HEALTH_MARKER = 'app_server_connection.state_changed';
|
||||
const DEFAULT_POLL = 1000;
|
||||
const DONE_CUE_MS = 800;
|
||||
const COOLDOWN_MS = 300;
|
||||
const IDLE_TIMEOUT_MS = 3000;
|
||||
|
||||
// ─── 简单递归文件查找 ──────────────────────────────────
|
||||
function findFiles(rootDir, suffix, maxDepth) {
|
||||
var results = [];
|
||||
maxDepth = maxDepth || 16;
|
||||
if (!fs.existsSync(rootDir)) return results;
|
||||
(function walk(dir, depth) {
|
||||
if (depth > maxDepth) return;
|
||||
var names;
|
||||
try { names = fs.readdirSync(dir); } catch (_) { return; }
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var name = names[i];
|
||||
var full = path.join(dir, name);
|
||||
var st;
|
||||
try { st = fs.statSync(full); } catch (_) { continue; }
|
||||
if (st.isDirectory()) walk(full, depth + 1);
|
||||
else if (st.isFile() && name.endsWith(suffix)) results.push(full);
|
||||
}
|
||||
})(rootDir, 0);
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── 状态映射 ───────────────────────────────────────────
|
||||
var STATE = {
|
||||
IDLE: 'idle', PROMPT: 'prompt', THINK: 'thinking', WORK: 'working',
|
||||
TYPING: 'typing', DONE: 'done', ERROR: 'error', WAITING: 'waiting',
|
||||
RECONNECTING: 'reconnecting', RECOVERED: 'recovered'
|
||||
};
|
||||
|
||||
var STATUS_DISPLAY = {};
|
||||
STATUS_DISPLAY['idle'] = { title: 'Ready', fill: '#22c55e', blink: false, breathe: false };
|
||||
STATUS_DISPLAY['prompt'] = { title: 'Prompt', fill: '#3b82f6', blink: false, breathe: false };
|
||||
STATUS_DISPLAY['thinking'] = { title: 'Thinking', fill: '#f59e0b', blink: false, breathe: true };
|
||||
STATUS_DISPLAY['working'] = { title: 'Working', fill: '#f59e0b', blink: true, breathe: false };
|
||||
STATUS_DISPLAY['typing'] = { title: 'Typing', fill: '#f59e0b', blink: false, breathe: true };
|
||||
STATUS_DISPLAY['done'] = { title: 'Done', fill: '#22c55e', blink: true, breathe: false };
|
||||
STATUS_DISPLAY['error'] = { title: 'Error', fill: '#ef4444', blink: false, breathe: false };
|
||||
STATUS_DISPLAY['waiting'] = { title: 'Waiting', fill: '#ef4444', blink: true, breathe: false };
|
||||
STATUS_DISPLAY['reconnecting'] = { title: 'Reconnect', fill: '#ef4444', blink: true, breathe: false };
|
||||
STATUS_DISPLAY['recovered'] = { title: 'Online', fill: '#22c55e', blink: false, breathe: false };
|
||||
|
||||
// 优先级: ERROR > WORK > TYPING > THINK > PROMPT > DONE > IDLE
|
||||
var STATE_PRIORITY = ['error', 'waiting', 'reconnecting', 'working', 'typing', 'thinking', 'prompt', 'done', 'idle'];
|
||||
|
||||
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="#1a1a2e"/><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(context, state) {
|
||||
var d = STATUS_DISPLAY[state] || STATUS_DISPLAY['idle'];
|
||||
plugin.setTitle(context, d.title);
|
||||
plugin.setImage(context, 'data:image/svg+xml;charset=utf-8,' + makeSvg(d.fill, d.title, d.blink, d.breathe));
|
||||
}
|
||||
|
||||
// ─── 状态管理 ──────────────────────────────────────────
|
||||
var currentCtx = null, currentState = 'idle', lastHwSet = 0, lastActivityTime = 0;
|
||||
var animationHandle = null;
|
||||
|
||||
function animateTick() {
|
||||
if (!currentCtx) return;
|
||||
var d = STATUS_DISPLAY[currentState] || STATUS_DISPLAY['idle'];
|
||||
if (d.blink || d.breathe) {
|
||||
var now = Date.now();
|
||||
var period = d.blink ? 800 : 2000;
|
||||
var 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="#1a1a2e"/><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(context, state) {
|
||||
if (state === currentState && context === currentCtx && Date.now() - lastHwSet < COOLDOWN_MS) return;
|
||||
if (state === 'done') {
|
||||
setDisplay(context, 'done');
|
||||
currentState = 'done';
|
||||
setTimeout(function () {
|
||||
if (currentState === 'done') { currentState = 'idle'; setDisplay(context, 'idle'); }
|
||||
}, DONE_CUE_MS);
|
||||
} else if (state === 'recovered') {
|
||||
currentState = 'idle'; setDisplay(context, 'idle');
|
||||
} else {
|
||||
currentState = state; setDisplay(context, state);
|
||||
}
|
||||
lastHwSet = Date.now();
|
||||
dlog('State: ' + state);
|
||||
}
|
||||
|
||||
// ─── Session JSONL 解析 ────────────────────────────────
|
||||
function normalizeEvent(obj) {
|
||||
var type = obj.type || '';
|
||||
var pt = (obj.payload || {}).type || '';
|
||||
var it = ((obj.payload || {}).item || {}).type || '';
|
||||
if (type === '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 (type === '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;
|
||||
}
|
||||
|
||||
// ─── Session tails ─────────────────────────────────────
|
||||
function latestSessionFiles() {
|
||||
try {
|
||||
var dir = path.join(HOME, '.codex', 'sessions');
|
||||
var files = findFiles(dir, '.jsonl');
|
||||
files.sort(function (a, b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; });
|
||||
return files.slice(0, 3);
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
function tailSession(filePath, context) {
|
||||
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 = STATE_PRIORITY.length, bestState = null;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
try {
|
||||
var obj = JSON.parse(lines[i]);
|
||||
var s = normalizeEvent(obj);
|
||||
if (s) {
|
||||
var pri = STATE_PRIORITY.indexOf(s);
|
||||
if (pri >= 0 && pri < bestPri) { bestPri = pri; bestState = s; }
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (bestState) {
|
||||
dlog('Detected: ' + bestState + ' from ' + path.basename(filePath));
|
||||
setState(context, bestState);
|
||||
detected = true;
|
||||
}
|
||||
} catch (_) { delete sessionOffsets[filePath]; }
|
||||
return detected;
|
||||
}
|
||||
|
||||
// ─── Health monitoring ─────────────────────────────────
|
||||
var healthState = { reconnecting: false };
|
||||
|
||||
function parseHealthLine(line) {
|
||||
if (line.indexOf(HEALTH_MARKER) === -1) return null;
|
||||
var fieldRe = /([A-Za-z][A-Za-z0-9_]*)=("[^"]*"|\S+)/g;
|
||||
var fields = {}, m;
|
||||
while ((m = fieldRe.exec(line)) !== null) {
|
||||
var val = m[2];
|
||||
if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') val = val.slice(1, -1);
|
||||
fields[m[1]] = val;
|
||||
}
|
||||
var next = fields.next || '';
|
||||
var prev = fields.previous || '';
|
||||
var attempt = parseInt(fields.reconnectAttempt || '0', 10);
|
||||
var scheduled = (fields.reconnectTimerScheduled || '').toLowerCase() === 'true';
|
||||
var err = fields.connectionError || '';
|
||||
var cause = fields.cause || '';
|
||||
if (next === 'connected' && healthState.reconnecting) { healthState.reconnecting = false; return 'recovered'; }
|
||||
if ((next === 'connecting' || next === 'disconnected') && (attempt > 0 || scheduled || (err && err.toLowerCase() !== 'null') || (prev === 'connected' && cause !== 'stop_process' && cause !== 'shutdown'))) {
|
||||
healthState.reconnecting = true; return 'reconnecting';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function latestHealthLogs() {
|
||||
try {
|
||||
var dir = process.platform === 'win32' ? path.join(HOME, 'AppData', 'Local', 'Codex', 'Logs') : path.join(HOME, 'Library', 'Logs', 'com.openai.codex');
|
||||
var files = findFiles(dir, '.log');
|
||||
files.sort(function (a, b) { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; });
|
||||
return files.slice(0, 3);
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
function tailHealth(filePath, context) {
|
||||
try {
|
||||
var stat = fs.statSync(filePath);
|
||||
var off = healthOffsets[filePath] || 0;
|
||||
if (stat.size < off) { healthOffsets[filePath] = 0; return; }
|
||||
if (stat.size === off) return;
|
||||
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);
|
||||
healthOffsets[filePath] = stat.size;
|
||||
var lines = buf.toString('utf-8').split('\n').filter(function (l) { return l.trim(); });
|
||||
var lastHealth = null;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var s = parseHealthLine(lines[i]);
|
||||
if (s) lastHealth = s;
|
||||
}
|
||||
if (lastHealth) { dlog('Health: ' + lastHealth); setState(context, lastHealth); }
|
||||
} catch (_) { delete healthOffsets[filePath]; }
|
||||
}
|
||||
|
||||
// ─── 主轮询 ────────────────────────────────────────────
|
||||
var pollCount = 0;
|
||||
function poll(context, interval, showHealth) {
|
||||
pollCount++;
|
||||
if (pollCount % 10 === 1) dlog('Poll #' + pollCount);
|
||||
|
||||
var detected = false;
|
||||
var files = latestSessionFiles();
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
if (tailSession(files[i], context)) detected = true;
|
||||
}
|
||||
|
||||
if (detected) lastActivityTime = Date.now();
|
||||
|
||||
// Idle timeout: return to IDLE if nothing happened for IDLE_TIMEOUT_MS
|
||||
if (currentState !== 'idle' && currentState !== 'done' && Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
|
||||
dlog('Idle timeout - returning to IDLE');
|
||||
setState(context, 'idle');
|
||||
}
|
||||
|
||||
if (showHealth) {
|
||||
var hfiles = latestHealthLogs();
|
||||
for (var j = 0; j < hfiles.length; j++) tailHealth(hfiles[j], context);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Action 定义 ───────────────────────────────────────
|
||||
plugin.monitor = new Actions({
|
||||
default: { pollInterval: DEFAULT_POLL, showHealth: true },
|
||||
|
||||
_willAppear: function (data) {
|
||||
var ctx = data.context;
|
||||
dlog('WillAppear: ' + ctx);
|
||||
currentCtx = ctx;
|
||||
currentState = 'idle';
|
||||
lastHwSet = 0;
|
||||
lastActivityTime = Date.now();
|
||||
setDisplay(ctx, 'idle');
|
||||
|
||||
var s = plugin.monitor.data[ctx];
|
||||
var interval = s.pollInterval || DEFAULT_POLL;
|
||||
var showHealth = s.showHealth !== false;
|
||||
poll(ctx, interval, showHealth);
|
||||
timers[ctx] = setInterval(function () { poll(ctx, interval, showHealth); }, interval);
|
||||
if (!animationHandle) animationHandle = setInterval(animateTick, 200);
|
||||
},
|
||||
|
||||
_willDisappear: function (data) {
|
||||
var ctx = data.context;
|
||||
dlog('WillDisappear: ' + ctx);
|
||||
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; }
|
||||
},
|
||||
|
||||
_didReceiveSettings: function (data) {
|
||||
var ctx = data.context;
|
||||
if (timers[ctx]) clearInterval(timers[ctx]);
|
||||
var s = plugin.monitor.data[ctx];
|
||||
var interval = s.pollInterval || DEFAULT_POLL;
|
||||
var showHealth = s.showHealth !== false;
|
||||
poll(ctx, interval, showHealth);
|
||||
timers[ctx] = setInterval(function () { poll(ctx, interval, showHealth); }, interval);
|
||||
},
|
||||
|
||||
keyUp: function (data) {
|
||||
var s = plugin.monitor.data[data.context];
|
||||
poll(data.context, s.pollInterval || DEFAULT_POLL, s.showHealth !== false);
|
||||
},
|
||||
|
||||
sendToPlugin: function (data) {
|
||||
if (data.payload && data.payload.command === 'refresh') {
|
||||
var s = plugin.monitor.data[data.context];
|
||||
poll(data.context, s.pollInterval || DEFAULT_POLL, s.showHealth !== false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"name": "codex-monitor",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codex-monitor",
|
||||
"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,16 @@
|
||||
{
|
||||
"name": "codex-monitor",
|
||||
"version": "1.0.0",
|
||||
"author": "StreamDock",
|
||||
"main": "index.js",
|
||||
"description": "Codex Desktop 工作流状态监控插件",
|
||||
"scripts": {
|
||||
"build": "ncc build index.js -m -o ./build && node autofile.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fs-extra": "^11.3.0",
|
||||
"log4js": "^6.9.1",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// 配置日志文件
|
||||
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;
|
||||
}
|
||||
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,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Codex Monitor</title>
|
||||
<link rel="stylesheet" href="../utils/css/sdpi.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #e0e0e0;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.section input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: #f59e0b;
|
||||
}
|
||||
.section input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: #16213e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.section .range-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.section .range-row span {
|
||||
font-size: 12px;
|
||||
color: #f59e0b;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle input { display: none; }
|
||||
.toggle .switch {
|
||||
width: 40px; height: 22px;
|
||||
background: #444;
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle .switch::after {
|
||||
content: '';
|
||||
width: 18px; height: 18px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 2px; left: 2px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.toggle input:checked + .switch {
|
||||
background: #f59e0b;
|
||||
}
|
||||
.toggle input:checked + .switch::after {
|
||||
transform: translateX(18px);
|
||||
}
|
||||
.status-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 12px; height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
}
|
||||
.status-dot.thinking { background: #f59e0b; }
|
||||
.status-dot.working { background: #f59e0b; animation: pulse 0.8s infinite; }
|
||||
.status-dot.error { background: #ef4444; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="section">
|
||||
<label data-localize="PollInterval">轮询间隔 (ms)</label>
|
||||
<div class="range-row">
|
||||
<input type="range" id="pollInterval" min="500" max="5000" step="100" value="1000">
|
||||
<span id="pollVal">1000ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="showHealth" checked>
|
||||
<div class="switch"></div>
|
||||
<span data-localize="ShowHealth">显示连接健康状态</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>状态预览</label>
|
||||
<div class="status-preview">
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<div class="status-text" id="statusText">就绪</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../utils/action.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const pollSlider = document.getElementById('pollInterval');
|
||||
const pollVal = document.getElementById('pollVal');
|
||||
const showHealthToggle = document.getElementById('showHealth');
|
||||
const statusDot = document.getElementById('statusDot');
|
||||
const statusText = document.getElementById('statusText');
|
||||
|
||||
// $settings is the SDK-provided settings proxy (auto-persists)
|
||||
$settings = $settings || {};
|
||||
|
||||
function applySettings(settings) {
|
||||
if (settings.pollInterval !== undefined) {
|
||||
pollSlider.value = settings.pollInterval;
|
||||
pollVal.textContent = settings.pollInterval + 'ms';
|
||||
}
|
||||
if (settings.showHealth !== undefined) {
|
||||
showHealthToggle.checked = settings.showHealth;
|
||||
}
|
||||
}
|
||||
|
||||
pollSlider.addEventListener('input', function () {
|
||||
const val = parseInt(this.value, 10);
|
||||
pollVal.textContent = val + 'ms';
|
||||
$settings.pollInterval = val;
|
||||
});
|
||||
|
||||
showHealthToggle.addEventListener('change', function () {
|
||||
$settings.showHealth = this.checked;
|
||||
});
|
||||
|
||||
// 监听插件传来的状态更新
|
||||
$websocket.on('sendToPropertyInspector', function (data) {
|
||||
if (data.payload && data.payload.status) {
|
||||
const st = data.payload.status;
|
||||
statusText.textContent = st;
|
||||
statusDot.className = 'status-dot';
|
||||
if (st.indexOf('Think') >= 0 || st.indexOf('思考') >= 0) statusDot.classList.add('thinking');
|
||||
else if (st.indexOf('Work') >= 0 || st.indexOf('执行') >= 0) statusDot.classList.add('working');
|
||||
else if (st.indexOf('Error') >= 0 || st.indexOf('错误') >= 0) statusDot.classList.add('error');
|
||||
else if (st.indexOf('Recon') >= 0 || st.indexOf('重连') >= 0) statusDot.classList.add('error');
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化时加载已有设置
|
||||
if ($settings.pollInterval) applySettings($settings);
|
||||
else {
|
||||
$settings.pollInterval = 1000;
|
||||
$settings.showHealth = true;
|
||||
}
|
||||
|
||||
$websocket.on('didReceiveSettings', function (data) {
|
||||
applySettings(data.payload.settings);
|
||||
});
|
||||
|
||||
// 启用自动翻译
|
||||
$local = true;
|
||||
})();
|
||||
@@ -0,0 +1,157 @@
|
||||
let $websocket, $uuid, $action, $context, $settings, $lang, $FileID = '';
|
||||
|
||||
WebSocket.prototype.setGlobalSettings = function(payload) {
|
||||
this.send(JSON.stringify({
|
||||
event: "setGlobalSettings",
|
||||
context: $uuid, payload
|
||||
}));
|
||||
}
|
||||
|
||||
WebSocket.prototype.getGlobalSettings = function() {
|
||||
this.send(JSON.stringify({
|
||||
event: "getGlobalSettings",
|
||||
context: $uuid,
|
||||
}));
|
||||
}
|
||||
|
||||
// 与插件通信
|
||||
WebSocket.prototype.sendToPlugin = function (payload) {
|
||||
this.send(JSON.stringify({
|
||||
event: "sendToPlugin",
|
||||
action: $action,
|
||||
context: $uuid,
|
||||
payload
|
||||
}));
|
||||
};
|
||||
|
||||
//设置标题
|
||||
WebSocket.prototype.setTitle = function (str, row = 0, num = 6) {
|
||||
console.log(str);
|
||||
let newStr = '';
|
||||
if (row) {
|
||||
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.send(JSON.stringify({
|
||||
event: "setTitle",
|
||||
context: $context,
|
||||
payload: {
|
||||
target: 0,
|
||||
title: newStr || str
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// 设置状态
|
||||
WebSocket.prototype.setState = function (state) {
|
||||
this.send(JSON.stringify({
|
||||
event: "setState",
|
||||
context: $context,
|
||||
payload: { state }
|
||||
}));
|
||||
};
|
||||
|
||||
// 设置背景
|
||||
WebSocket.prototype.setImage = function (url) {
|
||||
let image = new Image();
|
||||
image.src = url;
|
||||
image.onload = () => {
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
let ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0);
|
||||
this.send(JSON.stringify({
|
||||
event: "setImage",
|
||||
context: $context,
|
||||
payload: {
|
||||
target: 0,
|
||||
image: canvas.toDataURL("image/png")
|
||||
}
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
// 打开网页
|
||||
WebSocket.prototype.openUrl = function (url) {
|
||||
this.send(JSON.stringify({
|
||||
event: "openUrl",
|
||||
payload: { url }
|
||||
}));
|
||||
};
|
||||
|
||||
// 保存持久化数据
|
||||
WebSocket.prototype.saveData = $.debounce(function (payload) {
|
||||
this.send(JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: $uuid,
|
||||
payload
|
||||
}));
|
||||
});
|
||||
|
||||
// StreamDock 软件入口函数
|
||||
const connectSocket = connectElgatoStreamDeckSocket;
|
||||
async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) {
|
||||
info = JSON.parse(info);
|
||||
$uuid = uuid; $action = info.action;
|
||||
$context = info.context;
|
||||
$websocket = new WebSocket('ws://127.0.0.1:' + port);
|
||||
$websocket.onopen = () => $websocket.send(JSON.stringify({ event, uuid }));
|
||||
|
||||
// 持久数据代理
|
||||
$websocket.onmessage = e => {
|
||||
let data = JSON.parse(e.data);
|
||||
if (data.event === 'didReceiveSettings') {
|
||||
$settings = new Proxy(data.payload.settings, {
|
||||
get(target, property) {
|
||||
return target[property];
|
||||
},
|
||||
set(target, property, value) {
|
||||
target[property] = value;
|
||||
$websocket.saveData(data.payload.settings);
|
||||
}
|
||||
});
|
||||
if (!$back) $dom.main.style.display = 'block';
|
||||
}
|
||||
$propEvent[data.event]?.(data.payload);
|
||||
};
|
||||
|
||||
// 自动翻译页面
|
||||
if (!$local) return;
|
||||
$lang = await new Promise(resolve => {
|
||||
const req = new XMLHttpRequest();
|
||||
req.open('GET', `../../${JSON.parse(app).application.language}.json`);
|
||||
req.send();
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === 4) {
|
||||
resolve(JSON.parse(req.responseText).Localization);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 遍历文本节点并翻译所有文本节点
|
||||
const walker = document.createTreeWalker($dom.main, NodeFilter.SHOW_TEXT, (e) => {
|
||||
return e.data.trim() && NodeFilter.FILTER_ACCEPT;
|
||||
});
|
||||
while (walker.nextNode()) {
|
||||
console.log(walker.currentNode.data);
|
||||
walker.currentNode.data = $lang[walker.currentNode.data];
|
||||
}
|
||||
// placeholder 特殊处理
|
||||
const translate = item => {
|
||||
if (item.placeholder?.trim()) {
|
||||
console.log(item.placeholder);
|
||||
item.placeholder = $lang[item.placeholder];
|
||||
}
|
||||
};
|
||||
$('input', true).forEach(translate);
|
||||
$('textarea', true).forEach(translate);
|
||||
}
|
||||
|
||||
// StreamDock 文件路径回调
|
||||
Array.from($('input[type="file"]', true)).forEach(item => item.addEventListener('click', () => $FileID = item.id));
|
||||
const onFilePickerReturn = (url) => $emit.send(`File-${$FileID}`, JSON.parse(url));
|
||||
File diff suppressed because one or more lines are too long
+1556
File diff suppressed because it is too large
Load Diff
+7
File diff suppressed because one or more lines are too long
@@ -0,0 +1,81 @@
|
||||
// 自定义事件类
|
||||
class EventPlus {
|
||||
constructor() {
|
||||
this.event = new EventTarget();
|
||||
}
|
||||
on(name, callback) {
|
||||
this.event.addEventListener(name, e => callback(e.detail));
|
||||
}
|
||||
send(name, data) {
|
||||
this.event.dispatchEvent(new CustomEvent(name, {
|
||||
detail: data,
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 补零
|
||||
String.prototype.fill = function () {
|
||||
return this >= 10 ? this : '0' + this;
|
||||
};
|
||||
|
||||
// unicode编码转换字符串
|
||||
String.prototype.uTs = function () {
|
||||
return eval('"' + Array.from(this).join('') + '"');
|
||||
};
|
||||
|
||||
// 字符串转换unicode编码
|
||||
String.prototype.sTu = function (str = '') {
|
||||
Array.from(this).forEach(item => str += `\\u${item.charCodeAt(0).toString(16)}`);
|
||||
return str;
|
||||
};
|
||||
|
||||
// 全局变量/方法
|
||||
const $emit = new EventPlus(), $ = (selector, isAll = false) => {
|
||||
const element = document.querySelector(selector), methods = {
|
||||
on: function (event, callback) {
|
||||
this.addEventListener(event, callback);
|
||||
},
|
||||
attr: function (name, value = '') {
|
||||
value && this.setAttribute(name, value);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
if (!isAll && element) {
|
||||
return Object.assign(element, methods);
|
||||
} else if (!isAll && !element) {
|
||||
throw `HTML没有 ${selector} 元素! 请检查是否拼写错误`;
|
||||
}
|
||||
return Array.from(document.querySelectorAll(selector)).map(item => Object.assign(item, methods));
|
||||
};
|
||||
|
||||
// 节流函数
|
||||
$.throttle = (fn, delay) => {
|
||||
let Timer = null;
|
||||
return function () {
|
||||
if (Timer) return;
|
||||
Timer = setTimeout(() => {
|
||||
fn.apply(this, arguments);
|
||||
Timer = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
// 防抖函数
|
||||
$.debounce = (fn, delay) => {
|
||||
let Timer = null;
|
||||
return function () {
|
||||
clearTimeout(Timer);
|
||||
Timer = setTimeout(() => fn.apply(this, arguments), delay);
|
||||
};
|
||||
};
|
||||
|
||||
// 绑定限制数字方法
|
||||
Array.from($('input[type="num"]', true)).forEach(item => {
|
||||
item.addEventListener('input', function limitNum() {
|
||||
if (!item.value || /^\d+$/.test(item.value)) return;
|
||||
item.value = item.value.slice(0, -1);
|
||||
limitNum(item);
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="#1a1a2e"/><polygon points="72,24 112,60 112,108 72,144 32,108 32,60" fill="none" stroke="#f59e0b" stroke-width="3"/><text x="72" y="84" text-anchor="middle" fill="#f59e0b" font-family="Arial,sans-serif" font-size="28" font-weight="bold">CX</text></svg>
|
||||
|
After Width: | Height: | Size: 363 B |
@@ -0,0 +1 @@
|
||||
<svg width="144" height="144" xmlns="http://www.w3.org/2000/svg"><rect width="144" height="144" rx="20" fill="#1a1a2e"/><circle cx="72" cy="58" r="32" fill="#22c55e"/><text x="72" y="118" text-anchor="middle" fill="white" font-family="Arial,sans-serif" font-size="14" font-weight="bold">🟢 Ready</text></svg>
|
||||
|
After Width: | Height: | Size: 311 B |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Name": "Codex 监控",
|
||||
"Description": "监控 Codex Desktop AI 工作流状态,在 N4 按钮上实时显示",
|
||||
"PollInterval": "轮询间隔 (ms)",
|
||||
"ShowHealth": "显示连接健康状态",
|
||||
"StatusIdle": "就绪",
|
||||
"StatusPrompt": "收到提示",
|
||||
"StatusThinking": "思考中",
|
||||
"StatusWorking": "执行中",
|
||||
"StatusTyping": "回复中",
|
||||
"StatusDone": "完成",
|
||||
"StatusError": "错误",
|
||||
"StatusWaiting": "等待中",
|
||||
"StatusReconnecting": "重连中",
|
||||
"StatusRecovered": "已恢复"
|
||||
}
|
||||
Reference in New Issue
Block a user