@te-river/opencode-alibabatokenplan 0.1.0

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/dist/server.js ADDED
@@ -0,0 +1,1258 @@
1
+ // src/server/index.ts
2
+ import * as http from "node:http";
3
+ import * as cp from "node:child_process";
4
+ import * as fs2 from "node:fs";
5
+ import * as path2 from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ // src/core/bailian-snapshot.ts
9
+ var CREDITS_PER_YUAN = 250;
10
+ var BAILIAN_SNAPSHOT_AT = "2026-08-30";
11
+ var TOKEN_PLAN_TEXT_MODELS = [
12
+ "qwen3.8-max",
13
+ "qwen3.8-flash",
14
+ "qwen3.7-max",
15
+ "qwen3.7-plus",
16
+ "qwen3.6-plus",
17
+ "qwen3.6-flash",
18
+ "glm-5.2",
19
+ "deepseek-v4-pro-0813"
20
+ ];
21
+ var BAILIAN_WINDOW_RULES = [
22
+ {
23
+ id: "night-qwen38-max",
24
+ label: "Token Plan \u591C\u95F4\u4E94\u6298",
25
+ models: ["qwen3.8-max"],
26
+ factor: 0.5,
27
+ start: "22:00",
28
+ end: "08:00",
29
+ source: "\u5B98\u65B9\u6587\u6863 \xB7 Token Plan\uFF08\u4E2A\u4EBA\u7248\uFF09\u6982\u8FF0"
30
+ },
31
+ {
32
+ id: "night-deepseek-v4-pro",
33
+ label: "Token Plan \u591C\u95F4\u4E94\u6298",
34
+ models: ["deepseek-v4-pro-0813"],
35
+ factor: 0.5,
36
+ start: "22:00",
37
+ end: "08:00",
38
+ source: "\u5B98\u65B9\u6587\u6863 \xB7 Token Plan\uFF08\u4E2A\u4EBA\u7248\uFF09\u6982\u8FF0"
39
+ }
40
+ ];
41
+ function bailianSnapshotModels() {
42
+ const P = (name, type, price, unit = "\u6BCF\u767E\u4E07tokens", band, discount) => ({ name, type, price, unit, ...band ? { band } : {}, ...discount !== void 0 ? { discount } : {} });
43
+ return [
44
+ {
45
+ family: "qwen3.8-max",
46
+ model: "qwen3.8-max",
47
+ provider: "qwen",
48
+ context: 1e6,
49
+ prices: [P("\u8F93\u5165", "input_token", 12), P("\u8F93\u51FA", "output_token", 36), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 1.5), P("\u8F93\u5165\uFF08Batch Chat\uFF09", "input_token_batch_chat", 12, "\u6BCF\u767E\u4E07tokens", void 0, 0.5), P("\u8F93\u51FA\uFF08Batch Chat\uFF09", "output_token_batch_chat", 36, "\u6BCF\u767E\u4E07tokens", void 0, 0.5)]
50
+ },
51
+ {
52
+ family: "qwen3.8-flash",
53
+ model: "qwen3.8-flash",
54
+ provider: "qwen",
55
+ context: 1e6,
56
+ prices: [P("\u8F93\u5165", "input_token", 0.8), P("\u8F93\u51FA", "output_token", 2.7), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 0.1)]
57
+ },
58
+ {
59
+ family: "qwen3.7-max",
60
+ model: "qwen3.7-max",
61
+ provider: "qwen",
62
+ context: 1e6,
63
+ prices: [P("\u8F93\u5165", "input_token", 12), P("\u8F93\u51FA", "output_token", 36), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 2.4), P("\u8F93\u5165", "input_token", 12, "\u6BCF\u767E\u4E07tokens", "standard", 0.5)]
64
+ },
65
+ {
66
+ family: "qwen3.6-plus",
67
+ model: "qwen3.6-plus",
68
+ provider: "qwen",
69
+ context: 1e6,
70
+ prices: [P("\u8F93\u5165", "input_token", 0.8), P("\u8F93\u51FA", "output_token", 4.8), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 0.08)]
71
+ },
72
+ {
73
+ family: "deepseek",
74
+ model: "deepseek-v4-pro-0813",
75
+ provider: "deepseek",
76
+ context: 1e6,
77
+ prices: [P("\u8F93\u5165", "input_token", 9, "\u6BCF\u767E\u4E07tokens", "peak"), P("\u8F93\u5165", "input_token", 4.5, "\u6BCF\u767E\u4E07tokens", "offpeak"), P("\u8F93\u51FA", "output_token", 27, "\u6BCF\u767E\u4E07tokens", "peak"), P("\u8F93\u51FA", "output_token", 13.5, "\u6BCF\u767E\u4E07tokens", "offpeak"), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 0.9, "\u6BCF\u767E\u4E07tokens", "peak"), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 0.45, "\u6BCF\u767E\u4E07tokens", "offpeak")]
78
+ },
79
+ {
80
+ family: "deepseek",
81
+ model: "deepseek-v4-pro",
82
+ provider: "deepseek",
83
+ context: 1e6,
84
+ prices: [P("\u8F93\u5165", "input_token", 12), P("\u8F93\u51FA", "output_token", 24), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 1)]
85
+ },
86
+ {
87
+ family: "glm-5.2",
88
+ model: "glm-5.2",
89
+ provider: "zhipu",
90
+ context: 2e5,
91
+ prices: [P("\u8F93\u5165", "input_token", 8), P("\u8F93\u51FA", "output_token", 28), P("\u8F93\u5165\uFF08\u7F13\u5B58\u547D\u4E2D\uFF09", "input_token_cache", 2)]
92
+ },
93
+ {
94
+ family: "wan-video",
95
+ model: "wan-video",
96
+ provider: "wan",
97
+ prices: [P("\u89C6\u9891\u751F\u6210\uFF08480P\uFF09", "video_ratio_480p", 0.45, "\u6BCF\u79D2"), P("\u89C6\u9891\u751F\u6210\uFF08720P\uFF09", "video_ratio_720p", 0.9, "\u6BCF\u79D2"), P("\u89C6\u9891\u751F\u6210\uFF081080P\uFF09", "video_ratio_1080p", 1.8, "\u6BCF\u79D2"), P("\u89C6\u9891\u751F\u6210\uFF08720P\xB7\u9650\u65F6\uFF09", "video_ratio_720p", 0.6, "\u6BCF\u79D2", void 0, 0.7)]
98
+ },
99
+ {
100
+ family: "qwen-image",
101
+ model: "qwen-image",
102
+ provider: "qwen",
103
+ prices: [P("\u56FE\u7247\u751F\u6210", "image_number", 0.25, "\u6BCF\u5F20")]
104
+ },
105
+ {
106
+ family: "qwen-image-plus",
107
+ model: "qwen-image-plus",
108
+ provider: "qwen",
109
+ prices: [P("\u56FE\u7247\u751F\u6210", "image_number", 0.2, "\u6BCF\u5F20")]
110
+ }
111
+ ];
112
+ }
113
+ function parseGroups(list) {
114
+ const out = [];
115
+ const byName = /* @__PURE__ */ new Set();
116
+ for (const g of list || []) {
117
+ const items = Array.isArray(g.items) && g.items.length ? g.items : [g];
118
+ for (const it of items) {
119
+ const model = String(it.model || "");
120
+ if (!model) continue;
121
+ const m = model.match(/^(.*)-\d{4}-\d{2}-\d{2}$/);
122
+ if (m && byName.has(m[1])) continue;
123
+ const prices = [];
124
+ const mp = Array.isArray(it.multiPrices) ? it.multiPrices : [];
125
+ if (mp.length) {
126
+ for (const range of mp) {
127
+ for (const p of range.prices || []) {
128
+ prices.push({
129
+ name: p.priceName ?? p.type ?? "",
130
+ type: p.type ?? "",
131
+ price: Number(p.price),
132
+ unit: p.priceUnit ?? "",
133
+ ...p.timeBand ? { band: p.timeBand } : {},
134
+ ...p.discount !== void 0 && p.discount !== null ? { discount: Number(p.discount) } : {},
135
+ ...range.rangeName ? { range: range.rangeName } : {}
136
+ });
137
+ }
138
+ }
139
+ } else if (Array.isArray(it.prices)) {
140
+ for (const p of it.prices) {
141
+ prices.push({
142
+ name: p.priceName ?? p.type ?? "",
143
+ type: p.type ?? "",
144
+ price: Number(p.price),
145
+ unit: p.priceUnit ?? "",
146
+ ...p.timeBand ? { band: p.timeBand } : {},
147
+ ...p.discount !== void 0 && p.discount !== null ? { discount: Number(p.discount) } : {}
148
+ });
149
+ }
150
+ }
151
+ byName.add(model);
152
+ out.push({ family: model, model, provider: it.provider ?? g.provider ?? "", context: it.contextWindow, prices });
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ // src/core/qoder-snapshot.ts
159
+ var QODER_SNAPSHOT_DATE = "2026-08-31";
160
+ var W_OFFPEAK = { start: "22:00", end: "08:00", tz: "Asia/Shanghai" };
161
+ var L_QWENMAX = "https://docs.qoder.cn/events/qwen-max";
162
+ var L_QWEN37 = "https://docs.qoder.cn/product-overview/qwen-3-7-series-model-staggering-discount";
163
+ var L_OFFPEAK_RULES = "https://help.aliyun.com/document_detail/3042139.html";
164
+ var L_ULTIMATE = "https://docs.qoder.com/zh/events/ultimatediscount";
165
+ var L_CANTUS = "https://docs.qoder.com/zh/events/cantuslaunch";
166
+ var L_FREECALLS = "https://docs.qoder.com/zh/events/ultimatefreecalls";
167
+ var L_REFERRAL = "https://qoder.cn/referral";
168
+ function qoderSnapshotOffers() {
169
+ const S = QODER_SNAPSHOT_DATE;
170
+ const o = (r) => ({
171
+ ...r,
172
+ origins: ["qoder-snapshot"],
173
+ snapshotDate: S
174
+ });
175
+ return [
176
+ o({
177
+ id: "atp-offpeak-qwen38",
178
+ kind: "time-window",
179
+ label: "\u9519\u5CF0 5 \u6298\uFF08Off-Peak 50% off\uFF09",
180
+ models: ["qwen3.8-max"],
181
+ unit: "credits-factor",
182
+ factor: 0.25,
183
+ baseFactor: 0.5,
184
+ window: { ...W_OFFPEAK },
185
+ note: "\u5E38\u89C4 0.5x \u2192 \u9519\u5CF0\u65F6\u6BB5 0.25x\uFF085 \u6298\uFF09",
186
+ links: [L_QWENMAX]
187
+ }),
188
+ o({
189
+ id: "atp-offpeak-qwen37max",
190
+ kind: "time-window",
191
+ label: "\u9519\u5CF0 2 \u6298\uFF08Off-Peak 80% off\uFF09",
192
+ models: ["qwen3.7-max"],
193
+ unit: "credits-factor",
194
+ factor: 0.1,
195
+ baseFactor: 0.5,
196
+ window: { ...W_OFFPEAK },
197
+ note: "\u5E38\u89C4 0.5x \u2192 \u9519\u5CF0\u65F6\u6BB5 0.1x\uFF082 \u6298\uFF09",
198
+ links: [L_QWEN37]
199
+ }),
200
+ o({
201
+ id: "atp-offpeak-qwen37plus",
202
+ kind: "time-window",
203
+ label: "\u9519\u5CF0 4 \u6298\uFF08Off-Peak 60% off\uFF09",
204
+ models: ["qwen3.7-plus"],
205
+ unit: "credits-factor",
206
+ factor: 0.04,
207
+ baseFactor: 0.1,
208
+ window: { ...W_OFFPEAK },
209
+ note: "\u5E38\u89C4 0.1x \u2192 \u9519\u5CF0\u65F6\u6BB5 0.04x\uFF084 \u6298\uFF09",
210
+ links: [L_QWEN37]
211
+ }),
212
+ o({
213
+ id: "atp-flat-preview",
214
+ kind: "flat-cut",
215
+ label: "\u9650\u65F6 1 \u6298",
216
+ models: ["qwen3.8-max-preview"],
217
+ unit: "credits-factor",
218
+ factor: 0.1,
219
+ note: "\u6A21\u578B\u63CF\u8FF0\u539F\u6587\uFF1A\u5E38\u89C4\u65F6\u6BB5 1 \u6298",
220
+ links: [L_OFFPEAK_RULES]
221
+ }),
222
+ o({
223
+ id: "atp-flat-glm53f",
224
+ kind: "flat-cut",
225
+ label: "\u5212\u7EBF\u534A\u4EF7\uFF080.1 \u2192 0.05\uFF09",
226
+ models: ["glm-5.3-flash"],
227
+ unit: "credits-factor",
228
+ factor: 0.05,
229
+ baseFactor: 0.1,
230
+ note: "originalPriceFactor \u7ED3\u6784\uFF0C\u65E0 promotion \u6807\u8BB0\uFF08\u5FEB\u7167\uFF09",
231
+ links: [L_OFFPEAK_RULES]
232
+ }),
233
+ o({
234
+ id: "atp-flat-ultimate",
235
+ kind: "flat-cut",
236
+ label: "\u9650\u65F6\u6298\u6263\u8FDB\u884C\u4E2D",
237
+ models: ["ultimate", "cantus"],
238
+ unit: "credits-factor",
239
+ note: "\u6570\u503C\u672A\u516C\u5F00\uFF0C\u4EC5\u6D3B\u52A8\u6587\u6848\uFF08\u5FEB\u7167\uFF09",
240
+ links: [L_ULTIMATE, L_CANTUS]
241
+ }),
242
+ o({
243
+ id: "atp-free-200",
244
+ kind: "free-calls",
245
+ label: "\u6781\u81F4\u6A21\u578B 200 \u6B21\u514D\u8D39",
246
+ models: [],
247
+ unit: "credits-factor",
248
+ note: "\u514D\u8D39\u989D\u5EA6\u6D3B\u52A8\uFF1B\u5DF2\u8FC7\u671F\uFF08expired:true\uFF09",
249
+ expiry: "2026-07-30T23:59:59+08:00",
250
+ links: [L_FREECALLS]
251
+ }),
252
+ o({
253
+ id: "atp-free-800-2000",
254
+ kind: "free-calls",
255
+ label: "qwen3.8-max 800/2000 \u6B21\u514D\u8D39\uFF08claim\uFF09",
256
+ models: ["qwen3.8-max"],
257
+ unit: "credits-factor",
258
+ note: "activityId: qwen38_800_invoke / qwen38_2000_invoke\uFF08\u5FEB\u7167\uFF09",
259
+ links: []
260
+ }),
261
+ o({
262
+ id: "atp-referral",
263
+ kind: "policy",
264
+ label: "\u63A8\u8350\u8FD4\u79EF\u5206 \u226440000+",
265
+ models: [],
266
+ unit: "credits-factor",
267
+ note: "\u63A8\u8350\u6D3B\u52A8\u81F3 2026-09-03",
268
+ expiry: "2026-09-03T23:59:59+08:00",
269
+ links: [L_REFERRAL]
270
+ }),
271
+ o({
272
+ id: "atp-lite",
273
+ kind: "policy",
274
+ label: "lite \u57FA\u7840\u6A21\u578B\u964D\u7EA7\u653F\u7B56",
275
+ models: ["lite"],
276
+ unit: "credits-factor",
277
+ note: "Credits \u8017\u5C3D\u540E\u964D\u7EA7\u4E3A lite \u57FA\u7840\u6A21\u578B\uFF08\u514D\u8D39/\u6BCF\u65E5\u9650\u989D/\u9AD8\u5CF0\u65F6\u6BB5\u53D8\u6162\uFF09\uFF1B\u4F59\u989D <200 \u4F4E\u989D\u63D0\u793A\uFF1B\u9996\u6708 prorated\uFF08\u5FEB\u7167\uFF09",
278
+ links: []
279
+ })
280
+ ];
281
+ }
282
+ var QODER_INSALE_FACTORS = {
283
+ auto: { factor: 0.5 },
284
+ "qwen3.8-flash": { factor: 0.1 },
285
+ "qwen3.7-flash": { factor: 0.1 },
286
+ "deepseek-v4-pro": { factor: 0.8 },
287
+ "deepseek-v4-flash": { factor: 0.3 },
288
+ "glm-5.3": { factor: 0.6 },
289
+ "glm-5.2": { factor: 0.6 },
290
+ "kimi-k2.7-code": { factor: 0.3, note: "highspeed 0.6" },
291
+ "minimax-m2.7": { factor: 0.2 }
292
+ };
293
+
294
+ // src/core/merge.ts
295
+ function originRank(origin) {
296
+ return { "bailian-live": 0, "bailian-cli": 1, "qoder-snapshot": 2, "bailian-snapshot": 3 }[origin];
297
+ }
298
+ var OFFPEAK_WINDOW = { start: "22:00", end: "08:00", tz: "Asia/Shanghai" };
299
+ function catalogToOffers(models, origin, snapshotDate) {
300
+ const out = [];
301
+ const offpeakModels = models.filter((m) => m.prices.some((p) => p.band === "offpeak")).map((m) => m.model.toLowerCase());
302
+ if (offpeakModels.length) {
303
+ out.push({
304
+ id: "catalog-offpeak",
305
+ kind: "time-window",
306
+ label: "\u76EE\u5F55\u591C\u95F4\u4F18\u60E0\uFF08\u9519\u5CF0 5 \u6298\uFF09",
307
+ models: offpeakModels,
308
+ unit: "cny-price",
309
+ factor: 0.5,
310
+ window: { ...OFFPEAK_WINDOW },
311
+ note: "\u76EE\u5F55\u542B offpeak \u6863\u7684\u6A21\u578B\uFF0C\u8C37\u65F6\u4EF7=\u5CF0\u65F6 5 \u6298",
312
+ links: [],
313
+ origins: [origin],
314
+ ...snapshotDate ? { snapshotDate } : {}
315
+ });
316
+ }
317
+ const windowedModels = /* @__PURE__ */ new Set([
318
+ ...out.filter((o) => o.kind === "time-window").flatMap((o) => o.models.map((m) => m.toLowerCase())),
319
+ ...BAILIAN_WINDOW_RULES.flatMap((r) => r.models.map((m) => m.toLowerCase()))
320
+ ]);
321
+ for (const m of models) {
322
+ const model = m.model.toLowerCase();
323
+ if (windowedModels.has(model)) continue;
324
+ const std = m.prices.find((p) => p.type === "input_token" && p.discount !== void 0 && p.discount < 1);
325
+ const modelFactor = std?.discount;
326
+ const batchEntry = m.prices.find((p) => /batch/i.test(p.type) && p.discount !== void 0 && p.discount < 1);
327
+ const batchCombined = batchEntry ? Math.round((batchEntry.discount ?? 1) * (modelFactor ?? 1) * 1e3) / 1e3 : void 0;
328
+ if (modelFactor === void 0 && batchCombined === void 0) continue;
329
+ const factors = [];
330
+ if (modelFactor !== void 0) factors.push(modelFactor);
331
+ else if (batchCombined !== void 0) factors.push(batchCombined);
332
+ out.push({
333
+ id: "limited-" + model,
334
+ kind: "flat-cut",
335
+ label: "\u9650\u65F6\u4F18\u60E0",
336
+ models: [model],
337
+ unit: "cny-price",
338
+ factor: factors[0],
339
+ ...batchCombined !== void 0 ? { batchStack: batchCombined } : {},
340
+ note: modelFactor !== void 0 ? `\u6A21\u578B\u9650\u65F6 \xD7${modelFactor}` + (batchCombined !== void 0 ? `\uFF1BBatch \u53E0\u52A0 \xD7${batchCombined}` : "") : `\u4EC5 Batch \u7EF4\u5EA6 \xD7${batchCombined}`,
341
+ links: [],
342
+ origins: [origin],
343
+ ...snapshotDate ? { snapshotDate } : {}
344
+ });
345
+ }
346
+ return out;
347
+ }
348
+ function mergeKey(r) {
349
+ return [r.kind, r.unit, r.window ? `${r.window.start}-${r.window.end}@${r.window.tz}` : "-", r.factor ?? "-", r.baseFactor ?? "-", [...r.models].sort().join(",")].join("|");
350
+ }
351
+ function explodePerModel(rules) {
352
+ const out = [];
353
+ for (const r of rules) {
354
+ if (r.models.length <= 1) {
355
+ out.push(r);
356
+ continue;
357
+ }
358
+ for (const m of new Set(r.models)) out.push({ ...r, id: `${r.id}#${m}`, models: [m] });
359
+ }
360
+ return out;
361
+ }
362
+ function mergeOffers(all) {
363
+ const byKey = /* @__PURE__ */ new Map();
364
+ for (const r of explodePerModel(all)) {
365
+ const k = mergeKey(r);
366
+ const prev = byKey.get(k);
367
+ if (!prev) {
368
+ byKey.set(k, { ...r, models: [...r.models], origins: [...r.origins], links: [...r.links] });
369
+ continue;
370
+ }
371
+ for (const m of r.models) if (!prev.models.includes(m)) prev.models.push(m);
372
+ for (const o of r.origins) if (!prev.origins.includes(o)) prev.origins.push(o);
373
+ for (const l of r.links) if (l && !prev.links.includes(l)) prev.links.push(l);
374
+ if (r.snapshotDate && (!prev.snapshotDate || r.snapshotDate > prev.snapshotDate)) prev.snapshotDate = r.snapshotDate;
375
+ }
376
+ const list = [...byKey.values()];
377
+ const rank = (r) => Math.min(...r.origins.map(originRank));
378
+ const kindOrder = { "time-window": 0, "flat-cut": 1, "free-calls": 2, policy: 3 };
379
+ list.sort((a, b) => {
380
+ const rd = rank(a) - rank(b);
381
+ if (rd) return rd;
382
+ const kd = kindOrder[a.kind] - kindOrder[b.kind];
383
+ if (kd) return kd;
384
+ const fa = a.factor ?? 1, fb = b.factor ?? 1;
385
+ if (fa !== fb) return fa - fb;
386
+ return a.id.localeCompare(b.id);
387
+ });
388
+ return list;
389
+ }
390
+ function isCrossVerified(r) {
391
+ const hasQ = r.origins.includes("qoder-snapshot");
392
+ const hasB = r.origins.some((o) => o.startsWith("bailian-"));
393
+ return hasQ && hasB;
394
+ }
395
+
396
+ // src/core/rules.ts
397
+ var SH_OFFSET_MIN = 8 * 60;
398
+ var DAY_MIN = 1440;
399
+ function shanghaiMinutes(now = /* @__PURE__ */ new Date()) {
400
+ const shifted = new Date(now.getTime() + (now.getTimezoneOffset() + SH_OFFSET_MIN) * 6e4);
401
+ return shifted.getHours() * 60 + shifted.getMinutes() + shifted.getSeconds() / 60;
402
+ }
403
+ function parseHm(s) {
404
+ const [h, min] = s.split(":").map(Number);
405
+ return h * 60 + (min || 0);
406
+ }
407
+ function windowState(rule, now = /* @__PURE__ */ new Date()) {
408
+ const start = parseHm(rule.start);
409
+ const end = parseHm(rule.end);
410
+ const m = shanghaiMinutes(now);
411
+ const wraps = start > end;
412
+ const active = wraps ? m >= start || m < end : m >= start && m < end;
413
+ let toNext;
414
+ let kind;
415
+ if (active) {
416
+ toNext = wraps ? m >= start ? end + DAY_MIN - m : end - m : end - m;
417
+ kind = "end";
418
+ } else {
419
+ toNext = m < start ? start - m : start + DAY_MIN - m;
420
+ kind = "start";
421
+ }
422
+ return {
423
+ active,
424
+ minutesToNextBoundary: Math.max(0, Math.round(toNext)),
425
+ boundaryKind: kind,
426
+ nextBoundaryAtMs: now.getTime() + toNext * 6e4,
427
+ nextBoundaryAt: shanghaiISO(now.getTime() + toNext * 6e4)
428
+ };
429
+ }
430
+ function shanghaiISO(ms) {
431
+ const d = new Date(ms + SH_OFFSET_MIN * 6e4);
432
+ const p = (n, w = 2) => String(n).padStart(w, "0");
433
+ return d.getUTCFullYear() + "-" + p(d.getUTCMonth() + 1) + "-" + p(d.getUTCDate()) + "T" + p(d.getUTCHours()) + ":" + p(d.getUTCMinutes()) + ":" + p(d.getUTCSeconds()) + "." + p(d.getUTCMilliseconds(), 3) + "+08:00";
434
+ }
435
+ function fmtCountdown(mins) {
436
+ if (mins <= 0) return "\u4E0D\u8DB3 1 \u5206\u949F";
437
+ const d = Math.floor(mins / DAY_MIN);
438
+ const h = Math.floor(mins % DAY_MIN / 60);
439
+ const m = mins % 60;
440
+ const parts = [];
441
+ if (d) parts.push(d + " \u5929");
442
+ if (h) parts.push(h + " \u5C0F\u65F6");
443
+ parts.push(m + " \u5206\u949F");
444
+ return parts.join(" ");
445
+ }
446
+
447
+ // src/core/text.ts
448
+ function fmtFactor(f) {
449
+ if (f === void 0 || !Number.isFinite(f)) return void 0;
450
+ let s = f.toFixed(2);
451
+ s = s.replace(/0+$/, "").replace(/\.$/, ".0");
452
+ return s + "x";
453
+ }
454
+ function showStrike(base, cur) {
455
+ const b = fmtFactor(base), c = fmtFactor(cur);
456
+ return !!b && !!c && b !== c;
457
+ }
458
+ function strikethrough(s) {
459
+ return s.replace(/[^\u0300-\u036f]/g, (c) => c + "\u0336");
460
+ }
461
+
462
+ // src/core/status.ts
463
+ import * as fs from "node:fs";
464
+ import * as path from "node:path";
465
+ import * as os from "node:os";
466
+ var WRITTEN_BY = "@te-river/opencode-alibabatokenplan/server@0.1.0";
467
+ var STALE_AFTER_SEC = 600;
468
+ var CATALOG_ORIGIN = {
469
+ live: "bailian-live",
470
+ cli: "bailian-cli",
471
+ snapshot: "bailian-snapshot"
472
+ };
473
+ function windowId(start, end) {
474
+ if (start === "22:00" && end === "08:00") return "offpeak-22-08";
475
+ return `win-${start.replace(":", "")}-${end.replace(":", "")}`;
476
+ }
477
+ function buildOffers(catalog) {
478
+ const origin = CATALOG_ORIGIN[catalog.source] ?? "bailian-snapshot";
479
+ const legacy = BAILIAN_WINDOW_RULES.map((r) => ({
480
+ id: r.id,
481
+ kind: "time-window",
482
+ label: r.label,
483
+ models: [...r.models],
484
+ unit: "cny-price",
485
+ factor: r.factor,
486
+ window: { start: r.start, end: r.end, tz: "Asia/Shanghai" },
487
+ note: r.source,
488
+ links: [],
489
+ origins: ["bailian-snapshot"],
490
+ snapshotDate: BAILIAN_SNAPSHOT_AT
491
+ }));
492
+ return mergeOffers([...qoderSnapshotOffers(), ...legacy, ...catalogToOffers(catalog.models, origin, catalog.fetchedAt?.slice(0, 10))]);
493
+ }
494
+ function buildWindows(offers, now) {
495
+ const byWindow = /* @__PURE__ */ new Map();
496
+ for (const r of offers) {
497
+ if (r.kind !== "time-window" || !r.window) continue;
498
+ const key = `${r.window.start}-${r.window.end}@${r.window.tz}`;
499
+ let g = byWindow.get(key);
500
+ if (!g) {
501
+ g = { ...r.window, rules: [] };
502
+ byWindow.set(key, g);
503
+ }
504
+ g.rules.push(r);
505
+ }
506
+ const out = [];
507
+ for (const g of byWindow.values()) {
508
+ const w = windowState(g, now);
509
+ const models = [];
510
+ for (const r of g.rules) {
511
+ const cross = isCrossVerified(r);
512
+ for (const m of r.models) {
513
+ models.push({
514
+ model: m,
515
+ regular: fmtFactor(r.baseFactor),
516
+ effective: fmtFactor(r.factor),
517
+ badge: r.label + (cross ? " \xB7 \u53CC\u6E90\u5370\u8BC1" : ""),
518
+ origins: r.origins,
519
+ links: r.links
520
+ });
521
+ }
522
+ }
523
+ models.sort((a, b) => {
524
+ const fa = parseFloat(a.effective ?? "1"), fb = parseFloat(b.effective ?? "1");
525
+ if (fa !== fb) return fa - fb;
526
+ return a.model.localeCompare(b.model);
527
+ });
528
+ out.push({
529
+ id: windowId(g.start, g.end),
530
+ label: g.rules[0].label,
531
+ start: g.start,
532
+ end: g.end,
533
+ tz: g.tz,
534
+ activeNow: w.active,
535
+ boundaryKind: w.boundaryKind,
536
+ nextBoundaryAt: w.nextBoundaryAt,
537
+ models
538
+ });
539
+ }
540
+ out.sort((a, b) => Number(b.id.startsWith("offpeak")) - Number(a.id.startsWith("offpeak")) || a.start.localeCompare(b.start));
541
+ return out;
542
+ }
543
+ function buildDiscounts(offers) {
544
+ const byModel = /* @__PURE__ */ new Map();
545
+ for (const r of offers) {
546
+ if (r.kind === "free-calls" || r.kind === "policy") continue;
547
+ for (const m of r.models) {
548
+ const arr = byModel.get(m) ?? [];
549
+ arr.push(r);
550
+ byModel.set(m, arr);
551
+ }
552
+ }
553
+ const out = [];
554
+ for (const [model, applies] of byModel) {
555
+ const bestFactors = {};
556
+ for (const r of applies) {
557
+ if (r.factor === void 0) continue;
558
+ const cur = bestFactors[r.unit];
559
+ if (cur === void 0 || r.factor < cur) bestFactors[r.unit] = r.factor;
560
+ }
561
+ out.push({
562
+ model,
563
+ applies,
564
+ ...Object.keys(bestFactors).length ? { bestFactors } : {}
565
+ });
566
+ }
567
+ const lowest = (d) => {
568
+ const vs = Object.values(d.bestFactors ?? {});
569
+ return vs.length ? Math.min(...vs) : 1;
570
+ };
571
+ const seriesOrder = (n) => /^qwen/.test(n) ? 0 : /^(deepseek|glm|kimi|minimax)/.test(n) ? 1 : 2;
572
+ out.sort((a, b) => {
573
+ const fa = lowest(a), fb = lowest(b);
574
+ if (fa !== fb) return fa - fb;
575
+ const sd = seriesOrder(a.model) - seriesOrder(b.model);
576
+ if (sd) return sd;
577
+ return a.model.localeCompare(b.model);
578
+ });
579
+ return out;
580
+ }
581
+ function pickPrice(prices, type, band) {
582
+ return prices.find((p) => p.type === type && (band ? p.band === band : !p.band || p.band === "standard"));
583
+ }
584
+ function creditsPerM(price) {
585
+ const v = price * CREDITS_PER_YUAN;
586
+ return Number.isInteger(v) ? v.toLocaleString("en-US") : String(Number(v.toFixed(1)));
587
+ }
588
+ function buildTextModels(models, windows, now) {
589
+ return TOKEN_PLAN_TEXT_MODELS.map((name) => {
590
+ const m = models.find((x) => x.model === name) || models.find((x) => x.family === name);
591
+ let badge;
592
+ for (const w of windows) {
593
+ if (w.models.some((x) => x.model === name)) {
594
+ const ws = windowState(w, now);
595
+ badge = {
596
+ label: w.label + (w.models.find((x) => x.model === name)?.badge?.includes("\u53CC\u6E90") ? " \xB7 \u53CC\u6E90\u5370\u8BC1" : ""),
597
+ countdown: ws.active ? `${fmtCountdown(ws.minutesToNextBoundary)}\u540E\u7ED3\u675F\uFF08${w.end}\uFF09` : `${fmtCountdown(ws.minutesToNextBoundary)}\u540E\u5F00\u59CB\uFF08${w.start}\uFF09`
598
+ };
599
+ break;
600
+ }
601
+ }
602
+ const insale = QODER_INSALE_FACTORS[name];
603
+ if (!m) {
604
+ return { model: name, input: "\u2014", cache: "\u2014", output: "\u2014", ...insale ? { creditsFactor: fmtFactor(insale.factor) } : {}, ...badge ? { badge } : {} };
605
+ }
606
+ const peakIn = pickPrice(m.prices, "input_token", "peak");
607
+ const offIn = pickPrice(m.prices, "input_token", "offpeak");
608
+ const stdIn = pickPrice(m.prices, "input_token");
609
+ const stdOut = pickPrice(m.prices, "output_token", "peak") || pickPrice(m.prices, "output_token");
610
+ const offOut = pickPrice(m.prices, "output_token", "offpeak");
611
+ const stdCache = pickPrice(m.prices, "input_token_cache", "peak") || pickPrice(m.prices, "input_token_cache") || pickPrice(m.prices, "input_token_cache_read");
612
+ const offCache = pickPrice(m.prices, "input_token_cache", "offpeak") || pickPrice(m.prices, "input_token_cache_read", "offpeak");
613
+ const dual = !!(peakIn && offIn);
614
+ const pair = (a, b) => a ? b ? `${creditsPerM(a.price)} / ${creditsPerM(b.price)}` : creditsPerM(a.price) : "\u2014";
615
+ return {
616
+ model: name,
617
+ input: dual ? pair(peakIn, offIn) : pair(stdIn),
618
+ cache: dual ? pair(stdCache, offCache) : pair(stdCache),
619
+ output: dual ? pair(stdOut, offOut) : pair(stdOut),
620
+ ...dual ? { bands: "\u5CF0/\u8C37" } : {},
621
+ ...insale ? { creditsFactor: fmtFactor(insale.factor) } : {},
622
+ ...badge ? { badge } : {}
623
+ };
624
+ });
625
+ }
626
+ function buildNotices(offers, now) {
627
+ return offers.filter((r) => r.kind === "free-calls" || r.kind === "policy").map((r) => ({
628
+ kind: r.kind,
629
+ text: `${r.label}${r.note ? " \u2014 " + r.note : ""}`,
630
+ source: r.origins.includes("qoder-snapshot") ? `Qoder \u5FEB\u7167 ${r.snapshotDate ?? QODER_SNAPSHOT_DATE}` : r.origins[0] ?? "unknown",
631
+ expired: r.expiry ? new Date(r.expiry).getTime() < now.getTime() : void 0
632
+ }));
633
+ }
634
+ function buildStatus(catalog, now = /* @__PURE__ */ new Date(), quota) {
635
+ const offers = buildOffers(catalog);
636
+ const windows = buildWindows(offers, now);
637
+ return {
638
+ schemaVersion: 1,
639
+ writtenBy: WRITTEN_BY,
640
+ updatedAt: now.toISOString(),
641
+ staleAfterSec: STALE_AFTER_SEC,
642
+ timezone: "Asia/Shanghai",
643
+ creditsPerYuan: CREDITS_PER_YUAN,
644
+ creditsRateNote: "\u636E\u767E\u70BC\u8BA1\u8D39\u793A\u4F8B\u6362\u7B97\uFF0C\u975E\u5B98\u65B9\u6C47\u7387",
645
+ sources: {
646
+ catalog: CATALOG_ORIGIN[catalog.source] ?? "bailian-snapshot",
647
+ qoderSnapshot: QODER_SNAPSHOT_DATE,
648
+ ...catalog.error ? { error: catalog.error } : {}
649
+ },
650
+ windows,
651
+ offers,
652
+ discounts: buildDiscounts(offers),
653
+ textModels: buildTextModels(catalog.models, windows, now),
654
+ quota: quota ?? { available: false, hint: "\u5B9E\u65F6\u989D\u5EA6\u9700: bl auth login --console --console-site domestic" },
655
+ notices: buildNotices(offers, now)
656
+ };
657
+ }
658
+ var STATUS_SIZE_CAP = 256 * 1024;
659
+ function stateDir() {
660
+ return process.env.ATP_STATE_DIR || path.join(os.homedir(), ".cache", "opencode", "alibabatokenplan");
661
+ }
662
+ function cacheFilePath() {
663
+ return path.join(stateDir(), "cache.json");
664
+ }
665
+ function portFilePath() {
666
+ return path.join(stateDir(), ".port");
667
+ }
668
+ function legacyCacheFile() {
669
+ return path.join(os.homedir(), ".cache", "opencode", "bailian-discount-cache.json");
670
+ }
671
+ function legacyPortFile() {
672
+ return path.join(os.homedir(), ".cache", "opencode", "bailian-discount-panel.port");
673
+ }
674
+ function configFilePath() {
675
+ return process.env.ATP_CONFIG_DIR ? path.join(process.env.ATP_CONFIG_DIR, "config.jsonc") : path.join(os.homedir(), ".config", "opencode", "alibabatokenplan", "config.jsonc");
676
+ }
677
+ function parseJsonc(text) {
678
+ let out = "";
679
+ let i = 0;
680
+ let inStr = false;
681
+ let esc = false;
682
+ while (i < text.length) {
683
+ const c = text[i];
684
+ const n = text[i + 1];
685
+ if (inStr) {
686
+ out += c;
687
+ if (esc) esc = false;
688
+ else if (c === "\\") esc = true;
689
+ else if (c === '"') inStr = false;
690
+ i++;
691
+ continue;
692
+ }
693
+ if (c === '"') {
694
+ inStr = true;
695
+ out += c;
696
+ i++;
697
+ continue;
698
+ }
699
+ if (c === "/" && n === "/") {
700
+ while (i < text.length && text[i] !== "\n") i++;
701
+ continue;
702
+ }
703
+ if (c === "/" && n === "*") {
704
+ i += 2;
705
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
706
+ i += 2;
707
+ continue;
708
+ }
709
+ out += c;
710
+ i++;
711
+ }
712
+ let res = "";
713
+ inStr = false;
714
+ esc = false;
715
+ i = 0;
716
+ while (i < out.length) {
717
+ const c = out[i];
718
+ if (inStr) {
719
+ res += c;
720
+ if (esc) esc = false;
721
+ else if (c === "\\") esc = true;
722
+ else if (c === '"') inStr = false;
723
+ i++;
724
+ continue;
725
+ }
726
+ if (c === '"') {
727
+ inStr = true;
728
+ res += c;
729
+ i++;
730
+ continue;
731
+ }
732
+ if (c === ",") {
733
+ let j = i + 1;
734
+ while (j < out.length && /\s/.test(out[j])) j++;
735
+ if (out[j] === "}" || out[j] === "]") {
736
+ i++;
737
+ continue;
738
+ }
739
+ }
740
+ res += c;
741
+ i++;
742
+ }
743
+ return JSON.parse(res);
744
+ }
745
+ var DEFAULT_CONFIG = {
746
+ webpanel: { enabled: false, port: 7777 },
747
+ sidebar: { order: 160, showNonTokenPlan: false },
748
+ toast: true,
749
+ refresh: { catalogMin: 10, quotaMin: 5 }
750
+ };
751
+ function loadConfig() {
752
+ const d = structuredClone(DEFAULT_CONFIG);
753
+ try {
754
+ const raw = parseJsonc(fs.readFileSync(configFilePath(), "utf8"));
755
+ if (raw && typeof raw === "object") {
756
+ if (raw.webpanel?.enabled !== void 0) d.webpanel.enabled = !!raw.webpanel.enabled;
757
+ if (Number.isInteger(raw.webpanel?.port) && raw.webpanel.port >= 1024 && raw.webpanel.port <= 65535) d.webpanel.port = raw.webpanel.port;
758
+ if (Number.isInteger(raw.sidebar?.order)) d.sidebar.order = raw.sidebar.order;
759
+ if (raw.sidebar?.showNonTokenPlan !== void 0) d.sidebar.showNonTokenPlan = !!raw.sidebar.showNonTokenPlan;
760
+ if (raw.toast !== void 0) d.toast = !!raw.toast;
761
+ if (Number.isInteger(raw.refresh?.catalogMin) && raw.refresh.catalogMin > 0) d.refresh.catalogMin = raw.refresh.catalogMin;
762
+ if (Number.isInteger(raw.refresh?.quotaMin) && raw.refresh.quotaMin > 0) d.refresh.quotaMin = raw.refresh.quotaMin;
763
+ }
764
+ } catch {
765
+ }
766
+ return d;
767
+ }
768
+ function writeStatusAtomic(st) {
769
+ try {
770
+ const dir = stateDir();
771
+ fs.mkdirSync(dir, { recursive: true });
772
+ let payload = JSON.stringify(st);
773
+ if (Buffer.byteLength(payload, "utf8") > STATUS_SIZE_CAP) {
774
+ const shrunk = { ...st, textModels: st.textModels.slice(0, 20), discounts: st.discounts.slice(0, 60), offers: st.offers.slice(0, 60), windows: st.windows.slice(0, 8) };
775
+ payload = JSON.stringify(shrunk);
776
+ if (Buffer.byteLength(payload, "utf8") > STATUS_SIZE_CAP) payload = JSON.stringify({ ...shrunk, textModels: [], discounts: [] });
777
+ }
778
+ const tmp = path.join(dir, "status.json.tmp");
779
+ fs.writeFileSync(tmp, payload);
780
+ fs.renameSync(tmp, path.join(dir, "status.json"));
781
+ return true;
782
+ } catch {
783
+ return false;
784
+ }
785
+ }
786
+
787
+ // src/server/index.ts
788
+ var PKG_ID = "@te-river/opencode-alibabatokenplan";
789
+ var state = { source: "snapshot", fetchedAt: BAILIAN_SNAPSHOT_AT, models: bailianSnapshotModels() };
790
+ var quotaState = { available: false, hint: "\u5B9E\u65F6\u989D\u5EA6\u9700\u8981\u63A7\u5236\u53F0\u6388\u6743\uFF1A\u6267\u884C\u4E00\u6B21 bl auth login --console --console-site domestic \u540E\u81EA\u52A8\u542F\u7528" };
791
+ var ZELDA_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels";
792
+ var GATEWAY_URL = "https://bailian-cs.console.aliyun.com/cli/api.json?action=BroadScopeAspnGateway&product=sfm_bailian&api=" + encodeURIComponent(ZELDA_API);
793
+ async function fetchCatalogPage(pageNo, pageSize) {
794
+ const params = JSON.stringify({
795
+ Api: ZELDA_API,
796
+ V: "1.0",
797
+ Data: {
798
+ input: {
799
+ pageNo,
800
+ pageSize,
801
+ name: "",
802
+ providers: [],
803
+ inferenceProviders: [],
804
+ features: [],
805
+ group: false,
806
+ capabilities: [],
807
+ contextWindows: [],
808
+ queryPermissions: false,
809
+ queryApplyStatus: false,
810
+ queryActivationStatus: false,
811
+ queryPrice: true,
812
+ queryQpmInfo: false,
813
+ supports: { inference: true }
814
+ },
815
+ cornerstoneParam: { protocol: "V2", console: "ONE_CONSOLE", productCode: "p_efm", switchUserType: 3, consoleSite: "BAILIAN_ALIYUN" }
816
+ }
817
+ });
818
+ const res = await fetch(GATEWAY_URL, {
819
+ method: "POST",
820
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
821
+ body: "params=" + encodeURIComponent(params) + "&region=cn-beijing",
822
+ signal: AbortSignal.timeout(2e4)
823
+ });
824
+ if (!res.ok) throw new Error(`catalog HTTP ${res.status}`);
825
+ const j = await res.json();
826
+ const inner = j?.data?.DataV2?.data?.data;
827
+ if (!inner || !Array.isArray(inner.list)) throw new Error("catalog \u54CD\u5E94\u7ED3\u6784\u5F02\u5E38: " + JSON.stringify(j).slice(0, 160));
828
+ return { total: Number(inner.total) || inner.list.length, list: inner.list };
829
+ }
830
+ async function fetchCatalogLive() {
831
+ const all = [];
832
+ const pageSize = 50;
833
+ let total = Infinity;
834
+ for (let page = 1; page <= 15 && all.length < total; page++) {
835
+ const r = await fetchCatalogPage(page, pageSize);
836
+ total = r.total;
837
+ if (!r.list.length) break;
838
+ all.push(...r.list);
839
+ }
840
+ if (!all.length) throw new Error("catalog \u8FD4\u56DE\u4E3A\u7A7A");
841
+ return parseGroups(all);
842
+ }
843
+ function blSpawnPlan(args) {
844
+ return { command: "bl", args: [...args], options: { shell: process.platform === "win32", env: { ...process.env, NO_COLOR: "1" } } };
845
+ }
846
+ function runBl(args, timeoutMs = 3e4) {
847
+ return new Promise((resolve) => {
848
+ const plan = blSpawnPlan(args);
849
+ const child = cp.spawn(plan.command, plan.args, plan.options);
850
+ let stdout = "", stderr = "";
851
+ let done = false;
852
+ const timer = setTimeout(() => {
853
+ if (!done) {
854
+ done = true;
855
+ child.kill();
856
+ resolve({ code: -1, stdout, stderr: stderr + "\ntimeout" });
857
+ }
858
+ }, timeoutMs);
859
+ child.stdout.on("data", (d) => stdout += d.toString());
860
+ child.stderr.on("data", (d) => stderr += d.toString());
861
+ child.on("error", (e) => {
862
+ if (!done) {
863
+ done = true;
864
+ clearTimeout(timer);
865
+ resolve({ code: -1, stdout, stderr: String(e) });
866
+ }
867
+ });
868
+ child.on("close", (code) => {
869
+ if (!done) {
870
+ done = true;
871
+ clearTimeout(timer);
872
+ resolve({ code: code ?? -1, stdout, stderr });
873
+ }
874
+ });
875
+ });
876
+ }
877
+ async function fetchCatalogCli() {
878
+ const r = await runBl(["model", "list", "--page-size", "50", "--page", "1", "--output", "json"]);
879
+ if (r.code !== 0) throw new Error("bl model list \u9000\u51FA\u7801 " + r.code + " " + r.stderr.slice(0, 120));
880
+ const j = JSON.parse(r.stdout);
881
+ const groups = Array.isArray(j) ? j : j.items || [];
882
+ const models = parseGroups(groups);
883
+ if (!models.length) throw new Error("bl model list \u65E0\u6570\u636E");
884
+ return models;
885
+ }
886
+ function loadCache() {
887
+ try {
888
+ const j = JSON.parse(fs2.readFileSync(cacheFilePath(), "utf8"));
889
+ if (Array.isArray(j.models) && j.models.length) return j.models;
890
+ } catch {
891
+ }
892
+ try {
893
+ const j = JSON.parse(fs2.readFileSync(legacyCacheFile(), "utf8"));
894
+ if (Array.isArray(j.models) && j.models.length) return j.models;
895
+ } catch {
896
+ }
897
+ return null;
898
+ }
899
+ function saveCache(models) {
900
+ try {
901
+ fs2.mkdirSync(path2.dirname(cacheFilePath()), { recursive: true });
902
+ fs2.writeFileSync(cacheFilePath(), JSON.stringify({ savedAt: (/* @__PURE__ */ new Date()).toISOString(), models }, null, 2));
903
+ } catch {
904
+ }
905
+ }
906
+ function lastPort() {
907
+ for (const f of [portFilePath(), legacyPortFile()]) {
908
+ try {
909
+ const v = Number(fs2.readFileSync(f, "utf8").trim());
910
+ if (Number.isInteger(v) && v >= 1024 && v <= 65535) return v;
911
+ } catch {
912
+ }
913
+ }
914
+ return 0;
915
+ }
916
+ function savePort(port) {
917
+ try {
918
+ fs2.mkdirSync(path2.dirname(portFilePath()), { recursive: true });
919
+ fs2.writeFileSync(portFilePath(), String(port));
920
+ } catch {
921
+ }
922
+ }
923
+ var refreshing = false;
924
+ async function refreshCatalog(cfg, force = false) {
925
+ if (refreshing) return;
926
+ refreshing = true;
927
+ try {
928
+ let models = null;
929
+ let source = "snapshot";
930
+ let err = "";
931
+ try {
932
+ models = await fetchCatalogLive();
933
+ source = "live";
934
+ } catch (e) {
935
+ err = "live: " + String(e?.message || e);
936
+ try {
937
+ models = await fetchCatalogCli();
938
+ source = "cli";
939
+ } catch (e2) {
940
+ err += " | cli: " + String(e2?.message || e2);
941
+ }
942
+ }
943
+ if (models && models.length) {
944
+ state.source = source;
945
+ state.fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
946
+ state.models = models;
947
+ state.error = err || void 0;
948
+ saveCache(models);
949
+ } else {
950
+ state.error = err || state.error;
951
+ if (force) {
952
+ const cached = loadCache();
953
+ if (cached) {
954
+ state.models = cached;
955
+ state.fetchedAt = BAILIAN_SNAPSHOT_AT + "\uFF08\u7F13\u5B58\uFF09";
956
+ }
957
+ }
958
+ }
959
+ publishStatus();
960
+ if (cfg.toast) void maybeToast();
961
+ } finally {
962
+ refreshing = false;
963
+ }
964
+ }
965
+ async function refreshQuota() {
966
+ quotaState.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
967
+ const r = await runBl(["usage", "token-plan", "--output", "json"], 45e3);
968
+ const out = (r.stdout || "").trim();
969
+ if (r.code === 0 && out.startsWith("{")) {
970
+ try {
971
+ quotaState.data = JSON.parse(out);
972
+ quotaState.available = true;
973
+ quotaState.hint = void 0;
974
+ publishStatus();
975
+ return;
976
+ } catch {
977
+ }
978
+ }
979
+ quotaState.data = void 0;
980
+ quotaState.available = false;
981
+ const s = r.stderr + " " + r.stdout;
982
+ quotaState.hint = /console|token|login|auth/i.test(s) ? "\u5B9E\u65F6\u989D\u5EA6\u9700\u8981\u63A7\u5236\u53F0\u6388\u6743\uFF1A\u6267\u884C\u4E00\u6B21 bl auth login --console --console-site domestic \u540E\u81EA\u52A8\u542F\u7528" : "\u65E0\u6CD5\u83B7\u53D6\u5B9E\u65F6\u989D\u5EA6\uFF08bl CLI \u4E0D\u53EF\u7528\u6216\u67E5\u8BE2\u5931\u8D25\uFF1A" + (r.stderr || r.stdout || "").slice(0, 120) + "\uFF09";
983
+ publishStatus();
984
+ }
985
+ function currentStatus() {
986
+ return buildStatus(state, /* @__PURE__ */ new Date(), { ...quotaState });
987
+ }
988
+ function publishStatus() {
989
+ try {
990
+ writeStatusAtomic(currentStatus());
991
+ } catch {
992
+ }
993
+ }
994
+ var client = null;
995
+ var lastActive = /* @__PURE__ */ new Map();
996
+ async function maybeToast() {
997
+ const st = currentStatus();
998
+ for (const w of st.windows) {
999
+ const ws = windowState(w, /* @__PURE__ */ new Date());
1000
+ const prev = lastActive.get(w.id);
1001
+ lastActive.set(w.id, ws.active);
1002
+ if (cfgNow?.toast !== false && prev !== void 0 && prev !== ws.active && client) {
1003
+ const models = w.models.map((m) => `${m.model}${m.effective ? "\u2192" + m.effective : ""}`);
1004
+ const msg = ws.active ? `${w.label}\u5DF2\u5F00\u59CB\uFF1A${models.join("\u3001")}\uFF08\u81F3 ${w.end}\uFF0C\u5317\u4EAC\u65F6\u95F4\uFF09` : `${w.label}\u5DF2\u7ED3\u675F\uFF1A${models.join("\u3001")}\uFF08\u4E0B\u6B21 ${w.start} \u5F00\u59CB\uFF09`;
1005
+ try {
1006
+ await client.tui?.showToast?.({ body: { title: "Token Plan \u4F18\u60E0\u63D0\u9192", message: msg, variant: ws.active ? "success" : "info", duration: 8e3 } });
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ function renderHtml() {
1013
+ try {
1014
+ const here = path2.dirname(fileURLToPath(import.meta.url));
1015
+ for (const p of [path2.join(here, "..", "src", "server", "panel.html"), path2.join(here, "panel.html")]) {
1016
+ if (fs2.existsSync(p)) return fs2.readFileSync(p, "utf8");
1017
+ }
1018
+ } catch {
1019
+ }
1020
+ return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>Token Plan \u4F18\u60E0\u96F7\u8FBE</title></head><body><pre id="p">\u52A0\u8F7D\u4E2D\u2026</pre><script>async function l(){var r=await fetch("/api/status");document.getElementById("p").textContent=JSON.stringify(await r.json(),null,2)}l();setInterval(l,30000);</script></body></html>`;
1021
+ }
1022
+ var server = null;
1023
+ var REFRESH_THROTTLE_MS = 5e3;
1024
+ var lastRefreshAt = 0;
1025
+ function isSameOrigin(req) {
1026
+ const origin = req.headers.origin;
1027
+ if (origin !== void 0) {
1028
+ let oh;
1029
+ try {
1030
+ oh = new URL(String(origin)).host;
1031
+ } catch {
1032
+ return false;
1033
+ }
1034
+ if (oh !== String(req.headers.host ?? "")) return false;
1035
+ }
1036
+ const sfs = req.headers["sec-fetch-site"];
1037
+ if (sfs !== void 0 && String(sfs).toLowerCase() !== "same-origin") return false;
1038
+ return true;
1039
+ }
1040
+ function startPanel(port) {
1041
+ return new Promise((resolve) => {
1042
+ const handler = (req, res) => {
1043
+ const url = (req.url || "/").split("?")[0];
1044
+ if (url === "/api/status") {
1045
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1046
+ res.end(JSON.stringify(currentStatus()));
1047
+ return;
1048
+ }
1049
+ if (url === "/api/refresh") {
1050
+ if (!isSameOrigin(req)) {
1051
+ res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
1052
+ res.end("forbidden");
1053
+ return;
1054
+ }
1055
+ const t0 = Date.now();
1056
+ if (t0 - lastRefreshAt < REFRESH_THROTTLE_MS) {
1057
+ res.writeHead(429, { "Retry-After": "5", "Content-Type": "text/plain; charset=utf-8" });
1058
+ res.end("throttled");
1059
+ return;
1060
+ }
1061
+ lastRefreshAt = t0;
1062
+ refreshCatalog(cfgNow ?? loadConfig(), true).then(() => {
1063
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
1064
+ res.end("ok");
1065
+ }).catch(() => {
1066
+ res.writeHead(500);
1067
+ res.end("refresh failed");
1068
+ });
1069
+ return;
1070
+ }
1071
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1072
+ res.end(renderHtml());
1073
+ };
1074
+ const candidates = [.../* @__PURE__ */ new Set([lastPort(), port, ...Array.from({ length: 20 }, (_, i) => port + 1 + i)])].filter((p) => p >= 1024 && p <= 65535);
1075
+ let idx = 0;
1076
+ const tryListen = () => {
1077
+ const p = candidates[idx];
1078
+ const s = http.createServer(handler);
1079
+ s.on("error", () => {
1080
+ try {
1081
+ s.close();
1082
+ } catch {
1083
+ }
1084
+ if (++idx >= candidates.length) {
1085
+ resolve("");
1086
+ return;
1087
+ }
1088
+ tryListen();
1089
+ });
1090
+ s.listen(p, "127.0.0.1", () => {
1091
+ server = s;
1092
+ savePort(p);
1093
+ resolve(`http://127.0.0.1:${p}`);
1094
+ });
1095
+ };
1096
+ tryListen();
1097
+ });
1098
+ }
1099
+ async function loadToolHelper() {
1100
+ try {
1101
+ const m = await import("@opencode-ai/plugin");
1102
+ if (m?.tool) return m.tool;
1103
+ } catch {
1104
+ }
1105
+ const chain = function() {
1106
+ return chain;
1107
+ };
1108
+ chain.optional = () => chain;
1109
+ chain.describe = () => chain;
1110
+ const shim = (t) => t;
1111
+ shim.schema = { string: () => chain, number: () => chain, boolean: () => chain };
1112
+ return shim;
1113
+ }
1114
+ function unitLabel(r) {
1115
+ return r.unit === "credits-factor" ? "Credits \u500D\u7387" : "\xA5\u4EF7\u683C\u500D\u7387";
1116
+ }
1117
+ function renderToolOutput(filter) {
1118
+ const st = currentStatus();
1119
+ const lines = [];
1120
+ const srcNote = st.sources.catalog === "bailian-live" ? "\u5B9E\u65F6\u63A5\u53E3" : st.sources.catalog === "bailian-cli" ? "\u672C\u673A CLI" : "\u5FEB\u7167";
1121
+ lines.push(`\u6570\u636E\u6E90: ${srcNote}\uFF08\u66F4\u65B0\u4E8E ${state.fetchedAt}\uFF09+ Qoder \u5FEB\u7167 ${QODER_SNAPSHOT_DATE}\uFF08\u9759\u6001\uFF0C\u4E0D\u8C03\u79C1\u6709\u7AEF\u70B9\uFF09\uFF1B1 \u5143 = ${CREDITS_PER_YUAN} Credits\uFF08\u8BA1\u8D39\u793A\u4F8B\u6362\u7B97\uFF0C\u975E\u5B98\u65B9\u6C47\u7387\uFF09\uFF1B\u65F6\u533A Asia/Shanghai`);
1122
+ for (const w of st.windows) {
1123
+ const ws = windowState(w, /* @__PURE__ */ new Date());
1124
+ const cd = ws.active ? `${fmtCountdown(ws.minutesToNextBoundary)}\u540E\u7ED3\u675F\uFF08${w.end}\uFF09` : `${fmtCountdown(ws.minutesToNextBoundary)}\u540E\u5F00\u59CB\uFF08${w.start}\uFF09`;
1125
+ for (const m of w.models) {
1126
+ if (filter && m.model !== filter && !m.model.includes(filter)) continue;
1127
+ const strike = showStrike(parseFloat(m.regular ?? ""), parseFloat(m.effective ?? ""));
1128
+ lines.push(`\u2022 [${w.start}\u2013${w.end}] ${m.model}: ${strike ? strikethrough(m.regular) + " " : ""}${m.effective ?? ""} ${m.badge ?? ""}${isCrossVerified(m) ? "\uFF08\u53CC\u6E90\u5370\u8BC1\uFF09" : ""} \u2192 ${w.activeNow ? "\u4F18\u60E0\u4E2D\uFF0C" : ""}${cd}`);
1129
+ }
1130
+ }
1131
+ const flat = st.offers.filter((r) => r.kind === "flat-cut" && (!filter || r.models.some((x) => x === filter || x.includes(filter))));
1132
+ if (flat.length) {
1133
+ lines.push("\u9650\u65F6\u4F18\u60E0\uFF08\u4E24\u5957\u5355\u4F4D\u4E0D\u4E92\u7B97\uFF09\uFF1A");
1134
+ for (const r of flat.slice(0, 30)) {
1135
+ const tags = [r.factor !== void 0 ? `\xD7${r.factor}` : "", r.batchStack !== void 0 ? `Batch\u53E0\u52A0\xD7${r.batchStack}` : ""].filter(Boolean).join(" ");
1136
+ lines.push(`\u2022 ${r.models.join(", ")}\uFF08${unitLabel(r)}\uFF09: ${tags}${r.note ? " \u2014 " + r.note : ""}`);
1137
+ }
1138
+ }
1139
+ const tms = st.textModels.filter((m) => !filter || m.model.includes(filter));
1140
+ for (const m of tms) {
1141
+ lines.push(`\u2022 ${m.model}: \u8F93\u5165 ${m.input} / \u7F13\u5B58 ${m.cache} / \u8F93\u51FA ${m.output} Credits/\u767E\u4E07tokens${m.bands ? `\uFF08${m.bands}\uFF09` : ""}${m.creditsFactor ? ` \xB7 Qoder \u500D\u7387 ${m.creditsFactor}` : ""}${m.badge ? `\uFF08${m.badge.label}\uFF0C${m.badge.countdown}\uFF09` : ""}`);
1142
+ }
1143
+ const inSale = Object.entries(QODER_INSALE_FACTORS).filter(([k]) => !filter || k === filter);
1144
+ if (inSale.length && !filter) lines.push("Qoder \u5728\u5356\u5E38\u89C4\u500D\u7387\uFF08\u975E\u4F18\u60E0\uFF09\uFF1A" + inSale.map(([k, v]) => `${k} ${fmtFactor(v.factor)}${v.note ? "(" + v.note + ")" : ""}`).join(" \xB7 "));
1145
+ for (const n of st.notices) if (!n.expired || filter) lines.push(`\u2022 [${n.kind}] ${n.text}${n.expired ? "\uFF08\u5DF2\u8FC7\u671F\uFF09" : ""} \u6765\u6E90:${n.source}`);
1146
+ if (!st.quota.available) lines.push(`\u989D\u5EA6: ${st.quota.hint ?? "\u4E0D\u53EF\u7528"}`);
1147
+ else lines.push(`\u989D\u5EA6: \u5B9E\u65F6\u53EF\u7528\uFF08${st.quota.checkedAt}\uFF09`);
1148
+ if (st.sources.error) lines.push(`\u6570\u636E\u6E90\u964D\u7EA7: ${st.sources.error}`);
1149
+ lines.push("\u53E3\u5F84\uFF1A\u6D88\u8017/Cost \u5355\u4F4D Credit\uFF1BruntimeConfig \u4F1A\u53D8\u5B9E\u9645\u6D88\u8017\uFF1B\u6298\u6263\u4EE5\u63A7\u5236\u53F0\u8D26\u5355\u4E3A\u51C6\u3002");
1150
+ return lines.join("\n");
1151
+ }
1152
+ var cfgNow = null;
1153
+ var plugin = async (input, options) => {
1154
+ client = input?.client ?? null;
1155
+ const g = globalThis;
1156
+ if (g.__atpServerStarted) return { event: async () => {
1157
+ }, dispose: () => {
1158
+ } };
1159
+ g.__atpServerStarted = true;
1160
+ cfgNow = loadConfig();
1161
+ try {
1162
+ const extra = typeof options === "string" ? void 0 : options;
1163
+ if (extra && typeof extra === "object") {
1164
+ const merged = JSON.parse(JSON.stringify(cfgNow));
1165
+ for (const k of ["webpanel", "sidebar", "toast", "refresh"]) {
1166
+ const v = extra[k];
1167
+ if (v === void 0) continue;
1168
+ if (typeof v !== "object" || v === null) merged[k] = v;
1169
+ else Object.assign(merged[k] ?? {}, v);
1170
+ }
1171
+ cfgNow = merged;
1172
+ }
1173
+ } catch {
1174
+ }
1175
+ const cfg = cfgNow ?? loadConfig();
1176
+ cfgNow = cfg;
1177
+ const cached = loadCache();
1178
+ if (cached) {
1179
+ state.models = cached;
1180
+ state.fetchedAt = BAILIAN_SNAPSHOT_AT + "\uFF08\u7F13\u5B58\uFF09";
1181
+ }
1182
+ publishStatus();
1183
+ refreshCatalog(cfg);
1184
+ refreshQuota().catch(() => {
1185
+ });
1186
+ let panelUrl = "";
1187
+ if (cfg.webpanel.enabled) {
1188
+ panelUrl = await startPanel(cfg.webpanel.port);
1189
+ console.log(`[alibabatokenplan] \u9762\u677F\u5DF2\u542F\u52A8: ${panelUrl || "\u542F\u52A8\u5931\u8D25\uFF08\u7AEF\u53E3\u8017\u5C3D\uFF09"}`);
1190
+ }
1191
+ const catalogTimer = setInterval(() => {
1192
+ refreshCatalog(cfg).catch(() => {
1193
+ });
1194
+ }, cfg.refresh.catalogMin * 6e4);
1195
+ const quotaTimer = setInterval(() => {
1196
+ refreshQuota().catch(() => {
1197
+ });
1198
+ }, cfg.refresh.quotaMin * 6e4);
1199
+ const tickTimer = setInterval(() => {
1200
+ if (cfg.toast) maybeToast().catch(() => {
1201
+ });
1202
+ publishStatus();
1203
+ }, 6e4);
1204
+ for (const t of [catalogTimer, quotaTimer, tickTimer]) t.unref?.();
1205
+ const toolHelper = await loadToolHelper();
1206
+ const S = toolHelper.schema;
1207
+ const toolDef = (descPrefix) => toolHelper({
1208
+ description: descPrefix + "\u67E5\u8BE2\u767E\u70BC Token Plan \u5F53\u524D\u4F18\u60E0\u7B56\u7565\u3001\u4F18\u60E0\u65F6\u6BB5\u4E0E\u5F00\u59CB/\u7ED3\u675F\u5012\u8BA1\u65F6\uFF08\u542B Qoder \u5FEB\u7167 2026-08-31 \u9759\u6001\u6570\u636E\uFF09\uFF0C\u4EE5\u53CA\u5404\u6A21\u578B Credits \u6D88\u8017\u500D\u7387\u3002\u53EF\u9009\u53C2\u6570 model \u8FC7\u6EE4\u5355\u4E2A\u6A21\u578B\u3002\u65E0\u9700\u767B\u5F55\u5373\u53EF\u4F7F\u7528\u3002",
1209
+ args: {
1210
+ model: S.string().optional().describe("\u6309\u6A21\u578B\u540D\u8FC7\u6EE4\uFF0C\u4F8B\u5982 qwen3.8-max")
1211
+ },
1212
+ async execute(args) {
1213
+ return { title: "Token Plan \u4F18\u60E0\u4E0E\u500D\u7387", output: renderToolOutput(args?.model ? String(args.model) : "") };
1214
+ }
1215
+ });
1216
+ return {
1217
+ event: async () => {
1218
+ },
1219
+ // m8:宿主卸载 → 关面板 server + 清全部定时器,不留端口/句柄/后台刷新泄漏
1220
+ dispose: () => {
1221
+ for (const t of [catalogTimer, quotaTimer, tickTimer]) clearInterval(t);
1222
+ try {
1223
+ server?.close();
1224
+ } catch {
1225
+ }
1226
+ server = null;
1227
+ },
1228
+ config: async (config) => {
1229
+ config.command = config.command ?? {};
1230
+ config.command.tokenplan = {
1231
+ description: "\u67E5\u8BE2 Token Plan \u4F18\u60E0/\u500D\u7387/\u5012\u8BA1\u65F6\uFF08\u7531 @te-river/opencode-alibabatokenplan \u6CE8\u5165\uFF1B\u672C\u515C\u5E95\u8DEF\u5F84\u7ECF LLM\uFF0C\u4F1A\u6D88\u8017 token\uFF09",
1232
+ template: "\u8C03\u7528 tokenplan_discounts \u5DE5\u5177\uFF0C\u628A\u5F53\u524D Token Plan \u4F18\u60E0\u7A97\u53E3\u3001\u5012\u8BA1\u65F6\u4E0E\u5404\u6A21\u578B\u500D\u7387\u539F\u6837\u6574\u7406\u7ED9\u6211\uFF0C\u4E0D\u8981\u989D\u5916\u53D1\u6325\u3002$ARGUMENTS"
1233
+ };
1234
+ },
1235
+ tool: {
1236
+ tokenplan_discounts: toolDef(""),
1237
+ bailian_discounts: toolDef("(\u5DF2\u5E76\u5165 tokenplan_discounts) ")
1238
+ }
1239
+ };
1240
+ };
1241
+ var index_default = { id: PKG_ID, server: plugin };
1242
+ var AliBabaTokenPlanPlugin = plugin;
1243
+ var __testing = {
1244
+ blSpawnPlan,
1245
+ isSameOrigin,
1246
+ maybeToast,
1247
+ lastActive,
1248
+ getStatus: currentStatus,
1249
+ setClient: (c) => {
1250
+ client = c;
1251
+ }
1252
+ };
1253
+ export {
1254
+ AliBabaTokenPlanPlugin,
1255
+ __testing,
1256
+ blSpawnPlan,
1257
+ index_default as default
1258
+ };