dsh-db-tool 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/client/client.js +47 -8
- package/dist/adapters/sql-shared/pg-like.js +8 -0
- package/dist/http/index.js +3 -0
- package/dist/manager.js +15 -0
- package/package.json +5 -2
- package/patches/dsh-app-boot-route-scoped-hotfix.patch +26 -0
- package/scripts/apply-dsh-hotfix.ps1 +34 -0
- package/scripts/apply-dsh-hotfix.sh +20 -0
- package/scripts/hotfix-core.d.mts +26 -0
- package/scripts/hotfix-core.mjs +320 -0
- package/skills/db-admin/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -46,6 +46,23 @@ GaussDB 官方驱动未发布 npm,需先构建 vendor:`npm run build:gaussdb
|
|
|
46
46
|
- `run_script`:node:vm 独立 context、60s 超时、无 require/process/网络/文件系统,仅注入受限 `db.{query,execute}` 句柄
|
|
47
47
|
- 已知边界:对话内确认为提示级强制 + 审计兜底;DSH 无硬中断通道前,恶意对话仍可能诱导用户确认,请配合最小权限数据库账号使用
|
|
48
48
|
|
|
49
|
+
## Troubleshooting
|
|
50
|
+
|
|
51
|
+
### npm 安装插件导入失败(punycode / resolve.paths)
|
|
52
|
+
|
|
53
|
+
症状:DSH 启动后插件加载报 `failed to import`,伴随 `TypeError: Cannot read properties of null (reading 'Symbol(Symbol.iterator)')`,栈指向 `dsh-app-boot` 的 `routeScoped`。
|
|
54
|
+
|
|
55
|
+
根因:上游 `@deepseek-ai/dsh-app-boot` 对 `createRequire(parent).resolve.paths(name)` 直接做 `for..of`,而 Node 对 core-module 同名包(`punycode` 等)返回 `null`,hoisted profile 下凡依赖树含此类 npm 包的插件都会炸。
|
|
56
|
+
|
|
57
|
+
一键修复(幂等,应用前自动备份为 `index.js.bak-hotfix`;`--revert` 可还原):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm run patch:dsh # Windows(PowerShell)
|
|
61
|
+
npm run patch:dsh:sh # macOS / Linux
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
脚本自动探测 DSH 安装根(`--dsh-root` 可显式指定);补丁文件见 `patches/dsh-app-boot-route-scoped-hotfix.patch`,仅对 `0.1.7-rc.2` 声明兼容,其他版本会警告(`--force` 覆盖)。上游 issue:<https://github.com/mengqi1436/dsh-db-tool/issues>(占位,待上游仓库开放后替换)。
|
|
65
|
+
|
|
49
66
|
## 测试
|
|
50
67
|
|
|
51
68
|
```bash
|
|
@@ -62,7 +79,7 @@ npx stryker run # 变异测试(范围 lib/guard + lib/manager + lib/store,
|
|
|
62
79
|
lib/ host 插件(store / adapters×8 / guard / manager / http / index)
|
|
63
80
|
client/ 侧边栏单文件产物(client.js,即源码)
|
|
64
81
|
skills/ db-admin skill
|
|
65
|
-
scripts/ GaussDB vendor
|
|
82
|
+
scripts/ GaussDB vendor 构建、DSH dsh-app-boot 热修复(patch:dsh)
|
|
66
83
|
docs/ 安装、HTTP 契约(api-contract.md)、skill 说明
|
|
67
84
|
tests/ vitest(离线 mock + DBT_TEST_* 门控真机)
|
|
68
85
|
vendor/ gaussdb-pg 构建产物(gitignore,不入库)
|
package/client/client.js
CHANGED
|
@@ -66,6 +66,7 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
modeRw: "读写",
|
|
67
67
|
// 浏览
|
|
68
68
|
noDatabases: "无可用数据库",
|
|
69
|
+
noSchemas: "无模式",
|
|
69
70
|
noTables: "无表",
|
|
70
71
|
loadFailed: "加载失败",
|
|
71
72
|
structure: "结构",
|
|
@@ -150,6 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
150
151
|
modeRo: "read-only ro",
|
|
151
152
|
modeRw: "read-write rw",
|
|
152
153
|
noDatabases: "No databases available",
|
|
154
|
+
noSchemas: "No schemas",
|
|
153
155
|
noTables: "No tables",
|
|
154
156
|
loadFailed: "Failed to load",
|
|
155
157
|
structure: "Structure",
|
|
@@ -621,7 +623,7 @@ window.__ModuleLoader__.load({
|
|
|
621
623
|
{ className: "dbt-muted", style: { fontFamily: MONO_FONT, fontSize: 11, wordBreak: "break-all" } },
|
|
622
624
|
c.safeUrl || (c.host + ":" + (c.port || "")),
|
|
623
625
|
) : null,
|
|
624
|
-
testInfo[c.id] ? React.createElement("div", {
|
|
626
|
+
testInfo[c.id] ? React.createElement("div", { style: { fontSize: 12, color: testInfo[c.id].startsWith("✓") ? "var(--dbt-success, #30d158)" : "var(--dbt-danger, #ff453a)" } }, testInfo[c.id]) : null,
|
|
625
627
|
),
|
|
626
628
|
// 右列:动作按钮(次要语义,danger 仅删除)
|
|
627
629
|
React.createElement(
|
|
@@ -708,7 +710,8 @@ window.__ModuleLoader__.load({
|
|
|
708
710
|
const [loading, setLoading] = React.useState({});
|
|
709
711
|
const [error, setError] = React.useState({}); // 树节点加载失败信息(就地显示,可点重试)
|
|
710
712
|
const [dbs, setDbs] = React.useState({}); // connId -> string[]
|
|
711
|
-
const [
|
|
713
|
+
const [schemasMap, setSchemasMap] = React.useState({}); // "<connId>/<db>" -> string[](PG/GaussDB 库内 schema 层)
|
|
714
|
+
const [tablesMap, setTablesMap] = React.useState({}); // "<connId>/<db|schema>" -> TableInfo[]
|
|
712
715
|
const [sel, setSel] = React.useState(null); // {connId, db, table: TableInfo}
|
|
713
716
|
const [schema, setSchema] = React.useState([]);
|
|
714
717
|
const [preview, setPreview] = React.useState(null); // QueryResult
|
|
@@ -718,7 +721,7 @@ window.__ModuleLoader__.load({
|
|
|
718
721
|
|
|
719
722
|
// 会话项目切换 / 连接列表变化时清空树缓存,避免陈旧授权下的旧数据
|
|
720
723
|
React.useEffect(() => {
|
|
721
|
-
setOpen({}); setLoading({}); setError({}); setDbs({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
|
|
724
|
+
setOpen({}); setLoading({}); setError({}); setDbs({}); setSchemasMap({}); setTablesMap({}); setSel(null); setSchema([]); setPreview(null);
|
|
722
725
|
}, [projectPath]);
|
|
723
726
|
|
|
724
727
|
function toggle(key, load) {
|
|
@@ -744,11 +747,23 @@ window.__ModuleLoader__.load({
|
|
|
744
747
|
setDbs((m) => Object.assign({}, m, { [c.id]: list || [] }));
|
|
745
748
|
}));
|
|
746
749
|
}
|
|
750
|
+
// PG/GaussDB 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
|
|
751
|
+
const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
|
|
747
752
|
function toggleDb(c, d) {
|
|
748
753
|
if (!projectPath) return;
|
|
754
|
+
const useSchemas = !!HAS_SCHEMAS[c.kind];
|
|
755
|
+
const what = useSchemas ? "schemas" : "tables";
|
|
749
756
|
toggle("d:" + c.id + "/" + d, () =>
|
|
750
|
-
api(
|
|
751
|
-
|
|
757
|
+
api(what + qs({ project: projectPath, connId: c.id, database: d })).then((list) => {
|
|
758
|
+
const setter = useSchemas ? setSchemasMap : setTablesMap;
|
|
759
|
+
setter((m) => Object.assign({}, m, { [c.id + "/" + d]: list || [] }));
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
function toggleSchema(c, d, s) {
|
|
763
|
+
if (!projectPath) return;
|
|
764
|
+
toggle("s:" + c.id + "/" + d + "/" + s, () =>
|
|
765
|
+
api("tables" + qs({ project: projectPath, connId: c.id, database: s })).then((list) => {
|
|
766
|
+
setTablesMap((m) => Object.assign({}, m, { [c.id + "/" + s]: list || [] }));
|
|
752
767
|
}));
|
|
753
768
|
}
|
|
754
769
|
// 请求序号守卫:快速切换选中表/翻页时,丢弃晚到的旧响应,防止旧数据覆盖新选中项
|
|
@@ -795,19 +810,43 @@ window.__ModuleLoader__.load({
|
|
|
795
810
|
if (!list) continue;
|
|
796
811
|
if (list.length === 0) { treeRows.push(treerow(ck + ":e", 1, false, true, t("noDatabases"))); continue; }
|
|
797
812
|
for (const d of list) {
|
|
813
|
+
const useSchemas = !!HAS_SCHEMAS[c.kind];
|
|
798
814
|
const dk = "d:" + c.id + "/" + d;
|
|
799
815
|
const dOpen = !!open[dk];
|
|
800
816
|
treeRows.push(treerow(dk, 1, dOpen, false, d, () => toggleDb(c, d)));
|
|
801
817
|
if (!dOpen) continue;
|
|
802
|
-
const tkey = c.id + "/" + d;
|
|
803
|
-
const tlist = tablesMap[tkey];
|
|
804
818
|
if (loading[dk]) { treeRows.push(treerow(dk + ":l", 2, false, true, "…")); continue; }
|
|
805
819
|
if (error[dk]) { treeRows.push(treerow(dk + ":x", 2, false, true, t("loadFailed") + ":" + error[dk], () => retry(dk, () => toggleDb(c, d)))); continue; }
|
|
820
|
+
// PG/GaussDB:库 → 模式 → 表(三层,Navicat 官方层级);schema 节点复用 open/loading/error 状态
|
|
821
|
+
if (useSchemas) {
|
|
822
|
+
const slist = schemasMap[c.id + "/" + d];
|
|
823
|
+
if (!slist) continue;
|
|
824
|
+
if (slist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noSchemas"))); continue; }
|
|
825
|
+
for (const s of slist) {
|
|
826
|
+
const sk = "s:" + c.id + "/" + d + "/" + s;
|
|
827
|
+
const sOpen = !!open[sk];
|
|
828
|
+
treeRows.push(treerow(sk, 2, sOpen, false, s, () => toggleSchema(c, d, s)));
|
|
829
|
+
if (!sOpen) continue;
|
|
830
|
+
const tlist = tablesMap[c.id + "/" + s];
|
|
831
|
+
if (loading[sk]) { treeRows.push(treerow(sk + ":l", 3, false, true, "…")); continue; }
|
|
832
|
+
if (error[sk]) { treeRows.push(treerow(sk + ":x", 3, false, true, t("loadFailed") + ":" + error[sk], () => retry(sk, () => toggleSchema(c, d, s)))); continue; }
|
|
833
|
+
if (!tlist) continue;
|
|
834
|
+
if (tlist.length === 0) { treeRows.push(treerow(sk + ":e", 3, false, true, t("noTables"))); continue; }
|
|
835
|
+
for (const tb of tlist) {
|
|
836
|
+
const active = !!sel && sel.connId === c.id && sel.db === s && sel.table.name === tb.name;
|
|
837
|
+
treeRows.push(treerow("t:" + sk + "/" + tb.name, 3, false, true,
|
|
838
|
+
tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
|
|
839
|
+
() => setSel({ connId: c.id, db: s, table: tb }), active));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const tlist = tablesMap[c.id + "/" + d];
|
|
806
845
|
if (!tlist) continue;
|
|
807
846
|
if (tlist.length === 0) { treeRows.push(treerow(dk + ":e", 2, false, true, t("noTables"))); continue; }
|
|
808
847
|
for (const tb of tlist) {
|
|
809
848
|
const active = !!sel && sel.connId === c.id && sel.db === d && sel.table.name === tb.name;
|
|
810
|
-
treeRows.push(treerow("t:" +
|
|
849
|
+
treeRows.push(treerow("t:" + c.id + "/" + d + "/" + tb.name, 2, false, true,
|
|
811
850
|
tb.name + (tb.type && tb.type !== "table" ? " · " + tb.type : ""),
|
|
812
851
|
() => setSel({ connId: c.id, db: d, table: tb }), active));
|
|
813
852
|
}
|
|
@@ -116,6 +116,14 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
|
|
|
116
116
|
const res = await pool.query('SELECT datname FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY datname');
|
|
117
117
|
return res.rows.map((r) => String(r.datname));
|
|
118
118
|
}),
|
|
119
|
+
// 库内 schema 清单(Navicat 官方层级:数据库 → 模式 → 表)。
|
|
120
|
+
// PG 连接固定单库,database 参数无法跨库,忽略;pg_* 前缀覆盖 pg_catalog/pg_toast/pg_temp 系。
|
|
121
|
+
listSchemas: () => humanize(`${kind} 列出模式`, async () => {
|
|
122
|
+
const res = await pool.query(`SELECT nspname FROM pg_catalog.pg_namespace
|
|
123
|
+
WHERE nspname NOT LIKE 'pg\\_%' AND nspname <> 'information_schema'
|
|
124
|
+
ORDER BY nspname`);
|
|
125
|
+
return res.rows.map((r) => String(r.nspname));
|
|
126
|
+
}),
|
|
119
127
|
listTables: (database) => humanize(`${kind} 列出表`, async () => {
|
|
120
128
|
const schema = database ? assertIdent(database, 'schema') : defaultSchema();
|
|
121
129
|
const res = await pool.query(`SELECT table_name, table_type FROM information_schema.tables
|
package/dist/http/index.js
CHANGED
|
@@ -210,6 +210,9 @@ async function route(req, res, service, path, q, opts) {
|
|
|
210
210
|
if (path === '/api/tables' && req.method === 'GET') {
|
|
211
211
|
return sendOk(res, await service.tables(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
|
|
212
212
|
}
|
|
213
|
+
if (path === '/api/schemas' && req.method === 'GET') {
|
|
214
|
+
return sendOk(res, await service.schemas(projectOf(q), required(q, 'connId'), q.get('database') ?? undefined));
|
|
215
|
+
}
|
|
213
216
|
if (path === '/api/schema' && req.method === 'GET') {
|
|
214
217
|
return sendOk(res, await service.schema(projectOf(q), required(q, 'connId'), required(q, 'table'), q.get('database') ?? undefined));
|
|
215
218
|
}
|
package/dist/manager.js
CHANGED
|
@@ -146,6 +146,21 @@ export class DbToolService {
|
|
|
146
146
|
throw this.toDriverError(e);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
/** 库内 schema 清单(仅 PG/GaussDB 等三层语义适配器实现;ro 即可)。 */
|
|
150
|
+
async schemas(projectPath, connId, database) {
|
|
151
|
+
const { key, adapter } = await this.authorize(projectPath, connId, false);
|
|
152
|
+
if (!adapter.listSchemas)
|
|
153
|
+
return [];
|
|
154
|
+
try {
|
|
155
|
+
const result = await adapter.listSchemas(database);
|
|
156
|
+
this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, true);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
this.audit(key, connId, 'schemas', database ?? 'default', 'none', false, false, this.errText(e));
|
|
161
|
+
throw this.toDriverError(e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
149
164
|
async schema(projectPath, connId, table, database) {
|
|
150
165
|
const t = assertNonEmpty(table, 'table');
|
|
151
166
|
const { key, adapter } = await this.authorize(projectPath, connId, false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-tool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
29
|
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\"",
|
|
30
30
|
"build:gaussdb": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-gaussdb.ps1",
|
|
31
|
-
"build:gaussdb:sh": "bash scripts/build-gaussdb.sh"
|
|
31
|
+
"build:gaussdb:sh": "bash scripts/build-gaussdb.sh",
|
|
32
|
+
"patch:dsh": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/apply-dsh-hotfix.ps1",
|
|
33
|
+
"patch:dsh:sh": "bash scripts/apply-dsh-hotfix.sh"
|
|
32
34
|
},
|
|
33
35
|
"dsh": {
|
|
34
36
|
"bundle": {
|
|
@@ -72,6 +74,7 @@
|
|
|
72
74
|
"client",
|
|
73
75
|
"skills",
|
|
74
76
|
"docs",
|
|
77
|
+
"patches",
|
|
75
78
|
"scripts",
|
|
76
79
|
"README.md"
|
|
77
80
|
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# DSH ResolutionRouter routeScoped hotfix (upstream bug: createRequire(parent).resolve.paths(name)
|
|
2
|
+
# returns null for core-module names like "punycode", breaking hoisted profile installs).
|
|
3
|
+
# Target-Version: 0.1.7-rc.2
|
|
4
|
+
# Apply manually: git apply -p1 -d <dsh-app-boot package root> <this file>
|
|
5
|
+
# Or one command: npm run patch:dsh (scripts/apply-dsh-hotfix.ps1)
|
|
6
|
+
# Generated from @deepseek-ai/dsh-app-boot@0.1.7-rc.2 npm tarball vs patched local copy.
|
|
7
|
+
diff --git a/lib/index.js b/lib/index.js
|
|
8
|
+
index 8f6041d..89a138e 100644
|
|
9
|
+
--- a/lib/index.js
|
|
10
|
+
+++ b/lib/index.js
|
|
11
|
+
@@ -1419,7 +1419,14 @@ var ResolutionRouter = class {
|
|
12
|
+
const target = resolution.entries.get(name);
|
|
13
|
+
const candidates = [];
|
|
14
|
+
const localSearchPaths = [];
|
|
15
|
+
- for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
16
|
+
+ /* local hotfix: resolve.paths returns null for core-module names (e.g. "punycode"),
|
|
17
|
+
+ which made the for..of throw TypeError and broke every hoisted profile install
|
|
18
|
+
+ whose dependency tree requires such an npm package. Fall back to the standard
|
|
19
|
+
+ node_modules search chain so the local candidate can still be found. */
|
|
20
|
+
+ const _hotReq = createRequire(parent);
|
|
21
|
+
+ const _hotPaths = _hotReq.resolve.paths(name)
|
|
22
|
+
+ ?? _hotReq("node:module")._nodeModulePaths(_hotReq("node:path").dirname(parent));
|
|
23
|
+
+ for (const searchPath of _hotPaths) {
|
|
24
|
+
if (!searchPath.startsWith(layer.localPrefix)) break;
|
|
25
|
+
localSearchPaths.push(searchPath);
|
|
26
|
+
const candidate = localPackageCandidate(searchPath, name, flavor);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Apply or revert the DSH ResolutionRouter hotfix (dsh-app-boot routeScoped resolve.paths fix).
|
|
2
|
+
# Thin wrapper: all logic lives in scripts/hotfix-core.mjs (cross-platform, testable).
|
|
3
|
+
# Usage: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/apply-dsh-hotfix.ps1
|
|
4
|
+
# [-DshRoot <dir>] [-PatchFile <file>] [-Force] [-Revert] [-DryRun]
|
|
5
|
+
param(
|
|
6
|
+
[string]$DshRoot,
|
|
7
|
+
[string]$PatchFile,
|
|
8
|
+
[switch]$Force,
|
|
9
|
+
[switch]$Revert,
|
|
10
|
+
[switch]$DryRun
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
$core = Join-Path $PSScriptRoot 'hotfix-core.mjs'
|
|
15
|
+
if (-not (Test-Path $core)) {
|
|
16
|
+
Write-Error "hotfix-core.mjs not found next to this script ($PSScriptRoot)."
|
|
17
|
+
exit 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
21
|
+
if (-not $node) {
|
|
22
|
+
Write-Error 'node is required but was not found on PATH.'
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
$argv = @($core)
|
|
27
|
+
if ($DshRoot) { $argv += @('--dsh-root', $DshRoot) }
|
|
28
|
+
if ($PatchFile) { $argv += @('--patch-file', $PatchFile) }
|
|
29
|
+
if ($Force) { $argv += '--force' }
|
|
30
|
+
if ($Revert) { $argv += '--revert' }
|
|
31
|
+
if ($DryRun) { $argv += '--dry-run' }
|
|
32
|
+
|
|
33
|
+
& $node.Source @argv
|
|
34
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Apply or revert the DSH ResolutionRouter hotfix (dsh-app-boot routeScoped resolve.paths fix).
|
|
3
|
+
# Thin wrapper: all logic lives in scripts/hotfix-core.mjs (cross-platform, testable).
|
|
4
|
+
# Usage: bash scripts/apply-dsh-hotfix.sh [--dsh-root <dir>] [--patch-file <file>] [--force] [--revert] [--dry-run]
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
CORE="$SCRIPT_DIR/hotfix-core.mjs"
|
|
9
|
+
|
|
10
|
+
if [[ ! -f "$CORE" ]]; then
|
|
11
|
+
echo "hotfix-core.mjs not found next to this script ($SCRIPT_DIR)." >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
16
|
+
echo "node is required but was not found on PATH." >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
exec node "$CORE" "$@"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 类型声明:scripts/hotfix-core.mjs(无 allowJs,为 tests/scripts/hotfix.spec.ts 提供 import 类型)。
|
|
3
|
+
*/
|
|
4
|
+
export declare const TARGET_VERSION: string;
|
|
5
|
+
export declare const HOTFIX_MARKER: string;
|
|
6
|
+
export declare const BOOT_INDEX_REL: string;
|
|
7
|
+
export declare const BACKUP_SUFFIX: string;
|
|
8
|
+
export declare const ORIGINAL_LINE: string;
|
|
9
|
+
|
|
10
|
+
export declare function isApplied(content: string): boolean;
|
|
11
|
+
export declare function applyToContent(content: string): {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
content: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function revertContent(content: string): {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
content: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function parsePatchTargetVersion(patchText: string): string | undefined;
|
|
22
|
+
export declare function findBootIndex(root: string): string | undefined;
|
|
23
|
+
export declare function resolveCandidateRoots(explicitRoot?: string): string[];
|
|
24
|
+
export declare function resolveDshRoot(explicitRoot?: string): string | undefined;
|
|
25
|
+
export declare function applyToIndexFile(indexFile: string, patchFile: string): string;
|
|
26
|
+
export declare function run(argv?: string[]): number;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* DSH ResolutionRouter hotfix —— 核心逻辑(纯 Node、零依赖、可被测试 import)。
|
|
4
|
+
*
|
|
5
|
+
* 修复的 bug:@deepseek-ai/dsh-app-boot 的 routeScoped() 直接对
|
|
6
|
+
* `createRequire(parent).resolve.paths(name)` 做 for..of,而 Node 对 core-module
|
|
7
|
+
* 同名包(punycode 等)返回 null,导致 hoisted profile 下 npm 安装插件导入失败
|
|
8
|
+
* (TypeError: Cannot read properties of null)。
|
|
9
|
+
*
|
|
10
|
+
* 用法:node scripts/hotfix-core.mjs [--dsh-root <dir>] [--patch-file <file>]
|
|
11
|
+
* [--force] [--revert] [--dry-run]
|
|
12
|
+
* 退出码:0 成功/已应用跳过;1 失败;2 版本不匹配且未给 --force。
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
+
|
|
19
|
+
/** 补丁文件里的目标版本声明行(patches/*.patch 注释头)。 */
|
|
20
|
+
export const TARGET_VERSION = '0.1.7-rc.2';
|
|
21
|
+
/** 判断目标文件是否已打补丁的标识串(hotfix 注释首行片段)。 */
|
|
22
|
+
export const HOTFIX_MARKER = 'local hotfix: resolve.paths returns null';
|
|
23
|
+
/** dsh-app-boot 内 lib/index.js 相对包根的路径。 */
|
|
24
|
+
export const BOOT_INDEX_REL = path.join('lib', 'index.js');
|
|
25
|
+
/** 应用前备份文件后缀。 */
|
|
26
|
+
export const BACKUP_SUFFIX = '.bak-hotfix';
|
|
27
|
+
|
|
28
|
+
/** 原始(未打补丁)单行 —— 必须与 dsh-app-boot@0.1.7-rc.2 lib/index.js 逐字节一致。 */
|
|
29
|
+
export const ORIGINAL_LINE =
|
|
30
|
+
'\t\tfor (const searchPath of createRequire(parent).resolve.paths(name)) {';
|
|
31
|
+
|
|
32
|
+
/** 热修复代码块 —— 与 patches/dsh-app-boot-route-scoped-hotfix.patch 的 + 行一致。 */
|
|
33
|
+
const HOTFIX_BLOCK = [
|
|
34
|
+
'\t\t/* local hotfix: resolve.paths returns null for core-module names (e.g. "punycode"),',
|
|
35
|
+
'\t\t which made the for..of throw TypeError and broke every hoisted profile install',
|
|
36
|
+
'\t\t whose dependency tree requires such an npm package. Fall back to the standard',
|
|
37
|
+
'\t\t node_modules search chain so the local candidate can still be found. */',
|
|
38
|
+
'\t\tconst _hotReq = createRequire(parent);',
|
|
39
|
+
'\t\tconst _hotPaths = _hotReq.resolve.paths(name)',
|
|
40
|
+
'\t\t\t?? _hotReq("node:module")._nodeModulePaths(_hotReq("node:path").dirname(parent));',
|
|
41
|
+
'\t\tfor (const searchPath of _hotPaths) {',
|
|
42
|
+
].join('\n');
|
|
43
|
+
|
|
44
|
+
/** 目标文件是否已包含热修复。 */
|
|
45
|
+
export function isApplied(content) {
|
|
46
|
+
return content.includes(HOTFIX_MARKER);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 在文件内容上应用热修复(幂等:已应用时返回 already-applied)。
|
|
51
|
+
* @returns {{ ok: boolean, reason?: string, content: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function applyToContent(content) {
|
|
54
|
+
if (isApplied(content)) return { ok: false, reason: 'already-applied', content };
|
|
55
|
+
if (!content.includes(ORIGINAL_LINE)) {
|
|
56
|
+
return { ok: false, reason: 'target-line-not-found', content };
|
|
57
|
+
}
|
|
58
|
+
return { ok: true, content: content.replace(ORIGINAL_LINE, HOTFIX_BLOCK) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 还原热修复(优先用内建块反替换;调用方在磁盘层面应优先用 .bak-hotfix 备份)。
|
|
63
|
+
* @returns {{ ok: boolean, reason?: string, content: string }}
|
|
64
|
+
*/
|
|
65
|
+
export function revertContent(content) {
|
|
66
|
+
if (!isApplied(content)) return { ok: false, reason: 'not-applied', content };
|
|
67
|
+
if (!content.includes(HOTFIX_BLOCK)) {
|
|
68
|
+
return { ok: false, reason: 'hotfix-block-not-found', content };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, content: content.replace(HOTFIX_BLOCK, ORIGINAL_LINE) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 从补丁文本解析注释头声明的目标版本(无则返回 undefined)。 */
|
|
74
|
+
export function parsePatchTargetVersion(patchText) {
|
|
75
|
+
const m = /^\s*#\s*Target-Version:\s*(\S+)/m.exec(patchText);
|
|
76
|
+
return m?.[1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 在候选 dsh 根下定位 dsh-app-boot/lib/index.js。
|
|
81
|
+
* 候选解释:root=DSH 安装根(@deepseek-ai/dsh 包目录);也接受直接指向
|
|
82
|
+
* dsh-app-boot 包根或 node_modules/@deepseek-ai 层级。
|
|
83
|
+
* @returns {string | undefined} lib/index.js 绝对路径
|
|
84
|
+
*/
|
|
85
|
+
export function findBootIndex(root) {
|
|
86
|
+
const candidates = [
|
|
87
|
+
path.join(root, 'node_modules', '@deepseek-ai', 'dsh-app-boot', BOOT_INDEX_REL),
|
|
88
|
+
path.join(root, '@deepseek-ai', 'dsh-app-boot', BOOT_INDEX_REL),
|
|
89
|
+
path.join(root, BOOT_INDEX_REL),
|
|
90
|
+
];
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
try {
|
|
93
|
+
if (!fs.statSync(candidate).isFile()) continue;
|
|
94
|
+
// 防误伤:确认包根 package.json 的 name 是 dsh-app-boot。
|
|
95
|
+
const pkg = JSON.parse(
|
|
96
|
+
fs.readFileSync(path.join(path.dirname(path.dirname(candidate)), 'package.json'), 'utf8'),
|
|
97
|
+
);
|
|
98
|
+
if (pkg.name === '@deepseek-ai/dsh-app-boot') return candidate;
|
|
99
|
+
} catch {
|
|
100
|
+
/* 候选不存在或不可读,试下一个 */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 自动探测 DSH 安装根候选列表(dsh 包目录)。
|
|
108
|
+
* 顺序:--dsh-root 参数 → DSH_HOME → which/where dsh 的 shim 反推 → 常见全局路径。
|
|
109
|
+
* 调用方应逐个用 findBootIndex 试探(DSH_HOME 存在但不含 dsh-app-boot 时继续回退)。
|
|
110
|
+
*/
|
|
111
|
+
export function resolveCandidateRoots(explicitRoot) {
|
|
112
|
+
const roots = [];
|
|
113
|
+
if (explicitRoot) {
|
|
114
|
+
if (fs.existsSync(explicitRoot)) roots.push(path.resolve(explicitRoot));
|
|
115
|
+
return roots;
|
|
116
|
+
}
|
|
117
|
+
const fromEnv = process.env.DSH_HOME;
|
|
118
|
+
if (fromEnv && fs.existsSync(fromEnv)) roots.push(path.resolve(fromEnv));
|
|
119
|
+
|
|
120
|
+
const shim = findDshOnPath();
|
|
121
|
+
if (shim) {
|
|
122
|
+
// <global>/dsh(.cmd) → <global>/node_modules/@deepseek-ai/dsh
|
|
123
|
+
const globalDir = path.dirname(path.dirname(shim));
|
|
124
|
+
const candidate = path.join(globalDir, 'node_modules', '@deepseek-ai', 'dsh');
|
|
125
|
+
if (fs.existsSync(candidate)) roots.push(candidate);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
roots.push(...commonGlobalRoots());
|
|
129
|
+
return roots;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** resolveCandidateRoots 的单值便捷形式(取第一个候选)。 */
|
|
133
|
+
export function resolveDshRoot(explicitRoot) {
|
|
134
|
+
return resolveCandidateRoots(explicitRoot)[0];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findDshOnPath() {
|
|
138
|
+
const isWin = process.platform === 'win32';
|
|
139
|
+
try {
|
|
140
|
+
const out = execFileSync(isWin ? 'where.exe' : 'which', ['dsh'], {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
143
|
+
});
|
|
144
|
+
const first = out.split(/\r?\n/).find((line) => line.trim() !== '');
|
|
145
|
+
return first ? first.trim() : undefined;
|
|
146
|
+
} catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function commonGlobalRoots() {
|
|
152
|
+
const isWin = process.platform === 'win32';
|
|
153
|
+
const roots = [];
|
|
154
|
+
if (isWin) {
|
|
155
|
+
if (process.env.APPDATA) {
|
|
156
|
+
roots.push(path.join(process.env.APPDATA, 'npm', 'node_modules', '@deepseek-ai', 'dsh'));
|
|
157
|
+
}
|
|
158
|
+
// nvm-windows: <drive>:\Tool\nvm\v*\node_modules\@deepseek-ai\dsh 与 C:\nvm\v*\...
|
|
159
|
+
for (const base of ['E:\\Tool\\nvm', 'C:\\nvm', 'D:\\nvm']) {
|
|
160
|
+
try {
|
|
161
|
+
for (const entry of fs.readdirSync(base)) {
|
|
162
|
+
if (/^v/.test(entry)) {
|
|
163
|
+
roots.push(path.join(base, entry, 'node_modules', '@deepseek-ai', 'dsh'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
/* base 不存在 */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (const lib of ['/usr/local/lib', '/usr/lib', path.join(process.env.HOME ?? '', '.nvm')]) {
|
|
172
|
+
try {
|
|
173
|
+
if (lib.includes('.nvm')) {
|
|
174
|
+
const versions = path.join(lib, 'versions', 'node');
|
|
175
|
+
for (const v of fs.readdirSync(versions)) {
|
|
176
|
+
roots.push(
|
|
177
|
+
path.join(versions, v, 'lib', 'node_modules', '@deepseek-ai', 'dsh'),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
roots.push(path.join(lib, 'node_modules', '@deepseek-ai', 'dsh'));
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
/* 不可读 */
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return roots;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 首选 git apply(若 git 可用且补丁可干净应用);否则回退内建字符串替换。
|
|
193
|
+
* 两种路径结果一致(内建块与补丁 + 行逐字节相同,测试保证)。
|
|
194
|
+
*/
|
|
195
|
+
export function applyToIndexFile(indexFile, patchFile) {
|
|
196
|
+
if (isGitApplyable(indexFile, patchFile)) {
|
|
197
|
+
const bootRoot = path.dirname(path.dirname(indexFile));
|
|
198
|
+
execFileSync('git', ['apply', '-p1', '--directory', '.', patchFile], {
|
|
199
|
+
cwd: bootRoot,
|
|
200
|
+
});
|
|
201
|
+
return 'git-apply';
|
|
202
|
+
}
|
|
203
|
+
const content = fs.readFileSync(indexFile, 'utf8');
|
|
204
|
+
const result = applyToContent(content);
|
|
205
|
+
if (!result.ok) throw new Error(`apply failed: ${result.reason}`);
|
|
206
|
+
fs.writeFileSync(indexFile, result.content, 'utf8');
|
|
207
|
+
return 'builtin-replace';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isGitApplyable(indexFile, patchFile) {
|
|
211
|
+
const bootRoot = path.dirname(path.dirname(indexFile));
|
|
212
|
+
try {
|
|
213
|
+
// --directory 只接受相对路径(git 拒绝绝对目标 "invalid path"),故以包根为 cwd。
|
|
214
|
+
execFileSync('git', ['apply', '--check', '-p1', '--directory', '.', patchFile], {
|
|
215
|
+
cwd: bootRoot,
|
|
216
|
+
});
|
|
217
|
+
return true;
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 命令行主入口。@returns {number} 退出码 */
|
|
224
|
+
export function run(argv = process.argv.slice(2)) {
|
|
225
|
+
let dshRoot;
|
|
226
|
+
let patchFile = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'patches', 'dsh-app-boot-route-scoped-hotfix.patch');
|
|
227
|
+
let force = false;
|
|
228
|
+
let revert = false;
|
|
229
|
+
let dryRun = false;
|
|
230
|
+
for (let i = 0; i < argv.length; i++) {
|
|
231
|
+
const arg = argv[i];
|
|
232
|
+
if (arg === '--dsh-root') dshRoot = argv[++i];
|
|
233
|
+
else if (arg === '--patch-file') patchFile = argv[++i];
|
|
234
|
+
else if (arg === '--force') force = true;
|
|
235
|
+
else if (arg === '--revert') revert = true;
|
|
236
|
+
else if (arg === '--dry-run') dryRun = true;
|
|
237
|
+
else if (arg === '--help' || arg === '-h') {
|
|
238
|
+
console.log('usage: node scripts/hotfix-core.mjs [--dsh-root <dir>] [--patch-file <file>] [--force] [--revert] [--dry-run]');
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let indexFile;
|
|
244
|
+
for (const root of resolveCandidateRoots(dshRoot)) {
|
|
245
|
+
indexFile = findBootIndex(root);
|
|
246
|
+
if (indexFile) {
|
|
247
|
+
console.log(`[dsh-hotfix] using DSH root: ${root}`);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!indexFile) {
|
|
252
|
+
console.error('[dsh-hotfix] dsh-app-boot/lib/index.js not found in any candidate root; pass --dsh-root explicitly.');
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const pkgPath = path.join(path.dirname(path.dirname(indexFile)), 'package.json');
|
|
257
|
+
let actualVersion;
|
|
258
|
+
try {
|
|
259
|
+
actualVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
260
|
+
} catch {
|
|
261
|
+
/* 读不到版本号时跳过比对 */
|
|
262
|
+
}
|
|
263
|
+
const patchText = fs.existsSync(patchFile) ? fs.readFileSync(patchFile, 'utf8') : '';
|
|
264
|
+
const declared = parsePatchTargetVersion(patchText) ?? TARGET_VERSION;
|
|
265
|
+
if (actualVersion && actualVersion !== declared && !force) {
|
|
266
|
+
console.warn(`[dsh-hotfix] version mismatch: installed ${actualVersion} != patch target ${declared}. Use --force to apply anyway.`);
|
|
267
|
+
return 2;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let content;
|
|
271
|
+
try {
|
|
272
|
+
content = fs.readFileSync(indexFile, 'utf8');
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(`[dsh-hotfix] cannot read ${indexFile}: ${err.message}`);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (revert) {
|
|
279
|
+
const backup = indexFile + BACKUP_SUFFIX;
|
|
280
|
+
if (fs.existsSync(backup)) {
|
|
281
|
+
if (dryRun) {
|
|
282
|
+
console.log(`[dsh-hotfix] dry-run: would restore ${backup} -> ${indexFile}`);
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
fs.copyFileSync(backup, indexFile);
|
|
286
|
+
console.log(`[dsh-hotfix] reverted ${indexFile} from backup.`);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
const result = revertContent(content);
|
|
290
|
+
if (!result.ok) {
|
|
291
|
+
console.error(`[dsh-hotfix] revert failed: ${result.reason} (no backup at ${backup}).`);
|
|
292
|
+
return 1;
|
|
293
|
+
}
|
|
294
|
+
if (dryRun) {
|
|
295
|
+
console.log('[dsh-hotfix] dry-run: would revert via builtin replacement.');
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
fs.writeFileSync(indexFile, result.content, 'utf8');
|
|
299
|
+
console.log(`[dsh-hotfix] reverted ${indexFile} via builtin replacement.`);
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (isApplied(content)) {
|
|
304
|
+
console.log('[dsh-hotfix] already applied; nothing to do.');
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (dryRun) {
|
|
308
|
+
console.log(`[dsh-hotfix] dry-run: would apply patch to ${indexFile}.`);
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
const backup = indexFile + BACKUP_SUFFIX;
|
|
312
|
+
if (!fs.existsSync(backup)) fs.copyFileSync(indexFile, backup);
|
|
313
|
+
const method = applyToIndexFile(indexFile, patchFile);
|
|
314
|
+
console.log(`[dsh-hotfix] applied to ${indexFile} (via ${method}; backup at ${backup}).`);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
319
|
+
process.exitCode = run(process.argv.slice(2));
|
|
320
|
+
}
|
package/skills/db-admin/SKILL.md
CHANGED
|
@@ -57,6 +57,7 @@ DatabaseManager({ action: "schema", conn_id: "c1", database: "shop", table: "use
|
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
- 表列表返回 `TableInfo[]`:`{name, type?, comment?, database?}`(Redis 的 type 为 hash|list|set|zset|string|stream)。
|
|
60
|
+
- PostgreSQL/GaussDB 为「数据库 → 模式(schema) → 表」三层语义:`database` 参数此时应传 **schema 名**(如 `public`);不确定时先执行 `SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg\_%' AND nspname <> 'information_schema'` 列出可用 schema。
|
|
60
61
|
- 列结构返回 `ColumnInfo[]`:`{name, dataType, nullable, key?, default?, comment?}`。
|
|
61
62
|
|
|
62
63
|
### 5. preview — 预览表数据
|