send notifs at launch
This commit is contained in:
parent
9226295072
commit
4680b3ac44
3 changed files with 143 additions and 71 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
NTFY_URL=https://ntfy.sh
|
NTFY_URL=https://ntfy.sh
|
||||||
NTFY_TOPIC=
|
NTFY_TOPIC=
|
||||||
|
NTFY_TOKEN=
|
||||||
|
|
||||||
POLL_INTERVAL_SECONDS=60
|
POLL_INTERVAL_SECONDS=60
|
||||||
15
README.md
15
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Codex Reset Alerts 🚀
|
# Codex Reset Alerts 🚀
|
||||||
|
|
||||||
Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie une notification **ntfy** dès que ton quota est de nouveau disponible.
|
Petit watcher Node.js qui surveille ton quota **OpenAI Codex** et envoie des notifications **ntfy** au lancement, puis dès que ton quota est de nouveau disponible.
|
||||||
|
|
||||||
L'idée est simple : lorsque Codex indique que ton quota est épuisé, tu lances le watcher et tu peux passer à autre chose. Il surveille le quota en arrière-plan, t'envoie une notification lors du reset réel, puis s'arrête automatiquement.
|
L'idée est simple : lorsque Codex indique que ton quota est épuisé, tu lances le watcher et tu peux passer à autre chose. Il surveille le quota en arrière-plan, t'envoie une notification lors du reset réel, puis s'arrête automatiquement.
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ Le watcher utilise deux mécanismes :
|
||||||
|
|
||||||
Le timestamp `resetsAt` fourni par Codex est également surveillé, mais il n'est **pas considéré comme une garantie** : le quota peut être réinitialisé avant la date annoncée.
|
Le timestamp `resetsAt` fourni par Codex est également surveillé, mais il n'est **pas considéré comme une garantie** : le quota peut être réinitialisé avant la date annoncée.
|
||||||
|
|
||||||
La notification n'est envoyée que lorsqu'un passage réel de **quota épuisé → quota disponible** est détecté.
|
Une notification de démarrage est envoyée dès le lancement. La notification de reset n'est envoyée que lorsqu'un passage réel de **quota épuisé → quota disponible** est détecté.
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
|
|
@ -86,6 +86,7 @@ Crée un fichier `.env` à la racine :
|
||||||
```env
|
```env
|
||||||
NTFY_URL=https://ntfy.sh
|
NTFY_URL=https://ntfy.sh
|
||||||
NTFY_TOPIC=mon-topic-secret
|
NTFY_TOPIC=mon-topic-secret
|
||||||
|
NTFY_TOKEN=
|
||||||
|
|
||||||
POLL_INTERVAL_SECONDS=60
|
POLL_INTERVAL_SECONDS=60
|
||||||
```
|
```
|
||||||
|
|
@ -102,6 +103,12 @@ https://ntfy.sh
|
||||||
|
|
||||||
Tu peux également utiliser une instance ntfy auto-hébergée.
|
Tu peux également utiliser une instance ntfy auto-hébergée.
|
||||||
|
|
||||||
|
### `NTFY_TOKEN`
|
||||||
|
|
||||||
|
Token d'accès pour un serveur ntfy privé.
|
||||||
|
|
||||||
|
Laisse vide si ton topic ne demande pas d'authentification.
|
||||||
|
|
||||||
### `NTFY_TOPIC`
|
### `NTFY_TOPIC`
|
||||||
|
|
||||||
Topic sur lequel la notification sera envoyée.
|
Topic sur lequel la notification sera envoyée.
|
||||||
|
|
@ -133,6 +140,7 @@ npm start
|
||||||
Si le quota est effectivement épuisé :
|
Si le quota est effectivement épuisé :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
📱 Notification ntfy de démarrage envoyée.
|
||||||
🔌 Connexion à Codex App Server...
|
🔌 Connexion à Codex App Server...
|
||||||
✅ Connecté.
|
✅ Connecté.
|
||||||
|
|
||||||
|
|
@ -198,8 +206,9 @@ Tu peux vérifier manuellement que ton téléphone reçoit bien les notification
|
||||||
curl \
|
curl \
|
||||||
-H "Title: Test Codex" \
|
-H "Title: Test Codex" \
|
||||||
-H "Tags: robot" \
|
-H "Tags: robot" \
|
||||||
|
-H "Authorization: Bearer TON_TOKEN" \
|
||||||
-d "Hello depuis Codex Reset Alerts 🚀" \
|
-d "Hello depuis Codex Reset Alerts 🚀" \
|
||||||
https://ntfy.sh/TON_TOPIC
|
https://ntfy.shinuwa.fr/TON_TOPIC
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lancer le watcher en arrière-plan
|
## Lancer le watcher en arrière-plan
|
||||||
|
|
|
||||||
196
index.js
196
index.js
|
|
@ -26,6 +26,8 @@ loadEnv();
|
||||||
|
|
||||||
const NTFY_URL = process.env.NTFY_URL ?? "https://ntfy.sh";
|
const NTFY_URL = process.env.NTFY_URL ?? "https://ntfy.sh";
|
||||||
const NTFY_TOPIC = process.env.NTFY_TOPIC;
|
const NTFY_TOPIC = process.env.NTFY_TOPIC;
|
||||||
|
const NTFY_TOKEN = process.env.NTFY_TOKEN;
|
||||||
|
const TEST_NTFY = process.argv.includes("--test-ntfy");
|
||||||
|
|
||||||
// Toutes les 60 secondes par défaut.
|
// Toutes les 60 secondes par défaut.
|
||||||
const POLL_INTERVAL_MS =
|
const POLL_INTERVAL_MS =
|
||||||
|
|
@ -36,17 +38,10 @@ if (!NTFY_TOPIC) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codex = spawn("codex", ["app-server", "--stdio"], {
|
|
||||||
stdio: ["pipe", "pipe", "inherit"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const lines = readline.createInterface({
|
|
||||||
input: codex.stdout,
|
|
||||||
crlfDelay: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
const pending = new Map();
|
const pending = new Map();
|
||||||
|
let codex = null;
|
||||||
|
let lines = null;
|
||||||
|
|
||||||
let previousState = null;
|
let previousState = null;
|
||||||
let refreshInProgress = false;
|
let refreshInProgress = false;
|
||||||
|
|
@ -120,22 +115,26 @@ function formatDuration(ms) {
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendResetNotification(oldState, newState) {
|
async function sendNtfyNotification({
|
||||||
|
title,
|
||||||
|
tags,
|
||||||
|
priority = "default",
|
||||||
|
body,
|
||||||
|
}) {
|
||||||
const url =
|
const url =
|
||||||
`${NTFY_URL.replace(/\/$/, "")}/${encodeURIComponent(NTFY_TOPIC)}`;
|
`${NTFY_URL.replace(/\/$/, "")}/${encodeURIComponent(NTFY_TOPIC)}`;
|
||||||
|
const headers = {
|
||||||
|
Title: title,
|
||||||
|
Tags: tags,
|
||||||
|
Priority: priority,
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
...(NTFY_TOKEN ? { Authorization: `Bearer ${NTFY_TOKEN}` } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers,
|
||||||
Title: "Codex disponible",
|
body,
|
||||||
Tags: "robot,white_check_mark",
|
|
||||||
Priority: "high",
|
|
||||||
"Content-Type": "text/plain; charset=utf-8",
|
|
||||||
},
|
|
||||||
body:
|
|
||||||
`🚀 Ton quota Codex est de nouveau disponible !\n\n` +
|
|
||||||
`Avant : ${oldState.usedPercent}% utilisé\n` +
|
|
||||||
`Maintenant : ${newState.usedPercent}% utilisé`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -143,6 +142,29 @@ async function sendResetNotification(oldState, newState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendStartupNotification() {
|
||||||
|
await sendNtfyNotification({
|
||||||
|
title: "Codex Reset Alerts lancé",
|
||||||
|
tags: "robot,eyes",
|
||||||
|
priority: "default",
|
||||||
|
body:
|
||||||
|
`👀 Codex Reset Alerts vient de démarrer.\n\n` +
|
||||||
|
`Surveillance du quota Codex en cours.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendResetNotification(oldState, newState) {
|
||||||
|
await sendNtfyNotification({
|
||||||
|
title: "Codex disponible",
|
||||||
|
tags: "robot,white_check_mark",
|
||||||
|
priority: "high",
|
||||||
|
body:
|
||||||
|
`🚀 Ton quota Codex est de nouveau disponible !\n\n` +
|
||||||
|
`Avant : ${oldState.usedPercent}% utilisé\n` +
|
||||||
|
`Maintenant : ${newState.usedPercent}% utilisé`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function stop(exitCode = 0) {
|
function stop(exitCode = 0) {
|
||||||
if (shuttingDown) return;
|
if (shuttingDown) return;
|
||||||
|
|
||||||
|
|
@ -157,7 +179,7 @@ function stop(exitCode = 0) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
codex.kill();
|
codex?.kill();
|
||||||
} catch {
|
} catch {
|
||||||
// Rien.
|
// Rien.
|
||||||
}
|
}
|
||||||
|
|
@ -337,53 +359,84 @@ async function refreshQuota(source = "polling") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.on("line", async (line) => {
|
function startCodexServer() {
|
||||||
let message;
|
codex = spawn("codex", ["app-server", "--stdio"], {
|
||||||
|
stdio: ["pipe", "pipe", "inherit"],
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
lines = readline.createInterface({
|
||||||
message = JSON.parse(line);
|
input: codex.stdout,
|
||||||
} catch {
|
crlfDelay: Infinity,
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
lines.on("line", async (line) => {
|
||||||
message.id !== undefined &&
|
let message;
|
||||||
pending.has(message.id)
|
|
||||||
) {
|
|
||||||
const { resolve, reject } =
|
|
||||||
pending.get(message.id);
|
|
||||||
|
|
||||||
pending.delete(message.id);
|
try {
|
||||||
|
message = JSON.parse(line);
|
||||||
if (message.error) {
|
} catch {
|
||||||
reject(
|
return;
|
||||||
new Error(JSON.stringify(message.error))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolve(message.result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
if (
|
||||||
}
|
message.id !== undefined &&
|
||||||
|
pending.has(message.id)
|
||||||
|
) {
|
||||||
|
const { resolve, reject } =
|
||||||
|
pending.get(message.id);
|
||||||
|
|
||||||
if (
|
pending.delete(message.id);
|
||||||
message.method ===
|
|
||||||
"account/rateLimits/updated"
|
|
||||||
) {
|
|
||||||
console.log("");
|
|
||||||
console.log(
|
|
||||||
"⚡ Codex signale une modification du quota."
|
|
||||||
);
|
|
||||||
|
|
||||||
await refreshQuota("événement Codex");
|
if (message.error) {
|
||||||
}
|
reject(
|
||||||
});
|
new Error(JSON.stringify(message.error))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve(message.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.method ===
|
||||||
|
"account/rateLimits/updated"
|
||||||
|
) {
|
||||||
|
console.log("");
|
||||||
|
console.log(
|
||||||
|
"⚡ Codex signale une modification du quota."
|
||||||
|
);
|
||||||
|
|
||||||
|
await refreshQuota("événement Codex");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
codex.on("exit", (code) => {
|
||||||
|
if (!shuttingDown && code !== 0) {
|
||||||
|
console.error(
|
||||||
|
`❌ Codex App Server s'est arrêté avec le code ${code}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
try {
|
||||||
|
await sendStartupNotification();
|
||||||
|
console.log("📱 Notification ntfy de démarrage envoyée.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"❌ Échec de la notification ntfy de démarrage :",
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"🔌 Connexion à Codex App Server..."
|
"🔌 Connexion à Codex App Server..."
|
||||||
);
|
);
|
||||||
|
|
||||||
|
startCodexServer();
|
||||||
|
|
||||||
await request("initialize", {
|
await request("initialize", {
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
name: "codex-reset-alerts",
|
name: "codex-reset-alerts",
|
||||||
|
|
@ -428,15 +481,24 @@ process.on("SIGTERM", () => {
|
||||||
stop(0);
|
stop(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
codex.on("exit", (code) => {
|
if (TEST_NTFY) {
|
||||||
if (!shuttingDown && code !== 0) {
|
sendResetNotification(
|
||||||
console.error(
|
{ usedPercent: 100 },
|
||||||
`❌ Codex App Server s'est arrêté avec le code ${code}`
|
{ usedPercent: 0 }
|
||||||
);
|
)
|
||||||
}
|
.then(() => {
|
||||||
});
|
console.log("📱 Notification ntfy de test envoyée.");
|
||||||
|
})
|
||||||
main().catch((error) => {
|
.catch((error) => {
|
||||||
console.error("❌", error);
|
console.error(
|
||||||
stop(1);
|
"❌ Échec de la notification ntfy de test :",
|
||||||
});
|
error.message
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("❌", error);
|
||||||
|
stop(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue