dsh-background-by-model 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -1
- package/CHANGELOG.zh.md +56 -1
- package/README.md +30 -3
- package/README.zh.md +29 -2
- package/cordis.patch.yml +2 -2
- package/lib/client.js +373 -57
- package/lib/index.js +8018 -159
- package/package.json +7 -3
package/lib/client.js
CHANGED
|
@@ -474,7 +474,9 @@ window.__ModuleLoader__.load({
|
|
|
474
474
|
holidays: {
|
|
475
475
|
enabled: false,
|
|
476
476
|
items: defaultHolidayRules()
|
|
477
|
-
}
|
|
477
|
+
},
|
|
478
|
+
agentAccess: false,
|
|
479
|
+
rev: 0
|
|
478
480
|
};
|
|
479
481
|
}
|
|
480
482
|
/** Slot names reach the filesystem, so they are strictly whitelisted. */
|
|
@@ -740,7 +742,9 @@ window.__ModuleLoader__.load({
|
|
|
740
742
|
rightbarOpacity: normalizeRightbarOpacity(r.rightbarOpacity),
|
|
741
743
|
pageOpacity: clamp01(r.pageOpacity, 0),
|
|
742
744
|
autoExtract: r.autoExtract !== false,
|
|
743
|
-
holidays: normalizeHolidays(r.holidays)
|
|
745
|
+
holidays: normalizeHolidays(r.holidays),
|
|
746
|
+
agentAccess: r.agentAccess === true,
|
|
747
|
+
rev: typeof r.rev === "number" && isFinite(r.rev) && r.rev >= 0 ? Math.floor(r.rev) : 0
|
|
744
748
|
};
|
|
745
749
|
}
|
|
746
750
|
freshThemeConfig();
|
|
@@ -1029,12 +1033,23 @@ window.__ModuleLoader__.load({
|
|
|
1029
1033
|
}
|
|
1030
1034
|
rotIndex = 0;
|
|
1031
1035
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Record the revision the host just wrote.
|
|
1038
|
+
*
|
|
1039
|
+
* Called after EVERY successful write, and it is load-bearing rather than
|
|
1040
|
+
* bookkeeping: the next write names this number as the one it started from, so
|
|
1041
|
+
* leaving it stale makes the very next save look like a lost race — the host
|
|
1042
|
+
* refuses it, hands back the config it already has (which does not contain the
|
|
1043
|
+
* edit), and the panel adopts that. That is the "click + twice" bug: the first
|
|
1044
|
+
* click is refused as stale and rolled back, the second only works once a stale
|
|
1045
|
+
* revision happens to match again.
|
|
1046
|
+
*
|
|
1047
|
+
* Mutated on the live object, never replaced: `cfg` is read through its binding
|
|
1048
|
+
* everywhere, and swapping the reference would strand the modules that hold it.
|
|
1049
|
+
*/
|
|
1050
|
+
function setRev(rev) {
|
|
1051
|
+
if (!isFinite(rev) || rev < 0) return;
|
|
1052
|
+
cfg.rev = Math.floor(rev);
|
|
1038
1053
|
}
|
|
1039
1054
|
//#endregion
|
|
1040
1055
|
//#region src/client/rpc.ts
|
|
@@ -1083,6 +1098,124 @@ window.__ModuleLoader__.load({
|
|
|
1083
1098
|
}
|
|
1084
1099
|
const SAVE_DEBOUNCE_MS = 250;
|
|
1085
1100
|
let saveTimer;
|
|
1101
|
+
/**
|
|
1102
|
+
* Called when a write came back as a conflict, so the plugin can adopt the config
|
|
1103
|
+
* that won and repaint. Set once by `apply()` in ./index.
|
|
1104
|
+
*
|
|
1105
|
+
* A callback rather than an import because `./rpc` owns no state: the memory copy,
|
|
1106
|
+
* the repaint and the toast all live in the modules that own those, and this file
|
|
1107
|
+
* would have to import half the plugin to reach them.
|
|
1108
|
+
*/
|
|
1109
|
+
let onConflict = null;
|
|
1110
|
+
function setConflictHandler(handler) {
|
|
1111
|
+
onConflict = handler;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* One config write, with the conflict case handled rather than logged.
|
|
1115
|
+
*
|
|
1116
|
+
* The generic `rpcCall` answers `undefined` for anything that is not a plain
|
|
1117
|
+
* success, which is right for reads but wrong here: a REFUSED write is not "no
|
|
1118
|
+
* answer", it is an answer that carries the config which won. So this one goes
|
|
1119
|
+
* through `rpcRaw` and unwraps the outcome itself.
|
|
1120
|
+
*/
|
|
1121
|
+
async function writeConfigNow(config) {
|
|
1122
|
+
const res = await rpcRaw("writeConfig", {
|
|
1123
|
+
config,
|
|
1124
|
+
expectedRev: cfg.rev
|
|
1125
|
+
});
|
|
1126
|
+
if (res === void 0) return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
conflict: false,
|
|
1129
|
+
rev: cfg.rev
|
|
1130
|
+
};
|
|
1131
|
+
if (res.ok !== true) return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
conflict: false,
|
|
1134
|
+
rev: cfg.rev
|
|
1135
|
+
};
|
|
1136
|
+
const value = res.value;
|
|
1137
|
+
if (value?.ok === true) return {
|
|
1138
|
+
ok: true,
|
|
1139
|
+
conflict: false,
|
|
1140
|
+
rev: typeof value.rev === "number" ? value.rev : cfg.rev
|
|
1141
|
+
};
|
|
1142
|
+
if (value?.conflict === true) return {
|
|
1143
|
+
ok: false,
|
|
1144
|
+
conflict: true,
|
|
1145
|
+
config: value.config,
|
|
1146
|
+
rev: typeof value.rev === "number" ? value.rev : cfg.rev
|
|
1147
|
+
};
|
|
1148
|
+
return {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
conflict: false,
|
|
1151
|
+
rev: cfg.rev
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Persist, and adopt the winner when the write was refused as stale.
|
|
1156
|
+
*
|
|
1157
|
+
* Deliberately NOT retried: the edit that lost was computed from a config that is
|
|
1158
|
+
* no longer there, and replaying it on the winner would apply a change the user
|
|
1159
|
+
* made against a different state. Adopting is the honest answer — the user sees
|
|
1160
|
+
* the new configuration and can repeat the gesture if they still want it.
|
|
1161
|
+
*
|
|
1162
|
+
* The SUCCESS path is the one that has to be careful: a write that lands advances
|
|
1163
|
+
* the revision on disk, so this bundle's own copy must advance with it, or its next
|
|
1164
|
+
* save names a revision that is already gone, gets refused, and adopts the config
|
|
1165
|
+
* from BEFORE that save — which is the edit visibly undoing itself.
|
|
1166
|
+
*/
|
|
1167
|
+
/**
|
|
1168
|
+
* ONE writer at a time, and this is load-bearing rather than tidy.
|
|
1169
|
+
*
|
|
1170
|
+
* Every save names the revision it started from, and a successful one ADVANCES that
|
|
1171
|
+
* revision. Two saves fired back to back (a fast double delete, a drag followed by a
|
|
1172
|
+
* click) therefore both name the SAME revision — the first is accepted, the second is
|
|
1173
|
+
* refused as stale, and the refusal hands back the config from before the second
|
|
1174
|
+
* edit. The panel adopts it, so the second edit disappears from the screen while its
|
|
1175
|
+
* side effects (a picture's bytes, already deleted) do not: that is the card reading
|
|
1176
|
+
* "这张图片还没有取到内容" next to a rule the user just deleted.
|
|
1177
|
+
*
|
|
1178
|
+
* Chaining the writes means the second one starts after the first has recorded its
|
|
1179
|
+
* revision, which is the only state it can be correct against. It is a single chain
|
|
1180
|
+
* for the whole config rather than one per key because the config IS one document —
|
|
1181
|
+
* two concurrent writes to it can never both be right.
|
|
1182
|
+
*/
|
|
1183
|
+
let writeChain = Promise.resolve();
|
|
1184
|
+
/**
|
|
1185
|
+
* Work that must only happen once the config naming it has actually landed.
|
|
1186
|
+
*
|
|
1187
|
+
* Releasing a picture's bytes is the case: the bytes are what the config points AT,
|
|
1188
|
+
* so deleting them first and then losing the config write leaves the rule on screen
|
|
1189
|
+
* with nothing behind it. Queued here instead, a failed or superseded write simply
|
|
1190
|
+
* never runs the delete — the file stays, which is wrong only in that it is
|
|
1191
|
+
* unreferenced, and the cleanup card exists for exactly that.
|
|
1192
|
+
*/
|
|
1193
|
+
let deferredDeletes = [];
|
|
1194
|
+
/**
|
|
1195
|
+
* Delete one slot's bytes, but only after the config that stopped referencing it is
|
|
1196
|
+
* on disk.
|
|
1197
|
+
*/
|
|
1198
|
+
function deleteImageAfterSave(slot) {
|
|
1199
|
+
deferredDeletes.push(slot);
|
|
1200
|
+
}
|
|
1201
|
+
function persistAndResolve() {
|
|
1202
|
+
writeChain = writeChain.then(async () => {
|
|
1203
|
+
const outcome = await writeConfigNow(cfg);
|
|
1204
|
+
if (outcome.ok) {
|
|
1205
|
+
setRev(outcome.rev);
|
|
1206
|
+
const releasing = deferredDeletes;
|
|
1207
|
+
deferredDeletes = [];
|
|
1208
|
+
for (const slot of releasing) deleteImage(slot);
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (outcome.conflict && outcome.config !== void 0) {
|
|
1212
|
+
deferredDeletes = [];
|
|
1213
|
+
onConflict?.(outcome.config);
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
return writeChain;
|
|
1217
|
+
}
|
|
1218
|
+
/** Write the current config with the revision this bundle last saw. */
|
|
1086
1219
|
function saveConfig() {
|
|
1087
1220
|
if (!canWriteConfig()) {
|
|
1088
1221
|
holdWrite();
|
|
@@ -1091,7 +1224,7 @@ window.__ModuleLoader__.load({
|
|
|
1091
1224
|
if (saveTimer !== void 0) window.clearTimeout(saveTimer);
|
|
1092
1225
|
saveTimer = window.setTimeout(() => {
|
|
1093
1226
|
saveTimer = void 0;
|
|
1094
|
-
|
|
1227
|
+
persistAndResolve();
|
|
1095
1228
|
}, SAVE_DEBOUNCE_MS);
|
|
1096
1229
|
}
|
|
1097
1230
|
function flushSave() {
|
|
@@ -1102,7 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1102
1235
|
holdWrite();
|
|
1103
1236
|
return;
|
|
1104
1237
|
}
|
|
1105
|
-
|
|
1238
|
+
persistAndResolve();
|
|
1106
1239
|
}
|
|
1107
1240
|
/** Persist the current config immediately (no debounce). */
|
|
1108
1241
|
function persistConfig() {
|
|
@@ -1110,7 +1243,19 @@ window.__ModuleLoader__.load({
|
|
|
1110
1243
|
holdWrite();
|
|
1111
1244
|
return;
|
|
1112
1245
|
}
|
|
1113
|
-
|
|
1246
|
+
persistAndResolve();
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Run the queued byte deletions without writing anything.
|
|
1250
|
+
*
|
|
1251
|
+
* The queue is normally drained by the next successful write, and every path that
|
|
1252
|
+
* queues one writes immediately afterwards — so this exists only for the page-hide
|
|
1253
|
+
* flush, where a delete could otherwise outlive the tab that asked for it.
|
|
1254
|
+
*/
|
|
1255
|
+
function drainDeferredDeletes() {
|
|
1256
|
+
const releasing = deferredDeletes;
|
|
1257
|
+
deferredDeletes = [];
|
|
1258
|
+
for (const slot of releasing) deleteImage(slot);
|
|
1114
1259
|
}
|
|
1115
1260
|
/** Load the persisted config plus the list of stored image slots. */
|
|
1116
1261
|
async function loadPersisted() {
|
|
@@ -1119,7 +1264,7 @@ window.__ModuleLoader__.load({
|
|
|
1119
1264
|
const d = data;
|
|
1120
1265
|
const slots = Array.isArray(d.slots) ? d.slots.filter((s) => typeof s === "string") : [];
|
|
1121
1266
|
const schema = typeof d.schema === "number" && isFinite(d.schema) ? d.schema : null;
|
|
1122
|
-
hostShape = typeof schema === "number" && schema >=
|
|
1267
|
+
hostShape = typeof schema === "number" && schema >= 11 ? "current" : "legacy";
|
|
1123
1268
|
if (hostShape === "legacy") holdWrite();
|
|
1124
1269
|
return {
|
|
1125
1270
|
config: d.config,
|
|
@@ -1127,30 +1272,68 @@ window.__ModuleLoader__.load({
|
|
|
1127
1272
|
schema
|
|
1128
1273
|
};
|
|
1129
1274
|
}
|
|
1275
|
+
/**
|
|
1276
|
+
* The revision of the config on disk, or null when the host could not be asked.
|
|
1277
|
+
*
|
|
1278
|
+
* The whole answer is one number: this runs on a timer, and pulling the config to
|
|
1279
|
+
* find out whether it changed would cost a full read every few seconds for a
|
|
1280
|
+
* question that is almost always "no".
|
|
1281
|
+
*/
|
|
1282
|
+
async function readRev() {
|
|
1283
|
+
const data = await rpcCall("readRev", {});
|
|
1284
|
+
if (data === null || typeof data !== "object") return null;
|
|
1285
|
+
const rev = data.rev;
|
|
1286
|
+
return typeof rev === "number" && isFinite(rev) ? rev : null;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Per-slot queue for the byte operations.
|
|
1290
|
+
*
|
|
1291
|
+
* Slots are RECYCLED — `nextSlot` hands out the first free `mN`, and deleting a
|
|
1292
|
+
* rule frees its numbers immediately — so a fast delete-then-add can name the same
|
|
1293
|
+
* slot twice within one round trip. Those two calls are independent requests on one
|
|
1294
|
+
* connection, which means their order at the host is whatever the transport decides
|
|
1295
|
+
* that moment: the delete can land AFTER the write, leaving the config naming a slot
|
|
1296
|
+
* whose bytes were just removed. The panel then paints a card that reads "这张图片还没有
|
|
1297
|
+
* 取到内容" — the rule is right there, the picture is not, and nothing reports an
|
|
1298
|
+
* error because nothing failed.
|
|
1299
|
+
*
|
|
1300
|
+
* So the operations on ONE slot run in the order they were asked for. Different
|
|
1301
|
+
* slots never wait for each other: a queue per slot, not one global one, because
|
|
1302
|
+
* adding seven pictures at once must not serialise into seven round trips.
|
|
1303
|
+
*/
|
|
1304
|
+
const slotQueues = /* @__PURE__ */ new Map();
|
|
1305
|
+
function enqueueForSlot(slot, run) {
|
|
1306
|
+
const next = (slotQueues.get(slot) ?? Promise.resolve()).then(run, run);
|
|
1307
|
+
slotQueues.set(slot, next);
|
|
1308
|
+
next.finally(() => {
|
|
1309
|
+
if (slotQueues.get(slot) === next) slotQueues.delete(slot);
|
|
1310
|
+
});
|
|
1311
|
+
return next;
|
|
1312
|
+
}
|
|
1130
1313
|
/** Read one rule image as a data URL (null when the slot is empty). */
|
|
1131
1314
|
async function readImage(slot) {
|
|
1132
|
-
const data = await rpcCall("readImage", { slot });
|
|
1315
|
+
const data = await enqueueForSlot(slot, () => rpcCall("readImage", { slot }));
|
|
1133
1316
|
if (data === null || typeof data !== "object") return null;
|
|
1134
1317
|
const url = data.dataUrl;
|
|
1135
1318
|
return typeof url === "string" ? url : null;
|
|
1136
1319
|
}
|
|
1137
1320
|
/** Persist (or clear) one rule image; one-shot, no debounce. */
|
|
1138
1321
|
async function writeImage(slot, dataUrl) {
|
|
1139
|
-
return await rpcCall("writeImage", {
|
|
1322
|
+
return await enqueueForSlot(slot, () => rpcCall("writeImage", {
|
|
1140
1323
|
slot,
|
|
1141
1324
|
dataUrl
|
|
1142
|
-
}) === true;
|
|
1325
|
+
})) === true;
|
|
1143
1326
|
}
|
|
1144
1327
|
/** Remove one rule image from disk. */
|
|
1145
1328
|
async function deleteImage(slot) {
|
|
1146
|
-
return await rpcCall("deleteImage", { slot }) === true;
|
|
1329
|
+
return await enqueueForSlot(slot, () => rpcCall("deleteImage", { slot })) === true;
|
|
1147
1330
|
}
|
|
1148
1331
|
/** Download an image from a network URL into one slot, replacing its bytes. */
|
|
1149
1332
|
async function fetchImageUrl(slot, url) {
|
|
1150
|
-
const res = await rpcRaw("fetchImageUrl", {
|
|
1333
|
+
const res = await enqueueForSlot(slot, () => rpcRaw("fetchImageUrl", {
|
|
1151
1334
|
slot,
|
|
1152
1335
|
url
|
|
1153
|
-
});
|
|
1336
|
+
}));
|
|
1154
1337
|
if (!res) return {
|
|
1155
1338
|
ok: false,
|
|
1156
1339
|
error: "no response"
|
|
@@ -1200,15 +1383,24 @@ window.__ModuleLoader__.load({
|
|
|
1200
1383
|
};
|
|
1201
1384
|
return v;
|
|
1202
1385
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1386
|
+
async function adoptRecommended() {
|
|
1387
|
+
const res = await rpcRaw("adoptRecommended", {});
|
|
1388
|
+
if (res === void 0 || res.ok !== true) return {
|
|
1389
|
+
ok: false,
|
|
1390
|
+
rev: cfg.rev
|
|
1391
|
+
};
|
|
1392
|
+
const value = res.value;
|
|
1393
|
+
const base = {
|
|
1394
|
+
ok: value?.ok === true,
|
|
1395
|
+
rev: typeof value?.rev === "number" ? value.rev : cfg.rev
|
|
1396
|
+
};
|
|
1397
|
+
if (base.ok) return base;
|
|
1398
|
+
return {
|
|
1399
|
+
...base,
|
|
1400
|
+
error: typeof value?.error === "string" ? value.error : void 0,
|
|
1401
|
+
slot: typeof value?.slot === "string" ? value.slot : void 0,
|
|
1402
|
+
wiped: value?.wiped === true
|
|
1403
|
+
};
|
|
1212
1404
|
}
|
|
1213
1405
|
/** Host default model — used only when the per-session services are absent. */
|
|
1214
1406
|
async function readDefaultModel() {
|
|
@@ -3680,8 +3872,9 @@ window.__ModuleLoader__.load({
|
|
|
3680
3872
|
*
|
|
3681
3873
|
* Every interval the plugin owns is declared HERE rather than at its call site, so
|
|
3682
3874
|
* "what does this plugin poll, and how often" has one answer. The scheduler itself
|
|
3683
|
-
* lives in ./watchdogs; the questions live in their own modules
|
|
3684
|
-
* the
|
|
3875
|
+
* lives in ./watchdogs; the questions live in their own modules — the theme
|
|
3876
|
+
* watchdog and the holiday rollover in ./appearance-checks, the delayed boot
|
|
3877
|
+
* restores in ./appearance.
|
|
3685
3878
|
*/
|
|
3686
3879
|
/** The theme skin / host palette re-assert, once a second while a colour is live. */
|
|
3687
3880
|
const THEME_WATCH_MS = 1e3;
|
|
@@ -3693,6 +3886,16 @@ window.__ModuleLoader__.load({
|
|
|
3693
3886
|
const RESTORE_DELAYS_MS = [300, 1500];
|
|
3694
3887
|
/** The holiday rollover: the answer only moves at Beijing midnight, a whole day wide. */
|
|
3695
3888
|
const HOLIDAY_WATCH_MS = 3e4;
|
|
3889
|
+
/**
|
|
3890
|
+
* How often the panel checks whether the config on disk moved under it.
|
|
3891
|
+
*
|
|
3892
|
+
* Only matters once agent access is on (the tools are the other writer), so the
|
|
3893
|
+
* check is a conditional no-op the rest of the time: it asks the host for one
|
|
3894
|
+
* number and stops. 3 s is the compromise between noticing an agent's edit while
|
|
3895
|
+
* the user is still looking at the screen and not paying a request per second for
|
|
3896
|
+
* a file that usually has not changed.
|
|
3897
|
+
*/
|
|
3898
|
+
const REV_POLL_MS = 3e3;
|
|
3696
3899
|
//#endregion
|
|
3697
3900
|
//#region src/client/appearance.ts
|
|
3698
3901
|
function createAppearance(host) {
|
|
@@ -4687,9 +4890,13 @@ window.__ModuleLoader__.load({
|
|
|
4687
4890
|
if (url !== null) images[slot] = url;
|
|
4688
4891
|
}
|
|
4689
4892
|
return {
|
|
4690
|
-
version:
|
|
4893
|
+
version: 11,
|
|
4691
4894
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4692
|
-
config:
|
|
4895
|
+
config: {
|
|
4896
|
+
...cfg,
|
|
4897
|
+
agentAccess: false,
|
|
4898
|
+
rev: 0
|
|
4899
|
+
},
|
|
4693
4900
|
images
|
|
4694
4901
|
};
|
|
4695
4902
|
}
|
|
@@ -4719,20 +4926,24 @@ window.__ModuleLoader__.load({
|
|
|
4719
4926
|
* Write a whole theme file into the store: its rules, its wallpapers, nothing
|
|
4720
4927
|
* else.
|
|
4721
4928
|
*
|
|
4722
|
-
*
|
|
4723
|
-
*
|
|
4724
|
-
*
|
|
4725
|
-
*
|
|
4726
|
-
*
|
|
4929
|
+
* The recommended profile no longer comes through here. It used to, as a
|
|
4930
|
+
* `wipeFirst` variant of this function, and that was the shape of a real bug: the
|
|
4931
|
+
* wipe is a separate round trip that empties the config ON DISK, so the write this
|
|
4932
|
+
* function then made named a revision the emptied store had never heard of, was
|
|
4933
|
+
* refused as stale, and adopted the config it had just replaced — the button had to
|
|
4934
|
+
* be pressed twice. Installing the profile is now one node-half operation
|
|
4935
|
+
* (`adoptRecommended`), and this path only ever imports a file over a live store.
|
|
4727
4936
|
*/
|
|
4728
|
-
async function applyThemeFile(host, data
|
|
4729
|
-
if (typeof data.version === "number" && data.version >
|
|
4937
|
+
async function applyThemeFile(host, data) {
|
|
4938
|
+
if (typeof data.version === "number" && data.version > 11) return false;
|
|
4730
4939
|
if (typeof data.config !== "object" || data.config === null) return false;
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4940
|
+
const ownRev = cfg.rev;
|
|
4941
|
+
const ownAccess = cfg.agentAccess;
|
|
4942
|
+
adoptConfig({
|
|
4943
|
+
...data.config,
|
|
4944
|
+
agentAccess: ownAccess,
|
|
4945
|
+
rev: ownRev
|
|
4946
|
+
});
|
|
4736
4947
|
const incoming = data.images ?? {};
|
|
4737
4948
|
const keep = /* @__PURE__ */ new Set();
|
|
4738
4949
|
for (const slot of cfg.rules.flatMap(ruleSlots)) {
|
|
@@ -4743,10 +4954,10 @@ window.__ModuleLoader__.load({
|
|
|
4743
4954
|
writeImage(slot, raw);
|
|
4744
4955
|
} else {
|
|
4745
4956
|
setImage(slot, null);
|
|
4746
|
-
|
|
4957
|
+
deleteImageAfterSave(slot);
|
|
4747
4958
|
}
|
|
4748
4959
|
}
|
|
4749
|
-
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && SLOT_RE.test(slot))
|
|
4960
|
+
for (const slot of Object.keys(incoming)) if (!keep.has(slot) && SLOT_RE.test(slot)) deleteImageAfterSave(slot);
|
|
4750
4961
|
persistConfig();
|
|
4751
4962
|
host.applyActive();
|
|
4752
4963
|
return true;
|
|
@@ -4982,6 +5193,8 @@ window.__ModuleLoader__.load({
|
|
|
4982
5193
|
recFailTooLarge: "这份推荐配置体积过大,未做任何改动",
|
|
4983
5194
|
recFailImage: "这张壁纸没拿到(图床上这个地址没有回应):",
|
|
4984
5195
|
recFailApply: "未能写入推荐配置,你原来的配置没有被改动",
|
|
5196
|
+
recFailAfterWipe: "推荐配置写了一半就失败了,你原来的规则已经被清空(图片也一起没了)。再点一次「使用推荐配置」可以重来,图片是重新下载的",
|
|
5197
|
+
recFailNoStore: "壁纸没能下载,推荐配置尚未开始,你原来的配置没有被改动",
|
|
4985
5198
|
storeCardTitle: "图片存储",
|
|
4986
5199
|
storeCardDesc: "因意外导致的孤儿图片这里可以清掉",
|
|
4987
5200
|
storeOrphans: "可清理",
|
|
@@ -4998,6 +5211,9 @@ window.__ModuleLoader__.load({
|
|
|
4998
5211
|
repoLink: "在 GitHub 上打开仓库",
|
|
4999
5212
|
holidayTitle: "节日特殊背景",
|
|
5000
5213
|
holidayEnable: "启用节日特殊背景(中秋当天、国庆 10 月 1–7 日)",
|
|
5214
|
+
agentTitle: "允许 AI 修改背景",
|
|
5215
|
+
agentEnable: "打开后,AI 可以调用工具增删改规则、切图、调透明度/颜色/模糊/布局。关闭时这些工具对 AI 完全不可见,不占用上下文。",
|
|
5216
|
+
agentOn: "已开启:AI 可以修改这套外观,改动会即时生效。",
|
|
5001
5217
|
trTitle: "切换效果",
|
|
5002
5218
|
trEffect: "切换方式",
|
|
5003
5219
|
trEffectFade: "淡入淡出",
|
|
@@ -5163,6 +5379,8 @@ window.__ModuleLoader__.load({
|
|
|
5163
5379
|
recFailTooLarge: "The recommended profile is too large. Nothing was changed.",
|
|
5164
5380
|
recFailImage: "Could not fetch this wallpaper — the image host does not answer for that address:",
|
|
5165
5381
|
recFailApply: "Could not write the recommended profile — your own is untouched",
|
|
5382
|
+
recFailAfterWipe: "The recommended profile failed halfway, after your own rules had already been cleared (their pictures too). Pressing it again starts over, and the wallpapers are re-downloaded",
|
|
5383
|
+
recFailNoStore: "The wallpapers could not be downloaded, so nothing was installed — your own config is untouched",
|
|
5166
5384
|
storeCardTitle: "Image storage",
|
|
5167
5385
|
storeCardDesc: "Orphaned image files left behind by an accident can be cleared here",
|
|
5168
5386
|
storeOrphans: "Cleanable",
|
|
@@ -5179,6 +5397,9 @@ window.__ModuleLoader__.load({
|
|
|
5179
5397
|
repoLink: "Open the repository on GitHub",
|
|
5180
5398
|
holidayTitle: "Holiday backgrounds",
|
|
5181
5399
|
holidayEnable: "Enable holiday backgrounds (Mid-Autumn day, Oct 1–7)",
|
|
5400
|
+
agentTitle: "Let the AI change the background",
|
|
5401
|
+
agentEnable: "When on, the AI can add, edit and remove rules, switch images, and set opacity, colour, blur and layout. When off these tools are entirely absent from the AI, costing no context.",
|
|
5402
|
+
agentOn: "On: the AI can change this appearance, and edits take effect immediately.",
|
|
5182
5403
|
trTitle: "Switch effect",
|
|
5183
5404
|
trEffect: "Transition",
|
|
5184
5405
|
trEffectFade: "Cross-fade",
|
|
@@ -5341,7 +5562,9 @@ window.__ModuleLoader__.load({
|
|
|
5341
5562
|
holidays: {
|
|
5342
5563
|
enabled: true,
|
|
5343
5564
|
items: defaultHolidayRules()
|
|
5344
|
-
}
|
|
5565
|
+
},
|
|
5566
|
+
agentAccess: false,
|
|
5567
|
+
rev: 0
|
|
5345
5568
|
};
|
|
5346
5569
|
}
|
|
5347
5570
|
/**
|
|
@@ -5354,7 +5577,7 @@ window.__ModuleLoader__.load({
|
|
|
5354
5577
|
*/
|
|
5355
5578
|
function recommendedThemeFile(images = {}) {
|
|
5356
5579
|
return {
|
|
5357
|
-
version:
|
|
5580
|
+
version: 11,
|
|
5358
5581
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5359
5582
|
config: recommendedConfig(),
|
|
5360
5583
|
images
|
|
@@ -7834,6 +8057,7 @@ window.__ModuleLoader__.load({
|
|
|
7834
8057
|
const importRef = (0, react.useRef)(null);
|
|
7835
8058
|
p.useStore((s) => s.rev + s.rulesRev);
|
|
7836
8059
|
const holidays = cfg.holidays;
|
|
8060
|
+
const agentAccess = cfg.agentAccess;
|
|
7837
8061
|
const tr = cfg.transition;
|
|
7838
8062
|
const store = p.useStore((s) => s);
|
|
7839
8063
|
const [text, setText] = (0, react.useState)("");
|
|
@@ -7917,7 +8141,7 @@ window.__ModuleLoader__.load({
|
|
|
7917
8141
|
});
|
|
7918
8142
|
setRecBusy(false);
|
|
7919
8143
|
if (!res.ok) {
|
|
7920
|
-
notify(res.slot === void 0 ? t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork") : `${t("recFailImage")} ${res.slot}`, false);
|
|
8144
|
+
notify(res.slot === void 0 ? `${t(REC_ERROR_KEYS[res.error] ?? "recFailNetwork")} ${t("recFailNoStore")}` : `${t("recFailImage")} ${res.slot} ${t("recFailNoStore")}`, false);
|
|
7921
8145
|
return;
|
|
7922
8146
|
}
|
|
7923
8147
|
setPending(res.preset);
|
|
@@ -7926,10 +8150,14 @@ window.__ModuleLoader__.load({
|
|
|
7926
8150
|
const preset = pending;
|
|
7927
8151
|
if (preset === null) return;
|
|
7928
8152
|
setRecBusy(true);
|
|
7929
|
-
const
|
|
8153
|
+
const res = await p.applyRecommended(preset);
|
|
7930
8154
|
setRecBusy(false);
|
|
7931
8155
|
setPending(null);
|
|
7932
|
-
|
|
8156
|
+
if (res.ok) {
|
|
8157
|
+
notify(t("recDone"));
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8160
|
+
notify(res.wiped ? t("recFailAfterWipe") : t("recFailApply"), false);
|
|
7933
8161
|
};
|
|
7934
8162
|
const [usage, setUsage] = (0, react.useState)(null);
|
|
7935
8163
|
/** Distinguishes "not asked yet" from "asked, and the host cannot answer". */
|
|
@@ -7990,6 +8218,30 @@ window.__ModuleLoader__.load({
|
|
|
7990
8218
|
})
|
|
7991
8219
|
]
|
|
7992
8220
|
}),
|
|
8221
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
8222
|
+
className: "dab-card dab-rise",
|
|
8223
|
+
style: { "--d": 1 },
|
|
8224
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8225
|
+
className: "dab-chip-row",
|
|
8226
|
+
style: { alignItems: "center" },
|
|
8227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8228
|
+
type: "button",
|
|
8229
|
+
className: `dab-toggle${agentAccess ? " is-on" : ""}`,
|
|
8230
|
+
role: "switch",
|
|
8231
|
+
"aria-checked": agentAccess,
|
|
8232
|
+
title: t("agentEnable"),
|
|
8233
|
+
onClick: () => {
|
|
8234
|
+
const next = !agentAccess;
|
|
8235
|
+
p.setAgentAccess(next);
|
|
8236
|
+
if (next) notify(t("agentOn"));
|
|
8237
|
+
},
|
|
8238
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dab-toggle-knob" })
|
|
8239
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8240
|
+
className: "dab-holiday-label",
|
|
8241
|
+
children: t("agentTitle")
|
|
8242
|
+
})]
|
|
8243
|
+
})
|
|
8244
|
+
}),
|
|
7993
8245
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
7994
8246
|
className: "dab-card dab-rise",
|
|
7995
8247
|
style: { "--d": 1 },
|
|
@@ -8762,6 +9014,22 @@ window.__ModuleLoader__.load({
|
|
|
8762
9014
|
};
|
|
8763
9015
|
schedule.add(themeWatchdog(checkHost));
|
|
8764
9016
|
schedule.add(holidayRollover(checkHost));
|
|
9017
|
+
schedule.add({
|
|
9018
|
+
name: "rev",
|
|
9019
|
+
everyMs: REV_POLL_MS,
|
|
9020
|
+
run() {
|
|
9021
|
+
if (!cfg.agentAccess) return;
|
|
9022
|
+
(async () => {
|
|
9023
|
+
const rev = await readRev();
|
|
9024
|
+
if (rev === null || rev === cfg.rev) return;
|
|
9025
|
+
const persisted = await loadPersisted();
|
|
9026
|
+
if (persisted === null) return;
|
|
9027
|
+
adoptConfig(persisted.config);
|
|
9028
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) if (displayImageOf(slot) === null) await loadSlot(slot);
|
|
9029
|
+
applyActive();
|
|
9030
|
+
})();
|
|
9031
|
+
}
|
|
9032
|
+
});
|
|
8765
9033
|
(async () => {
|
|
8766
9034
|
const persisted = await loadPersisted();
|
|
8767
9035
|
if (persisted !== null) {
|
|
@@ -8773,6 +9041,13 @@ window.__ModuleLoader__.load({
|
|
|
8773
9041
|
}
|
|
8774
9042
|
applyActive();
|
|
8775
9043
|
})();
|
|
9044
|
+
setConflictHandler((config) => {
|
|
9045
|
+
adoptConfig(config);
|
|
9046
|
+
(async () => {
|
|
9047
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) if (displayImageOf(slot) === null) await loadSlot(slot);
|
|
9048
|
+
applyActive();
|
|
9049
|
+
})();
|
|
9050
|
+
});
|
|
8776
9051
|
ctx.effect(() => () => {
|
|
8777
9052
|
teardownWp();
|
|
8778
9053
|
}, "dsh-background-by-model: wp cleanup");
|
|
@@ -8871,7 +9146,7 @@ window.__ModuleLoader__.load({
|
|
|
8871
9146
|
for (const slot of ruleSlots(rule)) {
|
|
8872
9147
|
if (stillUsed.has(slot)) continue;
|
|
8873
9148
|
setImage(slot, null);
|
|
8874
|
-
|
|
9149
|
+
deleteImageAfterSave(slot);
|
|
8875
9150
|
}
|
|
8876
9151
|
}
|
|
8877
9152
|
rulesRev++;
|
|
@@ -8929,7 +9204,7 @@ window.__ModuleLoader__.load({
|
|
|
8929
9204
|
persistConfig();
|
|
8930
9205
|
if (!takenSlots().has(slot)) {
|
|
8931
9206
|
setImage(slot, null);
|
|
8932
|
-
|
|
9207
|
+
deleteImageAfterSave(slot);
|
|
8933
9208
|
}
|
|
8934
9209
|
repaintIfMoved(id, winner);
|
|
8935
9210
|
},
|
|
@@ -9133,6 +9408,11 @@ window.__ModuleLoader__.load({
|
|
|
9133
9408
|
sync();
|
|
9134
9409
|
saveConfig();
|
|
9135
9410
|
},
|
|
9411
|
+
setAgentAccess: (v) => {
|
|
9412
|
+
cfg.agentAccess = v;
|
|
9413
|
+
sync();
|
|
9414
|
+
persistConfig();
|
|
9415
|
+
},
|
|
9136
9416
|
setHolidaysEnabled: (v) => {
|
|
9137
9417
|
cfg.holidays.enabled = v;
|
|
9138
9418
|
persistConfig();
|
|
@@ -9147,7 +9427,7 @@ window.__ModuleLoader__.load({
|
|
|
9147
9427
|
try {
|
|
9148
9428
|
const data = JSON.parse(await file.text());
|
|
9149
9429
|
if (!data || typeof data !== "object") return false;
|
|
9150
|
-
return await applyThemeFile({ applyActive }, data
|
|
9430
|
+
return await applyThemeFile({ applyActive }, data);
|
|
9151
9431
|
} catch {
|
|
9152
9432
|
return false;
|
|
9153
9433
|
}
|
|
@@ -9226,9 +9506,42 @@ window.__ModuleLoader__.load({
|
|
|
9226
9506
|
};
|
|
9227
9507
|
},
|
|
9228
9508
|
/** Replace EVERYTHING with a downloaded profile: empty the store, then write it. */
|
|
9229
|
-
applyRecommended: async (
|
|
9230
|
-
if (!canWriteConfig()) return
|
|
9231
|
-
|
|
9509
|
+
applyRecommended: async (_preset) => {
|
|
9510
|
+
if (!canWriteConfig()) return {
|
|
9511
|
+
ok: false,
|
|
9512
|
+
wiped: false
|
|
9513
|
+
};
|
|
9514
|
+
const res = await adoptRecommended();
|
|
9515
|
+
if (!res.ok) {
|
|
9516
|
+
if (res.wiped === true) {
|
|
9517
|
+
const after = await loadPersisted();
|
|
9518
|
+
if (after !== null) {
|
|
9519
|
+
adoptConfig(after.config);
|
|
9520
|
+
clearImages();
|
|
9521
|
+
applyActive();
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
return {
|
|
9525
|
+
ok: false,
|
|
9526
|
+
error: res.error,
|
|
9527
|
+
slot: res.slot,
|
|
9528
|
+
wiped: res.wiped === true
|
|
9529
|
+
};
|
|
9530
|
+
}
|
|
9531
|
+
const persisted = await loadPersisted();
|
|
9532
|
+
if (persisted === null) return {
|
|
9533
|
+
ok: false,
|
|
9534
|
+
error: "the profile was installed but could not be read back",
|
|
9535
|
+
wiped: true
|
|
9536
|
+
};
|
|
9537
|
+
adoptConfig(persisted.config);
|
|
9538
|
+
clearImages();
|
|
9539
|
+
for (const slot of cfg.rules.flatMap(ruleSlots)) await loadSlot(slot);
|
|
9540
|
+
applyActive();
|
|
9541
|
+
return {
|
|
9542
|
+
ok: true,
|
|
9543
|
+
wiped: false
|
|
9544
|
+
};
|
|
9232
9545
|
},
|
|
9233
9546
|
readStoreUsage: () => readStoreUsage(cfg.rules.flatMap(ruleSlots)),
|
|
9234
9547
|
pruneOrphanImages: async () => {
|
|
@@ -9252,7 +9565,10 @@ window.__ModuleLoader__.load({
|
|
|
9252
9565
|
ctx.effect(() => () => {
|
|
9253
9566
|
disposeThemeResets();
|
|
9254
9567
|
}, "dsh-background-by-model: theme resets watch");
|
|
9255
|
-
const onPageHide = () =>
|
|
9568
|
+
const onPageHide = () => {
|
|
9569
|
+
flushSave();
|
|
9570
|
+
drainDeferredDeletes();
|
|
9571
|
+
};
|
|
9256
9572
|
window.addEventListener("pagehide", onPageHide);
|
|
9257
9573
|
ctx.effect(() => () => window.removeEventListener("pagehide", onPageHide), "dsh-background-by-model: pagehide flush");
|
|
9258
9574
|
}
|