dsh-session-autarchive 0.1.4 → 0.1.6
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.en.md +3 -2
- package/README.md +3 -2
- package/index.js +83 -71
- package/lib/client.js +135 -1
- package/package.json +3 -5
- package/scripts/smoke.mjs +96 -5
- package/src/entry.jsx +147 -7
- package/invariant.js +0 -10
package/README.en.md
CHANGED
|
@@ -11,6 +11,7 @@ Archives sessions that have gone unused for longer than a configurable number of
|
|
|
11
11
|
| Where | What it looks like | What it does |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| Session context menu | One row, "Do not auto-archive", next to Pin / Rename / Fork / Archive | Click once to mark, again to unmark |
|
|
14
|
+
| Session row trailing | On hover, a shield button next to the archive icon (hollow when unmarked, filled when marked) | Toggle "do not auto-archive" without opening the overflow menu |
|
|
14
15
|
| Session row start | A small shield on marked sessions | Shows at a glance that the session is protected |
|
|
15
16
|
| Hover card | One line, "Not auto-archived" | Same, with text |
|
|
16
17
|
| General settings | A row at the bottom, "Auto-archive after (days)" | Saved on blur or Enter, writes the same host field |
|
|
@@ -70,7 +71,7 @@ Writes are temp-file-then-rename, and reads retry — a half-written file is nev
|
|
|
70
71
|
## How it works
|
|
71
72
|
|
|
72
73
|
- The host half registers four local routes: `GET /dsh-session-autarchive/api/state`, `POST .../protect`, `POST .../config`, `POST .../run`. Loopback addresses only (`127.*`, `::1`, `::ffff:127.*`); anything else gets 403, a wrong method gets 405.
|
|
73
|
-
- The browser half mounts
|
|
74
|
+
- The browser half mounts five slots (menu row, row action, row start, hover card, settings row). Services are probed with `ctx.get`; when one is missing that entry is skipped and the others still mount.
|
|
74
75
|
- No network access, nothing uploaded. Decisions use local file times only.
|
|
75
76
|
|
|
76
77
|
## Known limits
|
|
@@ -85,7 +86,7 @@ Writes are temp-file-then-rename, and reads retry — a half-written file is nev
|
|
|
85
86
|
The source has two halves:
|
|
86
87
|
|
|
87
88
|
- `index.js` — host side (decision, archiving, four routes, the tool)
|
|
88
|
-
- `src/entry.jsx` — browser side (menu row, row shield, hover line, settings row, the 5-minute fast path)
|
|
89
|
+
- `src/entry.jsx` — browser side (menu row, row action shield, row shield, hover line, settings row, the 5-minute fast path)
|
|
89
90
|
|
|
90
91
|
Editing `src/entry.jsx` requires rebuilding. `lib/client.js` is a build artifact committed to the repository; without it the UI does not load:
|
|
91
92
|
|
package/README.md
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
把长期没动过的会话自动归档。按会话内最后一条对话结束的时间算天数,默认 5 天;已归档、置顶(可关)、标记「不自动归档」的都跳过。侧栏会话右键菜单给单个会话加标记,通用设置里改天数。
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 界面上的四处
|
|
10
10
|
|
|
11
11
|
| 位置 | 样子 | 作用 |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| 会话右键菜单 | 与「置顶会话/重命名/分叉会话/归档会话」并列的一行「不自动归档」 | 点一次标记,再点一次取消 |
|
|
14
|
+
| 会话行尾 | 悬停时归档图标旁边一个盾牌按钮(未标记空心盾、已标记实心盾) | 不用点开「更多」,直接开/关「不自动归档」 |
|
|
14
15
|
| 会话行首 | 被标记的会话多一个小盾牌 | 一眼看出这条不会被自动归档 |
|
|
15
16
|
| 悬停卡片 | 一行「不会被自动归档」 | 同上,带文字说明 |
|
|
16
17
|
| 通用设置 | 最底一行「自动归档天数」 | 失焦或回车即保存,写的是宿主同一个字段 |
|
|
@@ -85,7 +86,7 @@ dsh plugin --profile desktop add dsh-session-autarchive
|
|
|
85
86
|
源码分两半:
|
|
86
87
|
|
|
87
88
|
- `index.js` —— 宿主半边(判定、归档、四条路由、工具)
|
|
88
|
-
- `src/entry.jsx` ——
|
|
89
|
+
- `src/entry.jsx` —— 浏览器半边(菜单行、行尾盾牌按钮、行首盾牌、悬停说明、设置行、5 分钟快通道)
|
|
89
90
|
|
|
90
91
|
改过 `src/entry.jsx` 必须重新打包。`lib/client.js` 是提交进仓库的构建产物,缺了它界面不会加载:
|
|
91
92
|
|
package/index.js
CHANGED
|
@@ -549,87 +549,99 @@ function registerRoutes(ctx) {
|
|
|
549
549
|
|
|
550
550
|
/** 注册工具与路由。 */
|
|
551
551
|
export function apply(ctx) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
sessionId: { type: 'string', description: 'protect/unprotect 用:会话 id,形如 session-xxxxxxxx-…' },
|
|
560
|
-
dryRun: { type: 'boolean', description: 'run 用:true 时只列候选、不动手' },
|
|
561
|
-
days: { type: 'number', description: 'config 用:多少天没动就归档,默认 5' },
|
|
562
|
-
enabled: { type: 'boolean', description: 'config 用:是否启用自动归档' },
|
|
563
|
-
exemptPinned: { type: 'boolean', description: 'config 用:置顶会话是否也豁免(默认豁免)' },
|
|
564
|
-
},
|
|
565
|
-
required: [],
|
|
566
|
-
additionalProperties: false,
|
|
567
|
-
},
|
|
568
|
-
output: {
|
|
569
|
-
schema: {
|
|
552
|
+
// 工具注册失败(重名、DSH 升级后格式漂移)不该拖垮路由与兜底定时:
|
|
553
|
+
// 单独圈起来,失败只留一行警告,归档主功能照常。
|
|
554
|
+
try {
|
|
555
|
+
ctx.tools.register({
|
|
556
|
+
name: 'dsh-session-autarchive',
|
|
557
|
+
description: '超期会话自动归档的开关台:status 看配置、保护名单与当前候选;run 立刻归档候选;protect/unprotect 标记单个会话「不自动归档」;config 改阈值与开关。判定口径=会话内最后一条对话结束事件(turn/end)的时间——只是被打开/恢复过(会追写开场提示)不算活动;读不到事件时退回文件最后写入时间。已归档、保护名单内、置顶(可关)都跳过。浏览器半边每 5 分钟按同一份清单自动执行,侧栏右键菜单的「不自动归档」也写这份名单。',
|
|
558
|
+
parameters: {
|
|
570
559
|
type: 'object',
|
|
571
|
-
properties: {
|
|
572
|
-
|
|
560
|
+
properties: {
|
|
561
|
+
action: { type: 'string', description: '要做什么:status(默认,看状态)|run(立刻归档候选)|protect(标记不自动归档)|unprotect(取消标记)|config(改阈值/开关)' },
|
|
562
|
+
sessionId: { type: 'string', description: 'protect/unprotect 用:会话 id,形如 session-xxxxxxxx-…' },
|
|
563
|
+
dryRun: { type: 'boolean', description: 'run 用:true 时只列候选、不动手' },
|
|
564
|
+
days: { type: 'number', description: 'config 用:多少天没动就归档,默认 5' },
|
|
565
|
+
enabled: { type: 'boolean', description: 'config 用:是否启用自动归档' },
|
|
566
|
+
exemptPinned: { type: 'boolean', description: 'config 用:置顶会话是否也豁免(默认豁免)' },
|
|
567
|
+
},
|
|
568
|
+
required: [],
|
|
573
569
|
additionalProperties: false,
|
|
574
570
|
},
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const lines = [head]
|
|
591
|
-
for (const id of result.archived) lines.push(' ✓ ' + id.slice(8, 16))
|
|
592
|
-
for (const line of result.failed) lines.push(' ✗ ' + line)
|
|
593
|
-
if (args && args.dryRun === true) {
|
|
594
|
-
for (const c of result.candidates) lines.push(' · ' + c.id.slice(8, 16) + ' 最后对话 ' + stamp(c.last))
|
|
571
|
+
output: {
|
|
572
|
+
schema: {
|
|
573
|
+
type: 'object',
|
|
574
|
+
properties: { ok: { type: 'boolean' }, text: { type: 'string' } },
|
|
575
|
+
required: ['ok', 'text'],
|
|
576
|
+
additionalProperties: false,
|
|
577
|
+
},
|
|
578
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
579
|
+
},
|
|
580
|
+
async execute(args) {
|
|
581
|
+
const action = String((args && args.action) || 'status')
|
|
582
|
+
let state = readState()
|
|
583
|
+
|
|
584
|
+
if (action === 'status') {
|
|
585
|
+
return { ok: true, text: statusText(ctx, state) }
|
|
595
586
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
ok: true,
|
|
610
|
-
text: (action === 'protect' ? '已标记不自动归档:' : '已取消标记:') + sessionId
|
|
611
|
-
+ '\n保护名单现有 ' + state.protected.length + ' 个',
|
|
587
|
+
|
|
588
|
+
if (action === 'run') {
|
|
589
|
+
const result = await runArchive(ctx, state, args && args.dryRun === true)
|
|
590
|
+
const head = args && args.dryRun === true
|
|
591
|
+
? '干跑:候选 ' + result.candidates.length + ' 个,未动手'
|
|
592
|
+
: '已归档 ' + result.archived.length + ' 个' + (result.failed.length > 0 ? ',失败 ' + result.failed.length + ' 个' : '')
|
|
593
|
+
const lines = [head]
|
|
594
|
+
for (const id of result.archived) lines.push(' ✓ ' + id.slice(8, 16))
|
|
595
|
+
for (const line of result.failed) lines.push(' ✗ ' + line)
|
|
596
|
+
if (args && args.dryRun === true) {
|
|
597
|
+
for (const c of result.candidates) lines.push(' · ' + c.id.slice(8, 16) + ' 最后对话 ' + stamp(c.last))
|
|
598
|
+
}
|
|
599
|
+
return { ok: true, text: lines.join('\n') }
|
|
612
600
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
601
|
+
|
|
602
|
+
if (action === 'protect' || action === 'unprotect') {
|
|
603
|
+
const sessionId = String((args && args.sessionId) || '')
|
|
604
|
+
if (!/^session-/.test(sessionId)) return { ok: false, text: 'protect/unprotect 需要 sessionId(形如 session-xxxxxxxx-…)' }
|
|
605
|
+
const keep = new Set(state.protected)
|
|
606
|
+
if (action === 'protect') keep.add(sessionId)
|
|
607
|
+
else keep.delete(sessionId)
|
|
608
|
+
state = { ...state, protected: [...keep] }
|
|
609
|
+
writeProtected(state.protected)
|
|
610
|
+
audit((action === 'protect' ? 'protect ' : 'unprotect ') + sessionId + ',名单现有 ' + state.protected.length + ' 个')
|
|
611
|
+
return {
|
|
612
|
+
ok: true,
|
|
613
|
+
text: (action === 'protect' ? '已标记不自动归档:' : '已取消标记:') + sessionId
|
|
614
|
+
+ '\n保护名单现有 ' + state.protected.length + ' 个',
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (action === 'config') {
|
|
619
|
+
const next = { ...state }
|
|
620
|
+
if (args.days !== undefined) next.days = Number(args.days) > 0 ? Number(args.days) : next.days
|
|
621
|
+
if (args.enabled !== undefined) next.enabled = args.enabled === true
|
|
622
|
+
if (args.exemptPinned !== undefined) next.exemptPinned = args.exemptPinned === true
|
|
623
|
+
writeState(next)
|
|
624
|
+
return { ok: true, text: '已更新:' + describe(next) }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return { ok: false, text: '不认识的 action:' + action + '(可用 status/run/protect/unprotect/config)' }
|
|
628
|
+
},
|
|
629
|
+
})
|
|
630
|
+
} catch (error) {
|
|
631
|
+
console.warn('[dsh-session-autarchive] 工具未注册:' + (error && error.message ? error.message : error))
|
|
632
|
+
}
|
|
627
633
|
|
|
628
634
|
registerRoutes(ctx)
|
|
629
635
|
|
|
630
636
|
// 宿主侧兜底:浏览器窗口没开着也照常归档(客户端那条是让侧栏当场刷新的快通道)。
|
|
631
637
|
// 启动 60 秒后第一次(避开启动竞争),之后每 10 分钟。
|
|
632
|
-
|
|
638
|
+
// 开关每次现读:关掉自动归档后这条定时链要立刻停,不能等重启。
|
|
639
|
+
// 手动执行(工具/路由的 run)不在这里拦,那是用户主动点的,开关只管自动。
|
|
640
|
+
const tick = () => {
|
|
641
|
+
const state = readState()
|
|
642
|
+
if (!state.enabled) return
|
|
643
|
+
void runArchive(ctx, state, false).catch(() => { /* 这轮不行,下一轮再来 */ })
|
|
644
|
+
}
|
|
633
645
|
const first = setTimeout(tick, 60 * 1000)
|
|
634
646
|
const timer = setInterval(tick, 10 * 60 * 1000)
|
|
635
647
|
if (typeof ctx.effect === 'function') {
|
package/lib/client.js
CHANGED
|
@@ -26,9 +26,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
26
26
|
var entry_exports = {};
|
|
27
27
|
__export(entry_exports, {
|
|
28
28
|
AutoArchiveRow: () => AutoArchiveRow,
|
|
29
|
+
AutoArchiveToggleRow: () => AutoArchiveToggleRow,
|
|
29
30
|
ProtectHover: () => ProtectHover,
|
|
30
31
|
ProtectLeading: () => ProtectLeading,
|
|
31
32
|
ProtectMenuItem: () => ProtectMenuItem,
|
|
33
|
+
ProtectRowAction: () => ProtectRowAction,
|
|
32
34
|
apply: () => apply,
|
|
33
35
|
inject: () => inject
|
|
34
36
|
});
|
|
@@ -139,6 +141,35 @@ function ShieldIcon({ size = 14 }) {
|
|
|
139
141
|
}
|
|
140
142
|
);
|
|
141
143
|
}
|
|
144
|
+
function ShieldFilledIcon({ size = 14 }) {
|
|
145
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
146
|
+
"svg",
|
|
147
|
+
{
|
|
148
|
+
width: size,
|
|
149
|
+
height: size,
|
|
150
|
+
viewBox: "0 0 24 24",
|
|
151
|
+
"aria-hidden": "true",
|
|
152
|
+
focusable: "false",
|
|
153
|
+
fill: "currentColor",
|
|
154
|
+
stroke: "none",
|
|
155
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M12 3l7 2.6v5.2c0 4.3-3 8.2-7 9.4-4-1.2-7-5.1-7-9.4V5.6L12 3z" })
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
var ROW_ACTION = {
|
|
160
|
+
flex: "none",
|
|
161
|
+
display: "inline-flex",
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
justifyContent: "center",
|
|
164
|
+
width: "16px",
|
|
165
|
+
height: "16px",
|
|
166
|
+
border: "none",
|
|
167
|
+
borderRadius: "var(--dsw-radius-xs)",
|
|
168
|
+
padding: 0,
|
|
169
|
+
background: "transparent",
|
|
170
|
+
cursor: "pointer",
|
|
171
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
172
|
+
};
|
|
142
173
|
var ITEM = {
|
|
143
174
|
display: "flex",
|
|
144
175
|
alignItems: "center",
|
|
@@ -202,6 +233,39 @@ function ProtectMenuItem(props) {
|
|
|
202
233
|
}
|
|
203
234
|
);
|
|
204
235
|
}
|
|
236
|
+
function ProtectRowAction(props) {
|
|
237
|
+
const { sessionId } = props;
|
|
238
|
+
const state = useStore();
|
|
239
|
+
const [hover, setHover] = (0, import_react.useState)(false);
|
|
240
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
241
|
+
const on = state.protected.has(sessionId);
|
|
242
|
+
const label = on ? "\u53D6\u6D88\u300C\u4E0D\u81EA\u52A8\u5F52\u6863\u300D" : "\u4E0D\u81EA\u52A8\u5F52\u6863";
|
|
243
|
+
const click = () => {
|
|
244
|
+
if (busy) return;
|
|
245
|
+
setBusy(true);
|
|
246
|
+
void toggleProtect(sessionId, !on).catch(() => {
|
|
247
|
+
}).finally(() => setBusy(false));
|
|
248
|
+
};
|
|
249
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
250
|
+
"button",
|
|
251
|
+
{
|
|
252
|
+
type: "button",
|
|
253
|
+
title: label,
|
|
254
|
+
"aria-label": label,
|
|
255
|
+
"aria-pressed": on,
|
|
256
|
+
disabled: busy,
|
|
257
|
+
onClick: click,
|
|
258
|
+
onMouseEnter: () => setHover(true),
|
|
259
|
+
onMouseLeave: () => setHover(false),
|
|
260
|
+
style: {
|
|
261
|
+
...ROW_ACTION,
|
|
262
|
+
color: hover && !busy ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)",
|
|
263
|
+
opacity: busy ? 0.4 : 1
|
|
264
|
+
},
|
|
265
|
+
children: on ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShieldFilledIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ShieldIcon, {})
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
205
269
|
function ProtectLeading(props) {
|
|
206
270
|
const state = useStore();
|
|
207
271
|
if (!state.protected.has(props.sessionId)) return null;
|
|
@@ -223,6 +287,70 @@ var ROW_CSS = `
|
|
|
223
287
|
.zsa-unit { font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
224
288
|
.zsa-error { font-size:12px; line-height:18px; color:var(--dsw-alias-state-error-primary); }
|
|
225
289
|
`;
|
|
290
|
+
function Switch({ on, disabled, onChange }) {
|
|
291
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
292
|
+
"button",
|
|
293
|
+
{
|
|
294
|
+
type: "button",
|
|
295
|
+
role: "switch",
|
|
296
|
+
"aria-checked": on,
|
|
297
|
+
"aria-label": on ? "\u5173\u95ED\u81EA\u52A8\u5F52\u6863" : "\u5F00\u542F\u81EA\u52A8\u5F52\u6863",
|
|
298
|
+
disabled,
|
|
299
|
+
onClick: () => onChange(!on),
|
|
300
|
+
style: {
|
|
301
|
+
width: 36,
|
|
302
|
+
height: 20,
|
|
303
|
+
flex: "none",
|
|
304
|
+
border: 0,
|
|
305
|
+
borderRadius: 10,
|
|
306
|
+
padding: 0,
|
|
307
|
+
cursor: disabled ? "default" : "pointer",
|
|
308
|
+
position: "relative",
|
|
309
|
+
background: on ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-border-l1)",
|
|
310
|
+
opacity: disabled ? 0.5 : 1,
|
|
311
|
+
transition: "background .15s"
|
|
312
|
+
},
|
|
313
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
|
|
314
|
+
position: "absolute",
|
|
315
|
+
top: 2,
|
|
316
|
+
left: on ? 18 : 2,
|
|
317
|
+
width: 16,
|
|
318
|
+
height: 16,
|
|
319
|
+
borderRadius: 8,
|
|
320
|
+
background: "#fff",
|
|
321
|
+
transition: "left .15s"
|
|
322
|
+
} })
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
function AutoArchiveToggleRow() {
|
|
327
|
+
const state = useStore();
|
|
328
|
+
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
329
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
330
|
+
const on = state.enabled !== false;
|
|
331
|
+
const toggle = async (next) => {
|
|
332
|
+
if (busy) return;
|
|
333
|
+
setBusy(true);
|
|
334
|
+
setError(null);
|
|
335
|
+
try {
|
|
336
|
+
const data = await call("POST", ROUTE + "/config", { enabled: next });
|
|
337
|
+
patchStore({ enabled: data.enabled !== false });
|
|
338
|
+
} catch (err) {
|
|
339
|
+
setError("\u6CA1\u4FDD\u5B58\u4E0A\uFF1A" + String(err && err.message || err));
|
|
340
|
+
} finally {
|
|
341
|
+
setBusy(false);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "zsa-item", children: [
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: ROW_CSS }),
|
|
346
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "zsa-text", children: [
|
|
347
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zsa-title", children: "\u81EA\u52A8\u5F52\u6863" }),
|
|
348
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zsa-desc", children: "\u5173\u95ED\u540E\u540E\u53F0\u4E0D\u518D\u81EA\u52A8\u5F52\u6863\uFF1B\u624B\u52A8\u6267\u884C\u4ECD\u7136\u53EF\u7528\u3002\u5F53\u524D\u5019\u9009 " + state.candidates.length + " \u4E2A\u3002" }),
|
|
349
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zsa-error", role: "alert", children: error }) : null
|
|
350
|
+
] }),
|
|
351
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "zsa-field", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, { on, disabled: busy, onChange: (next) => void toggle(next) }) })
|
|
352
|
+
] });
|
|
353
|
+
}
|
|
226
354
|
function AutoArchiveRow() {
|
|
227
355
|
const state = useStore();
|
|
228
356
|
const [draft, setDraft] = (0, import_react.useState)(null);
|
|
@@ -277,7 +405,7 @@ function AutoArchiveRow() {
|
|
|
277
405
|
max: 3650,
|
|
278
406
|
step: 1,
|
|
279
407
|
value,
|
|
280
|
-
disabled: busy,
|
|
408
|
+
disabled: busy || state.enabled === false,
|
|
281
409
|
"aria-label": "\u81EA\u52A8\u5F52\u6863\u5929\u6570",
|
|
282
410
|
onChange: (event) => {
|
|
283
411
|
setDraft(event.target.value);
|
|
@@ -315,9 +443,15 @@ function apply(ctx) {
|
|
|
315
443
|
} catch {
|
|
316
444
|
}
|
|
317
445
|
place(ctx, "sidebar.workspaces.session.menu.item", 450, ProtectMenuItem);
|
|
446
|
+
place(ctx, "sidebar.workspaces.session.row.action", 150, ProtectRowAction);
|
|
318
447
|
place(ctx, "sidebar.session.row.leading", 100, ProtectLeading);
|
|
319
448
|
place(ctx, "sidebar.session.row.hover", 100, ProtectHover);
|
|
320
449
|
try {
|
|
450
|
+
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
451
|
+
name: "settings.general.item",
|
|
452
|
+
id: "session-autarchive-enabled",
|
|
453
|
+
order: 94
|
|
454
|
+
}, AutoArchiveToggleRow));
|
|
321
455
|
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
322
456
|
name: "settings.general.item",
|
|
323
457
|
id: "session-autarchive-days",
|
package/package.json
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-autarchive",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "把长期没动过的会话自动归档(默认 5 天),单个会话可以标成不归档。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
|
-
"./invariant": "./invariant.js",
|
|
10
9
|
"./client": "./lib/client.js",
|
|
11
10
|
"./package.json": "./package.json"
|
|
12
11
|
},
|
|
13
12
|
"files": [
|
|
14
13
|
"index.js",
|
|
15
|
-
"invariant.js",
|
|
16
14
|
"cordis.patch.yml",
|
|
17
15
|
"src",
|
|
18
16
|
"lib/client.js",
|
|
@@ -24,7 +22,7 @@
|
|
|
24
22
|
],
|
|
25
23
|
"engines": {
|
|
26
24
|
"node": "^22.19.0 || >=24.0.0",
|
|
27
|
-
"dsh": ">=0.2.1-
|
|
25
|
+
"dsh": ">=0.2.1-alpha.3"
|
|
28
26
|
},
|
|
29
27
|
"dsh": {
|
|
30
28
|
"client": {
|
|
@@ -48,7 +46,7 @@
|
|
|
48
46
|
"build:client": "node scripts/build-client.mjs",
|
|
49
47
|
"check:client": "node scripts/build-client.mjs --check",
|
|
50
48
|
"smoke": "node scripts/smoke.mjs",
|
|
51
|
-
"check": "node --check index.js
|
|
49
|
+
"check": "node --check index.js"
|
|
52
50
|
},
|
|
53
51
|
"devDependencies": {
|
|
54
52
|
"esbuild": "^0.28.1"
|
package/scripts/smoke.mjs
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 本机冒烟:不起应用、不发网络请求、不碰真实会话数据。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 七关:
|
|
6
6
|
* 1. 包结构 —— files 声明的必需文件都在(含已提交的 lib/client.js)
|
|
7
7
|
* 2. 组合声明 —— dsh.bundle.patch 指向的文件在,且里面的 insert id 等于包名
|
|
8
8
|
* 3. 宿主半边 —— 桩 ctx 跑 apply:注册 1 个工具、4 条路由,路径与包名一致
|
|
9
9
|
* 4. 路由鉴权 —— 非回环 403、非 POST 405、坏会话 id 400
|
|
10
10
|
* 5. 状态读写 —— 指向临时 DSH_HOME:改天数、加名单后回读一致
|
|
11
|
-
* 6.
|
|
11
|
+
* 6. 开关生效 —— enabled=false 时兜底定时不归档,手动 run 仍归档
|
|
12
|
+
* 7. 客户端产物 —— 外壳 id == 包名,产物里带上了路由与四个 slot 名
|
|
12
13
|
*
|
|
13
14
|
* 用法:node scripts/smoke.mjs (任一条失败退出码 1)
|
|
14
15
|
*
|
|
15
16
|
* ⚠️ 全程使用临时 DSH_HOME,跑完删掉;不会读到或改到你自己的会话数据。
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
|
-
import { readFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs'
|
|
19
|
+
import { readFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
|
|
19
20
|
import { tmpdir } from 'node:os'
|
|
20
21
|
import { dirname, join, resolve } from 'node:path'
|
|
21
22
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
@@ -33,7 +34,6 @@ const fail = (label, why) => {
|
|
|
33
34
|
/** 1. 包结构 */
|
|
34
35
|
const REQUIRED = [
|
|
35
36
|
'index.js',
|
|
36
|
-
'invariant.js',
|
|
37
37
|
'cordis.patch.yml',
|
|
38
38
|
'src/entry.jsx',
|
|
39
39
|
'lib/client.js',
|
|
@@ -184,11 +184,79 @@ if (routes.length === wanted.length) {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/** 6.
|
|
187
|
+
/** 6. 开关生效:enabled=false 时兜底定时不归档;手动 run 不受限 */
|
|
188
|
+
{
|
|
189
|
+
// 造一个 30 天前的会话目录(档里没有 turn/end,判定退回文件写入时间)
|
|
190
|
+
const wsDir = join(home, 'sessions', 'ws-smoke')
|
|
191
|
+
const sessDir = join(wsDir, 'session-smoke-old')
|
|
192
|
+
mkdirSync(sessDir, { recursive: true })
|
|
193
|
+
const oldFile = join(sessDir, 'session.v4.jsonl.zstd')
|
|
194
|
+
writeFileSync(oldFile, '')
|
|
195
|
+
const oldStamp = new Date(Date.now() - 30 * 86400000)
|
|
196
|
+
utimesSync(oldFile, oldStamp, oldStamp)
|
|
197
|
+
utimesSync(sessDir, oldStamp, oldStamp)
|
|
198
|
+
|
|
199
|
+
// 用一份新的桩 ctx 跑 apply,并把 60 秒的首次定时立即点一次
|
|
200
|
+
const tickOnce = async (enabled) => {
|
|
201
|
+
await call(routeOf('/config'), { method: 'POST', body: { enabled } })
|
|
202
|
+
const archived = []
|
|
203
|
+
const registry = {
|
|
204
|
+
archivedSessionIds: [],
|
|
205
|
+
pinnedSessionIds: [],
|
|
206
|
+
list: () => [{ sessionIds: ['session-smoke-old'] }],
|
|
207
|
+
archiveSession: async (id) => { archived.push(id) },
|
|
208
|
+
}
|
|
209
|
+
const hostTools = []
|
|
210
|
+
const hostCtx = {
|
|
211
|
+
tools: { register: (tool) => { hostTools.push(tool) } },
|
|
212
|
+
get: (name) => (name === 'webServer' ? webServer : name === 'workspaceRegistry' ? registry : undefined),
|
|
213
|
+
effect: (fn) => {
|
|
214
|
+
const cleanup = fn()
|
|
215
|
+
if (typeof cleanup === 'function') cleanups.push(cleanup)
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
const realSetTimeout = globalThis.setTimeout
|
|
219
|
+
globalThis.setTimeout = (fn, ms) => {
|
|
220
|
+
if (ms === 60 * 1000) { fn(); return { unref() {} } }
|
|
221
|
+
return realSetTimeout(fn, ms)
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
mod.apply(hostCtx)
|
|
225
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
226
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
227
|
+
} finally {
|
|
228
|
+
globalThis.setTimeout = realSetTimeout
|
|
229
|
+
}
|
|
230
|
+
return { archived, tools: hostTools }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const off = await tickOnce(false)
|
|
234
|
+
// 先记下定时链这一轮的归档数:手动 run 会往同一个数组里记,不能等到 run 之后再判
|
|
235
|
+
const tickArchivedWhileOff = off.archived.length
|
|
236
|
+
const manualTool = off.tools[0]
|
|
237
|
+
let manualArchived = false
|
|
238
|
+
if (manualTool) {
|
|
239
|
+
const result = await manualTool.execute({ action: 'run' })
|
|
240
|
+
manualArchived = result && result.ok === true && /已归档 1 个/.test(result.text)
|
|
241
|
+
}
|
|
242
|
+
const on = await tickOnce(true)
|
|
243
|
+
if (tickArchivedWhileOff !== 0) {
|
|
244
|
+
fail('开关生效', 'enabled=false 时兜底定时仍归档了 ' + tickArchivedWhileOff + ' 个')
|
|
245
|
+
} else if (!manualArchived) {
|
|
246
|
+
fail('开关生效', 'enabled=false 时手动 run 没有归档候选')
|
|
247
|
+
} else if (on.archived.length !== 1) {
|
|
248
|
+
fail('开关生效', 'enabled=true 时兜底定时归档 ' + on.archived.length + ' 个,应为 1')
|
|
249
|
+
} else {
|
|
250
|
+
ok('开关生效', '关闭时定时不归档、手动照常、打开后恢复')
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 7. 客户端产物 */
|
|
188
255
|
const client = readFileSync(join(root, 'lib', 'client.js'), 'utf8')
|
|
189
256
|
const idMatch = /^\s*id:\s*"([^"]+)"/m.exec(client)
|
|
190
257
|
const SLOTS = [
|
|
191
258
|
'sidebar.workspaces.session.menu.item',
|
|
259
|
+
'sidebar.workspaces.session.row.action',
|
|
192
260
|
'sidebar.session.row.leading',
|
|
193
261
|
'sidebar.session.row.hover',
|
|
194
262
|
'settings.general.item',
|
|
@@ -199,9 +267,32 @@ else if (!client.includes(pkg.name + '/api')) fail('客户端产物', '产物里
|
|
|
199
267
|
else {
|
|
200
268
|
const absent = SLOTS.filter((slot) => !client.includes(slot))
|
|
201
269
|
if (absent.length > 0) fail('客户端产物', '产物里缺 slot:' + absent.join('、'))
|
|
270
|
+
else if (!client.includes('session-autarchive-enabled')) fail('客户端产物', '产物里没有设置开关行(源码改了但没重新构建)')
|
|
202
271
|
else ok('客户端产物', 'id=' + pkg.name + ',' + Buffer.byteLength(client, 'utf8') + ' 字节')
|
|
203
272
|
}
|
|
204
273
|
|
|
274
|
+
/** 8. 失败隔离:工具注册抛错时,路由与定时器照常 */
|
|
275
|
+
{
|
|
276
|
+
const routesAfter = []
|
|
277
|
+
let effects = 0
|
|
278
|
+
const hostWeb = { port: 0, register: (route) => { routesAfter.push(route); return () => {} } }
|
|
279
|
+
const throwingCtx = {
|
|
280
|
+
tools: { register: () => { throw new Error('simulated duplicate tool') } },
|
|
281
|
+
get: (name) => (name === 'webServer' ? hostWeb : undefined),
|
|
282
|
+
effect: (fn) => {
|
|
283
|
+
effects += 1
|
|
284
|
+
const cleanup = fn()
|
|
285
|
+
if (typeof cleanup === 'function') cleanups.push(cleanup)
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
let threw = null
|
|
289
|
+
try { mod.apply(throwingCtx) } catch (error) { threw = error }
|
|
290
|
+
if (threw) fail('失败隔离', '工具注册抛错后 apply 整体抛出:' + threw.message)
|
|
291
|
+
else if (routesAfter.length !== 4) fail('失败隔离', '工具注册抛错后注册了 ' + routesAfter.length + ' 条路由,应为 4')
|
|
292
|
+
else if (effects !== 5) fail('失败隔离', '工具注册抛错后 effect 登记 ' + effects + ' 个,应为 5(4 条路由 + 1 个定时器)')
|
|
293
|
+
else ok('失败隔离', '工具注册抛错,路由 4 条与定时器照常')
|
|
294
|
+
}
|
|
295
|
+
|
|
205
296
|
/** 收尾:清掉宿主半边的定时器与临时目录 */
|
|
206
297
|
for (const cleanup of cleanups) {
|
|
207
298
|
try {
|
package/src/entry.jsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 超期会话自动归档(浏览器半边源码)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 会话列表上四处:
|
|
5
5
|
* 1. 会话菜单里平铺一行「不自动归档」——与官方「置顶会话/重命名/分叉会话/归档会话」同层并列,不另造「更多」;
|
|
6
|
-
* 2.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 2. 行尾悬停图标组里、归档图标紧旁边一个盾牌按钮(order 150,落在归档 100 与置顶 200 之间),
|
|
7
|
+
* 未保护空心盾、已保护实心盾,点它开/关「不自动归档」——不用点开「更多」;
|
|
8
|
+
* 3. 被标记的会话行首一个小盾牌;
|
|
9
|
+
* 4. 悬停卡片里一行说明。
|
|
9
10
|
* 设置里一行:通用设置最底「自动归档天数」(`settings.general.item`),失焦或回车即落盘。
|
|
10
11
|
* 外加每 5 分钟拉一次宿主的候选清单:拿得到官方归档通道就顺手归档,拿不到就只刷新名单——
|
|
11
12
|
* 判定在宿主,宿主自己有兜底定时(窗口没开也照常归档)。
|
|
@@ -140,6 +141,28 @@ function ShieldIcon({ size = 14 }) {
|
|
|
140
141
|
)
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 「已标记」实心盾:与描边版同一个轮廓,换成填充,被保护的会话一眼可辨。
|
|
146
|
+
* @param props.size - 边长,默认 14。
|
|
147
|
+
* @returns 一个实心盾牌。
|
|
148
|
+
*/
|
|
149
|
+
function ShieldFilledIcon({ size = 14 }) {
|
|
150
|
+
return (
|
|
151
|
+
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" focusable="false"
|
|
152
|
+
fill="currentColor" stroke="none">
|
|
153
|
+
<path d="M12 3l7 2.6v5.2c0 4.3-3 8.2-7 9.4-4-1.2-7-5.1-7-9.4V5.6L12 3z" />
|
|
154
|
+
</svg>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 行尾图标按钮:数值照 Rows.module.css 的 `.iconButton`(16px 格、14px 字形、tertiary → hover primary)。 */
|
|
159
|
+
const ROW_ACTION = {
|
|
160
|
+
flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
161
|
+
width: '16px', height: '16px', border: 'none', borderRadius: 'var(--dsw-radius-xs)',
|
|
162
|
+
padding: 0, background: 'transparent', cursor: 'pointer',
|
|
163
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
/** 菜单行:照 ui-primitives Menu.module.css 的 `.item`。 */
|
|
144
167
|
const ITEM = {
|
|
145
168
|
display: 'flex', alignItems: 'center', gap: '6px', width: '100%', minHeight: '34px',
|
|
@@ -201,6 +224,48 @@ export function ProtectMenuItem(props) {
|
|
|
201
224
|
)
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
/**
|
|
228
|
+
* 行尾悬停按钮(order 150,紧挨归档图标):盾牌一档,点它开/关「不自动归档」。
|
|
229
|
+
* 未保护=空心盾,已保护=实心盾;标题与 aria-label 写明当前状态要发生什么。
|
|
230
|
+
* 与官方置顶/归档按钮同一排同一套样式;点击不打开会话行(宿主 rowActions 已截住冒泡)。
|
|
231
|
+
* @param props - 行身份。
|
|
232
|
+
* @returns 一个图标按钮。
|
|
233
|
+
*/
|
|
234
|
+
export function ProtectRowAction(props) {
|
|
235
|
+
const { sessionId } = props
|
|
236
|
+
const state = useStore()
|
|
237
|
+
const [hover, setHover] = useState(false)
|
|
238
|
+
const [busy, setBusy] = useState(false)
|
|
239
|
+
const on = state.protected.has(sessionId)
|
|
240
|
+
const label = on ? '取消「不自动归档」' : '不自动归档'
|
|
241
|
+
const click = () => {
|
|
242
|
+
if (busy) return
|
|
243
|
+
setBusy(true)
|
|
244
|
+
void toggleProtect(sessionId, !on)
|
|
245
|
+
.catch(() => { /* 路由没挂或写盘失败:不改本地状态,界面保持原样 */ })
|
|
246
|
+
.finally(() => setBusy(false))
|
|
247
|
+
}
|
|
248
|
+
return (
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
title={label}
|
|
252
|
+
aria-label={label}
|
|
253
|
+
aria-pressed={on}
|
|
254
|
+
disabled={busy}
|
|
255
|
+
onClick={click}
|
|
256
|
+
onMouseEnter={() => setHover(true)}
|
|
257
|
+
onMouseLeave={() => setHover(false)}
|
|
258
|
+
style={{
|
|
259
|
+
...ROW_ACTION,
|
|
260
|
+
color: hover && !busy ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
261
|
+
opacity: busy ? 0.4 : 1,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
{on ? <ShieldFilledIcon /> : <ShieldIcon />}
|
|
265
|
+
</button>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
204
269
|
/**
|
|
205
270
|
* 会话行首标记:被保护的会话显示盾牌(该格只在行处于空闲态时才渲染)。
|
|
206
271
|
* @param props - 行身份。
|
|
@@ -242,6 +307,73 @@ const ROW_CSS = `
|
|
|
242
307
|
.zsa-error { font-size:12px; line-height:18px; color:var(--dsw-alias-state-error-primary); }
|
|
243
308
|
`
|
|
244
309
|
|
|
310
|
+
/** 开关:纯内联样式,不 import 宿主组件包。 */
|
|
311
|
+
function Switch({ on, disabled, onChange }) {
|
|
312
|
+
return (
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
role="switch"
|
|
316
|
+
aria-checked={on}
|
|
317
|
+
aria-label={on ? '关闭自动归档' : '开启自动归档'}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
onClick={() => onChange(!on)}
|
|
320
|
+
style={{
|
|
321
|
+
width: 36, height: 20, flex: 'none', border: 0, borderRadius: 10, padding: 0,
|
|
322
|
+
cursor: disabled ? 'default' : 'pointer', position: 'relative',
|
|
323
|
+
background: on ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)',
|
|
324
|
+
opacity: disabled ? 0.5 : 1, transition: 'background .15s',
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<span style={{
|
|
328
|
+
position: 'absolute', top: 2, left: on ? 18 : 2, width: 16, height: 16,
|
|
329
|
+
borderRadius: 8, background: '#fff', transition: 'left .15s',
|
|
330
|
+
}} />
|
|
331
|
+
</button>
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 通用设置里的一行:自动归档开关。
|
|
337
|
+
* 关掉后宿主定时链与浏览器侧都不再自动归档;手动 run 不受影响(宿主那边也这么判)。
|
|
338
|
+
* @returns 一行设置。
|
|
339
|
+
*/
|
|
340
|
+
export function AutoArchiveToggleRow() {
|
|
341
|
+
const state = useStore()
|
|
342
|
+
const [busy, setBusy] = useState(false)
|
|
343
|
+
const [error, setError] = useState(null)
|
|
344
|
+
const on = state.enabled !== false
|
|
345
|
+
|
|
346
|
+
const toggle = async (next) => {
|
|
347
|
+
if (busy) return
|
|
348
|
+
setBusy(true)
|
|
349
|
+
setError(null)
|
|
350
|
+
try {
|
|
351
|
+
const data = await call('POST', ROUTE + '/config', { enabled: next })
|
|
352
|
+
patchStore({ enabled: data.enabled !== false })
|
|
353
|
+
} catch (err) {
|
|
354
|
+
setError('没保存上:' + String((err && err.message) || err))
|
|
355
|
+
} finally {
|
|
356
|
+
setBusy(false)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className="zsa-item">
|
|
362
|
+
<style>{ROW_CSS}</style>
|
|
363
|
+
<div className="zsa-text">
|
|
364
|
+
<div className="zsa-title">自动归档</div>
|
|
365
|
+
<div className="zsa-desc">
|
|
366
|
+
{'关闭后后台不再自动归档;手动执行仍然可用。当前候选 ' + state.candidates.length + ' 个。'}
|
|
367
|
+
</div>
|
|
368
|
+
{error ? <div className="zsa-error" role="alert">{error}</div> : null}
|
|
369
|
+
</div>
|
|
370
|
+
<div className="zsa-field">
|
|
371
|
+
<Switch on={on} disabled={busy} onChange={(next) => void toggle(next)} />
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
245
377
|
/**
|
|
246
378
|
* 通用设置里的一行:自动归档天数。
|
|
247
379
|
* 失焦或回车即落盘(与设置里其它行一样即时生效),写的是宿主同一个 days 字段。
|
|
@@ -301,7 +433,7 @@ export function AutoArchiveRow() {
|
|
|
301
433
|
max={3650}
|
|
302
434
|
step={1}
|
|
303
435
|
value={value}
|
|
304
|
-
disabled={busy}
|
|
436
|
+
disabled={busy || state.enabled === false}
|
|
305
437
|
aria-label="自动归档天数"
|
|
306
438
|
onChange={(event) => { setDraft(event.target.value); setError(null) }}
|
|
307
439
|
onBlur={() => { void commit() }}
|
|
@@ -334,13 +466,21 @@ export function apply(ctx) {
|
|
|
334
466
|
} catch { /* 清理不了也只是多一个定时器 */ }
|
|
335
467
|
} catch { /* 见上 */ }
|
|
336
468
|
|
|
337
|
-
//
|
|
469
|
+
// 四处:菜单里并列一行 + 行尾悬停组里归档旁的盾牌按钮 + 行首标记 + 悬停说明。
|
|
470
|
+
// 行尾按钮 order 150:归档是 100、置顶是 200,插在两者之间,紧挨归档。
|
|
338
471
|
place(ctx, 'sidebar.workspaces.session.menu.item', 450, ProtectMenuItem)
|
|
472
|
+
place(ctx, 'sidebar.workspaces.session.row.action', 150, ProtectRowAction)
|
|
339
473
|
place(ctx, 'sidebar.session.row.leading', 100, ProtectLeading)
|
|
340
474
|
place(ctx, 'sidebar.session.row.hover', 100, ProtectHover)
|
|
341
475
|
|
|
342
|
-
//
|
|
476
|
+
// 通用设置里两行,排在「当前版本」上面(order 94/95,当前版本是 100):
|
|
477
|
+
// 「自动归档」开关与「自动归档天数」。先开关后天数,与页面从上到下的阅读顺序一致。
|
|
343
478
|
try {
|
|
479
|
+
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
|
|
480
|
+
name: 'settings.general.item',
|
|
481
|
+
id: 'session-autarchive-enabled',
|
|
482
|
+
order: 94,
|
|
483
|
+
}, AutoArchiveToggleRow))
|
|
344
484
|
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
|
|
345
485
|
name: 'settings.general.item',
|
|
346
486
|
id: 'session-autarchive-days',
|
package/invariant.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const PACKAGE_NAME = 'dsh-session-autarchive'
|
|
2
|
-
|
|
3
|
-
/** Cordis companion plugin name. */
|
|
4
|
-
export const name = 'dsh-session-autarchive-invariant'
|
|
5
|
-
/** Service required before the companion can reserve package ownership. */
|
|
6
|
-
export const inject = ['invariants']
|
|
7
|
-
|
|
8
|
-
const install = () => {}
|
|
9
|
-
|
|
10
|
-
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|