dsh-message-rollback 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -2
- package/cordis.patch.yml +3 -0
- package/lib/client.d.ts +7 -0
- package/lib/client.js +133 -0
- package/lib/index-vsThIC1E.d.ts +32 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/src-_KoxvZTI.js +8 -0
- package/package.json +56 -4
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DeepSeek Harness 消息回滚插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
这个插件向每条已完成的助手消息的操作栏添加“↶ 回滚”按钮。点击后可以选择:
|
|
4
|
+
|
|
5
|
+
- 只回滚消息:创建一个从该消息之前截断的新会话,原会话保留。
|
|
6
|
+
- 同时回滚代码更改:调用 Host 提供的 `messageRollback.rollback()`,由 Host 在同一事务中恢复消息之后的工作区快照。
|
|
7
|
+
|
|
8
|
+
插件使用 Harness 的公开 UI slot:`conversation.chat.assistant-actions`。它通过当前 Session 的事件窗口把消息 ID 映射到 durable event sequence,避免依赖 DOM 结构。
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
把本目录作为本地插件包加入 Loader 配置:
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
plugins:
|
|
16
|
+
- package: ./dsh-message-rollback
|
|
17
|
+
config: {}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
构建:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm run bundle
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 代码更改回滚的 Host 适配
|
|
28
|
+
|
|
29
|
+
Harness 当前公开的 `SessionFace` 提供 fork、队列和提示词控制,但不允许 UI 插件直接改写工作区。因此“同时回滚代码更改”需要 Host 注册 `ctx.messageRollback`,或者注册较小的 `ctx.workspaceRollback`:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
ctx.workspaceRollback = {
|
|
33
|
+
async rollbackTurn({ sessionId, eventSeq }) {
|
|
34
|
+
// 在 Host 侧把工作区恢复到 eventSeq 对应 turn 开始时的快照。
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
如果需要让 Host 同时负责恢复工作区、创建回滚 Session 并切换当前页面,可以注册完整服务:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
ctx.messageRollback = {
|
|
43
|
+
async rollback({ sessionId, messageId, includeWorkspaceChanges }) {
|
|
44
|
+
// 在 Host 侧定位消息和 turn 的快照。
|
|
45
|
+
// includeWorkspaceChanges 为 true 时恢复 turn 开始时的工作区。
|
|
46
|
+
// 然后切出/切换到回滚后的 Session。
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
未注册工作区回滚能力时,勾选代码更改会给出明确提示;未勾选时插件使用公开的 `ctx.sessions.fork()` 作为安全回退,原会话不会被删除。
|
package/cordis.patch.yml
ADDED
package/lib/client.d.ts
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { n as inject, r as name } from "./src-_KoxvZTI.js";
|
|
2
|
+
import { createElement, useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/client/index.tsx
|
|
5
|
+
const buttonStyle = {
|
|
6
|
+
alignItems: "center",
|
|
7
|
+
background: "transparent",
|
|
8
|
+
border: 0,
|
|
9
|
+
borderRadius: 6,
|
|
10
|
+
color: "var(--dsh-color-text-secondary, #777)",
|
|
11
|
+
cursor: "pointer",
|
|
12
|
+
display: "inline-flex",
|
|
13
|
+
fontSize: 12,
|
|
14
|
+
gap: 4,
|
|
15
|
+
padding: "4px 6px"
|
|
16
|
+
};
|
|
17
|
+
const panelStyle = {
|
|
18
|
+
background: "var(--dsh-color-surface, Canvas)",
|
|
19
|
+
border: "1px solid var(--dsh-color-border, #ddd)",
|
|
20
|
+
borderRadius: 8,
|
|
21
|
+
boxShadow: "0 8px 24px rgb(0 0 0 / 18%)",
|
|
22
|
+
display: "grid",
|
|
23
|
+
gap: 8,
|
|
24
|
+
minWidth: 240,
|
|
25
|
+
padding: 12,
|
|
26
|
+
position: "absolute",
|
|
27
|
+
right: 0,
|
|
28
|
+
top: "calc(100% + 4px)",
|
|
29
|
+
zIndex: 20
|
|
30
|
+
};
|
|
31
|
+
function messageEventEntries(ctx, sessionId) {
|
|
32
|
+
return ctx.sessions.binding(sessionId)?.eventSource.getSnapshot().entries ?? [];
|
|
33
|
+
}
|
|
34
|
+
function findMessageSeq(ctx, sessionId, messageId) {
|
|
35
|
+
for (const entry of messageEventEntries(ctx, sessionId)) {
|
|
36
|
+
if (entry.type !== "event") continue;
|
|
37
|
+
const event = entry.event;
|
|
38
|
+
if (event.data?.message?.id === messageId) return event.seq;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function executeRollback(ctx, sessionId, messageId, includeWorkspaceChanges) {
|
|
42
|
+
const eventSeq = findMessageSeq(ctx, sessionId, messageId);
|
|
43
|
+
if (eventSeq === void 0) throw new Error("该消息尚未出现在当前历史窗口中,请先加载更多历史。");
|
|
44
|
+
if (ctx.messageRollback) {
|
|
45
|
+
await ctx.messageRollback.rollback({
|
|
46
|
+
sessionId,
|
|
47
|
+
messageId,
|
|
48
|
+
includeWorkspaceChanges
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (includeWorkspaceChanges) {
|
|
53
|
+
if (!ctx.workspaceRollback) throw new Error("当前 Host 没有提供代码更改回滚能力;请取消“同时回滚代码更改”,或安装对应的 Host 端适配。");
|
|
54
|
+
await ctx.workspaceRollback.rollbackTurn({
|
|
55
|
+
sessionId,
|
|
56
|
+
eventSeq
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
await ctx.sessions.fork({
|
|
60
|
+
sessionId,
|
|
61
|
+
atSeq: Math.max(0, eventSeq - 1),
|
|
62
|
+
increaseTitle: true
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function RollbackAction({ messageId, ctx, sessionId }) {
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
const [includeWorkspaceChanges, setIncludeWorkspaceChanges] = useState(false);
|
|
68
|
+
const [busy, setBusy] = useState(false);
|
|
69
|
+
const [error, setError] = useState();
|
|
70
|
+
const stableMessageId = useMemo(() => String(messageId), [messageId]);
|
|
71
|
+
const submit = async (event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
setBusy(true);
|
|
74
|
+
setError(void 0);
|
|
75
|
+
try {
|
|
76
|
+
await executeRollback(ctx, sessionId, stableMessageId, includeWorkspaceChanges);
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
setError(cause instanceof Error ? cause.message : "回滚失败,请稍后重试。");
|
|
79
|
+
} finally {
|
|
80
|
+
setBusy(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return createElement("span", { style: { position: "relative" } }, createElement("button", {
|
|
84
|
+
type: "button",
|
|
85
|
+
title: "回滚到此消息",
|
|
86
|
+
"aria-expanded": open,
|
|
87
|
+
style: buttonStyle,
|
|
88
|
+
onClick: () => {
|
|
89
|
+
setOpen((value) => !value);
|
|
90
|
+
setError(void 0);
|
|
91
|
+
},
|
|
92
|
+
disabled: busy
|
|
93
|
+
}, "↶ 回滚"), open && createElement("form", {
|
|
94
|
+
style: panelStyle,
|
|
95
|
+
onSubmit: submit
|
|
96
|
+
}, createElement("strong", null, "回滚到此消息?"), createElement("label", { style: {
|
|
97
|
+
alignItems: "flex-start",
|
|
98
|
+
display: "flex",
|
|
99
|
+
gap: 8
|
|
100
|
+
} }, createElement("input", {
|
|
101
|
+
type: "checkbox",
|
|
102
|
+
checked: includeWorkspaceChanges,
|
|
103
|
+
onChange: (event) => setIncludeWorkspaceChanges(event.target.checked)
|
|
104
|
+
}), createElement("span", null, "同时回滚这条消息之后的代码更改")), createElement("small", { style: { color: "var(--dsh-color-text-secondary, #777)" } }, "默认会创建一个回滚后的新会话,原会话保留。"), error && createElement("div", {
|
|
105
|
+
role: "alert",
|
|
106
|
+
style: { color: "var(--dsh-color-danger, #c33)" }
|
|
107
|
+
}, error), createElement("div", { style: {
|
|
108
|
+
display: "flex",
|
|
109
|
+
gap: 8,
|
|
110
|
+
justifyContent: "flex-end"
|
|
111
|
+
} }, createElement("button", {
|
|
112
|
+
type: "button",
|
|
113
|
+
onClick: () => setOpen(false)
|
|
114
|
+
}, "取消"), createElement("button", {
|
|
115
|
+
type: "submit",
|
|
116
|
+
disabled: busy
|
|
117
|
+
}, busy ? "回滚中…" : "确认回滚"))));
|
|
118
|
+
}
|
|
119
|
+
function apply(ctx) {
|
|
120
|
+
ctx.slots.register({
|
|
121
|
+
name: "conversation.chat.assistant-actions",
|
|
122
|
+
id: name,
|
|
123
|
+
order: 500,
|
|
124
|
+
registrant: name
|
|
125
|
+
}, (props) => props.sessionId ? createElement(RollbackAction, {
|
|
126
|
+
...props,
|
|
127
|
+
ctx,
|
|
128
|
+
sessionId: props.sessionId
|
|
129
|
+
}) : null);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
export { apply, inject };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
/** Host-side mutation capability consumed by the browser surface. */
|
|
5
|
+
interface MessageRollbackService {
|
|
6
|
+
rollback(input: {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
messageId: string;
|
|
9
|
+
includeWorkspaceChanges: boolean;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** Optional Host capability for restoring the workspace snapshot for a turn. */
|
|
15
|
+
interface WorkspaceRollbackService {
|
|
16
|
+
rollbackTurn(input: {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
eventSeq: number;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
declare module '@deepseek-ai/cordis' {
|
|
22
|
+
interface Context {
|
|
23
|
+
messageRollback?: MessageRollbackService;
|
|
24
|
+
workspaceRollback?: WorkspaceRollbackService;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
declare const name = "message-rollback";
|
|
28
|
+
declare const inject: string[];
|
|
29
|
+
/** The node half is intentionally small; rollback is a browser action. */
|
|
30
|
+
declare function apply(_ctx: Context): void;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { name as a, inject as i, WorkspaceRollbackService as n, apply as r, MessageRollbackService as t };
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
const name = "message-rollback";
|
|
3
|
+
const inject = ["@deepseek-ai/dsh-client-ui-renderer", "@deepseek-ai/dsh-api-session-controller"];
|
|
4
|
+
/** The node half is intentionally small; rollback is a browser action. */
|
|
5
|
+
function apply(_ctx) {}
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
export { inject as n, name as r, apply as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-message-rollback",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DeepSeek Harness plugin that adds per-message rollback controls.",
|
|
5
|
+
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "rollback"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22",
|
|
9
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "lib/index.js",
|
|
13
|
+
"types": "lib/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
|
|
16
|
+
"./client": { "types": "./lib/client.d.ts", "default": "./lib/client.js" }
|
|
17
|
+
},
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"inject": [
|
|
24
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
25
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-session"
|
|
28
|
+
],
|
|
29
|
+
"platform": "web"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"bundle": "tsdown",
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
38
|
+
"react": "^18.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
42
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.2.0-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|
|
48
|
+
"@types/react": "~18.3.1",
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"tsdown": "^0.15.0",
|
|
51
|
+
"typescript": "^5.6.0"
|
|
52
|
+
},
|
|
53
|
+
"files": ["lib/*.js", "lib/*.d.ts", "lib/types/**/*.d.ts", "cordis.patch.yml", "README.md"],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public",
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
}
|
|
58
|
+
}
|