dsh-db-studio 1.0.1 → 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 +7 -0
- package/README.en.md +11 -0
- package/README.md +11 -0
- package/lib/client.js +380 -10
- package/lib/index.js +10 -4
- package/lib/service.js +79 -6
- package/lib/session-store.js +64 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.3 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
- 新增 `/db` 斜杠指令:输入 `/` 后可从指令列表直接打开数据库面板,带数据库图标
|
|
6
|
+
- 修复对话内数据库 tab 中数据表列表与数据页滚动未分离的问题;现在左侧表列表和右侧数据各自独立滚动
|
|
7
|
+
- 新增对话数据库面板「查询历史」tab:按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时、行数和成功状态,支持一键复现到 SQL 编辑器
|
|
8
|
+
- 左侧全局面板新增「查询历史」tab:按对话分组展示历史,点击分组或记录可进入对应对话
|
|
9
|
+
|
|
3
10
|
## v1.0.1 — 2026-10-24
|
|
4
11
|
|
|
5
12
|
首个稳定版。
|
package/README.en.md
CHANGED
|
@@ -5,6 +5,7 @@ English | [简体中文](./README.md)
|
|
|
5
5
|
[](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/etShaw-zh/dsh-db-studio/releases)
|
|
8
|
+
[](https://www.npmjs.com/package/dsh-db-studio)
|
|
8
9
|
|
|
9
10
|
Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH), then query and edit data right in the conversation.
|
|
10
11
|
|
|
@@ -22,12 +23,22 @@ Connect to databases in [DeepSeek Harness](https://github.com/deepseek-ai/deepse
|
|
|
22
23
|
- **Database tab in a conversation**: view and work with the selected databases without leaving the chat.
|
|
23
24
|
- **Multi-select picker in the composer**: choose the databases this conversation may use.
|
|
24
25
|
- **Talk to the data**: four agent tools for querying, creating tables and writing data.
|
|
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.
|
|
25
28
|
|
|
26
29
|
## Install
|
|
27
30
|
|
|
31
|
+
From npm:
|
|
32
|
+
|
|
28
33
|
```bash
|
|
29
34
|
# macOS desktop
|
|
30
35
|
DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
|
|
36
|
+
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or install directly from GitHub:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
31
42
|
"$DSH_CLI" plugin --profile desktop add github:etShaw-zh/dsh-db-studio
|
|
32
43
|
```
|
|
33
44
|
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://github.com/etShaw-zh/dsh-db-studio/actions/workflows/test.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/etShaw-zh/dsh-db-studio/releases)
|
|
8
|
+
[](https://www.npmjs.com/package/dsh-db-studio)
|
|
8
9
|
|
|
9
10
|
在 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)里连接数据库,直接在对话中查数据、改数据。
|
|
10
11
|
|
|
@@ -20,12 +21,22 @@
|
|
|
20
21
|
- **对话内「数据库」标签**:在当前对话里直接查看和操作已选数据库。
|
|
21
22
|
- **输入框旁多选下拉**:选择本次对话要使用的数据库,支持多选。
|
|
22
23
|
- **对话中直接操作**:内置 4 个助手工具,可以帮你查询、建表、写数据。
|
|
24
|
+
- **`/db` 指令**:输入 `/`,在指令列表里选择「数据库」,直接打开数据库面板。
|
|
25
|
+
- **查询历史**:对话数据库面板新增「查询历史」tab,按用户对话轮次分组、支持折叠,记录 SQL、连接、耗时和影响行数,点击即可复现;左侧全局面板的查询历史按对话分组,点击可进入对应对话。
|
|
23
26
|
|
|
24
27
|
## 安装
|
|
25
28
|
|
|
29
|
+
从 npm 安装:
|
|
30
|
+
|
|
26
31
|
```bash
|
|
27
32
|
# macOS 桌面端
|
|
28
33
|
DSH_CLI="/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh"
|
|
34
|
+
"$DSH_CLI" plugin --profile desktop add dsh-db-studio@1.0.3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
也可以直接从 GitHub 安装:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
29
40
|
"$DSH_CLI" plugin --profile desktop add github:etShaw-zh/dsh-db-studio
|
|
30
41
|
```
|
|
31
42
|
|
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: "搜索表…",
|
|
@@ -126,7 +144,9 @@ window.__ModuleLoader__.load({
|
|
|
126
144
|
connectedOf: "{ok}/{n} 已连接",
|
|
127
145
|
allConnected: "已连接",
|
|
128
146
|
schemaHint: "点击「结构」查看字段",
|
|
129
|
-
askInChat: "可以直接在对话里让助手查询或修改这些数据"
|
|
147
|
+
askInChat: "可以直接在对话里让助手查询或修改这些数据",
|
|
148
|
+
commandLabel: "数据库",
|
|
149
|
+
commandDesc: "打开数据库面板"
|
|
130
150
|
};
|
|
131
151
|
var en = {
|
|
132
152
|
title: "Database",
|
|
@@ -143,6 +163,24 @@ window.__ModuleLoader__.load({
|
|
|
143
163
|
schema: "Schema",
|
|
144
164
|
data: "Data",
|
|
145
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",
|
|
146
184
|
run: "Run",
|
|
147
185
|
running: "Running…",
|
|
148
186
|
searchTable: "Search tables…",
|
|
@@ -243,7 +281,9 @@ window.__ModuleLoader__.load({
|
|
|
243
281
|
connectedOf: "{ok}/{n} connected",
|
|
244
282
|
allConnected: "Connected",
|
|
245
283
|
schemaHint: "Use Schema to inspect columns",
|
|
246
|
-
askInChat: "Ask the assistant to query or change this data in chat"
|
|
284
|
+
askInChat: "Ask the assistant to query or change this data in chat",
|
|
285
|
+
commandLabel: "Database",
|
|
286
|
+
commandDesc: "Open the database panel"
|
|
247
287
|
};
|
|
248
288
|
|
|
249
289
|
function interpolate(text, params) {
|
|
@@ -514,6 +554,7 @@ window.__ModuleLoader__.load({
|
|
|
514
554
|
function CloseIcon(props) { return h(Svg, props, h("path", { d: "M6 6l12 12M18 6 6 18" })); }
|
|
515
555
|
function CheckIcon(props) { return h(Svg, props, h("path", { d: "M5 12.5 10 17.5 19 7" })); }
|
|
516
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" })); }
|
|
517
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" })); }
|
|
518
559
|
function ChevronDownIcon(props) { return h(Svg, props, h("path", { d: "m6 9.5 6 6 6-6" })); }
|
|
519
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" })); }
|
|
@@ -710,7 +751,12 @@ window.__ModuleLoader__.load({
|
|
|
710
751
|
.dshdb-statuspop-name{font-size:12px;font-weight:550;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
711
752
|
.dshdb-statuspop-detail{font-size:10.5px;color:var(--dsw-alias-label-tertiary);line-height:1.4;word-break:break-word}
|
|
712
753
|
.dshdb-statuspop-row:has(.dshdb-dot.bad) .dshdb-statuspop-detail{color:var(--dsw-alias-state-error-primary)}
|
|
713
|
-
.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}
|
|
714
760
|
.dshdb-cv *{box-sizing:border-box}
|
|
715
761
|
.dshdb-cvhead{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;flex:0 0 auto}
|
|
716
762
|
.dshdb-cvtitlewrap{display:flex;align-items:center;gap:10px;min-width:0}
|
|
@@ -731,6 +777,38 @@ window.__ModuleLoader__.load({
|
|
|
731
777
|
.dshdb-cv .dshdb-pager{padding:7px 12px}
|
|
732
778
|
.dshdb-cv .dshdb-sqlpane{padding:12px 14px;gap:10px;height:100%}
|
|
733
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}
|
|
734
812
|
.dshdb-cvempty{flex:1;display:flex;align-items:center;justify-content:center;padding:26px;min-height:300px}
|
|
735
813
|
.dshdb-cvloading{flex:1;display:flex;align-items:center;justify-content:center;min-height:300px}
|
|
736
814
|
.dshdb-cvmessage{padding:6px 0;flex:0 0 auto}
|
|
@@ -1075,6 +1153,10 @@ window.__ModuleLoader__.load({
|
|
|
1075
1153
|
var [sqlError, setSqlError] = React.useState(null);
|
|
1076
1154
|
var [sqlLoading, setSqlLoading] = React.useState(false);
|
|
1077
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({});
|
|
1078
1160
|
var [dialog, setDialog] = React.useState(null);
|
|
1079
1161
|
var [pendingDelete, setPendingDelete] = React.useState(null);
|
|
1080
1162
|
var [busy, setBusy] = React.useState(false);
|
|
@@ -1097,6 +1179,52 @@ window.__ModuleLoader__.load({
|
|
|
1097
1179
|
return function () { alive = false; };
|
|
1098
1180
|
}, []);
|
|
1099
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
|
+
|
|
1100
1228
|
React.useEffect(function () {
|
|
1101
1229
|
if (!activeId) { setTables([]); setSelectedTable(null); setSchema(null); setResult(null); return; }
|
|
1102
1230
|
var alive = true;
|
|
@@ -1441,6 +1569,62 @@ window.__ModuleLoader__.load({
|
|
|
1441
1569
|
) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
|
|
1442
1570
|
);
|
|
1443
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
|
+
|
|
1444
1628
|
main = h("section", { className: "dshdb-main" },
|
|
1445
1629
|
h("div", { className: "dshdb-mainhead" },
|
|
1446
1630
|
h("div", { className: "dshdb-mainheadtop" },
|
|
@@ -1468,11 +1652,12 @@ window.__ModuleLoader__.load({
|
|
|
1468
1652
|
),
|
|
1469
1653
|
h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
|
|
1470
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")),
|
|
1471
|
-
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"))
|
|
1472
1657
|
)
|
|
1473
1658
|
),
|
|
1474
|
-
tab === "data" ? dataPane : sqlPane,
|
|
1475
|
-
h("div", { className: "dshdb-chatbar" },
|
|
1659
|
+
tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane,
|
|
1660
|
+
tab === "history" ? null : h("div", { className: "dshdb-chatbar" },
|
|
1476
1661
|
h(SparkIcon, { size: 14 }),
|
|
1477
1662
|
h("div", null, h("b", null, t("inChat") + " · "), t("inChatHint"))
|
|
1478
1663
|
)
|
|
@@ -1840,6 +2025,47 @@ window.__ModuleLoader__.load({
|
|
|
1840
2025
|
}
|
|
1841
2026
|
|
|
1842
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
|
+
|
|
1843
2069
|
function ConversationDbView(props) {
|
|
1844
2070
|
var t = props.t || function (key) { return key; };
|
|
1845
2071
|
var sessionId = props.sessionId;
|
|
@@ -1866,6 +2092,15 @@ window.__ModuleLoader__.load({
|
|
|
1866
2092
|
var [sqlResult, setSqlResult] = React.useState(null);
|
|
1867
2093
|
var [sqlError, setSqlError] = React.useState(null);
|
|
1868
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);
|
|
1869
2104
|
var [dialog, setDialog] = React.useState(null);
|
|
1870
2105
|
var [busy, setBusy] = React.useState(false);
|
|
1871
2106
|
var [error, setError] = React.useState(null);
|
|
@@ -1906,6 +2141,44 @@ window.__ModuleLoader__.load({
|
|
|
1906
2141
|
return function () { alive = false; };
|
|
1907
2142
|
}, [activeId, sessionId]);
|
|
1908
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
|
+
|
|
1909
2182
|
function selectTable(connectionId, name, nextPage, internal) {
|
|
1910
2183
|
setSelectedTable(name);
|
|
1911
2184
|
setPage(nextPage || 0);
|
|
@@ -1968,7 +2241,10 @@ window.__ModuleLoader__.load({
|
|
|
1968
2241
|
setSqlResult({ readOnly: readOnly, data: data });
|
|
1969
2242
|
}).catch(function (failure) {
|
|
1970
2243
|
setSqlError(interpolate(t("queryFailed"), { error: failure.message }));
|
|
1971
|
-
}).finally(function () {
|
|
2244
|
+
}).finally(function () {
|
|
2245
|
+
setSqlLoading(false);
|
|
2246
|
+
loadHistory();
|
|
2247
|
+
});
|
|
1972
2248
|
}
|
|
1973
2249
|
|
|
1974
2250
|
async function openDemo() {
|
|
@@ -2176,6 +2452,75 @@ window.__ModuleLoader__.load({
|
|
|
2176
2452
|
) : h("div", { className: "dshdb-result" }, h("div", { className: "dshdb-placeholder" }, t("result")))
|
|
2177
2453
|
);
|
|
2178
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
|
+
|
|
2179
2524
|
return h("div", { className: "dshdb-cv" },
|
|
2180
2525
|
h("div", { className: "dshdb-cvhead" },
|
|
2181
2526
|
h("div", { className: "dshdb-cvtitlewrap" },
|
|
@@ -2208,10 +2553,11 @@ window.__ModuleLoader__.load({
|
|
|
2208
2553
|
),
|
|
2209
2554
|
h("div", { className: "dshdb-tabs", role: "tablist", "aria-label": t("tabsAria") },
|
|
2210
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")),
|
|
2211
|
-
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"))
|
|
2212
2558
|
),
|
|
2213
2559
|
error ? h("div", { className: "dshdb-cvmessage" }, h("div", { className: "dshdb-error" }, error)) : null,
|
|
2214
|
-
h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : sqlPane),
|
|
2560
|
+
h("div", { className: "dshdb-cvbody" }, tab === "data" ? dataPane : tab === "sql" ? sqlPane : historyPane),
|
|
2215
2561
|
dialogNode
|
|
2216
2562
|
);
|
|
2217
2563
|
}
|
|
@@ -2221,8 +2567,13 @@ window.__ModuleLoader__.load({
|
|
|
2221
2567
|
return h(DatabaseIcon, { size: props.size || 18 });
|
|
2222
2568
|
}
|
|
2223
2569
|
|
|
2570
|
+
/** Slash-command glyph (the trigger menu asks for a 14px icon). */
|
|
2571
|
+
function DbCommandIcon(props) {
|
|
2572
|
+
return h(DatabaseIcon, { size: props.size || 14 });
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2224
2575
|
exports.name = "dsh-db-studio";
|
|
2225
|
-
exports.inject = ["slots", "layout", "locale"];
|
|
2576
|
+
exports.inject = ["slots", "layout", "locale", "commandUi"];
|
|
2226
2577
|
exports.apply = function (ctx) {
|
|
2227
2578
|
APP_CTX = ctx;
|
|
2228
2579
|
installStyles(ctx);
|
|
@@ -2231,6 +2582,25 @@ window.__ModuleLoader__.load({
|
|
|
2231
2582
|
}, "dsh-db-studio: dictionaries");
|
|
2232
2583
|
var t = ctx.locale.bind(NS);
|
|
2233
2584
|
|
|
2585
|
+
// `/db` in the composer: same row treatment as the built-in commands,
|
|
2586
|
+
// with a database glyph in front and no message submitted.
|
|
2587
|
+
var commandUi = ctx.get("commandUi");
|
|
2588
|
+
if (commandUi && typeof commandUi.register === "function") {
|
|
2589
|
+
ctx.effect(function () {
|
|
2590
|
+
return commandUi.register({
|
|
2591
|
+
name: "db",
|
|
2592
|
+
available: function () { return true; },
|
|
2593
|
+
label: function () { return t("commandLabel"); },
|
|
2594
|
+
description: function () { return t("commandDesc"); },
|
|
2595
|
+
icon: DbCommandIcon,
|
|
2596
|
+
ui: {
|
|
2597
|
+
kind: "action",
|
|
2598
|
+
run: function () { openGlobalPanel(); }
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}, "dsh-db-studio: /db command");
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2234
2604
|
ctx.slots.inject("main", function () {
|
|
2235
2605
|
return ctx.slots.register({
|
|
2236
2606
|
name: "main",
|
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 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-studio",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Connect to databases in DeepSeek Harness (DSH), then query and edit data right in the conversation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"inject": [
|
|
43
43
|
"@deepseek-ai/dsh-client-locale",
|
|
44
44
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
45
|
-
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-commands"
|
|
46
47
|
]
|
|
47
48
|
}
|
|
48
49
|
},
|