terminal-bridge-setup 3.1.0 → 3.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.
@@ -148,13 +148,38 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
148
148
  return;
149
149
  }
150
150
  chrome.downloads.download(
151
- { url: record.dataUrl, filename: record.name, saveAs: true },
151
+ { url: record.dataUrl, filename: "yearning-csv/" + record.name, saveAs: false },
152
152
  (downloadId) => {
153
153
  if (chrome.runtime.lastError) {
154
154
  sendResponse({ ok: false, msg: chrome.runtime.lastError.message });
155
155
  return;
156
156
  }
157
- sendResponse({ ok: true, downloadId });
157
+ sendResponse({ ok: true, downloadId, path: "~/Downloads/yearning-csv/" + record.name });
158
+ }
159
+ );
160
+ });
161
+ return true; // 异步
162
+ }
163
+ if (msg.type === "CSV_PROMPT") {
164
+ // 🤖 按钮:静默下载该文件到固定目录,返回让 Agent 读文件的 prompt
165
+ getCsvExports().then(list => {
166
+ const record = list.find(e => e.id === msg.id);
167
+ if (!record) {
168
+ sendResponse({ ok: false, msg: "导出记录不存在" });
169
+ return;
170
+ }
171
+ chrome.downloads.download(
172
+ { url: record.dataUrl, filename: "yearning-csv/" + record.name, saveAs: false },
173
+ (downloadId) => {
174
+ if (chrome.runtime.lastError) {
175
+ sendResponse({ ok: false, msg: chrome.runtime.lastError.message });
176
+ return;
177
+ }
178
+ const table = record.table || record.name.split("_")[0];
179
+ sendResponse({
180
+ ok: true,
181
+ prompt: `表名${table}的查询结果请读这个文件 ~/Downloads/yearning-csv/${record.name}`,
182
+ });
158
183
  }
159
184
  );
160
185
  });
@@ -677,34 +702,47 @@ function csvCell(value) {
677
702
  return s;
678
703
  }
679
704
 
680
- // Yearning 结果 JSON → CSV 文本。多结果集时只取第一个非空表
681
- // (Yearning SHOW INDEX 会推两份相同结果,取一即可)
705
+ // Yearning 结果 JSON → CSV 文本。多结果集全部写入同一个 CSV,不能只取第一张表:
706
+ // 一次执行多条 SQL results 有多项,popup 行数是总和,文件也必须包含总和。
707
+ // 每组结果独立表头,组间用 Result Set 标记和空行分隔,兼容不同 SQL 的列结构。
682
708
  function resultJsonToCsv(jsonText) {
683
709
  let obj;
684
710
  try { obj = JSON.parse(jsonText); } catch { return null; }
685
711
  if (!Array.isArray(obj.results)) return null;
686
- const table = obj.results.find(t => t && Array.isArray(t.field) && t.field.length > 0)
687
- || obj.results[0];
688
- if (!table || !Array.isArray(table.field)) return null;
689
- const headers = table.field.map(f => csvCell(f.title || f.dataIndex || ""));
690
- const rows = (table.data || []).map(row =>
691
- table.field.map(f => csvCell(row[f.dataIndex])).join(",")
692
- );
693
- return "\uFEFF" + [headers.join(","), ...rows].join("\r\n") + "\r\n";
712
+
713
+ const blocks = [];
714
+ obj.results.forEach((table, index) => {
715
+ if (!table || !Array.isArray(table.field) || table.field.length === 0) return;
716
+ const headers = table.field.map(f => csvCell(f.title || f.dataIndex || ""));
717
+ const rows = (table.data || []).map(row =>
718
+ table.field.map(f => csvCell(row[f.dataIndex])).join(",")
719
+ );
720
+ // 多结果集时保留组标记;单结果集不额外增加噪音
721
+ if (obj.results.filter(t => t && Array.isArray(t.field) && t.field.length > 0).length > 1) {
722
+ blocks.push(`-- Result Set ${index + 1} --`);
723
+ }
724
+ blocks.push(headers.join(","), ...rows, "");
725
+ });
726
+ return blocks.length > 0 ? "\uFEFF" + blocks.join("\r\n") : null;
694
727
  }
695
728
 
696
729
  // 文件名:表名_库_数据源_日期。表名从 SQL 提取(from/into/update 后的词),
697
730
  // 库和数据源从目标 tab 的 meta(tapTabMeta)取。
698
- function buildCsvFileName(sql, tabId) {
699
- const sqlText = sql || "";
731
+ // 手动查询(sql manual-query)时先读编辑器里的实际 SQL 再提取表名。
732
+ async function buildCsvFileName(sql, tabId) {
733
+ let sqlText = sql || "";
734
+ if (!sqlText || sqlText === "manual-query") {
735
+ const editor = await sendFrameMessage(tabId, { type: "yr-sql-get" }, 0);
736
+ if (editor?.ok && editor.sql) sqlText = editor.sql; // 手动查询后编辑器里就是刚执行的 SQL
737
+ }
700
738
  const tableMatch = sqlText.match(/\b(?:from|into|update|join)\s+[`"]?(\w+)[`"]?/i);
701
- const table = (tableMatch ? tableMatch[1] : "query").slice(0, 40);
739
+ const table = (tableMatch ? tableMatch[1] : "").slice(0, 40) || "query";
702
740
  const meta = tapTabMeta.get(tabId) || {};
703
- const database = (meta.database || "").replace(/[^\w.-]+/g, "") || "nodb";
741
+ const database = (meta.database || "").replace(/[^\w-]+/g, "") || "nodb";
704
742
  const source = (meta.dataSource || "").split(" · ")[0].replace(/[^\w.-]+/g, "") || "nosrc";
705
743
  const date = new Date().toISOString().slice(0, 10);
706
744
  const time = new Date().toTimeString().slice(0, 5).replace(":", "");
707
- return `${table}_${database}_${source}_${date}_${time}.csv`;
745
+ return { name: `${table}_${database}_${source}_${date}_${time}.csv`, table };
708
746
  }
709
747
 
710
748
  async function handleYrExportCsv(frame) {
@@ -713,11 +751,12 @@ async function handleYrExportCsv(frame) {
713
751
  console.warn("[bg] yr-export-csv: 结果 JSON 解析失败或无表结构");
714
752
  return;
715
753
  }
716
- const name = buildCsvFileName(frame.sql, frame.tabId);
754
+ const { name, table } = await buildCsvFileName(frame.sql, frame.tabId);
717
755
  const dataUrl = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
718
756
  await addCsvExport({
719
757
  id: Date.now(),
720
758
  name,
759
+ table,
721
760
  rows: frame.rows || 0,
722
761
  sql: frame.sql || "",
723
762
  time: Date.now(),
@@ -107,7 +107,8 @@
107
107
  if (idc && idc !== dataSource) dataSource = dataSource ? `${dataSource} · ${idc}` : idc;
108
108
  } catch {}
109
109
 
110
- // 3. 兜底:form 启发式
110
+ // 3. 兜底:form 启发式(排除与 dataSource 重叠/相似的值,防止把
111
+ // 数据源名误认成数据库名——文件名第二段曾因此错成 dk_shard)
111
112
  if (!database || !dataSource) {
112
113
  const form = document.querySelector("form") || xpathNode(FORM_XPATH);
113
114
  if (form) {
@@ -117,9 +118,11 @@
117
118
  const value = el.tagName === "SELECT" ? el.options[el.selectedIndex]?.textContent : el.value;
118
119
  if (value?.trim()) values.push(value.trim());
119
120
  });
120
- const unique = [...new Set(values)].filter(v => !/^(查询|执行|取消|确定|SQL)$/i.test(v));
121
+ const unique = [...new Set(values)].filter(v =>
122
+ !/^(查询|执行|取消|确定|SQL)$/i.test(v) &&
123
+ v !== dataSource && !dataSource.includes(v) && !v.includes("shard"));
121
124
  if (!dataSource) dataSource = unique.find(v => /source|实例|数据源|tdsql|mysql|prod|test/i.test(v)) || "";
122
- if (!database) database = unique.find(v => /database|db|库|schema/i.test(v)) || "";
125
+ if (!database) database = unique.find(v => /database|schema|^\w+_dk\b/i.test(v)) || "";
123
126
  }
124
127
  }
125
128
 
@@ -293,6 +296,11 @@
293
296
  sendResponse({ ok: true });
294
297
  return true;
295
298
  }
299
+ if (msg.type === "yr-sql-get") {
300
+ // 读当前编辑器完整 SQL(手动查询结果帧到达时,编辑器里就是刚执行的 SQL)
301
+ sendResponse({ ok: true, sql: readMonacoText() });
302
+ return true;
303
+ }
296
304
  if (msg.type === "yr-verify-sql") {
297
305
  // CDP 注入后验证:读回 monaco view-lines 内容比对
298
306
  const current = readMonacoText();
@@ -131,6 +131,10 @@
131
131
  .csv-item:hover { background: #dcebfd; }
132
132
  .csv-item .csv-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
133
133
  .csv-item .csv-rows { color: #888; font-size: 10px; flex-shrink: 0; }
134
+ .csv-prompt-btn {
135
+ width: auto !important; padding: 2px 7px !important; margin: 0 !important;
136
+ font-size: 11px; background: #7c4dff; flex-shrink: 0;
137
+ }
134
138
  .guide {
135
139
  font-size: 11px;
136
140
  color: #666;
@@ -153,10 +153,13 @@ function refreshCsvList() {
153
153
  return `<div class="csv-item" data-id="${e.id}" title="${escapeHtml(e.sql || e.name)}">
154
154
  <span class="csv-name">📄 ${escapeHtml(e.name)}</span>
155
155
  <span class="csv-rows">${e.rows} 行 · ${time}</span>
156
+ <button class="copy-btn csv-prompt-btn" data-name="${escapeHtml(e.name)}" title="复制让 Agent 读这个文件的 Prompt">🤖</button>
156
157
  </div>`;
157
158
  }).join('');
158
159
  csvList.querySelectorAll('.csv-item').forEach(el => {
159
- el.onclick = () => {
160
+ el.onclick = (ev) => {
161
+ // 点复制按钮不触发行点击(下载)
162
+ if (ev.target.closest('.csv-prompt-btn')) return;
160
163
  chrome.runtime.sendMessage({ type: 'CSV_DOWNLOAD', id: Number(el.dataset.id) }, (r) => {
161
164
  if (chrome.runtime.lastError || !r || !r.ok) {
162
165
  console.warn('重新下载失败:', r && r.msg);
@@ -164,6 +167,22 @@ function refreshCsvList() {
164
167
  });
165
168
  };
166
169
  });
170
+ csvList.querySelectorAll('.csv-prompt-btn').forEach(btn => {
171
+ btn.onclick = (ev) => {
172
+ ev.stopPropagation();
173
+ const id = Number(btn.closest('.csv-item').dataset.id);
174
+ chrome.runtime.sendMessage({ type: 'CSV_PROMPT', id }, (r) => {
175
+ if (chrome.runtime.lastError || !r || !r.ok) {
176
+ console.warn('prompt 失败:', r && r.msg);
177
+ return;
178
+ }
179
+ navigator.clipboard.writeText(r.prompt).then(() => {
180
+ btn.textContent = '✓';
181
+ setTimeout(() => { btn.textContent = '🤖'; }, 1500);
182
+ }).catch(() => {});
183
+ });
184
+ };
185
+ });
167
186
  });
168
187
  }
169
188
  refreshCsvList();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-bridge-setup",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "一次性安装器:释放终端桥接(JumpServer / Arthas)的本地代理 + Chrome 插件,并注册 native messaging host。让 Agent 能通过浏览器 xterm 终端执行命令并拿回输出。",
5
5
  "license": "MIT",
6
6
  "author": "encorearon",