@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/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@te-river/opencode-alibabatokenplan",
3
+ "version": "0.1.0",
4
+ "description": "Opencode server+TUI plugin: Aliyun Bailian Token Plan discount radar (live gateway / bl CLI / snapshot + Qoder static snapshot) persisted to status.json, shown in sidebar/home with countdown, tools and toasts.",
5
+ "type": "module",
6
+ "main": "./dist/server.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./dist/server.js"
10
+ },
11
+ "./server": {
12
+ "default": "./dist/server.js"
13
+ },
14
+ "./tui": {
15
+ "default": "./dist/tui.js"
16
+ }
17
+ },
18
+ "oc-plugin": [
19
+ "server",
20
+ "tui"
21
+ ],
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "build": "node scripts/build.mjs",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "node scripts/test.mjs",
32
+ "smoke": "node scripts/smoke.mjs",
33
+ "publish:check": "npm run build && npm test && npm pack --dry-run"
34
+ },
35
+ "keywords": [
36
+ "opencode",
37
+ "plugin",
38
+ "bailian",
39
+ "token-plan",
40
+ "discount",
41
+ "qoder",
42
+ "toast"
43
+ ],
44
+ "author": "te-river",
45
+ "license": "MIT",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/te-river/opencode-alibabatokenplan.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/te-river/opencode-alibabatokenplan/issues"
55
+ },
56
+ "homepage": "https://github.com/te-river/opencode-alibabatokenplan#readme",
57
+ "dependencies": {
58
+ "@opentui/core": "0.4.5",
59
+ "@opentui/solid": "0.4.5",
60
+ "solid-js": "^1.9.12"
61
+ },
62
+ "devDependencies": {
63
+ "@opencode-ai/plugin": "1.18.24",
64
+ "@types/node": "^22.0.0",
65
+ "esbuild": "^0.24.0",
66
+ "typescript": "^5.7.0"
67
+ },
68
+ "peerDependencies": {
69
+ "@opencode-ai/plugin": "^1.4.3"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@opencode-ai/plugin": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "engines": {
77
+ "node": ">=22.0.0"
78
+ }
79
+ }
@@ -0,0 +1,146 @@
1
+ // 百炼内置快照(源3)+ 目录解析 —— 1:1 移植自 bailian-discount-panel.ts。
2
+ // live/cli 抓取函数在 server 内(03 包结构),本文件只放纯数据与纯函数。
3
+
4
+ import type { ModelEntry, PriceEntry } from "./types";
5
+
6
+ // 1 元 = 250 Credits(经官方计费示例逐分验证:qwen3.6-plus 输入 8349tok→1.67Cr 等)
7
+ // —— 仅作展示换算注记,来源标注「百炼计费示例验证,非官方汇率」(03-D2d)。
8
+ export const CREDITS_PER_YUAN = 250;
9
+
10
+ export const BAILIAN_SNAPSHOT_AT = "2026-08-30";
11
+
12
+ // Token Plan 个人版支持的文本模型(主表筛选)
13
+ export const TOKEN_PLAN_TEXT_MODELS = [
14
+ "qwen3.8-max",
15
+ "qwen3.8-flash",
16
+ "qwen3.7-max",
17
+ "qwen3.7-plus",
18
+ "qwen3.6-plus",
19
+ "qwen3.6-flash",
20
+ "glm-5.2",
21
+ "deepseek-v4-pro-0813",
22
+ ];
23
+
24
+ // 官方文档确认的窗口型优惠(时间均为北京时间)。数据源=bailian-snapshot。
25
+ export const BAILIAN_WINDOW_RULES: {
26
+ id: string;
27
+ label: string;
28
+ models: string[];
29
+ factor: number;
30
+ start: string;
31
+ end: string;
32
+ source: string;
33
+ }[] = [
34
+ {
35
+ id: "night-qwen38-max",
36
+ label: "Token Plan 夜间五折",
37
+ models: ["qwen3.8-max"],
38
+ factor: 0.5,
39
+ start: "22:00",
40
+ end: "08:00",
41
+ source: "官方文档 · Token Plan(个人版)概述",
42
+ },
43
+ {
44
+ id: "night-deepseek-v4-pro",
45
+ label: "Token Plan 夜间五折",
46
+ models: ["deepseek-v4-pro-0813"],
47
+ factor: 0.5,
48
+ start: "22:00",
49
+ end: "08:00",
50
+ source: "官方文档 · Token Plan(个人版)概述",
51
+ },
52
+ ];
53
+
54
+ export function bailianSnapshotModels(): ModelEntry[] {
55
+ const P = (name: string, type: string, price: number, unit = "每百万tokens", band?: string, discount?: number): PriceEntry =>
56
+ ({ name, type, price, unit, ...(band ? { band } : {}), ...(discount !== undefined ? { discount } : {}) });
57
+ return [
58
+ {
59
+ family: "qwen3.8-max", model: "qwen3.8-max", provider: "qwen", context: 1_000_000,
60
+ prices: [P("输入", "input_token", 12), P("输出", "output_token", 36), P("输入(缓存命中)", "input_token_cache", 1.5), P("输入(Batch Chat)", "input_token_batch_chat", 12, "每百万tokens", undefined, 0.5), P("输出(Batch Chat)", "output_token_batch_chat", 36, "每百万tokens", undefined, 0.5)],
61
+ },
62
+ {
63
+ family: "qwen3.8-flash", model: "qwen3.8-flash", provider: "qwen", context: 1_000_000,
64
+ prices: [P("输入", "input_token", 0.8), P("输出", "output_token", 2.7), P("输入(缓存命中)", "input_token_cache", 0.1)],
65
+ },
66
+ {
67
+ family: "qwen3.7-max", model: "qwen3.7-max", provider: "qwen", context: 1_000_000,
68
+ prices: [P("输入", "input_token", 12), P("输出", "output_token", 36), P("输入(缓存命中)", "input_token_cache", 2.4), P("输入", "input_token", 12, "每百万tokens", "standard", 0.5)],
69
+ },
70
+ {
71
+ family: "qwen3.6-plus", model: "qwen3.6-plus", provider: "qwen", context: 1_000_000,
72
+ prices: [P("输入", "input_token", 0.8), P("输出", "output_token", 4.8), P("输入(缓存命中)", "input_token_cache", 0.08)],
73
+ },
74
+ {
75
+ family: "deepseek", model: "deepseek-v4-pro-0813", provider: "deepseek", context: 1_000_000,
76
+ prices: [P("输入", "input_token", 9, "每百万tokens", "peak"), P("输入", "input_token", 4.5, "每百万tokens", "offpeak"), P("输出", "output_token", 27, "每百万tokens", "peak"), P("输出", "output_token", 13.5, "每百万tokens", "offpeak"), P("输入(缓存命中)", "input_token_cache", 0.9, "每百万tokens", "peak"), P("输入(缓存命中)", "input_token_cache", 0.45, "每百万tokens", "offpeak")],
77
+ },
78
+ {
79
+ family: "deepseek", model: "deepseek-v4-pro", provider: "deepseek", context: 1_000_000,
80
+ prices: [P("输入", "input_token", 12), P("输出", "output_token", 24), P("输入(缓存命中)", "input_token_cache", 1)],
81
+ },
82
+ {
83
+ family: "glm-5.2", model: "glm-5.2", provider: "zhipu", context: 200_000,
84
+ prices: [P("输入", "input_token", 8), P("输出", "output_token", 28), P("输入(缓存命中)", "input_token_cache", 2)],
85
+ },
86
+ {
87
+ family: "wan-video", model: "wan-video", provider: "wan",
88
+ prices: [P("视频生成(480P)", "video_ratio_480p", 0.45, "每秒"), P("视频生成(720P)", "video_ratio_720p", 0.9, "每秒"), P("视频生成(1080P)", "video_ratio_1080p", 1.8, "每秒"), P("视频生成(720P·限时)", "video_ratio_720p", 0.6, "每秒", undefined, 0.7)],
89
+ },
90
+ {
91
+ family: "qwen-image", model: "qwen-image", provider: "qwen",
92
+ prices: [P("图片生成", "image_number", 0.25, "每张")],
93
+ },
94
+ {
95
+ family: "qwen-image-plus", model: "qwen-image-plus", provider: "qwen",
96
+ prices: [P("图片生成", "image_number", 0.2, "每张")],
97
+ },
98
+ ];
99
+ }
100
+
101
+ /** 解析 listFoundationModels 平铺条目 → ModelEntry[](旧文件 1:1 移植,去 any 噪音)。 */
102
+ export function parseGroups(list: any[]): ModelEntry[] {
103
+ const out: ModelEntry[] = [];
104
+ const byName = new Set<string>();
105
+ for (const g of list || []) {
106
+ // group:false 平铺模式:每个条目即一个模型,multiPrices 含完整阶梯价与限时折扣
107
+ const items = Array.isArray(g.items) && g.items.length ? g.items : [g];
108
+ for (const it of items) {
109
+ const model = String(it.model || "");
110
+ if (!model) continue;
111
+ const m = model.match(/^(.*)-\d{4}-\d{2}-\d{2}$/);
112
+ if (m && byName.has(m[1])) continue; // 快照版本与正式版并存时只保留正式版
113
+ const prices: PriceEntry[] = [];
114
+ const mp = Array.isArray(it.multiPrices) ? it.multiPrices : [];
115
+ if (mp.length) {
116
+ for (const range of mp) {
117
+ for (const p of range.prices || []) {
118
+ prices.push({
119
+ name: p.priceName ?? p.type ?? "",
120
+ type: p.type ?? "",
121
+ price: Number(p.price),
122
+ unit: p.priceUnit ?? "",
123
+ ...(p.timeBand ? { band: p.timeBand } : {}),
124
+ ...(p.discount !== undefined && p.discount !== null ? { discount: Number(p.discount) } : {}),
125
+ ...(range.rangeName ? { range: range.rangeName } : {}),
126
+ });
127
+ }
128
+ }
129
+ } else if (Array.isArray(it.prices)) {
130
+ for (const p of it.prices) {
131
+ prices.push({
132
+ name: p.priceName ?? p.type ?? "",
133
+ type: p.type ?? "",
134
+ price: Number(p.price),
135
+ unit: p.priceUnit ?? "",
136
+ ...(p.timeBand ? { band: p.timeBand } : {}),
137
+ ...(p.discount !== undefined && p.discount !== null ? { discount: Number(p.discount) } : {}),
138
+ });
139
+ }
140
+ }
141
+ byName.add(model);
142
+ out.push({ family: model, model, provider: it.provider ?? g.provider ?? "", context: it.contextWindow, prices });
143
+ }
144
+ }
145
+ return out;
146
+ }
@@ -0,0 +1,10 @@
1
+ // core 汇聚入口:esbuild 以此打包 dist/core.mjs(node 直测/冒烟用)。
2
+ // 本目录所有模块零 opencode 依赖,仅 node 内建。
3
+
4
+ export * from "./types";
5
+ export * from "./rules";
6
+ export * from "./text";
7
+ export * from "./merge";
8
+ export * from "./qoder-snapshot";
9
+ export * from "./bailian-snapshot";
10
+ export * from "./status";
@@ -0,0 +1,148 @@
1
+ // 统一优惠模型合并(03-D2):别名归一 → 目录→Offer 转换 → 去重合并 → 优先级排序。
2
+ // 两套单位永不互算:credits-factor(Qoder Credits 倍率) 与 cny-price(百炼 ¥ 价格倍率)
3
+ // 各居其 facet;合并前多 model 规则拆 per-model(m1),仅当 (kind, model, 窗口, factor, unit)
4
+ // 全等才合并(origins 并集=双源印证)。
5
+
6
+ import type { ModelEntry, OfferOrigin, OfferRule } from "./types";
7
+ import { BAILIAN_WINDOW_RULES } from "./bailian-snapshot";
8
+
9
+ /** 契约 §1 别名表:Qoder 生态内 alias → canonical(仅同生态精确字符串匹配)。 */
10
+ export const MODEL_ALIASES: Record<string, string> = {
11
+ qmodel_38max: "qwen3.8-max",
12
+ qmodel_latest: "qwen3.7-max",
13
+ qmodel: "qwen3.7-plus",
14
+ qfmodel: "qwen3.8-flash",
15
+ q37fmodel: "qwen3.7-flash",
16
+ dmodel: "deepseek-v4-pro",
17
+ gmodel: "glm-5.3",
18
+ gfmodel: "glm-5.3-flash",
19
+ gm51model: "glm-5.2",
20
+ kmodel: "kimi-k2.7-code",
21
+ mmodel: "minimax-m2.7",
22
+ };
23
+
24
+ /** 小写 + 别名归一。禁止与百炼目录行强行 join,除非小写全等。 */
25
+ export function canonicalize(model: string): string {
26
+ const k = String(model || "").toLowerCase();
27
+ return MODEL_ALIASES[k] ?? k;
28
+ }
29
+
30
+ /** 展示优先级:bailian-live > bailian-cli > qoder-snapshot > bailian-snapshot(03-D2c) */
31
+ export function originRank(origin: OfferOrigin): number {
32
+ return { "bailian-live": 0, "bailian-cli": 1, "qoder-snapshot": 2, "bailian-snapshot": 3 }[origin];
33
+ }
34
+
35
+ const OFFPEAK_WINDOW = { start: "22:00", end: "08:00", tz: "Asia/Shanghai" };
36
+
37
+ /**
38
+ * 百炼目录(live/cli/snapshot 任一源)→ OfferRule[]:
39
+ * (a) offpeak 档模型 → time-window(cny-price, ×0.5);
40
+ * (b) 普通 input 带 discount<1 → flat-cut(cny-price, 限时系数);
41
+ * (c) batch 维度 discount<1 → 叠加系数 batchCombined=模型×Batch(03/D2c),记在 note+batchStack。
42
+ */
43
+ export function catalogToOffers(models: ModelEntry[], origin: OfferOrigin, snapshotDate?: string): OfferRule[] {
44
+ const out: OfferRule[] = [];
45
+ const offpeakModels = models.filter((m) => m.prices.some((p) => p.band === "offpeak")).map((m) => m.model.toLowerCase());
46
+ if (offpeakModels.length) {
47
+ out.push({
48
+ id: "catalog-offpeak",
49
+ kind: "time-window",
50
+ label: "目录夜间优惠(错峰 5 折)",
51
+ models: offpeakModels,
52
+ unit: "cny-price",
53
+ factor: 0.5,
54
+ window: { ...OFFPEAK_WINDOW },
55
+ note: "目录含 offpeak 档的模型,谷时价=峰时 5 折",
56
+ links: [],
57
+ origins: [origin],
58
+ ...(snapshotDate ? { snapshotDate } : {}),
59
+ });
60
+ }
61
+ // m2:跳过集 = 模型已存在任一 time-window offer(目录 offpeak 档产出的 catalog-offpeak
62
+ // ∪ 官方文档窗口规则 BAILIAN_WINDOW_RULES)→ 保住旧 428 行「窗口/限时互斥」语义,不漂移。
63
+ const windowedModels = new Set<string>([
64
+ ...out.filter((o) => o.kind === "time-window").flatMap((o) => o.models.map((m) => m.toLowerCase())),
65
+ ...BAILIAN_WINDOW_RULES.flatMap((r) => r.models.map((m) => m.toLowerCase())),
66
+ ]);
67
+ for (const m of models) {
68
+ const model = m.model.toLowerCase();
69
+ if (windowedModels.has(model)) continue; // 已有 time-window offer → 不再出限时型
70
+ const std = m.prices.find((p) => p.type === "input_token" && p.discount !== undefined && p.discount < 1);
71
+ const modelFactor = std?.discount;
72
+ const batchEntry = m.prices.find((p) => /batch/i.test(p.type) && p.discount !== undefined && p.discount < 1);
73
+ // 叠加规则:Batch 维度系数 = 模型限时系数 × Batch 维度系数(如 0.8 × 0.5 = 0.4)
74
+ const batchCombined = batchEntry ? Math.round((batchEntry.discount ?? 1) * (modelFactor ?? 1) * 1000) / 1000 : undefined;
75
+ if (modelFactor === undefined && batchCombined === undefined) continue;
76
+ const factors: number[] = [];
77
+ if (modelFactor !== undefined) factors.push(modelFactor);
78
+ else if (batchCombined !== undefined) factors.push(batchCombined);
79
+ out.push({
80
+ id: "limited-" + model,
81
+ kind: "flat-cut",
82
+ label: "限时优惠",
83
+ models: [model],
84
+ unit: "cny-price",
85
+ factor: factors[0],
86
+ ...(batchCombined !== undefined ? { batchStack: batchCombined } : {}),
87
+ note:
88
+ modelFactor !== undefined ? `模型限时 ×${modelFactor}` + (batchCombined !== undefined ? `;Batch 叠加 ×${batchCombined}` : "") :
89
+ `仅 Batch 维度 ×${batchCombined}`,
90
+ links: [],
91
+ origins: [origin],
92
+ ...(snapshotDate ? { snapshotDate } : {}),
93
+ });
94
+ }
95
+ return out;
96
+ }
97
+
98
+ function mergeKey(r: OfferRule): string {
99
+ 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("|");
100
+ }
101
+
102
+ /** m1:多 model 规则拆成 per-model 行再合并(id 加 `#model` 后缀);≤1 model(含 notices 空集)原样。 */
103
+ function explodePerModel(rules: OfferRule[]): OfferRule[] {
104
+ const out: OfferRule[] = [];
105
+ for (const r of rules) {
106
+ if (r.models.length <= 1) { out.push(r); continue; }
107
+ for (const m of new Set(r.models)) out.push({ ...r, id: `${r.id}#${m}`, models: [m] });
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** 去重合并 + 优先级排序。同 key(per-model 全等)→ origins/links 并集(双源印证行)。 */
113
+ export function mergeOffers(all: OfferRule[]): OfferRule[] {
114
+ const byKey = new Map<string, OfferRule>();
115
+ for (const r of explodePerModel(all)) {
116
+ const k = mergeKey(r);
117
+ const prev = byKey.get(k);
118
+ if (!prev) {
119
+ byKey.set(k, { ...r, models: [...r.models], origins: [...r.origins], links: [...r.links] });
120
+ continue;
121
+ }
122
+ for (const m of r.models) if (!prev.models.includes(m)) prev.models.push(m);
123
+ for (const o of r.origins) if (!prev.origins.includes(o)) prev.origins.push(o);
124
+ for (const l of r.links) if (l && !prev.links.includes(l)) prev.links.push(l);
125
+ // 快照新者胜(各携 snapshotDate,03-D2c)
126
+ if (r.snapshotDate && (!prev.snapshotDate || r.snapshotDate > prev.snapshotDate)) prev.snapshotDate = r.snapshotDate;
127
+ }
128
+ const list = [...byKey.values()];
129
+ const rank = (r: OfferRule) => Math.min(...r.origins.map(originRank));
130
+ const kindOrder: Record<string, number> = { "time-window": 0, "flat-cut": 1, "free-calls": 2, policy: 3 };
131
+ list.sort((a, b) => {
132
+ const rd = rank(a) - rank(b);
133
+ if (rd) return rd;
134
+ const kd = kindOrder[a.kind] - kindOrder[b.kind];
135
+ if (kd) return kd;
136
+ const fa = a.factor ?? 1, fb = b.factor ?? 1; // 价低先
137
+ if (fa !== fb) return fa - fb;
138
+ return a.id.localeCompare(b.id);
139
+ });
140
+ return list;
141
+ }
142
+
143
+ /** origins 同时含 Qoder 与百炼系 → 「双源印证」徽标(展示层判定)。 */
144
+ export function isCrossVerified(r: { origins: OfferOrigin[] }): boolean {
145
+ const hasQ = r.origins.includes("qoder-snapshot");
146
+ const hasB = r.origins.some((o) => o.startsWith("bailian-"));
147
+ return hasQ && hasB;
148
+ }
@@ -0,0 +1,153 @@
1
+ // Qoder 优惠快照 —— 04 号契约 §3 数据表全量转录(快照日 2026-08-31)。
2
+ //
3
+ // 隐私红线(03-D8④):本模块与包内任何代码 **绝不** fetch / 调用
4
+ // Qoder 的任何私有端点(其网关/openapi 子域一律不出现、不请求)。
5
+ // 这里只是静态快照数据 + 对外文档链接(links 仅作展示字符串)。
6
+ // 更新方式:手工修订本文件并重发版(见 README「快照更新流程」)。
7
+
8
+ import type { OfferRule } from "./types";
9
+
10
+ export const QODER_SNAPSHOT_DATE = "2026-08-31";
11
+ export const QODER_ORIGIN_LABEL = "Qoder 快照 2026-08-31";
12
+
13
+ const W_OFFPEAK = { start: "22:00", end: "08:00", tz: "Asia/Shanghai" } as const;
14
+
15
+ const L_QWENMAX = "https://docs.qoder.cn/events/qwen-max";
16
+ const L_QWEN37 = "https://docs.qoder.cn/product-overview/qwen-3-7-series-model-staggering-discount";
17
+ const L_OFFPEAK_RULES = "https://help.aliyun.com/document_detail/3042139.html";
18
+ const L_ULTIMATE = "https://docs.qoder.com/zh/events/ultimatediscount";
19
+ const L_CANTUS = "https://docs.qoder.com/zh/events/cantuslaunch";
20
+ const L_FREECALLS = "https://docs.qoder.com/zh/events/ultimatefreecalls";
21
+ const L_REFERRAL = "https://qoder.cn/referral";
22
+
23
+ /** §3 表逐条 → OfferRule。来源串固定「Qoder 快照 2026-08-31」。 */
24
+ export function qoderSnapshotOffers(): OfferRule[] {
25
+ const S = QODER_SNAPSHOT_DATE;
26
+ const o = (r: Omit<OfferRule, "origins" | "snapshotDate">): OfferRule => ({
27
+ ...r,
28
+ origins: ["qoder-snapshot"],
29
+ snapshotDate: S,
30
+ });
31
+ return [
32
+ o({
33
+ id: "atp-offpeak-qwen38",
34
+ kind: "time-window",
35
+ label: "错峰 5 折(Off-Peak 50% off)",
36
+ models: ["qwen3.8-max"],
37
+ unit: "credits-factor",
38
+ factor: 0.25,
39
+ baseFactor: 0.5,
40
+ window: { ...W_OFFPEAK },
41
+ note: "常规 0.5x → 错峰时段 0.25x(5 折)",
42
+ links: [L_QWENMAX],
43
+ }),
44
+ o({
45
+ id: "atp-offpeak-qwen37max",
46
+ kind: "time-window",
47
+ label: "错峰 2 折(Off-Peak 80% off)",
48
+ models: ["qwen3.7-max"],
49
+ unit: "credits-factor",
50
+ factor: 0.1,
51
+ baseFactor: 0.5,
52
+ window: { ...W_OFFPEAK },
53
+ note: "常规 0.5x → 错峰时段 0.1x(2 折)",
54
+ links: [L_QWEN37],
55
+ }),
56
+ o({
57
+ id: "atp-offpeak-qwen37plus",
58
+ kind: "time-window",
59
+ label: "错峰 4 折(Off-Peak 60% off)",
60
+ models: ["qwen3.7-plus"],
61
+ unit: "credits-factor",
62
+ factor: 0.04,
63
+ baseFactor: 0.1,
64
+ window: { ...W_OFFPEAK },
65
+ note: "常规 0.1x → 错峰时段 0.04x(4 折)",
66
+ links: [L_QWEN37],
67
+ }),
68
+ o({
69
+ id: "atp-flat-preview",
70
+ kind: "flat-cut",
71
+ label: "限时 1 折",
72
+ models: ["qwen3.8-max-preview"],
73
+ unit: "credits-factor",
74
+ factor: 0.1,
75
+ note: "模型描述原文:常规时段 1 折",
76
+ links: [L_OFFPEAK_RULES],
77
+ }),
78
+ o({
79
+ id: "atp-flat-glm53f",
80
+ kind: "flat-cut",
81
+ label: "划线半价(0.1 → 0.05)",
82
+ models: ["glm-5.3-flash"],
83
+ unit: "credits-factor",
84
+ factor: 0.05,
85
+ baseFactor: 0.1,
86
+ note: "originalPriceFactor 结构,无 promotion 标记(快照)",
87
+ links: [L_OFFPEAK_RULES],
88
+ }),
89
+ o({
90
+ id: "atp-flat-ultimate",
91
+ kind: "flat-cut",
92
+ label: "限时折扣进行中",
93
+ models: ["ultimate", "cantus"],
94
+ unit: "credits-factor",
95
+ note: "数值未公开,仅活动文案(快照)",
96
+ links: [L_ULTIMATE, L_CANTUS],
97
+ }),
98
+ o({
99
+ id: "atp-free-200",
100
+ kind: "free-calls",
101
+ label: "极致模型 200 次免费",
102
+ models: [],
103
+ unit: "credits-factor",
104
+ note: "免费额度活动;已过期(expired:true)",
105
+ expiry: "2026-07-30T23:59:59+08:00",
106
+ links: [L_FREECALLS],
107
+ }),
108
+ o({
109
+ id: "atp-free-800-2000",
110
+ kind: "free-calls",
111
+ label: "qwen3.8-max 800/2000 次免费(claim)",
112
+ models: ["qwen3.8-max"],
113
+ unit: "credits-factor",
114
+ note: "activityId: qwen38_800_invoke / qwen38_2000_invoke(快照)",
115
+ links: [],
116
+ }),
117
+ o({
118
+ id: "atp-referral",
119
+ kind: "policy",
120
+ label: "推荐返积分 ≤40000+",
121
+ models: [],
122
+ unit: "credits-factor",
123
+ note: "推荐活动至 2026-09-03",
124
+ expiry: "2026-09-03T23:59:59+08:00",
125
+ links: [L_REFERRAL],
126
+ }),
127
+ o({
128
+ id: "atp-lite",
129
+ kind: "policy",
130
+ label: "lite 基础模型降级政策",
131
+ models: ["lite"],
132
+ unit: "credits-factor",
133
+ note: "Credits 耗尽后降级为 lite 基础模型(免费/每日限额/高峰时段变慢);余额 <200 低额提示;首月 prorated(快照)",
134
+ links: [],
135
+ }),
136
+ ];
137
+ }
138
+
139
+ /**
140
+ * §3 表 E 行「在卖常规倍率表」——非优惠,供「当前会话模型倍率行」展示基线。
141
+ * 来源:Qoder 客户端公开界面快照转录 2026-08-31。kimi-k2.7-code highspeed=0.6。
142
+ */
143
+ export const QODER_INSALE_FACTORS: Record<string, { factor: number; note?: string }> = {
144
+ auto: { factor: 0.5 },
145
+ "qwen3.8-flash": { factor: 0.1 },
146
+ "qwen3.7-flash": { factor: 0.1 },
147
+ "deepseek-v4-pro": { factor: 0.8 },
148
+ "deepseek-v4-flash": { factor: 0.3 },
149
+ "glm-5.3": { factor: 0.6 },
150
+ "glm-5.2": { factor: 0.6 },
151
+ "kimi-k2.7-code": { factor: 0.3, note: "highspeed 0.6" },
152
+ "minimax-m2.7": { factor: 0.2 },
153
+ };
@@ -0,0 +1,105 @@
1
+ // 规则引擎(北京时间 UTC+8,无夏令时)—— 移植自 bailian-discount-panel.ts,
2
+ // 新增 boundaryAbsolute():输出 ISO 绝对时刻,供 TUI 秒级本地 tick 免轮询(03-D3)。
3
+
4
+ export const SH_OFFSET_MIN = 8 * 60;
5
+ export const DAY_MIN = 1440;
6
+
7
+ /** 现在=北京时间的「当日零点 UTC 起算的分钟数」(含秒小数)。不依赖本机时区。 */
8
+ export function shanghaiMinutes(now = new Date()): number {
9
+ const shifted = new Date(now.getTime() + (now.getTimezoneOffset() + SH_OFFSET_MIN) * 60_000);
10
+ return shifted.getHours() * 60 + shifted.getMinutes() + shifted.getSeconds() / 60;
11
+ }
12
+
13
+ export function parseHm(s: string): number {
14
+ const [h, min] = s.split(":").map(Number);
15
+ return h * 60 + (min || 0);
16
+ }
17
+
18
+ export interface WindowState {
19
+ active: boolean;
20
+ minutesToNextBoundary: number;
21
+ boundaryKind: "start" | "end";
22
+ /** 下一边界绝对时刻 epoch ms */
23
+ nextBoundaryAtMs: number;
24
+ /** 下一边界 ISO 字符串(+08:00 时区后缀,如 2026-08-31T22:00:00.000+08:00) */
25
+ nextBoundaryAt: string;
26
+ }
27
+
28
+ /**
29
+ * 窗口态 + 绝对边界。start>end 视为跨午夜(如 22:00–08:00)。
30
+ * 旧 ruleWindow() 的 active/minutesToNextBoundary 语义保持 1:1。
31
+ */
32
+ export function windowState(rule: { start: string; end: string }, now = new Date()): WindowState {
33
+ const start = parseHm(rule.start);
34
+ const end = parseHm(rule.end);
35
+ const m = shanghaiMinutes(now);
36
+ const wraps = start > end;
37
+ const active = wraps ? m >= start || m < end : m >= start && m < end;
38
+ let toNext: number;
39
+ let kind: "start" | "end";
40
+ if (active) {
41
+ toNext = wraps ? (m >= start ? end + DAY_MIN - m : end - m) : end - m;
42
+ kind = "end";
43
+ } else {
44
+ toNext = m < start ? start - m : start + DAY_MIN - m;
45
+ kind = "start";
46
+ }
47
+ return {
48
+ active,
49
+ minutesToNextBoundary: Math.max(0, Math.round(toNext)),
50
+ boundaryKind: kind,
51
+ nextBoundaryAtMs: now.getTime() + toNext * 60_000,
52
+ nextBoundaryAt: shanghaiISO(now.getTime() + toNext * 60_000),
53
+ };
54
+ }
55
+
56
+ /** epoch ms → 北京时间 ISO 串(带 +08:00 后缀)。 */
57
+ export function shanghaiISO(ms: number): string {
58
+ const d = new Date(ms + SH_OFFSET_MIN * 60_000);
59
+ const p = (n: number, w = 2) => String(n).padStart(w, "0");
60
+ return (
61
+ d.getUTCFullYear() + "-" + p(d.getUTCMonth() + 1) + "-" + p(d.getUTCDate()) +
62
+ "T" + p(d.getUTCHours()) + ":" + p(d.getUTCMinutes()) + ":" + p(d.getUTCSeconds()) +
63
+ "." + p(d.getUTCMilliseconds(), 3) + "+08:00"
64
+ );
65
+ }
66
+
67
+ /** 兼容旧签名:仅窗口激活态与剩余分钟(toast 去重复用)。 */
68
+ export function ruleWindow(rule: { start: string; end: string }, now = new Date()) {
69
+ const w = windowState(rule, now);
70
+ return { active: w.active, minutesToNextBoundary: w.minutesToNextBoundary };
71
+ }
72
+
73
+ /** "N 天 N 小时 N 分钟"(旧 fmtCountdown 1:1)。 */
74
+ export function fmtCountdown(mins: number): string {
75
+ if (mins <= 0) return "不足 1 分钟";
76
+ const d = Math.floor(mins / DAY_MIN);
77
+ const h = Math.floor((mins % DAY_MIN) / 60);
78
+ const m = mins % 60;
79
+ const parts: string[] = [];
80
+ if (d) parts.push(d + " 天");
81
+ if (h) parts.push(h + " 小时");
82
+ parts.push(m + " 分钟");
83
+ return parts.join(" ");
84
+ }
85
+
86
+ /** HH:MM:SS 秒级倒计时(TUI 本地每秒 tick 用)。ms<=0 → "00:00:00"。 */
87
+ export function fmtHMS(msRemaining: number): string {
88
+ const total = Math.max(0, Math.floor(msRemaining / 1000));
89
+ const h = Math.floor(total / 3600);
90
+ const m = Math.floor((total % 3600) / 60);
91
+ const s = total % 60;
92
+ const p = (n: number) => String(n).padStart(2, "0");
93
+ return `${p(h)}:${p(m)}:${p(s)}`;
94
+ }
95
+
96
+ /** 倒计时短横串,如 "5h23m"(home_bottom 一行摘要用)。 */
97
+ export function fmtShortDur(msRemaining: number): string {
98
+ const total = Math.max(0, Math.floor(msRemaining / 60_000));
99
+ const d = Math.floor(total / DAY_MIN);
100
+ const h = Math.floor((total % DAY_MIN) / 60);
101
+ const m = total % 60;
102
+ if (d) return `${d}d${h}h`;
103
+ if (h) return `${h}h${m}m`;
104
+ return `${m}m`;
105
+ }