first commit: StreamDock AI Monitor - Codex Desktop status display
This commit is contained in:
@@ -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.
Reference in New Issue
Block a user