dsh-db-studio 1.0.2 → 1.0.3
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/CHANGELOG.md +4 -1
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +349 -7
- package/lib/index.js +10 -4
- package/lib/service.js +79 -6
- package/lib/session-store.js +64 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v1.0.
|
|
3
|
+
## v1.0.3 — 2026-10-25
|
|
4
4
|
|
|
5
5
|
- 新增 `/db` 斜杠指令:输入 `/` 后可从指令列表直接打开数据库面板,带数据库图标
|
|
6
|
+
- 修复对话内数据库 tab 中数据表列表与数据页滚动未分离的问题;现在左侧表列表和右侧数据各自独立滚动
|
|
7
|
+
- 新增对话数据库面板「查询历史」tab:按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时、行数和成功状态,支持一键复现到 SQL 编辑器
|
|
8
|
+
- 左侧全局面板新增「查询历史」tab:按对话分组展示历史,点击分组或记录可进入对应对话
|
|
6
9
|
|
|
7
10
|
## v1.0.1 — 2026-10-24
|
|
8
11
|
|
package/README.en.md
CHANGED
|
@@ -24,6 +24,7 @@ Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepse
|
|
|
24
24
|
- **Multi-select picker in the composer**: choose the databases this conversation may use.
|
|
25
25
|
- **Talk to the data**: four agent tools for querying, creating tables and writing data.
|
|
26
26
|
- **`/db` command**: type `/`, pick **Database** from the command list, and open the database panel.
|
|
27
|
+
- **Query history**: a **Query history** tab in the conversation database panel groups SQL by user turn, supports collapsing, and records SQL, connection, timing and affected rows; click an entry to load it back into the SQL editor. The global database panel groups the same history by conversation, and clicking a group opens that conversation.
|
|
27
28
|
|
|
28
29
|
## Install
|
|
29
30
|
|
|
@@ -32,7 +33,7 @@ From npm:
|
|
|
32
33
|
```bash
|
|
33
34
|
# macOS desktop
|
|
34
35
|
DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
|
|
35
|
-
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.
|
|
36
|
+
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
Or install directly from GitHub:
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- **输入框旁多选下拉**:选择本次对话要使用的数据库,支持多选。
|
|
23
23
|
- **对话中直接操作**:内置 4 个助手工具,可以帮你查询、建表、写数据。
|
|
24
24
|
- **`/db` 指令**:输入 `/`,在指令列表里选择「数据库」,直接打开数据库面板。
|
|
25
|
+
- **查询历史**:对话数据库面板新增「查询历史」tab,按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时和影响行数,点击即可复现;左侧全局面板的查询历史按对话分组,点击可进入对应对话。
|
|
25
26
|
|
|
26
27
|
## 安装
|
|
27
28
|
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
```bash
|
|
31
32
|
# macOS 桌面端
|
|
32
33
|
DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
|
|
33
|
-
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.
|
|
34
|
+
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
也可以直接从 GitHub 安装:
|
package/lib/client.js
CHANGED
|
@@ -26,6 +26,24 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
schema: "结构",
|
|
27
27
|
data: "数据",
|
|
28
28
|
sql: "SQL",
|
|
29
|
+
history: "查询历史",
|
|
30
|
+
historyEmpty: "还没有查询记录。在 SQL 页运行查询后会出现在这里。",
|
|
31
|
+
historyClear: "清空历史",
|
|
32
|
+
historyReproduce: "复现",
|
|
33
|
+
historyReproduceHint: "载入到 SQL 页,不会自动执行",
|
|
34
|
+
historySuccess: "成功",
|
|
35
|
+
historyFailed: "失败",
|
|
36
|
+
historyRows: "{n} 行",
|
|
37
|
+
historyChanges: "影响 {n} 行",
|
|
38
|
+
historyAgent: "助手",
|
|
39
|
+
historyConsole: "控制台",
|
|
40
|
+
historyUnavailable: "连接 {name} 已不在当前会话的选择范围内",
|
|
41
|
+
historyOpen: "进入对话",
|
|
42
|
+
historyOpenFailed: "无法打开对话",
|
|
43
|
+
historyOpenFailedDetail: "打开对话失败:{error}",
|
|
44
|
+
historyTurn: "第 {n} 轮",
|
|
45
|
+
historyManual: "手动查询",
|
|
46
|
+
historyTurnNoPrompt: "未命名输入",
|
|
29
47
|
run: "运行",
|
|
30
48
|
running: "运行中…",
|
|
31
49
|
searchTable: "搜索表…",
|
|
@@ -145,6 +163,24 @@ window.__ModuleLoader__.load({
|
|
|
145
163
|
schema: "Schema",
|
|
146
164
|
data: "Data",
|
|
147
165
|
sql: "SQL",
|
|
166
|
+
history: "Query history",
|
|
167
|
+
historyEmpty: "No queries yet. Run one from the SQL tab and it will show up here.",
|
|
168
|
+
historyClear: "Clear history",
|
|
169
|
+
historyReproduce: "Reproduce",
|
|
170
|
+
historyReproduceHint: "Load into the SQL tab without running it",
|
|
171
|
+
historySuccess: "OK",
|
|
172
|
+
historyFailed: "Failed",
|
|
173
|
+
historyRows: "{n} rows",
|
|
174
|
+
historyChanges: "{n} rows affected",
|
|
175
|
+
historyAgent: "Assistant",
|
|
176
|
+
historyConsole: "Console",
|
|
177
|
+
historyUnavailable: "Connection {name} is no longer selected for this conversation",
|
|
178
|
+
historyOpen: "Open conversation",
|
|
179
|
+
historyOpenFailed: "Could not open the conversation",
|
|
180
|
+
historyOpenFailedDetail: "Could not open the conversation: {error}",
|
|
181
|
+
historyTurn: "Turn {n}",
|
|
182
|
+
historyManual: "Manual queries",
|
|
183
|
+
historyTurnNoPrompt: "Untitled input",
|
|
148
184
|
run: "Run",
|
|
149
185
|
running: "Running…",
|
|
150
186
|
searchTable: "Search tables…",
|
|
@@ -518,6 +554,7 @@ window.__ModuleLoader__.load({
|
|
|
518
554
|
function CloseIcon(props) { return h(Svg, props, h("path", { d: "M6 6l12 12M18 6 6 18" })); }
|
|
519
555
|
function CheckIcon(props) { return h(Svg, props, h("path", { d: "M5 12.5 10 17.5 19 7" })); }
|
|
520
556
|
function SearchIcon(props) { return h(Svg, props, h("circle", { cx: 11, cy: 11, r: 6.5 }), h("path", { d: "m16 16 4 4" })); }
|
|
557
|
+
function HistoryIcon(props) { return h(Svg, props, h("circle", { cx: 12, cy: 12, r: 8 }), h("path", { d: "M12 7.5V12l3 2" })); }
|
|
521
558
|
function SparkIcon(props) { return h(Svg, props, h("path", { d: "M12 3.5 13.8 9l5.5 1.8-5.5 1.8L12 18l-1.8-5.4L4.7 10.8 10.2 9 12 3.5Z" })); }
|
|
522
559
|
function ChevronDownIcon(props) { return h(Svg, props, h("path", { d: "m6 9.5 6 6 6-6" })); }
|
|
523
560
|
function CopyIcon(props) { return h(Svg, props, h("rect", { x: 8.5, y: 8.5, width: 11, height: 11, rx: 2 }), h("path", { d: "M15.5 5.5h-9a2 2 0 0 0-2 2v9" })); }
|
|
@@ -714,7 +751,12 @@ window.__ModuleLoader__.load({
|
|
|
714
751
|
.dshdb-statuspop-name{font-size:12px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
715
752
|
.dshdb-statuspop-detail{font-size:10.5px;color:var(--dsw-alias-label-tertiary);line-height:1.4;word-break:break-word}
|
|
716
753
|
.dshdb-statuspop-row:has(.dshdb-dot.bad) .dshdb-statuspop-detail{color:var(--dsw-alias-state-error-primary)}
|
|
717
|
-
.dshdb-cv{height:100%;min-height:
|
|
754
|
+
.dshdb-cv{height:100%;min-height:0;display:flex;flex-direction:column;padding:14px 22px 16px;box-sizing:border-box;overflow:hidden;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);background:var(--dsw-alias-bg-base)}
|
|
755
|
+
/* The conversation shell gives a view an auto-height flex row; without bounding
|
|
756
|
+
that ancestor the workbench grows with the table list and drags the data pane
|
|
757
|
+
with it. Bounding it keeps the table list and the data grid on separate
|
|
758
|
+
scroll ports, exactly like the global panel. */
|
|
759
|
+
*:has(> * > .dshdb-cv){flex:1 1 0!important;min-height:0!important;overflow:hidden!important}
|
|
718
760
|
.dshdb-cv *{box-sizing:border-box}
|
|
719
761
|
.dshdb-cvhead{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;flex:0 0 auto}
|
|
720
762
|
.dshdb-cvtitlewrap{display:flex;align-items:center;gap:10px;min-width:0}
|
|
@@ -735,6 +777,38 @@ window.__ModuleLoader__.load({
|
|
|
735
777
|
.dshdb-cv .dshdb-pager{padding:7px 12px}
|
|
736
778
|
.dshdb-cv .dshdb-sqlpane{padding:12px 14px;gap:10px;height:100%}
|
|
737
779
|
.dshdb-cv .dshdb-tabs{margin-top:3px;flex:0 0 auto}
|
|
780
|
+
.dshdb-historypage{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:14px 18px;gap:8px;overflow:hidden}
|
|
781
|
+
.dshdb-historypagehead{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
782
|
+
.dshdb-historygroups{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding:4px 2px 6px}
|
|
783
|
+
.dshdb-historygroup{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;overflow:hidden;background:var(--dsw-alias-bg-base)}
|
|
784
|
+
.dshdb-historygrouphead{appearance:none;border:0;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 38%,var(--dsw-alias-bg-base));color:var(--dsw-alias-label-primary);font:inherit;width:100%;text-align:left;display:flex;align-items:center;gap:9px;padding:9px 11px;cursor:pointer}
|
|
785
|
+
.dshdb-historygrouphead:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
786
|
+
.dshdb-historygroupmain{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:2px}
|
|
787
|
+
.dshdb-historygrouptitle{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
788
|
+
.dshdb-historygroupcwd{font-size:10px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
789
|
+
.dshdb-historygroupcount{font-size:9.5px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;padding:1px 6px;flex:0 0 auto}
|
|
790
|
+
.dshdb-historyopen{display:inline-flex;align-items:center;gap:3px;font-size:10.5px;color:var(--dsw-alias-brand-primary);flex:0 0 auto}
|
|
791
|
+
.dshdb-historygrouprows{display:flex;flex-direction:column;gap:4px;padding:6px}
|
|
792
|
+
.dshdb-historypane{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:12px 14px;gap:8px;overflow:hidden}
|
|
793
|
+
.dshdb-historybar{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:2px 2px 8px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
794
|
+
.dshdb-historytitle{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:600}
|
|
795
|
+
.dshdb-historyturn{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-base)}
|
|
796
|
+
.dshdb-historyturnhead{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;width:100%;text-align:left;display:flex;align-items:center;gap:7px;padding:7px 9px;cursor:pointer}
|
|
797
|
+
.dshdb-historyturnhead:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
798
|
+
.dshdb-historyturnlabel{font-size:11px;font-weight:600;flex:0 0 auto;white-space:nowrap}
|
|
799
|
+
.dshdb-historyturnprompt{font-size:11px;color:var(--dsw-alias-label-tertiary);min-width:0;flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
800
|
+
.dshdb-historyturncount{font-size:9.5px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;padding:1px 6px;flex:0 0 auto}
|
|
801
|
+
.dshdb-historyturnrows{display:flex;flex-direction:column;gap:4px;padding:6px;border-top:1px solid var(--dsw-alias-border-l2)}
|
|
802
|
+
.dshdb-historylist{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:8px 2px}
|
|
803
|
+
.dshdb-historyrow{display:flex;align-items:flex-start;gap:8px;width:100%;text-align:left;appearance:none;border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:8px;padding:8px 9px;cursor:pointer;font:inherit;color:var(--dsw-alias-label-primary)}
|
|
804
|
+
.dshdb-historyrow:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
805
|
+
.dshdb-historyrow.bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 28%,var(--dsw-alias-border-l2))}
|
|
806
|
+
.dshdb-historystatus{display:flex;color:var(--dsw-alias-state-success-primary);flex:0 0 auto;margin-top:2px}
|
|
807
|
+
.dshdb-historyrow.bad .dshdb-historystatus{color:var(--dsw-alias-state-error-primary)}
|
|
808
|
+
.dshdb-historymain{min-width:0;flex:1 1 auto;display:flex;flex-direction:column;gap:3px}
|
|
809
|
+
.dshdb-historysql{font-family:var(--ds-font-family-code);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary)}
|
|
810
|
+
.dshdb-historymeta{display:flex;gap:7px;flex-wrap:wrap;font-size:10px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
|
|
811
|
+
.dshdb-historyaction{display:inline-flex;align-items:center;gap:4px;flex:0 0 auto;font-size:10.5px;color:var(--dsw-alias-brand-primary);padding:2px 3px;white-space:nowrap}
|
|
738
812
|
.dshdb-cvempty{flex:1;display:flex;align-items:center;justify-content:center;padding:26px;min-height:300px}
|
|
739
813
|
.dshdb-cvloading{flex:1;display:flex;align-items:center;justify-content:center;min-height:300px}
|
|
740
814
|
.dshdb-cvmessage{padding:6px 0;flex:0 0 auto}
|
|
@@ -1079,6 +1153,10 @@ window.__ModuleLoader__.load({
|
|
|
1079
1153
|
var [sqlError, setSqlError] = React.useState(null);
|
|
1080
1154
|
var [sqlLoading, setSqlLoading] = React.useState(false);
|
|
1081
1155
|
var [sqlHistory, setSqlHistory] = React.useState([]);
|
|
1156
|
+
var [historyGroups, setHistoryGroups] = React.useState([]);
|
|
1157
|
+
var [historyLoading, setHistoryLoading] = React.useState(false);
|
|
1158
|
+
var [historyError, setHistoryError] = React.useState(null);
|
|
1159
|
+
var [sessionTitles, setSessionTitles] = React.useState({});
|
|
1082
1160
|
var [dialog, setDialog] = React.useState(null);
|
|
1083
1161
|
var [pendingDelete, setPendingDelete] = React.useState(null);
|
|
1084
1162
|
var [busy, setBusy] = React.useState(false);
|
|
@@ -1101,6 +1179,52 @@ window.__ModuleLoader__.load({
|
|
|
1101
1179
|
return function () { alive = false; };
|
|
1102
1180
|
}, []);
|
|
1103
1181
|
|
|
1182
|
+
React.useEffect(function () {
|
|
1183
|
+
var sessions = APP_CTX && APP_CTX.get ? APP_CTX.get("sessions") : null;
|
|
1184
|
+
if (!sessions || !sessions.list) return;
|
|
1185
|
+
function update() {
|
|
1186
|
+
var snapshot = sessions.list.getSnapshot();
|
|
1187
|
+
setSessionTitles(snapshot && snapshot.byId ? snapshot.byId : {});
|
|
1188
|
+
}
|
|
1189
|
+
update();
|
|
1190
|
+
var off = sessions.list.subscribe ? sessions.list.subscribe(update) : null;
|
|
1191
|
+
return function () { if (typeof off === "function") off(); };
|
|
1192
|
+
}, []);
|
|
1193
|
+
|
|
1194
|
+
React.useEffect(function () {
|
|
1195
|
+
if (tab === "history") loadAllHistory();
|
|
1196
|
+
}, [tab]);
|
|
1197
|
+
|
|
1198
|
+
function loadAllHistory() {
|
|
1199
|
+
setHistoryLoading(true);
|
|
1200
|
+
setHistoryError(null);
|
|
1201
|
+
api({ action: "history-all" }).then(function (data) {
|
|
1202
|
+
setHistoryGroups(Array.isArray(data.sessions) ? data.sessions : []);
|
|
1203
|
+
}).catch(function (failure) {
|
|
1204
|
+
setHistoryError(interpolate(t("loadFailed"), { error: failure.message }));
|
|
1205
|
+
}).finally(function () { setHistoryLoading(false); });
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function sessionLabel(sessionId) {
|
|
1209
|
+
var row = sessionTitles[sessionId];
|
|
1210
|
+
var title = row && (row.title || row.displayTitle);
|
|
1211
|
+
if (title && String(title).trim()) return String(title).trim();
|
|
1212
|
+
return String(sessionId || "").slice(0, 8);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function openConversation(sessionId) {
|
|
1216
|
+
var workspace = APP_CTX && APP_CTX.get ? APP_CTX.get("uiWorkspace") : null;
|
|
1217
|
+
if (!workspace || typeof workspace.openSession !== "function") {
|
|
1218
|
+
setHistoryError(t("historyOpenFailed"));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
try {
|
|
1222
|
+
workspace.openSession(sessionId);
|
|
1223
|
+
} catch (failure) {
|
|
1224
|
+
setHistoryError(interpolate(t("historyOpenFailedDetail"), { error: failure.message }));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1104
1228
|
React.useEffect(function () {
|
|
1105
1229
|
if (!activeId) { setTables([]); setSelectedTable(null); setSchema(null); setResult(null); return; }
|
|
1106
1230
|
var alive = true;
|
|
@@ -1445,6 +1569,62 @@ window.__ModuleLoader__.load({
|
|
|
1445
1569
|
) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
|
|
1446
1570
|
);
|
|
1447
1571
|
|
|
1572
|
+
var historyPane = h("div", { className: "dshdb-historypage" },
|
|
1573
|
+
h("div", { className: "dshdb-historypagehead" },
|
|
1574
|
+
h("div", { className: "dshdb-historytitle" },
|
|
1575
|
+
h(HistoryIcon, { size: 14 }),
|
|
1576
|
+
t("history"),
|
|
1577
|
+
h("span", { className: "dshdb-gridmeta" },
|
|
1578
|
+
historyGroups.reduce(function (sum, group) { return sum + (group.entries ? group.entries.length : 0); }, 0))
|
|
1579
|
+
),
|
|
1580
|
+
h("button", { type: "button", className: "dshdb-btn small ghost", disabled: historyLoading, onClick: loadAllHistory }, h(RefreshIcon, { size: 13 }), t("refresh"))
|
|
1581
|
+
),
|
|
1582
|
+
historyError ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, historyError)) : null,
|
|
1583
|
+
historyLoading
|
|
1584
|
+
? h("div", { className: "dshdb-placeholder" }, h(Spinner))
|
|
1585
|
+
: historyGroups.length === 0
|
|
1586
|
+
? h("div", { className: "dshdb-placeholder" }, t("historyEmpty"))
|
|
1587
|
+
: h("div", { className: "dshdb-historygroups" }, historyGroups.map(function (group) {
|
|
1588
|
+
var rows = Array.isArray(group.entries) ? group.entries : [];
|
|
1589
|
+
var folder = group.cwd ? String(group.cwd).split(/[\\/]/).filter(Boolean).pop() : "";
|
|
1590
|
+
return h("div", { key: group.sessionId, className: "dshdb-historygroup" },
|
|
1591
|
+
h("button", { type: "button", className: "dshdb-historygrouphead", onClick: function () { openConversation(group.sessionId); } },
|
|
1592
|
+
h("span", { className: "dshdb-historygroupmain" },
|
|
1593
|
+
h("span", { className: "dshdb-historygrouptitle" }, sessionLabel(group.sessionId)),
|
|
1594
|
+
folder ? h("span", { className: "dshdb-historygroupcwd" }, folder) : null
|
|
1595
|
+
),
|
|
1596
|
+
h("span", { className: "dshdb-historygroupcount" }, rows.length),
|
|
1597
|
+
h("span", { className: "dshdb-historyopen" }, t("historyOpen"), h(ChevronRightIcon, { size: 11 }))
|
|
1598
|
+
),
|
|
1599
|
+
h("div", { className: "dshdb-historygrouprows" }, rows.map(function (row) {
|
|
1600
|
+
var ok = row.ok !== false;
|
|
1601
|
+
var source = row.source === "agent" ? t("historyAgent") : t("historyConsole");
|
|
1602
|
+
var count = row.changes != null
|
|
1603
|
+
? interpolate(t("historyChanges"), { n: fmtNumber(row.changes) })
|
|
1604
|
+
: interpolate(t("historyRows"), { n: fmtNumber(row.rows || 0) });
|
|
1605
|
+
return h("button", {
|
|
1606
|
+
key: row.id,
|
|
1607
|
+
type: "button",
|
|
1608
|
+
className: "dshdb-historyrow" + (ok ? "" : " bad"),
|
|
1609
|
+
title: (ok ? "" : (row.error || t("historyFailed")) + " · ") + t("historyOpen"),
|
|
1610
|
+
onClick: function () { openConversation(group.sessionId); }
|
|
1611
|
+
},
|
|
1612
|
+
h("span", { className: "dshdb-historystatus" }, h(ok ? CheckIcon : CloseIcon, { size: 13 })),
|
|
1613
|
+
h("span", { className: "dshdb-historymain" },
|
|
1614
|
+
h("span", { className: "dshdb-historysql" }, row.sql),
|
|
1615
|
+
h("span", { className: "dshdb-historymeta" },
|
|
1616
|
+
h("span", null, row.connectionName || row.connectionId),
|
|
1617
|
+
h("span", null, source),
|
|
1618
|
+
h("span", null, new Date(row.at).toLocaleString()),
|
|
1619
|
+
h("span", null, count)
|
|
1620
|
+
)
|
|
1621
|
+
)
|
|
1622
|
+
);
|
|
1623
|
+
}))
|
|
1624
|
+
);
|
|
1625
|
+
}))
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1448
1628
|
main = h("section", { className: "dshdb-main" },
|
|
1449
1629
|
h("div", { className: "dshdb-mainhead" },
|
|
1450
1630
|
h("div", { className: "dshdb-mainheadtop" },
|
|
@@ -1472,11 +1652,12 @@ window.__ModuleLoader__.load({
|
|
|
1472
1652
|
),
|
|
1473
1653
|
h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
|
|
1474
1654
|
h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
|
|
1475
|
-
h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
|
|
1655
|
+
h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql")),
|
|
1656
|
+
h("button", { type: "button", role: "tab", "aria-selected": tab === "history", className: "dshdb-tab" + (tab === "history" ? " active" : ""), onClick: function () { setTab("history"); } }, h(HistoryIcon, { size: 14 }), t("history"))
|
|
1476
1657
|
)
|
|
1477
1658
|
),
|
|
1478
|
-
tab === "data" ? dataPane : sqlPane,
|
|
1479
|
-
h("div", { className: "dshdb-chatbar" },
|
|
1659
|
+
tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane,
|
|
1660
|
+
tab === "history" ? null : h("div", { className: "dshdb-chatbar" },
|
|
1480
1661
|
h(SparkIcon, { size: 14 }),
|
|
1481
1662
|
h("div", null, h("b", null, t("inChat") + " · "), t("inChatHint"))
|
|
1482
1663
|
)
|
|
@@ -1844,6 +2025,47 @@ window.__ModuleLoader__.load({
|
|
|
1844
2025
|
}
|
|
1845
2026
|
|
|
1846
2027
|
// ── conversation tab: the selected databases ────────────────────────
|
|
2028
|
+
function messageContentText(content) {
|
|
2029
|
+
if (typeof content === "string") return content;
|
|
2030
|
+
if (!Array.isArray(content)) return "";
|
|
2031
|
+
return content.map(function (block) {
|
|
2032
|
+
return block && block.type === "text" && typeof block.text === "string" ? block.text : "";
|
|
2033
|
+
}).join(" ").replace(/\s+/g, " ").trim();
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/** Group recorded SQL by the loaded conversation turn whose time window contains it. */
|
|
2037
|
+
function sqlHistoryTurns(history, turns, nodes, t) {
|
|
2038
|
+
var loaded = (Array.isArray(turns) ? turns : []).map(function (item) {
|
|
2039
|
+
var node = nodes && typeof nodes.get === "function" ? nodes.get(item.anchorKey) : null;
|
|
2040
|
+
return {
|
|
2041
|
+
id: "turn-" + item.turn,
|
|
2042
|
+
turn: Number(item.turn) || 0,
|
|
2043
|
+
time: Number(node && node.data ? node.data.time : 0) || 0,
|
|
2044
|
+
prompt: item.prompt || "",
|
|
2045
|
+
entries: [],
|
|
2046
|
+
};
|
|
2047
|
+
});
|
|
2048
|
+
if (loaded.length === 0) return [{ id: "manual", index: 0, label: t("historyManual"), prompt: "", entries: history }];
|
|
2049
|
+
var groups = loaded.map(function (turn) {
|
|
2050
|
+
return { id: turn.id, index: 0, label: interpolate(t("historyTurn"), { n: turn.turn }), prompt: turn.prompt, time: turn.time, entries: [] };
|
|
2051
|
+
});
|
|
2052
|
+
var orphans = [];
|
|
2053
|
+
history.forEach(function (row) {
|
|
2054
|
+
var at = Number(row.at) || 0;
|
|
2055
|
+
var index = -1;
|
|
2056
|
+
for (var cursor = 0; cursor < loaded.length; cursor += 1) {
|
|
2057
|
+
if (loaded[cursor].time <= at) index = cursor;
|
|
2058
|
+
else break;
|
|
2059
|
+
}
|
|
2060
|
+
if (index < 0) orphans.push(row);
|
|
2061
|
+
else groups[index].entries.push(row);
|
|
2062
|
+
});
|
|
2063
|
+
var result = groups.filter(function (group) { return group.entries.length > 0; }).reverse();
|
|
2064
|
+
if (orphans.length) result.push({ id: "orphans", index: result.length, label: t("historyManual"), prompt: "", entries: orphans });
|
|
2065
|
+
result.forEach(function (group, index) { group.index = index; });
|
|
2066
|
+
return result;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
1847
2069
|
function ConversationDbView(props) {
|
|
1848
2070
|
var t = props.t || function (key) { return key; };
|
|
1849
2071
|
var sessionId = props.sessionId;
|
|
@@ -1870,6 +2092,15 @@ window.__ModuleLoader__.load({
|
|
|
1870
2092
|
var [sqlResult, setSqlResult] = React.useState(null);
|
|
1871
2093
|
var [sqlError, setSqlError] = React.useState(null);
|
|
1872
2094
|
var [sqlLoading, setSqlLoading] = React.useState(false);
|
|
2095
|
+
var [history, setHistory] = React.useState([]);
|
|
2096
|
+
var [historyLoading, setHistoryLoading] = React.useState(false);
|
|
2097
|
+
var [historyError, setHistoryError] = React.useState(null);
|
|
2098
|
+
var [collapsedTurns, setCollapsedTurns] = React.useState({});
|
|
2099
|
+
var fallbackUseChat = function () { return undefined; };
|
|
2100
|
+
var useChat = typeof props.useChat === "function" ? props.useChat : fallbackUseChat;
|
|
2101
|
+
var chatTurns = useChat(function (snapshot) { return snapshot.navigation ? snapshot.navigation.items() : undefined; });
|
|
2102
|
+
var chatNodes = useChat(function (snapshot) { return snapshot.nodes; });
|
|
2103
|
+
var turnGroups = sqlHistoryTurns(history, chatTurns, chatNodes, t);
|
|
1873
2104
|
var [dialog, setDialog] = React.useState(null);
|
|
1874
2105
|
var [busy, setBusy] = React.useState(false);
|
|
1875
2106
|
var [error, setError] = React.useState(null);
|
|
@@ -1910,6 +2141,44 @@ window.__ModuleLoader__.load({
|
|
|
1910
2141
|
return function () { alive = false; };
|
|
1911
2142
|
}, [activeId, sessionId]);
|
|
1912
2143
|
|
|
2144
|
+
React.useEffect(function () {
|
|
2145
|
+
if (tab === "history") loadHistory();
|
|
2146
|
+
}, [tab, sessionId]);
|
|
2147
|
+
|
|
2148
|
+
function loadHistory() {
|
|
2149
|
+
if (!sessionId) return;
|
|
2150
|
+
setHistoryLoading(true);
|
|
2151
|
+
setHistoryError(null);
|
|
2152
|
+
api({ action: "history-list", sessionId: sessionId }).then(function (data) {
|
|
2153
|
+
setHistory(Array.isArray(data.entries) ? data.entries : []);
|
|
2154
|
+
}).catch(function (failure) {
|
|
2155
|
+
setHistoryError(interpolate(t("loadFailed"), { error: failure.message }));
|
|
2156
|
+
}).finally(function () { setHistoryLoading(false); });
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function clearHistory() {
|
|
2160
|
+
if (!sessionId) return;
|
|
2161
|
+
api({ action: "history-clear", sessionId: sessionId }).then(function () {
|
|
2162
|
+
setHistory([]);
|
|
2163
|
+
}).catch(function (failure) {
|
|
2164
|
+
setHistoryError(interpolate(t("saveFailed"), { error: failure.message }));
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function reproduceHistory(row) {
|
|
2169
|
+
var connection = selected.find(function (item) { return item.id === row.connectionId; });
|
|
2170
|
+
if (!connection) {
|
|
2171
|
+
setError(interpolate(t("historyUnavailable"), { name: row.connectionName || row.connectionId }));
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
setError(null);
|
|
2175
|
+
setActiveId(row.connectionId);
|
|
2176
|
+
setSql(row.sql || "");
|
|
2177
|
+
setSqlResult(null);
|
|
2178
|
+
setSqlError(null);
|
|
2179
|
+
setTab("sql");
|
|
2180
|
+
}
|
|
2181
|
+
|
|
1913
2182
|
function selectTable(connectionId, name, nextPage, internal) {
|
|
1914
2183
|
setSelectedTable(name);
|
|
1915
2184
|
setPage(nextPage || 0);
|
|
@@ -1972,7 +2241,10 @@ window.__ModuleLoader__.load({
|
|
|
1972
2241
|
setSqlResult({ readOnly: readOnly, data: data });
|
|
1973
2242
|
}).catch(function (failure) {
|
|
1974
2243
|
setSqlError(interpolate(t("queryFailed"), { error: failure.message }));
|
|
1975
|
-
}).finally(function () {
|
|
2244
|
+
}).finally(function () {
|
|
2245
|
+
setSqlLoading(false);
|
|
2246
|
+
loadHistory();
|
|
2247
|
+
});
|
|
1976
2248
|
}
|
|
1977
2249
|
|
|
1978
2250
|
async function openDemo() {
|
|
@@ -2180,6 +2452,75 @@ window.__ModuleLoader__.load({
|
|
|
2180
2452
|
) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
|
|
2181
2453
|
);
|
|
2182
2454
|
|
|
2455
|
+
var historyPane = h("div", { className: "dshdb-historypane" },
|
|
2456
|
+
h("div", { className: "dshdb-historybar" },
|
|
2457
|
+
h("div", { className: "dshdb-historytitle" },
|
|
2458
|
+
h(HistoryIcon, { size: 14 }),
|
|
2459
|
+
t("history"),
|
|
2460
|
+
h("span", { className: "dshdb-gridmeta" }, history.length)
|
|
2461
|
+
),
|
|
2462
|
+
h("button", {
|
|
2463
|
+
type: "button",
|
|
2464
|
+
className: "dshdb-btn small ghost",
|
|
2465
|
+
disabled: historyLoading || history.length === 0,
|
|
2466
|
+
onClick: clearHistory
|
|
2467
|
+
}, h(TrashIcon, { size: 13 }), t("historyClear"))
|
|
2468
|
+
),
|
|
2469
|
+
historyError ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, historyError)) : null,
|
|
2470
|
+
historyLoading
|
|
2471
|
+
? h("div", { className: "dshdb-placeholder" }, h(Spinner))
|
|
2472
|
+
: history.length === 0
|
|
2473
|
+
? h("div", { className: "dshdb-placeholder" }, t("historyEmpty"))
|
|
2474
|
+
: h("div", { className: "dshdb-historylist" }, turnGroups.map(function (group) {
|
|
2475
|
+
var collapsed = collapsedTurns[group.id] !== undefined ? collapsedTurns[group.id] : group.index > 0;
|
|
2476
|
+
return h("div", { key: group.id, className: "dshdb-historyturn" },
|
|
2477
|
+
h("button", {
|
|
2478
|
+
type: "button",
|
|
2479
|
+
className: "dshdb-historyturnhead",
|
|
2480
|
+
"aria-expanded": !collapsed,
|
|
2481
|
+
onClick: function () {
|
|
2482
|
+
setCollapsedTurns(function (previous) {
|
|
2483
|
+
var next = Object.assign({}, previous);
|
|
2484
|
+
next[group.id] = !collapsed;
|
|
2485
|
+
return next;
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
h(ChevronDownIcon, { size: 13, style: { transform: collapsed ? "rotate(-90deg)" : "none", transition: "transform .12s ease" } }),
|
|
2490
|
+
h("span", { className: "dshdb-historyturnlabel" }, group.label),
|
|
2491
|
+
group.prompt ? h("span", { className: "dshdb-historyturnprompt", title: group.prompt }, group.prompt) : null,
|
|
2492
|
+
h("span", { className: "dshdb-historyturncount" }, group.entries.length)
|
|
2493
|
+
),
|
|
2494
|
+
collapsed ? null : h("div", { className: "dshdb-historyturnrows" }, group.entries.map(function (row) {
|
|
2495
|
+
var ok = row.ok !== false;
|
|
2496
|
+
var source = row.source === "agent" ? t("historyAgent") : t("historyConsole");
|
|
2497
|
+
var count = row.changes != null
|
|
2498
|
+
? interpolate(t("historyChanges"), { n: fmtNumber(row.changes) })
|
|
2499
|
+
: interpolate(t("historyRows"), { n: fmtNumber(row.rows || 0) });
|
|
2500
|
+
return h("button", {
|
|
2501
|
+
key: row.id,
|
|
2502
|
+
type: "button",
|
|
2503
|
+
className: "dshdb-historyrow" + (ok ? "" : " bad"),
|
|
2504
|
+
title: ok ? t("historyReproduceHint") : (row.error || t("historyFailed")),
|
|
2505
|
+
onClick: function () { reproduceHistory(row); }
|
|
2506
|
+
},
|
|
2507
|
+
h("span", { className: "dshdb-historystatus" }, h(ok ? CheckIcon : CloseIcon, { size: 13 })),
|
|
2508
|
+
h("span", { className: "dshdb-historymain" },
|
|
2509
|
+
h("span", { className: "dshdb-historysql" }, row.sql),
|
|
2510
|
+
h("span", { className: "dshdb-historymeta" },
|
|
2511
|
+
h("span", null, row.connectionName || row.connectionId),
|
|
2512
|
+
h("span", null, source),
|
|
2513
|
+
h("span", null, new Date(row.at).toLocaleString()),
|
|
2514
|
+
h("span", null, count)
|
|
2515
|
+
)
|
|
2516
|
+
),
|
|
2517
|
+
h("span", { className: "dshdb-historyaction" }, h(PlayIcon, { size: 11 }), t("historyReproduce"))
|
|
2518
|
+
);
|
|
2519
|
+
}))
|
|
2520
|
+
);
|
|
2521
|
+
}))
|
|
2522
|
+
);
|
|
2523
|
+
|
|
2183
2524
|
return h("div", { className: "dshdb-cv" },
|
|
2184
2525
|
h("div", { className: "dshdb-cvhead" },
|
|
2185
2526
|
h("div", { className: "dshdb-cvtitlewrap" },
|
|
@@ -2212,10 +2553,11 @@ window.__ModuleLoader__.load({
|
|
|
2212
2553
|
),
|
|
2213
2554
|
h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
|
|
2214
2555
|
h("button", { type: "button", role: "tab", "aria-selected": tab === "data", className: "dshdb-tab" + (tab === "data" ? " active" : ""), onClick: function () { setTab("data"); } }, h(TableIcon, { size: 14 }), t("data")),
|
|
2215
|
-
h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql"))
|
|
2556
|
+
h("button", { type: "button", role: "tab", "aria-selected": tab === "sql", className: "dshdb-tab" + (tab === "sql" ? " active" : ""), onClick: function () { setTab("sql"); } }, h(PlayIcon, { size: 13 }), t("sql")),
|
|
2557
|
+
h("button", { type: "button", role: "tab", "aria-selected": tab === "history", className: "dshdb-tab" + (tab === "history" ? " active" : ""), onClick: function () { setTab("history"); } }, h(HistoryIcon, { size: 14 }), t("history"))
|
|
2216
2558
|
),
|
|
2217
2559
|
error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
|
|
2218
|
-
h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : sqlPane),
|
|
2560
|
+
h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane),
|
|
2219
2561
|
dialogNode
|
|
2220
2562
|
);
|
|
2221
2563
|
}
|
package/lib/index.js
CHANGED
|
@@ -284,7 +284,7 @@ function buildToolDefinitions(studio) {
|
|
|
284
284
|
async execute(args, exec) {
|
|
285
285
|
const session = sessionOf(exec)
|
|
286
286
|
const id = await resolveConnectionId(studio, args?.connectionId, session)
|
|
287
|
-
const result = await studio.query(id, args?.sql, { limit: args?.limit, ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
|
|
287
|
+
const result = await studio.query(id, args?.sql, { limit: args?.limit, source: 'agent', ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
|
|
288
288
|
const rows = result.rows ?? []
|
|
289
289
|
const summaryParts = [`查询成功,返回 ${rows.length} 行${result.truncated ? '(已截断)' : ''},耗时 ${result.elapsedMs} ms。`]
|
|
290
290
|
const table = markdownTable(result.columns, rows, 25)
|
|
@@ -328,7 +328,7 @@ function buildToolDefinitions(studio) {
|
|
|
328
328
|
if (args?.confirm !== true) throw new Error('写操作需要显式确认:请先在对话中得到用户同意,再以 confirm: true 重试。')
|
|
329
329
|
const session = sessionOf(exec)
|
|
330
330
|
const id = await resolveConnectionId(studio, args?.connectionId, session)
|
|
331
|
-
const result = await studio.execute(id, args?.sql, session ? { sessionId: session.id, cwd: session.cwd } : {})
|
|
331
|
+
const result = await studio.execute(id, args?.sql, { source: 'agent', ...(session ? { sessionId: session.id, cwd: session.cwd } : {}) })
|
|
332
332
|
const affected = result.changes ?? 0
|
|
333
333
|
return {
|
|
334
334
|
summary: `执行成功:影响 ${affected} 行${result.lastInsertRowid != null ? `,最后插入 id = ${result.lastInsertRowid}` : ''},耗时 ${result.elapsedMs ?? 0} ms。`,
|
|
@@ -365,9 +365,15 @@ async function dispatch(studio, body, options = {}) {
|
|
|
365
365
|
case 'rows':
|
|
366
366
|
return { ok: true, ...(await studio.rows(body.id, body.table, { limit: body.limit, offset: body.offset, ...scoped })) }
|
|
367
367
|
case 'query':
|
|
368
|
-
return { ok: true, ...(await studio.query(body.id, body.sql, { limit: body.limit, ...scoped })) }
|
|
368
|
+
return { ok: true, ...(await studio.query(body.id, body.sql, { limit: body.limit, source: 'ui', ...scoped })) }
|
|
369
369
|
case 'execute':
|
|
370
|
-
return { ok: true, ...(await studio.execute(body.id, body.sql, scoped)) }
|
|
370
|
+
return { ok: true, ...(await studio.execute(body.id, body.sql, { source: 'ui', ...scoped })) }
|
|
371
|
+
case 'history-list':
|
|
372
|
+
return { ok: true, ...(await studio.sqlHistory(sessionId)) }
|
|
373
|
+
case 'history-all':
|
|
374
|
+
return { ok: true, ...(await studio.allSqlHistory()) }
|
|
375
|
+
case 'history-clear':
|
|
376
|
+
return { ok: true, ...(await studio.clearSqlHistory(sessionId)) }
|
|
371
377
|
case 'test':
|
|
372
378
|
return { ok: true, ...(await studio.testConnection(body.connection ?? body)) }
|
|
373
379
|
case 'add':
|
package/lib/service.js
CHANGED
|
@@ -398,16 +398,73 @@ export class DatabaseStudio {
|
|
|
398
398
|
return { connection: publicConnection(connection), table: String(table), limit, offset, ...result }
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Record one SQL run for the conversation's history. History is a convenience
|
|
403
|
+
* surface: a write failure must never turn a successful query into an error,
|
|
404
|
+
* so recording errors are logged and swallowed.
|
|
405
|
+
*/
|
|
406
|
+
async #recordSqlHistory(sessionId, connection, sql, source, outcome) {
|
|
407
|
+
if (!sessionId) return
|
|
408
|
+
try {
|
|
409
|
+
await this.selections.addHistory(sessionId, {
|
|
410
|
+
sql: String(sql ?? '').slice(0, 20000),
|
|
411
|
+
connectionId: connection.id,
|
|
412
|
+
connectionName: connection.name,
|
|
413
|
+
source: source === 'agent' ? 'agent' : 'ui',
|
|
414
|
+
...outcome,
|
|
415
|
+
})
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.warn('dsh-db-studio: failed to record SQL history —', error?.message ?? error)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Every conversation's SQL history, grouped by Session. */
|
|
422
|
+
async allSqlHistory() {
|
|
423
|
+
await this.ready()
|
|
424
|
+
return { sessions: await this.selections.allHistory() }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** One Session's SQL history, newest first. */
|
|
428
|
+
async sqlHistory(sessionId) {
|
|
429
|
+
await this.ready()
|
|
430
|
+
if (!sessionId) throw new Error('缺少会话 id / missing session id')
|
|
431
|
+
return { sessionId, entries: await this.selections.history(sessionId) }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Clear one Session's SQL history. */
|
|
435
|
+
async clearSqlHistory(sessionId) {
|
|
436
|
+
await this.ready()
|
|
437
|
+
if (!sessionId) throw new Error('缺少会话 id / missing session id')
|
|
438
|
+
const cleared = await this.selections.clearHistory(sessionId)
|
|
439
|
+
return { sessionId, cleared }
|
|
440
|
+
}
|
|
441
|
+
|
|
401
442
|
/** Run a read-only query. */
|
|
402
443
|
async query(id, sql, options = {}) {
|
|
403
444
|
if (options.sessionId) await this.assertSessionConnection(options.sessionId, id, options.cwd)
|
|
404
445
|
const connection = await this.connection(id)
|
|
405
446
|
const limit = clampLimit(options.limit, this.config.maxRows, this.config.maxRows)
|
|
406
447
|
const adapter = await this.#adapter(connection)
|
|
448
|
+
const source = options.source
|
|
407
449
|
const started = Date.now()
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
450
|
+
try {
|
|
451
|
+
const result = await adapter.query(sql, { limit })
|
|
452
|
+
await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
|
|
453
|
+
ok: true,
|
|
454
|
+
elapsedMs: Date.now() - started,
|
|
455
|
+
rows: Array.isArray(result.rows) ? result.rows.length : 0,
|
|
456
|
+
truncated: result.truncated === true,
|
|
457
|
+
})
|
|
458
|
+
this.touchUsage(connection.id, options.sessionId, options.cwd)
|
|
459
|
+
return { connection: publicConnection(connection), elapsedMs: Date.now() - started, ...result }
|
|
460
|
+
} catch (error) {
|
|
461
|
+
await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
|
|
462
|
+
ok: false,
|
|
463
|
+
elapsedMs: Date.now() - started,
|
|
464
|
+
error: error?.message ?? String(error),
|
|
465
|
+
})
|
|
466
|
+
throw error
|
|
467
|
+
}
|
|
411
468
|
}
|
|
412
469
|
|
|
413
470
|
/** Run a write statement (or any single statement) after policy checks. */
|
|
@@ -419,10 +476,26 @@ export class DatabaseStudio {
|
|
|
419
476
|
if (!isSingleStatement(sql)) throw new Error('只允许执行一条 SQL / only one statement is allowed')
|
|
420
477
|
return this.#serialized(connection.id, async () => {
|
|
421
478
|
const adapter = await this.#adapter(connection)
|
|
479
|
+
const source = options.source
|
|
422
480
|
const started = Date.now()
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
481
|
+
try {
|
|
482
|
+
const result = await adapter.execute(sql)
|
|
483
|
+
await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
|
|
484
|
+
ok: true,
|
|
485
|
+
elapsedMs: result.elapsedMs ?? (Date.now() - started),
|
|
486
|
+
changes: result.changes ?? 0,
|
|
487
|
+
lastInsertRowid: result.lastInsertRowid == null ? undefined : String(result.lastInsertRowid),
|
|
488
|
+
})
|
|
489
|
+
this.touchUsage(connection.id, options.sessionId, options.cwd)
|
|
490
|
+
return { connection: publicConnection(connection), elapsedMs: result.elapsedMs ?? (Date.now() - started), ...result }
|
|
491
|
+
} catch (error) {
|
|
492
|
+
await this.#recordSqlHistory(options.sessionId, connection, sql, source, {
|
|
493
|
+
ok: false,
|
|
494
|
+
elapsedMs: Date.now() - started,
|
|
495
|
+
error: error?.message ?? String(error),
|
|
496
|
+
})
|
|
497
|
+
throw error
|
|
498
|
+
}
|
|
426
499
|
})
|
|
427
500
|
}
|
|
428
501
|
|
package/lib/session-store.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @module dsh-db-studio/session-store
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { randomUUID } from 'node:crypto'
|
|
23
24
|
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
24
25
|
import { join } from 'node:path'
|
|
25
26
|
import { studioHome } from './home.js'
|
|
@@ -27,11 +28,12 @@ import { studioHome } from './home.js'
|
|
|
27
28
|
const FILE_MODE = 0o600
|
|
28
29
|
const DIR_MODE = 0o700
|
|
29
30
|
const MAX_SESSIONS = 200
|
|
31
|
+
const MAX_HISTORY = 100
|
|
30
32
|
const SAVE_DELAY_MS = 400
|
|
31
33
|
|
|
32
34
|
/** Empty state document. */
|
|
33
35
|
function emptyState() {
|
|
34
|
-
return { version:
|
|
36
|
+
return { version: 2, sessions: {}, usage: {}, history: {} }
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** Keep only the newest N sessions. */
|
|
@@ -42,6 +44,14 @@ function pruneSessions(sessions) {
|
|
|
42
44
|
return Object.fromEntries(rows.slice(0, MAX_SESSIONS))
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/** Keep only the newest N history rows per Session. */
|
|
48
|
+
function pruneHistory(history) {
|
|
49
|
+
const rows = Object.entries(history)
|
|
50
|
+
if (rows.length <= MAX_SESSIONS) return history
|
|
51
|
+
rows.sort((left, right) => (right[1]?.[0]?.at ?? 0) - (left[1]?.[0]?.at ?? 0))
|
|
52
|
+
return Object.fromEntries(rows.slice(0, MAX_SESSIONS))
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
/** Normalize one stored entry. */
|
|
46
56
|
function normalizeEntry(entry) {
|
|
47
57
|
return {
|
|
@@ -71,9 +81,10 @@ export class SessionSelectionStore {
|
|
|
71
81
|
const parsed = JSON.parse(raw)
|
|
72
82
|
if (parsed && typeof parsed === 'object') {
|
|
73
83
|
this.state = {
|
|
74
|
-
version:
|
|
84
|
+
version: 2,
|
|
75
85
|
sessions: parsed.sessions && typeof parsed.sessions === 'object' ? parsed.sessions : {},
|
|
76
86
|
usage: parsed.usage && typeof parsed.usage === 'object' ? parsed.usage : {},
|
|
87
|
+
history: parsed.history && typeof parsed.history === 'object' ? parsed.history : {},
|
|
77
88
|
}
|
|
78
89
|
}
|
|
79
90
|
} catch (error) {
|
|
@@ -93,6 +104,7 @@ export class SessionSelectionStore {
|
|
|
93
104
|
this.saveQueue = this.saveQueue.catch(() => {}).then(async () => {
|
|
94
105
|
await mkdir(this.dir, { recursive: true, mode: DIR_MODE })
|
|
95
106
|
this.state.sessions = pruneSessions(this.state.sessions)
|
|
107
|
+
this.state.history = pruneHistory(this.state.history)
|
|
96
108
|
const body = JSON.stringify(this.state, null, 2) + '\n'
|
|
97
109
|
const temp = `${this.file}.${process.pid}.${Date.now()}.tmp`
|
|
98
110
|
await writeFile(temp, body, { mode: FILE_MODE })
|
|
@@ -207,6 +219,56 @@ export class SessionSelectionStore {
|
|
|
207
219
|
return normalizeEntry(this.#entry(sessionId)).ids
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
/** Append one SQL run to a Session's history (newest first, bounded). */
|
|
223
|
+
async addHistory(sessionId, entry) {
|
|
224
|
+
if (!sessionId) return null
|
|
225
|
+
await this.load()
|
|
226
|
+
const record = {
|
|
227
|
+
id: randomUUID(),
|
|
228
|
+
at: Date.now(),
|
|
229
|
+
...entry,
|
|
230
|
+
}
|
|
231
|
+
const rows = Array.isArray(this.state.history[sessionId]) ? this.state.history[sessionId] : []
|
|
232
|
+
rows.unshift(record)
|
|
233
|
+
this.state.history[sessionId] = rows.slice(0, MAX_HISTORY)
|
|
234
|
+
await this.save()
|
|
235
|
+
return record
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Every Session's SQL history, newest activity first. */
|
|
239
|
+
async allHistory() {
|
|
240
|
+
await this.load()
|
|
241
|
+
const groups = []
|
|
242
|
+
for (const [sessionId, entries] of Object.entries(this.state.history)) {
|
|
243
|
+
if (!Array.isArray(entries) || entries.length === 0) continue
|
|
244
|
+
const session = normalizeEntry(this.state.sessions[sessionId])
|
|
245
|
+
groups.push({
|
|
246
|
+
sessionId,
|
|
247
|
+
cwd: session.cwd ?? null,
|
|
248
|
+
sessionUpdatedAt: session.updatedAt,
|
|
249
|
+
entries,
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
groups.sort((left, right) => (right.entries[0]?.at ?? 0) - (left.entries[0]?.at ?? 0))
|
|
253
|
+
return groups
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** One Session's SQL history, newest first. */
|
|
257
|
+
async history(sessionId) {
|
|
258
|
+
await this.load()
|
|
259
|
+
const rows = this.state.history[sessionId]
|
|
260
|
+
return Array.isArray(rows) ? rows.slice() : []
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Clear one Session's SQL history. */
|
|
264
|
+
async clearHistory(sessionId) {
|
|
265
|
+
await this.load()
|
|
266
|
+
if (!this.state.history[sessionId]) return false
|
|
267
|
+
delete this.state.history[sessionId]
|
|
268
|
+
await this.save()
|
|
269
|
+
return true
|
|
270
|
+
}
|
|
271
|
+
|
|
210
272
|
/** All usage rows (tests/debug). */
|
|
211
273
|
usageSnapshot() {
|
|
212
274
|
return { ...this.state.usage }
|