dsh-opencode-palette 1.7.1 → 2.0.1
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/README.md +100 -97
- package/lib/channel.mjs +27 -0
- package/lib/client.js +1925 -50
- package/lib/event-list.json +42 -0
- package/lib/index.js +156 -8
- package/lib/vendor/dsh-plugin-update/client.js +30 -0
- package/lib/vendor/dsh-plugin-update/commands.js +85 -0
- package/lib/vendor/dsh-plugin-update/config.js +102 -0
- package/lib/vendor/dsh-plugin-update/gate.js +120 -0
- package/lib/vendor/dsh-plugin-update/host.js +284 -0
- package/lib/vendor/dsh-plugin-update/ports.js +10 -0
- package/lib/vendor/dsh-plugin-update/reader.js +198 -0
- package/lib/vendor/dsh-plugin-update/service.js +360 -0
- package/lib/vendor/dsh-plugin-update/store.js +351 -0
- package/package.json +12 -1
package/lib/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-opencode-palette
|
|
2
|
+
* dsh-opencode-palette v2.0.1 — 浏览器半(构建产物,勿手改)
|
|
3
3
|
* 数据驱动管线:opencode v1.18.12 官方主题 JSON → 颜色解析 → DSH 适配注入
|
|
4
|
-
*
|
|
4
|
+
* 面板「检查更新」:dsh-plugin-update 客户端入口经构建期内联(vendor)
|
|
5
|
+
* 源:src/engine/* + runtime/*.mjs + npm 包 dsh-log / dsh-plugin-update 的客户端入口
|
|
5
6
|
*/
|
|
6
7
|
window.__ModuleLoader__.load({
|
|
7
8
|
id: "dsh-opencode-palette",
|
|
@@ -11,6 +12,825 @@ window.__ModuleLoader__.load({
|
|
|
11
12
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" })
|
|
12
13
|
var __mods = {};
|
|
13
14
|
(function () {
|
|
15
|
+
// AUTO-GENERATED by node packages/dsh-plugin-update/build.mjs — DO NOT EDIT. Source: packages/dsh-plugin-update/src/config.ts
|
|
16
|
+
const DEFAULT_PREFIX = "wf";
|
|
17
|
+
const PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall"];
|
|
18
|
+
const DEFAULT_TARGET_PACKAGE = "dsh-mattpocock-skills-deck";
|
|
19
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
|
|
20
|
+
const LEGACY_PLUGIN_ID = "dsh-mattpocock-skills-deck";
|
|
21
|
+
const LEGACY_DIR_SEGMENT = "updates";
|
|
22
|
+
const STATE_FILE = "state.json";
|
|
23
|
+
const LOCK_FILE = "install.lock";
|
|
24
|
+
const BACKUP_FILE = "before.json";
|
|
25
|
+
const DEFAULT_CHECK_TIMEOUT_MS = 1e4;
|
|
26
|
+
const DEFAULT_CONFIRMATION_TTL_MS = 10 * 6e4;
|
|
27
|
+
const DEFAULT_INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
28
|
+
const DEFAULT_PANEL_POLL_MS = 1e3;
|
|
29
|
+
const RECHECK_WINDOW_MS = 2e3;
|
|
30
|
+
const MIN_PANEL_POLL_MS = 250;
|
|
31
|
+
function assertPluginId(value) {
|
|
32
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
33
|
+
throw new Error("[dsh-plugin-update] \u63D2\u4EF6\u6807\u8BC6 pluginId \u5FC5\u586B\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09");
|
|
34
|
+
}
|
|
35
|
+
if (value.includes("/") || value.includes("\\") || value.includes("\0")) {
|
|
36
|
+
throw new Error("[dsh-plugin-update] \u63D2\u4EF6\u6807\u8BC6 pluginId \u975E\u6CD5\uFF1A\u4E0D\u5F97\u542B\u6709\u8DEF\u5F84\u5206\u9694\u7B26\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09");
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function assertPrefix(value, role) {
|
|
41
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
42
|
+
throw new Error("[dsh-plugin-update] " + role + " \u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09");
|
|
43
|
+
}
|
|
44
|
+
if (value.includes(".") || value.includes("/") || value.includes("\\") || /\s/.test(value)) {
|
|
45
|
+
throw new Error("[dsh-plugin-update] " + role + " \u975E\u6CD5\uFF1A\u4E0D\u5F97\u542B\u6709\u70B9\u3001\u8DEF\u5F84\u5206\u9694\u7B26\u6216\u7A7A\u767D\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09");
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function assertPositiveFinite(value, role) {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
51
|
+
throw new Error("[dsh-plugin-update] " + role + " \u975E\u6CD5\uFF1A\u987B\u4E3A\u6709\u9650\u5927\u4E8E 0 \u7684\u6570\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09");
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function resolveUpdateConfig(input) {
|
|
56
|
+
if (!input || typeof input !== "object") {
|
|
57
|
+
throw new Error("[dsh-plugin-update] \u5EFA\u66F4\u65B0\u80FD\u529B\u7F3A\u5C11\u914D\u7F6E\uFF1A\u63D2\u4EF6\u6807\u8BC6 pluginId \u5FC5\u586B");
|
|
58
|
+
}
|
|
59
|
+
const pluginId = assertPluginId(input.pluginId);
|
|
60
|
+
const prefix = input.prefix === void 0 ? DEFAULT_PREFIX : assertPrefix(input.prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
|
|
61
|
+
const targetPackageName = input.targetPackageName === void 0 ? DEFAULT_TARGET_PACKAGE : String(input.targetPackageName);
|
|
62
|
+
if (!targetPackageName) {
|
|
63
|
+
throw new Error("[dsh-plugin-update] \u76EE\u6807\u5305\u540D targetPackageName \u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
64
|
+
}
|
|
65
|
+
const registryUrl = input.registryUrl === void 0 ? DEFAULT_REGISTRY : String(input.registryUrl);
|
|
66
|
+
if (!registryUrl) {
|
|
67
|
+
throw new Error("[dsh-plugin-update] \u5B98\u65B9\u6E90 registryUrl \u975E\u6CD5\uFF1A\u987B\u4E3A\u975E\u7A7A\u5B57\u7B26\u4E32");
|
|
68
|
+
}
|
|
69
|
+
const homeDir = input.homeDir === void 0 ? null : String(input.homeDir);
|
|
70
|
+
const checkTimeoutMs = input.checkTimeoutMs === void 0 ? DEFAULT_CHECK_TIMEOUT_MS : assertPositiveFinite(input.checkTimeoutMs, "\u8054\u7F51\u8D85\u65F6 checkTimeoutMs");
|
|
71
|
+
const confirmationTtlMs = input.confirmationTtlMs === void 0 ? DEFAULT_CONFIRMATION_TTL_MS : assertPositiveFinite(input.confirmationTtlMs, "\u51ED\u8BC1\u6709\u6548\u671F confirmationTtlMs");
|
|
72
|
+
const installTimeoutMs = input.installTimeoutMs === void 0 ? DEFAULT_INSTALL_TIMEOUT_MS : assertPositiveFinite(input.installTimeoutMs, "\u5B89\u88C5\u65F6\u9650 installTimeoutMs");
|
|
73
|
+
const panelPollMs = input.panelPollMs === void 0 ? DEFAULT_PANEL_POLL_MS : assertPositiveFinite(input.panelPollMs, "\u9762\u677F\u8F6E\u8BE2 panelPollMs");
|
|
74
|
+
if (panelPollMs < MIN_PANEL_POLL_MS) {
|
|
75
|
+
throw new Error("[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2 panelPollMs \u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 " + JSON.stringify(input.panelPollMs) + "\uFF09");
|
|
76
|
+
}
|
|
77
|
+
return { pluginId, prefix, targetPackageName, registryUrl, homeDir, checkTimeoutMs, confirmationTtlMs, installTimeoutMs, panelPollMs };
|
|
78
|
+
}
|
|
79
|
+
function buildPhoneNames(prefix) {
|
|
80
|
+
const checked = assertPrefix(prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
|
|
81
|
+
return {
|
|
82
|
+
updateStatus: checked + ".updateStatus",
|
|
83
|
+
updateCheck: checked + ".updateCheck",
|
|
84
|
+
updateInstall: checked + ".updateInstall"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function buildPhoneName(prefix, action) {
|
|
88
|
+
return buildPhoneNames(prefix)[action];
|
|
89
|
+
}
|
|
90
|
+
__mods["upd-config"] = { BACKUP_FILE, DEFAULT_CHECK_TIMEOUT_MS, DEFAULT_CONFIRMATION_TTL_MS, DEFAULT_INSTALL_TIMEOUT_MS, DEFAULT_PANEL_POLL_MS, DEFAULT_PREFIX, DEFAULT_REGISTRY, DEFAULT_TARGET_PACKAGE, LEGACY_DIR_SEGMENT, LEGACY_PLUGIN_ID, LOCK_FILE, MIN_PANEL_POLL_MS, PHONE_ACTIONS, RECHECK_WINDOW_MS, STATE_FILE, assertPluginId, assertPrefix, buildPhoneName, buildPhoneNames, resolveUpdateConfig };
|
|
91
|
+
|
|
92
|
+
})();
|
|
93
|
+
(function () {
|
|
94
|
+
// AUTO-GENERATED by node packages/dsh-plugin-update/build.mjs — DO NOT EDIT. Source: packages/dsh-plugin-update/src/commands.ts
|
|
95
|
+
const PACKAGE_NAME = "dsh-mattpocock-skills-deck";
|
|
96
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
97
|
+
const INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
98
|
+
function validVersion(v) {
|
|
99
|
+
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
100
|
+
}
|
|
101
|
+
function parseTriple(v) {
|
|
102
|
+
const parts = String(v).split(".");
|
|
103
|
+
if (parts.length > 3) return null;
|
|
104
|
+
const nums = [];
|
|
105
|
+
for (const p of parts) {
|
|
106
|
+
if (!/^\d+$/.test(p)) return null;
|
|
107
|
+
const n = Number(p);
|
|
108
|
+
if (!Number.isSafeInteger(n)) return null;
|
|
109
|
+
nums.push(n);
|
|
110
|
+
}
|
|
111
|
+
while (nums.length < 3) nums.push(0);
|
|
112
|
+
return [nums[0], nums[1], nums[2]];
|
|
113
|
+
}
|
|
114
|
+
function compareVersions(a, b) {
|
|
115
|
+
const pa = parseTriple(a);
|
|
116
|
+
const pb = parseTriple(b);
|
|
117
|
+
if (!pa || !pb) throw new Error("invalid-release");
|
|
118
|
+
for (let i = 0; i < 3; i++) {
|
|
119
|
+
if (pa[i] < pb[i]) return -1;
|
|
120
|
+
if (pa[i] > pb[i]) return 1;
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
function usableProfileName(raw) {
|
|
125
|
+
const name = typeof raw === "string" ? raw.trim() : "";
|
|
126
|
+
if (!name || name.length > 255 || name.startsWith("-")) return null;
|
|
127
|
+
if ([".", "..", "node_modules"].includes(name)) return null;
|
|
128
|
+
return name;
|
|
129
|
+
}
|
|
130
|
+
function installRecipe(input) {
|
|
131
|
+
const name = usableProfileName(input?.profileName);
|
|
132
|
+
const version = input?.version;
|
|
133
|
+
if (!name || !validVersion(version)) return null;
|
|
134
|
+
const kind = input?.environmentKind;
|
|
135
|
+
if (kind !== "desktop" && kind !== "cli") return null;
|
|
136
|
+
const targetName = input?.targetPackageName ?? PACKAGE_NAME;
|
|
137
|
+
const registry = input?.registryUrl ?? NPM_REGISTRY;
|
|
138
|
+
const timeoutMs = input?.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
139
|
+
if (!targetName || !registry) return null;
|
|
140
|
+
return {
|
|
141
|
+
route: kind === "desktop" ? "desktop-service" : "cli-process",
|
|
142
|
+
profileName: name,
|
|
143
|
+
version,
|
|
144
|
+
// 参数数组原样交给命令行工具:使用范围名不做引号包裹、不按空格拆分。
|
|
145
|
+
pluginArgs: ["add", "--save-exact", `${targetName}@${version}`, `--registry=${registry}`],
|
|
146
|
+
timeoutMs
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function manualCommand(input) {
|
|
150
|
+
if (input.sourceInstall || input.blockedReason === "source-install" || input.blockedReason === "unknown-profile") return null;
|
|
151
|
+
const name = usableProfileName(input.profileName);
|
|
152
|
+
if (!name) return null;
|
|
153
|
+
const targetName = input.targetPackageName ?? PACKAGE_NAME;
|
|
154
|
+
const registry = input.registryUrl ?? NPM_REGISTRY;
|
|
155
|
+
if (!targetName || !registry) return null;
|
|
156
|
+
const arg = /^[A-Za-z0-9_.-]+$/.test(name) ? name : JSON.stringify(name);
|
|
157
|
+
const picks = [input.latestVersion, input.jobTargetVersion, input.installedVersion].filter(validVersion);
|
|
158
|
+
let version = picks.length > 0 ? picks[0] : "latest";
|
|
159
|
+
try {
|
|
160
|
+
const ranked = picks.filter((v) => compareVersions(v, input.runningVersion) >= 0);
|
|
161
|
+
if (ranked.length > 0) {
|
|
162
|
+
version = ranked[0];
|
|
163
|
+
for (const v of ranked) if (compareVersions(v, version) === 1) version = v;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
return `dsh plugin --profile ${arg} add --save-exact ${targetName}@${version} --registry=${registry}`;
|
|
168
|
+
}
|
|
169
|
+
__mods["upd-commands"] = { INSTALL_TIMEOUT_MS, installRecipe, manualCommand };
|
|
170
|
+
|
|
171
|
+
})();
|
|
172
|
+
(function () {
|
|
173
|
+
// AUTO-GENERATED by node packages/dsh-plugin-update/build.mjs — DO NOT EDIT. Source: packages/dsh-plugin-update/src/client.ts
|
|
174
|
+
const { buildPhoneNames, DEFAULT_PANEL_POLL_MS, MIN_PANEL_POLL_MS } = __mods["upd-config"];
|
|
175
|
+
const { buildPhoneName, buildPhoneNames: buildPhoneNames2 } = __mods["upd-config"];
|
|
176
|
+
const { manualCommand: manualCommand2 } = __mods["upd-commands"];
|
|
177
|
+
const CLIENT_POLL = {
|
|
178
|
+
defaultMs: DEFAULT_PANEL_POLL_MS,
|
|
179
|
+
minMs: MIN_PANEL_POLL_MS
|
|
180
|
+
};
|
|
181
|
+
function buildClientPhoneNames(prefix) {
|
|
182
|
+
return buildPhoneNames(prefix);
|
|
183
|
+
}
|
|
184
|
+
function assertPollInterval(ms) {
|
|
185
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < MIN_PANEL_POLL_MS) {
|
|
186
|
+
throw new Error("[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2\u95F4\u9694\u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 " + JSON.stringify(ms) + "\uFF09");
|
|
187
|
+
}
|
|
188
|
+
return ms;
|
|
189
|
+
}
|
|
190
|
+
__mods["upd-client"] = { CLIENT_POLL, assertPollInterval, buildClientPhoneNames, buildPhoneName, buildPhoneNames: buildPhoneNames2, manualCommand: manualCommand2 };
|
|
191
|
+
|
|
192
|
+
})();
|
|
193
|
+
(function () {
|
|
194
|
+
// AUTO-GENERATED by node packages/dsh-log/build.mjs — DO NOT EDIT. Source: packages/dsh-log/src/config.ts
|
|
195
|
+
const LOG_DEBOUNCE_MS = 1e3;
|
|
196
|
+
const DEFAULT_MAX_QUEUE = 1e3;
|
|
197
|
+
const CLIENT_BATCH_MAX = 50;
|
|
198
|
+
const CLIENT_BATCH_INTERVAL_MS = 1e3;
|
|
199
|
+
const CLIENT_PACKET_BYTES = 128 * 1024;
|
|
200
|
+
const CLIENT_QUEUE_MAX = 100;
|
|
201
|
+
const LEGACY_LOG_DIR_NAME = "logs";
|
|
202
|
+
const LEGACY_SWITCH_FILE_NAME = "log-switch.json";
|
|
203
|
+
const PHONE_ACTIONS = ["logBatch", "logExport", "logClear", "logGetSwitch", "logSetSwitch"];
|
|
204
|
+
const ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
205
|
+
function assertPluginId(value, role) {
|
|
206
|
+
if (typeof value !== "string" || !ID_PATTERN.test(value)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"[dsh-log] " + role + " \u975E\u6CD5\uFF1A\u53EA\u80FD\u7528\u5C0F\u5199\u82F1\u6587\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E2D\u6A2A\u7EBF\uFF0C\u957F\u5EA6 1 \u5230 32\uFF08\u6536\u5230 " + JSON.stringify(value) + "\uFF09"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
function resolveHostLogConfig(input) {
|
|
214
|
+
if (!input || typeof input !== "object") throw new Error("[dsh-log] \u5EFA\u65E5\u5FD7\u5E93\u7F3A\u5C11\u914D\u7F6E\uFF1A\u63D2\u4EF6\u6807\u8BC6 pluginId \u5FC5\u586B");
|
|
215
|
+
const pluginId = assertPluginId(input.pluginId, "\u63D2\u4EF6\u6807\u8BC6 pluginId");
|
|
216
|
+
const prefix = input.prefix === void 0 ? pluginId : assertPluginId(input.prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
|
|
217
|
+
const logDirName = input.logDirName !== void 0 ? input.logDirName : pluginId === "wf" ? LEGACY_LOG_DIR_NAME : LEGACY_LOG_DIR_NAME + "-" + pluginId;
|
|
218
|
+
const switchFileName = input.switchFileName !== void 0 ? input.switchFileName : pluginId === "wf" ? LEGACY_SWITCH_FILE_NAME : "log-switch-" + pluginId + ".json";
|
|
219
|
+
const fileNamePolicy = input.fileNamePolicy === void 0 ? "daily" : input.fileNamePolicy;
|
|
220
|
+
if (fileNamePolicy !== "daily" && fileNamePolicy !== "four-segment") {
|
|
221
|
+
throw new Error("[dsh-log] \u6587\u4EF6\u540D\u7B56\u7565 fileNamePolicy \u975E\u6CD5\uFF1A\u53EA\u8BB8 daily \u6216 four-segment");
|
|
222
|
+
}
|
|
223
|
+
const maxQueue = input.maxQueue === void 0 ? DEFAULT_MAX_QUEUE : input.maxQueue;
|
|
224
|
+
if (typeof maxQueue !== "number" || !isFinite(maxQueue) || maxQueue < 1) {
|
|
225
|
+
throw new Error("[dsh-log] \u5185\u5B58\u961F\u5217\u4E0A\u9650 maxQueue \u975E\u6CD5\uFF1A\u5FC5\u987B\u662F\u4E0D\u5C0F\u4E8E 1 \u7684\u6570\u5B57");
|
|
226
|
+
}
|
|
227
|
+
const eventList = input.eventList === void 0 ? null : input.eventList;
|
|
228
|
+
if (eventList !== null && typeof eventList === "object") {
|
|
229
|
+
parseEventListManifest(eventList);
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
pluginId,
|
|
233
|
+
prefix,
|
|
234
|
+
logDirName,
|
|
235
|
+
switchFileName,
|
|
236
|
+
fileNamePolicy,
|
|
237
|
+
maxQueue: Math.floor(maxQueue),
|
|
238
|
+
eventList
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function buildPhoneName(prefix, action) {
|
|
242
|
+
return assertPluginId(prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix") + "." + action;
|
|
243
|
+
}
|
|
244
|
+
function buildPhoneNames(prefix) {
|
|
245
|
+
const checked = assertPluginId(prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
|
|
246
|
+
const names = {};
|
|
247
|
+
for (const action of PHONE_ACTIONS) names[action] = checked + "." + action;
|
|
248
|
+
return names;
|
|
249
|
+
}
|
|
250
|
+
function formatDailyFileName(date) {
|
|
251
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
252
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
253
|
+
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + ".log";
|
|
254
|
+
}
|
|
255
|
+
function formatFourSegmentFileName(date, pluginId, pid, startedAt) {
|
|
256
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
257
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
258
|
+
const day = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
|
|
259
|
+
const safeId = assertPluginId(pluginId, "\u63D2\u4EF6\u6807\u8BC6 pluginId");
|
|
260
|
+
const safePid = typeof pid === "number" && isFinite(pid) && pid > 0 ? Math.floor(pid) : 0;
|
|
261
|
+
const safeStartedAt = String(startedAt || "").replace(/[:/\\]/g, "-");
|
|
262
|
+
return day + "." + safeId + "." + safePid + "." + safeStartedAt + ".log";
|
|
263
|
+
}
|
|
264
|
+
function resolveLogFileName(config, date, pid, startedAt) {
|
|
265
|
+
if (config.fileNamePolicy === "four-segment") {
|
|
266
|
+
return formatFourSegmentFileName(date, config.pluginId, pid, startedAt);
|
|
267
|
+
}
|
|
268
|
+
return formatDailyFileName(date);
|
|
269
|
+
}
|
|
270
|
+
function logFileNamePattern(config) {
|
|
271
|
+
if (config.fileNamePolicy === "four-segment") {
|
|
272
|
+
return /^\d{4}-\d{2}-\d{2}\.[a-z0-9-]{1,32}\.\d+\..+\.log$/;
|
|
273
|
+
}
|
|
274
|
+
return /^\d{4}-\d{2}-\d{2}\.log$/;
|
|
275
|
+
}
|
|
276
|
+
function matchExportFileName(config, candidates, wantDate, fallbackFileName) {
|
|
277
|
+
if (config.fileNamePolicy !== "four-segment") {
|
|
278
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(wantDate) ? wantDate + ".log" : fallbackFileName;
|
|
279
|
+
}
|
|
280
|
+
const pattern = logFileNamePattern(config);
|
|
281
|
+
const hits = (Array.isArray(candidates) ? candidates : []).filter((name) => typeof name === "string" && pattern.test(name) && name.indexOf(wantDate + ".") === 0).sort();
|
|
282
|
+
return hits.length > 0 ? hits[0] : fallbackFileName;
|
|
283
|
+
}
|
|
284
|
+
const EVENT_LEVELS = ["error", "warn", "info", "debug"];
|
|
285
|
+
const EVENT_KINDS = ["resident", "ondemand", "selfmon"];
|
|
286
|
+
function isNonEmptyString(value) {
|
|
287
|
+
return typeof value === "string" && value.length > 0;
|
|
288
|
+
}
|
|
289
|
+
function assertStringArray(value, what) {
|
|
290
|
+
if (!Array.isArray(value)) throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A" + what + " \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4");
|
|
291
|
+
const seen = [];
|
|
292
|
+
for (const item of value) {
|
|
293
|
+
if (!isNonEmptyString(item)) throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A" + what + " \u91CC\u6709\u7A7A\u5B57\u6BB5\u540D");
|
|
294
|
+
if (seen.indexOf(item) >= 0) throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A" + what + " \u91CC\u5B57\u6BB5\u540D\u91CD\u590D\uFF1A" + item);
|
|
295
|
+
seen.push(item);
|
|
296
|
+
}
|
|
297
|
+
return seen;
|
|
298
|
+
}
|
|
299
|
+
function parseEventListManifest(value) {
|
|
300
|
+
if (typeof value === "string") {
|
|
301
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u8DEF\u5F84\u5F62\u5F0F\u8BF7\u8C03\u7528\u65B9\u81EA\u5DF1\u8BFB\u6210\u5BF9\u8C61\u518D\u4F20\u5165\uFF0C\u65E5\u5FD7\u5305\u4E0D\u8BFB\u76D8");
|
|
302
|
+
}
|
|
303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
304
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u53EA\u6536\u5BF9\u8C61\u5F62\u5F0F\uFF08\u7A7A\u6A21\u677F\u89C1\u5305\u5185\u7684 event-list.template.json\uFF09");
|
|
305
|
+
}
|
|
306
|
+
const input = value;
|
|
307
|
+
if (input["version"] !== 1) {
|
|
308
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1Aversion \u73B0\u5728\u53EA\u8BA4 1\uFF08\u6536\u5230 " + JSON.stringify(input["version"]) + "\uFF09");
|
|
309
|
+
}
|
|
310
|
+
const pluginId = assertPluginId(input["pluginId"], "\u4E8B\u4EF6\u6E05\u5355 pluginId");
|
|
311
|
+
const countsRaw = input["counts"];
|
|
312
|
+
if (!countsRaw || typeof countsRaw !== "object" || Array.isArray(countsRaw)) {
|
|
313
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1Acounts \u5FC5\u987B\u662F\u542B\u4E09\u7C7B\u8BA1\u6570\u7684\u5BF9\u8C61");
|
|
314
|
+
}
|
|
315
|
+
const countsRecord = countsRaw;
|
|
316
|
+
const counts = { resident: 0, ondemand: 0, selfmon: 0 };
|
|
317
|
+
for (const kind of EVENT_KINDS) {
|
|
318
|
+
const n = countsRecord[kind];
|
|
319
|
+
if (typeof n !== "number" || !isFinite(n) || Math.floor(n) !== n || n < 0) {
|
|
320
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1Acounts." + kind + " \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
|
|
321
|
+
}
|
|
322
|
+
counts[kind] = n;
|
|
323
|
+
}
|
|
324
|
+
const eventsRaw = input["events"];
|
|
325
|
+
if (!eventsRaw || typeof eventsRaw !== "object" || Array.isArray(eventsRaw)) {
|
|
326
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1Aevents \u5FC5\u987B\u662F\u4E8B\u4EF6\u540D\u5230\u6761\u76EE\u7684\u5BF9\u8C61");
|
|
327
|
+
}
|
|
328
|
+
const events = {};
|
|
329
|
+
for (const name of Object.keys(eventsRaw)) {
|
|
330
|
+
events[name] = parseEventEntry(name, eventsRaw[name]);
|
|
331
|
+
}
|
|
332
|
+
return { version: 1, pluginId, counts, events };
|
|
333
|
+
}
|
|
334
|
+
function parseEventEntry(name, value) {
|
|
335
|
+
if (!isNonEmptyString(name)) throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6\u540D\u4E0D\u80FD\u4E3A\u7A7A");
|
|
336
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
337
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6 " + name + " \u5FC5\u987B\u662F\u5BF9\u8C61");
|
|
338
|
+
}
|
|
339
|
+
const input = value;
|
|
340
|
+
if (EVENT_LEVELS.indexOf(input["level"]) < 0) {
|
|
341
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6 " + name + " \u7684 level \u53EA\u8BB8 error\u3001warn\u3001info\u3001debug");
|
|
342
|
+
}
|
|
343
|
+
if (EVENT_KINDS.indexOf(input["kind"]) < 0) {
|
|
344
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6 " + name + " \u7684 kind \u53EA\u8BB8 resident\u3001ondemand\u3001selfmon");
|
|
345
|
+
}
|
|
346
|
+
for (const key of Object.keys(input)) {
|
|
347
|
+
if (["level", "kind", "fields", "codes", "rules", "guard"].indexOf(key) < 0) {
|
|
348
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6 " + name + " \u6709\u4E0D\u8BA4\u8BC6\u7684\u952E " + key);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const entry = {
|
|
352
|
+
level: input["level"],
|
|
353
|
+
kind: input["kind"],
|
|
354
|
+
fields: assertStringArray(input["fields"], "\u4E8B\u4EF6 " + name + " \u7684 fields")
|
|
355
|
+
};
|
|
356
|
+
if (input["codes"] !== void 0) entry.codes = assertStringArray(input["codes"], "\u4E8B\u4EF6 " + name + " \u7684 codes");
|
|
357
|
+
if (input["rules"] !== void 0) entry.rules = assertStringArray(input["rules"], "\u4E8B\u4EF6 " + name + " \u7684 rules");
|
|
358
|
+
if (input["guard"] !== void 0) {
|
|
359
|
+
if (typeof input["guard"] !== "string") {
|
|
360
|
+
throw new Error("[dsh-log] \u4E8B\u4EF6\u6E05\u5355 eventList \u975E\u6CD5\uFF1A\u4E8B\u4EF6 " + name + " \u7684 guard \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
|
|
361
|
+
}
|
|
362
|
+
entry.guard = input["guard"];
|
|
363
|
+
}
|
|
364
|
+
return entry;
|
|
365
|
+
}
|
|
366
|
+
function checkEventFields(manifest, eventName, fieldNames) {
|
|
367
|
+
const entry = manifest.events[eventName];
|
|
368
|
+
if (!entry) return { ok: false, unknownEvent: true, unknownFields: [] };
|
|
369
|
+
const allowed = new Set(entry.fields);
|
|
370
|
+
const unknownFields = (Array.isArray(fieldNames) ? fieldNames : []).filter(
|
|
371
|
+
(field) => !allowed.has(field)
|
|
372
|
+
);
|
|
373
|
+
return { ok: unknownFields.length === 0, unknownEvent: false, unknownFields };
|
|
374
|
+
}
|
|
375
|
+
function checkEventCounts(manifest) {
|
|
376
|
+
const actual = { resident: 0, ondemand: 0, selfmon: 0 };
|
|
377
|
+
for (const name of Object.keys(manifest.events)) {
|
|
378
|
+
actual[manifest.events[name].kind] += 1;
|
|
379
|
+
}
|
|
380
|
+
const problems = [];
|
|
381
|
+
for (const kind of EVENT_KINDS) {
|
|
382
|
+
if (actual[kind] !== manifest.counts[kind]) {
|
|
383
|
+
problems.push(
|
|
384
|
+
"[dsh-log] \u4E8B\u4EF6\u6E05\u5355\u8BA1\u6570\u5BF9\u4E0D\u4E0A\uFF1A" + kind + " \u7C7B\u5B9E\u9645 " + actual[kind] + " \u6761\uFF0C\u6E05\u5355\u81EA\u62A5 " + manifest.counts[kind] + " \u6761"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { ok: problems.length === 0, problems };
|
|
389
|
+
}
|
|
390
|
+
__mods["log-config"] = { CLIENT_BATCH_INTERVAL_MS, CLIENT_BATCH_MAX, CLIENT_PACKET_BYTES, CLIENT_QUEUE_MAX, DEFAULT_MAX_QUEUE, EVENT_KINDS, EVENT_LEVELS, LEGACY_LOG_DIR_NAME, LEGACY_SWITCH_FILE_NAME, LOG_DEBOUNCE_MS, PHONE_ACTIONS, assertPluginId, buildPhoneName, buildPhoneNames, checkEventCounts, checkEventFields, formatDailyFileName, formatFourSegmentFileName, logFileNamePattern, matchExportFileName, parseEventListManifest, resolveHostLogConfig, resolveLogFileName };
|
|
391
|
+
|
|
392
|
+
})();
|
|
393
|
+
(function () {
|
|
394
|
+
// AUTO-GENERATED by node packages/dsh-log/build.mjs — DO NOT EDIT. Source: packages/dsh-log/src/client.ts
|
|
395
|
+
const { assertPluginId, buildPhoneNames, CLIENT_BATCH_INTERVAL_MS, CLIENT_BATCH_MAX, CLIENT_PACKET_BYTES, CLIENT_QUEUE_MAX, parseEventListManifest } = __mods["log-config"];
|
|
396
|
+
const { buildPhoneName, buildPhoneNames: buildPhoneNames2 } = __mods["log-config"];
|
|
397
|
+
const CLIENT_BATCH = {
|
|
398
|
+
maxPerBatch: CLIENT_BATCH_MAX,
|
|
399
|
+
intervalMs: CLIENT_BATCH_INTERVAL_MS,
|
|
400
|
+
packetBytes: CLIENT_PACKET_BYTES,
|
|
401
|
+
queueMax: CLIENT_QUEUE_MAX
|
|
402
|
+
};
|
|
403
|
+
function buildClientPhoneNames(prefix) {
|
|
404
|
+
return buildPhoneNames(prefix);
|
|
405
|
+
}
|
|
406
|
+
function resolveClientLogConfig(input) {
|
|
407
|
+
const raw = input === void 0 || input === null ? {} : input;
|
|
408
|
+
const pluginId = raw.pluginId === void 0 ? "wf" : assertPluginId(raw.pluginId, "\u63D2\u4EF6\u6807\u8BC6 pluginId");
|
|
409
|
+
const prefix = raw.prefix === void 0 ? pluginId : assertPluginId(raw.prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
|
|
410
|
+
const eventList = raw.eventList === void 0 ? null : raw.eventList;
|
|
411
|
+
if (eventList !== null && typeof eventList === "object") {
|
|
412
|
+
parseEventListManifest(eventList);
|
|
413
|
+
}
|
|
414
|
+
return { pluginId, prefix, eventList };
|
|
415
|
+
}
|
|
416
|
+
const LOG_DEBUG_KEY = "dsws.debug";
|
|
417
|
+
const LOG_BATCH_MAX = CLIENT_BATCH_MAX;
|
|
418
|
+
const LOG_FLUSH_MS = CLIENT_BATCH_INTERVAL_MS;
|
|
419
|
+
const LOG_PACKET_BYTES = CLIENT_PACKET_BYTES;
|
|
420
|
+
const LOG_QUEUE_MAX = CLIENT_QUEUE_MAX;
|
|
421
|
+
const LOG_WATCHDOG_MS = 5e3;
|
|
422
|
+
const LOG_REV = 1;
|
|
423
|
+
const LOG_LEVELS = ["error", "warn", "info", "debug"];
|
|
424
|
+
function createClientLog(deps, configInput) {
|
|
425
|
+
const input = deps || {};
|
|
426
|
+
const host = input.host === void 0 ? null : input.host;
|
|
427
|
+
const timer = input.timer === void 0 ? null : input.timer;
|
|
428
|
+
const storage = input.storage !== void 0 && input.storage !== null ? input.storage : input.localStorage === void 0 ? null : input.localStorage;
|
|
429
|
+
const broadcast = typeof input.broadcastLogSwitch === "function" ? input.broadcastLogSwitch : null;
|
|
430
|
+
const config = resolveClientLogConfig(configInput);
|
|
431
|
+
const phoneNames = buildClientPhoneNames(config.prefix);
|
|
432
|
+
function readLocalDebugSwitch() {
|
|
433
|
+
const fallback = { enabled: false, sampleRate: 1, rev: LOG_REV };
|
|
434
|
+
try {
|
|
435
|
+
if (!storage || typeof storage.getItem !== "function") return fallback;
|
|
436
|
+
const raw = storage.getItem(LOG_DEBUG_KEY);
|
|
437
|
+
if (!raw) return fallback;
|
|
438
|
+
const saved = JSON.parse(raw);
|
|
439
|
+
if (!saved || typeof saved !== "object") return fallback;
|
|
440
|
+
return {
|
|
441
|
+
enabled: saved.enabled === true,
|
|
442
|
+
sampleRate: typeof saved.sampleRate === "number" && isFinite(saved.sampleRate) ? saved.sampleRate : 1,
|
|
443
|
+
rev: typeof saved.rev === "number" && isFinite(saved.rev) ? saved.rev : LOG_REV
|
|
444
|
+
};
|
|
445
|
+
} catch (e) {
|
|
446
|
+
void e;
|
|
447
|
+
return fallback;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function persistLocalDebugSwitch(state) {
|
|
451
|
+
try {
|
|
452
|
+
if (!storage || typeof storage.setItem !== "function") return false;
|
|
453
|
+
storage.setItem(
|
|
454
|
+
LOG_DEBUG_KEY,
|
|
455
|
+
JSON.stringify({
|
|
456
|
+
enabled: !!(state && state.enabled),
|
|
457
|
+
sampleRate: state && typeof state.sampleRate === "number" && isFinite(state.sampleRate) ? state.sampleRate : 1,
|
|
458
|
+
rev: state && typeof state.rev === "number" && isFinite(state.rev) ? state.rev : LOG_REV
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
return true;
|
|
462
|
+
} catch (e) {
|
|
463
|
+
void e;
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const logSwitch = readLocalDebugSwitch();
|
|
468
|
+
const logQueue = [];
|
|
469
|
+
const logDroppedState = { count: 0 };
|
|
470
|
+
const logForwardState = { lastSummaryAt: 0, lastSummaryDropped: 0, lastReason: "" };
|
|
471
|
+
const logFlushTimer = { id: null };
|
|
472
|
+
const setLogSwitchGen = { n: 0 };
|
|
473
|
+
function isEnabled(level) {
|
|
474
|
+
if (level === "error" || level === "warn") return true;
|
|
475
|
+
try {
|
|
476
|
+
return logSwitch.enabled === true;
|
|
477
|
+
} catch (e) {
|
|
478
|
+
void e;
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function log(level, event, fields) {
|
|
483
|
+
if (!isEnabled(level)) return;
|
|
484
|
+
if (logQueue.length >= LOG_QUEUE_MAX) {
|
|
485
|
+
logDroppedState.count += 1;
|
|
486
|
+
logForwardState.lastReason = "queue-full";
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
logQueue.push({
|
|
490
|
+
ts: Date.now(),
|
|
491
|
+
level,
|
|
492
|
+
event: String(event || ""),
|
|
493
|
+
fields: fields && typeof fields === "object" ? fields : {}
|
|
494
|
+
});
|
|
495
|
+
if (level === "error" || level === "warn") scheduleLogFlush(true);
|
|
496
|
+
else scheduleLogFlush(false);
|
|
497
|
+
}
|
|
498
|
+
function scheduleLogFlush(immediate) {
|
|
499
|
+
const later = function(fn, ms) {
|
|
500
|
+
try {
|
|
501
|
+
if (timer !== null && timer !== void 0 && typeof timer.timeout === "function") return timer.timeout(fn, ms);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
void e;
|
|
504
|
+
}
|
|
505
|
+
return setTimeout(fn, ms);
|
|
506
|
+
};
|
|
507
|
+
if (immediate) {
|
|
508
|
+
if (logFlushTimer.id !== null) {
|
|
509
|
+
try {
|
|
510
|
+
clearTimeout(logFlushTimer.id);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
void e;
|
|
513
|
+
}
|
|
514
|
+
logFlushTimer.id = null;
|
|
515
|
+
}
|
|
516
|
+
later(sendLogBatch, 0);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (logFlushTimer.id !== null) return;
|
|
520
|
+
logFlushTimer.id = later(function() {
|
|
521
|
+
logFlushTimer.id = null;
|
|
522
|
+
sendLogBatch();
|
|
523
|
+
}, LOG_FLUSH_MS);
|
|
524
|
+
}
|
|
525
|
+
function estimateBatchBytes(entries) {
|
|
526
|
+
try {
|
|
527
|
+
const text = JSON.stringify(entries);
|
|
528
|
+
const g = globalThis;
|
|
529
|
+
if (typeof g.TextEncoder !== "undefined") return new g.TextEncoder().encode(text).length;
|
|
530
|
+
return String(text).length;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
void e;
|
|
533
|
+
return LOG_PACKET_BYTES + 1;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function maybeForwardSummary() {
|
|
537
|
+
const delta = logDroppedState.count - logForwardState.lastSummaryDropped;
|
|
538
|
+
if (delta <= 0) return;
|
|
539
|
+
logForwardState.lastSummaryDropped = logDroppedState.count;
|
|
540
|
+
const now = Date.now();
|
|
541
|
+
const windowMs = now - logForwardState.lastSummaryAt;
|
|
542
|
+
logForwardState.lastSummaryAt = now;
|
|
543
|
+
try {
|
|
544
|
+
log("warn", "log.forward.summary", {
|
|
545
|
+
droppedDelta: delta,
|
|
546
|
+
totalDropped: logDroppedState.count,
|
|
547
|
+
reason: logForwardState.lastReason || "send-fail",
|
|
548
|
+
windowMs
|
|
549
|
+
});
|
|
550
|
+
} catch (e) {
|
|
551
|
+
void e;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function hash8(value) {
|
|
555
|
+
try {
|
|
556
|
+
const t = String(value || "");
|
|
557
|
+
let h = 5381;
|
|
558
|
+
for (let i = 0; i < t.length; i++) h = (h << 5) + h + t.charCodeAt(i) >>> 0;
|
|
559
|
+
return ("0000000" + h.toString(16)).slice(-8);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
void e;
|
|
562
|
+
return "00000000";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function logExportFail(op, reason, err) {
|
|
566
|
+
try {
|
|
567
|
+
const g = globalThis;
|
|
568
|
+
if (typeof g.dswsLogHash === "function" && typeof g.dswsLogTrunc === "function") {
|
|
569
|
+
const trunc = g.dswsLogTrunc;
|
|
570
|
+
const hash = g.dswsLogHash;
|
|
571
|
+
const raw = err && typeof err === "object" && "message" in err ? String(err.message) : String(err || reason);
|
|
572
|
+
log("warn", "log.export.fail", { op, reason, errorHash: hash(trunc(raw, 120, "error")) });
|
|
573
|
+
} else {
|
|
574
|
+
const raw = err && typeof err === "object" && "message" in err ? String(err.message) : String(err || reason);
|
|
575
|
+
log("warn", "log.export.fail", { op, reason, errorHash: hash8(raw) });
|
|
576
|
+
}
|
|
577
|
+
} catch (e) {
|
|
578
|
+
void e;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function watchSwitchOp(op, pending) {
|
|
582
|
+
let settled = false;
|
|
583
|
+
try {
|
|
584
|
+
if (pending && typeof pending.then === "function") {
|
|
585
|
+
;
|
|
586
|
+
pending.then(
|
|
587
|
+
function() {
|
|
588
|
+
settled = true;
|
|
589
|
+
},
|
|
590
|
+
function() {
|
|
591
|
+
settled = true;
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
void e;
|
|
597
|
+
}
|
|
598
|
+
const fire = function() {
|
|
599
|
+
if (!settled) {
|
|
600
|
+
settled = true;
|
|
601
|
+
try {
|
|
602
|
+
log("warn", "log.switch.watchdog", { op, timeoutMs: LOG_WATCHDOG_MS, stage: "waiting-host" });
|
|
603
|
+
} catch (e) {
|
|
604
|
+
void e;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
if (timer !== null && timer !== void 0 && typeof timer.timeout === "function") {
|
|
610
|
+
timer.timeout(fire, LOG_WATCHDOG_MS);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
} catch (e) {
|
|
614
|
+
void e;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
setTimeout(fire, LOG_WATCHDOG_MS);
|
|
618
|
+
} catch (e2) {
|
|
619
|
+
void e2;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function sendLogBatch() {
|
|
623
|
+
if (logQueue.length === 0) return Promise.resolve({ ok: true, sent: 0 });
|
|
624
|
+
const entries = logQueue.splice(0, LOG_BATCH_MAX);
|
|
625
|
+
let trimmed = 0;
|
|
626
|
+
while (entries.length > 1 && estimateBatchBytes(entries) > LOG_PACKET_BYTES) {
|
|
627
|
+
entries.pop();
|
|
628
|
+
trimmed += 1;
|
|
629
|
+
}
|
|
630
|
+
while (logQueue.length > LOG_QUEUE_MAX) {
|
|
631
|
+
logQueue.shift();
|
|
632
|
+
trimmed += 1;
|
|
633
|
+
}
|
|
634
|
+
if (trimmed > 0) {
|
|
635
|
+
logDroppedState.count += trimmed;
|
|
636
|
+
logForwardState.lastReason = "packet-trim";
|
|
637
|
+
try {
|
|
638
|
+
entries[entries.length - 1].truncated = true;
|
|
639
|
+
} catch (e) {
|
|
640
|
+
void e;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const args = { entries, droppedCount: logDroppedState.count };
|
|
644
|
+
const onlySummary = entries.length === 1 && entries[0] && entries[0].event === "log.forward.summary";
|
|
645
|
+
if (!host || typeof host.call !== "function") {
|
|
646
|
+
logDroppedState.count += entries.length;
|
|
647
|
+
logForwardState.lastReason = "send-fail";
|
|
648
|
+
if (!onlySummary) maybeForwardSummary();
|
|
649
|
+
return Promise.resolve({ ok: false, sent: 0 });
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
return host.call(phoneNames.logBatch, args).then(function(res) {
|
|
653
|
+
const ok = !!res && typeof res === "object" && res.ok === true;
|
|
654
|
+
if (!ok) {
|
|
655
|
+
logDroppedState.count += entries.length;
|
|
656
|
+
logForwardState.lastReason = "host-reject";
|
|
657
|
+
}
|
|
658
|
+
if (ok) maybeForwardSummary();
|
|
659
|
+
else if (!onlySummary) maybeForwardSummary();
|
|
660
|
+
return { ok, sent: entries.length };
|
|
661
|
+
}).catch(function() {
|
|
662
|
+
logDroppedState.count += entries.length;
|
|
663
|
+
logForwardState.lastReason = "send-fail";
|
|
664
|
+
if (!onlySummary) maybeForwardSummary();
|
|
665
|
+
return { ok: false, sent: 0 };
|
|
666
|
+
});
|
|
667
|
+
} catch (e) {
|
|
668
|
+
void e;
|
|
669
|
+
logDroppedState.count += entries.length;
|
|
670
|
+
logForwardState.lastReason = "send-fail";
|
|
671
|
+
if (!onlySummary) maybeForwardSummary();
|
|
672
|
+
return Promise.resolve({ ok: false, sent: 0 });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function flush() {
|
|
676
|
+
if (logFlushTimer.id !== null) {
|
|
677
|
+
try {
|
|
678
|
+
clearTimeout(logFlushTimer.id);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
void e;
|
|
681
|
+
}
|
|
682
|
+
logFlushTimer.id = null;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
sendLogBatch();
|
|
686
|
+
} catch (e) {
|
|
687
|
+
void e;
|
|
688
|
+
}
|
|
689
|
+
return { ok: true };
|
|
690
|
+
}
|
|
691
|
+
function getDroppedCount() {
|
|
692
|
+
return logDroppedState.count;
|
|
693
|
+
}
|
|
694
|
+
function reconcileLogSwitch() {
|
|
695
|
+
if (!host || typeof host.call !== "function") {
|
|
696
|
+
return Promise.resolve({ ok: false, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate });
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
const pendingGet = host.call(phoneNames.logGetSwitch, {});
|
|
700
|
+
watchSwitchOp("reconcile", pendingGet);
|
|
701
|
+
return pendingGet.then(function(res) {
|
|
702
|
+
const body = res && typeof res === "object" ? res : null;
|
|
703
|
+
if (!body || body.ok !== true) return { ok: false, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate };
|
|
704
|
+
logSwitch.enabled = body.enabled === true;
|
|
705
|
+
if (typeof body.sampleRate === "number" && isFinite(body.sampleRate)) logSwitch.sampleRate = body.sampleRate;
|
|
706
|
+
persistLocalDebugSwitch(logSwitch);
|
|
707
|
+
try {
|
|
708
|
+
if (broadcast) broadcast();
|
|
709
|
+
} catch (e) {
|
|
710
|
+
void e;
|
|
711
|
+
}
|
|
712
|
+
return { ok: true, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate };
|
|
713
|
+
}).catch(function() {
|
|
714
|
+
return { ok: false, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate };
|
|
715
|
+
});
|
|
716
|
+
} catch (e) {
|
|
717
|
+
void e;
|
|
718
|
+
return Promise.resolve({ ok: false, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const logSwitchSetFail = function(kind, hint) {
|
|
722
|
+
try {
|
|
723
|
+
log("warn", "host.call.fail", {
|
|
724
|
+
method: phoneNames.logSetSwitch,
|
|
725
|
+
kind: "set-switch-" + kind,
|
|
726
|
+
errorHash: hash8(String(hint === void 0 || hint === null ? kind : hint).slice(0, 120))
|
|
727
|
+
});
|
|
728
|
+
} catch (e) {
|
|
729
|
+
void e;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const switchThrowKind = function(e) {
|
|
733
|
+
const msg = String((e && typeof e === "object" && ("code" in e || "message" in e) ? e.code || e.message : e) || "");
|
|
734
|
+
if (/unknown endpoint/i.test(msg)) return "throw-unknown-endpoint";
|
|
735
|
+
if (/connection|host\.call 不可用|unavailable/i.test(msg)) return "throw-connection";
|
|
736
|
+
return "throw";
|
|
737
|
+
};
|
|
738
|
+
const failByThrow = function(e) {
|
|
739
|
+
const kind = switchThrowKind(e);
|
|
740
|
+
logSwitchSetFail(
|
|
741
|
+
kind,
|
|
742
|
+
(e && typeof e === "object" && "message" in e ? e.message : void 0) || e
|
|
743
|
+
);
|
|
744
|
+
return kind;
|
|
745
|
+
};
|
|
746
|
+
function setLogSwitch(enabled, sampleRate) {
|
|
747
|
+
const next = {
|
|
748
|
+
enabled: enabled === true,
|
|
749
|
+
sampleRate: typeof sampleRate === "number" && isFinite(sampleRate) ? sampleRate : logSwitch.sampleRate
|
|
750
|
+
};
|
|
751
|
+
if (!host || typeof host.call !== "function") {
|
|
752
|
+
logSwitchSetFail("host-unavailable", "host-unavailable");
|
|
753
|
+
return Promise.resolve({ ok: false, enabled: logSwitch.enabled, error: "host-unavailable" });
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const pendingSet = host.call(phoneNames.logSetSwitch, next);
|
|
757
|
+
watchSwitchOp("set", pendingSet);
|
|
758
|
+
const myGen = setLogSwitchGen.n += 1;
|
|
759
|
+
const timeoutAt = new Promise(function(resolve) {
|
|
760
|
+
const fire = function() {
|
|
761
|
+
resolve({ switchTimedOut: true });
|
|
762
|
+
};
|
|
763
|
+
try {
|
|
764
|
+
if (timer !== null && timer !== void 0 && typeof timer.timeout === "function") {
|
|
765
|
+
timer.timeout(fire, LOG_WATCHDOG_MS);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
} catch (e) {
|
|
769
|
+
void e;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
setTimeout(fire, LOG_WATCHDOG_MS);
|
|
773
|
+
} catch (e2) {
|
|
774
|
+
void e2;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
return Promise.race([pendingSet, timeoutAt]).then(function(res) {
|
|
778
|
+
const body = res && typeof res === "object" ? res : null;
|
|
779
|
+
if (body && body["switchTimedOut"] === true) {
|
|
780
|
+
logSwitchSetFail("timeout", "timeout-" + LOG_WATCHDOG_MS);
|
|
781
|
+
return { ok: false, enabled: logSwitch.enabled, error: "switch-timeout" };
|
|
782
|
+
}
|
|
783
|
+
if (myGen !== setLogSwitchGen.n) return { ok: false, enabled: logSwitch.enabled, error: "stale" };
|
|
784
|
+
if (!body || body["ok"] !== true) {
|
|
785
|
+
logSwitchSetFail("host-rejected", "host-rejected");
|
|
786
|
+
return { ok: false, enabled: logSwitch.enabled, error: "host-rejected" };
|
|
787
|
+
}
|
|
788
|
+
logSwitch.enabled = body["enabled"] === true;
|
|
789
|
+
logSwitch.sampleRate = next.sampleRate;
|
|
790
|
+
persistLocalDebugSwitch(logSwitch);
|
|
791
|
+
try {
|
|
792
|
+
if (broadcast) broadcast();
|
|
793
|
+
} catch (e) {
|
|
794
|
+
void e;
|
|
795
|
+
}
|
|
796
|
+
return { ok: true, enabled: logSwitch.enabled, sampleRate: logSwitch.sampleRate };
|
|
797
|
+
}).catch(function(e) {
|
|
798
|
+
if (myGen !== setLogSwitchGen.n) return { ok: false, enabled: logSwitch.enabled, error: "stale" };
|
|
799
|
+
return { ok: false, enabled: logSwitch.enabled, error: failByThrow(e) };
|
|
800
|
+
});
|
|
801
|
+
} catch (e) {
|
|
802
|
+
return Promise.resolve({ ok: false, enabled: logSwitch.enabled, error: failByThrow(e) });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
config,
|
|
807
|
+
phoneNames,
|
|
808
|
+
logSwitch,
|
|
809
|
+
logQueue,
|
|
810
|
+
logDroppedState,
|
|
811
|
+
logForwardState,
|
|
812
|
+
logFlushTimer,
|
|
813
|
+
isEnabled,
|
|
814
|
+
log,
|
|
815
|
+
scheduleLogFlush,
|
|
816
|
+
estimateBatchBytes,
|
|
817
|
+
maybeForwardSummary,
|
|
818
|
+
hash8,
|
|
819
|
+
logExportFail,
|
|
820
|
+
watchSwitchOp,
|
|
821
|
+
sendLogBatch,
|
|
822
|
+
flush,
|
|
823
|
+
getDroppedCount,
|
|
824
|
+
readLocalDebugSwitch,
|
|
825
|
+
persistLocalDebugSwitch,
|
|
826
|
+
reconcileLogSwitch,
|
|
827
|
+
setLogSwitch
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
__mods["log-client"] = { CLIENT_BATCH, LOG_BATCH_MAX, LOG_DEBUG_KEY, LOG_FLUSH_MS, LOG_LEVELS, LOG_PACKET_BYTES, LOG_QUEUE_MAX, LOG_REV, LOG_WATCHDOG_MS, buildClientPhoneNames, buildPhoneName, buildPhoneNames: buildPhoneNames2, createClientLog, resolveClientLogConfig };
|
|
831
|
+
|
|
832
|
+
})();
|
|
833
|
+
(function () {
|
|
14
834
|
// resolve.mjs — 颜色解析器:把 opencode 主题 JSON 的颜色引用链解析为确定 HEX
|
|
15
835
|
// 支持:hex 字符串 / 'transparent'|'none' / defs 引用 / theme 自引用 / ANSI 数字 / {dark,light} 变体
|
|
16
836
|
// 循环引用抛错;单键解析失败由 resolveThemeColors 捕获为 { __error },不影响其余色位
|
|
@@ -276,8 +1096,12 @@ const CSS_RULES = [
|
|
|
276
1096
|
]
|
|
277
1097
|
|
|
278
1098
|
// ── 5. 字体预设(主题无关维度;等宽栈尾部保留 CJK 字体避免 Windows 中文回退 SimSun)──
|
|
279
|
-
// 回退栈按可用性排序:自选 > 随包 OFL >
|
|
1099
|
+
// 回退栈按可用性排序:自选 > 随包 OFL > OFL 但不随包(Maple Mono NF CN:中英同字体)>
|
|
1100
|
+
// 私有本地检测(SF Mono/Consolas 沉底)> 系统 > CJK;
|
|
280
1101
|
// SANS 栈顶为 Inter(opencode 桌面端 UI 字体,OFL 随包),失败即回退宿主原生栈。
|
|
1102
|
+
// Maple Mono NF CN 为 OFL-1.1,中英文同一字体(中英宽度 2:1,混排对齐),但全量 CJK
|
|
1103
|
+
// 体积是 latin 子集的数十倍(单文件 ~18MB,切片 webfont 每字重 ~5MB),内联随包会把发布包
|
|
1104
|
+
// 从 0.4MB 级推到 15MB 级 —— 故与 SF Mono/Consolas 同策:只做本机检测,不随包内联。
|
|
281
1105
|
const SANS_STACK = [
|
|
282
1106
|
"'Inter'", '-apple-system', 'BlinkMacSystemFont', "'Segoe UI'", "'PingFang SC'",
|
|
283
1107
|
"'Hiragino Sans GB'", "'Microsoft YaHei'", "'Helvetica Neue'", 'Helvetica', 'Arial', 'sans-serif',
|
|
@@ -288,6 +1112,7 @@ const FONTS = {
|
|
|
288
1112
|
'Cascadia Code': "'Cascadia Code','JetBrains Mono','Fira Code','IBM Plex Mono','SF Mono',Consolas,'Courier New','PingFang SC','Microsoft YaHei'",
|
|
289
1113
|
'Fira Code': "'Fira Code','JetBrains Mono','Cascadia Code','IBM Plex Mono','SF Mono',Consolas,'Courier New','PingFang SC','Microsoft YaHei'",
|
|
290
1114
|
'IBM Plex Mono': "'IBM Plex Mono','JetBrains Mono','Fira Code','Cascadia Code','SF Mono',Consolas,'Courier New','PingFang SC','Microsoft YaHei'",
|
|
1115
|
+
'Maple Mono NF CN': "'Maple Mono NF CN','JetBrains Mono','Fira Code','Cascadia Code','IBM Plex Mono','SF Mono',Consolas,'Courier New','PingFang SC','Microsoft YaHei'",
|
|
291
1116
|
'SF Mono': "'SF Mono','JetBrains Mono','Fira Code','Cascadia Code','IBM Plex Mono',Consolas,'Courier New','PingFang SC','Microsoft YaHei'",
|
|
292
1117
|
'Consolas': "Consolas,'JetBrains Mono','Fira Code','Cascadia Code','IBM Plex Mono','SF Mono','Courier New','PingFang SC','Microsoft YaHei'",
|
|
293
1118
|
}
|
|
@@ -304,10 +1129,327 @@ const FONT_FACE_CSS = "@font-face{font-family:\"JetBrains Mono\";font-style:norm
|
|
|
304
1129
|
__mods["font-face"] = { BUNDLED_FONTS, FONT_FACE_CSS }
|
|
305
1130
|
})();
|
|
306
1131
|
(function () {
|
|
1132
|
+
// font-avail.mjs — 本机字体可用性判定(宽度对比法;document 以参数注入,可在 node 直接测)
|
|
1133
|
+
//
|
|
1134
|
+
// 为什么不用 document.fonts.check():它对「没有声明 @font-face 的家族名」一律返回 true
|
|
1135
|
+
// (实测连 __dsh_palette_absent_font__ 这种确定不存在的名字也是 true),只有「已声明但尚未
|
|
1136
|
+
// 加载」才返回 false —— 拿它判「本机是否装了某字体」永远判不出缺失(全部显示「本地」)。
|
|
1137
|
+
//
|
|
1138
|
+
// 宽度对比法:同一段探针文本,分别用「候选家族」与一个确定不存在的家族量宽。候选家族缺字时
|
|
1139
|
+
// 会和后者回退到同一个字体 → 同宽 = 本机未装;不同宽 = 本机确有该字体。
|
|
1140
|
+
//
|
|
1141
|
+
// 保守性:量不出来(SSR / 无 document / 无布局 / 抛异常)时一律返回「可用」,
|
|
1142
|
+
// 宁可漏提示也不误判成缺失、不把可用字体灰掉。
|
|
1143
|
+
const FONT_PROBE_TEXT = 'mmmmwwwwMMMM'
|
|
1144
|
+
const FONT_PROBE_ABSENT = '__dsh_palette_absent_font__'
|
|
1145
|
+
const FONT_PROBE_SIZE = 64
|
|
1146
|
+
|
|
1147
|
+
// createFontAvailability(resolveDoc, bundledFonts) → isFontAvailable(family)
|
|
1148
|
+
// resolveDoc: () => document|undefined —— 惰性取宿主 document(SSR / 模块早于 DOM 就绪都安全)
|
|
1149
|
+
// bundledFonts: 随包内联 @font-face 的家族名列表(恒可用,不量宽)
|
|
1150
|
+
function createFontAvailability(resolveDoc, bundledFonts) {
|
|
1151
|
+
const cache = Object.create(null)
|
|
1152
|
+
|
|
1153
|
+
function measure(family) {
|
|
1154
|
+
const doc = resolveDoc && resolveDoc()
|
|
1155
|
+
if (!doc || typeof doc.createElement !== 'function') return null
|
|
1156
|
+
const host = doc.body || doc.documentElement
|
|
1157
|
+
if (!host || typeof host.appendChild !== 'function') return null
|
|
1158
|
+
const span = doc.createElement('span')
|
|
1159
|
+
span.style.cssText = 'position:absolute;left:-9999px;top:-9999px;visibility:hidden;white-space:pre;font-size:' + FONT_PROBE_SIZE + 'px;font-weight:400;font-style:normal;letter-spacing:0;font-feature-settings:normal'
|
|
1160
|
+
// 家族名必须用配对的引号整体包住(引号写错 → 整条声明被浏览器静默丢弃 → 所有字体都量成回退宽度,
|
|
1161
|
+
// 于是全部被误判成「未装」)。两端统一用双引号,赋不上值就直接返回 null 交给调用方保守处理。
|
|
1162
|
+
span.style.fontFamily = '"' + family + '"'
|
|
1163
|
+
if (span.style.fontFamily === '') return null // 声明没生效(家族名含引号等)→ 交给调用方保守处理
|
|
1164
|
+
span.textContent = FONT_PROBE_TEXT
|
|
1165
|
+
host.appendChild(span)
|
|
1166
|
+
let w = null
|
|
1167
|
+
try {
|
|
1168
|
+
w = span.getBoundingClientRect ? span.getBoundingClientRect().width : null
|
|
1169
|
+
} finally {
|
|
1170
|
+
if (typeof host.removeChild === 'function') host.removeChild(span)
|
|
1171
|
+
}
|
|
1172
|
+
return typeof w === 'number' && w > 0 ? w : null
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
return function isFontAvailable(family) {
|
|
1176
|
+
try {
|
|
1177
|
+
if (bundledFonts && bundledFonts.indexOf(family) >= 0) return true
|
|
1178
|
+
if (family in cache) return cache[family]
|
|
1179
|
+
const absent = measure(FONT_PROBE_ABSENT)
|
|
1180
|
+
const actual = measure(family)
|
|
1181
|
+
// 量不出来 → 保守可用,且不写缓存(环境后续可能就绪)
|
|
1182
|
+
if (absent === null || actual === null) return true
|
|
1183
|
+
const ok = Math.abs(actual - absent) >= 0.01
|
|
1184
|
+
cache[family] = ok
|
|
1185
|
+
return ok
|
|
1186
|
+
} catch (e) { return true }
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
__mods["font-avail"] = { FONT_PROBE_TEXT, FONT_PROBE_ABSENT, FONT_PROBE_SIZE, createFontAvailability }
|
|
1191
|
+
})();
|
|
1192
|
+
(function () {
|
|
1193
|
+
// font-names.mjs — 任意字体族名的净化与 CSS 安全包裹
|
|
1194
|
+
//
|
|
1195
|
+
// 为什么单独成模块:族名会经字符串拼接落进注入宿主页面的 <style>(generate.mjs 的
|
|
1196
|
+
// buildTypographyCss)。族名一旦含 `{` `}` `;` `<` 就能提前闭合规则或标签,把用户可控的
|
|
1197
|
+
// 值变成 CSS/HTML 注入。所以「净化」是引擎层的硬约束,不是面板层的输入提示。
|
|
1198
|
+
|
|
1199
|
+
// 族名长度上限:真实字体家族名最长也就数十字符(含 Nerd Font 后缀与 CJK 名),
|
|
1200
|
+
// 给足余量的同时挡住把整段文本塞进 CSS 的用法
|
|
1201
|
+
const FONT_NAME_MAX = 128
|
|
1202
|
+
|
|
1203
|
+
// CSS 字符串字面量无法安全承载的字符(会在注入的 <style> 里取得语法意义):
|
|
1204
|
+
// {} 结构 ; 声明分隔 <> 逃出标签 \ 转义 " ' 引号终结(' 单独其实安全,一并挡掉更省心)
|
|
1205
|
+
// ` 模板拼接 换行 / 制表 控制字符
|
|
1206
|
+
const UNSAFE_FONT_NAME_RE = /[{}<>;\\"'\r\n\t\f\v`\u0000-\u001f\u007f]/
|
|
1207
|
+
|
|
1208
|
+
// sanitizeFontName(name) → 合法族名 | null
|
|
1209
|
+
// 非法(非字符串 / 空 / 超长 / 含保留字符)一律返回 null,由调用方落到默认回退栈。
|
|
1210
|
+
// 刻意不做「剥离字符后继续用」的修复:修复会把一个用户没要的名字当成他的选择,比拒绝更糟。
|
|
1211
|
+
function sanitizeFontName(name) {
|
|
1212
|
+
if (typeof name !== 'string') return null
|
|
1213
|
+
const trimmed = name.trim()
|
|
1214
|
+
if (trimmed === '' || trimmed.length > FONT_NAME_MAX) return null
|
|
1215
|
+
if (UNSAFE_FONT_NAME_RE.test(trimmed)) return null
|
|
1216
|
+
return trimmed
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// quoteFontFamily(name) → "'Family Name'" | null
|
|
1220
|
+
// 用配对单引号整体包住族名;名字里若带单引号,改用双引号。
|
|
1221
|
+
// 两侧都带引号才是浏览器认得的「家族名」写法,裸写会被拆成多个家族名。
|
|
1222
|
+
// 拒绝(净化不通过)时返回 null —— 绝不返回未加引号的原始输入。
|
|
1223
|
+
function quoteFontFamily(name) {
|
|
1224
|
+
const safe = sanitizeFontName(name)
|
|
1225
|
+
if (safe === null) return null
|
|
1226
|
+
return safe.indexOf("'") >= 0 ? '"' + safe + '"' : "'" + safe + "'"
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
__mods["font-names"] = { FONT_NAME_MAX, sanitizeFontName, quoteFontFamily }
|
|
1230
|
+
})();
|
|
1231
|
+
(function () {
|
|
1232
|
+
// local-fonts.mjs — 本机字体清单:取(queryLocalFonts)→ 去重 → 等宽判定 → 候选分级
|
|
1233
|
+
//
|
|
1234
|
+
// document / window 与等宽测量句柄都以参数注入,保持引擎层可在 node 里直接跑(与 font-avail.mjs 同策)。
|
|
1235
|
+
//
|
|
1236
|
+
// 浏览器侧的三道门(实测于真实宿主,见 issue #11 证据表):
|
|
1237
|
+
// 1) 必须真实用户手势:click → await 别的东西 → queryLocalFonts 会丢手势(SecurityError);
|
|
1238
|
+
// 2) 必须已授权 local-fonts:拒绝抛 NotAllowedError;
|
|
1239
|
+
// 3) 页面必须可见:窗口不可见抛 SecurityError。
|
|
1240
|
+
// 另有「被自动拒绝时不抛错、直接返回空数组」这种宿主行为,故空数组不能一律说成「本机没字体」,
|
|
1241
|
+
// 要用 Permissions API 的状态区分(reason 取值见下)。
|
|
1242
|
+
|
|
1243
|
+
const { FONTS } = __mods["map-dsh"]
|
|
1244
|
+
const { quoteFontFamily } = __mods["font-names"]
|
|
1245
|
+
|
|
1246
|
+
// 等宽判定探针:必须含「窄 + 宽」两类字符 —— 比例字体里 i 与 W 宽度差得很远,
|
|
1247
|
+
// 等宽字体里两者逐像素同宽;只用单一种窄字符(如 iiiii)判不出任何东西。
|
|
1248
|
+
const MONO_PROBE_TEXT = 'iiiiWWWW'
|
|
1249
|
+
const MONO_PROBE_SIZE = 32
|
|
1250
|
+
// 等宽测量的数量上限:清单通常 200–400 条,超出部分不再量(省掉每条一次布局),
|
|
1251
|
+
// 只是排在「等宽优先」后面,仍可正常选用
|
|
1252
|
+
const MONO_MEASURE_LIMIT = 400
|
|
1253
|
+
|
|
1254
|
+
// 不可枚举/取不到清单的原因(面板按它给不同人话提示)
|
|
1255
|
+
const FONT_ENUM_UNAVAILABLE = 'unavailable' // 宿主平台没有这个 API(Firefox / Safari / 非安全上下文)
|
|
1256
|
+
const FONT_ENUM_DENIED = 'denied' // 用户或宿主拒绝了授权(NotAllowedError / 权限态 denied)
|
|
1257
|
+
const FONT_ENUM_BLOCKED = 'blocked' // 手势 / 页面可见性等 SecurityError
|
|
1258
|
+
const FONT_ENUM_EMPTY = 'empty' // 授了权也调得通,但没拿到任何字体(含被自动拒绝返回空数组)
|
|
1259
|
+
const FONT_ENUM_FAILED = 'failed' // 其它异常(兜底,绝不把异常抛给面板)
|
|
1260
|
+
|
|
1261
|
+
function reasonOfError(err) {
|
|
1262
|
+
const name = err && err.name
|
|
1263
|
+
if (name === 'NotAllowedError') return FONT_ENUM_DENIED
|
|
1264
|
+
if (name === 'SecurityError') return FONT_ENUM_BLOCKED
|
|
1265
|
+
return FONT_ENUM_FAILED
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// queryLocalFonts 的权限态('granted' / 'denied' / 'prompt'),拿不到就解析成 null
|
|
1269
|
+
function permissionState(nav) {
|
|
1270
|
+
try {
|
|
1271
|
+
const perms = nav && nav.permissions
|
|
1272
|
+
if (!perms || typeof perms.query !== 'function') return Promise.resolve(null)
|
|
1273
|
+
const p = perms.query({ name: 'local-fonts' })
|
|
1274
|
+
if (!p || typeof p.then !== 'function') return Promise.resolve(null)
|
|
1275
|
+
return p.then(function (st) { return (st && st.state) || null }, function () { return null })
|
|
1276
|
+
} catch (e) { return Promise.resolve(null) }
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// collectLocalFonts(scope, opts) → { ok, fonts, reason }
|
|
1280
|
+
// scope: { queryLocalFonts?, navigator?, document? }(浏览器里直接传 window)
|
|
1281
|
+
// opts.measureMono(families): (families) => { family: true }(默认见 measureMonospaceFonts)
|
|
1282
|
+
// opts.onCalled(): 枚举调用**同步发起后立刻**回调(无参)。用来验证「调用确实落在用户手势的
|
|
1283
|
+
// 同步段里」——一旦有人把调用挪到 await 之后,手势就丢了,这个信号会先于失败出现(见 #11 三道门)。
|
|
1284
|
+
// 调用必须在真实用户手势里**同步发起**(本函数同步发起调用,返回 promise)。
|
|
1285
|
+
// 不抛异常:所有失败都收敛成 reason,面板据此决定提示文案。
|
|
1286
|
+
function collectLocalFonts(scope, opts) {
|
|
1287
|
+
const settings = opts || {}
|
|
1288
|
+
const win = scope || {}
|
|
1289
|
+
const query = win.queryLocalFonts
|
|
1290
|
+
if (typeof query !== 'function') {
|
|
1291
|
+
return Promise.resolve({ ok: false, fonts: [], reason: FONT_ENUM_UNAVAILABLE })
|
|
1292
|
+
}
|
|
1293
|
+
let raw
|
|
1294
|
+
try {
|
|
1295
|
+
// 同步发起:调用发生在手势事件处理器的同步段里,await 之后就不算手势了
|
|
1296
|
+
raw = query.call(win)
|
|
1297
|
+
if (typeof settings.onCalled === 'function') settings.onCalled()
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
if (typeof settings.onCalled === 'function') settings.onCalled(err)
|
|
1300
|
+
return Promise.resolve({ ok: false, fonts: [], reason: reasonOfError(err) })
|
|
1301
|
+
}
|
|
1302
|
+
return Promise.resolve(raw).then(function (list) {
|
|
1303
|
+
const families = dedupeFamilies(list)
|
|
1304
|
+
if (families.length === 0) {
|
|
1305
|
+
// 空清单有两种来源:本机真没字体 / 宿主静默拒绝。用权限态区分,别一口咬定「没字体」。
|
|
1306
|
+
return permissionState(win.navigator).then(function (state) {
|
|
1307
|
+
return { ok: false, fonts: [], reason: state === 'denied' ? FONT_ENUM_DENIED : FONT_ENUM_EMPTY }
|
|
1308
|
+
})
|
|
1309
|
+
}
|
|
1310
|
+
return families
|
|
1311
|
+
}, function (err) {
|
|
1312
|
+
return { ok: false, fonts: [], reason: reasonOfError(err) }
|
|
1313
|
+
}).then(function (value) {
|
|
1314
|
+
// 成功路径拿到的是裸数组,失败路径是结果对象(在上一段收敛)
|
|
1315
|
+
if (!Array.isArray(value)) return value
|
|
1316
|
+
const measure = settings.measureMono || function (list) { return measureMonospaceFonts(win.document, list) }
|
|
1317
|
+
let mono = {}
|
|
1318
|
+
// 只量前 MONO_MEASURE_LIMIT 款:清单通常 200–400 条,超出部分不量(省掉每条一次布局),
|
|
1319
|
+
// 只是排在「等宽优先」后面,仍可正常选用
|
|
1320
|
+
try { mono = measure(value.slice(0, MONO_MEASURE_LIMIT)) || {} } catch (e) { mono = {} }
|
|
1321
|
+
// 等宽置顶(不按名字猜,靠实测宽度),组内按字母序;非等宽同样列出可选
|
|
1322
|
+
const fonts = value.slice().sort(function (a, b) {
|
|
1323
|
+
const d = (mono[a] ? 0 : 1) - (mono[b] ? 0 : 1)
|
|
1324
|
+
return d !== 0 ? d : a.localeCompare(b)
|
|
1325
|
+
})
|
|
1326
|
+
return { ok: true, fonts: fonts, mono: mono, reason: null }
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// 按 family 去重(一个族通常返回多份 style/bold/italic 记录,下拉只列族名一次)
|
|
1331
|
+
function dedupeFamilies(list) {
|
|
1332
|
+
const out = []
|
|
1333
|
+
const seen = Object.create(null)
|
|
1334
|
+
if (!list) return out
|
|
1335
|
+
for (let i = 0; i < list.length; i++) {
|
|
1336
|
+
const item = list[i]
|
|
1337
|
+
// FontData 的族名字段是 family;个别环境可能只给 fullName,兜底取它
|
|
1338
|
+
const name = item && typeof item === 'object'
|
|
1339
|
+
? (typeof item.family === 'string' && item.family !== '' ? item.family
|
|
1340
|
+
: (typeof item.fullName === 'string' ? item.fullName : null))
|
|
1341
|
+
: null
|
|
1342
|
+
if (typeof name !== 'string') continue
|
|
1343
|
+
const key = name.trim()
|
|
1344
|
+
if (key === '' || seen[key]) continue
|
|
1345
|
+
seen[key] = true
|
|
1346
|
+
out.push(key)
|
|
1347
|
+
}
|
|
1348
|
+
return out
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// measureMonospaceFonts(doc, families) → { family: true }
|
|
1352
|
+
// canvas 实测量宽:探针文本里所有窄字符同宽 = 等宽。长度少于 2 的家族名不量(画布量不出有意义的差别)。
|
|
1353
|
+
// 量不了(无 document / 无 canvas / 抛异常)就返回空表 —— 面板按「未判为等宽」处理,绝不误标。
|
|
1354
|
+
function measureMonospaceFonts(doc, families) {
|
|
1355
|
+
const result = Object.create(null)
|
|
1356
|
+
const list = families || []
|
|
1357
|
+
if (!doc || typeof doc.createElement !== 'function') return result
|
|
1358
|
+
let ctx = null
|
|
1359
|
+
try {
|
|
1360
|
+
const canvas = doc.createElement('canvas')
|
|
1361
|
+
ctx = canvas && typeof canvas.getContext === 'function' ? canvas.getContext('2d') : null
|
|
1362
|
+
} catch (e) { ctx = null }
|
|
1363
|
+
if (!ctx || typeof ctx.measureText !== 'function') return result
|
|
1364
|
+
for (let i = 0; i < list.length; i++) {
|
|
1365
|
+
const family = list[i]
|
|
1366
|
+
if (typeof family !== 'string' || family.length < 2) continue
|
|
1367
|
+
let widths = null
|
|
1368
|
+
try {
|
|
1369
|
+
ctx.font = MONO_PROBE_SIZE + 'px ' + quoteFontFamily(family)
|
|
1370
|
+
widths = []
|
|
1371
|
+
for (let c = 0; c < MONO_PROBE_TEXT.length; c++) {
|
|
1372
|
+
widths.push(ctx.measureText(MONO_PROBE_TEXT.charAt(c)).width)
|
|
1373
|
+
}
|
|
1374
|
+
} catch (e) { continue }
|
|
1375
|
+
if (!widths || widths.length < MONO_PROBE_TEXT.length) continue
|
|
1376
|
+
let mono = true
|
|
1377
|
+
for (let w = 1; w < widths.length; w++) {
|
|
1378
|
+
if (Math.abs(widths[w] - widths[0]) >= 0.01) { mono = false; break }
|
|
1379
|
+
}
|
|
1380
|
+
if (mono) result[family] = true
|
|
1381
|
+
}
|
|
1382
|
+
return result
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// buildFontCandidates(localFamilies, isFontAvailable, monoMap, presetOrder, bundledFonts) → 候选清单
|
|
1386
|
+
// 一条候选:{ key, isPreset, isLocal, bundled, ok, installed, mono, stack }
|
|
1387
|
+
// isPreset 在预设表里(置顶「常用预设」分区)
|
|
1388
|
+
// isLocal 在本机枚举结果里(本地分区的成员资格以枚举为准)
|
|
1389
|
+
// bundled 随包内联 @font-face,恒可用(面板据此不标「未装」)
|
|
1390
|
+
// ok 可用(本地枚举或宽度对比法判定;bundled 恒真)
|
|
1391
|
+
// installed ok && !bundled —— 即「确实在本机装过」,可直接写「已装」;bundled 的恒可用但不必说「本机装了」
|
|
1392
|
+
// mono 等宽(canvas 实测;没量到就是 false)
|
|
1393
|
+
// stack 该项自己的回退栈(预设查表;自定义族名 = null,由 codeFontStack 合成)
|
|
1394
|
+
// 排序:预设按传入顺序在前,本机字体按传入顺序(已被 collectLocalFonts 排成「等宽置顶 + 字母序」)在后。
|
|
1395
|
+
function buildFontCandidates(localFamilies, isFontAvailable, monoMap, presetOrder, bundledFonts) {
|
|
1396
|
+
const available = typeof isFontAvailable === 'function' ? isFontAvailable : function () { return true }
|
|
1397
|
+
const bundled = bundledFonts || []
|
|
1398
|
+
const mono = monoMap || {}
|
|
1399
|
+
const locals = []
|
|
1400
|
+
const localSeen = Object.create(null)
|
|
1401
|
+
for (const name of (localFamilies || [])) {
|
|
1402
|
+
if (typeof name !== 'string') continue
|
|
1403
|
+
const key = name.trim()
|
|
1404
|
+
if (key === '' || localSeen[key]) continue
|
|
1405
|
+
localSeen[key] = true
|
|
1406
|
+
locals.push(key)
|
|
1407
|
+
}
|
|
1408
|
+
const presets = (presetOrder && presetOrder.length ? presetOrder : Object.keys(FONTS))
|
|
1409
|
+
const out = []
|
|
1410
|
+
const taken = Object.create(null)
|
|
1411
|
+
for (const key of presets) {
|
|
1412
|
+
if (taken[key]) continue
|
|
1413
|
+
taken[key] = true
|
|
1414
|
+
const isBundled = bundled.indexOf(key) >= 0
|
|
1415
|
+
// 随包字体恒可用;其余预设靠宽度对比法探;本机字体不需要探(枚举到即存在,见下)
|
|
1416
|
+
const ok = isBundled ? true : available(key)
|
|
1417
|
+
out.push({
|
|
1418
|
+
key: key,
|
|
1419
|
+
isPreset: true,
|
|
1420
|
+
isLocal: locals.indexOf(key) >= 0,
|
|
1421
|
+
bundled: isBundled,
|
|
1422
|
+
ok: ok,
|
|
1423
|
+
installed: ok && !isBundled,
|
|
1424
|
+
mono: !!mono[key],
|
|
1425
|
+
stack: FONTS[key],
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
for (const key of locals) {
|
|
1429
|
+
if (taken[key]) continue
|
|
1430
|
+
taken[key] = true
|
|
1431
|
+
out.push({
|
|
1432
|
+
key: key,
|
|
1433
|
+
isPreset: false,
|
|
1434
|
+
isLocal: true,
|
|
1435
|
+
bundled: false,
|
|
1436
|
+
ok: true,
|
|
1437
|
+
installed: true,
|
|
1438
|
+
mono: !!mono[key],
|
|
1439
|
+
stack: null,
|
|
1440
|
+
})
|
|
1441
|
+
}
|
|
1442
|
+
return out
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
__mods["local-fonts"] = { MONO_PROBE_TEXT, MONO_PROBE_SIZE, MONO_MEASURE_LIMIT, FONT_ENUM_UNAVAILABLE, FONT_ENUM_DENIED, FONT_ENUM_BLOCKED, FONT_ENUM_EMPTY, FONT_ENUM_FAILED, collectLocalFonts, dedupeFamilies, measureMonospaceFonts, buildFontCandidates }
|
|
1446
|
+
})();
|
|
1447
|
+
(function () {
|
|
307
1448
|
// generate.mjs — 生成器:解析后的主题色位 + 排印参数 → DSH 注入物 { tokens, css }
|
|
308
1449
|
// 不变式:输出完全由输入决定(确定性);system 主题(colors=null)只输出排印,不碰颜色
|
|
309
1450
|
const { TOKEN_MAP, DERIVED_TOKENS, SHIKI_MAP, CSS_RULES, FONTS, SANS_STACK } = __mods["map-dsh"]
|
|
310
1451
|
const { FONT_FACE_CSS } = __mods["font-face"]
|
|
1452
|
+
const { quoteFontFamily } = __mods["font-names"]
|
|
311
1453
|
const { withAlpha } = __mods["resolve"]
|
|
312
1454
|
|
|
313
1455
|
const TRANSPARENT = 'transparent'
|
|
@@ -344,13 +1486,22 @@ function buildTokens(colors) {
|
|
|
344
1486
|
return tokens
|
|
345
1487
|
}
|
|
346
1488
|
|
|
1489
|
+
// 代码字体栈:fontKey 是预设名 → 查预设表;是任意族名(本机字体现选)→ 把该族名安全包裹后
|
|
1490
|
+
// 夹在默认预设栈最前(用户字体 → 随包 OFL 字体 → 系统 → CJK),缺字不断行;
|
|
1491
|
+
// 名字非法(含 {} ; < 等能逃出注入 <style> 的字符)→ 一律落到默认栈,绝不把原始输入拼进 CSS。
|
|
1492
|
+
function codeFontStack(fontKey) {
|
|
1493
|
+
if (FONTS[fontKey]) return FONTS[fontKey]
|
|
1494
|
+
const quoted = quoteFontFamily(fontKey)
|
|
1495
|
+
return quoted ? quoted + ',' + FONTS['JetBrains Mono'] : FONTS['JetBrains Mono']
|
|
1496
|
+
}
|
|
1497
|
+
|
|
347
1498
|
// 排印 CSS(与主题无关,system 模式也输出)
|
|
348
1499
|
function buildTypographyCss(typography) {
|
|
349
1500
|
const size = (typography && typography.size) || 13
|
|
350
1501
|
const mode = (typography && typography.mode) || 'mono'
|
|
351
1502
|
const fontKey = (typography && typography.fontKey) || 'JetBrains Mono'
|
|
352
|
-
const
|
|
353
|
-
const
|
|
1503
|
+
const codeFont = codeFontStack(fontKey)
|
|
1504
|
+
const bodyFont = mode === 'mono' ? codeFont : SANS_STACK
|
|
354
1505
|
const lh = size + 9
|
|
355
1506
|
const small = size - 1
|
|
356
1507
|
return [FONT_FACE_CSS,
|
|
@@ -409,7 +1560,7 @@ function generateTheme(colors, typography, themeName) {
|
|
|
409
1560
|
meta: { theme: themeName || (colors ? 'theme' : 'system'), typography: typography || {} },
|
|
410
1561
|
}
|
|
411
1562
|
}
|
|
412
|
-
__mods["generate"] = { buildTokens, buildTypographyCss, buildColorCss, generateTheme }
|
|
1563
|
+
__mods["generate"] = { buildTokens, codeFontStack, buildTypographyCss, buildColorCss, generateTheme }
|
|
413
1564
|
})();
|
|
414
1565
|
(function () {
|
|
415
1566
|
// zh-names.mjs — 主题中文名表(单一来源)
|
|
@@ -418,7 +1569,7 @@ __mods["generate"] = { buildTokens, buildTypographyCss, buildColorCss, generateT
|
|
|
418
1569
|
|
|
419
1570
|
const THEME_ZH = {
|
|
420
1571
|
opencode: "opencode", tokyonight: "东京之夜", dracula: "德古拉", gruvbox: "复古凹槽",
|
|
421
|
-
matrix: "黑客帝国",
|
|
1572
|
+
matrix: "黑客帝国", rosepine: "玫瑰松林", catppuccin: "卡布奇诺", "catppuccin-frappe": "卡布奇诺·冰沙",
|
|
422
1573
|
"catppuccin-macchiato": "卡布奇诺·玛奇朵", solarized: "日光浴", synthwave84: "合成波 84",
|
|
423
1574
|
everforest: "常青森林", nord: "北极", kanagawa: "神奈川", nightowl: "夜猫子",
|
|
424
1575
|
"one-dark": "One Dark", monokai: "Monokai", palenight: "苍白之夜", material: "材料设计",
|
|
@@ -8499,20 +9650,320 @@ function auditAll() {
|
|
|
8499
9650
|
__mods["index"] = { renderTheme, previewColors, themeNames, themeGroups, GROUP_ORDER, GROUP_COLORS, themeStats, auditAll }
|
|
8500
9651
|
})();
|
|
8501
9652
|
(function () {
|
|
9653
|
+
/**
|
|
9654
|
+
* runtime/channel.mjs — 浏览器半与宿主半共用的接线常量(单一真源)
|
|
9655
|
+
*
|
|
9656
|
+
* 两侧必须逐个对上,写错一个就整条通道静默不通,所以只在这里定义一次:
|
|
9657
|
+
* - 宿主半用 fetch.register 注册精确路由 ROUTE_PATH;
|
|
9658
|
+
* - 浏览器半用 connection.rpc.call(CHANNEL, ENDPOINT, ...) 调它(拼出的路径同为 ROUTE_PATH)。
|
|
9659
|
+
*
|
|
9660
|
+
* 命名规则(DSH 连接服务):通道 CHANNEL 必须匹配 /^\/[A-Za-z0-9._~-]+$/(单段,故只能 /api);
|
|
9661
|
+
* 端点 ENDPOINT 必须匹配 /^[A-Za-z0-9_$.-]+$/。真正的电话名在请求体里,不受这条限制。
|
|
9662
|
+
*/
|
|
9663
|
+
|
|
9664
|
+
/** /api 载体上的通道名(与 deck、im-companion 同构:DSH 公开的精确路由载体)。 */
|
|
9665
|
+
const CHANNEL = '/api'
|
|
9666
|
+
/** 本插件的端点名;注册路径 = CHANNEL + '/' + ENDPOINT。 */
|
|
9667
|
+
const ENDPOINT = 'opencode-palette'
|
|
9668
|
+
/** 宿主注册的精确路径,客户端拼出的路径必须与它一致。 */
|
|
9669
|
+
const ROUTE_PATH = CHANNEL + '/' + ENDPOINT
|
|
9670
|
+
|
|
9671
|
+
/** 插件标识(日志落盘目录、更新环境指纹、事件清单三处共用一个值)。 */
|
|
9672
|
+
const PLUGIN_ID = 'dsh-opencode-palette'
|
|
9673
|
+
/** 电话名前缀:日志 5 条与更新 3 条共用,拼出的电话名形如 palette.updateStatus。 */
|
|
9674
|
+
const PHONE_PREFIX = 'palette'
|
|
9675
|
+
/** 要检查更新的那个包(本插件自己)。 */
|
|
9676
|
+
const TARGET_PACKAGE_NAME = 'dsh-opencode-palette'
|
|
9677
|
+
|
|
9678
|
+
/** 浏览器半向宿主半回一个失败的调用结果(宿主不可用时用,形状与宿主回包一致)。 */
|
|
9679
|
+
const HOST_UNAVAILABLE = 'host-unavailable'
|
|
9680
|
+
|
|
9681
|
+
__mods["channel"] = { CHANNEL, ENDPOINT, ROUTE_PATH, PLUGIN_ID, PHONE_PREFIX, TARGET_PACKAGE_NAME, HOST_UNAVAILABLE }
|
|
9682
|
+
})();
|
|
9683
|
+
(function () {
|
|
9684
|
+
/**
|
|
9685
|
+
* runtime/update-panel.mjs — 面板「检查更新」的状态机(纯逻辑:不碰 React、不碰 DOM、不碰全局宿主)
|
|
9686
|
+
*
|
|
9687
|
+
* 三条口径(沿用更新包 README 与规格):
|
|
9688
|
+
* 1. 「待重启」只认宿主当场算出的原因码 pending-restart,不自己比较版本号;
|
|
9689
|
+
* 2. 打开面板只静默读一次本地状态(只读、不联网);联网只在用户亲手点「检查更新」时发生一次;
|
|
9690
|
+
* 3. 有新版才开弹窗;无新版只回一句「已是最新」,不弹窗、不打扰。
|
|
9691
|
+
*
|
|
9692
|
+
* 电话名与轮询间隔由调用方从更新包的客户端入口派生后传进来(本文件不写字面量)。
|
|
9693
|
+
*/
|
|
9694
|
+
|
|
9695
|
+
/** 宿主回包 → 面板要的几个事实(纯函数,便于离线核对)。 */
|
|
9696
|
+
function readSnapshot(res) {
|
|
9697
|
+
const snap = res && res.snapshot ? res.snapshot : null
|
|
9698
|
+
const job = snap && snap.job ? snap.job : null
|
|
9699
|
+
const text = function (value) { return typeof value === 'string' ? value : '' }
|
|
9700
|
+
return {
|
|
9701
|
+
job: job,
|
|
9702
|
+
jobState: job && job.state ? String(job.state) : null,
|
|
9703
|
+
jobMessage: job && job.message ? String(job.message) : null,
|
|
9704
|
+
blocked: snap && snap.blockedReason ? String(snap.blockedReason) : null,
|
|
9705
|
+
pending: !!(snap && snap.blockedReason === 'pending-restart'),
|
|
9706
|
+
running: snap ? text(snap.runningVersion) : '',
|
|
9707
|
+
installed: snap ? text(snap.installedVersion) : '',
|
|
9708
|
+
latest: snap && text(snap.latestVersion) ? text(snap.latestVersion) : null,
|
|
9709
|
+
canInstall: !!(snap && snap.canInstall === true && text(snap.latestVersion)),
|
|
9710
|
+
hasManual: !!(res && Object.prototype.hasOwnProperty.call(res, 'manual')),
|
|
9711
|
+
manual: res ? res.manual : null,
|
|
9712
|
+
checkId: res && res.receipt && res.receipt.checkId ? String(res.receipt.checkId) : null,
|
|
9713
|
+
}
|
|
9714
|
+
}
|
|
9715
|
+
|
|
9716
|
+
/** 按钮状态:UI 据此选词条,不在渲染里堆条件。 */
|
|
9717
|
+
function buttonState(state) {
|
|
9718
|
+
if (state.installing) return 'installing'
|
|
9719
|
+
if (state.checking) return 'checking'
|
|
9720
|
+
if (state.pending) return 'pending'
|
|
9721
|
+
if (state.hasNew) return 'hasNew'
|
|
9722
|
+
return 'idle'
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9725
|
+
/** 装不了的原因码 → 一句话人话的 i18n 键(更新包 README 第 8 节八种)。 */
|
|
9726
|
+
function blockedReasonKey(code) {
|
|
9727
|
+
const known = [
|
|
9728
|
+
'unknown-profile',
|
|
9729
|
+
'source-install',
|
|
9730
|
+
'invalid-installation',
|
|
9731
|
+
'installation-changed',
|
|
9732
|
+
'pending-restart',
|
|
9733
|
+
'registry-conflict',
|
|
9734
|
+
'incompatible-node',
|
|
9735
|
+
'recovery-required',
|
|
9736
|
+
]
|
|
9737
|
+
return known.indexOf(String(code)) >= 0 ? 'blocked.' + code : 'blocked.unknown'
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9740
|
+
/**
|
|
9741
|
+
* 建更新控制器。
|
|
9742
|
+
* @param {object} deps
|
|
9743
|
+
* @param {((phone: string, args: object) => Promise<object>)|null} deps.call 宿主桥(不可用时整块降级)
|
|
9744
|
+
* @param {{updateStatus: string, updateCheck: string, updateInstall: string}} deps.phones 电话名(从更新包派生)
|
|
9745
|
+
* @param {number} deps.pollMs 安装中轮询间隔(从更新包派生)
|
|
9746
|
+
* @param {(level: string, event: string, fields: object) => void} [deps.log] 记一行日志
|
|
9747
|
+
* @param {(fn: () => void, ms: number) => unknown} [deps.setTimer] / @param {(id: unknown) => void} [deps.clearTimer]
|
|
9748
|
+
*/
|
|
9749
|
+
function createUpdateController(deps) {
|
|
9750
|
+
const input = deps || {}
|
|
9751
|
+
const call = typeof input.call === 'function' ? input.call : null
|
|
9752
|
+
const phones = input.phones || null
|
|
9753
|
+
const pollMs = typeof input.pollMs === 'number' && input.pollMs > 0 ? input.pollMs : 1000
|
|
9754
|
+
const log = typeof input.log === 'function' ? input.log : function () {}
|
|
9755
|
+
const setTimer = typeof input.setTimer === 'function' ? input.setTimer : setTimeout
|
|
9756
|
+
const clearTimer = typeof input.clearTimer === 'function' ? input.clearTimer : clearTimeout
|
|
9757
|
+
|
|
9758
|
+
let state = {
|
|
9759
|
+
available: !!(call && phones && phones.updateStatus),
|
|
9760
|
+
checking: false,
|
|
9761
|
+
installing: false,
|
|
9762
|
+
dialogOpen: false,
|
|
9763
|
+
running: '',
|
|
9764
|
+
installed: '',
|
|
9765
|
+
latest: null,
|
|
9766
|
+
hasNew: false,
|
|
9767
|
+
canInstall: false,
|
|
9768
|
+
blocked: null,
|
|
9769
|
+
manual: null,
|
|
9770
|
+
checkId: null,
|
|
9771
|
+
jobState: null,
|
|
9772
|
+
jobMessage: null,
|
|
9773
|
+
pending: false,
|
|
9774
|
+
failure: null,
|
|
9775
|
+
}
|
|
9776
|
+
let pollTimer = null
|
|
9777
|
+
const listeners = []
|
|
9778
|
+
|
|
9779
|
+
function emit() {
|
|
9780
|
+
for (let i = 0; i < listeners.length; i = i + 1) {
|
|
9781
|
+
try { listeners[i]() } catch (e) { /* 订阅者自己的错不影响状态机 */ }
|
|
9782
|
+
}
|
|
9783
|
+
}
|
|
9784
|
+
function patch(next) {
|
|
9785
|
+
state = Object.assign({}, state, next)
|
|
9786
|
+
emit()
|
|
9787
|
+
}
|
|
9788
|
+
function record(level, event, fields, latencyMs) {
|
|
9789
|
+
try { log(level, event, fields) } catch (e) { /* 忽略 */ }
|
|
9790
|
+
}
|
|
9791
|
+
function stopPolling() {
|
|
9792
|
+
if (pollTimer !== null) {
|
|
9793
|
+
try { clearTimer(pollTimer) } catch (e) { /* 忽略 */ }
|
|
9794
|
+
pollTimer = null
|
|
9795
|
+
}
|
|
9796
|
+
}
|
|
9797
|
+
function startPolling() {
|
|
9798
|
+
stopPolling()
|
|
9799
|
+
pollTimer = setTimer(function () {
|
|
9800
|
+
pollTimer = null
|
|
9801
|
+
void readStatus()
|
|
9802
|
+
}, pollMs)
|
|
9803
|
+
}
|
|
9804
|
+
|
|
9805
|
+
/** 把一次宿主回包并进状态(不弹窗、不提示,纯收状态)。 */
|
|
9806
|
+
function absorb(res) {
|
|
9807
|
+
const facts = readSnapshot(res)
|
|
9808
|
+
patch({
|
|
9809
|
+
running: facts.running || state.running,
|
|
9810
|
+
installed: facts.installed || state.installed,
|
|
9811
|
+
latest: facts.latest,
|
|
9812
|
+
hasNew: facts.canInstall,
|
|
9813
|
+
canInstall: facts.canInstall,
|
|
9814
|
+
blocked: facts.blocked,
|
|
9815
|
+
pending: facts.pending,
|
|
9816
|
+
jobState: facts.jobState,
|
|
9817
|
+
jobMessage: facts.jobMessage,
|
|
9818
|
+
checkId: facts.checkId || state.checkId,
|
|
9819
|
+
manual: facts.hasManual ? facts.manual : state.manual,
|
|
9820
|
+
})
|
|
9821
|
+
if (facts.jobState === 'installing' || facts.jobState === 'verifying') startPolling()
|
|
9822
|
+
else stopPolling()
|
|
9823
|
+
return facts
|
|
9824
|
+
}
|
|
9825
|
+
|
|
9826
|
+
function transportFail(phone, kind, error) {
|
|
9827
|
+
record('warn', 'host.call.fail', {
|
|
9828
|
+
method: phone,
|
|
9829
|
+
kind: kind,
|
|
9830
|
+
errorHash: hash8(String((error && error.message) || error || kind)),
|
|
9831
|
+
pluginId: 'dsh-opencode-palette',
|
|
9832
|
+
})
|
|
9833
|
+
}
|
|
9834
|
+
|
|
9835
|
+
async function invoke(phone, args) {
|
|
9836
|
+
const startedAt = Date.now()
|
|
9837
|
+
const res = await call(phone, args)
|
|
9838
|
+
if (res && res.ok === true) {
|
|
9839
|
+
record('info', 'host.call', {
|
|
9840
|
+
method: phone,
|
|
9841
|
+
latencyMs: Date.now() - startedAt,
|
|
9842
|
+
ok: true,
|
|
9843
|
+
kind: String((args && args.kind) || phone),
|
|
9844
|
+
pluginId: 'dsh-opencode-palette',
|
|
9845
|
+
})
|
|
9846
|
+
} else {
|
|
9847
|
+
transportFail(phone, 'not-ok', (res && res.error) || 'not-ok')
|
|
9848
|
+
}
|
|
9849
|
+
return res
|
|
9850
|
+
}
|
|
9851
|
+
|
|
9852
|
+
/** 打开面板时的静默读(本地、不联网)。 */
|
|
9853
|
+
async function readStatus() {
|
|
9854
|
+
if (!state.available || state.checking || state.installing) return null
|
|
9855
|
+
try {
|
|
9856
|
+
const res = await invoke(phones.updateStatus, {})
|
|
9857
|
+
if (res && res.ok === true) { absorb(res); return res }
|
|
9858
|
+
return null
|
|
9859
|
+
} catch (e) {
|
|
9860
|
+
transportFail(phones.updateStatus, 'throw', e)
|
|
9861
|
+
return null
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
|
|
9865
|
+
/** 用户亲手点的检查(联网一次)。回 'new' | 'latest' | 'failed'。 */
|
|
9866
|
+
async function check() {
|
|
9867
|
+
if (!state.available) return 'failed'
|
|
9868
|
+
if (state.checking || state.installing) return 'busy'
|
|
9869
|
+
// 已查到有新版且凭证还在:直接开弹窗,不重复联网
|
|
9870
|
+
if (state.hasNew && state.checkId) { patch({ dialogOpen: true, failure: null }); return 'new' }
|
|
9871
|
+
patch({ checking: true, failure: null })
|
|
9872
|
+
try {
|
|
9873
|
+
const res = await invoke(phones.updateCheck, {})
|
|
9874
|
+
patch({ checking: false })
|
|
9875
|
+
if (!res || res.ok !== true) { return 'failed' }
|
|
9876
|
+
const facts = absorb(res)
|
|
9877
|
+
if (facts.canInstall) { patch({ dialogOpen: true }); return 'new' }
|
|
9878
|
+
if (facts.pending) return 'latest'
|
|
9879
|
+
return 'latest'
|
|
9880
|
+
} catch (e) {
|
|
9881
|
+
patch({ checking: false })
|
|
9882
|
+
transportFail(phones.updateCheck, 'throw', e)
|
|
9883
|
+
return 'failed'
|
|
9884
|
+
}
|
|
9885
|
+
}
|
|
9886
|
+
|
|
9887
|
+
/** 弹窗里的「立即升级」。 */
|
|
9888
|
+
async function install() {
|
|
9889
|
+
if (!state.available || state.installing) return false
|
|
9890
|
+
if (!state.checkId) return false
|
|
9891
|
+
patch({ installing: true, failure: null })
|
|
9892
|
+
const requestId = 'req-' + String(Date.now()) + '-' + String(Math.floor(Math.random() * 100000))
|
|
9893
|
+
try {
|
|
9894
|
+
const res = await invoke(phones.updateInstall, { checkId: state.checkId, requestId: requestId })
|
|
9895
|
+
patch({ installing: false })
|
|
9896
|
+
if (res && res.ok === true) {
|
|
9897
|
+
absorb(res)
|
|
9898
|
+
patch({ dialogOpen: false })
|
|
9899
|
+
return true
|
|
9900
|
+
}
|
|
9901
|
+
const facts = readSnapshot(res)
|
|
9902
|
+
patch({ blocked: facts.blocked || state.blocked, failure: 'install-failed' })
|
|
9903
|
+
await readStatus()
|
|
9904
|
+
return false
|
|
9905
|
+
} catch (e) {
|
|
9906
|
+
patch({ installing: false, failure: 'install-failed' })
|
|
9907
|
+
transportFail(phones.updateInstall, 'throw', e)
|
|
9908
|
+
try { await readStatus() } catch (e2) { /* 忽略 */ }
|
|
9909
|
+
return false
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
|
|
9913
|
+
return {
|
|
9914
|
+
getState: function () { return Object.assign({}, state) },
|
|
9915
|
+
subscribe: function (fn) {
|
|
9916
|
+
listeners.push(fn)
|
|
9917
|
+
return function () {
|
|
9918
|
+
const i = listeners.indexOf(fn)
|
|
9919
|
+
if (i >= 0) listeners.splice(i, 1)
|
|
9920
|
+
}
|
|
9921
|
+
},
|
|
9922
|
+
readStatus: readStatus,
|
|
9923
|
+
check: check,
|
|
9924
|
+
install: install,
|
|
9925
|
+
openDialog: function () { patch({ dialogOpen: true }) },
|
|
9926
|
+
closeDialog: function () { patch({ dialogOpen: false, failure: null }) },
|
|
9927
|
+
dispose: function () { stopPolling(); listeners.length = 0 },
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9931
|
+
/** 与日志包同款的 8 位散列(失败只记散列,不记原文)。 */
|
|
9932
|
+
function hash8(value) {
|
|
9933
|
+
try {
|
|
9934
|
+
const text = String(value || '')
|
|
9935
|
+
let h = 5381
|
|
9936
|
+
for (let i = 0; i < text.length; i = i + 1) h = ((h << 5) + h + text.charCodeAt(i)) >>> 0
|
|
9937
|
+
return ('0000000' + h.toString(16)).slice(-8)
|
|
9938
|
+
} catch (e) {
|
|
9939
|
+
return '00000000'
|
|
9940
|
+
}
|
|
9941
|
+
}
|
|
9942
|
+
|
|
9943
|
+
__mods["update-panel"] = { readSnapshot, buttonState, blockedReasonKey, createUpdateController, hash8 }
|
|
9944
|
+
})();
|
|
9945
|
+
(function () {
|
|
8502
9946
|
// runtime/client.mjs — 浏览器运行时:注入/热切换/持久化/设置面板(组合 1 布局)
|
|
8503
9947
|
// 布局:标题行 → 排印调节(顶部)→ 主题选择(色系分组标签 + mini 芯片)→ 状态开关
|
|
8504
9948
|
// 依赖注入:theme(dsh-client-ui-theme)、slots(settings.plugins.tab / tool.view.cordis)
|
|
8505
9949
|
const { renderTheme, previewColors, themeNames, themeGroups } = __mods["index"]
|
|
8506
|
-
const { FONTS
|
|
9950
|
+
const { FONTS } = __mods["map-dsh"]
|
|
8507
9951
|
const { BUNDLED_FONTS } = __mods["font-face"]
|
|
9952
|
+
const { createFontAvailability } = __mods["font-avail"]
|
|
9953
|
+
const { codeFontStack } = __mods["generate"]
|
|
9954
|
+
const { buildFontCandidates, collectLocalFonts } = __mods["local-fonts"]
|
|
8508
9955
|
const { THEME_ZH } = __mods["zh-names"]
|
|
9956
|
+
const { createClientLog } = __mods["log-client"]
|
|
9957
|
+
const { buildClientPhoneNames, CLIENT_POLL } = __mods["upd-client"]
|
|
9958
|
+
const { createUpdateController, buttonState, blockedReasonKey } = __mods["update-panel"]
|
|
9959
|
+
const { CHANNEL, ENDPOINT, PLUGIN_ID, PHONE_PREFIX } = __mods["channel"]
|
|
8509
9960
|
|
|
8510
9961
|
const STORAGE_KEY = 'dsh.opencode-palette.v2'
|
|
8511
9962
|
// 兼容迁移:旧插件(dsh-opencode-tui-theme)的本地设置键,读到即迁移到新键
|
|
8512
9963
|
const LEGACY_STORAGE_KEY = 'dsh.opencode-tui-theme.v2'
|
|
8513
9964
|
const DEFAULT_STATE = { enabled: true, theme: 'opencode', mode: 'mono', size: 13, fontKey: 'JetBrains Mono' }
|
|
8514
9965
|
// 构建时由 scripts/build-client.mjs 替换为 package.json 版本(面板底部署小字)
|
|
8515
|
-
const PALETTE_VERSION = '
|
|
9966
|
+
const PALETTE_VERSION = '2.0.1'
|
|
8516
9967
|
|
|
8517
9968
|
function getReact() {
|
|
8518
9969
|
if (typeof require === 'function') { try { return require('react') } catch (e) { /* 动态版无 require */ } }
|
|
@@ -8520,6 +9971,36 @@ function getReact() {
|
|
|
8520
9971
|
return null
|
|
8521
9972
|
}
|
|
8522
9973
|
|
|
9974
|
+
// ── 宿主桥:浏览器半调宿主半的电话 ──
|
|
9975
|
+
// 两条方言都要接:
|
|
9976
|
+
// 1) 包版(本插件装成 npm 包):ctx.get('connection').rpc.call('/api', 'opencode-palette', { method, payload })
|
|
9977
|
+
// 走宿主 connection.fetch.register 注册的精确路由;两个常量来自 runtime/channel.mjs(两侧同一份)。
|
|
9978
|
+
// 2) 动态版(cordis_define):runner 给闭包注入自由变量 host,直接 host.call(电话名, 入参)。
|
|
9979
|
+
// 都没有时返回 null,整块更新功能降级隐藏(不影响主题面板本身)。
|
|
9980
|
+
function rpcCall(rpc, phone, args) {
|
|
9981
|
+
return Promise.resolve(rpc.call(CHANNEL, ENDPOINT, { method: phone, payload: args })).then(function (res) {
|
|
9982
|
+
if (res && res.ok) return res.value
|
|
9983
|
+
const why = res && res.error && res.error.message ? res.error.message : 'rpc failed: ' + phone
|
|
9984
|
+
throw new Error(why)
|
|
9985
|
+
})
|
|
9986
|
+
}
|
|
9987
|
+
|
|
9988
|
+
function createHostBridge(ctx) {
|
|
9989
|
+
try {
|
|
9990
|
+
const connection = ctx && typeof ctx.get === 'function' ? ctx.get('connection') : null
|
|
9991
|
+
const rpc = connection && connection.rpc ? connection.rpc : null
|
|
9992
|
+
if (rpc && typeof rpc.call === 'function') {
|
|
9993
|
+
return { call: function (phone, args) { return rpcCall(rpc, phone, args) } }
|
|
9994
|
+
}
|
|
9995
|
+
} catch (e) { /* 落到下一条方言 */ }
|
|
9996
|
+
try {
|
|
9997
|
+
if (typeof host !== 'undefined' && host && typeof host.call === 'function') {
|
|
9998
|
+
return { call: function (phone, args) { return host.call(phone, args) } }
|
|
9999
|
+
}
|
|
10000
|
+
} catch (e) { /* 无宿主 */ }
|
|
10001
|
+
return null
|
|
10002
|
+
}
|
|
10003
|
+
|
|
8523
10004
|
function loadState() {
|
|
8524
10005
|
try {
|
|
8525
10006
|
// 新键优先;旧插件(dsh-opencode-tui-theme)的键命中则一次性迁移
|
|
@@ -8560,11 +10041,23 @@ const I18N = {
|
|
|
8560
10041
|
codeFont: { zh: '代码字体', en: 'Code font' },
|
|
8561
10042
|
fontNotInstalled: { zh: '本机未装', en: 'missing' },
|
|
8562
10043
|
fontLocal: { zh: '本地', en: 'local' },
|
|
10044
|
+
fontMono: { zh: '等宽', en: 'mono' },
|
|
10045
|
+
fontPresets: { zh: '常用预设', en: 'Common presets' },
|
|
10046
|
+
fontLocals: { zh: '本机字体', en: 'Installed on this machine' },
|
|
10047
|
+
fontSearch: { zh: '搜索本机字体…', en: 'Search installed fonts…' },
|
|
10048
|
+
fontNoMatch: { zh: '没有匹配的字体', en: 'No matching fonts' },
|
|
10049
|
+
fontCounting: { zh: '正在读取本机字体…', en: 'Reading installed fonts…' },
|
|
10050
|
+
fontLoadedCount: { zh: '已读取本机 {n} 款字体', en: '{n} fonts found on this machine' },
|
|
10051
|
+
fontScanFail: { zh: '未能读取本机字体清单,先列出常用预设', en: 'Could not read the font list — showing the common presets' },
|
|
10052
|
+
fontScanDenied: { zh: '本机字体访问被拒绝,浏览器地址栏授权后再试;先列出常用预设', en: 'Font access was denied — allow it in the browser, then retry; showing the common presets' },
|
|
10053
|
+
fontScanUnsupported: { zh: '当前环境不支持读取本机字体清单,先列出常用预设', en: 'This environment cannot list installed fonts — showing the common presets' },
|
|
10054
|
+
fontScanEmpty: { zh: '没读到本机字体清单,先列出常用预设', en: 'The font list came back empty — showing the common presets' },
|
|
10055
|
+
fontRetry: { zh: '重试', en: 'Retry' },
|
|
8563
10056
|
themeSection: { zh: '选择主题', en: 'Themes' },
|
|
8564
10057
|
themeCount: { zh: '38 款 · 按色系分组', en: '38 · by color family' },
|
|
8565
10058
|
search: { zh: '搜索主题…', en: 'Search themes…' },
|
|
8566
10059
|
noMatch: { zh: '未找到匹配的主题', en: 'No matching themes' },
|
|
8567
|
-
systemDefault: { zh: 'system
|
|
10060
|
+
systemDefault: { zh: 'system(跟随系统)', en: 'system (default)' },
|
|
8568
10061
|
'group.warm': { zh: '暖橙', en: 'Warm' },
|
|
8569
10062
|
'group.yellow-green': { zh: '黄绿', en: 'Yellow-green' },
|
|
8570
10063
|
'group.teal': { zh: '青绿', en: 'Teal' },
|
|
@@ -8574,6 +10067,45 @@ const I18N = {
|
|
|
8574
10067
|
'group.neutral': { zh: '中性', en: 'Neutral' },
|
|
8575
10068
|
'group.transparent': { zh: '透明', en: 'Transparent' },
|
|
8576
10069
|
'group.special': { zh: '特殊', en: 'Special' },
|
|
10070
|
+
// 底部「作者其他插件」引流卡片(兄弟插件导流位,对齐 MattSkillsDeck 同款卡片)
|
|
10071
|
+
authorPlugins: { zh: '作者其他插件', en: 'More from the author' },
|
|
10072
|
+
authorPluginOpen: { zh: '在新窗口打开', en: 'Open in new window' },
|
|
10073
|
+
'authorPlugin.skillsDeck': { zh: '装好即自带 25 个工程/效率技能,右侧面板直接调用', en: '25 engineering skills built in — call them from the side panel' },
|
|
10074
|
+
'authorPlugin.prompt': { zh: '常用 prompt 预置或者自定义保存,开发只需要一键注入,不再繁琐', en: 'Save your prompt presets, inject them into a dev task with one click' },
|
|
10075
|
+
'authorPlugin.imCompanion': { zh: 'dsh-im 的增强插件,在原插件基础上提供了超过你想象力的能力', en: "Supercharges dsh-im with more than you'd expect" },
|
|
10076
|
+
// ── 检查更新(面板头行按钮 + 升级弹窗 + 待重启横幅)──
|
|
10077
|
+
updateCheck: { zh: '检查更新', en: 'Check for updates' },
|
|
10078
|
+
updateChecking: { zh: '检查中…', en: 'Checking…' },
|
|
10079
|
+
updateInstalling: { zh: '正在升级…', en: 'Updating…' },
|
|
10080
|
+
updateToVersion: { zh: '更新至 v{v}', en: 'Update to v{v}' },
|
|
10081
|
+
updateRestart: { zh: '待重启', en: 'Restart needed' },
|
|
10082
|
+
updateLatest: { zh: '已是最新版本(v{v})', en: "You're on the latest version (v{v})" },
|
|
10083
|
+
updateCheckFail: { zh: '检查更新失败,请稍后再试', en: 'Could not check for updates — try again later' },
|
|
10084
|
+
updateToastRestart: { zh: '新版 v{v} 已装好,重启 DSH 后生效', en: 'v{v} installed — restart DSH to apply' },
|
|
10085
|
+
updateDialogTitle: { zh: '发现新版本 v{v}', en: 'New version v{v}' },
|
|
10086
|
+
updateVersions: { zh: '当前版本 v{running} → 最新版本 v{latest}', en: 'Current v{running} → latest v{latest}' },
|
|
10087
|
+
updateRestartNote: { zh: '装好后要重启 DSH 才会生效(正在运行的还是旧版)。', en: 'Restart DSH after installing — the running process keeps the old version until then.' },
|
|
10088
|
+
updateStart: { zh: '立即升级', en: 'Update now' },
|
|
10089
|
+
updateLater: { zh: '稍后', en: 'Later' },
|
|
10090
|
+
updateFailInstall: { zh: '升级失败,已保持当前版本', en: 'Update failed — the current version was kept' },
|
|
10091
|
+
updateFailChanged: { zh: '安装位置在升级过程中变了,重开 DSH 再试一次', en: 'The install location changed mid-update — reopen DSH and retry' },
|
|
10092
|
+
updateFailRecovery: { zh: '上次安装被打断,重新点一次「立即升级」', en: 'The last install was interrupted — press “Update now” again' },
|
|
10093
|
+
updateManualTitle: { zh: '自动升级没成功,可复制这条命令手动执行', en: 'Auto-update did not go through — copy this command and run it yourself' },
|
|
10094
|
+
updateManualNote: { zh: '在终端里执行(需要 pnpm 在 PATH 里)。', en: 'Run it in a terminal (pnpm must be on your PATH).' },
|
|
10095
|
+
updateCopy: { zh: '复制', en: 'Copy' },
|
|
10096
|
+
updateCopied: { zh: '已复制', en: 'Copied' },
|
|
10097
|
+
updateCopyFail: { zh: '复制失败,请手动选中复制', en: 'Copy failed — select the command manually' },
|
|
10098
|
+
updateUpToDateTitle: { zh: '已是最新版本', en: 'Up to date' },
|
|
10099
|
+
// 装不了的原因(更新包 README 第 8 节八种,文案照「用户该做什么」那一列)
|
|
10100
|
+
'blocked.unknown-profile': { zh: '使用范围认不出:检查范围名是否含特殊字符、目录是否还在', en: 'Unknown profile — check the profile name and that the directory still exists' },
|
|
10101
|
+
'blocked.source-install': { zh: '当前是按源码装的,想走更新先按版本号重装一次', en: 'Installed from source — reinstall by version first' },
|
|
10102
|
+
'blocked.invalid-installation': { zh: '已装的包不完整,先重装当前版本', en: 'The installed package is incomplete — reinstall the current version' },
|
|
10103
|
+
'blocked.installation-changed': { zh: '安装位置在使用中途变了,重开 DSH 再查一次', en: 'The install location changed — reopen DSH and check again' },
|
|
10104
|
+
'blocked.pending-restart': { zh: '新版已装到磁盘,重启 DSH 后生效', en: 'The new version is on disk — restart DSH to apply' },
|
|
10105
|
+
'blocked.registry-conflict': { zh: '清单里那行写的不是版本号,改成版本号再试', en: 'The profile manifest does not pin a version — pin one and retry' },
|
|
10106
|
+
'blocked.incompatible-node': { zh: '新版要求的 Node 更高,先升级 Node', en: 'The new version needs a newer Node — upgrade it first' },
|
|
10107
|
+
'blocked.recovery-required': { zh: '上次安装被打断,重新点一次安装', en: 'The last install was interrupted — retry the install' },
|
|
10108
|
+
'blocked.unknown': { zh: '当前装不了:重开 DSH 再查一次', en: 'Cannot update right now — reopen DSH and try again' },
|
|
8577
10109
|
}
|
|
8578
10110
|
|
|
8579
10111
|
// 语言检测(回退):html[lang] 优先,回退浏览器语言
|
|
@@ -8584,16 +10116,30 @@ function getLang() {
|
|
|
8584
10116
|
} catch (e) { return 'en' }
|
|
8585
10117
|
}
|
|
8586
10118
|
|
|
8587
|
-
// 字体可用性:随包字体(OFL 内联 @font-face
|
|
8588
|
-
//
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
10119
|
+
// 字体可用性:随包字体(OFL 内联 @font-face)恒可用;本机字体(SF Mono / Consolas /
|
|
10120
|
+
// Maple Mono NF CN 等)缺失即灰显提示(仍可选中,回退栈保证不断字)。
|
|
10121
|
+
// 判定实现(宽度对比法)与理由见 engine/font-avail.mjs。
|
|
10122
|
+
const isFontAvailable = createFontAvailability(
|
|
10123
|
+
() => (typeof document === 'undefined' ? undefined : document),
|
|
10124
|
+
BUNDLED_FONTS
|
|
10125
|
+
)
|
|
10126
|
+
|
|
10127
|
+
// ── 本机字体清单(枚举 → 去重 → 等宽置顶 → 候选分级)──
|
|
10128
|
+
// 缓存:一次会话只读一次本机清单(同一次枚举的结论对面板多次开合都成立);
|
|
10129
|
+
// 面板侧只在「读不到」时给「重试」入口,重试才强制重读。
|
|
10130
|
+
let localFontsCache = null
|
|
10131
|
+
function collectFontCandidates() {
|
|
10132
|
+
const scope = typeof window === 'undefined' ? null : window
|
|
10133
|
+
return collectLocalFonts(scope).then(function (res) {
|
|
10134
|
+
const candidates = buildFontCandidates(res.ok ? res.fonts : [], isFontAvailable, res.mono, Object.keys(FONTS), BUNDLED_FONTS)
|
|
10135
|
+
localFontsCache = {
|
|
10136
|
+
candidates: candidates,
|
|
10137
|
+
count: res.ok ? res.fonts.length : 0,
|
|
10138
|
+
reason: res.reason || null,
|
|
10139
|
+
ok: !!res.ok,
|
|
8594
10140
|
}
|
|
8595
|
-
|
|
8596
|
-
|
|
10141
|
+
return localFontsCache
|
|
10142
|
+
})
|
|
8597
10143
|
}
|
|
8598
10144
|
|
|
8599
10145
|
// 宿主明暗信号:DSH 深色为 body[data-ds-dark-theme],缺席即浅色(与 engine/generate.mjs 的 CSS 约定一致)
|
|
@@ -8627,6 +10173,25 @@ function createClient(slotTarget) {
|
|
|
8627
10173
|
let state = loadState()
|
|
8628
10174
|
let tokenDispose = null
|
|
8629
10175
|
let styleTag = null
|
|
10176
|
+
// ── 宿主桥 + 日志骨架 + 更新控制器(宿主不可用时整块降级,不影响主题面板)──
|
|
10177
|
+
const hostBridge = createHostBridge(ctx)
|
|
10178
|
+
const clientLog = createClientLog(
|
|
10179
|
+
{
|
|
10180
|
+
host: hostBridge,
|
|
10181
|
+
timer: { timeout: function (fn, ms) { return setTimeout(fn, ms) } },
|
|
10182
|
+
storage: (globalThis.localStorage ? globalThis.localStorage : null),
|
|
10183
|
+
},
|
|
10184
|
+
{ pluginId: PLUGIN_ID, prefix: PHONE_PREFIX }
|
|
10185
|
+
)
|
|
10186
|
+
const update = createUpdateController({
|
|
10187
|
+
call: hostBridge ? function (phone, args) { return hostBridge.call(phone, args) } : null,
|
|
10188
|
+
phones: buildClientPhoneNames(PHONE_PREFIX),
|
|
10189
|
+
pollMs: CLIENT_POLL.defaultMs,
|
|
10190
|
+
log: function (level, event, fields) { try { clientLog.log(level, event, fields) } catch (e) { /* 忽略 */ } },
|
|
10191
|
+
})
|
|
10192
|
+
// 启动时向宿主对账调试开关(以宿主为准);宿主不可用时静默,不抛错
|
|
10193
|
+
try { clientLog.reconcileLogSwitch() } catch (e) { /* 忽略 */ }
|
|
10194
|
+
try { clientLog.log('info', 'host.call', { method: 'boot', latencyMs: 0, ok: true, kind: 'boot', pluginId: PLUGIN_ID }) } catch (e) { /* 忽略 */ }
|
|
8630
10195
|
|
|
8631
10196
|
// ── i18n 运行时:语言跟随 DSH(官方 locale 服务为信号源,html[lang] 仅作回退)──
|
|
8632
10197
|
const localeSvc = ctx.get('locale') || ctx.locale || null
|
|
@@ -8640,6 +10205,14 @@ function createClient(slotTarget) {
|
|
|
8640
10205
|
const entry = I18N[key]
|
|
8641
10206
|
return entry ? (entry[currentLang] !== undefined ? entry[currentLang] : key) : key
|
|
8642
10207
|
}
|
|
10208
|
+
// 带占位符的文案:trf('updateToVersion', { v: '1.8.0' })
|
|
10209
|
+
function trf(key, vars) {
|
|
10210
|
+
let text = tr(key)
|
|
10211
|
+
for (const name in vars) {
|
|
10212
|
+
if (Object.prototype.hasOwnProperty.call(vars, name)) text = text.split('{' + name + '}').join(String(vars[name]))
|
|
10213
|
+
}
|
|
10214
|
+
return text
|
|
10215
|
+
}
|
|
8643
10216
|
function notifyLocale() {
|
|
8644
10217
|
const next = currentLocale()
|
|
8645
10218
|
if (next === currentLang) return
|
|
@@ -8741,6 +10314,12 @@ function createClient(slotTarget) {
|
|
|
8741
10314
|
const h = react.createElement
|
|
8742
10315
|
const [query, setQuery] = react.useState('')
|
|
8743
10316
|
const [fontOpen, setFontOpen] = react.useState(false)
|
|
10317
|
+
// 字体下拉:本机清单(枚举缓存)+ 搜索词 + 进行中标志
|
|
10318
|
+
const [fontList, setFontList] = react.useState(localFontsCache)
|
|
10319
|
+
const [fontQuery, setFontQuery] = react.useState('')
|
|
10320
|
+
const [fontBusy, setFontBusy] = react.useState(false)
|
|
10321
|
+
// 一次会话只自动读一次本机清单;「重试」显式复位后才允许再读
|
|
10322
|
+
const fontTriedRef = react.useRef(localFontsCache !== null)
|
|
8744
10323
|
const [sizeOpen, setSizeOpen] = react.useState(false)
|
|
8745
10324
|
const fontRef = react.useRef(null)
|
|
8746
10325
|
const sizeRef = react.useRef(null)
|
|
@@ -8761,6 +10340,22 @@ function createClient(slotTarget) {
|
|
|
8761
10340
|
return function () { document.removeEventListener('mousedown', onDoc) }
|
|
8762
10341
|
}, [fontOpen, sizeOpen])
|
|
8763
10342
|
|
|
10343
|
+
// 本机字体清单:**只有用户真的点开下拉才读**(queryLocalFonts 要用户手势 + 可能弹授权),
|
|
10344
|
+
// 打开面板时不读。读不到也不影响控件:候选恒含预设兜底。
|
|
10345
|
+
react.useEffect(function () {
|
|
10346
|
+
if (!fontOpen) return
|
|
10347
|
+
if (fontList !== null || fontBusy || fontTriedRef.current) return
|
|
10348
|
+
fontTriedRef.current = true
|
|
10349
|
+
setFontBusy(true)
|
|
10350
|
+
collectFontCandidates().then(function (res) {
|
|
10351
|
+
setFontList(res)
|
|
10352
|
+
setFontBusy(false)
|
|
10353
|
+
}, function () {
|
|
10354
|
+
setFontList({ candidates: buildFontCandidates([], isFontAvailable, null, Object.keys(FONTS), BUNDLED_FONTS), count: 0, reason: 'failed', ok: false })
|
|
10355
|
+
setFontBusy(false)
|
|
10356
|
+
})
|
|
10357
|
+
}, [fontOpen, fontList, fontBusy])
|
|
10358
|
+
|
|
8764
10359
|
// 语言切换:DSH 界面语言变化时重渲染(文案跟随)
|
|
8765
10360
|
react.useEffect(function () {
|
|
8766
10361
|
return props.subscribeLocale(function () { setUi(props.getState()) })
|
|
@@ -8829,7 +10424,7 @@ function createClient(slotTarget) {
|
|
|
8829
10424
|
}, opt.label)
|
|
8830
10425
|
}))
|
|
8831
10426
|
}
|
|
8832
|
-
//
|
|
10427
|
+
// 通用下拉(字号):紧凑按钮 + 弹出菜单,对齐 setup-panel 样例
|
|
8833
10428
|
const dd = function (open, setOpen, ref, labelNode, items) {
|
|
8834
10429
|
return h('div', { ref: ref, style: { position: 'relative' } }, [
|
|
8835
10430
|
h('button', {
|
|
@@ -8854,6 +10449,122 @@ function createClient(slotTarget) {
|
|
|
8854
10449
|
return h('span', { style: { width: size, height: size, borderRadius: '50%', background: color || dotFallback, display: 'inline-block', flex: 'none' } })
|
|
8855
10450
|
}
|
|
8856
10451
|
|
|
10452
|
+
// ── 代码字体下拉:常用预设(置顶)+ 本机字体(等宽置顶,可搜索)──
|
|
10453
|
+
// 候选缺省只有预设:本机清单读不到时控件照常可用(绝不出现空下拉)。
|
|
10454
|
+
const fallbackCandidates = buildFontCandidates([], isFontAvailable, null, Object.keys(FONTS), BUNDLED_FONTS)
|
|
10455
|
+
const candidates = fontList && fontList.candidates ? fontList.candidates : fallbackCandidates
|
|
10456
|
+
|
|
10457
|
+
// 读不到清单的原因 → 人话提示(区分「没授权」与「没读到」,别都说成没字体)
|
|
10458
|
+
const fontHint = function () {
|
|
10459
|
+
if (fontBusy) return { text: tr('fontCounting'), retry: false }
|
|
10460
|
+
if (!fontList) return null
|
|
10461
|
+
if (fontList.ok) return { text: trf('fontLoadedCount', { n: fontList.count }), retry: false }
|
|
10462
|
+
const key = fontList.reason === 'denied' ? 'fontScanDenied'
|
|
10463
|
+
: fontList.reason === 'unavailable' ? 'fontScanUnsupported'
|
|
10464
|
+
: fontList.reason === 'empty' ? 'fontScanEmpty' : 'fontScanFail'
|
|
10465
|
+
return { text: tr(key), retry: true }
|
|
10466
|
+
}
|
|
10467
|
+
const retryFonts = function () {
|
|
10468
|
+
fontTriedRef.current = false
|
|
10469
|
+
localFontsCache = null
|
|
10470
|
+
setFontList(null)
|
|
10471
|
+
}
|
|
10472
|
+
// 候选行:族名按自己的字体渲染(预览即所得);未装灰显标注,确实装在本机的标「已装」/「等宽」
|
|
10473
|
+
const fontItem = function (k) {
|
|
10474
|
+
const on = k.key === st.fontKey
|
|
10475
|
+
const suffix = k.ok && k.installed ? '(' + (k.mono ? tr('fontMono') : tr('fontLocal')) + ')' : (k.ok ? '' : '(' + tr('fontNotInstalled') + ')')
|
|
10476
|
+
return h('div', {
|
|
10477
|
+
key: k.key,
|
|
10478
|
+
onClick: function () {
|
|
10479
|
+
props.refresh(st.mode, st.size, k.key)
|
|
10480
|
+
setUi(props.getState())
|
|
10481
|
+
setFontOpen(false)
|
|
10482
|
+
},
|
|
10483
|
+
title: k.ok ? (k.installed ? tr('fontLocal') : '') : tr('fontNotInstalled'),
|
|
10484
|
+
style: {
|
|
10485
|
+
padding: '6px 10px', fontSize: 12, borderRadius: 5, cursor: 'pointer',
|
|
10486
|
+
background: on ? ddItemOnBg : 'transparent',
|
|
10487
|
+
color: on ? base : muted,
|
|
10488
|
+
opacity: k.ok ? 1 : 0.45,
|
|
10489
|
+
},
|
|
10490
|
+
}, h('span', { style: { fontFamily: k.stack || codeFontStack(k.key), whiteSpace: 'nowrap' } }, k.key + suffix))
|
|
10491
|
+
}
|
|
10492
|
+
const hint = fontHint()
|
|
10493
|
+
const hintNode = hint
|
|
10494
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px 7px', fontSize: 11, color: muted, borderBottom: '1px solid var(--dsw-alias-border-l1)' } }, [
|
|
10495
|
+
h('span', { style: { flex: '1 1 auto' } }, hint.text),
|
|
10496
|
+
hint.retry ? h('span', {
|
|
10497
|
+
onClick: function () { retryFonts() },
|
|
10498
|
+
style: { cursor: 'pointer', color: 'var(--dsw-alias-brand-primary)', flex: 'none' },
|
|
10499
|
+
}, tr('fontRetry')) : null,
|
|
10500
|
+
])
|
|
10501
|
+
: null
|
|
10502
|
+
const menuItem = function (key, label, onClick) {
|
|
10503
|
+
return h('div', {
|
|
10504
|
+
key: key,
|
|
10505
|
+
onClick: onClick,
|
|
10506
|
+
style: { padding: '6px 10px', fontSize: 12, borderRadius: 5, cursor: 'pointer', color: base },
|
|
10507
|
+
}, label)
|
|
10508
|
+
}
|
|
10509
|
+
const fontMenu = function () {
|
|
10510
|
+
const q = fontQuery.trim().toLowerCase()
|
|
10511
|
+
const match = function (k) { return q === '' || k.key.toLowerCase().indexOf(q) >= 0 }
|
|
10512
|
+
const presets = candidates.filter(function (k) { return k.isPreset && match(k) })
|
|
10513
|
+
// 枚举到的本机字体已按「等宽置顶 + 字母序」排好,这里只做过滤与截断(大清单别一次铺满 DOM)
|
|
10514
|
+
const all = candidates.filter(function (k) { return !k.isPreset && match(k) })
|
|
10515
|
+
const locals = all.slice(0, 400)
|
|
10516
|
+
const rows = []
|
|
10517
|
+
if (presets.length > 0) {
|
|
10518
|
+
rows.push(menuItem('sec-presets', tr('fontPresets'), function () {}))
|
|
10519
|
+
for (const k of presets) rows.push(fontItem(k))
|
|
10520
|
+
}
|
|
10521
|
+
if (locals.length > 0) {
|
|
10522
|
+
rows.push(menuItem('sec-locals', tr('fontLocals') + ' · ' + all.length, function () {}))
|
|
10523
|
+
for (const k of locals) rows.push(fontItem(k))
|
|
10524
|
+
}
|
|
10525
|
+
if (rows.length === 0) rows.push(menuItem('no-match', tr('fontNoMatch'), function () {}))
|
|
10526
|
+
return h('div', null, [
|
|
10527
|
+
h('input', {
|
|
10528
|
+
value: fontQuery,
|
|
10529
|
+
onChange: function (e) { setFontQuery(e.target.value) },
|
|
10530
|
+
onClick: function (e) { if (e && typeof e.stopPropagation === 'function') e.stopPropagation() },
|
|
10531
|
+
placeholder: tr('fontSearch'),
|
|
10532
|
+
style: {
|
|
10533
|
+
width: '100%', boxSizing: 'border-box', background: 'var(--dsw-alias-bg-layer-2)',
|
|
10534
|
+
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 6, padding: '6px 10px',
|
|
10535
|
+
fontSize: 12, outline: 'none', color: base, fontFamily: 'var(--dsw-font-family)', marginBottom: 4,
|
|
10536
|
+
},
|
|
10537
|
+
}),
|
|
10538
|
+
hintNode,
|
|
10539
|
+
h('div', { style: { maxHeight: 280, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 1 } }, rows),
|
|
10540
|
+
])
|
|
10541
|
+
}
|
|
10542
|
+
const fontPicker = function () {
|
|
10543
|
+
return h('div', { ref: fontRef, style: { position: 'relative' } }, [
|
|
10544
|
+
h('button', {
|
|
10545
|
+
// 手势必须在同步段里:queryLocalFonts 只能由真实用户激活触发,异步等待之后就丢了
|
|
10546
|
+
onClick: function () { setFontOpen(!fontOpen); setFontQuery('') },
|
|
10547
|
+
style: {
|
|
10548
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
10549
|
+
background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)',
|
|
10550
|
+
borderRadius: 6, padding: '5px 10px', fontSize: 12, cursor: 'pointer',
|
|
10551
|
+
color: base, fontFamily: 'var(--dsw-font-family)',
|
|
10552
|
+
},
|
|
10553
|
+
}, [
|
|
10554
|
+
h('span', { style: { fontFamily: codeFontStack(st.fontKey) } }, st.fontKey),
|
|
10555
|
+
h('span', { style: { color: muted } }, '▾'),
|
|
10556
|
+
]),
|
|
10557
|
+
fontOpen ? h('div', {
|
|
10558
|
+
key: 'font-menu',
|
|
10559
|
+
style: {
|
|
10560
|
+
position: 'absolute', top: 'calc(100% + 4px)', left: 0, zIndex: 20,
|
|
10561
|
+
background: 'var(--dsw-alias-bg-overlay)', border: '1px solid var(--dsw-alias-border-l1)',
|
|
10562
|
+
borderRadius: 8, minWidth: 240, width: 'max-content', maxWidth: 'calc(100vw - 48px)', padding: 4, boxShadow: menuShadow,
|
|
10563
|
+
},
|
|
10564
|
+
}, [fontMenu()]) : null,
|
|
10565
|
+
])
|
|
10566
|
+
}
|
|
10567
|
+
|
|
8857
10568
|
// 主题 mini 芯片(组合 1)
|
|
8858
10569
|
const chip = function (t) {
|
|
8859
10570
|
const isCur = t.name === st.theme
|
|
@@ -8879,18 +10590,194 @@ function createClient(slotTarget) {
|
|
|
8879
10590
|
|
|
8880
10591
|
|
|
8881
10592
|
|
|
10593
|
+
// ── 作者其他插件(底部引流位)──
|
|
10594
|
+
// 只列兄弟插件、不自我推荐(本面板自己的星标链接就在标题行);
|
|
10595
|
+
// 静态外链列表,不做「已安装」检测(面板侧无稳定安装信号)。
|
|
10596
|
+
const authorPlugins = [
|
|
10597
|
+
{ repo: 'dsh-mattpocock-skills-deck', descKey: 'authorPlugin.skillsDeck' },
|
|
10598
|
+
{ repo: 'dsh-prompt', descKey: 'authorPlugin.prompt' },
|
|
10599
|
+
{ repo: 'dsh-im-companion', descKey: 'authorPlugin.imCompanion' },
|
|
10600
|
+
]
|
|
10601
|
+
// 外链小图标(方框 + 右上斜箭头,与 MattSkills 同款语义)
|
|
10602
|
+
const extLinkIcon = function () {
|
|
10603
|
+
return h('svg', {
|
|
10604
|
+
width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none',
|
|
10605
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
10606
|
+
style: { display: 'block' },
|
|
10607
|
+
}, [
|
|
10608
|
+
h('path', { d: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' }),
|
|
10609
|
+
h('polyline', { points: '15 3 21 3 21 9' }),
|
|
10610
|
+
h('line', { x1: 10, y1: 14, x2: 21, y2: 3 }),
|
|
10611
|
+
])
|
|
10612
|
+
}
|
|
10613
|
+
// 四宫格小图标(卡片标题前)
|
|
10614
|
+
const gridIcon = function () {
|
|
10615
|
+
return h('svg', {
|
|
10616
|
+
width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none',
|
|
10617
|
+
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
10618
|
+
style: { display: 'block' },
|
|
10619
|
+
}, [
|
|
10620
|
+
h('rect', { key: 'tl', x: 3, y: 3, width: 7, height: 7, rx: 1 }),
|
|
10621
|
+
h('rect', { key: 'tr', x: 14, y: 3, width: 7, height: 7, rx: 1 }),
|
|
10622
|
+
h('rect', { key: 'bl', x: 3, y: 14, width: 7, height: 7, rx: 1 }),
|
|
10623
|
+
h('rect', { key: 'br', x: 14, y: 14, width: 7, height: 7, rx: 1 }),
|
|
10624
|
+
])
|
|
10625
|
+
}
|
|
10626
|
+
// 底部引流卡片:3 行兄弟插件,整行可点(锚点),新窗口打开
|
|
10627
|
+
const authorCard = h('div', {
|
|
10628
|
+
style: {
|
|
10629
|
+
marginTop: 4,
|
|
10630
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
10631
|
+
borderRadius: 10, padding: '12px 14px',
|
|
10632
|
+
display: 'flex', flexDirection: 'column', gap: 4,
|
|
10633
|
+
},
|
|
10634
|
+
}, [
|
|
10635
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 7, color: base, fontSize: 13, fontWeight: 600, marginBottom: 6 } }, [
|
|
10636
|
+
h('span', { key: 'ico', style: { color: muted, display: 'inline-flex' } }, gridIcon()),
|
|
10637
|
+
tr('authorPlugins'),
|
|
10638
|
+
]),
|
|
10639
|
+
...authorPlugins.map(function (p) {
|
|
10640
|
+
return h('a', {
|
|
10641
|
+
key: p.repo,
|
|
10642
|
+
href: 'https://github.com/FeatherHunter/' + p.repo,
|
|
10643
|
+
target: '_blank', rel: 'noopener noreferrer',
|
|
10644
|
+
title: tr('authorPluginOpen') + ':' + p.repo,
|
|
10645
|
+
style: {
|
|
10646
|
+
display: 'flex', alignItems: 'baseline', gap: 10,
|
|
10647
|
+
padding: '4px 6px', margin: '0 -6px', borderRadius: 6,
|
|
10648
|
+
textDecoration: 'none', cursor: 'pointer',
|
|
10649
|
+
background: 'transparent',
|
|
10650
|
+
},
|
|
10651
|
+
}, [
|
|
10652
|
+
h('span', { key: 'name', style: { fontFamily: 'var(--ds-font-family-code)', fontSize: 12, fontWeight: 700, color: base, whiteSpace: 'nowrap' } }, p.repo),
|
|
10653
|
+
h('span', { key: 'desc', style: { fontSize: 12, color: muted, flex: 1 } }, tr(p.descKey)),
|
|
10654
|
+
h('span', { key: 'ext', style: { color: 'var(--dsw-alias-label-tertiary)', display: 'inline-flex', flex: 'none' } }, extLinkIcon()),
|
|
10655
|
+
])
|
|
10656
|
+
}),
|
|
10657
|
+
])
|
|
10658
|
+
|
|
10659
|
+
// ── 检查更新:订阅控制器(宿主不可用时整块不渲染)──
|
|
10660
|
+
const [upd, setUpd] = react.useState(props.update ? props.update.getState() : null)
|
|
10661
|
+
const [copied, setCopied] = react.useState(false)
|
|
10662
|
+
react.useEffect(function () {
|
|
10663
|
+
if (!props.update) return undefined
|
|
10664
|
+
setUpd(props.update.getState())
|
|
10665
|
+
const unsub = props.update.subscribe(function () { setUpd(props.update.getState()) })
|
|
10666
|
+
// 打开面板静默读一次本地状态:只读、不联网(联网只在用户点按钮时发生)
|
|
10667
|
+
props.update.readStatus()
|
|
10668
|
+
return unsub
|
|
10669
|
+
}, [])
|
|
10670
|
+
const updState = upd
|
|
10671
|
+
const updFailKey = function (s) {
|
|
10672
|
+
const message = String((s && s.jobMessage) || '')
|
|
10673
|
+
if (message === 'installation-changed') return 'updateFailChanged'
|
|
10674
|
+
if (message === 'recovery-required') return 'updateFailRecovery'
|
|
10675
|
+
return 'updateFailInstall'
|
|
10676
|
+
}
|
|
10677
|
+
const updLabel = function (s) {
|
|
10678
|
+
const which = buttonState(s)
|
|
10679
|
+
if (which === 'installing') return tr('updateInstalling')
|
|
10680
|
+
if (which === 'checking') return tr('updateChecking')
|
|
10681
|
+
if (which === 'pending') return tr('updateRestart')
|
|
10682
|
+
if (which === 'hasNew') return trf('updateToVersion', { v: s.latest })
|
|
10683
|
+
return tr('updateCheck')
|
|
10684
|
+
}
|
|
10685
|
+
const copyManual = function () {
|
|
10686
|
+
try {
|
|
10687
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) navigator.clipboard.writeText(String(updState.manual))
|
|
10688
|
+
setCopied(true)
|
|
10689
|
+
} catch (e) { /* 复制不可用 */ }
|
|
10690
|
+
}
|
|
10691
|
+
const updateButton = (updState && updState.available)
|
|
10692
|
+
? h('button', {
|
|
10693
|
+
key: 'upd',
|
|
10694
|
+
onClick: function () { props.update.check() },
|
|
10695
|
+
disabled: !!(updState.checking || updState.installing),
|
|
10696
|
+
title: tr('updateCheck'),
|
|
10697
|
+
style: {
|
|
10698
|
+
border: '1px solid ' + (updState.hasNew ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)'),
|
|
10699
|
+
borderRadius: 6, padding: '3px 9px', fontSize: 11, cursor: 'pointer',
|
|
10700
|
+
background: updState.hasNew ? segOnBg : 'transparent',
|
|
10701
|
+
color: updState.hasNew ? 'var(--dsw-alias-brand-primary)' : muted,
|
|
10702
|
+
fontFamily: 'var(--dsw-font-family)', whiteSpace: 'nowrap',
|
|
10703
|
+
},
|
|
10704
|
+
}, updLabel(updState))
|
|
10705
|
+
: null
|
|
10706
|
+
const restartBanner = (updState && updState.pending)
|
|
10707
|
+
? h('div', {
|
|
10708
|
+
key: 'restart',
|
|
10709
|
+
style: {
|
|
10710
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
10711
|
+
border: '1px solid var(--dsw-alias-state-warn-primary)',
|
|
10712
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
10713
|
+
borderRadius: 8, padding: '8px 12px', fontSize: 12,
|
|
10714
|
+
color: 'var(--dsw-alias-state-warn-primary)',
|
|
10715
|
+
},
|
|
10716
|
+
}, trf('updateToastRestart', { v: updState.installed || '' }))
|
|
10717
|
+
: null
|
|
10718
|
+
const updateDialog = (updState && updState.available && updState.dialogOpen)
|
|
10719
|
+
? h('div', {
|
|
10720
|
+
key: 'updDlg',
|
|
10721
|
+
onClick: function (e) { if (e.target === e.currentTarget) props.update.closeDialog() },
|
|
10722
|
+
style: { position: 'fixed', left: 0, top: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.45)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' },
|
|
10723
|
+
}, h('div', {
|
|
10724
|
+
style: {
|
|
10725
|
+
width: 460, maxWidth: 'calc(100vw - 32px)', boxSizing: 'border-box',
|
|
10726
|
+
background: 'var(--dsw-alias-bg-overlay)', border: '1px solid var(--dsw-alias-border-l1)',
|
|
10727
|
+
borderRadius: 12, padding: 18, boxShadow: menuShadow,
|
|
10728
|
+
display: 'flex', flexDirection: 'column', gap: 10,
|
|
10729
|
+
},
|
|
10730
|
+
}, [
|
|
10731
|
+
h('div', { key: 'ttl', style: { fontSize: 14, fontWeight: 600, color: base } }, trf('updateDialogTitle', { v: updState.latest || '' })),
|
|
10732
|
+
h('div', { key: 'ver', style: { fontSize: 12, color: muted } }, trf('updateVersions', { running: updState.running || PALETTE_VERSION, latest: updState.latest || '' })),
|
|
10733
|
+
h('div', { key: 'note', style: { fontSize: 12, color: muted } }, tr('updateRestartNote')),
|
|
10734
|
+
updState.failure ? h('div', { key: 'fail', style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, tr(updFailKey(updState))) : null,
|
|
10735
|
+
(updState.blocked && !updState.canInstall) ? h('div', { key: 'blocked', style: { fontSize: 12, color: muted } }, tr(blockedReasonKey(updState.blocked))) : null,
|
|
10736
|
+
updState.manual ? h('div', { key: 'manual', style: { display: 'flex', flexDirection: 'column', gap: 6 } }, [
|
|
10737
|
+
h('div', { key: 'mh', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, fontSize: 12, color: base } }, [
|
|
10738
|
+
h('span', { key: 't' }, tr('updateManualTitle')),
|
|
10739
|
+
h('button', {
|
|
10740
|
+
key: 'c',
|
|
10741
|
+
onClick: copyManual,
|
|
10742
|
+
style: { border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', background: 'transparent', color: muted, fontFamily: 'var(--dsw-font-family)' },
|
|
10743
|
+
}, tr(copied ? 'updateCopied' : 'updateCopy')),
|
|
10744
|
+
]),
|
|
10745
|
+
h('pre', { key: 'p', style: { margin: 0, padding: '8px 10px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 6, fontSize: 11, overflowX: 'auto', color: base, fontFamily: 'var(--ds-font-family-code)' } }, String(updState.manual)),
|
|
10746
|
+
h('div', { key: 'mn', style: { fontSize: 11, color: muted } }, tr('updateManualNote')),
|
|
10747
|
+
]) : null,
|
|
10748
|
+
h('div', { key: 'acts', style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 2 } }, [
|
|
10749
|
+
updState.canInstall && updState.checkId
|
|
10750
|
+
? h('button', {
|
|
10751
|
+
key: 'go',
|
|
10752
|
+
onClick: function () { props.update.install() },
|
|
10753
|
+
disabled: !!updState.installing,
|
|
10754
|
+
style: { border: '1px solid var(--dsw-alias-brand-primary)', borderRadius: 6, padding: '5px 14px', fontSize: 12, cursor: 'pointer', background: segOnBg, color: 'var(--dsw-alias-brand-primary)', fontFamily: 'var(--dsw-font-family)' },
|
|
10755
|
+
}, tr('updateStart'))
|
|
10756
|
+
: null,
|
|
10757
|
+
h('button', {
|
|
10758
|
+
key: 'later',
|
|
10759
|
+
onClick: function () { props.update.closeDialog() },
|
|
10760
|
+
style: { border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 6, padding: '5px 14px', fontSize: 12, cursor: 'pointer', background: 'transparent', color: muted, fontFamily: 'var(--dsw-font-family)' },
|
|
10761
|
+
}, tr('updateLater')),
|
|
10762
|
+
]),
|
|
10763
|
+
]))
|
|
10764
|
+
: null
|
|
10765
|
+
|
|
8882
10766
|
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 920 } }, [
|
|
10767
|
+
restartBanner,
|
|
8883
10768
|
// 头行:标题 + 状态开关(一个状态一个控制)
|
|
8884
10769
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } }, [
|
|
8885
10770
|
h('strong', null, '🎨 ' + tr('panelName')),
|
|
8886
10771
|
h('div', { style: { display: 'inline-flex', alignItems: 'center', gap: 8 } }, [
|
|
8887
10772
|
h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)' } }, 'v' + PALETTE_VERSION),
|
|
10773
|
+
updateButton,
|
|
8888
10774
|
h('a', {
|
|
8889
10775
|
href: 'https://github.com/FeatherHunter/dsh-opencode-palette',
|
|
8890
10776
|
target: '_blank', rel: 'noopener noreferrer',
|
|
8891
|
-
title: '你的 ⭐是我夜空中最亮的星',
|
|
10777
|
+
title: '你的 ⭐是我夜空中最亮的星 🌹',
|
|
8892
10778
|
style: { color: muted, display: 'inline-flex', cursor: 'pointer', fontSize: 15, lineHeight: 1, textDecoration: 'none' },
|
|
8893
10779
|
}, '🌟'),
|
|
10780
|
+
// ISSUE 入口:消息气泡形态(信息图标认不出「提需求」,气泡才读得出是反馈)
|
|
8894
10781
|
h('a', {
|
|
8895
10782
|
href: 'https://github.com/FeatherHunter/dsh-opencode-palette/issues',
|
|
8896
10783
|
target: '_blank', rel: 'noopener noreferrer',
|
|
@@ -8900,11 +10787,9 @@ function createClient(slotTarget) {
|
|
|
8900
10787
|
width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none',
|
|
8901
10788
|
stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
8902
10789
|
style: { display: 'block' },
|
|
8903
|
-
},
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
h('line', { x1: 12, y1: 16, x2: 12.01, y2: 16 }),
|
|
8907
|
-
])),
|
|
10790
|
+
}, h('path', {
|
|
10791
|
+
d: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z',
|
|
10792
|
+
}))),
|
|
8908
10793
|
h('span', { style: { color: st.enabled ? 'var(--dsw-alias-state-success-primary)' : muted, fontSize: 12 } },
|
|
8909
10794
|
st.enabled ? tr('enabled') : tr('disabled')),
|
|
8910
10795
|
h('span', {
|
|
@@ -8951,29 +10836,7 @@ function createClient(slotTarget) {
|
|
|
8951
10836
|
},
|
|
8952
10837
|
}, String(s) + 'px')
|
|
8953
10838
|
})),
|
|
8954
|
-
|
|
8955
|
-
h('span', { style: { fontFamily: FONTS[st.fontKey] || FONTS['JetBrains Mono'] } }, st.fontKey),
|
|
8956
|
-
Object.keys(FONTS).map(function (k) {
|
|
8957
|
-
const on = k === st.fontKey
|
|
8958
|
-
const ok = isFontAvailable(k)
|
|
8959
|
-
const bundled = BUNDLED_FONTS && BUNDLED_FONTS.indexOf(k) >= 0
|
|
8960
|
-
const suffix = bundled ? '' : '(' + (ok ? tr('fontLocal') : tr('fontNotInstalled')) + ')'
|
|
8961
|
-
return h('div', {
|
|
8962
|
-
key: k,
|
|
8963
|
-
onClick: function () {
|
|
8964
|
-
props.refresh(st.mode, st.size, k)
|
|
8965
|
-
setUi(props.getState())
|
|
8966
|
-
setFontOpen(false)
|
|
8967
|
-
},
|
|
8968
|
-
title: ok ? '' : tr('fontNotInstalled'),
|
|
8969
|
-
style: {
|
|
8970
|
-
padding: '6px 10px', fontSize: 12, borderRadius: 5, cursor: 'pointer',
|
|
8971
|
-
background: on ? ddItemOnBg : 'transparent',
|
|
8972
|
-
color: on ? base : muted,
|
|
8973
|
-
opacity: ok ? 1 : 0.45,
|
|
8974
|
-
},
|
|
8975
|
-
}, h('span', { style: { fontFamily: FONTS[k], whiteSpace: 'nowrap' } }, k + suffix))
|
|
8976
|
-
})),
|
|
10839
|
+
fontPicker(),
|
|
8977
10840
|
]),
|
|
8978
10841
|
// ── 主题选择(色系分组标签 + mini 芯片)──
|
|
8979
10842
|
h('div', { style: secTitle }, [
|
|
@@ -9003,6 +10866,9 @@ function createClient(slotTarget) {
|
|
|
9003
10866
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } }, g.themes.map(chip)),
|
|
9004
10867
|
])
|
|
9005
10868
|
}),
|
|
10869
|
+
// ── 底部:作者其他插件(引流位)──
|
|
10870
|
+
authorCard,
|
|
10871
|
+
updateDialog,
|
|
9006
10872
|
])
|
|
9007
10873
|
}
|
|
9008
10874
|
// 面板 API(settings.plugins.tab 与 settings.section 两个入口共享同一份 state)
|
|
@@ -9013,6 +10879,13 @@ function createClient(slotTarget) {
|
|
|
9013
10879
|
refresh: refresh,
|
|
9014
10880
|
setTheme: setTheme,
|
|
9015
10881
|
themeNames: themeNames,
|
|
10882
|
+
// 检查更新:控制器 + 日志器交给面板(宿主不可用时 update.available 为假,按钮不渲染)
|
|
10883
|
+
update: update,
|
|
10884
|
+
log: clientLog,
|
|
10885
|
+
// 本机字体清单:面板打开下拉时调(要用户手势),未打开下拉不读;失败恒回退预设
|
|
10886
|
+
collectFonts: collectFontCandidates,
|
|
10887
|
+
// 面板文案(测试/调试用,走同一份双语表)
|
|
10888
|
+
text: function (key, vars) { return vars ? trf(key, vars) : tr(key) },
|
|
9016
10889
|
groups: function () { return themeGroups() },
|
|
9017
10890
|
subscribeLocale: function (fn) {
|
|
9018
10891
|
localeListeners.push(fn)
|
|
@@ -9060,6 +10933,8 @@ function createClient(slotTarget) {
|
|
|
9060
10933
|
try { if (dictDispose) dictDispose() } catch (e) { /* 忽略 */ }
|
|
9061
10934
|
try { if (localeUnsub) localeUnsub() } catch (e) { /* 忽略 */ }
|
|
9062
10935
|
try { if (localeObserver) localeObserver.disconnect() } catch (e) { /* 忽略 */ }
|
|
10936
|
+
try { update.dispose() } catch (e) { /* 忽略 */ }
|
|
10937
|
+
try { clientLog.flush() } catch (e) { /* 忽略 */ }
|
|
9063
10938
|
}
|
|
9064
10939
|
}, 'dsh-opencode-palette: styles')
|
|
9065
10940
|
}
|
|
@@ -9067,7 +10942,7 @@ function createClient(slotTarget) {
|
|
|
9067
10942
|
|
|
9068
10943
|
__mods["client"] = { createClient }
|
|
9069
10944
|
})();
|
|
9070
|
-
exports.inject = ["theme","slots","locale"]
|
|
10945
|
+
exports.inject = ["theme","slots","locale","connection"]
|
|
9071
10946
|
exports.apply = __mods["client"].createClient("settings.plugins.tab")
|
|
9072
10947
|
return module.exports
|
|
9073
10948
|
},
|