@duke-dsh-plugins/dsh-archive-manager 1.3.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/LICENSE +21 -0
- package/README.md +100 -0
- package/client.js +515 -0
- package/cordis.patch.yml +4 -0
- package/index.js +276 -0
- package/package.json +48 -0
- package/typert.host.js +224 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 duke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" color="#4D6BFE"><rect x="1.5" y="2" width="11" height="3.5" rx="0.8"/><path d="M2.5 5.5v5a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-5"/><path d="M5.5 8h3"/></svg>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h3 align="center">DeepSeek Harness 归档管理插件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/DSH-Plugin-4D6BFE?style=flat" alt="DSH plugin">
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat" alt="MIT License"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/Web%20UI-Yes-22C55E?style=flat" alt="Web UI">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><sub>中文</sub></p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
为 [DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) Web UI 打造的**归档管理**插件:把已归档的会话重新变得可见、可管理。
|
|
18
|
+
|
|
19
|
+
归档后消失在侧栏的会话,在这里统一呈现——**按项目分组、按更新时间倒序、支持搜索**,并且每个归档都能**一键还原**或**永久删除**。
|
|
20
|
+
|
|
21
|
+
## 功能
|
|
22
|
+
|
|
23
|
+
| 功能 | 说明 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 📂 按项目分组 | 归档会话按工作区(项目)分组,含"未分组"桶 |
|
|
26
|
+
| ⏱ 更新时间倒序 | 每个项目组内按最新更新时间从新到旧 |
|
|
27
|
+
| 🔍 搜索 | 按会话标题 / 项目名实时过滤 |
|
|
28
|
+
| ♻️ 还原 | 从归档集合移除,会话回到侧栏项目原位 |
|
|
29
|
+
| 🗑️ 删除 | 永久删除会话日志 + 移除归档记录;二次确认;运行中的智能体会拦截 |
|
|
30
|
+
| 🧹 无效记录清理 | "日志已不存在"的残留归档记录单独分组,可一键清理 |
|
|
31
|
+
| 👻 已删会话提示 | 已删除但仍驻留内存的会话自动隐藏,提示重启后彻底清除 |
|
|
32
|
+
| 🌗 主题适配 | 全部使用 DSH 设计 token,明暗主题自动跟随 |
|
|
33
|
+
| 🖥 跨平台 | Host 半只用 Node.js `fs` API,Windows / macOS / Linux 均可运行 |
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
### 标准安装(推荐)
|
|
38
|
+
|
|
39
|
+
本插件是**标准 DSH bundle**:`package.json` 声明 `dsh.bundle.patch`,包内自带 `cordis.patch.yml`,用官方 `dsh plugin` 命令安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# 本地开发:pnpm 软链到本仓库,改代码即生效(无需重新复制)
|
|
43
|
+
dsh plugin --profile web add /path/to/dsh-archive-manager
|
|
44
|
+
|
|
45
|
+
# 正式发布:从 GitHub Release tarball 安装
|
|
46
|
+
dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-archive-manager/releases/download/v1.3.0/dsh-archive-manager-1.3.0.tgz
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
重启 DSH 后,侧栏底部(Cordis Plugin 下方、设置上方)会出现"归档"按钮。
|
|
50
|
+
|
|
51
|
+
> `dsh plugin add` 把插件装成 profile 的 npm 依赖并追加到 `dsh.profile.bundles`,启动时 DSH 自动应用包内的 `cordis.patch.yml` 挂载插件。卸载:`dsh plugin --profile web remove dsh-archive-manager`。
|
|
52
|
+
|
|
53
|
+
## 使用
|
|
54
|
+
|
|
55
|
+
1. 点侧栏底部的 **🗂 归档** 按钮(带归档数量)。
|
|
56
|
+
2. 面板按项目列出所有归档会话:
|
|
57
|
+
- **还原**:会话回到侧栏对应项目下。
|
|
58
|
+
- **删除**:点一下变"确认删除?",再点一次才真正删除(永久,不可恢复)。
|
|
59
|
+
- **无效的归档记录**:日志已不存在的残留,点删除清理记录。
|
|
60
|
+
3. 顶部搜索框可实时过滤。
|
|
61
|
+
|
|
62
|
+
## 工作原理
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
DSH Web UI
|
|
66
|
+
└─ client.js (window.__ModuleLoader__.load bundle)
|
|
67
|
+
└─ ctx.remote.archiveManager.{restore|delete|state} ← Remote 调用
|
|
68
|
+
└─ index.js (ArchiveManagerService, TypertRemoteService)
|
|
69
|
+
├─ restore: workspaceRegistry.setState(移除归档) → 侧栏复原
|
|
70
|
+
├─ delete : 拒绝 running agent → fs.rm 删日志(跨平台)→ 移除归档
|
|
71
|
+
└─ state : 检测"live 但日志已删"的 ghost 会话
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 目录结构
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
dsh-archive-manager/
|
|
78
|
+
├── index.js # Host 半:ArchiveManagerService(Remote 服务)
|
|
79
|
+
├── client.js # Client 半:归档管理 UI bundle
|
|
80
|
+
├── typert.host.js # Typert Host manifest(Remote 方法描述)
|
|
81
|
+
├── cordis.patch.yml # dsh bundle patch(挂载行)
|
|
82
|
+
├── .github/workflows/ # GitHub Actions 发布
|
|
83
|
+
├── AGENTS.md # 面向 AI agent 的开发指南(含踩坑)
|
|
84
|
+
└── LICENSE # MIT
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 开发
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm run check # node --check index.js client.js typert.host.js
|
|
91
|
+
dsh plugin --profile web add /path/to/dsh-archive-manager # 安装/重装到本机 DSH profile
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
详见 [AGENTS.md](AGENTS.md)——记录了 DSH 正式插件(Host/Client/Typert 三件套)的完整机制和踩坑。
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
本项目遵循 [MIT License](LICENSE)。
|
|
99
|
+
|
|
100
|
+
> 本项目是基于 DeepSeek Harness 构建的社区插件,并非 DeepSeek 官方产品。
|
package/client.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-archive-manager — Client half (web bundle).
|
|
3
|
+
*
|
|
4
|
+
* Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Registers:
|
|
5
|
+
* - a sidebar footer action button (archived-session entry),
|
|
6
|
+
* - a frame-wide archive manager panel (search / group / restore / delete).
|
|
7
|
+
*
|
|
8
|
+
* The panel mirrors the DSH 工作区 (workspace) browsing region: a dialog
|
|
9
|
+
* surface using the same design tokens as the workspace browser — folder
|
|
10
|
+
* group headers, compact 32px session rows with a leading slot icon and
|
|
11
|
+
* relative time, and hover-revealed actions (workspace rowActions pattern).
|
|
12
|
+
*
|
|
13
|
+
* Host communication goes through the `archiveManager` Remote namespace
|
|
14
|
+
* (`ctx.remote.archiveManager.restore/delete/state`), published by the Host
|
|
15
|
+
* half in `index.js`.
|
|
16
|
+
*/
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: "dsh-archive-manager",
|
|
19
|
+
factory: (require) => {
|
|
20
|
+
var module = { exports: {} };
|
|
21
|
+
var exports = module.exports;
|
|
22
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
23
|
+
const React = require("react");
|
|
24
|
+
|
|
25
|
+
// ---- CSS (package-owned, mirrors the DSH workspace browser + Modal) ----
|
|
26
|
+
const CSS = `
|
|
27
|
+
[class*="footerActions"]{flex-direction:column;align-items:center;}
|
|
28
|
+
|
|
29
|
+
/* Overlay — same mask surface as DSH Modal */
|
|
30
|
+
.am-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.4));backdrop-filter:var(--dsw-mask-blur,blur(2px));display:flex;align-items:center;justify-content:center;padding:24px;}
|
|
31
|
+
|
|
32
|
+
/* Panel — same surface & size as the DSH Settings dialog (width 800px) */
|
|
33
|
+
.am-panel{position:relative;display:flex;flex-direction:column;width:800px;max-width:calc(100vw - 48px);height:min(800px,100vh - 48px);overflow:hidden;border:1px solid var(--dsw-alias-border-inverted,rgba(128,128,128,.3));border-radius:24px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-shadow-lv3,0 12px 40px rgba(0,0,0,.25));}
|
|
34
|
+
|
|
35
|
+
/* Header — DSH Modal header */
|
|
36
|
+
.am-header{display:flex;align-items:center;gap:10px;padding:18px 14px 10px 24px;}
|
|
37
|
+
.am-title{font-weight:500;font-size:16px;line-height:24px;flex:1;color:var(--dsw-alias-label-primary,inherit);}
|
|
38
|
+
.am-count{font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary,inherit);flex:none;}
|
|
39
|
+
.am-close{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:8px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary,inherit);padding:0;}
|
|
40
|
+
.am-close:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));color:var(--dsw-alias-label-primary,inherit);}
|
|
41
|
+
|
|
42
|
+
/* Search — workspace expanded search input */
|
|
43
|
+
.am-search{padding:0 24px 12px;}
|
|
44
|
+
.am-search input{box-sizing:border-box;width:100%;height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));border-radius:10px;background:transparent;color:var(--dsw-alias-label-primary,inherit);font-size:13px;line-height:18px;outline:none;}
|
|
45
|
+
.am-search input::placeholder{color:var(--dsw-alias-label-tertiary,inherit);}
|
|
46
|
+
.am-search input:focus{border-color:var(--dsw-alias-brand-primary,#4a7dff);}
|
|
47
|
+
|
|
48
|
+
/* Body — workspace list area */
|
|
49
|
+
.am-body{flex:1;overflow-y:auto;padding:4px 8px 16px;overscroll-behavior:contain;}
|
|
50
|
+
|
|
51
|
+
/* Group sections, spaced like workspace group sections */
|
|
52
|
+
.am-group{margin-top:2px;}
|
|
53
|
+
.am-group+.am-group{margin-top:4px;}
|
|
54
|
+
|
|
55
|
+
/* Group head — workspace project row (folder + label + count), clickable + collapsible */
|
|
56
|
+
.am-group-head{box-sizing:border-box;display:flex;align-items:center;gap:6px;height:34px;width:100%;padding:0 8px;border-radius:8px;border:none;background:transparent;color:inherit;font-family:inherit;font-size:inherit;text-align:left;cursor:pointer;}
|
|
57
|
+
.am-group-head:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.08));}
|
|
58
|
+
.am-chevron{flex:none;width:14px;height:14px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);transition:transform .15s var(--ds-ease-in-out);}
|
|
59
|
+
.am-chevron.am-open{transform:rotate(90deg);}
|
|
60
|
+
.am-group-icon{flex:none;width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
61
|
+
.am-group-label{flex:1;min-width:0;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary,inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
62
|
+
.am-group-count{flex:none;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
63
|
+
|
|
64
|
+
/* Session row — workspace session row (32px, radius 8, hover bg), indented under its group */
|
|
65
|
+
.am-row{display:flex;align-items:center;gap:6px;height:32px;padding:0 8px 0 32px;border-radius:8px;color:var(--dsw-alias-label-primary,inherit);}
|
|
66
|
+
.am-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.08));}
|
|
67
|
+
.am-slot{flex:none;width:16px;height:20px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
68
|
+
.am-title{flex:1;min-width:0;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
69
|
+
.am-time{flex:none;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary,inherit);}
|
|
70
|
+
.am-row-actions{flex:none;display:none;align-items:center;gap:2px;}
|
|
71
|
+
.am-row:hover .am-time{display:none;}
|
|
72
|
+
.am-row:hover .am-row-actions{display:inline-flex;}
|
|
73
|
+
|
|
74
|
+
/* Action buttons — subtle text buttons, hover-revealed like workspace rowActions */
|
|
75
|
+
.am-btn{border:none;background:transparent;font-size:12px;line-height:18px;padding:4px 8px;border-radius:8px;cursor:pointer;white-space:nowrap;}
|
|
76
|
+
.am-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));}
|
|
77
|
+
.am-btn-restore{color:var(--dsw-alias-brand-primary,#4a7dff);}
|
|
78
|
+
.am-btn-danger{color:var(--dsw-alias-state-error-primary,#e5484d);}
|
|
79
|
+
.am-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(229,72,77,.1));}
|
|
80
|
+
.am-btn[disabled]{opacity:.5;cursor:default;}
|
|
81
|
+
|
|
82
|
+
/* Orphan rows (log gone) */
|
|
83
|
+
.am-orphan .am-slot{color:var(--dsw-alias-state-warn-primary,#d99a06);}
|
|
84
|
+
.am-orphan .am-title{color:var(--dsw-alias-label-secondary,inherit);}
|
|
85
|
+
|
|
86
|
+
/* Empty / error / ghost — workspace empty style */
|
|
87
|
+
.am-empty{color:var(--dsw-alias-label-tertiary,inherit);padding:16px 12px;font-size:13px;line-height:20px;text-align:center;}
|
|
88
|
+
.am-error{color:var(--dsw-alias-state-error-primary,#e5484d);padding:8px 24px;font-size:12px;line-height:18px;}
|
|
89
|
+
.am-ghost{color:var(--dsw-alias-label-secondary,inherit);padding:8px 24px;font-size:12px;line-height:18px;}
|
|
90
|
+
|
|
91
|
+
/* Sidebar footer entry (unchanged) */
|
|
92
|
+
.am-foot-layer{flex:none;width:100%;margin:8px 0 0;display:flex;align-items:center;}
|
|
93
|
+
.am-foot-layer .am-foot{width:100%;height:49px;border-radius:12px;padding:0 8px 0 6px;display:inline-flex;align-items:center;gap:8px;background:transparent;border:none;color:var(--dsw-alias-label-primary,inherit);font-family:inherit;font-size:14px;cursor:pointer;overflow:hidden;}
|
|
94
|
+
.am-foot-layer .am-foot:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1));}
|
|
95
|
+
.am-foot-layer .am-foot.am-active{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14));}
|
|
96
|
+
.am-foot-layer .am-foot .am-foot-icon{flex:none;display:inline-flex;align-items:center;justify-content:center;}
|
|
97
|
+
.am-foot-layer .am-foot .am-foot-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden;}
|
|
98
|
+
.am-foot-layer .am-foot .am-badge{color:var(--dsw-alias-label-tertiary,inherit);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px;}
|
|
99
|
+
.am-foot-layer.am-foot-rail{width:36px;height:36px;margin:0;}
|
|
100
|
+
.am-foot-layer.am-foot-rail .am-foot{width:36px;height:36px;padding:0;justify-content:center;border-radius:10px;}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
// ---- Client Remote contribution ----------------------------------------
|
|
104
|
+
// The browser-side `remote.archiveManager` service only exists after this
|
|
105
|
+
// module mounts its namespace via ctx.remote.$mount(): dsh-api-remotes'
|
|
106
|
+
// client assembly mounts only the five official namespaces, so a plugin
|
|
107
|
+
// must mount its own. Mirrors the invocations in typert.host.js (ids,
|
|
108
|
+
// service/namespace/method, wire fields). zod is not requirable in the
|
|
109
|
+
// browser module loader, so codecs use passthrough schemas — the runtime
|
|
110
|
+
// contract only requires typeSymbol + schema.parse().
|
|
111
|
+
const passthrough = () => ({ parse: (v) => v });
|
|
112
|
+
const CLIENT_REMOTE = {
|
|
113
|
+
package: "dsh-archive-manager",
|
|
114
|
+
descriptors: [
|
|
115
|
+
{
|
|
116
|
+
id: "dsh-archive-manager#archiveManager/restore",
|
|
117
|
+
service: "archiveManager",
|
|
118
|
+
namespace: "archiveManager",
|
|
119
|
+
method: "restore",
|
|
120
|
+
invocation: { kind: "direct" },
|
|
121
|
+
parameters: [
|
|
122
|
+
{
|
|
123
|
+
name: "request",
|
|
124
|
+
wire: "request",
|
|
125
|
+
source: "json",
|
|
126
|
+
codec: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreRequest", schema: passthrough() },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreResult", schema: passthrough() },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "dsh-archive-manager#archiveManager/delete",
|
|
133
|
+
service: "archiveManager",
|
|
134
|
+
namespace: "archiveManager",
|
|
135
|
+
method: "delete",
|
|
136
|
+
invocation: { kind: "direct" },
|
|
137
|
+
parameters: [
|
|
138
|
+
{
|
|
139
|
+
name: "request",
|
|
140
|
+
wire: "request",
|
|
141
|
+
source: "json",
|
|
142
|
+
codec: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteRequest", schema: passthrough() },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteResult", schema: passthrough() },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "dsh-archive-manager#archiveManager/state",
|
|
149
|
+
service: "archiveManager",
|
|
150
|
+
namespace: "archiveManager",
|
|
151
|
+
method: "state",
|
|
152
|
+
invocation: { kind: "direct" },
|
|
153
|
+
parameters: [],
|
|
154
|
+
result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerStateResult", schema: passthrough() },
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
async function apply(ctx) {
|
|
160
|
+
// Mount the archiveManager namespace before anything touches it; the
|
|
161
|
+
// mount's lifetime is bound to this plugin's context by $mount itself.
|
|
162
|
+
await ctx.remote.$mount(CLIENT_REMOTE);
|
|
163
|
+
|
|
164
|
+
const styleTag = document.createElement("style");
|
|
165
|
+
styleTag.textContent = CSS;
|
|
166
|
+
document.head.appendChild(styleTag);
|
|
167
|
+
ctx.effect(() => () => styleTag.remove());
|
|
168
|
+
|
|
169
|
+
// ctx.get() reads the service without the property-accessor inject
|
|
170
|
+
// guard; it exists because the $mount above just created it.
|
|
171
|
+
const remote = ctx.get("remote.archiveManager");
|
|
172
|
+
|
|
173
|
+
// Shared open state between the footer button and the overlay.
|
|
174
|
+
let open = false;
|
|
175
|
+
const listeners = new Set();
|
|
176
|
+
function getOpen() { return open; }
|
|
177
|
+
function setOpen(v) { if (open !== v) { open = v; for (const fn of listeners) fn(); } }
|
|
178
|
+
function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
|
|
179
|
+
function useOpen() { return React.useSyncExternalStore(subscribe, getOpen); }
|
|
180
|
+
|
|
181
|
+
// Ghost state (deleted-but-resident archived sessions), shared by both components.
|
|
182
|
+
const ghostStore = (() => {
|
|
183
|
+
let ids = [];
|
|
184
|
+
const listeners = new Set();
|
|
185
|
+
return {
|
|
186
|
+
getSnapshot: () => ids,
|
|
187
|
+
subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
|
|
188
|
+
set: (next) => { ids = next; for (const fn of listeners) fn(); },
|
|
189
|
+
};
|
|
190
|
+
})();
|
|
191
|
+
function useGhostIds() { return React.useSyncExternalStore(ghostStore.subscribe, ghostStore.getSnapshot); }
|
|
192
|
+
async function refreshState() {
|
|
193
|
+
try {
|
|
194
|
+
const res = await remote.state();
|
|
195
|
+
ghostStore.set(res && res.ok && Array.isArray(res.value.ghostIds) ? res.value.ghostIds : []);
|
|
196
|
+
} catch {
|
|
197
|
+
ghostStore.set([]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ArchiveIcon() {
|
|
202
|
+
return React.createElement(
|
|
203
|
+
"svg",
|
|
204
|
+
{ width: 16, height: 16, viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: 1.1, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
205
|
+
React.createElement("rect", { x: 1.5, y: 2, width: 11, height: 3.5, rx: 0.8 }),
|
|
206
|
+
React.createElement("path", { d: "M2.5 5.5v5a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-5" }),
|
|
207
|
+
React.createElement("path", { d: "M5.5 8h3" })
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function FolderIcon() {
|
|
212
|
+
return React.createElement(
|
|
213
|
+
"svg",
|
|
214
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
215
|
+
React.createElement("path", { d: "M1.5 4.5a1 1 0 0 1 1-1h3l1.5 2h6.5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7Z" })
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function WarningIcon() {
|
|
220
|
+
return React.createElement(
|
|
221
|
+
"svg",
|
|
222
|
+
{ width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
223
|
+
React.createElement("path", { d: "M8 2.5 14.5 13.5h-13L8 2.5Z" }),
|
|
224
|
+
React.createElement("path", { d: "M8 6.5v3" }),
|
|
225
|
+
React.createElement("circle", { cx: 8, cy: 11.5, r: 0.4, fill: "currentColor" })
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ChevronIcon() {
|
|
230
|
+
return React.createElement(
|
|
231
|
+
"svg",
|
|
232
|
+
{ width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
|
|
233
|
+
React.createElement("path", { d: "m5 3.5 3.5 3.5L5 10.5" })
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fmtTime(ts) {
|
|
238
|
+
if (typeof ts !== "number" || !isFinite(ts) || ts <= 0) return "";
|
|
239
|
+
const diff = Date.now() - ts;
|
|
240
|
+
const min = 60 * 1000, hour = 60 * min, day = 24 * hour, month = 30 * day;
|
|
241
|
+
if (diff < min) return "刚刚";
|
|
242
|
+
if (diff < hour) return Math.floor(diff / min) + " 分钟前";
|
|
243
|
+
if (diff < day) return Math.floor(diff / hour) + " 小时前";
|
|
244
|
+
if (diff < month) return Math.floor(diff / day) + " 天前";
|
|
245
|
+
const d = new Date(ts);
|
|
246
|
+
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function deriveGroups(byId, ids, workspaces, archivedIds, query) {
|
|
250
|
+
const q = (query || "").trim().toLowerCase();
|
|
251
|
+
const archived = new Set(archivedIds);
|
|
252
|
+
const groups = [];
|
|
253
|
+
const accounted = new Set();
|
|
254
|
+
const resolved = new Set();
|
|
255
|
+
for (const ws of workspaces) {
|
|
256
|
+
const members = [];
|
|
257
|
+
for (const sid of ws.sessionIds) {
|
|
258
|
+
if (!archived.has(sid)) continue;
|
|
259
|
+
resolved.add(sid);
|
|
260
|
+
const s = byId[sid];
|
|
261
|
+
if (s === undefined) continue;
|
|
262
|
+
accounted.add(sid);
|
|
263
|
+
if (s.blank) continue;
|
|
264
|
+
const label = s.displayTitle || s.title || "未命名会话";
|
|
265
|
+
if (q && label.toLowerCase().indexOf(q) === -1 && ws.title.toLowerCase().indexOf(q) === -1 && String(s.cwd || "").toLowerCase().indexOf(q) === -1) continue;
|
|
266
|
+
members.push(s);
|
|
267
|
+
}
|
|
268
|
+
if (members.length === 0) continue;
|
|
269
|
+
members.sort((a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1));
|
|
270
|
+
groups.push({ key: ws.workspaceId, label: ws.title, sessions: members });
|
|
271
|
+
}
|
|
272
|
+
const stray = [];
|
|
273
|
+
for (const sid of ids) {
|
|
274
|
+
if (accounted.has(sid)) continue;
|
|
275
|
+
if (!archived.has(sid)) continue;
|
|
276
|
+
resolved.add(sid);
|
|
277
|
+
const s = byId[sid];
|
|
278
|
+
if (s === undefined) continue;
|
|
279
|
+
if (s.blank) continue;
|
|
280
|
+
const label = s.displayTitle || s.title || "未命名会话";
|
|
281
|
+
if (q && label.toLowerCase().indexOf(q) === -1 && String(s.cwd || "").toLowerCase().indexOf(q) === -1) continue;
|
|
282
|
+
stray.push(s);
|
|
283
|
+
}
|
|
284
|
+
if (stray.length > 0) {
|
|
285
|
+
stray.sort((a, b) => b.updatedAt - a.updatedAt || (a.id < b.id ? -1 : 1));
|
|
286
|
+
groups.push({ key: "__ungrouped__", label: "未分组", sessions: stray });
|
|
287
|
+
}
|
|
288
|
+
const orphans = [];
|
|
289
|
+
for (const sid of archivedIds) {
|
|
290
|
+
if (resolved.has(sid)) continue;
|
|
291
|
+
const s = byId[sid];
|
|
292
|
+
if (s !== undefined && !s.blank) continue;
|
|
293
|
+
if (q) {
|
|
294
|
+
const sidLower = String(sid).toLowerCase();
|
|
295
|
+
if (sidLower.indexOf(q) === -1) continue;
|
|
296
|
+
}
|
|
297
|
+
orphans.push({ id: sid, orphan: true, displayTitle: "未知会话(日志已不存在)", updatedAt: 0, cwd: undefined });
|
|
298
|
+
}
|
|
299
|
+
return { groups, orphans };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ArchiveManager({ slotProps, onClose }) {
|
|
303
|
+
const [query, setQuery] = React.useState("");
|
|
304
|
+
const [busy, setBusy] = React.useState(null);
|
|
305
|
+
const [confirming, setConfirming] = React.useState(null);
|
|
306
|
+
const [error, setError] = React.useState(null);
|
|
307
|
+
// Groups are collapsed by default so a large archive stays manageable;
|
|
308
|
+
// a non-empty search query force-expands every group to show matches.
|
|
309
|
+
const [expanded, setExpanded] = React.useState(() => new Set());
|
|
310
|
+
const ghostIds = useGhostIds();
|
|
311
|
+
const rawArchivedIds = slotProps.useWorkspaces((s) => s.archivedSessionIds);
|
|
312
|
+
const byId = slotProps.useSessions((s) => s.byId);
|
|
313
|
+
const ids = slotProps.useSessions((s) => s.ids);
|
|
314
|
+
const workspaces = slotProps.useWorkspaces((s) => s.items);
|
|
315
|
+
React.useEffect(() => { refreshState(); }, []);
|
|
316
|
+
const archivedIds = React.useMemo(() => {
|
|
317
|
+
if (ghostIds.length === 0) return rawArchivedIds;
|
|
318
|
+
const ghost = new Set(ghostIds);
|
|
319
|
+
return rawArchivedIds.filter((id) => !ghost.has(id));
|
|
320
|
+
}, [rawArchivedIds, ghostIds]);
|
|
321
|
+
const derived = React.useMemo(() => {
|
|
322
|
+
try {
|
|
323
|
+
const d = deriveGroups(byId, ids, workspaces, archivedIds, query);
|
|
324
|
+
return { groups: d.groups, orphans: d.orphans, error: null };
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return { groups: [], orphans: [], error: e instanceof Error ? e.message : String(e) };
|
|
327
|
+
}
|
|
328
|
+
}, [byId, ids, workspaces, archivedIds, query]);
|
|
329
|
+
const runAction = async (action, sessionId) => {
|
|
330
|
+
setBusy(action + ":" + sessionId);
|
|
331
|
+
setError(null);
|
|
332
|
+
try {
|
|
333
|
+
const res = await remote[action]({ sessionId });
|
|
334
|
+
if (res && res.ok) {
|
|
335
|
+
if (action === "delete") {
|
|
336
|
+
try {
|
|
337
|
+
const sessions = ctx.get("sessions");
|
|
338
|
+
if (sessions && typeof sessions.refresh === "function") sessions.refresh();
|
|
339
|
+
} catch { /* non-fatal */ }
|
|
340
|
+
refreshState();
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const err = res && res.error;
|
|
344
|
+
setError((err && err.message) || (err && err.code) || "操作失败");
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
348
|
+
} finally {
|
|
349
|
+
setBusy(null);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const total = archivedIds.length;
|
|
353
|
+
const groups = derived.groups.slice();
|
|
354
|
+
if (derived.orphans.length > 0) {
|
|
355
|
+
groups.push({ key: "__orphan__", label: "无效的归档记录", orphanGroup: true, sessions: derived.orphans });
|
|
356
|
+
}
|
|
357
|
+
const searching = query.trim() !== "";
|
|
358
|
+
const isExpanded = (key) => searching || expanded.has(key);
|
|
359
|
+
const toggleGroup = (key) => {
|
|
360
|
+
setExpanded((prev) => {
|
|
361
|
+
const next = new Set(prev);
|
|
362
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
363
|
+
return next;
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
const empty = total === 0 && ghostIds.length === 0;
|
|
367
|
+
const allGhost = total === 0 && ghostIds.length > 0;
|
|
368
|
+
const header = React.createElement(
|
|
369
|
+
"div", { className: "am-header" },
|
|
370
|
+
React.createElement("span", { className: "am-title" }, "归档管理"),
|
|
371
|
+
React.createElement("span", { className: "am-count" }, total + " 个归档"),
|
|
372
|
+
typeof onClose === "function"
|
|
373
|
+
? React.createElement("button", { className: "am-close", onClick: onClose, title: "关闭", "aria-label": "关闭" }, "✕")
|
|
374
|
+
: null
|
|
375
|
+
);
|
|
376
|
+
const search = React.createElement(
|
|
377
|
+
"div", { className: "am-search" },
|
|
378
|
+
React.createElement("input", {
|
|
379
|
+
type: "text",
|
|
380
|
+
placeholder: "搜索归档标题或项目…",
|
|
381
|
+
value: query,
|
|
382
|
+
onChange: (e) => { setQuery(e.target.value); setConfirming(null); },
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
const body = React.createElement(
|
|
386
|
+
"div", { className: "am-body" },
|
|
387
|
+
derived.error
|
|
388
|
+
? React.createElement("div", { className: "am-error" }, "渲染出错:" + derived.error)
|
|
389
|
+
: empty
|
|
390
|
+
? React.createElement("div", { className: "am-empty" }, "暂无归档会话")
|
|
391
|
+
: allGhost
|
|
392
|
+
? React.createElement("div", { className: "am-empty" }, "归档均已删除,重启 DSH 后自动清除")
|
|
393
|
+
: groups.map((g) =>
|
|
394
|
+
React.createElement(
|
|
395
|
+
"div", { className: "am-group", key: g.key },
|
|
396
|
+
React.createElement(
|
|
397
|
+
"button", {
|
|
398
|
+
type: "button",
|
|
399
|
+
className: "am-group-head",
|
|
400
|
+
onClick: () => toggleGroup(g.key),
|
|
401
|
+
"aria-expanded": isExpanded(g.key),
|
|
402
|
+
},
|
|
403
|
+
React.createElement(
|
|
404
|
+
"span", { className: "am-chevron" + (isExpanded(g.key) ? " am-open" : ""), "aria-hidden": true },
|
|
405
|
+
React.createElement(ChevronIcon)
|
|
406
|
+
),
|
|
407
|
+
React.createElement(
|
|
408
|
+
"span", { className: "am-group-icon", "aria-hidden": true },
|
|
409
|
+
g.orphanGroup ? React.createElement(WarningIcon) : React.createElement(FolderIcon)
|
|
410
|
+
),
|
|
411
|
+
React.createElement("span", { className: "am-group-label" }, g.label),
|
|
412
|
+
React.createElement("span", { className: "am-group-count" }, g.sessions.length + " 个")
|
|
413
|
+
),
|
|
414
|
+
isExpanded(g.key) ? g.sessions.map((s) => {
|
|
415
|
+
const restoring = busy === "restore:" + s.id;
|
|
416
|
+
const deleting = busy === "delete:" + s.id;
|
|
417
|
+
const isConfirm = confirming === s.id;
|
|
418
|
+
const isOrphan = s.orphan === true;
|
|
419
|
+
const timeLabel = isOrphan ? s.id : fmtTime(s.updatedAt);
|
|
420
|
+
const titleAttr = isOrphan ? s.id : (s.cwd || undefined);
|
|
421
|
+
const actions = [];
|
|
422
|
+
if (!isOrphan) {
|
|
423
|
+
actions.push(React.createElement("button", {
|
|
424
|
+
key: "restore",
|
|
425
|
+
type: "button",
|
|
426
|
+
className: "am-btn am-btn-restore",
|
|
427
|
+
disabled: !!busy,
|
|
428
|
+
onClick: () => runAction("restore", s.id),
|
|
429
|
+
}, restoring ? "…" : "还原"));
|
|
430
|
+
}
|
|
431
|
+
actions.push(React.createElement("button", {
|
|
432
|
+
key: "delete",
|
|
433
|
+
type: "button",
|
|
434
|
+
className: "am-btn am-btn-danger",
|
|
435
|
+
disabled: !!busy,
|
|
436
|
+
onClick: () => {
|
|
437
|
+
if (isConfirm) { setConfirming(null); runAction("delete", s.id); }
|
|
438
|
+
else { setConfirming(s.id); }
|
|
439
|
+
},
|
|
440
|
+
}, deleting ? "…" : (isConfirm ? "确认删除?" : "删除")));
|
|
441
|
+
return React.createElement(
|
|
442
|
+
"div", { className: "am-row" + (isOrphan ? " am-orphan" : ""), key: s.id, title: titleAttr },
|
|
443
|
+
React.createElement(
|
|
444
|
+
"span", { className: "am-slot", "aria-hidden": true },
|
|
445
|
+
isOrphan ? React.createElement(WarningIcon) : React.createElement(ArchiveIcon)
|
|
446
|
+
),
|
|
447
|
+
React.createElement("span", { className: "am-title" }, isOrphan ? "未知会话(日志已不存在)" : (s.displayTitle || s.title || "未命名会话")),
|
|
448
|
+
React.createElement("span", { className: "am-time" }, timeLabel),
|
|
449
|
+
React.createElement("span", { className: "am-row-actions" }, actions)
|
|
450
|
+
);
|
|
451
|
+
}) : null
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
const ghostNotice = ghostIds.length > 0
|
|
456
|
+
? React.createElement("div", { className: "am-ghost" }, ghostIds.length + " 个已删除会话仍驻留内存(已隐藏),重启 DSH 后自动清除")
|
|
457
|
+
: null;
|
|
458
|
+
return React.createElement(
|
|
459
|
+
"div", { className: "am-panel", onClick: (e) => e.stopPropagation() },
|
|
460
|
+
header,
|
|
461
|
+
error ? React.createElement("div", { className: "am-error" }, error) : null,
|
|
462
|
+
ghostNotice,
|
|
463
|
+
search,
|
|
464
|
+
body
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Sidebar footer action entry (Cordis Plugin 下方、设置上方;纵向排列由注入 CSS 保证)。
|
|
469
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
|
|
470
|
+
{ name: "sidebar.footer.action", id: "archive-manager", order: 500, label: () => "归档" },
|
|
471
|
+
(props) => {
|
|
472
|
+
const wide = props.wide === true;
|
|
473
|
+
const ghostIds = useGhostIds();
|
|
474
|
+
let rawCount = 0;
|
|
475
|
+
try { rawCount = props.useWorkspaces((s) => s.archivedSessionIds.length) || 0; } catch { rawCount = 0; }
|
|
476
|
+
React.useEffect(() => { refreshState(); }, []);
|
|
477
|
+
const count = rawCount - (ghostIds.length > rawCount ? rawCount : ghostIds.length);
|
|
478
|
+
const safeCount = count > 0 ? count : 0;
|
|
479
|
+
const isOpen = useOpen();
|
|
480
|
+
const children = [];
|
|
481
|
+
children.push(React.createElement("span", { key: "icon", className: "am-foot-icon", "aria-hidden": true }, React.createElement(ArchiveIcon)));
|
|
482
|
+
if (wide) children.push(React.createElement("span", { key: "label", className: "am-foot-label" }, "归档"));
|
|
483
|
+
if (wide && safeCount > 0) children.push(React.createElement("span", { key: "cnt", className: "am-badge" }, String(safeCount)));
|
|
484
|
+
return React.createElement(
|
|
485
|
+
"div", { className: "am-foot-layer" + (wide ? "" : " am-foot-rail") },
|
|
486
|
+
React.createElement("button", {
|
|
487
|
+
type: "button",
|
|
488
|
+
onClick: () => { refreshState(); setOpen(!getOpen()); },
|
|
489
|
+
title: "归档管理",
|
|
490
|
+
"aria-label": "归档管理",
|
|
491
|
+
className: "am-foot" + (isOpen ? " am-active" : ""),
|
|
492
|
+
}, children)
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
));
|
|
496
|
+
|
|
497
|
+
// Frame-wide archive manager panel.
|
|
498
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register(
|
|
499
|
+
{ name: "shell.overlay", id: "archive-manager", order: 200 },
|
|
500
|
+
(props) => {
|
|
501
|
+
const isOpen = useOpen();
|
|
502
|
+
if (!isOpen) return null;
|
|
503
|
+
return React.createElement(
|
|
504
|
+
"div", { className: "am-overlay", onClick: () => setOpen(false) },
|
|
505
|
+
React.createElement(ArchiveManager, { slotProps: props, onClose: () => setOpen(false) })
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
exports.apply = apply;
|
|
512
|
+
exports.inject = ["slots", "remote"];
|
|
513
|
+
return module.exports;
|
|
514
|
+
},
|
|
515
|
+
});
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-archive-manager — Host half.
|
|
3
|
+
*
|
|
4
|
+
* A Cordis "class plugin": this module exports an `ArchiveManagerService`
|
|
5
|
+
* extending `TypertRemoteService`. The DSH loader instantiates the class and
|
|
6
|
+
* registers it as the `archiveManager` service; the Typert Gateway exposes its
|
|
7
|
+
* `@Remote`-marked methods to the browser Client half under the
|
|
8
|
+
* `archiveManager` Remote namespace.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - `restore`: remove a session from the workspace registry's archive set
|
|
12
|
+
* (durable write through the registry's own state writer, which keeps the
|
|
13
|
+
* registry cache, the domain store and the `host/archived-sessions-changed`
|
|
14
|
+
* client frame in sync).
|
|
15
|
+
* - `delete`: refuse sessions whose agent is actually running, remove the
|
|
16
|
+
* session's durable log directory with plain `node:fs` calls (fully
|
|
17
|
+
* cross-platform: Windows, macOS, Linux), then drop the archive
|
|
18
|
+
* entry. Live-but-idle sessions keep their archive entry so they do not
|
|
19
|
+
* "revive" in the sidebar; the `state` call marks them as ghost.
|
|
20
|
+
* - `state`: report which archived sessions are still live in memory while
|
|
21
|
+
* their log files no longer exist (ghost records the Client hides).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
25
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { rm } from "node:fs/promises";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mark one instance method as a Remote export without relying on decorator
|
|
31
|
+
* syntax (Node ESM does not support the proposal decorators here). We drive
|
|
32
|
+
* the same `Remote(name)` decorator manually through a synthetic decorator
|
|
33
|
+
* context and run the registered initializers against the instance.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} instance - live service instance whose prototype is marked.
|
|
36
|
+
* @param {string} method - public instance method name.
|
|
37
|
+
* @param {string} [exportName] - wire export name; defaults to the method name.
|
|
38
|
+
*/
|
|
39
|
+
function markRemoteMethod(instance, method, exportName) {
|
|
40
|
+
const decorator = Remote(method, undefined);
|
|
41
|
+
const initializers = [];
|
|
42
|
+
decorator(undefined, {
|
|
43
|
+
kind: "method",
|
|
44
|
+
name: method,
|
|
45
|
+
static: false,
|
|
46
|
+
private: false,
|
|
47
|
+
addInitializer: (fn) => initializers.push(fn),
|
|
48
|
+
});
|
|
49
|
+
for (const fn of initializers) fn.call(instance);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read the current archive set from the authoritative workspace domain, with a
|
|
54
|
+
* fallback to the registry's own getter when the domain is unavailable.
|
|
55
|
+
*/
|
|
56
|
+
function readArchived(domain) {
|
|
57
|
+
if (domain) {
|
|
58
|
+
try {
|
|
59
|
+
const state = domain.global.get();
|
|
60
|
+
if (state && Array.isArray(state.archivedSessionIds)) {
|
|
61
|
+
return [...state.archivedSessionIds];
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
/* fall through to registry getter */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const registry = this?.ctx?.get?.("workspaceRegistry");
|
|
68
|
+
if (registry && Array.isArray(registry.archivedSessionIds)) {
|
|
69
|
+
return [...registry.archivedSessionIds];
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class ArchiveManagerService extends TypertRemoteService {
|
|
75
|
+
static inject = [
|
|
76
|
+
"workspaceRegistry",
|
|
77
|
+
"storageDomain",
|
|
78
|
+
"sessionPersistence",
|
|
79
|
+
"sessionQuery",
|
|
80
|
+
"sessions",
|
|
81
|
+
"agents",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Cordis instantiates class plugins with `new Callback(ctx, config)` — the
|
|
86
|
+
* second argument is the plugin config, NOT the service key. `TypertRemoteService`
|
|
87
|
+
* has a `(ctx, serviceKey, options)` constructor that validates the key; without
|
|
88
|
+
* an explicit constructor here, `config` is passed as `serviceKey` and the
|
|
89
|
+
* `validateName` check throws during construction. As a result the `archiveManager`
|
|
90
|
+
* service was never registered, so the client stayed pending forever on
|
|
91
|
+
* `remote.archiveManager` (web boot: 1 entry did not activate). Match the official
|
|
92
|
+
* `MessageFeedbackService` pattern: pass the exact service key to `super()`.
|
|
93
|
+
*/
|
|
94
|
+
constructor(ctx, config) {
|
|
95
|
+
super(ctx, "archiveManager");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Cordis class-plugin initializer: runs right after construction, before the
|
|
100
|
+
* service is published. Mark every Remote method and cache resolved config.
|
|
101
|
+
*/
|
|
102
|
+
[Service.init]() {
|
|
103
|
+
markRemoteMethod(this, "restore", "restore");
|
|
104
|
+
markRemoteMethod(this, "delete", "delete");
|
|
105
|
+
markRemoteMethod(this, "state", "state");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Resolve the workspace domain (the domain the registry opened). */
|
|
109
|
+
workspaceDomain() {
|
|
110
|
+
const domain = this.ctx.get("storageDomain");
|
|
111
|
+
return domain ? domain.get("workspace") : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove a session from the archive set using the registry's own state
|
|
116
|
+
* writer: durable write + registry cache sync + `domain/changed` emission.
|
|
117
|
+
*/
|
|
118
|
+
async writeArchived(next) {
|
|
119
|
+
const domain = this.workspaceDomain();
|
|
120
|
+
if (!domain) throw new Error("workspace domain unavailable");
|
|
121
|
+
const state = domain.global.get();
|
|
122
|
+
await this.ctx.workspaceRegistry.setState({
|
|
123
|
+
...state,
|
|
124
|
+
archivedSessionIds: next,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Resolve a session header (live-preferred, then persisted). */
|
|
129
|
+
async findHeader(sessionId) {
|
|
130
|
+
const sessionQuery = this.ctx.get("sessionQuery");
|
|
131
|
+
if (sessionQuery && typeof sessionQuery.listSessions === "function") {
|
|
132
|
+
try {
|
|
133
|
+
const records = await sessionQuery.listSessions();
|
|
134
|
+
const rec = records.find((r) => String(r.header.id) === sessionId);
|
|
135
|
+
if (rec) return rec.header;
|
|
136
|
+
} catch {
|
|
137
|
+
/* fall through */
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sessionPersistence = this.ctx.get("sessionPersistence");
|
|
141
|
+
if (sessionPersistence && typeof sessionPersistence.list === "function") {
|
|
142
|
+
try {
|
|
143
|
+
const headers = await sessionPersistence.list();
|
|
144
|
+
return headers.find((h) => String(h.id) === sessionId);
|
|
145
|
+
} catch {
|
|
146
|
+
/* fall through */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Restore: remove `sessionId` from the archive set so the session returns to
|
|
154
|
+
* its project slot in the sidebar.
|
|
155
|
+
*/
|
|
156
|
+
async restore(request) {
|
|
157
|
+
const sessionId = request && request.sessionId;
|
|
158
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
159
|
+
return { ok: false, error: { code: "invalid-session" } };
|
|
160
|
+
}
|
|
161
|
+
const ids = readArchived.call(this, this.workspaceDomain());
|
|
162
|
+
if (!ids.includes(sessionId)) {
|
|
163
|
+
return { ok: false, error: { code: "not-archived" } };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await this.writeArchived(ids.filter((id) => id !== sessionId));
|
|
167
|
+
return { ok: true, value: { restored: true } };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: { code: "write-failed", message: String(error && error.message ? error.message : error) },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Delete: refuse a genuinely running agent, remove the session log directory
|
|
178
|
+
* durably, then clear the archive entry. Live-but-idle sessions keep the
|
|
179
|
+
* entry (ghost); non-live sessions are fully removed.
|
|
180
|
+
*/
|
|
181
|
+
async delete(request) {
|
|
182
|
+
const sessionId = request && request.sessionId;
|
|
183
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
184
|
+
return { ok: false, error: { code: "invalid-session" } };
|
|
185
|
+
}
|
|
186
|
+
const agents = this.ctx.get("agents");
|
|
187
|
+
let agent;
|
|
188
|
+
try {
|
|
189
|
+
agent = agents ? agents.get(sessionId) : undefined;
|
|
190
|
+
} catch {
|
|
191
|
+
agent = undefined;
|
|
192
|
+
}
|
|
193
|
+
if (agent !== undefined && agent.status === "running") {
|
|
194
|
+
return { ok: false, error: { code: "agent-running", message: "该会话的智能体正在运行中,请等待其完成或停止后再删除" } };
|
|
195
|
+
}
|
|
196
|
+
const header = await this.findHeader(sessionId);
|
|
197
|
+
let location;
|
|
198
|
+
if (header) {
|
|
199
|
+
const sessionPersistence = this.ctx.get("sessionPersistence");
|
|
200
|
+
try {
|
|
201
|
+
location = sessionPersistence.locate(header);
|
|
202
|
+
} catch {
|
|
203
|
+
location = undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
let removed = false;
|
|
207
|
+
let fileError;
|
|
208
|
+
if (location && location.path) {
|
|
209
|
+
const target = String(location.path);
|
|
210
|
+
try {
|
|
211
|
+
// Delete straight from the host process via node:fs — no shell, no
|
|
212
|
+
// PowerShell, no sandbox escalation, so this behaves identically on
|
|
213
|
+
// Windows, macOS and Linux. Composition plugins run in-process with
|
|
214
|
+
// the host's own privileges, which makes the sandbox ACL dance the
|
|
215
|
+
// previous shell-based implementation needed unnecessary. maxRetries
|
|
216
|
+
// absorbs transient Windows file locks (antivirus / indexers) and is
|
|
217
|
+
// a no-op elsewhere.
|
|
218
|
+
await rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
219
|
+
removed = !existsSync(target);
|
|
220
|
+
if (!removed) fileError = "path-still-exists";
|
|
221
|
+
} catch (error) {
|
|
222
|
+
fileError = String(error && error.message ? error.message : error);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
removed = true; // no located artifact → already gone
|
|
226
|
+
}
|
|
227
|
+
if (!removed) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
error: { code: "delete-failed", message: "删除会话文件失败(" + (fileError || "未知错误") + "),已保留归档条目" },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const sessions = this.ctx.get("sessions");
|
|
234
|
+
const live = !!(sessions && sessions.get(sessionId) !== undefined);
|
|
235
|
+
if (!live) {
|
|
236
|
+
const ids = readArchived.call(this, this.workspaceDomain());
|
|
237
|
+
if (ids.includes(sessionId)) {
|
|
238
|
+
try {
|
|
239
|
+
await this.writeArchived(ids.filter((id) => id !== sessionId));
|
|
240
|
+
} catch {
|
|
241
|
+
/* log already gone; entry cleanup best-effort */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { ok: true, value: { fileRemoved: !!(location && location.path), live } };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* State: report archived sessions that are still live in memory while their
|
|
250
|
+
* log files no longer exist. The Client hides these (ghost) and tells the
|
|
251
|
+
* user they clear on restart.
|
|
252
|
+
*/
|
|
253
|
+
async state() {
|
|
254
|
+
const sessions = this.ctx.get("sessions");
|
|
255
|
+
const sessionPersistence = this.ctx.get("sessionPersistence");
|
|
256
|
+
if (!sessions || !sessionPersistence || typeof sessionPersistence.locate !== "function") {
|
|
257
|
+
return { ok: true, value: { ghostIds: [] } };
|
|
258
|
+
}
|
|
259
|
+
const ids = readArchived.call(this, this.workspaceDomain());
|
|
260
|
+
const ghostIds = [];
|
|
261
|
+
for (const id of ids) {
|
|
262
|
+
try {
|
|
263
|
+
const session = sessions.get(id);
|
|
264
|
+
if (session === undefined) continue; // not live in memory → not a ghost
|
|
265
|
+
const location = sessionPersistence.locate(session.header);
|
|
266
|
+
// Live but the log directory is gone → ghost the client hides.
|
|
267
|
+
if (location && location.path && !existsSync(String(location.path))) ghostIds.push(id);
|
|
268
|
+
} catch {
|
|
269
|
+
/* skip */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { ok: true, value: { ghostIds } };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default ArchiveManagerService;
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@duke-dsh-plugins/dsh-archive-manager",
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Archive manager for DeepSeek Harness: group archived sessions by project, newest-first, with search, restore and delete.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/MoonlitDropOfBlood/dsh-archive-manager.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/MoonlitDropOfBlood/dsh-archive-manager",
|
|
10
|
+
"bugs": { "url": "https://github.com/MoonlitDropOfBlood/dsh-archive-manager/issues" },
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"keywords": ["dsh", "deepseek-harness", "plugin", "session", "archive"],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./index.js",
|
|
17
|
+
"./client": "./client.js",
|
|
18
|
+
"./typert": "./typert.host.js",
|
|
19
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"client.js",
|
|
25
|
+
"typert.host.js",
|
|
26
|
+
"cordis.patch.yml"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dsh": {
|
|
30
|
+
"bundle": {
|
|
31
|
+
"patch": "./cordis.patch.yml"
|
|
32
|
+
},
|
|
33
|
+
"client": {
|
|
34
|
+
"platform": "web"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
+
"@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
|
|
40
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"zod": "^4.4.3"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"check": "node --check index.js && node --check client.js && node --check typert.host.js"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/typert.host.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-archive-manager — Typert Host manifest.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written TYPERT manifest (the format the DSH typert-loader consumes from
|
|
5
|
+
* the package's `./typert` export). It describes the `archiveManager` Remote
|
|
6
|
+
* service the Host half publishes so the browser Client half can call it
|
|
7
|
+
* through `ctx.remote.archiveManager.{restore,delete,state}`.
|
|
8
|
+
*
|
|
9
|
+
* Keep the invocation ids, service/namespace names and method names in sync
|
|
10
|
+
* with `index.js` (ArchiveManagerService) and `client.js`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// ---- shared shapes ---------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const sessionIdSchema = z.intersection(z.string(), z.unknown()).readonly();
|
|
18
|
+
|
|
19
|
+
const restoreResultSchema = z.union([
|
|
20
|
+
z.object({
|
|
21
|
+
ok: z.literal(true).readonly(),
|
|
22
|
+
value: z.object({
|
|
23
|
+
restored: z.literal(true).readonly(),
|
|
24
|
+
}).readonly(),
|
|
25
|
+
}).readonly(),
|
|
26
|
+
z.object({
|
|
27
|
+
ok: z.literal(false).readonly(),
|
|
28
|
+
error: z.object({
|
|
29
|
+
code: z.string().readonly(),
|
|
30
|
+
message: z.string().readonly().optional(),
|
|
31
|
+
}).readonly(),
|
|
32
|
+
}).readonly(),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const deleteResultSchema = z.union([
|
|
36
|
+
z.object({
|
|
37
|
+
ok: z.literal(true).readonly(),
|
|
38
|
+
value: z.object({
|
|
39
|
+
fileRemoved: z.boolean().readonly(),
|
|
40
|
+
live: z.boolean().readonly(),
|
|
41
|
+
}).readonly(),
|
|
42
|
+
}).readonly(),
|
|
43
|
+
z.object({
|
|
44
|
+
ok: z.literal(false).readonly(),
|
|
45
|
+
error: z.object({
|
|
46
|
+
code: z.string().readonly(),
|
|
47
|
+
message: z.string().readonly().optional(),
|
|
48
|
+
}).readonly(),
|
|
49
|
+
}).readonly(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const stateResultSchema = z.union([
|
|
53
|
+
z.object({
|
|
54
|
+
ok: z.literal(true).readonly(),
|
|
55
|
+
value: z.object({
|
|
56
|
+
ghostIds: z.array(sessionIdSchema).readonly(),
|
|
57
|
+
}).readonly(),
|
|
58
|
+
}).readonly(),
|
|
59
|
+
z.object({
|
|
60
|
+
ok: z.literal(false).readonly(),
|
|
61
|
+
error: z.object({
|
|
62
|
+
code: z.string().readonly(),
|
|
63
|
+
message: z.string().readonly().optional(),
|
|
64
|
+
}).readonly(),
|
|
65
|
+
}).readonly(),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// ---- per-invocation parameter/result schemas -------------------------------
|
|
69
|
+
|
|
70
|
+
const _archiveManager_restore_parameter_0$schema = z.object({
|
|
71
|
+
sessionId: sessionIdSchema,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const _archiveManager_delete_parameter_0$schema = z.object({
|
|
75
|
+
sessionId: sessionIdSchema,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const TYPERT = {
|
|
79
|
+
package: "dsh-archive-manager",
|
|
80
|
+
face: "host",
|
|
81
|
+
schemas: [],
|
|
82
|
+
invocations: [
|
|
83
|
+
{
|
|
84
|
+
id: "dsh-archive-manager#archiveManager/restore",
|
|
85
|
+
service: "archiveManager",
|
|
86
|
+
namespace: "archiveManager",
|
|
87
|
+
method: "restore",
|
|
88
|
+
invocation: { kind: "direct" },
|
|
89
|
+
parameters: [
|
|
90
|
+
{
|
|
91
|
+
name: "request",
|
|
92
|
+
wire: "request",
|
|
93
|
+
source: "json",
|
|
94
|
+
codec: {
|
|
95
|
+
mode: "strict",
|
|
96
|
+
typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreRequest",
|
|
97
|
+
schema: _archiveManager_restore_parameter_0$schema,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
result: {
|
|
102
|
+
mode: "strict",
|
|
103
|
+
typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreResult",
|
|
104
|
+
schema: restoreResultSchema,
|
|
105
|
+
},
|
|
106
|
+
sourceLocation: { file: "index.js", line: 1, column: 1 },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "dsh-archive-manager#archiveManager/delete",
|
|
110
|
+
service: "archiveManager",
|
|
111
|
+
namespace: "archiveManager",
|
|
112
|
+
method: "delete",
|
|
113
|
+
invocation: { kind: "direct" },
|
|
114
|
+
parameters: [
|
|
115
|
+
{
|
|
116
|
+
name: "request",
|
|
117
|
+
wire: "request",
|
|
118
|
+
source: "json",
|
|
119
|
+
codec: {
|
|
120
|
+
mode: "strict",
|
|
121
|
+
typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteRequest",
|
|
122
|
+
schema: _archiveManager_delete_parameter_0$schema,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
result: {
|
|
127
|
+
mode: "strict",
|
|
128
|
+
typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteResult",
|
|
129
|
+
schema: deleteResultSchema,
|
|
130
|
+
},
|
|
131
|
+
sourceLocation: { file: "index.js", line: 1, column: 1 },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "dsh-archive-manager#archiveManager/state",
|
|
135
|
+
service: "archiveManager",
|
|
136
|
+
namespace: "archiveManager",
|
|
137
|
+
method: "state",
|
|
138
|
+
invocation: { kind: "direct" },
|
|
139
|
+
parameters: [],
|
|
140
|
+
result: {
|
|
141
|
+
mode: "strict",
|
|
142
|
+
typeSymbol: "dsh-archive-manager#ArchiveManagerStateResult",
|
|
143
|
+
schema: stateResultSchema,
|
|
144
|
+
},
|
|
145
|
+
sourceLocation: { file: "index.js", line: 1, column: 1 },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
model: {
|
|
149
|
+
services: [
|
|
150
|
+
{
|
|
151
|
+
description:
|
|
152
|
+
"Archive manager service: restore, delete and inspect archived sessions for the DeepSeek Harness web UI.",
|
|
153
|
+
summary: "Archive manager service.",
|
|
154
|
+
tags: [],
|
|
155
|
+
jsDoc:
|
|
156
|
+
"/**\n * Archive manager service: restore, delete and inspect archived sessions.\n */",
|
|
157
|
+
key: "archiveManager",
|
|
158
|
+
exportName: "ArchiveManagerService",
|
|
159
|
+
members: [
|
|
160
|
+
{
|
|
161
|
+
kind: "method",
|
|
162
|
+
name: "restore",
|
|
163
|
+
signature:
|
|
164
|
+
"@Remote('restore') async restore(request: ArchiveManagerRestoreRequest): Promise<ArchiveManagerRestoreResult>",
|
|
165
|
+
summary: "Remove one session from the archive set so it returns to its project slot.",
|
|
166
|
+
jsDoc:
|
|
167
|
+
"/**\n * Remove one session from the archive set so it returns to its project slot.\n * @param request - target session id.\n * @returns success or a business failure.\n */",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
kind: "method",
|
|
171
|
+
name: "delete",
|
|
172
|
+
signature:
|
|
173
|
+
"@Remote('delete') async delete(request: ArchiveManagerDeleteRequest): Promise<ArchiveManagerDeleteResult>",
|
|
174
|
+
summary: "Permanently delete one archived session's log directory and clear its archive entry.",
|
|
175
|
+
jsDoc:
|
|
176
|
+
"/**\n * Permanently delete one archived session's log directory and clear its archive\n * entry. Refuses sessions whose agent is running; live-but-idle sessions keep\n * their entry (ghost) until restart.\n * @param request - target session id.\n * @returns success or a business failure.\n */",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
kind: "method",
|
|
180
|
+
name: "state",
|
|
181
|
+
signature:
|
|
182
|
+
"@Remote('state') async state(): Promise<ArchiveManagerStateResult>",
|
|
183
|
+
summary: "Report archived sessions that are live in memory but whose logs are gone.",
|
|
184
|
+
jsDoc:
|
|
185
|
+
"/**\n * Report archived sessions that are still live in memory while their log files\n * no longer exist (ghost records the client hides).\n * @returns the ghost session ids.\n */",
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
types: [
|
|
189
|
+
{
|
|
190
|
+
name: "ArchiveManagerRestoreRequest",
|
|
191
|
+
declaration:
|
|
192
|
+
"export interface ArchiveManagerRestoreRequest {\n readonly sessionId: SessionId;\n}",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "ArchiveManagerRestoreResult",
|
|
196
|
+
declaration:
|
|
197
|
+
"export type ArchiveManagerRestoreResult = { ok: true; value: { restored: true } } | { ok: false; error: { code: string; message?: string } };",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "ArchiveManagerDeleteRequest",
|
|
201
|
+
declaration:
|
|
202
|
+
"export interface ArchiveManagerDeleteRequest {\n readonly sessionId: SessionId;\n}",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "ArchiveManagerDeleteResult",
|
|
206
|
+
declaration:
|
|
207
|
+
"export type ArchiveManagerDeleteResult = { ok: true; value: { fileRemoved: boolean; live: boolean } } | { ok: false; error: { code: string; message?: string } };",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "ArchiveManagerStateResult",
|
|
211
|
+
declaration:
|
|
212
|
+
"export type ArchiveManagerStateResult = { ok: true; value: { ghostIds: readonly SessionId[] } } | { ok: false; error: { code: string; message?: string } };",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "SessionId",
|
|
216
|
+
declaration: "export type SessionId = Branded<'SessionId'>;",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
events: [],
|
|
222
|
+
objects: [],
|
|
223
|
+
},
|
|
224
|
+
};
|