dsh-manual-compact-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +322 -0
- package/lib/index.js +127 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# dsh-manual-compact-plugin
|
|
2
|
+
|
|
3
|
+
手动上下文压缩插件(DeepSeek Harness / DSH)。
|
|
4
|
+
|
|
5
|
+
> 点击上下文圆圈(或输入框旁的"压缩"按钮)→ 选择保留最近 N 条 → 停止或分批压缩 → 每次压缩都有存档可查。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- **保留最近 N 条**:5 / 10(推荐)/ 20 / 30 / 50 / 自定义
|
|
10
|
+
- **处理方式**:
|
|
11
|
+
- 完成一批后停止
|
|
12
|
+
- 分批处理直到完成
|
|
13
|
+
- **压缩存档**:每次压缩显示时间、条数、约计 tokens,点击展开完整摘要
|
|
14
|
+
- 调用 DSH 官方 compaction API(`compaction.compactRegion`):不拆开工具调用对、会话忙时自动拒绝、失败不改动会话
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
dsh plugin --profile web add dsh-manual-compact-plugin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
安装后重启一次 DSH。
|
|
23
|
+
|
|
24
|
+
## 界面位置
|
|
25
|
+
|
|
26
|
+
- 如果 DSH 带有 `conversation.input.contextMeterPanel` Slot(对应上游补丁),控件嵌入**上下文圆圈弹窗**内
|
|
27
|
+
- 否则自动降级为**输入框工具行右侧的"压缩"按钮**(弹窗面板)
|
|
28
|
+
|
|
29
|
+
## 工作原理
|
|
30
|
+
|
|
31
|
+
- 浏览器端通过 `manual-compact` 设置命名空间写入 `request`(会话、保留数、模式)
|
|
32
|
+
- 宿主端读取请求,经当前会话的 compaction 服务执行,把结果写回 `lastRun`
|
|
33
|
+
- 存档来自会话日志中已落地的 `compaction/summary` 事件,随会话持久化
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-manual-compact — browser face.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written loader module mirroring the in-repo client bundle format
|
|
5
|
+
* (window.__ModuleLoader__.load with a CommonJS factory) — no JSX, no
|
|
6
|
+
* bundler, no TS. React is required through the app's module table.
|
|
7
|
+
*
|
|
8
|
+
* Two contributions:
|
|
9
|
+
* 1. `conversation.input.contextMeterPanel` — the manual-compaction section
|
|
10
|
+
* inside the context meter popup (DSH builds that declare the slot).
|
|
11
|
+
* 2. `conversation.input.right` — a small composer button with the same panel
|
|
12
|
+
* in a popup, registered only when the meter-panel slot is absent.
|
|
13
|
+
*
|
|
14
|
+
* Both read and write the `manual-compact` settings namespace through the
|
|
15
|
+
* shared `ctx.settingsScope` transport; the host executes the compaction and
|
|
16
|
+
* publishes the outcome into `lastRun`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
window.__ModuleLoader__.load({
|
|
20
|
+
id: "dsh-manual-compact-plugin/client",
|
|
21
|
+
factory: (require) => {
|
|
22
|
+
var module = { exports: {} };
|
|
23
|
+
var exports = module.exports;
|
|
24
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
var react = require("react");
|
|
26
|
+
var useSyncExternalStore = react.useSyncExternalStore;
|
|
27
|
+
var useState = react.useState;
|
|
28
|
+
var useEffect = react.useEffect;
|
|
29
|
+
var useRef = react.useRef;
|
|
30
|
+
|
|
31
|
+
var CSS_ID = "dsh-manual-compact-plugin/client";
|
|
32
|
+
var css = [
|
|
33
|
+
".dsmc-title{font-weight:600;margin:10px 0 6px;font-size:12px;color:var(--dsw-alias-label-primary)}",
|
|
34
|
+
".dsmc-sep{border:0;border-top:1px solid var(--dsw-alias-border-l3);margin:8px 0}",
|
|
35
|
+
".dsmc-row{display:flex;align-items:center;gap:6px;margin:6px 0}",
|
|
36
|
+
".dsmc-row label{flex:none;min-width:52px;color:var(--dsw-alias-label-secondary);font-size:12px}",
|
|
37
|
+
".dsmc-row select,.dsmc-row input{box-sizing:border-box;min-width:0;flex:1;padding:4px 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;background:var(--dsw-specific-input);color:var(--dsw-alias-label-primary);font-size:12px}",
|
|
38
|
+
".dsmc-actions{display:flex;justify-content:flex-end;margin-top:8px}",
|
|
39
|
+
".dsmc-actions button{border:0;border-radius:6px;padding:5px 10px;cursor:pointer;font-size:12px}",
|
|
40
|
+
".dsmc-actions button:disabled{cursor:default;opacity:.6}",
|
|
41
|
+
".dsmc-confirm{background:var(--dsw-alias-interactive-bg-primary);color:var(--dsw-alias-label-on-color)}",
|
|
42
|
+
".dsmc-msg{margin-top:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
|
|
43
|
+
".dsmc-msg.dsmc-err{color:var(--dsw-alias-state-error-primary)}",
|
|
44
|
+
".dsmc-archive{max-height:160px;overflow:auto}",
|
|
45
|
+
".dsmc-arch-item{border:0;background:transparent;width:100%;text-align:left;padding:4px 0;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}",
|
|
46
|
+
".dsmc-arch-item:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
47
|
+
".dsmc-arch-summary{font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);margin:4px 0 8px;white-space:pre-wrap}",
|
|
48
|
+
".dsmc-empty{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
|
|
49
|
+
".dsmc-wrap{position:relative;display:inline-flex}",
|
|
50
|
+
".dsmc-btn{height:24px;padding:0 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}",
|
|
51
|
+
".dsmc-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
52
|
+
".dsmc-pop{position:absolute;right:0;bottom:32px;z-index:120;width:280px;max-height:440px;overflow:auto;padding:12px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);font-size:12px;text-align:left}",
|
|
53
|
+
].join("");
|
|
54
|
+
if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + CSS_ID + '"]') === null) {
|
|
55
|
+
var tag = document.createElement("style");
|
|
56
|
+
tag.dataset.plugin = "dsh-manual-compact-plugin";
|
|
57
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
58
|
+
tag.textContent = css;
|
|
59
|
+
document.head.appendChild(tag);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read the `manual-compact` namespace snapshot reactively.
|
|
64
|
+
* @param scope - the bound SettingsScope.
|
|
65
|
+
* @returns `{ value, writable, ready }`.
|
|
66
|
+
*/
|
|
67
|
+
function useNamespaceSnapshot(scope) {
|
|
68
|
+
var snapshot = useSyncExternalStore(
|
|
69
|
+
function (onChange) { return scope.subscribe(onChange); },
|
|
70
|
+
function () { return scope.getSnapshot(); }
|
|
71
|
+
);
|
|
72
|
+
var ready = snapshot.status === "ready" && snapshot.value !== undefined;
|
|
73
|
+
return {
|
|
74
|
+
value: ready ? snapshot.value : undefined,
|
|
75
|
+
writable: snapshot.writable === true && ready,
|
|
76
|
+
ready: snapshot.status === "ready",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function fmtTime(ms) {
|
|
81
|
+
if (typeof ms !== "number" || !isFinite(ms)) return "";
|
|
82
|
+
var d = new Date(ms);
|
|
83
|
+
var pad = function (v) { return String(v).padStart(2, "0"); };
|
|
84
|
+
return (d.getMonth() + 1) + "/" + d.getDate() + " " + pad(d.getHours()) + ":" + pad(d.getMinutes());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fmtTokens(v) {
|
|
88
|
+
if (typeof v !== "number" || !isFinite(v)) return "?";
|
|
89
|
+
if (v >= 1000) return Math.round(v / 1000) + "k";
|
|
90
|
+
return String(v);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The shared controls: keep selector, mode selector, submit, last result,
|
|
95
|
+
* and the landed compaction archive read from the conversation snapshot.
|
|
96
|
+
*/
|
|
97
|
+
function CompactControls(props) {
|
|
98
|
+
var state = useNamespaceSnapshot(props.scope);
|
|
99
|
+
var value = state.value || {};
|
|
100
|
+
var [keep, setKeep] = useState("10");
|
|
101
|
+
var [customKeep, setCustomKeep] = useState("");
|
|
102
|
+
var [mode, setMode] = useState("stop");
|
|
103
|
+
var [localError, setLocalError] = useState("");
|
|
104
|
+
var [expandedSeq, setExpandedSeq] = useState(null);
|
|
105
|
+
|
|
106
|
+
var request = value.request;
|
|
107
|
+
var busy = request !== null && request !== undefined;
|
|
108
|
+
var lastRun = value.lastRun;
|
|
109
|
+
|
|
110
|
+
var legacy = props.session && props.session.chat && props.session.chat.legacy;
|
|
111
|
+
var nodes = legacy && Array.isArray(legacy.nodes) ? legacy.nodes : [];
|
|
112
|
+
var entries = [];
|
|
113
|
+
for (var i = nodes.length - 1; i >= 0; i -= 1) {
|
|
114
|
+
var n = nodes[i];
|
|
115
|
+
if (n && n.kind === "compaction") entries.push(n);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var effectiveKeep = keep === "custom" ? customKeep : keep;
|
|
119
|
+
function submit() {
|
|
120
|
+
var n = Number(effectiveKeep);
|
|
121
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
122
|
+
setLocalError("请输入大于 0 的保留数量。");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
setLocalError("");
|
|
126
|
+
if (props.sessionId === undefined || props.sessionId === null) {
|
|
127
|
+
setLocalError("当前没有可压缩的会话。");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
props.scope.set("request", {
|
|
131
|
+
nonce: Date.now() * 1000 + Math.floor(Math.random() * 1000),
|
|
132
|
+
sessionId: props.sessionId,
|
|
133
|
+
keep: n,
|
|
134
|
+
mode: mode,
|
|
135
|
+
}).catch(function (error) {
|
|
136
|
+
console.error("[dsh-manual-compact-plugin] submit failed:", error);
|
|
137
|
+
setLocalError("请求发送失败,请稍后重试。");
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return react.createElement(
|
|
142
|
+
"div",
|
|
143
|
+
{ className: "dsmc", "data-manual-compact": true },
|
|
144
|
+
react.createElement("hr", { className: "dsmc-sep" }),
|
|
145
|
+
react.createElement("div", { className: "dsmc-title" }, "手动压缩"),
|
|
146
|
+
react.createElement(
|
|
147
|
+
"div",
|
|
148
|
+
{ className: "dsmc-row" },
|
|
149
|
+
react.createElement("label", null, "保留最近"),
|
|
150
|
+
react.createElement(
|
|
151
|
+
"select",
|
|
152
|
+
{ value: keep, disabled: busy, onChange: function (e) { setKeep(e.target.value); } },
|
|
153
|
+
react.createElement("option", { value: "5" }, "5 条"),
|
|
154
|
+
react.createElement("option", { value: "10" }, "10 条(推荐)"),
|
|
155
|
+
react.createElement("option", { value: "20" }, "20 条"),
|
|
156
|
+
react.createElement("option", { value: "30" }, "30 条"),
|
|
157
|
+
react.createElement("option", { value: "50" }, "50 条"),
|
|
158
|
+
react.createElement("option", { value: "custom" }, "自定义")
|
|
159
|
+
)
|
|
160
|
+
),
|
|
161
|
+
keep === "custom"
|
|
162
|
+
? react.createElement("input", {
|
|
163
|
+
type: "number",
|
|
164
|
+
min: 1,
|
|
165
|
+
max: 10000,
|
|
166
|
+
placeholder: "输入数量",
|
|
167
|
+
disabled: busy,
|
|
168
|
+
onChange: function (e) { setCustomKeep(e.target.value); },
|
|
169
|
+
})
|
|
170
|
+
: null,
|
|
171
|
+
react.createElement(
|
|
172
|
+
"div",
|
|
173
|
+
{ className: "dsmc-row" },
|
|
174
|
+
react.createElement("label", null, "处理方式"),
|
|
175
|
+
react.createElement(
|
|
176
|
+
"select",
|
|
177
|
+
{ value: mode, disabled: busy, onChange: function (e) { setMode(e.target.value); } },
|
|
178
|
+
react.createElement("option", { value: "stop" }, "完成一批后停止"),
|
|
179
|
+
react.createElement("option", { value: "batch" }, "分批处理直到完成")
|
|
180
|
+
)
|
|
181
|
+
),
|
|
182
|
+
react.createElement(
|
|
183
|
+
"div",
|
|
184
|
+
{ className: "dsmc-actions" },
|
|
185
|
+
react.createElement(
|
|
186
|
+
"button",
|
|
187
|
+
{ type: "button", className: "dsmc-confirm", disabled: busy, onClick: submit },
|
|
188
|
+
busy ? "处理中…" : "开始压缩"
|
|
189
|
+
)
|
|
190
|
+
),
|
|
191
|
+
localError
|
|
192
|
+
? react.createElement("div", { className: "dsmc-msg dsmc-err" }, localError)
|
|
193
|
+
: lastRun
|
|
194
|
+
? react.createElement(
|
|
195
|
+
"div",
|
|
196
|
+
{ className: lastRun.ok ? "dsmc-msg" : "dsmc-msg dsmc-err" },
|
|
197
|
+
lastRun.message + "(" + fmtTime(lastRun.at) + ")"
|
|
198
|
+
)
|
|
199
|
+
: null,
|
|
200
|
+
react.createElement("hr", { className: "dsmc-sep" }),
|
|
201
|
+
react.createElement("div", { className: "dsmc-title" }, "压缩存档(" + entries.length + ")"),
|
|
202
|
+
entries.length === 0
|
|
203
|
+
? react.createElement("div", { className: "dsmc-empty" }, "暂无压缩记录。")
|
|
204
|
+
: react.createElement(
|
|
205
|
+
"div",
|
|
206
|
+
{ className: "dsmc-archive" },
|
|
207
|
+
entries.map(function (n) {
|
|
208
|
+
return react.createElement(
|
|
209
|
+
"div",
|
|
210
|
+
{ key: String(n.seq) },
|
|
211
|
+
react.createElement(
|
|
212
|
+
"button",
|
|
213
|
+
{
|
|
214
|
+
type: "button",
|
|
215
|
+
className: "dsmc-arch-item",
|
|
216
|
+
onClick: function () { setExpandedSeq(expandedSeq === n.seq ? null : n.seq); },
|
|
217
|
+
},
|
|
218
|
+
fmtTime(n.time) + " · " + (n.shadowedItemCount === null ? "?" : n.shadowedItemCount) + " 条 · ~" + fmtTokens(n.shadowedTokenCount) + " tokens"
|
|
219
|
+
),
|
|
220
|
+
expandedSeq === n.seq && n.summary
|
|
221
|
+
? react.createElement("div", { className: "dsmc-arch-summary" }, n.summary)
|
|
222
|
+
: null
|
|
223
|
+
);
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Inline entry for the context meter popup (declared by patched DSH). */
|
|
230
|
+
function MeterEntry(props) {
|
|
231
|
+
return react.createElement(CompactControls, { scope: props.scope, session: props.session, sessionId: props.sessionId });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Composer-button entry with a floating popup, for stock DSH builds. */
|
|
235
|
+
function RightEntry(props) {
|
|
236
|
+
var scope = props.scope;
|
|
237
|
+
var [open, setOpen] = useState(false);
|
|
238
|
+
var rootRef = useRef(null);
|
|
239
|
+
useEffect(function () {
|
|
240
|
+
if (!open) return;
|
|
241
|
+
function onPointerDown(e) {
|
|
242
|
+
if (e.target instanceof Node && rootRef.current && rootRef.current.contains(e.target)) return;
|
|
243
|
+
setOpen(false);
|
|
244
|
+
}
|
|
245
|
+
function onKeyDown(e) {
|
|
246
|
+
if (e.key === "Escape") setOpen(false);
|
|
247
|
+
}
|
|
248
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
249
|
+
document.addEventListener("keydown", onKeyDown);
|
|
250
|
+
return function () {
|
|
251
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
252
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
253
|
+
};
|
|
254
|
+
}, [open]);
|
|
255
|
+
return react.createElement(
|
|
256
|
+
"span",
|
|
257
|
+
{ ref: rootRef, className: "dsmc-wrap" },
|
|
258
|
+
react.createElement(
|
|
259
|
+
"button",
|
|
260
|
+
{
|
|
261
|
+
type: "button",
|
|
262
|
+
className: "dsmc-btn",
|
|
263
|
+
"aria-expanded": open,
|
|
264
|
+
onClick: function () { setOpen(!open); },
|
|
265
|
+
},
|
|
266
|
+
"压缩"
|
|
267
|
+
),
|
|
268
|
+
open
|
|
269
|
+
? react.createElement(
|
|
270
|
+
"div",
|
|
271
|
+
{ className: "dsmc-pop", role: "dialog", "aria-label": "手动压缩上下文" },
|
|
272
|
+
react.createElement(CompactControls, { scope: scope, session: props.session, sessionId: props.sessionId })
|
|
273
|
+
)
|
|
274
|
+
: null
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Browser plugin body: bind the `manual-compact` settings scope, then
|
|
280
|
+
* register the meter-panel section and — only when that slot is absent —
|
|
281
|
+
* the composer button fallback.
|
|
282
|
+
* @param ctx - client cordis context.
|
|
283
|
+
*/
|
|
284
|
+
function apply(ctx) {
|
|
285
|
+
var scope = ctx.settingsScope.bind({ namespace: "manual-compact" });
|
|
286
|
+
var meterDeclared = false;
|
|
287
|
+
ctx.slots.inject("conversation.input.contextMeterPanel", function () {
|
|
288
|
+
meterDeclared = true;
|
|
289
|
+
return ctx.slots.register({
|
|
290
|
+
name: "conversation.input.contextMeterPanel",
|
|
291
|
+
id: "manual-compact",
|
|
292
|
+
order: 0,
|
|
293
|
+
}, function (props) {
|
|
294
|
+
return react.createElement(MeterEntry, { scope: scope, session: props.session, sessionId: props.sessionId });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
ctx.timeout(function () {
|
|
298
|
+
if (meterDeclared) return;
|
|
299
|
+
ctx.slots.inject("conversation.input.right", function () {
|
|
300
|
+
return ctx.slots.register({
|
|
301
|
+
name: "conversation.input.right",
|
|
302
|
+
id: "manual-compact-button",
|
|
303
|
+
order: 50,
|
|
304
|
+
}, function (props) {
|
|
305
|
+
return react.createElement(RightEntry, { scope: scope, session: props.session, sessionId: props.sessionId });
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}, 5000);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
exports.name = "manual-compact-ui";
|
|
312
|
+
exports.apply = apply;
|
|
313
|
+
// Cordis service names the browser loader must wait for — NOT package
|
|
314
|
+
// names (those belong in package.json's dsh.client.inject).
|
|
315
|
+
exports.inject = [
|
|
316
|
+
"slots",
|
|
317
|
+
"settingsScope",
|
|
318
|
+
"timer",
|
|
319
|
+
];
|
|
320
|
+
return module.exports;
|
|
321
|
+
},
|
|
322
|
+
});
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-manual-compact — host face.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `manual-compact` settings namespace as the Client<->Host
|
|
5
|
+
* transport (the same pattern dsh-plugin-proxy uses). The browser half writes
|
|
6
|
+
* a `request` object; this half executes the compaction through the current
|
|
7
|
+
* agent's official compaction seam (`compaction.compactRegion`) and writes the
|
|
8
|
+
* outcome into `lastRun`, clearing the request. Safe boundaries (tool-call
|
|
9
|
+
* pairing, busy session, cancellation) are enforced by the compaction seam.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import z from '@deepseek-ai/schemastery'
|
|
13
|
+
import { installSettingsSection } from '@deepseek-ai/dsh-settings'
|
|
14
|
+
|
|
15
|
+
const name = 'dsh-manual-compact-plugin'
|
|
16
|
+
const inject = ['settings', 'agents']
|
|
17
|
+
const NS = 'manual-compact'
|
|
18
|
+
|
|
19
|
+
/** Composition-row configuration (also the settings schema). */
|
|
20
|
+
const Config = z.object({
|
|
21
|
+
request: z.union([z.const(null), z.object({
|
|
22
|
+
nonce: z.number(),
|
|
23
|
+
sessionId: z.string(),
|
|
24
|
+
keep: z.number(),
|
|
25
|
+
mode: z.union([z.const('stop'), z.const('batch')]),
|
|
26
|
+
})]).default(null),
|
|
27
|
+
lastRun: z.union([z.const(null), z.object({
|
|
28
|
+
at: z.number(),
|
|
29
|
+
keep: z.number(),
|
|
30
|
+
mode: z.string(),
|
|
31
|
+
ok: z.boolean(),
|
|
32
|
+
shadowed: z.number(),
|
|
33
|
+
message: z.string(),
|
|
34
|
+
})]).default(null),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run one manual compaction request against the requesting session.
|
|
39
|
+
* @param ctx - registrant context (logger only).
|
|
40
|
+
* @param agents - live agent registry; resolves the session's agent.
|
|
41
|
+
* @param request - validated request payload.
|
|
42
|
+
* @returns the outcome: ok, shadowed item count, and a human message.
|
|
43
|
+
*/
|
|
44
|
+
async function executeCompact(ctx, agents, request) {
|
|
45
|
+
const agent = agents.get(request.sessionId)
|
|
46
|
+
const compaction = agent && agent.ctx && agent.ctx.get('compaction')
|
|
47
|
+
if (!agent || !compaction) {
|
|
48
|
+
return { ok: false, shadowed: 0, message: '当前会话暂时没有可用的压缩服务。' }
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
let total = 0
|
|
52
|
+
while (true) {
|
|
53
|
+
const nodes = agent.session.surface.nodes
|
|
54
|
+
if (!Array.isArray(nodes) || nodes.length <= request.keep) break
|
|
55
|
+
const endIndex = Math.min(nodes.length - request.keep - 1, request.mode === 'batch' ? 19 : nodes.length - request.keep - 1)
|
|
56
|
+
if (endIndex < 0) break
|
|
57
|
+
const result = await compaction.compactRegion(nodes[0], nodes[endIndex], agent)
|
|
58
|
+
total += result.shadowedSeqs.length
|
|
59
|
+
if (request.mode !== 'batch') break
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
shadowed: total,
|
|
64
|
+
message: total
|
|
65
|
+
? `已压缩 ${total} 条历史,保留最近 ${request.keep} 条。`
|
|
66
|
+
: `没有可压缩的历史,已保留最近 ${request.keep} 条。`,
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const messages = {
|
|
70
|
+
busy: '当前正在压缩或模型仍在工作,请稍后再试。',
|
|
71
|
+
changed: '会话发生变化,本次压缩已停止。',
|
|
72
|
+
summary: '无法生成有效摘要,本次压缩已停止。',
|
|
73
|
+
commit: '压缩提交未完成。',
|
|
74
|
+
persistence: '摘要生成完成,但保存会话失败。',
|
|
75
|
+
}
|
|
76
|
+
return { ok: false, shadowed: 0, message: messages[error && error.code] || `压缩失败:${error && error.message ? error.message : String(error)}` }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cordis plugin body.
|
|
82
|
+
* @param ctx - registrant context (`settings`, `agents`, `logger`).
|
|
83
|
+
* @param config - validated composition configuration.
|
|
84
|
+
*/
|
|
85
|
+
function apply(ctx, config) {
|
|
86
|
+
let getter = () => config
|
|
87
|
+
let lastHandledNonce = null
|
|
88
|
+
let handling = null
|
|
89
|
+
const handle = () => {
|
|
90
|
+
if (handling !== null) return
|
|
91
|
+
handling = (async () => {
|
|
92
|
+
try {
|
|
93
|
+
const current = getter()
|
|
94
|
+
const request = current && current.request
|
|
95
|
+
if (request === null || request === undefined || request.nonce === lastHandledNonce) return
|
|
96
|
+
lastHandledNonce = request.nonce
|
|
97
|
+
const result = await executeCompact(ctx, ctx.agents, request)
|
|
98
|
+
await ctx.settings.update(NS, {
|
|
99
|
+
request: null,
|
|
100
|
+
lastRun: {
|
|
101
|
+
at: Date.now(),
|
|
102
|
+
keep: request.keep,
|
|
103
|
+
mode: request.mode,
|
|
104
|
+
ok: result.ok,
|
|
105
|
+
shadowed: result.shadowed,
|
|
106
|
+
message: result.message,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
} catch (error) {
|
|
110
|
+
ctx.logger.warn('manual-compact: handling a request failed: %s', String(error))
|
|
111
|
+
} finally {
|
|
112
|
+
handling = null
|
|
113
|
+
}
|
|
114
|
+
})()
|
|
115
|
+
}
|
|
116
|
+
installSettingsSection(ctx, NS, Config, config, {
|
|
117
|
+
setSource: (current) => {
|
|
118
|
+
getter = current
|
|
119
|
+
handle()
|
|
120
|
+
},
|
|
121
|
+
onChange: () => {
|
|
122
|
+
handle()
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { Config, apply, inject, name }
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-manual-compact-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Manual context compaction for DeepSeek Harness: keep the latest N conversation items with stop or batch mode and a per-session compaction archive — embedded in the context meter popup when the meter panel slot exists, or a composer button otherwise.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"cordis.patch.yml",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"dsh",
|
|
20
|
+
"dsh-plugin",
|
|
21
|
+
"cordis",
|
|
22
|
+
"compaction",
|
|
23
|
+
"context",
|
|
24
|
+
"archive"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dsh": {
|
|
28
|
+
"bundle": {
|
|
29
|
+
"patch": "./cordis.patch.yml"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"inject": [
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
34
|
+
"@deepseek-ai/dsh-settings"
|
|
35
|
+
],
|
|
36
|
+
"platform": "web"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
42
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
|
|
44
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
45
|
+
"react": "^18.2.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-conversation": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/kimkingyu/dsh-manual-compact.git"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/kimkingyu/dsh-manual-compact#readme",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/kimkingyu/dsh-manual-compact/issues"
|
|
62
|
+
},
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
}
|
|
66
|
+
}
|