first commit: StreamDock AI Monitor - Codex Desktop status display

This commit is contained in:
2026-06-18 14:28:09 +08:00
commit 6a75b46453
22 changed files with 2881 additions and 0 deletions
@@ -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
File diff suppressed because it is too large Load Diff
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);
});
});