@cxxl/dsh-sqlite 0.1.1 → 0.3.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/README.md CHANGED
@@ -45,6 +45,20 @@ dsh plugin --profile web add @cxxl/dsh-sqlite
45
45
  - `to` / `from` 建议使用绝对路径
46
46
  - 部分导出时注意表间外键引用关系
47
47
 
48
+ ## 稳定触发(v1.2)
49
+
50
+ 插件向每个会话注入一段常驻提示词规则(约 40 token/回合):用户要求"记住/记录/跟踪/保存"或表达"以后查/对比/统计"意图时,使用 sqlite 工具而非文本文件。规则不覆盖模型判断,最终决策权在模型。
51
+
52
+ ## 跨会话协作(v2)
53
+
54
+ 多会话并行同一任务时的对齐机制(设计见 DESIGN.md 第 15 节):
55
+
56
+ - 协作数据放**命名库**(任务名,kebab-case),同一任务的会话共用同一个命名库
57
+ - 会话开工时自动收到**协作库清单**(含各库 meta 描述与表说明)
58
+ - 其他会话对命名库的写入会**自动提醒**相关会话(直到你查询确认);**新库出现会广播**
59
+ - 库表带描述:新建协作库后先建 meta 表(label / description / table:<名>)
60
+ - 默认库 agent.db 是个人数据区,不参与协作提醒
61
+
48
62
  ## 引擎
49
63
 
50
64
  Node 内置 `node:sqlite`(实验性,Node ≥ 22.5),零原生依赖;所有引擎调用隔离在 `lib/engine.js`,可整体替换为 better-sqlite3 而不动其余代码。
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,52 @@ 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
+ }
385
+
386
+ // 读取库内 meta 约定表(label/description/table:<名> 键值对);无表/读失败返回空对象。
387
+ export function readDbMeta(dbName, signal) {
388
+ assertNotAborted(signal)
389
+ const { file } = dbFile(dbName)
390
+ if (!existsSync(file)) return {}
391
+ const { db } = openDb(dbName)
392
+ const out = {}
393
+ try {
394
+ const rows = db.prepare('SELECT key, value FROM meta').all()
395
+ for (const r of rows) {
396
+ if (r.key !== undefined && r.value !== undefined) out[String(r.key)] = String(r.value)
397
+ }
398
+ } catch { /* 无 meta 表:返回空 */ }
399
+ return out
400
+ }
package/lib/index.js CHANGED
@@ -1,20 +1,126 @@
1
1
  // index.js — 插件入口:注册五个工具 + 可选挂载自测。
2
2
  import { defineTool } from '@deepseek-ai/dsh-tools'
3
- import { CallId } from '@deepseek-ai/dsh-llm'
3
+ import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
4
4
  import { mkdtempSync, rmSync } from 'node:fs'
5
5
  import { tmpdir } from 'node:os'
6
6
  import { join } from 'node:path'
7
7
  import { toolDefs } from './tools.js'
8
8
  import * as engine from './engine.js'
9
+ import { COLLAB_RULE, createCoordination, frame, onPostExecute, preStepTexts } from './coordination.js'
9
10
 
10
11
  export const name = '@cxxl/dsh-sqlite'
11
- export const inject = ['tools']
12
+ export const inject = ['tools', 'webServer', 'systemPrompt']
13
+
14
+ function sendJson(res, code, payload) {
15
+ const body = JSON.stringify(payload)
16
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
17
+ res.end(body)
18
+ }
19
+
20
+ function queryOf(req) {
21
+ return Object.fromEntries(new URL(req.url ?? '/', 'http://x').searchParams.entries())
22
+ }
23
+
24
+ // 面板只读 API:三个 HTTP 路由(与页面同源,client 直接 fetch)。
25
+ // webServer 声明为硬依赖(inject),cordis 会等服务就绪后再挂载本插件。
26
+ function registerPanelRoutes(ctx) {
27
+ const webServer = ctx.webServer
28
+ ctx.effect(() => {
29
+ const disposers = [
30
+ webServer.register({
31
+ kind: 'exact',
32
+ path: '/dsh-sqlite-api/list-dbs',
33
+ handler: async (req, res) => {
34
+ try {
35
+ sendJson(res, 200, { ok: true, dbs: engine.listDbFiles() })
36
+ } catch (err) {
37
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
38
+ }
39
+ },
40
+ }),
41
+ webServer.register({
42
+ kind: 'exact',
43
+ path: '/dsh-sqlite-api/tables',
44
+ handler: async (req, res) => {
45
+ try {
46
+ const q = queryOf(req)
47
+ sendJson(res, 200, { ok: true, ...engine.listTables(q.db ?? 'default') })
48
+ } catch (err) {
49
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
50
+ }
51
+ },
52
+ }),
53
+ webServer.register({
54
+ kind: 'exact',
55
+ path: '/dsh-sqlite-api/preview',
56
+ handler: async (req, res) => {
57
+ try {
58
+ const q = queryOf(req)
59
+ sendJson(res, 200, { ok: true, ...engine.tablePreview(q.db ?? 'default', q.table, 50) })
60
+ } catch (err) {
61
+ sendJson(res, 400, { ok: false, error: (err && err.message) || String(err) })
62
+ }
63
+ },
64
+ }),
65
+ ]
66
+ console.log('[dsh-sqlite] panel routes registered: /dsh-sqlite-api/list-dbs|tables|preview')
67
+ return () => { for (const d of disposers) d() }
68
+ }, 'dsh-sqlite panel routes')
69
+ }
70
+
71
+ // v1.2:常驻短规则注入——把"持久化场景 → sqlite 工具"的触发率推向接近确定。
72
+ // 机制依据:官方 dsh-plan-mode 的 systemPrompt.section 先例(DESIGN.md 第 14 节)。
73
+ function registerPersistenceRule(ctx) {
74
+ const dispose = ctx.systemPrompt.section({
75
+ name: 'dsh-sqlite:persistence-rule',
76
+ order: 1000,
77
+ text: '持久化规则:当用户要求记住、记录、跟踪、保存结构化数据,或表达"以后还要查/对比/统计"的意图时,使用 sqlite_exec(写)与 sqlite_query(读)工具,不要用普通文本文件替代数据库;不确定库里有什么时先调 sqlite_tables。' + COLLAB_RULE,
78
+ })
79
+ console.log('[dsh-sqlite] persistence rule section registered (order 1000)')
80
+ ctx.effect(() => dispose, 'dsh-sqlite persistence rule')
81
+ }
82
+
83
+ // v2:跨会话协作感知(DESIGN.md 第 15 节)。观察者语义:绝不修改执行链路。
84
+ function registerCoordination(ctx) {
85
+ const coord = createCoordination()
86
+ const names = new Set()
87
+ try {
88
+ for (const f of engine.listDbFiles()) {
89
+ if (f.name !== 'agent.db') names.add(f.name.replace(/\.db$/, ''))
90
+ }
91
+ } catch { /* 启动快照失败则按空处理 */ }
92
+ coord.knownDbs = names
93
+
94
+ ctx.on('tools/post-execute', (exec, result, next) => {
95
+ try { onPostExecute(coord, exec, result) } catch { /* 观察者绝不抛 */ }
96
+ return next()
97
+ })
98
+
99
+ ctx.on('agent/pre-step', async (payload, next) => {
100
+ const decision = await next()
101
+ if (decision === undefined || decision.kind === 'reject') return decision
102
+ try {
103
+ const texts = preStepTexts(coord, payload.agent, engine)
104
+ if (texts.length === 0) return decision
105
+ const msgs = texts.map((text) => createUserMessage({
106
+ content: [{ type: 'text', text: frame(text) }],
107
+ source: { kind: 'plugin', plugin: 'dsh-sqlite' },
108
+ }))
109
+ return { ...decision, messages: [...msgs, ...decision.messages] }
110
+ } catch {
111
+ return decision
112
+ }
113
+ })
114
+ }
12
115
 
13
116
  export function apply(ctx) {
14
117
  for (const key of Object.keys(toolDefs)) {
15
118
  ctx.tools.register(defineTool(toolDefs[key]))
16
119
  }
17
120
  ctx.effect(() => () => engine.closeAll())
121
+ registerPanelRoutes(ctx)
122
+ registerPersistenceRule(ctx)
123
+ registerCoordination(ctx)
18
124
 
19
125
  // 挂载自测:DSH_PLUGIN_SELFTEST=1 时在临时数据目录跑一遍真实执行管线。
20
126
  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.3.0",
4
+ "description": "DSH plugin: persistent SQLite tools for the agent (sqlite_query / sqlite_exec / sqlite_tables / sqlite_export / sqlite_import), a read-only table browser settings page, and a persistence-usage prompt rule — 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": [