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:
|
|
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
|
-
//
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
table.
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
699
|
-
|
|
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] : "
|
|
739
|
+
const table = (tableMatch ? tableMatch[1] : "").slice(0, 40) || "query";
|
|
702
740
|
const meta = tapTabMeta.get(tabId) || {};
|
|
703
|
-
const database = (meta.database || "").replace(/[^\w
|
|
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 =>
|
|
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|
|
|
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;
|
package/files/extension/popup.js
CHANGED
|
@@ -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