@cxxl/dsh-sqlite 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,179 @@
1
+ window.__ModuleLoader__.load({
2
+ id: '@cxxl/dsh-sqlite',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
7
+ const React = require('react')
8
+
9
+ const CSS = `
10
+ .dsql-root { display: flex; flex-direction: column; gap: 10px; font-family: inherit; font-size: 12px; color: var(--dsw-alias-label-primary, #1a1a1a); }
11
+ .dsql-head { display: flex; align-items: center; gap: 8px; }
12
+ .dsql-title { font-size: 13px; font-weight: 600; flex: none; }
13
+ .dsql-select { flex: 1; min-width: 0; padding: 4px 6px; border: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.12)); border-radius: 6px; background: var(--dsw-alias-bg-base, #ffffff); color: inherit; font: inherit; }
14
+ .dsql-btn { flex: none; border: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.12)); background: transparent; color: var(--dsw-alias-label-secondary, #666666); border-radius: 6px; padding: 4px 10px; cursor: pointer; font: inherit; }
15
+ .dsql-btn:hover { background: var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.16)); color: var(--dsw-alias-label-primary, #1a1a1a); }
16
+ .dsql-btn:disabled { opacity: 0.5; cursor: default; }
17
+ .dsql-error { padding: 8px 10px; border-radius: 6px; background: rgba(224, 49, 49, 0.1); color: #c92a2a; }
18
+ .dsql-status { color: var(--dsw-alias-label-secondary, #666666); }
19
+ .dsql-tables { display: flex; flex-direction: column; gap: 4px; }
20
+ .dsql-table-row { display: flex; align-items: center; gap: 8px; text-align: left; border: 1px solid transparent; background: transparent; border-radius: 6px; padding: 6px 8px; cursor: pointer; font: inherit; color: inherit; }
21
+ .dsql-table-row:hover, .dsql-table-row-active { background: var(--dsw-alias-interactive-bg-hover-solid, rgba(127,127,127,0.16)); }
22
+ .dsql-table-name { font-weight: 600; }
23
+ .dsql-table-meta { color: var(--dsw-alias-label-secondary, #666666); font-size: 11px; }
24
+ .dsql-empty { padding: 12px 8px; color: var(--dsw-alias-label-secondary, #666666); text-align: center; }
25
+ .dsql-preview { border: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.12)); border-radius: 8px; overflow: hidden; }
26
+ .dsql-preview-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.12)); font-weight: 600; }
27
+ .dsql-preview-note { margin-left: auto; font-weight: 400; font-size: 11px; color: var(--dsw-alias-label-secondary, #666666); }
28
+ .dsql-cols { padding: 6px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.12)); color: var(--dsw-alias-label-secondary, #666666); font-size: 11px; }
29
+ .dsql-scroll { overflow-x: auto; }
30
+ table.dsql-grid { border-collapse: collapse; width: 100%; font-size: 12px; }
31
+ table.dsql-grid th, table.dsql-grid td { border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(0,0,0,0.08)); padding: 4px 8px; text-align: left; white-space: nowrap; }
32
+ table.dsql-grid th { position: sticky; top: 0; background: var(--dsw-alias-bg-layer-1, #f7f7f7); font-weight: 600; }
33
+ `
34
+
35
+ function injectCss(ctx) {
36
+ const tag = document.createElement('style')
37
+ tag.dataset.plugin = 'dsh-sqlite-browser'
38
+ tag.textContent = CSS
39
+ document.head.appendChild(tag)
40
+ ctx.effect(() => () => tag.remove(), 'dsh-sqlite: panel stylesheet')
41
+ }
42
+
43
+ async function api(path) {
44
+ const res = await fetch(path)
45
+ const data = await res.json().catch(() => null)
46
+ if (!res.ok || data === null || data.ok !== true) {
47
+ throw new Error((data && data.error) || ('HTTP ' + res.status))
48
+ }
49
+ return data
50
+ }
51
+
52
+ function fmtBytes(n) {
53
+ if (n < 1024) return n + ' B'
54
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
55
+ return (n / 1024 / 1024).toFixed(1) + ' MB'
56
+ }
57
+
58
+ function dbOptions(files) {
59
+ return (files || []).map((f) => ({
60
+ value: f.name === 'agent.db' ? 'default' : f.name.replace(/\.db$/, ''),
61
+ label: f.name + ' (' + fmtBytes(f.bytes) + ')',
62
+ }))
63
+ }
64
+
65
+ function cellText(v) {
66
+ if (v === null || v === undefined) return 'NULL'
67
+ return String(v)
68
+ }
69
+
70
+ function SqliteBrowser() {
71
+ const [files, setFiles] = React.useState([])
72
+ const [db, setDb] = React.useState('default')
73
+ const [info, setInfo] = React.useState(null)
74
+ const [preview, setPreview] = React.useState(null)
75
+ const [error, setError] = React.useState(null)
76
+ const [loading, setLoading] = React.useState(false)
77
+
78
+ const loadTables = React.useCallback(async (target) => {
79
+ setLoading(true)
80
+ setError(null)
81
+ setPreview(null)
82
+ try {
83
+ setInfo(await api('/dsh-sqlite-api/tables?db=' + encodeURIComponent(target)))
84
+ } catch (err) {
85
+ setError((err && err.message) || String(err))
86
+ } finally {
87
+ setLoading(false)
88
+ }
89
+ }, [])
90
+
91
+ React.useEffect(() => {
92
+ api('/dsh-sqlite-api/list-dbs')
93
+ .then((d) => setFiles(d.dbs || []))
94
+ .catch((err) => setError((err && err.message) || String(err)))
95
+ loadTables('default')
96
+ }, [loadTables])
97
+
98
+ const onTable = async (t) => {
99
+ setError(null)
100
+ try {
101
+ setPreview(await api('/dsh-sqlite-api/preview?db=' + encodeURIComponent(db) + '&table=' + encodeURIComponent(t)))
102
+ } catch (err) {
103
+ setError((err && err.message) || String(err))
104
+ }
105
+ }
106
+
107
+ const tables = (info && info.tables) || []
108
+
109
+ return React.createElement('div', { className: 'dsql-root' },
110
+ React.createElement('div', { className: 'dsql-head' },
111
+ React.createElement('span', { className: 'dsql-title' }, 'SQLite 数据库'),
112
+ React.createElement('select', {
113
+ className: 'dsql-select',
114
+ value: db,
115
+ onChange: (e) => { const v = e.target.value; setDb(v); loadTables(v) },
116
+ },
117
+ dbOptions(files).map((o) => React.createElement('option', { key: o.value, value: o.value }, o.label)),
118
+ ),
119
+ React.createElement('button', { className: 'dsql-btn', type: 'button', disabled: loading, onClick: () => loadTables(db) }, loading ? '加载中…' : '刷新'),
120
+ ),
121
+ error !== null && React.createElement('div', { className: 'dsql-error' }, String(error)),
122
+ files.length === 0 && React.createElement('div', { className: 'dsql-empty' }, '尚无数据库文件(~/.dsh/data/)。先让 AI 用 sqlite_exec 建表即可。'),
123
+ info !== null && React.createElement('div', { className: 'dsql-status' },
124
+ 'quick_check:' + String(info.quickCheck || 'ok') + ' · ' + tables.length + ' 张表',
125
+ ),
126
+ tables.length > 0 && React.createElement('div', { className: 'dsql-tables' },
127
+ tables.map((t) => React.createElement('button', {
128
+ key: t.name,
129
+ type: 'button',
130
+ className: 'dsql-table-row' + (preview !== null && preview.table === t.name ? ' dsql-table-row-active' : ''),
131
+ onClick: () => onTable(t.name),
132
+ },
133
+ React.createElement('span', { className: 'dsql-table-name' }, t.name),
134
+ React.createElement('span', { className: 'dsql-table-meta' }, t.columns.length + ' 列 · ' + t.rowCount + ' 行'),
135
+ )),
136
+ ),
137
+ tables.length === 0 && files.length > 0 && React.createElement('div', { className: 'dsql-empty' }, '(暂无表,可用 sqlite_exec 建表)'),
138
+ preview !== null && React.createElement('div', { className: 'dsql-preview' },
139
+ React.createElement('div', { className: 'dsql-preview-head' },
140
+ React.createElement('span', null, preview.table),
141
+ React.createElement('span', { className: 'dsql-preview-note' },
142
+ preview.rows.length + ' 行' + (preview.truncated ? '(截断至前 50 行)' : ''),
143
+ ),
144
+ ),
145
+ React.createElement('div', { className: 'dsql-cols' },
146
+ '列:' + preview.columns.join(' '),
147
+ ),
148
+ React.createElement('div', { className: 'dsql-scroll' },
149
+ React.createElement('table', { className: 'dsql-grid' },
150
+ React.createElement('thead', null,
151
+ React.createElement('tr', null,
152
+ preview.columns.map((c) => React.createElement('th', { key: c }, c)),
153
+ ),
154
+ ),
155
+ React.createElement('tbody', null,
156
+ preview.rows.map((r, i) => React.createElement('tr', { key: i },
157
+ preview.columns.map((c) => React.createElement('td', { key: c }, cellText(r[c]))),
158
+ )),
159
+ ),
160
+ ),
161
+ ),
162
+ ),
163
+ )
164
+ }
165
+
166
+ function apply(ctx) {
167
+ injectCss(ctx)
168
+ ctx.slots.inject('settings.section', () => ctx.slots.register(
169
+ { name: 'settings.section', id: 'sqlite-browser', order: 30, label: 'SQLite 数据库' },
170
+ () => React.createElement(SqliteBrowser),
171
+ ))
172
+ }
173
+
174
+ exports.apply = apply
175
+ exports.inject = ['slots']
176
+ exports.name = 'dsh-sqlite-browser'
177
+ return module.exports
178
+ },
179
+ })
package/lib/engine.js CHANGED
@@ -349,3 +349,36 @@ export function importFromFile(dbName, from, signal) {
349
349
  db.exec(text)
350
350
  return { path: resolve(src), statements: stmts.length, bytes: st.size }
351
351
  }
352
+
353
+ // 面板只读数据源:指定表的前 N 行(硬上限 50),返回 JSON 安全的结构化数据。
354
+ export function tablePreview(dbName, table, limit, signal) {
355
+ assertNotAborted(signal)
356
+ const name = String(table ?? '')
357
+ if (name === '') throw new Error('table 参数为空')
358
+ const existing = allTableNames(dbName, signal)
359
+ if (!existing.includes(name)) {
360
+ throw new Error(`表不存在:${name}(现有表:${existing.join(', ') || '无'})`)
361
+ }
362
+ const cap = Math.max(1, Math.min(50, Number.isFinite(limit) ? Math.floor(limit) : 50))
363
+ const { db } = openDb(dbName)
364
+ const prepared = db.prepare(`SELECT * FROM ${quoteIdent(name)} LIMIT ${cap}`)
365
+ const rows = prepared.all()
366
+ let columns = []
367
+ try {
368
+ const raw = prepared.columns()
369
+ if (Array.isArray(raw) && raw.length > 0) columns = raw.map((c) => (typeof c === 'string' ? c : c.name))
370
+ } catch { /* older node fallback */ }
371
+ if (columns.length === 0 && rows.length > 0) columns = Object.keys(rows[0])
372
+ const clean = rows.map((r) => {
373
+ const o = {}
374
+ for (const c of columns) {
375
+ const v = r[c]
376
+ o[c] = v === undefined || v === null ? null
377
+ : v instanceof Uint8Array ? '<blob>'
378
+ : typeof v === 'bigint' ? String(v)
379
+ : v
380
+ }
381
+ return o
382
+ })
383
+ return { table: name, columns, rows: clean, truncated: rows.length >= cap }
384
+ }
package/lib/index.js CHANGED
@@ -8,13 +8,71 @@ import { toolDefs } from './tools.js'
8
8
  import * as engine from './engine.js'
9
9
 
10
10
  export const name = '@cxxl/dsh-sqlite'
11
- export const inject = ['tools']
11
+ export const inject = ['tools', 'webServer']
12
+
13
+ function sendJson(res, code, payload) {
14
+ const body = JSON.stringify(payload)
15
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
16
+ res.end(body)
17
+ }
18
+
19
+ function queryOf(req) {
20
+ return Object.fromEntries(new URL(req.url ?? '/', 'http://x').searchParams.entries())
21
+ }
22
+
23
+ // 面板只读 API:三个 HTTP 路由(与页面同源,client 直接 fetch)。
24
+ // webServer 声明为硬依赖(inject),cordis 会等服务就绪后再挂载本插件。
25
+ function registerPanelRoutes(ctx) {
26
+ const webServer = ctx.webServer
27
+ ctx.effect(() => {
28
+ const disposers = [
29
+ webServer.register({
30
+ kind: 'exact',
31
+ path: '/dsh-sqlite-api/list-dbs',
32
+ handler: async (req, res) => {
33
+ try {
34
+ sendJson(res, 200, { ok: true, dbs: engine.listDbFiles() })
35
+ } catch (err) {
36
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
37
+ }
38
+ },
39
+ }),
40
+ webServer.register({
41
+ kind: 'exact',
42
+ path: '/dsh-sqlite-api/tables',
43
+ handler: async (req, res) => {
44
+ try {
45
+ const q = queryOf(req)
46
+ sendJson(res, 200, { ok: true, ...engine.listTables(q.db ?? 'default') })
47
+ } catch (err) {
48
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
49
+ }
50
+ },
51
+ }),
52
+ webServer.register({
53
+ kind: 'exact',
54
+ path: '/dsh-sqlite-api/preview',
55
+ handler: async (req, res) => {
56
+ try {
57
+ const q = queryOf(req)
58
+ sendJson(res, 200, { ok: true, ...engine.tablePreview(q.db ?? 'default', q.table, 50) })
59
+ } catch (err) {
60
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
61
+ }
62
+ },
63
+ }),
64
+ ]
65
+ console.log('[dsh-sqlite] panel routes registered: /dsh-sqlite-api/list-dbs|tables|preview')
66
+ return () => { for (const d of disposers) d() }
67
+ }, 'dsh-sqlite panel routes')
68
+ }
12
69
 
13
70
  export function apply(ctx) {
14
71
  for (const key of Object.keys(toolDefs)) {
15
72
  ctx.tools.register(defineTool(toolDefs[key]))
16
73
  }
17
74
  ctx.effect(() => () => engine.closeAll())
75
+ registerPanelRoutes(ctx)
18
76
 
19
77
  // 挂载自测:DSH_PLUGIN_SELFTEST=1 时在临时数据目录跑一遍真实执行管线。
20
78
  if (process.env.DSH_PLUGIN_SELFTEST === '1') void selfTest(ctx)
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@cxxl/dsh-sqlite",
3
- "version": "0.1.1",
4
- "description": "DSH plugin: persistent SQLite tools for the agent (sqlite_query / sqlite_exec / sqlite_tables / sqlite_export / sqlite_import) — zero runtime deps beyond Node 22 node:sqlite.",
3
+ "version": "0.2.0",
4
+ "description": "DSH plugin: persistent SQLite tools for the agent (sqlite_query / sqlite_exec / sqlite_tables / sqlite_export / sqlite_import) plus a read-only table browser settings page — zero runtime deps beyond Node 22 node:sqlite.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
8
8
  ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
9
10
  "./cordis.patch.yml": "./cordis.patch.yml",
10
11
  "./package.json": "./package.json"
11
12
  },
@@ -13,6 +14,7 @@
13
14
  "lib/index.js",
14
15
  "lib/tools.js",
15
16
  "lib/engine.js",
17
+ "lib/client.js",
16
18
  "cordis.patch.yml",
17
19
  "README.md",
18
20
  "LICENSE"
@@ -28,6 +30,10 @@
28
30
  "dsh": {
29
31
  "bundle": {
30
32
  "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "platform": "web",
36
+ "inject": []
31
37
  }
32
38
  },
33
39
  "keywords": [