llm-api-gateway-cli 1.0.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/.env.example +10 -0
- package/README.md +1127 -0
- package/cli-agent.js +666 -0
- package/cli-anthropic.js +236 -0
- package/cli-claude-code.js +317 -0
- package/cli-openai.js +212 -0
- package/completions/_llm-api-gateway-cli +65 -0
- package/completions/llm-api-gateway-cli.bash +64 -0
- package/completions/llm-api-gateway-cli.fish +43 -0
- package/images/chat.png +0 -0
- package/images/settings.png +0 -0
- package/images/task.png +0 -0
- package/lib/agent.js +607 -0
- package/lib/commands.js +468 -0
- package/lib/common.js +196 -0
- package/lib/config.js +70 -0
- package/lib/configcmd.js +230 -0
- package/lib/hub.js +1494 -0
- package/lib/jsonstore.js +49 -0
- package/lib/mcp.js +375 -0
- package/lib/memory.js +109 -0
- package/lib/plandoc.js +178 -0
- package/lib/pricing.js +52 -0
- package/lib/runner.js +234 -0
- package/lib/runstore.js +96 -0
- package/lib/secrets.js +198 -0
- package/lib/sessionstore.js +269 -0
- package/lib/settings.js +517 -0
- package/lib/tasksession.js +594 -0
- package/lib/taskstore.js +740 -0
- package/lib/tools.js +927 -0
- package/package.json +55 -0
- package/public/app.js +1055 -0
- package/public/index.html +167 -0
- package/public/manual.css +215 -0
- package/public/manual.html +381 -0
- package/public/manual.js +186 -0
- package/public/models.js +121 -0
- package/public/render.js +250 -0
- package/public/styles.css +955 -0
- package/public/task-slash.js +493 -0
- package/public/task.css +739 -0
- package/public/task.html +220 -0
- package/public/task.js +3127 -0
- package/public/theme.js +91 -0
- package/public/tint.js +261 -0
- package/scripts/install.ps1 +537 -0
- package/scripts/install.sh +510 -0
- package/server.js +14 -0
- package/task-server.js +15 -0
package/public/theme.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主题切换(白天 / 黑夜 / 跟随系统)
|
|
3
|
+
*
|
|
4
|
+
* 三态而不是两态:没手动选过时跟随系统,选过一次就固定下来。
|
|
5
|
+
* 主题值写在 <html data-theme="dark|light"> 上,CSS 变量两套都定义在 styles.css 里。
|
|
6
|
+
*
|
|
7
|
+
* 关键点:**防闪白**。设置的读取与应用必须在浏览器首次绘制之前完成,
|
|
8
|
+
* 所以每个页面的 <head> 里都内联了一小段同逻辑的脚本(见 index.html / task.html);
|
|
9
|
+
* 这里的 apply() 只是页面加载后再同步一次,并负责按钮交互。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
(() => {
|
|
13
|
+
const KEY = 'lgw.theme'; // 'dark' | 'light' | 不存在 = 跟随系统
|
|
14
|
+
const root = document.documentElement;
|
|
15
|
+
const media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: light)') : null;
|
|
16
|
+
|
|
17
|
+
/** 用户明确选过的主题;没选过返回 null */
|
|
18
|
+
function stored() {
|
|
19
|
+
try {
|
|
20
|
+
const v = localStorage.getItem(KEY);
|
|
21
|
+
return v === 'dark' || v === 'light' ? v : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null; // 隐私模式下 localStorage 可能直接抛错
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 外部集成传了底色时(tint.js 在 head 里先跑、并挂出 window.__lgwTint),
|
|
29
|
+
* 主题就由底色的明暗定死:本地存的偏好、系统偏好都不再参与 —— 宿主给什么底,就用配套的浅色/深色文字。
|
|
30
|
+
*/
|
|
31
|
+
function forced() {
|
|
32
|
+
return (window.__lgwTint && window.__lgwTint.theme) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 当前实际生效的主题:底色 > 选过的 > 系统 */
|
|
36
|
+
function effective() {
|
|
37
|
+
return forced() || stored() || (media && media.matches ? 'light' : 'dark');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function apply(theme) {
|
|
41
|
+
theme = forced() || theme;
|
|
42
|
+
root.setAttribute('data-theme', theme);
|
|
43
|
+
// 让浏览器原生控件(滚动条、下拉、日期框)也跟着变
|
|
44
|
+
root.style.colorScheme = theme;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setTheme(theme) {
|
|
48
|
+
try {
|
|
49
|
+
if (theme === 'system') localStorage.removeItem(KEY);
|
|
50
|
+
else localStorage.setItem(KEY, theme);
|
|
51
|
+
} catch {
|
|
52
|
+
/* 存不了也照常切换,只是刷新后不记得 */
|
|
53
|
+
}
|
|
54
|
+
apply(effective());
|
|
55
|
+
syncButtons();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function syncButtons() {
|
|
59
|
+
const now = stored() || 'system';
|
|
60
|
+
for (const btn of document.querySelectorAll('[data-theme-option]')) {
|
|
61
|
+
const on = btn.dataset.themeOption === now;
|
|
62
|
+
btn.classList.toggle('active', on);
|
|
63
|
+
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
64
|
+
}
|
|
65
|
+
const label = { dark: '黑夜', light: '白天', system: '跟随系统' }[now];
|
|
66
|
+
for (const el of document.querySelectorAll('[data-theme-label]')) el.textContent = label;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function wire() {
|
|
70
|
+
for (const btn of document.querySelectorAll('[data-theme-option]')) {
|
|
71
|
+
btn.addEventListener('click', () => setTheme(btn.dataset.themeOption));
|
|
72
|
+
}
|
|
73
|
+
// 跟随系统时,系统主题变了要立刻跟上
|
|
74
|
+
if (media && media.addEventListener) {
|
|
75
|
+
media.addEventListener('change', () => {
|
|
76
|
+
if (!stored()) {
|
|
77
|
+
apply(effective());
|
|
78
|
+
syncButtons();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
syncButtons();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 页面加载后立刻同步一次,避免内联脚本与这里不一致
|
|
86
|
+
apply(effective());
|
|
87
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
|
|
88
|
+
else wire();
|
|
89
|
+
|
|
90
|
+
window.lgwTheme = { get: effective, set: setTheme, stored };
|
|
91
|
+
})();
|
package/public/tint.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 外部集成配色:把宿主的底色带进来
|
|
3
|
+
*
|
|
4
|
+
* 场景:聊天页 / 任务页被别的系统用 iframe 嵌进去时,页面自带的白底或黑底和宿主对不上,
|
|
5
|
+
* 看着像「贴上去的一块」。所以支持在地址上带一个底色:
|
|
6
|
+
*
|
|
7
|
+
* http://127.0.0.1:3100/task?bg=%23f0f4ff 浅底
|
|
8
|
+
* http://127.0.0.1:3100/task?bg=%23111a2b 深底
|
|
9
|
+
* http://127.0.0.1:3100/?bg=rgb(20,24,32) 聊天页同样支持
|
|
10
|
+
* http://127.0.0.1:3100/task?bg=none 清掉(回到页面自己的配色)
|
|
11
|
+
*
|
|
12
|
+
* 只传一个底色,其余层次由它推导:面板 / 悬浮层 / 代码块 / 描边按「离底色的距离」算出来,
|
|
13
|
+
* 文字与强调色则按底色的明暗自动走深色或浅色主题 —— 所以深底不会出现黑字、浅底不会出现白字。
|
|
14
|
+
*
|
|
15
|
+
* 三个关键点:
|
|
16
|
+
* 1. **必须在首次绘制前把变量挂上**,否则会先闪一下默认配色再变色。所以本文件在 <head> 里
|
|
17
|
+
* 是**阻塞**脚本(不是 defer),且排在「定主题」的内联脚本之后、theme.js 之前。
|
|
18
|
+
* 2. 主题由底色明暗决定(`l < 0.5` 走深色,否则浅色),并把结果挂到 `window.__lgwTint`;
|
|
19
|
+
* theme.js 读到它就让位,本地存的偏好与系统偏好都不再参与。
|
|
20
|
+
* 3. 传了底色的页面,顶栏那个三挡主题开关会隐藏(见 styles.css 的 `html[data-tint]`)——
|
|
21
|
+
* 颜色既然由宿主决定,再让用户在这里切就没意义了,切出来的浅色主题压在你给的深底上还会瞎眼。
|
|
22
|
+
*
|
|
23
|
+
* 认不出的颜色(比如 `?bg=red;background:url(...)`)一律当作没传,直接忽略:
|
|
24
|
+
* 不会把任意字符串写进 CSS 变量。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
(() => {
|
|
28
|
+
/** sessionStorage:同一个标签页里翻页保持(换标签页或宿主不带参数就回到默认) */
|
|
29
|
+
const KEY = 'lgw.tint';
|
|
30
|
+
/** 认这些参数名,`bg` 是主名字,另两个是常见写法 */
|
|
31
|
+
const PARAMS = ['bg', 'bgcolor', 'bg-color'];
|
|
32
|
+
/** 显式关掉的口令:?bg=none */
|
|
33
|
+
const OFF = ['none', 'off', 'default', '0'];
|
|
34
|
+
|
|
35
|
+
/** CSS 具名颜色只认这些常见的(够用即可;颜色值优先用 hex / rgb() / hsl()) */
|
|
36
|
+
const NAMED = {
|
|
37
|
+
black: [0, 0, 0], white: [255, 255, 255], silver: [192, 192, 192],
|
|
38
|
+
gray: [128, 128, 128], grey: [128, 128, 128], red: [255, 0, 0],
|
|
39
|
+
maroon: [128, 0, 0], yellow: [255, 255, 0], olive: [128, 128, 0],
|
|
40
|
+
lime: [0, 255, 0], green: [0, 128, 0], aqua: [0, 255, 255],
|
|
41
|
+
cyan: [0, 255, 255], teal: [0, 128, 128], blue: [0, 0, 255],
|
|
42
|
+
navy: [0, 0, 128], fuchsia: [255, 0, 255], magenta: [255, 0, 255],
|
|
43
|
+
purple: [128, 0, 128], orange: [255, 165, 0],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 各层次相对底色的偏移(HSL 明度 dl / 饱和度 ds)。
|
|
48
|
+
* 这组数是**实测**出来的:拿默认的深色底 `#0f1115` 推出来就是原来那一套深色,
|
|
49
|
+
* 拿默认的浅色底 `#f5f6f8` 推出来就是原来那一套浅色(tests/tint.test.mjs 盯着这两条)。
|
|
50
|
+
* 也就是说「传了默认底色 = 页面长得和默认一样」,换别的底色只是整体挪个色。
|
|
51
|
+
*/
|
|
52
|
+
const LAYERS = {
|
|
53
|
+
'--bg-soft': { dark: [0.037, 0.04], light: [0.033, 0] },
|
|
54
|
+
'--bg-raised': { dark: [0.067, 0.04], light: [0.033, 0] },
|
|
55
|
+
'--bg-code': { dark: [0.018, 0], light: [-0.008, 0.062] },
|
|
56
|
+
'--bg-deep': { dark: [-0.016, 0], light: [0.018, 0] },
|
|
57
|
+
'--border': { dark: [0.113, 0], light: [-0.064, 0] },
|
|
58
|
+
'--border-strong': { dark: [0.196, 0], light: [-0.161, 0] },
|
|
59
|
+
'--border-focus': { dark: [0.212, 0.11], light: [-0.235, 0.33] },
|
|
60
|
+
'--border-dashed': { dark: [0.237, 0.095], light: [-0.161, 0] },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
|
|
64
|
+
const clamp255 = (n) => (n < 0 ? 0 : n > 255 ? 255 : Math.round(n));
|
|
65
|
+
const toHex = (rgb) => `#${rgb.map((c) => clamp255(c).toString(16).padStart(2, '0')).join('')}`;
|
|
66
|
+
|
|
67
|
+
/** HSL(h 度 / s,l 都是 0~1)→ RGB(0~255) */
|
|
68
|
+
function hslToRgb(h, s, l) {
|
|
69
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
70
|
+
const hp = ((((h % 360) + 360) % 360) / 60);
|
|
71
|
+
const x = c * (1 - Math.abs((hp % 2) - 1));
|
|
72
|
+
const seg =
|
|
73
|
+
hp < 1 ? [c, x, 0] : hp < 2 ? [x, c, 0] : hp < 3 ? [0, c, x] : hp < 4 ? [0, x, c] : hp < 5 ? [x, 0, c] : [c, 0, x];
|
|
74
|
+
const m = l - c / 2;
|
|
75
|
+
return seg.map((v) => clamp255((v + m) * 255));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** RGB → HSL(s,l 都是 0~1) */
|
|
79
|
+
function rgbToHsl(r, g, b) {
|
|
80
|
+
const R = r / 255, G = g / 255, B = b / 255;
|
|
81
|
+
const max = Math.max(R, G, B), min = Math.min(R, G, B), d = max - min;
|
|
82
|
+
const l = (max + min) / 2;
|
|
83
|
+
if (!d) return { h: 0, s: 0, l };
|
|
84
|
+
const s = l === 0 || l === 1 ? 0 : d / (1 - Math.abs(2 * l - 1));
|
|
85
|
+
const h = max === R ? 60 * (((G - B) / d) % 6) : max === G ? 60 * ((B - R) / d + 2) : 60 * ((R - G) / d + 4);
|
|
86
|
+
return { h: (h + 360) % 360, s, l };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 从 0~255 造一个归一化的颜色对象(透明通道一律忽略,底色必须是实色) */
|
|
90
|
+
function make(r, g, b) {
|
|
91
|
+
const rgb = [clamp255(r), clamp255(g), clamp255(b)];
|
|
92
|
+
return { rgb, hex: toHex(rgb), ...rgbToHsl(rgb[0], rgb[1], rgb[2]) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 拆 `1, 2, 3` / `1 2 3 / 50%`,顺便把不认识的写法挡在门外 */
|
|
96
|
+
function channels(text) {
|
|
97
|
+
const parts = String(text).split(/[,\s/]+/).filter(Boolean);
|
|
98
|
+
if (!parts.length || parts.some((p) => !/^(?:\d+(?:\.\d+)?|\.\d+)%?$/.test(p))) return null;
|
|
99
|
+
return parts;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `50%` 按满值折算,`128` 按原值;空值返回 NaN */
|
|
103
|
+
function scale(token, full) {
|
|
104
|
+
const n = Number.parseFloat(token);
|
|
105
|
+
if (!Number.isFinite(n)) return NaN;
|
|
106
|
+
return token.endsWith('%') ? (n / 100) * full : n;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 解析一个 CSS 颜色 → { rgb, hex, h, s, l };认不出返回 null。
|
|
111
|
+
* 支持:#rgb / #rgba / #rrggbb / #rrggbbaa、rgb()/rgba()、hsl()/hsla()、常见具名色。
|
|
112
|
+
*/
|
|
113
|
+
function parse(input) {
|
|
114
|
+
if (typeof input !== 'string') return null;
|
|
115
|
+
const v = input.trim().toLowerCase();
|
|
116
|
+
if (!v) return null;
|
|
117
|
+
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(NAMED, v)) return make(...NAMED[v]);
|
|
119
|
+
|
|
120
|
+
let m = /^#([0-9a-f]{3,8})$/.exec(v);
|
|
121
|
+
if (m) {
|
|
122
|
+
const h = m[1];
|
|
123
|
+
// 3/4 位是每位翻倍;4/8 位带的 alpha 忽略(底色当实色用)
|
|
124
|
+
if (h.length === 3 || h.length === 4) return make(...[...h.slice(0, 3)].map((c) => Number.parseInt(c + c, 16)));
|
|
125
|
+
if (h.length === 6 || h.length === 8) {
|
|
126
|
+
return make(...[0, 2, 4].map((i) => Number.parseInt(h.slice(i, i + 2), 16)));
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
m = /^rgba?\(([^)]*)\)$/.exec(v);
|
|
132
|
+
if (m) {
|
|
133
|
+
const parts = channels(m[1]);
|
|
134
|
+
if (!parts || (parts.length !== 3 && parts.length !== 4)) return null;
|
|
135
|
+
const rgb = parts.slice(0, 3).map((p) => scale(p, 255));
|
|
136
|
+
if (rgb.some((n) => !Number.isFinite(n))) return null;
|
|
137
|
+
return make(...rgb);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
m = /^hsla?\(([^)]*)\)$/.exec(v);
|
|
141
|
+
if (m) {
|
|
142
|
+
const parts = channels(m[1]);
|
|
143
|
+
if (!parts || (parts.length !== 3 && parts.length !== 4)) return null;
|
|
144
|
+
const h = Number.parseFloat(parts[0]); // 允许 `210deg`、`0.5turn` 只取数字部分
|
|
145
|
+
const s = scale(parts[1], 100);
|
|
146
|
+
const l = scale(parts[2], 100);
|
|
147
|
+
if (![h, s, l].every(Number.isFinite)) return null;
|
|
148
|
+
return make(...hslToRgb(h, clamp01(s / 100), clamp01(l / 100)));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 底色 → 一整套 CSS 变量值(深底加亮层次,浅底也加亮层次,描边按主题反向走) */
|
|
155
|
+
function derive(color) {
|
|
156
|
+
const dark = color.l < 0.5;
|
|
157
|
+
const vars = { '--bg': color.hex };
|
|
158
|
+
for (const [name, off] of Object.entries(LAYERS)) {
|
|
159
|
+
const [dl, ds] = dark ? off.dark : off.light;
|
|
160
|
+
const rgb = hslToRgb(color.h, clamp01(color.s + ds), clamp01(color.l + dl));
|
|
161
|
+
vars[name] = toHex(rgb);
|
|
162
|
+
if (name === '--bg-raised') vars['--raised-translucent'] = `rgba(${rgb.join(', ')}, .92)`;
|
|
163
|
+
}
|
|
164
|
+
return { dark, theme: dark ? 'dark' : 'light', vars };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 地址上的底色;没这个参数返回 null。`?bg=` 后面跟 `#` 会被浏览器当成锚点,这里兜一把 */
|
|
168
|
+
function fromUrl() {
|
|
169
|
+
let sp = null;
|
|
170
|
+
try {
|
|
171
|
+
sp = new URLSearchParams(window.location.search || '');
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
for (const name of PARAMS) {
|
|
176
|
+
if (!sp.has(name)) continue;
|
|
177
|
+
const v = sp.get(name) || '';
|
|
178
|
+
if (v) return v;
|
|
179
|
+
const hash = String(window.location.hash || '').replace(/^#/, '');
|
|
180
|
+
return /^[0-9a-f]{3,8}$/i.test(hash) ? `#${hash}` : '';
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 本次该用哪个底色:地址参数优先,其次是同一标签页里上次用过的 */
|
|
186
|
+
function pick() {
|
|
187
|
+
const raw = fromUrl();
|
|
188
|
+
if (raw !== null) {
|
|
189
|
+
const text = raw.trim().toLowerCase();
|
|
190
|
+
if (OFF.includes(text)) return { off: true };
|
|
191
|
+
const color = parse(raw);
|
|
192
|
+
return color ? { color } : { off: true }; // 认不出就当没传,并清掉旧值,免得留下跟地址不符的颜色
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const saved = sessionStorage.getItem(KEY);
|
|
196
|
+
const color = saved ? parse(saved) : null;
|
|
197
|
+
if (color) return { color };
|
|
198
|
+
} catch {
|
|
199
|
+
/* 隐私模式下读不到就算了 */
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function remember(hex) {
|
|
205
|
+
try {
|
|
206
|
+
if (hex) sessionStorage.setItem(KEY, hex);
|
|
207
|
+
else sessionStorage.removeItem(KEY);
|
|
208
|
+
} catch {
|
|
209
|
+
/* 存不了也不影响本次生效 */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 把推导结果挂到 <html> 上:内联变量的优先级高于 styles.css 里的两套主题,所以必然生效 */
|
|
214
|
+
function apply(tint) {
|
|
215
|
+
const root = document.documentElement;
|
|
216
|
+
for (const [name, value] of Object.entries(tint.vars)) root.style.setProperty(name, value);
|
|
217
|
+
root.setAttribute('data-tint', tint.vars['--bg']);
|
|
218
|
+
root.setAttribute('data-theme', tint.theme);
|
|
219
|
+
root.style.colorScheme = tint.theme;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 页内跳到聊天/任务页时把底色带上,否则新标签页里会退回默认配色 */
|
|
223
|
+
function patchLinks(hex) {
|
|
224
|
+
const SELF = ['/', '/index.html', '/task', '/task.html'];
|
|
225
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
226
|
+
let url;
|
|
227
|
+
try {
|
|
228
|
+
url = new URL(a.getAttribute('href'), window.location.href);
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const sameOrigin = url.origin === window.location.origin;
|
|
233
|
+
const isEntry = a.id === 'btn-open-task' || a.id === 'btn-open-chat';
|
|
234
|
+
if (!isEntry && !(sameOrigin && SELF.includes(url.pathname))) continue;
|
|
235
|
+
url.searchParams.set('bg', hex);
|
|
236
|
+
a.setAttribute('href', sameOrigin ? `${url.pathname}${url.search}${url.hash}` : url.toString());
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ready(fn) {
|
|
241
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
|
242
|
+
else fn();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const api = { key: KEY, params: PARAMS, parse, derive, apply, fromUrl, pick, patchLinks };
|
|
246
|
+
window.lgwTint = api;
|
|
247
|
+
|
|
248
|
+
const picked = pick();
|
|
249
|
+
if (!picked) return; // 没传底色:一切照旧,连 data-tint 都不挂
|
|
250
|
+
|
|
251
|
+
if (picked.off) {
|
|
252
|
+
remember(null);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const tint = { hex: picked.color.hex, ...derive(picked.color) };
|
|
257
|
+
window.__lgwTint = tint; // theme.js 据此让位(它在 head 里排在本文件后面)
|
|
258
|
+
apply(tint);
|
|
259
|
+
remember(tint.hex);
|
|
260
|
+
ready(() => patchLinks(tint.hex));
|
|
261
|
+
})();
|