dsh-lh-data 0.1.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.
Files changed (89) hide show
  1. package/README.md +332 -0
  2. package/cordis.patch.yml +7 -0
  3. package/lib/admin-contract.d.ts +274 -0
  4. package/lib/admin-http.d.ts +31 -0
  5. package/lib/admin-validate.d.ts +87 -0
  6. package/lib/admin.d.ts +60 -0
  7. package/lib/client.js +2969 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/datasource/columns.d.ts +13 -0
  10. package/lib/datasource/connection.d.ts +33 -0
  11. package/lib/datasource/connector/base.d.ts +21 -0
  12. package/lib/datasource/connector/mysql.d.ts +23 -0
  13. package/lib/datasource/connector/postgresql.d.ts +23 -0
  14. package/lib/datasource/crypto.d.ts +13 -0
  15. package/lib/datasource/driver.d.ts +39 -0
  16. package/lib/datasource/errors.d.ts +25 -0
  17. package/lib/datasource/importer.d.ts +38 -0
  18. package/lib/datasource/source-sql.d.ts +11 -0
  19. package/lib/datasource/source-store.d.ts +28 -0
  20. package/lib/datasource/types.d.ts +119 -0
  21. package/lib/db.d.ts +91 -0
  22. package/lib/http-common.d.ts +31 -0
  23. package/lib/http.d.ts +13 -0
  24. package/lib/index.d.ts +50 -0
  25. package/lib/index.js +5735 -0
  26. package/lib/parse.d.ts +41 -0
  27. package/lib/preview.d.ts +75 -0
  28. package/lib/render.d.ts +100 -0
  29. package/lib/scope-registry.d.ts +47 -0
  30. package/lib/scope.d.ts +56 -0
  31. package/lib/sql.d.ts +133 -0
  32. package/lib/store.d.ts +174 -0
  33. package/lib/table.d.ts +38 -0
  34. package/lib/tooling.d.ts +267 -0
  35. package/lib/tools/datasource.d.ts +16 -0
  36. package/lib/tools/import.d.ts +21 -0
  37. package/lib/tools/read.d.ts +77 -0
  38. package/lib/tools/registry.d.ts +12 -0
  39. package/lib/tools/write.d.ts +37 -0
  40. package/lib/view.d.ts +138 -0
  41. package/package.json +61 -0
  42. package/src/admin-contract.ts +351 -0
  43. package/src/admin-http.ts +254 -0
  44. package/src/admin-validate.ts +393 -0
  45. package/src/admin.ts +558 -0
  46. package/src/client/index.ts +403 -0
  47. package/src/client/settings/CreateForm.tsx +186 -0
  48. package/src/client/settings/DataSourceForm.tsx +278 -0
  49. package/src/client/settings/DataSourcesPanel.tsx +301 -0
  50. package/src/client/settings/DatasetEditor.tsx +245 -0
  51. package/src/client/settings/DatasetTable.tsx +110 -0
  52. package/src/client/settings/DatasetsPanel.tsx +207 -0
  53. package/src/client/settings/RowsPanel.tsx +196 -0
  54. package/src/client/settings/Section.tsx +41 -0
  55. package/src/client/settings/SourceTablesPanel.tsx +226 -0
  56. package/src/client/settings/api.ts +154 -0
  57. package/src/client/settings/styles.ts +125 -0
  58. package/src/datasource/columns.ts +56 -0
  59. package/src/datasource/connection.ts +124 -0
  60. package/src/datasource/connector/base.ts +54 -0
  61. package/src/datasource/connector/mysql.ts +187 -0
  62. package/src/datasource/connector/postgresql.ts +212 -0
  63. package/src/datasource/crypto.ts +61 -0
  64. package/src/datasource/driver.ts +108 -0
  65. package/src/datasource/errors.ts +69 -0
  66. package/src/datasource/importer.ts +262 -0
  67. package/src/datasource/index.ts +62 -0
  68. package/src/datasource/source-sql.ts +64 -0
  69. package/src/datasource/source-store.ts +152 -0
  70. package/src/datasource/types.ts +133 -0
  71. package/src/db.ts +277 -0
  72. package/src/http-common.ts +91 -0
  73. package/src/http.ts +130 -0
  74. package/src/index.ts +486 -0
  75. package/src/parse.ts +213 -0
  76. package/src/preview.ts +198 -0
  77. package/src/render.ts +294 -0
  78. package/src/scope-registry.ts +111 -0
  79. package/src/scope.ts +159 -0
  80. package/src/sql.ts +491 -0
  81. package/src/store.ts +412 -0
  82. package/src/table.ts +160 -0
  83. package/src/tooling.ts +551 -0
  84. package/src/tools/datasource.ts +378 -0
  85. package/src/tools/import.ts +282 -0
  86. package/src/tools/read.ts +536 -0
  87. package/src/tools/registry.ts +56 -0
  88. package/src/tools/write.ts +241 -0
  89. package/src/view.ts +371 -0
package/lib/parse.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 文件解析与类型推断 —— 移植自 `agentic-data-mini` 的 `src/lib/utils/fileParser.ts`。
3
+ *
4
+ * 保留的关键行为(设计文档 §4):
5
+ * - XLSX:`XLSX.read({ type: 'buffer', cellDates: true })` + `sheet_to_json({ raw: true, defval: null })`,
6
+ * Date → `YYYY-MM-DD`;默认第一个 sheet。
7
+ * - CSV:`Papa.parse({ header: true, skipEmptyLines: true, dynamicTyping: true, transformHeader: trim })`。
8
+ * - 列名:**保留原名(含中文)**,SQL 中双引号包裹;空列名 → `column`;重名追加 `_2`。
9
+ * - 类型推断:编码/号码类列名强制 text;13+ 位整数或 `xE+12` 科学计数法强制 text(防精度丢失);
10
+ * 其余 numeric>80% / boolean>90% / date 正则>70%。
11
+ */
12
+ export type ColumnType = 'text' | 'numeric' | 'boolean' | 'date';
13
+ export interface ColumnInfo {
14
+ /** 原始表头(如「物料编码」)。 */
15
+ name: string;
16
+ /** SQL 中使用的列名(与 name 相同,或去重后加 `_2`)。 */
17
+ sanitizedName: string;
18
+ type: ColumnType;
19
+ nullable: boolean;
20
+ sample: unknown[];
21
+ description?: string;
22
+ }
23
+ export interface ParsedFile {
24
+ headers: string[];
25
+ rows: Record<string, unknown>[];
26
+ rowCount: number;
27
+ columns: ColumnInfo[];
28
+ }
29
+ export declare const DEFAULT_SAMPLE_ROWS = 100;
30
+ /** 列名消毒:trim,空列名回落 `column`,其余保留原名。 */
31
+ export declare function sanitizeColumnName(name: string): string;
32
+ /** 重名列追加 `_2` / `_3`(原地修改)。 */
33
+ export declare function deduplicateColumnNames(columns: ColumnInfo[]): void;
34
+ /** 编码/号码类列:即使值是数字也应存 text(无数学含义)。 */
35
+ export declare function isCodeOrIdField(columnName: string): boolean;
36
+ /** 大数判定:13+ 位整数或 `1.78E+12` 这类科学计数法。 */
37
+ export declare function isLargeNumber(value: unknown): boolean;
38
+ /** 从采样值 + 列名推断列类型;列名优先级最高。 */
39
+ export declare function inferColumnType(values: unknown[], columnName?: string): ColumnType;
40
+ export declare function parseCSV(buffer: Buffer, sampleRows?: number): ParsedFile;
41
+ export declare function parseXLSX(buffer: Buffer, sheetName?: string, sampleRows?: number): ParsedFile;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 模型可见片段的构造 —— 设计文档 §5.3。
3
+ *
4
+ * 这里只做两件事:把结果裁成「前几行 + 可选尾几行」的预览,以及基于**全量数据**
5
+ * 生成类型化摘要(不是只看预览行)。摘要让模型在不读全表的情况下也能下结论,
6
+ * 从而不再需要逐页翻数据。
7
+ */
8
+ import type { Database, Row } from './db';
9
+ import type { ColumnType } from './parse';
10
+ export type PreviewStrategy = 'head' | 'head-tail';
11
+ export interface PreviewOptions {
12
+ previewRows: number;
13
+ previewStrategy: PreviewStrategy;
14
+ previewColumns: number;
15
+ summaryEnabled: boolean;
16
+ /** 参与摘要的列数上限(宽表保护)。 */
17
+ summaryMaxColumns: number;
18
+ /** 文本列取 top 值的列数上限。 */
19
+ summaryMaxTextColumns: number;
20
+ }
21
+ /** 一列的统计摘要;字段按类型给出,缺失即不适用。 */
22
+ export interface ColumnSummary {
23
+ name: string;
24
+ type: ColumnType;
25
+ /** 非空值个数。 */
26
+ count: number;
27
+ min?: number | string | null;
28
+ max?: number | string | null;
29
+ avg?: number | null;
30
+ sum?: number | null;
31
+ /** distinct 取值数(文本列)。 */
32
+ distinct?: number;
33
+ /** 高频取值(文本列,distinct 过多时省略)。 */
34
+ top?: {
35
+ value: string;
36
+ count: number;
37
+ }[];
38
+ /** 布尔列为真的行数。 */
39
+ trueCount?: number;
40
+ }
41
+ export interface PreviewSlice {
42
+ rows: Row[];
43
+ columns: string[];
44
+ /** 是否折叠了列(结果列数超过 previewColumns)。 */
45
+ columnTruncated: boolean;
46
+ /** 头尾之间是否跳过了行。 */
47
+ gap: boolean;
48
+ /** 被跳过的行数。 */
49
+ skipped: number;
50
+ }
51
+ /** 头/尾各取多少行;`tail > 0` 时渲染层会在中间插入省略提示。 */
52
+ export declare function previewSlicePlan(totalRows: number, options: PreviewOptions): {
53
+ head: number;
54
+ tail: number;
55
+ };
56
+ /** 裁剪预览列(超宽表只保留前 N 列)。 */
57
+ export declare function previewColumns(columns: string[], options: PreviewOptions): {
58
+ columns: string[];
59
+ truncated: boolean;
60
+ };
61
+ /** 拼装一次预览切片(头行 + 可选尾行),同时报告跳过了多少行。 */
62
+ export declare function buildPreviewSlice(headRows: Row[], tailRows: Row[], columns: string[], totalRows: number, options: PreviewOptions): PreviewSlice;
63
+ /** 单元格按字符数截断,返回文本与被截断标记。 */
64
+ export declare function truncateCell(value: unknown, max: number): {
65
+ text: string;
66
+ truncated: boolean;
67
+ };
68
+ /**
69
+ * 基于全表生成列摘要:numeric/date/boolean 走一次聚合扫描,
70
+ * 文本列额外做 distinct 计数与 top 取值(受列数上限保护)。
71
+ */
72
+ export declare function summarizeColumns(db: Database, tableName: string, columns: {
73
+ name: string;
74
+ type: ColumnType;
75
+ }[], options: PreviewOptions): Promise<ColumnSummary[]>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 模型可见文本的渲染(对齐 `dsh-lh-judge` 的 render.ts 定位)。
3
+ *
4
+ * 约定:`output.render` 只做纯函数渲染,不读状态;物理表名绝不出现在这里。
5
+ */
6
+ import type { Row } from './db';
7
+ import type { ColumnInfo } from './parse';
8
+ import { type ColumnSummary } from './preview';
9
+ export interface DatasetListItem {
10
+ datasetId: string;
11
+ name: string;
12
+ rowCount: number;
13
+ columnCount: number;
14
+ status: string;
15
+ sourcePath: string | null;
16
+ createdAt: number;
17
+ }
18
+ export declare const MAX_RENDER_CELL = 40;
19
+ export declare const MAX_RENDER_ROWS = 50;
20
+ export declare function truncateText(text: string, max: number): string;
21
+ /** 单元格转义:去掉换行与竖线,避免破坏 markdown 表格。 */
22
+ export declare function formatValue(value: unknown): string;
23
+ export declare function formatTimestamp(epochMs: number): string;
24
+ export declare function renderDatasetList(items: DatasetListItem[]): string;
25
+ export declare function renderColumns(columns: ColumnInfo[]): string;
26
+ export declare function renderRows(columns: string[], rows: Row[], maxRows?: number): string;
27
+ /** 片段渲染的输入(结构化而非 import 工具类型,避免 render → tools 的循环)。 */
28
+ export interface QueryPreviewInput {
29
+ name: string;
30
+ datasetId: string;
31
+ columnCount: number;
32
+ /** 实际命中行数。 */
33
+ matchedRows: number;
34
+ /** 本次可服务行数(已按上限收敛)。 */
35
+ totalRows: number;
36
+ preview: {
37
+ rows: Row[];
38
+ columns: string[];
39
+ /** 头段行数;`gap` 为真时其后是尾段。 */
40
+ headCount: number;
41
+ gap: boolean;
42
+ skipped: number;
43
+ columnTruncated: boolean;
44
+ hiddenColumns: number;
45
+ };
46
+ summary: ColumnSummary[];
47
+ /** 有小节被省略(预览只是结果的一部分)。 */
48
+ truncated: boolean;
49
+ /** 存在前端视图。 */
50
+ view?: {
51
+ pageSize: number;
52
+ stable: boolean;
53
+ };
54
+ }
55
+ /**
56
+ * 渲染查询结果的模型可见片段:行数概览 + 少量预览行 + 类型化摘要 + 视图提示。
57
+ * 目标是把上下文占用压到常量级,同时保留「足以下结论」的统计信息。
58
+ */
59
+ export declare function renderQueryPreview(input: QueryPreviewInput): string;
60
+ export interface SourceListItem {
61
+ id: string;
62
+ name: string;
63
+ type: string;
64
+ host: string;
65
+ port: number;
66
+ database: string;
67
+ status: string;
68
+ lastError: string | null;
69
+ lastCheckedAt: number | null;
70
+ }
71
+ export interface SourceTableItem {
72
+ tableName: string;
73
+ schemaName: string | null;
74
+ rowCount: number;
75
+ columnCount: number;
76
+ primaryKey: string | null;
77
+ }
78
+ export declare function renderSourceList(items: SourceListItem[]): string;
79
+ export declare function renderSourceTables(sourceName: string, schema: string | null, items: SourceTableItem[]): string;
80
+ export interface ConnectionTestView {
81
+ name: string;
82
+ success: boolean;
83
+ latency: number;
84
+ version: string | null;
85
+ error: string | null;
86
+ }
87
+ export declare function renderConnectionTest(view: ConnectionTestView): string;
88
+ /** 包装成 dsh 的回注用户消息(带 source 标记,便于去重/追踪)。 */
89
+ export interface UserMessage {
90
+ role: 'user';
91
+ content: {
92
+ type: 'text';
93
+ text: string;
94
+ }[];
95
+ source: {
96
+ kind: 'plugin';
97
+ plugin: string;
98
+ };
99
+ }
100
+ export declare function toUserMessage(text: string, plugin: string): UserMessage;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 工作区(scope)注册表 —— 设置页「聚合展示全部工作区」的枚举来源。
3
+ *
4
+ * 为什么需要这张表:`perWorkspace=false`(默认)时所有 scope 共用一个库文件,
5
+ * `SELECT DISTINCT scope_key FROM datasets` 就能枚举;但 `perWorkspace=true`
6
+ * 时每个 scope 是独立的 `<hash>.db`,库文件名是 scope 的单向哈希,
7
+ * **没有任何办法反查「这台机器上有哪些工作区」**。
8
+ *
9
+ * 因此把见过的 scope 记在**目录库**里:一个不做 scope 分片的固定库
10
+ * (`perWorkspace=false` 时与业务库是同一个文件,不额外占地方)。
11
+ *
12
+ * 与 `DatasetStore` 分开,是为了让 store 保持单一职责(数据集元数据)并守住
13
+ * 200 行的类体积上限。
14
+ */
15
+ import { type DbConfig } from './db';
16
+ export interface ScopeEntry {
17
+ /** 规范化后的工作区目录(默认模式)或 `ws:<id>`(perWorkspace 模式)。 */
18
+ scopeKey: string;
19
+ firstSeen: number;
20
+ lastSeen: number;
21
+ }
22
+ export declare class ScopeRegistry {
23
+ private readonly cfg;
24
+ private ready;
25
+ constructor(cfg: DbConfig);
26
+ /**
27
+ * 记录一次 scope 使用:新建数据集时调用,已存在则刷新 `last_seen`。
28
+ * 目录库要么与业务库同一个连接,要么是同目录下的兄弟文件,
29
+ * 因此这里的失败一律向上抛 —— 静默吞掉会让工作区从设置页里凭空消失。
30
+ */
31
+ record(scopeKey: string): Promise<void>;
32
+ /** 全部已知工作区,按最近使用倒序。 */
33
+ list(): Promise<ScopeEntry[]>;
34
+ /**
35
+ * 该 scope 是否已知。写操作的准入校验:设置页的请求带不到会话上下文,
36
+ * 若放行任意字符串,浏览器就能凭空造工作区、造库文件。
37
+ */
38
+ has(scopeKey: string): Promise<boolean>;
39
+ /** 目录库连接:幂等建表,且只回填一次。 */
40
+ private catalog;
41
+ /**
42
+ * 回填:`perWorkspace=true` 时目录库里没有 `datasets` 表,语句会失败 ——
43
+ * 那是预期情况,此时注册表本就只靠 `record()` 累积,静默跳过即可。
44
+ */
45
+ private backfill;
46
+ }
47
+ export declare function createScopeRegistry(cfg: DbConfig): ScopeRegistry;
package/lib/scope.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 作用域与路径解析(设计文档 §2.4 / §7.4)。
3
+ *
4
+ * - scopeKey:默认取调用 agent 的会话工作目录(规范化后的绝对路径),即
5
+ * `exec.agent.session.header.cwd`(`SessionHeader`);`perWorkspace` 时提升为
6
+ * `WorkspaceId`(`ws:<id>`),不可用时回落 cwd。
7
+ * - 拿不到会话 cwd 时 fail-loud(抛 `ScopeError`):不拿 `process.cwd()` 顶替,
8
+ * 否则数据集会静默登记到 dsh 进程的启动目录。
9
+ * - 路径:导入文件必须解析后落在 scope 目录内(防 `../` 穿越)+ 扩展名白名单。
10
+ */
11
+ import type { ToolExec } from './tooling';
12
+ export interface ScopeContext {
13
+ /** 数据集归属键(写入 `datasets.scope_key`)。 */
14
+ scopeKey: string;
15
+ /** 解析相对路径用的工作区目录。 */
16
+ cwd: string;
17
+ /** `perWorkspace` 模式下的 WorkspaceId。 */
18
+ workspaceId?: string;
19
+ }
20
+ export declare const FILE_EXTENSIONS: readonly [".xlsx", ".xls", ".csv"];
21
+ export declare const DEFAULT_SCOPE = "default";
22
+ export declare class ScopeError extends Error {
23
+ readonly code = "SCOPE_ERROR";
24
+ constructor(message: string);
25
+ }
26
+ /** 规范化目录:解析为绝对路径,并在 Windows 上把盘符小写化(便于比较)。 */
27
+ export declare function normalizeDirectory(input: string): string;
28
+ /**
29
+ * 目录的文件系统身份:`realpath` 后再规范化。
30
+ *
31
+ * Windows 的 junction / 符号链接、macOS 的 `/var` → `/private/var` 都会让纯词法
32
+ * 比较误判归属,因此 scope 目录与导入目标一律按文件系统身份比较。与 dsh 的
33
+ * `canonicalPath`(packages/sandbox/sandbox/src/roots.ts)同思路:用
34
+ * `realpathSync.native` 逐段跟随链接;路径不存在时原样返回,由后续的存在性校验
35
+ * (`assertReadableFile`)给出真正的错误。
36
+ */
37
+ export declare function canonicalDirectory(input: string): string;
38
+ /**
39
+ * 从一次工具执行解析 scope。
40
+ *
41
+ * 拿不到会话 cwd 时 fail-loud(抛 `ScopeError`):宁可让这次调用失败,也不能静默用
42
+ * `process.cwd()` 顶替 —— 后者是 dsh 进程的启动目录,数据集会登记到错误的工作区
43
+ * (换回正确 cwd 后 `dataset_list` 就看不见它们)。
44
+ * `perWorkspace` 且拿不到 WorkspaceId 时仍用 cwd。
45
+ */
46
+ export declare function resolveScope(ctx: unknown, cfg: {
47
+ perWorkspace: boolean;
48
+ }, exec: ToolExec): Promise<ScopeContext>;
49
+ /** target 是否位于 baseDir 之内(含子目录)。 */
50
+ export declare function isInside(baseDir: string, target: string): boolean;
51
+ /** 解析导入路径:相对路径以 cwd 为基,越界或扩展名不在白名单即拒绝。 */
52
+ export declare function resolveInputFile(cwd: string, input: string): string;
53
+ /** 读取前校验:必须是普通文件、非空且不超过 `maxBytes`。 */
54
+ export declare function assertReadableFile(path: string, maxBytes: number): Promise<{
55
+ size: number;
56
+ }>;
package/lib/sql.d.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 只读校验器 + 结构化查询拼装 + 标识符引号化(设计文档 §7.1 / §7.3)。
3
+ *
4
+ * 参考实现的「禁词表」会误伤字符串字面量(如 `WHERE name='DROP'`),这里改为五步校验:
5
+ * 1. 按引号状态机剥离 `--` 与 `/* *\/` 注释(不动字符串字面量);
6
+ * 2. 把字符串字面量内容置空后再扫分号,拒绝多语句;
7
+ * 3. 首关键字白名单 `{ SELECT, WITH, EXPLAIN }`;
8
+ * 4. 顶层关键字扫描(命中写/DDL/事务关键字即拒绝,因此 `WITH … DELETE` 也会被拦下);
9
+ * 5. 表引用必须落在当前 scope 已登记的表名集合内;
10
+ * 6. 未显式声明 `LIMIT` 时补 `maxQueryRows`(有 `OFFSET` 无 `LIMIT` 直接拒绝)。
11
+ */
12
+ export declare class SqlError extends Error {
13
+ readonly code = "INVALID_SQL";
14
+ constructor(message: string);
15
+ }
16
+ /** 标识符双引号包裹,内部 `"` 转义为 `""`(列名保留原名/中文)。 */
17
+ export declare function quoteIdentifier(name: string): string;
18
+ /**
19
+ * 剥离注释,保留字符串字面量与引号标识符内的原样内容。
20
+ * 逐个扫描字符,按 `'` / `"` / `` ` `` 的状态机跳过引号区域。
21
+ */
22
+ export declare function stripComments(sql: string): string;
23
+ /** 把单引号字符串字面量的内容替换为空格(保留引号与长度),供关键字扫描使用。 */
24
+ export declare function blankStringLiterals(sql: string): string;
25
+ /**
26
+ * 把模型书写的 `ds` 别名替换为数据集的物理表名。
27
+ *
28
+ * 模型拿不到(也不该拿到)物理表名,因此原始 SQL 用保留别名 `ds` 指代本数据集;
29
+ * 只替换裸标识符,引号与字符串字面量内部不受影响。
30
+ */
31
+ export declare function substituteDatasetAlias(sql: string, tableName: string): string;
32
+ /** 提取 `FROM` / `JOIN` 之后的表名(跳过子查询 `( … )`)。 */
33
+ export declare function extractTableReferences(sql: string): string[];
34
+ export interface ReadOnlyQueryOptions {
35
+ /** 当前 scope 已登记的物理表名集合。 */
36
+ allowedTables: Iterable<string>;
37
+ /** 未显式声明 LIMIT 时补上的行数上限。 */
38
+ maxRows: number;
39
+ }
40
+ /** 一条语句解析出的 `LIMIT` / `OFFSET`(只认字面量整数,参数化写法交给调用方)。 */
41
+ export interface LimitClause {
42
+ limit: number;
43
+ offset: number;
44
+ }
45
+ /** 解析 `LIMIT n [OFFSET m | , m]`;只有 OFFSET 没有 LIMIT 时抛错(SQLite 要求 LIMIT 在前)。 */
46
+ export declare function parseLimitClause(blanked: string): LimitClause | undefined;
47
+ /** 校验后的只读语句:分页/计数都从 `baseSql` 派生。 */
48
+ export interface NormalizedQuery {
49
+ /** 去掉注释与尾部分号、已通过关键字与表引用校验的语句(仍可能带模型声明的 LIMIT)。 */
50
+ stripped: string;
51
+ /** 与 `stripped` **等长**、字符串字面量置空的版本(关键字扫描用)。 */
52
+ blanked: string;
53
+ /** 首关键字:`SELECT` / `WITH` / `EXPLAIN`。 */
54
+ leading: string;
55
+ /** 模型显式声明的 LIMIT/OFFSET。 */
56
+ limit?: LimitClause;
57
+ }
58
+ /**
59
+ * 只读校验的前五步(设计文档 §5 的 1–5),不做 LIMIT 补全。
60
+ * 校验器只在这里收紧一次,派生的 count/page 语句都建立在它的输出上。
61
+ */
62
+ export declare function normalizeReadOnlyQuery(sql: string, options: ReadOnlyQueryOptions): NormalizedQuery;
63
+ /**
64
+ * 校验并规范化一条只读 SQL。返回值是可直接执行的单语句(已补 LIMIT)。
65
+ * 任何不合规都抛 `SqlError`。
66
+ */
67
+ export declare function validateReadOnlyQuery(sql: string, options: ReadOnlyQueryOptions): string;
68
+ /** `where` 是模型提供的布尔表达式片段(不含 WHERE 关键字),做同样的收紧校验。 */
69
+ export declare function validateWhereExpression(expression: string): string;
70
+ /** `orderBy` 形如 `单价 DESC, 名称`,逐项校验并引号化。 */
71
+ export declare function validateOrderBy(expression: string): string;
72
+ export interface StructuredQuery {
73
+ table: string;
74
+ columns?: string[];
75
+ where?: string;
76
+ orderBy?: string;
77
+ limit?: number;
78
+ offset?: number;
79
+ /** 该数据集已登记的列名(不含系统列之外的任意标识符)。 */
80
+ allowedColumns: string[];
81
+ maxRows: number;
82
+ }
83
+ /** 结构化查询的骨架:不带 LIMIT 的基础语句 + 稳定排序(总是以 `_row_id` 兜底)。 */
84
+ export interface StructuredPlan {
85
+ baseSql: string;
86
+ orderBy: string;
87
+ }
88
+ /**
89
+ * 拼装结构化查询的骨架(插件自己拼 SQL,天然安全,作为推荐路径)。
90
+ * 排序恒定追加 `_row_id` 兜底键,保证翻页不重不漏(设计文档 §6)。
91
+ */
92
+ export declare function buildStructuredPlan(query: StructuredQuery): StructuredPlan;
93
+ /** 拼装结构化查询(插件自己拼 SQL,天然安全,作为推荐路径)。 */
94
+ export declare function buildStructuredQuery(query: StructuredQuery): {
95
+ sql: string;
96
+ params: unknown[];
97
+ };
98
+ /** 一条可分页的只读查询计划。三条语句都建立在**已校验**的基础语句之上。 */
99
+ export interface QueryPlan {
100
+ /** 已校验、已切掉模型声明的 LIMIT/OFFSET 与 ORDER BY 的基础语句。 */
101
+ baseSql: string;
102
+ /** 模型(或结构化参数)给出的排序项,已引号化;可空。 */
103
+ baseOrder?: string;
104
+ /** 兜底排序键(通常 `_row_id`);拿不到时为空,此时不保证翻页稳定。 */
105
+ tie?: string;
106
+ /** `SELECT COUNT(*) … FROM (<baseSql>)`。 */
107
+ countSql: string;
108
+ /** 默认排序下的分页语句:`… ORDER BY … LIMIT ? OFFSET ?`。 */
109
+ pageSql: string;
110
+ /** 该计划能服务的最大行数(模型声明的 LIMIT 与 maxRows 取小)。 */
111
+ rowCap: number;
112
+ /** 模型是否显式声明了 LIMIT(未声明时视图上限另按 maxViewRows 计算)。 */
113
+ declaredLimit?: number;
114
+ /** 模型声明的 OFFSET:只影响它看到的片段,视图始终从第一行开始。 */
115
+ previewOffset: number;
116
+ /** 排序是否稳定:false 时前端禁止深翻页。 */
117
+ stable: boolean;
118
+ /** EXPLAIN 语句:不产生视图,也不分页。 */
119
+ explain: boolean;
120
+ }
121
+ /** 按「基础语句 + 排序项 + 兜底键」拼分页语句。 */
122
+ export declare function composePagedSql(plan: Pick<QueryPlan, 'baseSql' | 'baseOrder' | 'tie'>): string;
123
+ /** 由结构化参数派生查询计划(排序恒定以 `_row_id` 兜底)。 */
124
+ export declare function planStructuredQuery(query: StructuredQuery): QueryPlan;
125
+ /**
126
+ * 由模型书写的原始 SQL 派生查询计划。
127
+ *
128
+ * 基础语句先过 `normalizeReadOnlyQuery`(含表引用白名单),随后:
129
+ * 1. 切掉尾部 `LIMIT/OFFSET` —— 分页由视图接管,模型声明的 LIMIT 降级为行数上限;
130
+ * 2. 只有「单表 + 无 GROUP BY/DISTINCT + 非复合查询」时才补 `_row_id` 兜底排序,
131
+ * 其余情况保留模型自己的 ORDER BY,并把 `stable` 记为 false。
132
+ */
133
+ export declare function planRawQuery(sql: string, options: ReadOnlyQueryOptions): QueryPlan;
package/lib/store.d.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 数据集元数据(替代参考实现的 drizzle `dataset_metadata`)+ 归属断言。
3
+ *
4
+ * 设计文档 §5.3:`datasets` 表是插件自有的元数据表,物理表名只在本模块生成与持有,
5
+ * 对外只暴露 `datasetId` / 登记名。所有读写都带 `scope_key` 过滤(`assertOwned`)。
6
+ */
7
+ import { type Database } from './db';
8
+ import type { DataSourceStore } from './datasource/source-store';
9
+ import { type ScopeRegistry } from './scope-registry';
10
+ import type { ColumnInfo } from './parse';
11
+ import type { ScopeContext } from './scope';
12
+ import type { ToolExec } from './tooling';
13
+ import type { ViewRegistry } from './view';
14
+ /** 插件配置(结构子集由 index.ts 的 `Config` 复用,避免 tools → index 的类型循环)。 */
15
+ export interface DataConfig {
16
+ /** libSQL 库路径;空 → `$DSH_HOME/lh-data/data.db`。 */
17
+ dbPath: string;
18
+ /** 非空则覆盖 dbPath,可为 `file:` 或 `libsql://`。 */
19
+ dbUrl: string;
20
+ /** 远程 Turso token;建议留空并走 `TURSO_AUTH_TOKEN`。 */
21
+ authToken: string;
22
+ /** true 时 scope 用 WorkspaceId 且每个工作区独立库文件。 */
23
+ perWorkspace: boolean;
24
+ requireApprovalForWrites: boolean;
25
+ allowRawSql: boolean;
26
+ maxFileBytes: number;
27
+ maxInsertRows: number;
28
+ maxQueryRows: number;
29
+ batchSize: number;
30
+ backgroundThresholdRows: number;
31
+ previewSampleRows: number;
32
+ readOnly: boolean;
33
+ /** 视图模式:auto(超阈值才建视图)/ always / never。 */
34
+ viewMode: 'auto' | 'always' | 'never';
35
+ viewThresholdRows: number;
36
+ viewThresholdBytes: number;
37
+ previewRows: number;
38
+ previewStrategy: 'head' | 'head-tail';
39
+ previewCellChars: number;
40
+ previewColumns: number;
41
+ summaryEnabled: boolean;
42
+ summaryMaxColumns: number;
43
+ summaryMaxTextColumns: number;
44
+ defaultPageSize: number;
45
+ maxPageSize: number;
46
+ maxViewRows: number;
47
+ viewRoutePrefix: string;
48
+ /** 是否挂载设置页管理接口(关闭后路由不注册,前端显示不可用)。 */
49
+ adminEnabled: boolean;
50
+ /** 管理接口请求体字节上限。 */
51
+ adminMaxBodyBytes: number;
52
+ /** 聚合列表扫描的数据集上限,超限即截断并提示。 */
53
+ adminMaxDatasets: number;
54
+ /** 是否启用数据源(关闭后不注册 datasource_* 工具与 /sources 接口)。 */
55
+ datasourceEnabled: boolean;
56
+ /** 远端表分块拉取的行数。 */
57
+ datasourceFetchBatchSize: number;
58
+ /** 连接 / 连通性测试的超时毫秒数。 */
59
+ datasourceConnectTimeoutMs: number;
60
+ /** 单次导入的行数上限,0 表示不限。 */
61
+ datasourceMaxImportRows: number;
62
+ /** 数据源密码的加密密钥;空 → 环境变量 LH_DATA_ENCRYPT_KEY。 */
63
+ datasourceEncryptKey: string;
64
+ }
65
+ export type DatasetStatus = 'importing' | 'ready' | 'failed';
66
+ export interface DatasetRecord {
67
+ id: string;
68
+ scopeKey: string;
69
+ name: string;
70
+ tableName: string;
71
+ sourcePath: string | null;
72
+ /** 来自数据源时记录数据源 id;文件导入为 null。 */
73
+ sourceId: string | null;
74
+ /** 来自数据源时的定位串 `<schema>.<table>`(脱敏,不含凭据)。 */
75
+ sourceRef: string | null;
76
+ /** 用户可编辑的说明(设置页「改描述」的落点);未填为 null。 */
77
+ description: string | null;
78
+ rowCount: number;
79
+ columns: ColumnInfo[];
80
+ status: DatasetStatus;
81
+ error: string | null;
82
+ createdAt: number;
83
+ updatedAt: number;
84
+ }
85
+ export declare class DatasetError extends Error {
86
+ readonly code = "DATASET_ERROR";
87
+ constructor(message: string);
88
+ }
89
+ /** 物理表名形状:`d_<scopeHash8>_<base40>_<ts36>`。 */
90
+ export declare const PHYSICAL_TABLE_PATTERN: RegExp;
91
+ export declare function assertPhysicalTableName(tableName: string): void;
92
+ /** 物理表名生成:只由插件调用,去掉参考实现的 userId,改用 scope 哈希。 */
93
+ export declare function generateTableName(base: string, scopeHash: string): string;
94
+ export declare function makeDatasetId(): string;
95
+ export declare class DatasetStore {
96
+ private readonly cfg;
97
+ /** 工作区注册表(跨工作区聚合枚举来源);默认随配置自动创建。 */
98
+ readonly scopes: ScopeRegistry;
99
+ private readonly initialized;
100
+ constructor(cfg: DataConfig,
101
+ /** 工作区注册表(跨工作区聚合枚举来源);默认随配置自动创建。 */
102
+ scopes?: ScopeRegistry);
103
+ /** 该 scope 对应的库连接(幂等建表 + 增量迁移)。 */
104
+ database(scopeKey: string): Promise<Database>;
105
+ list(scopeKey: string): Promise<DatasetRecord[]>;
106
+ /** 按 datasetId 或登记名查找;找不到返回 undefined(不抛错)。 */
107
+ find(scopeKey: string, reference: string): Promise<DatasetRecord | undefined>;
108
+ /** 当前 scope 已登记的物理表名集合(dataset_query 的 sql 白名单)。 */
109
+ tableNames(scopeKey: string): Promise<string[]>;
110
+ /** 解析句柄 + 归属断言(defense-in-depth)。 */
111
+ require(scopeKey: string, reference: string, options?: {
112
+ requireReady?: boolean;
113
+ }): Promise<DatasetRecord>;
114
+ /** scope 内唯一的名字:已存在则追加 `_2` / `_3`。 */
115
+ uniqueName(scopeKey: string, name: string): Promise<string>;
116
+ create(record: DatasetRecord): Promise<void>;
117
+ update(scopeKey: string, id: string, patch: Partial<Pick<DatasetRecord, 'name' | 'rowCount' | 'columns' | 'status' | 'error' | 'sourcePath' | 'sourceId' | 'sourceRef' | 'description'>>): Promise<void>;
118
+ remove(scopeKey: string, id: string): Promise<void>;
119
+ }
120
+ export declare function createStore(cfg: DataConfig): DatasetStore;
121
+ /** 后台任务注册表的最小视图(`ctx.jobs` 可选依赖)。 */
122
+ export interface JobRegistryLike {
123
+ start(spec: {
124
+ kind: string;
125
+ label: string;
126
+ owner?: unknown;
127
+ run(): {
128
+ cancel(reason?: string): void;
129
+ done: Promise<unknown>;
130
+ };
131
+ }): string;
132
+ }
133
+ /**
134
+ * `dsh-host-webserver` 的最小视图(duck-typed,避免依赖 dsh 运行时)。
135
+ * 缺失时(CLI / TUI 剖面)不注册任何路由,视图能力整体降级。
136
+ */
137
+ export interface WebServerLike {
138
+ register(route: {
139
+ kind: 'exact' | 'prefix';
140
+ path: string;
141
+ handler(request: unknown, response: unknown): void | Promise<void>;
142
+ }): () => void;
143
+ }
144
+ /** `dsh-client-connection` 的最小视图:给自建路由复用平台鉴权。 */
145
+ export interface ConnectionLike {
146
+ /** 通过返回 undefined;否则返回应写入的 HTTP 状态码。 */
147
+ requestRejection(request: unknown): 401 | 403 | undefined;
148
+ }
149
+ /** 注入给工具的运行时依赖。 */
150
+ export interface DataServices {
151
+ cfg: DataConfig;
152
+ store: DatasetStore;
153
+ /** 数据源登记表(catalog 库,全局共享)。 */
154
+ sources: DataSourceStore;
155
+ /** 解析一次调用的工作区;拿不到会话 cwd 时会 reject(`ScopeError`)。 */
156
+ scopeOf(exec: ToolExec): Promise<ScopeContext>;
157
+ /** 可选:`ctx.jobs` 不可用时后台导入降级为前台执行。 */
158
+ jobs?: JobRegistryLike;
159
+ /** 结果视图注册中心(设计文档 §6)。 */
160
+ views?: ViewRegistry;
161
+ /** 可选:HTTP 路由宿主;缺失即不提供前端分页。 */
162
+ webServer?: WebServerLike;
163
+ /** 可选:连接服务;缺失即不注册路由(自建路由必须有鉴权手段)。 */
164
+ connection?: ConnectionLike;
165
+ }
166
+ export interface ResolvedDataset {
167
+ scope: ScopeContext;
168
+ record: DatasetRecord;
169
+ db: Database;
170
+ }
171
+ /** 工具的统一入口:解析 scope → 断言归属 → 拿到库连接。 */
172
+ export declare function resolveDataset(services: DataServices, exec: ToolExec, reference: string, options?: {
173
+ requireReady?: boolean;
174
+ }): Promise<ResolvedDataset>;
package/lib/table.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 物理表的建/插/删 —— 移植自 `agentic-data-mini` 的 `src/lib/utils/tableManager.ts`。
3
+ *
4
+ * 保留的关键行为:`_row_id` 自增主键 + 业务列 + `_uploaded_at`;批量插入 100 行/批,
5
+ * 并按列类型做转换(boolean→0/1、numeric→Number、date→ISO、对象/数组→JSON)。
6
+ * 插件侧新增:批次之间的 `signal.throwIfAborted()` 检查点。
7
+ */
8
+ import type { Database, Row } from './db';
9
+ import type { ColumnInfo, ColumnType } from './parse';
10
+ /** 系统列:插入/更新时必须剔除。 */
11
+ export declare const SYSTEM_COLUMNS: readonly ["_row_id", "_uploaded_at"];
12
+ export declare const DEFAULT_BATCH_SIZE = 100;
13
+ export declare function columnTypeToSqlite(type: ColumnType): string;
14
+ export declare function createDatasetTable(db: Database, tableName: string, columns: ColumnInfo[]): Promise<void>;
15
+ /** 按列类型转换一个值(批量插入与单行更新共用)。 */
16
+ export declare function coerceValue(raw: unknown, type: ColumnType): unknown;
17
+ export interface InsertOptions {
18
+ batchSize?: number;
19
+ signal?: AbortSignal;
20
+ }
21
+ /** 批量插入,返回实际插入行数;每批之间检查取消信号。 */
22
+ export declare function insertRows(db: Database, tableName: string, columns: ColumnInfo[], rows: Record<string, unknown>[], options?: InsertOptions): Promise<number>;
23
+ export declare function dropDatasetTable(db: Database, tableName: string): Promise<void>;
24
+ export declare function countRows(db: Database, tableName: string): Promise<number>;
25
+ /** 剔除系统列,并报告未登记的列(写操作要明确拒绝未知列)。 */
26
+ export declare function sanitizeRowData(data: Record<string, unknown>, columns: ColumnInfo[]): {
27
+ values: Record<string, unknown>;
28
+ unknownColumns: string[];
29
+ };
30
+ /** 单行更新:按 `_row_id` 定位,返回受影响行数。 */
31
+ export declare function updateRow(db: Database, tableName: string, columns: ColumnInfo[], rowId: number, data: Record<string, unknown>): Promise<number>;
32
+ /** 单行删除:按 `_row_id` 定位,返回受影响行数。 */
33
+ export declare function deleteRow(db: Database, tableName: string, rowId: number): Promise<number>;
34
+ /** 读取查询结果(列信息从首行推导)。 */
35
+ export declare function selectRows(db: Database, sql: string, params?: unknown[]): Promise<{
36
+ rows: Row[];
37
+ columns: string[];
38
+ }>;