opencode-pollinations-plugin 5.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +128 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +135 -0
- package/dist/provider_v1.d.ts +1 -0
- package/dist/provider_v1.js +135 -0
- package/dist/server/commands.d.ts +10 -0
- package/dist/server/commands.js +302 -0
- package/dist/server/config.d.ts +49 -0
- package/dist/server/config.js +159 -0
- package/dist/server/generate-config.d.ts +13 -0
- package/dist/server/generate-config.js +154 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +120 -0
- package/dist/server/pollinations-api.d.ts +48 -0
- package/dist/server/pollinations-api.js +147 -0
- package/dist/server/proxy.d.ts +2 -0
- package/dist/server/proxy.js +588 -0
- package/dist/server/quota.d.ts +15 -0
- package/dist/server/quota.js +210 -0
- package/dist/server/router.d.ts +8 -0
- package/dist/server/router.js +122 -0
- package/dist/server/status.d.ts +3 -0
- package/dist/server/status.js +31 -0
- package/dist/server/toast.d.ts +6 -0
- package/dist/server/toast.js +78 -0
- package/package.json +53 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as https from 'https'; // Use Native HTTPS
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
// === CACHE ===
|
|
5
|
+
const CACHE_TTL = 30000; // 30 secondes
|
|
6
|
+
let cachedQuota = null;
|
|
7
|
+
let lastQuotaFetch = 0;
|
|
8
|
+
// === TIER LIMITS ===
|
|
9
|
+
const TIER_LIMITS = {
|
|
10
|
+
spore: { pollen: 1, emoji: '🦠' },
|
|
11
|
+
seed: { pollen: 3, emoji: '🌱' },
|
|
12
|
+
flower: { pollen: 10, emoji: '🌸' },
|
|
13
|
+
nectar: { pollen: 20, emoji: '🍯' },
|
|
14
|
+
};
|
|
15
|
+
// === LOGGING ===
|
|
16
|
+
function logQuota(msg) {
|
|
17
|
+
try {
|
|
18
|
+
fs.appendFileSync('/tmp/pollinations_quota_debug.log', `[${new Date().toISOString()}] ${msg}\n`);
|
|
19
|
+
}
|
|
20
|
+
catch (e) { }
|
|
21
|
+
}
|
|
22
|
+
// === FONCTIONS PRINCIPALES ===
|
|
23
|
+
export async function getQuotaStatus(forceRefresh = false) {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
if (!config.apiKey) {
|
|
26
|
+
// Pas de clé = Mode manual par défaut, pas de quota
|
|
27
|
+
return {
|
|
28
|
+
tierRemaining: 0,
|
|
29
|
+
tierUsed: 0,
|
|
30
|
+
tierLimit: 0,
|
|
31
|
+
walletBalance: 0,
|
|
32
|
+
nextResetAt: new Date(),
|
|
33
|
+
timeUntilReset: 0,
|
|
34
|
+
canUseEnterprise: false,
|
|
35
|
+
isUsingWallet: false,
|
|
36
|
+
needsAlert: false,
|
|
37
|
+
tier: 'none',
|
|
38
|
+
tierEmoji: '❌'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
if (!forceRefresh && cachedQuota && (now - lastQuotaFetch) < CACHE_TTL) {
|
|
43
|
+
return cachedQuota;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
logQuota("Fetching Quota Data...");
|
|
47
|
+
// Fetch parallèle using HTTPS helper
|
|
48
|
+
const [profileRes, balanceRes, usageRes] = await Promise.all([
|
|
49
|
+
fetchAPI('/account/profile', config.apiKey),
|
|
50
|
+
fetchAPI('/account/balance', config.apiKey),
|
|
51
|
+
fetchAPI('/account/usage', config.apiKey)
|
|
52
|
+
]);
|
|
53
|
+
logQuota(`Fetch Success. Tier: ${profileRes.tier}, Balance: ${balanceRes.balance}`);
|
|
54
|
+
const profile = profileRes;
|
|
55
|
+
const balance = balanceRes.balance;
|
|
56
|
+
const usage = usageRes.usage || [];
|
|
57
|
+
const tierInfo = TIER_LIMITS[profile.tier] || { pollen: 1, emoji: '❓' }; // Default 1 (Spore)
|
|
58
|
+
const tierLimit = tierInfo.pollen;
|
|
59
|
+
// Calculer le reset
|
|
60
|
+
const resetInfo = calculateResetInfo(profile.nextResetAt);
|
|
61
|
+
// Calculer l'usage de la période actuelle
|
|
62
|
+
const { tierUsed } = calculateCurrentPeriodUsage(usage, resetInfo);
|
|
63
|
+
const tierRemaining = Math.max(0, tierLimit - tierUsed);
|
|
64
|
+
// Fix rounding errors
|
|
65
|
+
const cleanTierRemaining = Math.max(0, parseFloat(tierRemaining.toFixed(4)));
|
|
66
|
+
// Le wallet c'est le reste (balance totale - ce qu'il reste du tier gratuit non consommé)
|
|
67
|
+
const walletBalance = Math.max(0, balance - cleanTierRemaining);
|
|
68
|
+
const cleanWalletBalance = Math.max(0, parseFloat(walletBalance.toFixed(4)));
|
|
69
|
+
cachedQuota = {
|
|
70
|
+
tierRemaining: cleanTierRemaining,
|
|
71
|
+
tierUsed,
|
|
72
|
+
tierLimit,
|
|
73
|
+
walletBalance: cleanWalletBalance,
|
|
74
|
+
nextResetAt: resetInfo.nextReset,
|
|
75
|
+
timeUntilReset: resetInfo.timeUntilReset,
|
|
76
|
+
canUseEnterprise: cleanTierRemaining > 0.05 || cleanWalletBalance > 0.05,
|
|
77
|
+
isUsingWallet: cleanTierRemaining <= 0.05 && cleanWalletBalance > 0.05,
|
|
78
|
+
needsAlert: tierLimit > 0 ? (cleanTierRemaining / tierLimit * 100) <= config.thresholds.tier : false,
|
|
79
|
+
tier: profile.tier,
|
|
80
|
+
tierEmoji: tierInfo.emoji
|
|
81
|
+
};
|
|
82
|
+
lastQuotaFetch = now;
|
|
83
|
+
return cachedQuota;
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
logQuota(`ERROR fetching quota: ${e}`);
|
|
87
|
+
// Retourner le cache ou un état par défaut safe
|
|
88
|
+
return cachedQuota || {
|
|
89
|
+
tierRemaining: 0,
|
|
90
|
+
tierUsed: 0,
|
|
91
|
+
tierLimit: 1,
|
|
92
|
+
walletBalance: 0,
|
|
93
|
+
nextResetAt: new Date(),
|
|
94
|
+
timeUntilReset: 0,
|
|
95
|
+
canUseEnterprise: false,
|
|
96
|
+
isUsingWallet: false,
|
|
97
|
+
needsAlert: true,
|
|
98
|
+
tier: 'error',
|
|
99
|
+
tierEmoji: '⚠️'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// === HELPERS (Native HTTPS) ===
|
|
104
|
+
function fetchAPI(endpoint, apiKey) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const options = {
|
|
107
|
+
hostname: 'gen.pollinations.ai',
|
|
108
|
+
port: 443,
|
|
109
|
+
path: endpoint,
|
|
110
|
+
method: 'GET',
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
113
|
+
'User-Agent': 'opencode-pollinations-plugin/5.1.0'
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const req = https.request(options, (res) => {
|
|
117
|
+
let data = '';
|
|
118
|
+
res.on('data', (chunk) => data += chunk);
|
|
119
|
+
res.on('end', () => {
|
|
120
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
121
|
+
reject(new Error(`API Error ${res.statusCode}: ${data}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(data);
|
|
126
|
+
resolve(parsed);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
reject(new Error(`JSON Parse Error: ${e.message}`));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
req.on('error', (e) => {
|
|
134
|
+
reject(new Error(`Network Error: ${e.message}`));
|
|
135
|
+
});
|
|
136
|
+
req.end();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function calculateResetInfo(nextResetAt) {
|
|
140
|
+
const nextResetFromAPI = new Date(nextResetAt);
|
|
141
|
+
const now = new Date();
|
|
142
|
+
// Extraire l'heure de reset depuis l'API (varie par utilisateur!)
|
|
143
|
+
const resetHour = nextResetFromAPI.getUTCHours();
|
|
144
|
+
const resetMinute = nextResetFromAPI.getUTCMinutes();
|
|
145
|
+
const resetSecond = nextResetFromAPI.getUTCSeconds();
|
|
146
|
+
// Calculer le reset d'aujourd'hui à cette heure
|
|
147
|
+
const todayResetUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), resetHour, resetMinute, resetSecond));
|
|
148
|
+
let lastReset;
|
|
149
|
+
let nextReset;
|
|
150
|
+
if (now >= todayResetUTC) {
|
|
151
|
+
// Le reset d'aujourd'hui est passé
|
|
152
|
+
lastReset = todayResetUTC;
|
|
153
|
+
nextReset = new Date(todayResetUTC.getTime() + 24 * 60 * 60 * 1000);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Le reset d'aujourd'hui n'est pas encore passé
|
|
157
|
+
lastReset = new Date(todayResetUTC.getTime() - 24 * 60 * 60 * 1000);
|
|
158
|
+
nextReset = todayResetUTC;
|
|
159
|
+
}
|
|
160
|
+
const timeUntilReset = nextReset.getTime() - now.getTime();
|
|
161
|
+
const timeSinceReset = now.getTime() - lastReset.getTime();
|
|
162
|
+
const cycleDuration = 24 * 60 * 60 * 1000;
|
|
163
|
+
const progressPercent = (timeSinceReset / cycleDuration) * 100;
|
|
164
|
+
return {
|
|
165
|
+
nextReset,
|
|
166
|
+
lastReset,
|
|
167
|
+
timeUntilReset,
|
|
168
|
+
timeSinceReset,
|
|
169
|
+
resetHour,
|
|
170
|
+
resetMinute,
|
|
171
|
+
resetSecond,
|
|
172
|
+
progressPercent
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function calculateCurrentPeriodUsage(usage, resetInfo) {
|
|
176
|
+
let tierUsed = 0;
|
|
177
|
+
let packUsed = 0;
|
|
178
|
+
// Parser le timestamp de l'API avec Z pour UTC
|
|
179
|
+
function parseUsageTimestamp(timestamp) {
|
|
180
|
+
// Format: "2026-01-23 01:11:21"
|
|
181
|
+
const isoString = timestamp.replace(' ', 'T') + 'Z';
|
|
182
|
+
return new Date(isoString);
|
|
183
|
+
}
|
|
184
|
+
// FILTRER: Ne garder que les entrées APRÈS le dernier reset
|
|
185
|
+
const entriesAfterReset = usage.filter(entry => {
|
|
186
|
+
const entryTime = parseUsageTimestamp(entry.timestamp);
|
|
187
|
+
return entryTime >= resetInfo.lastReset;
|
|
188
|
+
});
|
|
189
|
+
for (const entry of entriesAfterReset) {
|
|
190
|
+
if (entry.meter_source === 'tier') {
|
|
191
|
+
tierUsed += entry.cost_usd;
|
|
192
|
+
}
|
|
193
|
+
else if (entry.meter_source === 'pack') {
|
|
194
|
+
packUsed += entry.cost_usd;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { tierUsed, packUsed };
|
|
198
|
+
}
|
|
199
|
+
// === EXPORT POUR LES ALERTES ===
|
|
200
|
+
export function formatQuotaForToast(quota) {
|
|
201
|
+
const tierPercent = quota.tierLimit > 0
|
|
202
|
+
? Math.round((quota.tierRemaining / quota.tierLimit) * 100)
|
|
203
|
+
: 0;
|
|
204
|
+
// Format compact: 1h23m
|
|
205
|
+
const ms = quota.timeUntilReset;
|
|
206
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
207
|
+
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
|
|
208
|
+
const resetIn = `${hours}h${minutes}m`;
|
|
209
|
+
return `${quota.tierEmoji} Tier: ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} (${tierPercent}%) | 💎 Wallet: $${quota.walletBalance.toFixed(2)} | ⏰ Reset: ${resetIn}`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RoutingDecision {
|
|
2
|
+
targetUrl: string;
|
|
3
|
+
actualModel: string;
|
|
4
|
+
authHeader?: string;
|
|
5
|
+
fallbackUsed: boolean;
|
|
6
|
+
fallbackReason?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveRouting(requestedModel: string, isAgent?: boolean): Promise<RoutingDecision>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { getQuotaStatus } from './quota.js';
|
|
3
|
+
import { emitToast } from './toast.js';
|
|
4
|
+
// === MAIN ROUTER ===
|
|
5
|
+
export async function resolveRouting(requestedModel, isAgent = false) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
// Normalisation de l'ID modèle pour l'analyse
|
|
8
|
+
// Peut arriver sous forme: "pollinations/free/gemini" OU "free/gemini"
|
|
9
|
+
const isEnterprise = requestedModel.includes('/enter/') || requestedModel.startsWith('enter/');
|
|
10
|
+
const isFree = requestedModel.includes('/free/') || requestedModel.startsWith('free/');
|
|
11
|
+
// Extraction du "baseModel" (ex: "gemini", "openai")
|
|
12
|
+
let baseModel = requestedModel;
|
|
13
|
+
baseModel = baseModel.replace(/^pollinations\//, ''); // Remove plugin prefix
|
|
14
|
+
baseModel = baseModel.replace(/^(enter|free)\//, ''); // Remove tier prefix
|
|
15
|
+
// === MODE MANUAL ===
|
|
16
|
+
if (config.mode === 'manual') {
|
|
17
|
+
if (isEnterprise && config.apiKey) {
|
|
18
|
+
return {
|
|
19
|
+
targetUrl: 'https://gen.pollinations.ai/v1/chat/completions',
|
|
20
|
+
actualModel: baseModel,
|
|
21
|
+
authHeader: `Bearer ${config.apiKey}`,
|
|
22
|
+
fallbackUsed: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Default Free
|
|
26
|
+
return {
|
|
27
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
28
|
+
actualModel: baseModel,
|
|
29
|
+
authHeader: undefined,
|
|
30
|
+
fallbackUsed: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// === MODES INTELLIGENTS ===
|
|
34
|
+
if (!config.apiKey) {
|
|
35
|
+
return {
|
|
36
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
37
|
+
actualModel: baseModel,
|
|
38
|
+
authHeader: undefined,
|
|
39
|
+
fallbackUsed: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const quota = await getQuotaStatus();
|
|
43
|
+
handleQuotaAlerts(quota, config);
|
|
44
|
+
// === ALWAYSFREE ===
|
|
45
|
+
if (config.mode === 'alwaysfree') {
|
|
46
|
+
if (isEnterprise) {
|
|
47
|
+
if (quota.tierRemaining > 0) {
|
|
48
|
+
return {
|
|
49
|
+
targetUrl: 'https://gen.pollinations.ai/v1/chat/completions',
|
|
50
|
+
actualModel: baseModel,
|
|
51
|
+
authHeader: `Bearer ${config.apiKey}`,
|
|
52
|
+
fallbackUsed: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const fallbackModel = isAgent ? config.fallbackModels.agent : config.fallbackModels.main;
|
|
57
|
+
emitToast('warning', `Quota Free épuisé 🛑 → Relai sur ${fallbackModel} gratuit 🔀`, 'Mode AlwaysFree');
|
|
58
|
+
return {
|
|
59
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
60
|
+
actualModel: fallbackModel,
|
|
61
|
+
authHeader: undefined,
|
|
62
|
+
fallbackUsed: true,
|
|
63
|
+
fallbackReason: 'tier_exhausted'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Free
|
|
68
|
+
return {
|
|
69
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
70
|
+
actualModel: baseModel,
|
|
71
|
+
authHeader: undefined,
|
|
72
|
+
fallbackUsed: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// === PRO ===
|
|
76
|
+
if (config.mode === 'pro') {
|
|
77
|
+
if (isEnterprise) {
|
|
78
|
+
if (quota.canUseEnterprise) {
|
|
79
|
+
if (quota.isUsingWallet) {
|
|
80
|
+
emitToast('info', `Tier épuisé → Utilisation du Wallet ($${quota.walletBalance.toFixed(2)})`, 'Mode Pro');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
targetUrl: 'https://gen.pollinations.ai/v1/chat/completions',
|
|
84
|
+
actualModel: baseModel,
|
|
85
|
+
authHeader: `Bearer ${config.apiKey}`,
|
|
86
|
+
fallbackUsed: false
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const fallbackModel = isAgent ? config.fallbackModels.agent : config.fallbackModels.main;
|
|
91
|
+
emitToast('error', `💸 Wallet épuisé ! Fallback sur ${fallbackModel}`, 'Mode Pro');
|
|
92
|
+
return {
|
|
93
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
94
|
+
actualModel: fallbackModel,
|
|
95
|
+
authHeader: undefined,
|
|
96
|
+
fallbackUsed: true,
|
|
97
|
+
fallbackReason: 'wallet_exhausted'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Free
|
|
102
|
+
return {
|
|
103
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
104
|
+
actualModel: baseModel,
|
|
105
|
+
authHeader: undefined,
|
|
106
|
+
fallbackUsed: false
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Default
|
|
110
|
+
return {
|
|
111
|
+
targetUrl: 'https://text.pollinations.ai/openai/chat/completions',
|
|
112
|
+
actualModel: baseModel,
|
|
113
|
+
authHeader: undefined,
|
|
114
|
+
fallbackUsed: false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function handleQuotaAlerts(quota, config) {
|
|
118
|
+
if (quota.needsAlert && quota.tierLimit > 0) {
|
|
119
|
+
const tierPercent = Math.round((quota.tierRemaining / quota.tierLimit) * 100);
|
|
120
|
+
emitToast('warning', `⚠️ Quota Tier à ${tierPercent}%`, 'Alerte Quota');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { getQuotaStatus } from './quota.js';
|
|
3
|
+
export function createStatusHooks(client) {
|
|
4
|
+
return {
|
|
5
|
+
'session.idle': async () => {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
// Si la barre de statut est activée via 'status_bar' (bool)
|
|
8
|
+
// L'utilisateur peut l'activer via /pollinations config status_bar true
|
|
9
|
+
if (config.statusBar) {
|
|
10
|
+
const quota = await getQuotaStatus(false);
|
|
11
|
+
const statusText = formatStatus(quota);
|
|
12
|
+
try {
|
|
13
|
+
// ASTUCE: Toasts longue durée (30s) rafraîchis à chaque idle
|
|
14
|
+
// Simule un widget persistent à droite.
|
|
15
|
+
await client.tui.showToast({
|
|
16
|
+
body: {
|
|
17
|
+
message: statusText,
|
|
18
|
+
variant: 'info',
|
|
19
|
+
duration: 30000
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (e) { }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function formatStatus(quota) {
|
|
29
|
+
const tierName = quota.tier === 'alwaysfree' ? 'Free' : quota.tier;
|
|
30
|
+
return `${tierName} ${quota.tierRemaining.toFixed(2)}/${quota.tierLimit} 🌼 | Wallet $${quota.walletBalance.toFixed(2)}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function setGlobalClient(client: any): void;
|
|
2
|
+
export declare function emitLogToast(type: 'info' | 'warning' | 'error' | 'success', message: string, title?: string): void;
|
|
3
|
+
export declare function emitStatusToast(type: 'info' | 'warning' | 'error' | 'success', message: string, title?: string): void;
|
|
4
|
+
export declare function createToastHooks(client: any): {
|
|
5
|
+
'session.idle': ({ event }: any) => Promise<void>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { loadConfig } from './config.js';
|
|
3
|
+
const toastQueue = [];
|
|
4
|
+
let globalClient = null;
|
|
5
|
+
// === CONFIGURATION ===
|
|
6
|
+
// On charge la config au moment de l'émission pour décider
|
|
7
|
+
// === FONCTIONS PUBLIQUES ===
|
|
8
|
+
export function setGlobalClient(client) {
|
|
9
|
+
globalClient = client;
|
|
10
|
+
}
|
|
11
|
+
// 1. CANAL LOGS (Technique)
|
|
12
|
+
export function emitLogToast(type, message, title) {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const verbosity = config.gui.logs;
|
|
15
|
+
if (verbosity === 'none')
|
|
16
|
+
return;
|
|
17
|
+
if (verbosity === 'error' && type !== 'error' && type !== 'warning')
|
|
18
|
+
return;
|
|
19
|
+
// 'verbose' shows all
|
|
20
|
+
dispatchToast('log', type, message, title || 'Pollinations Log');
|
|
21
|
+
}
|
|
22
|
+
// 2. CANAL STATUS (Dashboard)
|
|
23
|
+
export function emitStatusToast(type, message, title) {
|
|
24
|
+
const config = loadConfig();
|
|
25
|
+
const verbosity = config.gui.status;
|
|
26
|
+
if (verbosity === 'none')
|
|
27
|
+
return;
|
|
28
|
+
// 'alert' logic handled by caller (proxy.ts) usually, but we can filter here too?
|
|
29
|
+
// Actually, 'all' sends everything. 'alert' sends only warnings/errors.
|
|
30
|
+
if (verbosity === 'alert' && type !== 'error' && type !== 'warning')
|
|
31
|
+
return;
|
|
32
|
+
dispatchToast('status', type, message, title || 'Pollinations Status');
|
|
33
|
+
}
|
|
34
|
+
// INTERNAL DISPATCHER
|
|
35
|
+
function dispatchToast(channel, type, message, title) {
|
|
36
|
+
const toast = {
|
|
37
|
+
id: `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
38
|
+
channel,
|
|
39
|
+
type,
|
|
40
|
+
title,
|
|
41
|
+
message,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
displayed: false
|
|
44
|
+
};
|
|
45
|
+
toastQueue.push(toast);
|
|
46
|
+
logToastToFile(toast);
|
|
47
|
+
if (globalClient) {
|
|
48
|
+
globalClient.tui.showToast({
|
|
49
|
+
body: {
|
|
50
|
+
title: toast.title,
|
|
51
|
+
message: toast.message,
|
|
52
|
+
variant: toast.type,
|
|
53
|
+
duration: channel === 'status' ? 6000 : 4000 // Status stays longer
|
|
54
|
+
}
|
|
55
|
+
}).then(() => {
|
|
56
|
+
toast.displayed = true;
|
|
57
|
+
}).catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
while (toastQueue.length > 20) {
|
|
60
|
+
toastQueue.shift();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// === HELPERS ===
|
|
64
|
+
function logToastToFile(toast) {
|
|
65
|
+
try {
|
|
66
|
+
const logLine = `[${new Date(toast.timestamp).toISOString()}] [${toast.channel.toUpperCase()}] [${toast.type.toUpperCase()}] ${toast.message}`;
|
|
67
|
+
fs.appendFileSync('/tmp/pollinations-toasts.log', logLine + '\n');
|
|
68
|
+
}
|
|
69
|
+
catch (e) { }
|
|
70
|
+
}
|
|
71
|
+
export function createToastHooks(client) {
|
|
72
|
+
return {
|
|
73
|
+
'session.idle': async ({ event }) => {
|
|
74
|
+
// Deprecated: We use immediate dispatch now.
|
|
75
|
+
// Kept for backward compat if needed or legacy queued items.
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-pollinations-plugin",
|
|
3
|
+
"displayName": "Pollinations AI (V5.1)",
|
|
4
|
+
"version": "5.1.3",
|
|
5
|
+
"description": "Native Pollinations.ai Provider Plugin for OpenCode",
|
|
6
|
+
"publisher": "pollinations",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/fkom13/opencode-pollinations-plugin.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"engines": {
|
|
21
|
+
"vscode": "^1.80.0"
|
|
22
|
+
},
|
|
23
|
+
"activationEvents": [
|
|
24
|
+
"onStartupFinished"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"package": "npx vsce package"
|
|
29
|
+
},
|
|
30
|
+
"contributes": {
|
|
31
|
+
"commands": [
|
|
32
|
+
{
|
|
33
|
+
"command": "pollinations.mode",
|
|
34
|
+
"title": "Pollinations: Change Mode"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"command": "pollinations.usage",
|
|
38
|
+
"title": "Pollinations: Show Usage"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@opencode-ai/plugin": "^1.0.85",
|
|
47
|
+
"zod": "^3.22.4"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|