@cowwo/dsh-icon-custom 0.7.1 → 0.8.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.zh.md +22 -0
- package/client/client.js +1178 -13
- package/lib/index.js +71 -0
- package/lib/typert.host.js +92 -8
- package/lib/unread.js +131 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -27,6 +27,15 @@ import { mkdir, rename, writeFile } from 'node:fs/promises'
|
|
|
27
27
|
import { createRequire } from 'node:module'
|
|
28
28
|
import { homedir } from 'node:os'
|
|
29
29
|
import { join, resolve } from 'node:path'
|
|
30
|
+
import {
|
|
31
|
+
LAST_TURN_END_KEY,
|
|
32
|
+
applyLastTurnEnd,
|
|
33
|
+
initLastTurnEnd,
|
|
34
|
+
lastTurnEndStateSchema,
|
|
35
|
+
lastTurnEndView,
|
|
36
|
+
lastTurnEndViewSchema,
|
|
37
|
+
normalizeUnreadConfig
|
|
38
|
+
} from './unread.js'
|
|
30
39
|
|
|
31
40
|
const MAX_BYTES = 10 * 1024 * 1024 // 10 MB
|
|
32
41
|
// Normalized mime -> safe extension. CUR (mouse cursor) is accepted as ICO.
|
|
@@ -306,13 +315,33 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
306
315
|
this.storageDir = join(resolveHome(), 'custom-favicon')
|
|
307
316
|
this.libraryDir = join(this.storageDir, 'library')
|
|
308
317
|
this.indexPath = join(this.storageDir, 'library.json')
|
|
318
|
+
this.unreadPath = join(this.storageDir, 'unread.json')
|
|
309
319
|
// { activeId, icons: { [id]: { id, mime, ext, name, size, savedAt, rev, pwa, logo } } }
|
|
310
320
|
this.library = { activeId: null, icons: {} }
|
|
311
321
|
// The active icon's loaded state: { rev, mime, ext, name, size, savedAt, pwa, logo, bytes } | null
|
|
312
322
|
this.current = null
|
|
323
|
+
// Unread-badge rule: { reasons: { [reason]: boolean }, pending: boolean }.
|
|
324
|
+
// Kept in its own file so a settings change can never corrupt the icon
|
|
325
|
+
// library index.
|
|
326
|
+
this.unread = normalizeUnreadConfig(null)
|
|
313
327
|
// Platform PWA manifest (raw + parsed), read once at startup.
|
|
314
328
|
this.baseManifest = loadBaseManifest()
|
|
315
329
|
this._load()
|
|
330
|
+
this._loadUnread()
|
|
331
|
+
// "How did this session's last turn end" as an official session
|
|
332
|
+
// projection. Optional registration on purpose: this plugin is a display
|
|
333
|
+
// plugin first, so a deployment without the projection service loses the
|
|
334
|
+
// badge's number, never the favicon.
|
|
335
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
336
|
+
projectionCtx.sessionProjections.register({
|
|
337
|
+
key: LAST_TURN_END_KEY,
|
|
338
|
+
stateSchema: lastTurnEndStateSchema,
|
|
339
|
+
stateVersion: 1,
|
|
340
|
+
init: () => initLastTurnEnd(),
|
|
341
|
+
apply: applyLastTurnEnd,
|
|
342
|
+
wire: { viewSchema: lastTurnEndViewSchema, view: lastTurnEndView }
|
|
343
|
+
})
|
|
344
|
+
})
|
|
316
345
|
ctx.effect(() => ctx.webServer.register({
|
|
317
346
|
kind: 'exact',
|
|
318
347
|
path: '/icon-custom.svg',
|
|
@@ -383,6 +412,19 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
383
412
|
return writeFile(this.indexPath, JSON.stringify(this.library, null, 2))
|
|
384
413
|
}
|
|
385
414
|
|
|
415
|
+
/** Load the unread-badge rule (missing or corrupt file = defaults). */
|
|
416
|
+
_loadUnread() {
|
|
417
|
+
try {
|
|
418
|
+
const raw = JSON.parse(readFileSync(this.unreadPath).toString('utf8'))
|
|
419
|
+
this.unread = normalizeUnreadConfig(raw)
|
|
420
|
+
} catch { /* no persisted rule yet: keep defaults */ }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Persist the unread-badge rule (best effort, advisory file). */
|
|
424
|
+
_saveUnread() {
|
|
425
|
+
return writeFile(this.unreadPath, JSON.stringify(this.unread, null, 2))
|
|
426
|
+
}
|
|
427
|
+
|
|
386
428
|
/** Currently active icon as a JSON-safe status object (no raw bytes). */
|
|
387
429
|
status() {
|
|
388
430
|
const c = this.current
|
|
@@ -396,6 +438,35 @@ export class FaviconCustomService extends TypertRemoteService {
|
|
|
396
438
|
return this.status()
|
|
397
439
|
}
|
|
398
440
|
|
|
441
|
+
/**
|
|
442
|
+
* typert endpoint: read the unread-badge rule.
|
|
443
|
+
*
|
|
444
|
+
* Deliberately its own endpoint instead of a field on `status()`: the five
|
|
445
|
+
* icon RPCs (`getStatus` / `setIcon` / `setPwa` / `setLogo` / `resetIcon`)
|
|
446
|
+
* keep their EXACT pre-badge response shape, so a problem in this feature
|
|
447
|
+
* can never break the icon itself.
|
|
448
|
+
* @returns `{ reasons, pending }`.
|
|
449
|
+
*/
|
|
450
|
+
async getUnreadRule() {
|
|
451
|
+
return { reasons: this.unread.reasons, pending: this.unread.pending, workspaceDot: this.unread.workspaceDot }
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* typert endpoint: replace the unread-badge rule — which turn-end reasons
|
|
456
|
+
* light the badge. The rule is applied on the client, so this takes effect
|
|
457
|
+
* without re-folding any session log.
|
|
458
|
+
* @param request - `{ reasons, pending? }`; unknown or missing keys fall back to defaults.
|
|
459
|
+
* @returns the stored rule.
|
|
460
|
+
*/
|
|
461
|
+
async setUnreadRule(request) {
|
|
462
|
+
if (request === null || typeof request !== 'object') {
|
|
463
|
+
throw new Error('缺少 reasons 参数')
|
|
464
|
+
}
|
|
465
|
+
this.unread = normalizeUnreadConfig(request)
|
|
466
|
+
await this._saveUnread().catch(() => {}) // rule file is advisory
|
|
467
|
+
return { reasons: this.unread.reasons, pending: this.unread.pending, workspaceDot: this.unread.workspaceDot }
|
|
468
|
+
}
|
|
469
|
+
|
|
399
470
|
/** typert endpoint: store a new icon uploaded as a data URL and make it active. */
|
|
400
471
|
async setIcon(request) {
|
|
401
472
|
if (request === null || typeof request !== 'object') throw new Error('缺少图标参数')
|
package/lib/typert.host.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
/* Hand-written typert host manifest for dsh-icon-custom (strict face).
|
|
2
|
+
*
|
|
3
|
+
* DSH 0.2.x contract: every strict codec carries a `create()` FACTORY returning
|
|
4
|
+
* its zod schema — the gateway calls `codec.create().parse(value)` and the
|
|
5
|
+
* registry caches the result. The 0.1.x bare `schema:` field is gone; keeping
|
|
6
|
+
* it makes `validateTypertManifest` throw `has no create() factory`, so the
|
|
7
|
+
* host endpoints below never register and the settings page silently stops
|
|
8
|
+
* working (0.8.0 shipped this way — see the 0.8.1 fix). Do not reintroduce
|
|
9
|
+
* `schema:` here: run the compatibility check before publishing.
|
|
10
|
+
*
|
|
2
11
|
* The request/response shapes are unchanged from 0.6.x. From 0.7.0 the host
|
|
3
12
|
* additionally derives a canonical PNG set (see lib/index.js); `status.mime` /
|
|
4
13
|
* `status.ext` still reflect the *uploaded* format, while the PWA/logo path
|
|
@@ -34,6 +43,24 @@ const setLogoRequestSchema = z.object({
|
|
|
34
43
|
enabled: z.boolean()
|
|
35
44
|
})
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Unread-badge rule: which `turn/end` reasons light the badge, plus whether
|
|
48
|
+
* "someone is waiting for you" counts. Its own endpoint pair on purpose — the
|
|
49
|
+
* icon RPCs above keep their exact pre-badge response shape.
|
|
50
|
+
*/
|
|
51
|
+
const unreadRuleSchema = z.object({
|
|
52
|
+
reasons: z.record(z.string(), z.boolean()),
|
|
53
|
+
pending: z.boolean(),
|
|
54
|
+
/** 实验项:工作区图标上的红点(贴产品 DOM,可一键关闭)。 */
|
|
55
|
+
workspaceDot: z.boolean()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const setUnreadRuleRequestSchema = z.object({
|
|
59
|
+
reasons: z.record(z.string(), z.boolean()),
|
|
60
|
+
pending: z.boolean().optional(),
|
|
61
|
+
workspaceDot: z.boolean().optional()
|
|
62
|
+
})
|
|
63
|
+
|
|
37
64
|
const setIconResultSchema = statusSchema
|
|
38
65
|
|
|
39
66
|
export const TYPERT = {
|
|
@@ -51,7 +78,7 @@ export const TYPERT = {
|
|
|
51
78
|
result: {
|
|
52
79
|
mode: 'strict',
|
|
53
80
|
typeSymbol: 'dsh-icon-custom#iconCustom/getStatus:result',
|
|
54
|
-
|
|
81
|
+
create: () => statusSchema
|
|
55
82
|
},
|
|
56
83
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
57
84
|
},
|
|
@@ -69,14 +96,14 @@ export const TYPERT = {
|
|
|
69
96
|
codec: {
|
|
70
97
|
mode: 'strict',
|
|
71
98
|
typeSymbol: 'dsh-icon-custom#iconCustom/setIcon:request',
|
|
72
|
-
|
|
99
|
+
create: () => setIconRequestSchema
|
|
73
100
|
}
|
|
74
101
|
}
|
|
75
102
|
],
|
|
76
103
|
result: {
|
|
77
104
|
mode: 'strict',
|
|
78
105
|
typeSymbol: 'dsh-icon-custom#iconCustom/setIcon:result',
|
|
79
|
-
|
|
106
|
+
create: () => setIconResultSchema
|
|
80
107
|
},
|
|
81
108
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
82
109
|
},
|
|
@@ -94,14 +121,14 @@ export const TYPERT = {
|
|
|
94
121
|
codec: {
|
|
95
122
|
mode: 'strict',
|
|
96
123
|
typeSymbol: 'dsh-icon-custom#iconCustom/setPwa:request',
|
|
97
|
-
|
|
124
|
+
create: () => setPwaRequestSchema
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
],
|
|
101
128
|
result: {
|
|
102
129
|
mode: 'strict',
|
|
103
130
|
typeSymbol: 'dsh-icon-custom#iconCustom/setPwa:result',
|
|
104
|
-
|
|
131
|
+
create: () => statusSchema
|
|
105
132
|
},
|
|
106
133
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
107
134
|
},
|
|
@@ -119,14 +146,14 @@ export const TYPERT = {
|
|
|
119
146
|
codec: {
|
|
120
147
|
mode: 'strict',
|
|
121
148
|
typeSymbol: 'dsh-icon-custom#iconCustom/setLogo:request',
|
|
122
|
-
|
|
149
|
+
create: () => setLogoRequestSchema
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
152
|
],
|
|
126
153
|
result: {
|
|
127
154
|
mode: 'strict',
|
|
128
155
|
typeSymbol: 'dsh-icon-custom#iconCustom/setLogo:result',
|
|
129
|
-
|
|
156
|
+
create: () => statusSchema
|
|
130
157
|
},
|
|
131
158
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
132
159
|
},
|
|
@@ -140,7 +167,46 @@ export const TYPERT = {
|
|
|
140
167
|
result: {
|
|
141
168
|
mode: 'strict',
|
|
142
169
|
typeSymbol: 'dsh-icon-custom#iconCustom/resetIcon:result',
|
|
143
|
-
|
|
170
|
+
create: () => statusSchema
|
|
171
|
+
},
|
|
172
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'dsh-icon-custom#iconCustom/getUnreadRule',
|
|
176
|
+
service: 'iconCustom',
|
|
177
|
+
namespace: 'iconCustom',
|
|
178
|
+
method: 'getUnreadRule',
|
|
179
|
+
invocation: { kind: 'direct' },
|
|
180
|
+
parameters: [],
|
|
181
|
+
result: {
|
|
182
|
+
mode: 'strict',
|
|
183
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/getUnreadRule:result',
|
|
184
|
+
create: () => unreadRuleSchema
|
|
185
|
+
},
|
|
186
|
+
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'dsh-icon-custom#iconCustom/setUnreadRule',
|
|
190
|
+
service: 'iconCustom',
|
|
191
|
+
namespace: 'iconCustom',
|
|
192
|
+
method: 'setUnreadRule',
|
|
193
|
+
invocation: { kind: 'direct' },
|
|
194
|
+
parameters: [
|
|
195
|
+
{
|
|
196
|
+
name: 'request',
|
|
197
|
+
wire: 'request',
|
|
198
|
+
source: 'json',
|
|
199
|
+
codec: {
|
|
200
|
+
mode: 'strict',
|
|
201
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/setUnreadRule:request',
|
|
202
|
+
create: () => setUnreadRuleRequestSchema
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
result: {
|
|
207
|
+
mode: 'strict',
|
|
208
|
+
typeSymbol: 'dsh-icon-custom#iconCustom/setUnreadRule:result',
|
|
209
|
+
create: () => unreadRuleSchema
|
|
144
210
|
},
|
|
145
211
|
sourceLocation: { file: 'lib/index.js', line: 1, column: 1 }
|
|
146
212
|
}
|
|
@@ -199,6 +265,24 @@ export const TYPERT = {
|
|
|
199
265
|
summary: "Clear the custom favicon.",
|
|
200
266
|
jsDoc: "/** Clear the custom favicon. */",
|
|
201
267
|
tags: []
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
kind: 'method',
|
|
271
|
+
name: 'getUnreadRule',
|
|
272
|
+
signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean }>',
|
|
273
|
+
description: "Read the unread-badge rule.",
|
|
274
|
+
summary: "Read the unread-badge rule.",
|
|
275
|
+
jsDoc: "/** Read the unread-badge rule. */",
|
|
276
|
+
tags: []
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
kind: 'method',
|
|
280
|
+
name: 'setUnreadRule',
|
|
281
|
+
signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean }>',
|
|
282
|
+
description: "Replace the unread-badge rule: which turn-end reasons light the red dot.",
|
|
283
|
+
summary: "Set the unread-badge rule.",
|
|
284
|
+
jsDoc: "/** Set the unread-badge rule. */",
|
|
285
|
+
tags: []
|
|
202
286
|
}
|
|
203
287
|
],
|
|
204
288
|
types: []
|
package/lib/unread.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-icon-custom · "未读"规则(宿主半,纯函数)
|
|
3
|
+
*
|
|
4
|
+
* 只干一件事:把会话日志里的 `turn/end` 折叠成一条**事实**——
|
|
5
|
+
* 「这个会话最后一次是怎么结束的、什么时候结束的」。
|
|
6
|
+
*
|
|
7
|
+
* 它**不判断**「这种结束值不值得亮红点」。那是用户设置,由客户端套用。
|
|
8
|
+
* 这样勾掉一个原因会立刻生效,不需要回头重折任何日志。
|
|
9
|
+
*
|
|
10
|
+
* 这个文件不碰 cordis、不碰文件系统、不认任何其他插件:
|
|
11
|
+
* 纯 reducer + 一套原因词汇表。以后想把它搬进独立的包,原样搬走即可。
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 红点可以为哪些结束原因亮起来。
|
|
17
|
+
* `aborted`(被取消)按取消来源拆成两条——你亲手按的停止不该提醒你。
|
|
18
|
+
*/
|
|
19
|
+
export const UNREAD_REASONS = [
|
|
20
|
+
'completed',
|
|
21
|
+
'error',
|
|
22
|
+
'blocked',
|
|
23
|
+
'max-tokens',
|
|
24
|
+
'interrupted',
|
|
25
|
+
'aborted:user',
|
|
26
|
+
'aborted:other'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/** 默认:除了"你自己按的停止",其余都算。 */
|
|
30
|
+
export const UNREAD_REASON_DEFAULTS = {
|
|
31
|
+
completed: true,
|
|
32
|
+
error: true,
|
|
33
|
+
blocked: true,
|
|
34
|
+
'max-tokens': true,
|
|
35
|
+
interrupted: true,
|
|
36
|
+
'aborted:user': false,
|
|
37
|
+
'aborted:other': true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 投影 key:客户端从 `entry.projectionValues[LAST_TURN_END_KEY]` 读它。 */
|
|
41
|
+
export const LAST_TURN_END_KEY = 'lastTurnEnd'
|
|
42
|
+
|
|
43
|
+
/** 默认:门铃("有人在等你")也算一条。 */
|
|
44
|
+
export const UNREAD_PENDING_DEFAULT = true
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 默认:在工作区那一行的文件夹图标上画红点。
|
|
48
|
+
*
|
|
49
|
+
* 这是全套功能里**唯一贴着产品 DOM 走**的部分(侧栏那片没有插槽),
|
|
50
|
+
* 所以它有自己的开关:一旦 DSH 升级改了结构导致它失效或画歪,
|
|
51
|
+
* 关掉它就行,红点数字/清单/跳转那些稳的部分完全不受影响。
|
|
52
|
+
*/
|
|
53
|
+
export const UNREAD_WORKSPACE_DOT_DEFAULT = true
|
|
54
|
+
|
|
55
|
+
/** 把任意输入夹成一张完整、只有布尔值的开关表(缺项取默认)。 */
|
|
56
|
+
export function normalizeReasons(input) {
|
|
57
|
+
const out = {}
|
|
58
|
+
for (const key of UNREAD_REASONS) {
|
|
59
|
+
const value = input !== null && typeof input === 'object' ? input[key] : undefined
|
|
60
|
+
out[key] = typeof value === 'boolean' ? value : UNREAD_REASON_DEFAULTS[key]
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 把任意输入夹成完整设置。
|
|
67
|
+
* @param input - `{ reasons?, pending? }`,可以是脏的、缺项的、null
|
|
68
|
+
* @returns `{ reasons, pending }` —— 两张表都完整
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeUnreadConfig(input) {
|
|
71
|
+
const source = input !== null && typeof input === 'object' ? input : {}
|
|
72
|
+
return {
|
|
73
|
+
reasons: normalizeReasons(source.reasons),
|
|
74
|
+
pending: typeof source.pending === 'boolean' ? source.pending : UNREAD_PENDING_DEFAULT,
|
|
75
|
+
workspaceDot: typeof source.workspaceDot === 'boolean' ? source.workspaceDot : UNREAD_WORKSPACE_DOT_DEFAULT
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 一个 `turn/end` 的 reason → 稳定的原因 key。
|
|
81
|
+
* `aborted` 再看一层 `reason.kind`:`user` 是你自己按的,其余是重启/热更/父级取消。
|
|
82
|
+
* @param reason - `TurnEndReason`
|
|
83
|
+
* @returns 原因 key(`'aborted:user'` / `'aborted:other'` / 原 kind / `'unknown'`)
|
|
84
|
+
*/
|
|
85
|
+
export function reasonKey(reason) {
|
|
86
|
+
const kind = reason !== null && typeof reason === 'object' && typeof reason.kind === 'string' ? reason.kind : 'unknown'
|
|
87
|
+
if (kind !== 'aborted') return kind
|
|
88
|
+
const cause = reason.reason !== null && typeof reason.reason === 'object' ? reason.reason.kind : undefined
|
|
89
|
+
return cause === 'user' ? 'aborted:user' : 'aborted:other'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 折叠初始态:还没有任何回合结束过。 */
|
|
93
|
+
export function initLastTurnEnd() {
|
|
94
|
+
return { endAt: 0, reason: null }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 纯转移:只有 `turn/end` 改动它,其余事件原样返回同一个引用
|
|
99
|
+
* (契约要求 `Object.is` 不变以产生零下游工作)。
|
|
100
|
+
* @param state - 覆盖此前全部事件的状态
|
|
101
|
+
* @param event - 下一个已提交的会话事件
|
|
102
|
+
* @returns 下一个状态(事件与本单元无关时返回同一个引用)
|
|
103
|
+
*/
|
|
104
|
+
export function applyLastTurnEnd(state, event) {
|
|
105
|
+
if (event === null || typeof event !== 'object' || event.type !== 'turn/end') return state
|
|
106
|
+
const data = event.data !== null && typeof event.data === 'object' ? event.data : {}
|
|
107
|
+
const reason = reasonKey(data.reason)
|
|
108
|
+
const endAt = typeof event.time === 'number' ? event.time : 0
|
|
109
|
+
if (state.endAt === endAt && state.reason === reason) return state
|
|
110
|
+
return { endAt, reason }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 状态 → 上线载荷。 */
|
|
114
|
+
export function lastTurnEndView(state) {
|
|
115
|
+
return {
|
|
116
|
+
endAt: typeof state.endAt === 'number' ? state.endAt : 0,
|
|
117
|
+
reason: typeof state.reason === 'string' && state.reason !== '' ? state.reason : null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 持久化折叠状态(纯 JSON,供投影缓存检查点)。 */
|
|
122
|
+
export const lastTurnEndStateSchema = z.object({
|
|
123
|
+
endAt: z.number(),
|
|
124
|
+
reason: z.string().nullable()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/** 上线载荷。 */
|
|
128
|
+
export const lastTurnEndViewSchema = z.object({
|
|
129
|
+
endAt: z.number(),
|
|
130
|
+
reason: z.string().nullable()
|
|
131
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowwo/dsh-icon-custom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|