@yejiming/dsh-data-agent 0.0.11 → 0.0.12

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { o as clientsSchema, t as createConnectionService } from "./connections-5sfdEDsG.js";
2
2
  import { a as DEFAULT_PRESET_ID, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-DP4RyRh1.js";
3
3
  import { r as apply$1 } from "./command-DuCpwVbl.js";
4
- import { n as apply$2 } from "./tool-DVh61An-.js";
4
+ import { n as apply$2 } from "./tool-DgL0fBfj.js";
5
5
  import { createHash } from "node:crypto";
6
6
  import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
7
7
  import { homedir } from "node:os";
@@ -186,12 +186,11 @@ function resolveDshHome(env = process.env) {
186
186
  /**
187
187
  * Install the packaged `preset/data-agent/` directory into
188
188
  * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target is
189
- * normally left untouched. The exact package-owned 0.0.9 composition is
190
- * migrated once because its two dynamic package rows are incompatible with
191
- * DSH Desktop's unpacked-ASAR loader; user-edited compositions are never
192
- * overwritten. `installPreset: false` never calls this. Best-effort a
193
- * failure logs a warning with manual install instructions instead of failing
194
- * the boot.
189
+ * normally left untouched. Exact package-owned legacy compositions are
190
+ * migrated once when their runtime contract changes; user-edited compositions
191
+ * are never overwritten. `installPreset: false` never calls this. Best-effort
192
+ * a failure logs a warning with manual install instructions instead of
193
+ * failing the boot.
195
194
  */
196
195
  async function installPreset(ctx, presetId) {
197
196
  const targetDir = join(resolveDshHome(), ".agent-presets", presetId);
@@ -210,13 +209,13 @@ async function installPreset(ctx, presetId) {
210
209
  return false;
211
210
  }
212
211
  }
213
- /** SHA-256 of the unmodified 0.0.9 composition that imported /tool and /command dynamically. */
214
- const LEGACY_PRESET_0_0_9_SHA256 = "bae875a90d638ea78715030246b0f8a9f1a2c3359ca61febb6ceb59d0fcd930a";
212
+ /** SHA-256 values of unmodified package-owned compositions safe to migrate. */
213
+ const LEGACY_MANAGED_PRESET_SHA256 = /* @__PURE__ */ new Set(["bae875a90d638ea78715030246b0f8a9f1a2c3359ca61febb6ceb59d0fcd930a", "d3c6f4049580069eec1c6b7de101f12c7fb30482ad317434afb69afb08a91fc6"]);
215
214
  /** Public for regression tests of the non-destructive preset migration gate. */
216
215
  function isLegacyManagedPreset(source) {
217
- return createHash("sha256").update(source).digest("hex") === LEGACY_PRESET_0_0_9_SHA256;
216
+ return LEGACY_MANAGED_PRESET_SHA256.has(createHash("sha256").update(source).digest("hex"));
218
217
  }
219
- /** Upgrade only the exact package-owned legacy composition; preserve every edited preset. */
218
+ /** Upgrade only exact package-owned legacy compositions; preserve every edited preset. */
220
219
  async function synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId) {
221
220
  const composition = join(targetDir, "agent.cordis.yml");
222
221
  try {
@@ -224,7 +223,7 @@ async function synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId) {
224
223
  if (isLegacyManagedPreset(current)) {
225
224
  const replacement = await readFile(join(sourceDir, "agent.cordis.yml"), "utf8");
226
225
  await writeFile(composition, replacement, "utf8");
227
- ctx.logger.info("data-agent: migrated preset at %s to profile-preloaded tools (removed dynamic /tool and /command rows)", composition);
226
+ ctx.logger.info("data-agent: migrated package-owned preset at %s to the current runtime contract", composition);
228
227
  return true;
229
228
  }
230
229
  if (current.includes("@yejiming/dsh-data-agent/tool") || current.includes("@yejiming/dsh-data-agent/command")) {
@@ -1,5 +1,8 @@
1
1
  import { a as classifyStatement, c as assertSingleStatement, i as runClientQuery, n as redactQueryResult, o as clientsSchema, r as redactSecretText, s as enforceReadRowLimit } from "./connections-5sfdEDsG.js";
2
2
  import { i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS } from "./defaults-DP4RyRh1.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { link, mkdir, unlink, writeFile } from "node:fs/promises";
5
+ import { resolve } from "node:path";
3
6
  import z from "schemastery";
4
7
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
8
  const VIEW_KINDS = [
@@ -203,11 +206,14 @@ function parseAnalysisRequest(input, prefix = "render-analysis") {
203
206
  if (!isRecord(input)) fail(prefix + ": 请求必须是对象");
204
207
  assertOnlyKeys(input, [
205
208
  "title",
209
+ "outputName",
206
210
  "summary",
207
211
  "datasets",
208
212
  "views"
209
213
  ], prefix);
210
214
  const title = requireNonEmptyString(input["title"], prefix + ".title");
215
+ const outputName = optionalString(input, "outputName", prefix);
216
+ if (outputName !== void 0 && outputName.trim().length === 0) fail(prefix + ".outputName: 必须是非空字符串");
211
217
  const summary = optionalString(input, "summary", prefix);
212
218
  const datasets = input["datasets"];
213
219
  if (!Array.isArray(datasets) || datasets.length < 1 || datasets.length > 6) fail(prefix + ": datasets 必须是 1-6 个");
@@ -242,6 +248,7 @@ function parseAnalysisRequest(input, prefix = "render-analysis") {
242
248
  datasets: parsedDatasets,
243
249
  views: parsedViews
244
250
  };
251
+ if (outputName !== void 0) request.outputName = outputName;
245
252
  if (summary !== void 0) request.summary = summary;
246
253
  return request;
247
254
  }
@@ -312,13 +319,15 @@ function rowsToArrays(columns, rows) {
312
319
  }
313
320
  /** JSON-encoded UTF-8 size of the normalized report (the 512 KiB bound). */
314
321
  function reportJsonBytes(report) {
315
- return new TextEncoder().encode(JSON.stringify(report)).length;
322
+ const { htmlPath: _htmlPath, ...dataReport } = report;
323
+ return new TextEncoder().encode(JSON.stringify(dataReport)).length;
316
324
  }
317
325
  /** One-line model-facing summary; never re-injects rows into model context (D5). */
318
326
  function formatAnalysisSummary(report) {
319
327
  const emptyIds = report.datasets.filter((dataset) => dataset.rows.length === 0).map((dataset) => dataset.id);
320
328
  let text = "已生成分析报告《" + report.title + "》:" + report.datasets.length + " 个数据集、" + report.views.length + " 个视图(version 1)。";
321
329
  if (emptyIds.length > 0) text += "其中 " + emptyIds.length + " 个数据集无数据:" + emptyIds.join("、") + "。";
330
+ if (report.htmlPath !== void 0) text += "Dashboard HTML已保存:" + report.htmlPath;
322
331
  return text;
323
332
  }
324
333
  const BASE_VIEW_PROPERTIES = {
@@ -508,6 +517,10 @@ const RENDER_ANALYSIS_PARAMETERS = {
508
517
  required: true,
509
518
  description: "报告标题,如「月度经营分析」"
510
519
  },
520
+ outputName: {
521
+ type: "string",
522
+ description: "可选语义化HTML文件名(仅basename,可省略.html),如「电商经营全景分析-2023-09至2026-08」;缺省时使用title,不要使用随机ID"
523
+ },
511
524
  summary: {
512
525
  type: "string",
513
526
  description: "可选一句话结论/摘要,显示在报告头部"
@@ -554,6 +567,10 @@ const ANALYSIS_REPORT_OUTPUT_SCHEMA = {
554
567
  required: true
555
568
  },
556
569
  summary: { type: "string" },
570
+ htmlPath: {
571
+ type: "string",
572
+ required: true
573
+ },
557
574
  datasets: {
558
575
  type: "array",
559
576
  required: true,
@@ -810,6 +827,127 @@ async function runStructuredReadQuery(ctx, connection, sql, resolved, toolName,
810
827
  };
811
828
  }
812
829
  //#endregion
830
+ //#region src/analysis-html.ts
831
+ /**
832
+ * Offline HTML artifact for one validated AnalysisReportV1.
833
+ *
834
+ * The generated page has no network/runtime dependencies. Untrusted report
835
+ * strings stay inside escaped JSON and are projected with textContent only;
836
+ * chart geometry is derived from already-validated finite numeric fields.
837
+ * @module @yejiming/dsh-data-agent/analysis-html
838
+ */
839
+ const ANALYSIS_REPORT_DIRECTORY = "analysis-reports";
840
+ /** Convert a report title/output name into a bounded, readable filename segment. */
841
+ function analysisFileSegment(value, fallback) {
842
+ const sanitize = (candidate) => candidate.normalize("NFKC").replace(/\.html$/i, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, "").replace(/[^\p{Letter}\p{Number}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 96);
843
+ return sanitize(value) || sanitize(fallback) || "analysis-report";
844
+ }
845
+ /** Relative path shared by the writer and DSH's mutation presentation. */
846
+ function analysisArtifactRelativePath(title, outputName) {
847
+ const basename = analysisFileSegment(outputName ?? title, "分析报告");
848
+ return `${ANALYSIS_REPORT_DIRECTORY}/${basename}.html`;
849
+ }
850
+ /** Escape JSON so data cannot close its application/json script element. */
851
+ function escapeJsonForHtmlScript(value) {
852
+ return JSON.stringify(value).replace(/&/g, "\\u0026").replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
853
+ }
854
+ /** Render one complete, offline Dashboard document. */
855
+ function renderAnalysisHtml(report, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
856
+ return `<!doctype html>
857
+ <html lang="zh-CN">
858
+ <head>
859
+ <meta charset="utf-8">
860
+ <meta name="viewport" content="width=device-width, initial-scale=1">
861
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none'; font-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'">
862
+ <title>DSH Data Agent Analysis</title>
863
+ <style>
864
+ :root{color-scheme:light dark;--bg:oklch(97.4% .006 255);--panel:oklch(99.2% .003 255);--text:oklch(27% .035 255);--muted:oklch(52% .025 255);--line:oklch(88% .012 255);--grid:oklch(92% .008 255);--accent:oklch(58% .16 255);--palette:#4e79a7,#f28e2b,#59a14f,#e15759,#76b7b2,#edc948,#b07aa1,#9c755f}
865
+ @media(prefers-color-scheme:dark){:root{--bg:oklch(19% .018 255);--panel:oklch(23% .02 255);--text:oklch(93% .012 255);--muted:oklch(72% .018 255);--line:oklch(35% .022 255);--grid:oklch(31% .018 255);--accent:oklch(72% .13 255)}}
866
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}main{width:min(1440px,calc(100% - 32px));margin:0 auto;padding:24px 0 48px}header{padding-bottom:16px;margin-bottom:16px;border-bottom:1px solid var(--line)}h1{margin:0;font-size:24px;line-height:1.25;font-weight:650;letter-spacing:-.015em}header p{max-width:1120px;margin:7px 0 0;color:var(--muted)}.report-count{font-size:13px;color:var(--text)}.metric-band{display:flex;flex-wrap:wrap;gap:8px;padding-bottom:12px;margin-bottom:12px;border-bottom:1px solid var(--line)}.metric{flex:1 1 180px;min-width:150px;padding:9px 12px;background:var(--panel);border:1px solid var(--line);border-radius:8px;break-inside:avoid}.metric-label{margin:0;color:var(--muted);font-size:12px}.metric-value{margin:2px 0 0;font-size:18px;line-height:1.35;font-weight:650;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.dashboard{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.view{min-width:0;padding:11px 12px;background:var(--panel);border:1px solid var(--line);border-radius:8px;break-inside:avoid}.view.full,.view.table{grid-column:1/-1}.view h2{font-size:13px;line-height:1.4;font-weight:650;margin:0 0 8px}.empty{padding:28px 12px;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:7px}.chart{width:100%;height:auto;min-height:260px;display:block}.axis{stroke:var(--line);stroke-width:1}.grid{stroke:var(--grid);stroke-width:1}.axis-label,.legend{fill:var(--muted);font-size:11px}.legend-row{display:flex;gap:12px;flex-wrap:wrap;color:var(--muted);font-size:12px;margin-top:6px}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}details{margin-top:10px;border-top:1px solid var(--line);padding-top:8px}summary{cursor:pointer;color:var(--accent);font-size:12px}.table-wrap{overflow:auto;max-height:460px;border:1px solid var(--line);border-radius:6px}table{width:100%;border-collapse:collapse;white-space:nowrap;font-size:12px}th,td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:top}th{position:sticky;top:0;background:var(--bg);font-weight:600}td{max-width:420px;white-space:pre-wrap;overflow-wrap:anywhere}.null{color:var(--muted);font-style:italic}footer{margin-top:18px;color:var(--muted);font-size:12px}
867
+ @media(max-width:880px){main{width:min(100% - 20px,1440px);padding-top:18px}h1{font-size:21px}.dashboard{grid-template-columns:1fr}.view{grid-column:1!important}.metric{flex-basis:100%}}
868
+ @media print{:root{color-scheme:light;--bg:oklch(98.5% .003 255);--panel:oklch(99.5% .002 255);--text:oklch(24% .025 255);--muted:oklch(48% .02 255);--line:oklch(84% .01 255);--grid:oklch(90% .008 255)}.dashboard{display:block}.view{margin:0 0 12px}.table-wrap{max-height:none;overflow:visible}details{display:block}details>summary{display:none}details>*{display:block!important}main{width:100%;padding:0}}
869
+ </style>
870
+ </head>
871
+ <body>
872
+ <main>
873
+ <header id="report-header"></header>
874
+ <section id="metric-band" class="metric-band" aria-label="关键指标"></section>
875
+ <section id="dashboard" class="dashboard" aria-label="分析视图"></section>
876
+ <footer id="report-footer"></footer>
877
+ </main>
878
+ <script type="application/json" id="report-data">${escapeJsonForHtmlScript(report)}<\/script>
879
+ <script>
880
+ (()=>{'use strict';
881
+ const report=JSON.parse(document.getElementById('report-data').textContent||'{}');
882
+ const palette=['#4e79a7','#f28e2b','#59a14f','#e15759','#76b7b2','#edc948','#b07aa1','#9c755f'];
883
+ const ns='http://www.w3.org/2000/svg';
884
+ const el=(tag,className,text)=>{const node=document.createElement(tag);if(className)node.className=className;if(text!==undefined)node.textContent=String(text);return node};
885
+ const svgEl=(tag,attrs={})=>{const node=document.createElementNS(ns,tag);for(const [key,value] of Object.entries(attrs))node.setAttribute(key,String(value));return node};
886
+ const datasetFor=id=>report.datasets.find(item=>item.id===id);
887
+ const indexOf=(dataset,field)=>dataset.columns.indexOf(field);
888
+ const number=value=>value===null||value===undefined||value===''?null:(Number.isFinite(Number(value))?Number(value):null);
889
+ const extent=(values,includeZero=false)=>{const finite=values.filter(Number.isFinite);let min=finite.length?Math.min(...finite):0,max=finite.length?Math.max(...finite):1;if(includeZero){min=Math.min(0,min);max=Math.max(0,max)}if(min===max){min-=1;max+=1}return[min,max]};
890
+ const scale=(value,min,max,start,end)=>start+(value-min)/(max-min)*(end-start);
891
+ const titleFor=view=>view.label||({metric:'指标',line:'趋势',bar:'对比',pie:'构成',scatter:'分布',table:'明细'}[view.kind]||view.id);
892
+ const empty=()=>el('div','empty','暂无数据');
893
+ function tableFor(dataset,columns){const selected=(columns&&columns.length?columns:dataset.columns).map(name=>[name,indexOf(dataset,name)]);const wrap=el('div','table-wrap');const table=el('table');const head=el('thead');const hr=el('tr');selected.forEach(([name])=>hr.append(el('th','',name)));head.append(hr);table.append(head);const body=el('tbody');dataset.rows.forEach(row=>{const tr=el('tr');selected.forEach(([,index])=>{const value=row[index];const td=el('td',value===null?'null':'',value===null?'NULL':value);tr.append(td)});body.append(tr)});table.append(body);wrap.append(table);return wrap}
894
+ function detailsFor(dataset,columns){const details=el('details');details.append(el('summary','','查看原始数据('+dataset.rows.length+'行)'));details.append(tableFor(dataset,columns));return details}
895
+ function baseSvg(){const svg=svgEl('svg',{viewBox:'0 0 760 300',role:'img',class:'chart','aria-label':'数据图表'});for(let i=0;i<5;i++){const y=30+i*55;svg.append(svgEl('line',{x1:58,y1:y,x2:738,y2:y,class:'grid'}))}svg.append(svgEl('line',{x1:58,y1:250,x2:738,y2:250,class:'axis'}));svg.append(svgEl('line',{x1:58,y1:20,x2:58,y2:250,class:'axis'}));return svg}
896
+ function axisText(svg,text,x,y,anchor='start'){const node=svgEl('text',{x,y,'text-anchor':anchor,class:'axis-label'});node.textContent=String(text);svg.append(node)}
897
+ function legend(card,names){if(names.length<2)return;const row=el('div','legend-row');names.forEach((name,index)=>{const item=el('span');const dot=el('i','dot');dot.style.background=palette[index%palette.length];item.append(dot,document.createTextNode(String(name)));row.append(item)});card.append(row)}
898
+ function lineChart(card,view,dataset){const xIndex=indexOf(dataset,view.x.field);const grouped=new Map();if(view.seriesField){const groupIndex=indexOf(dataset,view.seriesField),yIndex=indexOf(dataset,view.y[0]);dataset.rows.forEach((row,index)=>{const name=row[groupIndex]??'';if(!grouped.has(name))grouped.set(name,[]);grouped.get(name).push({index,x:row[xIndex],y:number(row[yIndex])})})}else view.y.forEach(field=>{const yIndex=indexOf(dataset,field);grouped.set(field,dataset.rows.map((row,index)=>({index,x:row[xIndex],y:number(row[yIndex])}))) });const all=[...grouped.values()].flat().map(point=>point.y).filter(value=>value!==null);if(!all.length){card.append(empty());return}const [min,max]=extent(all);const svg=baseSvg();axisText(svg,max.toLocaleString(),52,27,'end');axisText(svg,min.toLocaleString(),52,250,'end');axisText(svg,view.x.label||view.x.field,398,286,'middle');const count=Math.max(2,dataset.rows.length);[...grouped.entries()].forEach(([name,points],seriesIndex)=>{let segment=[];const flush=()=>{if(segment.length){svg.append(svgEl('polyline',{points:segment.join(' '),fill:'none',stroke:palette[seriesIndex%palette.length],'stroke-width':3,'stroke-linejoin':'round','stroke-linecap':'round'}));segment=[]}};points.forEach(point=>{if(point.y===null){flush();return}const x=scale(point.index,0,count-1,62,734),y=scale(point.y,min,max,246,24);segment.push(x+','+y);svg.append(svgEl('circle',{cx:x,cy:y,r:3,fill:palette[seriesIndex%palette.length]}))});flush()});card.append(svg);legend(card,[...grouped.keys()])}
899
+ function barChart(card,view,dataset){const xIndex=indexOf(dataset,view.x.field);const series=view.seriesField?[view.seriesField]:view.y;const values=[];const entries=[];if(view.seriesField){const groupIndex=indexOf(dataset,view.seriesField),yIndex=indexOf(dataset,view.y[0]);dataset.rows.forEach((row,index)=>{const value=number(row[yIndex]);if(value!==null){values.push(value);entries.push({index,value,name:row[groupIndex]??'',x:row[xIndex]??''})}})}else dataset.rows.forEach((row,index)=>view.y.forEach((field,seriesIndex)=>{const value=number(row[indexOf(dataset,field)]);if(value!==null){values.push(value);entries.push({index,value,name:field,seriesIndex,x:row[xIndex]??''})}}));if(!values.length){card.append(empty());return}const [min,max]=extent(values,true),svg=baseSvg(),zero=scale(0,min,max,246,24),groups=Math.max(1,dataset.rows.length),barWidth=Math.max(2,Math.min(36,620/(groups*Math.max(1,series.length))));svg.append(svgEl('line',{x1:58,y1:zero,x2:738,y2:zero,stroke:'var(--muted)','stroke-width':1.5}));entries.forEach((entry,entryIndex)=>{const seriesIndex=entry.seriesIndex??Math.max(0,series.indexOf(entry.name));const center=scale(entry.index+.5,0,groups,62,734);const offset=(seriesIndex-(series.length-1)/2)*barWidth;const y=scale(entry.value,min,max,246,24);svg.append(svgEl('rect',{x:center+offset-barWidth*.42,y:Math.min(y,zero),width:barWidth*.84,height:Math.max(1,Math.abs(zero-y)),rx:2,fill:palette[(seriesIndex<0?entryIndex:seriesIndex)%palette.length]}))});axisText(svg,max.toLocaleString(),52,27,'end');axisText(svg,min.toLocaleString(),52,250,'end');axisText(svg,view.x.label||view.x.field,398,286,'middle');card.append(svg);legend(card,series)}
900
+ function pieChart(card,view,dataset){const cIndex=indexOf(dataset,view.categoryField),vIndex=indexOf(dataset,view.valueField);const entries=dataset.rows.map(row=>({name:row[cIndex]??'',value:number(row[vIndex])??0})),total=entries.reduce((sum,item)=>sum+item.value,0);if(total<=0){card.append(empty());return}const svg=svgEl('svg',{viewBox:'0 0 760 300',role:'img',class:'chart','aria-label':'构成图'}),cx=235,cy=150,r=105;let angle=-Math.PI/2;entries.forEach((entry,index)=>{const next=angle+entry.value/total*Math.PI*2,x1=cx+Math.cos(angle)*r,y1=cy+Math.sin(angle)*r,x2=cx+Math.cos(next)*r,y2=cy+Math.sin(next)*r,large=next-angle>Math.PI?1:0;const path=svgEl('path',{d:'M '+cx+' '+cy+' L '+x1+' '+y1+' A '+r+' '+r+' 0 '+large+' 1 '+x2+' '+y2+' Z',fill:palette[index%palette.length]});svg.append(path);angle=next});entries.forEach((entry,index)=>{const y=46+index*25;svg.append(svgEl('circle',{cx:490,cy:y-4,r:5,fill:palette[index%palette.length]}));axisText(svg,entry.name+' '+(entry.value/total*100).toFixed(1)+'%',505,y)});card.append(svg)}
901
+ function scatterChart(card,view,dataset){const xi=indexOf(dataset,view.xField),yi=indexOf(dataset,view.yField),points=dataset.rows.map(row=>[number(row[xi]),number(row[yi])]).filter(point=>point[0]!==null&&point[1]!==null);if(!points.length){card.append(empty());return}const [xmin,xmax]=extent(points.map(point=>point[0])),[ymin,ymax]=extent(points.map(point=>point[1])),svg=baseSvg();points.forEach(point=>svg.append(svgEl('circle',{cx:scale(point[0],xmin,xmax,62,734),cy:scale(point[1],ymin,ymax,246,24),r:4,fill:palette[0],opacity:.82})));axisText(svg,ymax.toLocaleString(),52,27,'end');axisText(svg,ymin.toLocaleString(),52,250,'end');axisText(svg,xmin.toLocaleString(),62,270);axisText(svg,xmax.toLocaleString(),734,270,'end');axisText(svg,view.xField,398,288,'middle');card.append(svg)}
902
+ const header=document.getElementById('report-header');header.append(el('h1','',report.title));if(report.summary)header.append(el('p','',report.summary));header.append(el('p','report-count',report.datasets.length+'个数据集 · '+report.views.length+'个视图'));
903
+ const widths=new Map();let firstChartPlaced=false;report.views.forEach(view=>{if(view.kind==='metric')return;if(view.width){widths.set(view.id,view.width);if(['line','bar','pie','scatter'].includes(view.kind))firstChartPlaced=true;return}const width=view.kind==='table'||!firstChartPlaced?'full':'half';widths.set(view.id,width);if(['line','bar','pie','scatter'].includes(view.kind))firstChartPlaced=true});
904
+ const metricBand=document.getElementById('metric-band');const metrics=report.views.filter(view=>view.kind==='metric');if(metrics.length===0)metricBand.remove();else metrics.forEach(view=>{const dataset=datasetFor(view.datasetId),metric=el('article','metric');metric.append(el('p','metric-label',titleFor(view)));if(!dataset||dataset.rows.length===0)metric.append(el('p','metric-value','—'));else{const value=number(dataset.rows[0][indexOf(dataset,view.field)]);metric.append(el('p','metric-value',value===null?'—':(view.format==='percent'?(value*100).toLocaleString()+'%':value.toLocaleString())))}metricBand.append(metric)});
905
+ const dashboard=document.getElementById('dashboard');report.views.filter(view=>view.kind!=='metric').forEach(view=>{const dataset=datasetFor(view.datasetId),card=el('article','view '+(widths.get(view.id)==='full'?'full ':'')+view.kind);card.append(el('h2','',titleFor(view)));if(!dataset||dataset.rows.length===0)card.append(empty());else if(view.kind==='table')card.append(tableFor(dataset,view.columns));else{if(view.kind==='line')lineChart(card,view,dataset);if(view.kind==='bar')barChart(card,view,dataset);if(view.kind==='pie')pieChart(card,view,dataset);if(view.kind==='scatter')scatterChart(card,view,dataset);card.append(detailsFor(dataset))}dashboard.append(card)});
906
+ document.getElementById('report-footer').textContent='由 DSH Data Agent 生成 · '+${escapeJsonForHtmlScript(generatedAt)}+' · 离线HTML';
907
+ })();
908
+ <\/script>
909
+ </body>
910
+ </html>`;
911
+ }
912
+ /** Atomically persist one report and return the report enriched with htmlPath. */
913
+ async function writeAnalysisHtml(report, options) {
914
+ const directory = resolve(options.cwd, ANALYSIS_REPORT_DIRECTORY);
915
+ const relativePath = analysisArtifactRelativePath(report.title, options.outputName);
916
+ const htmlPath = resolve(options.cwd, relativePath);
917
+ const complete = {
918
+ ...report,
919
+ htmlPath
920
+ };
921
+ const basename = analysisFileSegment(options.outputName ?? report.title, "分析报告");
922
+ const temporaryPath = resolve(directory, `.${basename}.${randomUUID()}.tmp`);
923
+ try {
924
+ await mkdir(directory, { recursive: true });
925
+ await writeFile(temporaryPath, renderAnalysisHtml(complete, options.generatedAt), {
926
+ encoding: "utf8",
927
+ flag: "wx"
928
+ });
929
+ await link(temporaryPath, htmlPath);
930
+ await unlink(temporaryPath).catch(() => void 0);
931
+ } catch (error) {
932
+ await unlink(temporaryPath).catch(() => void 0);
933
+ const message = error instanceof Error ? error.message : String(error);
934
+ const detail = error?.code === "EEXIST" ? "目标文件已存在,请使用更具体的outputName" : message;
935
+ throw new Error(`render-analysis: 保存Dashboard HTML失败(${htmlPath}):${detail}`, { cause: error });
936
+ }
937
+ return complete;
938
+ }
939
+ //#endregion
940
+ //#region src/presentation-text.ts
941
+ /** Surface-neutral control-sequence sanitization for generic tool cards. */
942
+ const CONTROL_ESCAPE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
943
+ const OSC_SEQUENCE = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/gu;
944
+ const CSI_SEQUENCE = /\u001b\[[0-?]*[ -/]*[@-~]/gu;
945
+ const ESC_SEQUENCE = /\u001b(?:[@-_]|[ -/]+[@-~]?)/gu;
946
+ /** Remove control effects while keeping their presence visible to the user. */
947
+ function sanitizePresentationText(value) {
948
+ return String(value ?? "").replace(OSC_SEQUENCE, "⟦OSC⟧").replace(CSI_SEQUENCE, "⟦ESC⟧").replace(ESC_SEQUENCE, "⟦ESC⟧").replace(/\r\n?/gu, "\\n").replace(/\n/gu, "\\n").replace(/\t/gu, "\\t").replace(CONTROL_ESCAPE, (character) => `\\x${character.codePointAt(0).toString(16).padStart(2, "0")}`);
949
+ }
950
+ //#endregion
813
951
  //#region src/tool.ts
814
952
  /** Cordis plugin name (diagnostics only). */
815
953
  const name = "data-agent-tool";
@@ -852,7 +990,7 @@ function validateSingleSql(sql, toolName) {
852
990
  assertSingleStatement(sql, toolName);
853
991
  }
854
992
  /**
855
- * The Web-only render-analysis tool (D1-D5): one call builds one versioned
993
+ * The surface-neutral render-analysis tool (D1-D5): one call builds one versioned
856
994
  * analysis report from 1-6 read-only datasets and 1-8 views. The full report
857
995
  * is persisted as presentationMeta; the model only receives a short summary
858
996
  * (output.render), never the rows themselves.
@@ -860,7 +998,7 @@ function validateSingleSql(sql, toolName) {
860
998
  function defineRenderAnalysisTool(ctx, resolved) {
861
999
  return defineTool({
862
1000
  name: "render-analysis",
863
- description: "Web only: render one versioned analysis report (v1) from 1-6 read-only datasets and 1-8 metric, line, bar, pie, scatter, or table views. First use sql-query to inspect and verify data, then call this tool only when visualization adds value. Use one primary chart for a simple relationship or 3-6 complementary views for multi-metric, time-series, or segmented analysis. Put aggregation, Top N, and sorting in SQL, and add ORDER BY for line or time datasets. Reuse a dataset across views via datasetId; each dataset runs once. Arbitrary chart options, scripts, HTML, CSS, and URLs are not accepted. Empty datasets are valid and render as no-data states.",
1001
+ description: "Render one versioned analysis report (v1) from 1-6 read-only datasets using 1-8 metric, line, bar, pie, scatter, or table views, then save an offline Dashboard HTML file under analysis-reports/ in the current session workspace. First use sql-query to inspect and verify data, then call this tool only when visualization adds value. Use one primary chart for a simple relationship or 3-6 complementary views for multi-metric, time-series, or segmented analysis. Put aggregation, Top N, and sorting in SQL, and add ORDER BY for line or time datasets. Reuse a dataset across views via datasetId; each dataset runs once. Arbitrary chart options, scripts, HTML, CSS, and URLs are not accepted. Empty datasets are valid and render as no-data states.",
864
1002
  parameters: RENDER_ANALYSIS_PARAMETERS,
865
1003
  output: {
866
1004
  schema: ANALYSIS_REPORT_OUTPUT_SCHEMA,
@@ -872,14 +1010,18 @@ function defineRenderAnalysisTool(ctx, resolved) {
872
1010
  },
873
1011
  presentCall: (args) => ({
874
1012
  card: "generic",
875
- kind: "read",
876
- title: "render-analysis《" + args.title + "》",
877
- rawInput: args.title
1013
+ kind: "edit",
1014
+ title: "render-analysis《" + sanitizePresentationText(args.title) + "》",
1015
+ rawInput: sanitizePresentationText(args.title),
1016
+ locations: [{ path: analysisArtifactRelativePath(args.title, args.outputName) }]
878
1017
  }),
879
1018
  presentResult: (args, result) => ({
880
1019
  card: "generic",
881
- title: "render-analysis《" + args.title + "》",
882
- content: result.content
1020
+ title: "render-analysis《" + sanitizePresentationText(args.title) + "》",
1021
+ content: result.content.map((item) => item.type === "text" ? {
1022
+ ...item,
1023
+ text: sanitizePresentationText(item.text)
1024
+ } : item)
883
1025
  }),
884
1026
  async execute(args, exec) {
885
1027
  const request = parseAnalysisRequest(args);
@@ -928,7 +1070,11 @@ function defineRenderAnalysisTool(ctx, resolved) {
928
1070
  };
929
1071
  const bytes = reportJsonBytes(report);
930
1072
  if (bytes > 524288) throw new Error("render-analysis: 报告 JSON 超过 524288 字节上限(当前 " + bytes + " 字节);请聚合、筛选或拆分报告,不得静默删减数据");
931
- return report;
1073
+ const sessionCwd = exec.agent?.session?.header?.cwd;
1074
+ return await writeAnalysisHtml(report, {
1075
+ cwd: typeof sessionCwd === "string" && sessionCwd.length > 0 ? sessionCwd : process.cwd(),
1076
+ outputName: request.outputName
1077
+ });
932
1078
  }
933
1079
  });
934
1080
  }
@@ -1122,7 +1268,7 @@ function apply(ctx, config) {
1122
1268
  return runRedactedClientQuery(ctx, connection, classifyStatement(args.sql, connection.type) === "read" ? enforceReadRowLimit(args.sql, connection.type, resolved.maxRows) : args.sql, runnerOptions(resolved), exec.signal);
1123
1269
  }
1124
1270
  }));
1125
- if (ctx.get("webServer") !== void 0) ctx.tools.register(defineRenderAnalysisTool(ctx, resolved));
1271
+ ctx.tools.register(defineRenderAnalysisTool(ctx, resolved));
1126
1272
  }
1127
1273
  //#endregion
1128
1274
  export { name as i, apply as n, inject as r, Config as t };
package/lib/tool.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as name, n as apply, r as inject, t as Config } from "./tool-DVh61An-.js";
1
+ import { i as name, n as apply, r as inject, t as Config } from "./tool-DgL0fBfj.js";
2
2
  export { Config, apply, inject, name };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Offline HTML artifact for one validated AnalysisReportV1.
3
+ *
4
+ * The generated page has no network/runtime dependencies. Untrusted report
5
+ * strings stay inside escaped JSON and are projected with textContent only;
6
+ * chart geometry is derived from already-validated finite numeric fields.
7
+ * @module @yejiming/dsh-data-agent/analysis-html
8
+ */
9
+ import type { AnalysisReportV1 } from './analysis.ts';
10
+ export declare const ANALYSIS_REPORT_DIRECTORY = "analysis-reports";
11
+ /** Convert a report title/output name into a bounded, readable filename segment. */
12
+ export declare function analysisFileSegment(value: string, fallback: string): string;
13
+ /** Relative path shared by the writer and DSH's mutation presentation. */
14
+ export declare function analysisArtifactRelativePath(title: string, outputName?: string): string;
15
+ /** Escape JSON so data cannot close its application/json script element. */
16
+ export declare function escapeJsonForHtmlScript(value: unknown): string;
17
+ /** Render one complete, offline Dashboard document. */
18
+ export declare function renderAnalysisHtml(report: AnalysisReportV1, generatedAt?: string): string;
19
+ export interface WriteAnalysisHtmlOptions {
20
+ cwd: string;
21
+ outputName?: string;
22
+ generatedAt?: string;
23
+ }
24
+ /** Atomically persist one report and return the report enriched with htmlPath. */
25
+ export declare function writeAnalysisHtml(report: AnalysisReportV1, options: WriteAnalysisHtmlOptions): Promise<AnalysisReportV1 & {
26
+ htmlPath: string;
27
+ }>;
@@ -87,6 +87,8 @@ export interface AnalysisDatasetRequestV1 {
87
87
  /** The wire request accepted by the render-analysis tool. */
88
88
  export interface AnalysisRequestV1 {
89
89
  title: string;
90
+ /** Semantic output basename; directory is always analysis-reports/. */
91
+ outputName?: string;
90
92
  summary?: string;
91
93
  datasets: AnalysisDatasetRequestV1[];
92
94
  views: AnalysisViewV1[];
@@ -102,6 +104,8 @@ export interface AnalysisReportV1 {
102
104
  version: typeof ANALYSIS_REPORT_VERSION;
103
105
  title: string;
104
106
  summary?: string;
107
+ /** Absolute path of the generated HTML artifact (absent on legacy v1 meta). */
108
+ htmlPath?: string;
105
109
  datasets: AnalysisDatasetResultV1[];
106
110
  views: AnalysisViewV1[];
107
111
  }
@@ -136,7 +140,7 @@ export declare function rowsToArrays(columns: string[], rows: readonly Record<st
136
140
  /** JSON-encoded UTF-8 size of the normalized report (the 512 KiB bound). */
137
141
  export declare function reportJsonBytes(report: AnalysisReportV1): number;
138
142
  /** One-line model-facing summary; never re-injects rows into model context (D5). */
139
- export declare function formatAnalysisSummary(report: Pick<AnalysisReportV1, 'title' | 'datasets' | 'views'>): string;
143
+ export declare function formatAnalysisSummary(report: Pick<AnalysisReportV1, 'title' | 'datasets' | 'views' | 'htmlPath'>): string;
140
144
  /** The view union: exactly the six supported kinds, nothing else. */
141
145
  export declare const ANALYSIS_VIEWS_SCHEMA: {
142
146
  readonly oneOf: readonly [{
@@ -423,6 +427,10 @@ export declare const RENDER_ANALYSIS_PARAMETERS: {
423
427
  readonly required: true;
424
428
  readonly description: "报告标题,如「月度经营分析」";
425
429
  };
430
+ readonly outputName: {
431
+ readonly type: "string";
432
+ readonly description: "可选语义化HTML文件名(仅basename,可省略.html),如「电商经营全景分析-2023-09至2026-08」;缺省时使用title,不要使用随机ID";
433
+ };
426
434
  readonly summary: {
427
435
  readonly type: "string";
428
436
  readonly description: "可选一句话结论/摘要,显示在报告头部";
@@ -748,6 +756,10 @@ export declare const ANALYSIS_REPORT_OUTPUT_SCHEMA: {
748
756
  readonly summary: {
749
757
  readonly type: "string";
750
758
  };
759
+ readonly htmlPath: {
760
+ readonly type: "string";
761
+ readonly required: true;
762
+ };
751
763
  readonly datasets: {
752
764
  readonly type: "array";
753
765
  readonly required: true;
@@ -166,12 +166,11 @@ export declare function resolveDshHome(env?: Record<string, string | undefined>)
166
166
  /**
167
167
  * Install the packaged `preset/data-agent/` directory into
168
168
  * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target is
169
- * normally left untouched. The exact package-owned 0.0.9 composition is
170
- * migrated once because its two dynamic package rows are incompatible with
171
- * DSH Desktop's unpacked-ASAR loader; user-edited compositions are never
172
- * overwritten. `installPreset: false` never calls this. Best-effort a
173
- * failure logs a warning with manual install instructions instead of failing
174
- * the boot.
169
+ * normally left untouched. Exact package-owned legacy compositions are
170
+ * migrated once when their runtime contract changes; user-edited compositions
171
+ * are never overwritten. `installPreset: false` never calls this. Best-effort
172
+ * a failure logs a warning with manual install instructions instead of
173
+ * failing the boot.
175
174
  */
176
175
  export declare function installPreset(ctx: Context, presetId: string): Promise<boolean>;
177
176
  /** Public for regression tests of the non-destructive preset migration gate. */
@@ -0,0 +1,3 @@
1
+ /** Surface-neutral control-sequence sanitization for generic tool cards. */
2
+ /** Remove control effects while keeping their presence visible to the user. */
3
+ export declare function sanitizePresentationText(value: unknown): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yejiming/dsh-data-agent",
3
3
  "description": "Data Agent for DSH Web and TUI: shared database connections, a masked TUI form, secure credential references, SQL tools, and the data-agent preset",
4
- "version": "0.0.11",
4
+ "version": "0.0.12",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -78,25 +78,25 @@
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@deepseek-ai/cordis": "^4.0.1",
81
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
82
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
83
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
84
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
85
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
86
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
87
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
88
- "@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.6",
89
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
90
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
91
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
92
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
93
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
94
- "@deepseek-ai/dsh-storage": "^0.1.0-rc.6",
95
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
96
- "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.6",
97
- "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
98
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
99
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
81
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
82
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.7",
83
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
84
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
85
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
86
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
87
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
88
+ "@deepseek-ai/dsh-client-ui-tool": "^0.1.0-rc.7",
89
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
90
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
91
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
92
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
93
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.7",
94
+ "@deepseek-ai/dsh-storage": "^0.1.0-rc.7",
95
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.7",
96
+ "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.7",
97
+ "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.7",
98
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
99
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.7",
100
100
  "react": "^18.2.0 || ^19.0.0"
101
101
  },
102
102
  "peerDependenciesMeta": {
@@ -130,26 +130,26 @@
130
130
  },
131
131
  "devDependencies": {
132
132
  "@deepseek-ai/cordis": "^4.0.1",
133
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
134
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
135
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
136
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
137
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
138
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
139
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
140
- "@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.6",
141
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
142
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
143
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
144
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
145
- "@deepseek-ai/dsh-scope": "^0.1.0-rc.6",
146
- "@deepseek-ai/dsh-storage": "^0.1.0-rc.6",
147
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
148
- "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.6",
149
- "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.6",
150
- "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.6",
151
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
152
- "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
133
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
134
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.7",
135
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
136
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
137
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
138
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
139
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
140
+ "@deepseek-ai/dsh-client-ui-tool": "0.1.0-rc.7",
141
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
142
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.7",
143
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.7",
144
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.7",
145
+ "@deepseek-ai/dsh-scope": "^0.1.0-rc.7",
146
+ "@deepseek-ai/dsh-storage": "^0.1.0-rc.7",
147
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.7",
148
+ "@deepseek-ai/dsh-storage-json": "^0.1.0-rc.7",
149
+ "@deepseek-ai/dsh-subprocess": "^0.1.0-rc.7",
150
+ "@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.7",
151
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
152
+ "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.7",
153
153
  "@testing-library/dom": "^10.4.1",
154
154
  "@testing-library/react": "^16.3.2",
155
155
  "@types/node": "^25.0.0",
@@ -27,9 +27,11 @@
27
27
  你是数据工程师 Agent。工作目录 {{cwd}}。可用工具:sql-query(只读 SQL,返回结构化
28
28
  JSON 结果)、sql-write(写/管理 SQL,单条自动提交)、sql-cmd(原始客户端输出,兼容
29
29
  SHOW TABLES、DESCRIBE users 等命令)、str_replace_editor(查看、创建和修改本地文件)。
30
- Web 界面可用时另有 render-analysis:把一次调用渲染成一份版本化分析报告(1-6 个只读
31
- 数据集、1-8 个 metric/line/bar/pie/scatter/table 视图,同一数据集可被多个视图通过
32
- datasetId 复用)。是否生成分析由你自主判断:先用 sql-query 探查表结构与样例数据并
30
+ 另有 render-analysis:把一次调用渲染成一份版本化分析
31
+ 报告(1-6 个只读数据集、1-8 个 metric/line/bar/pie/scatter/table 视图,同一数据集可被
32
+ 多个视图通过 datasetId 复用),并在当前工作目录的 analysis-reports/ 中保存离线 HTML
33
+ Dashboard。Web 可同时打开分析面板;TUI 只返回 HTML 路径,不输出字符图。是否生成分析
34
+ 由你自主判断:先用 sql-query 探查表结构与样例数据并
33
35
  核对事实(禁止臆造表名与列名);schema 探查、单标量或原始明细等无视觉增益的查询
34
36
  直接回答、不强制画图;简单关系可生成一个主图,涉及多指标、时间或分群的复杂问题可
35
37
  生成 3-6 个互补视图。聚合、Top N、排序必须写在 SQL 中,line/time 数据需 ORDER BY。