terminal-bridge-setup 3.0.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.
|
@@ -135,6 +135,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
135
135
|
});
|
|
136
136
|
return true; // 异步
|
|
137
137
|
}
|
|
138
|
+
if (msg.type === "CSV_CLEAR") {
|
|
139
|
+
chrome.storage.session.remove(CSV_STORAGE_KEY).catch(() => {});
|
|
140
|
+
sendResponse({ ok: true });
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
138
143
|
if (msg.type === "CSV_DOWNLOAD") {
|
|
139
144
|
getCsvExports().then(list => {
|
|
140
145
|
const record = list.find(e => e.id === msg.id);
|
|
@@ -143,13 +148,38 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
143
148
|
return;
|
|
144
149
|
}
|
|
145
150
|
chrome.downloads.download(
|
|
146
|
-
{ url: record.dataUrl, filename: record.name, saveAs:
|
|
151
|
+
{ url: record.dataUrl, filename: "yearning-csv/" + record.name, saveAs: false },
|
|
147
152
|
(downloadId) => {
|
|
148
153
|
if (chrome.runtime.lastError) {
|
|
149
154
|
sendResponse({ ok: false, msg: chrome.runtime.lastError.message });
|
|
150
155
|
return;
|
|
151
156
|
}
|
|
152
|
-
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
|
+
});
|
|
153
183
|
}
|
|
154
184
|
);
|
|
155
185
|
});
|
|
@@ -672,20 +702,47 @@ function csvCell(value) {
|
|
|
672
702
|
return s;
|
|
673
703
|
}
|
|
674
704
|
|
|
675
|
-
// Yearning 结果 JSON → CSV
|
|
676
|
-
//
|
|
705
|
+
// Yearning 结果 JSON → CSV 文本。多结果集全部写入同一个 CSV,不能只取第一张表:
|
|
706
|
+
// 一次执行多条 SQL 时 results 有多项,popup 行数是总和,文件也必须包含总和。
|
|
707
|
+
// 每组结果独立表头,组间用 Result Set 标记和空行分隔,兼容不同 SQL 的列结构。
|
|
677
708
|
function resultJsonToCsv(jsonText) {
|
|
678
709
|
let obj;
|
|
679
710
|
try { obj = JSON.parse(jsonText); } catch { return null; }
|
|
680
711
|
if (!Array.isArray(obj.results)) return null;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
table.
|
|
687
|
-
|
|
688
|
-
|
|
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;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// 文件名:表名_库_数据源_日期。表名从 SQL 提取(from/into/update 后的词),
|
|
730
|
+
// 库和数据源从目标 tab 的 meta(tapTabMeta)取。
|
|
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
|
+
}
|
|
738
|
+
const tableMatch = sqlText.match(/\b(?:from|into|update|join)\s+[`"]?(\w+)[`"]?/i);
|
|
739
|
+
const table = (tableMatch ? tableMatch[1] : "").slice(0, 40) || "query";
|
|
740
|
+
const meta = tapTabMeta.get(tabId) || {};
|
|
741
|
+
const database = (meta.database || "").replace(/[^\w-]+/g, "") || "nodb";
|
|
742
|
+
const source = (meta.dataSource || "").split(" · ")[0].replace(/[^\w.-]+/g, "") || "nosrc";
|
|
743
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
744
|
+
const time = new Date().toTimeString().slice(0, 5).replace(":", "");
|
|
745
|
+
return { name: `${table}_${database}_${source}_${date}_${time}.csv`, table };
|
|
689
746
|
}
|
|
690
747
|
|
|
691
748
|
async function handleYrExportCsv(frame) {
|
|
@@ -694,13 +751,12 @@ async function handleYrExportCsv(frame) {
|
|
|
694
751
|
console.warn("[bg] yr-export-csv: 结果 JSON 解析失败或无表结构");
|
|
695
752
|
return;
|
|
696
753
|
}
|
|
697
|
-
const
|
|
698
|
-
const sqlHead = (frame.sql || "query").slice(0, 30).replace(/[^\w-]+/g, "_");
|
|
699
|
-
const name = `yearning-${sqlHead}-${stamp}.csv`;
|
|
754
|
+
const { name, table } = await buildCsvFileName(frame.sql, frame.tabId);
|
|
700
755
|
const dataUrl = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
|
|
701
756
|
await addCsvExport({
|
|
702
757
|
id: Date.now(),
|
|
703
758
|
name,
|
|
759
|
+
table,
|
|
704
760
|
rows: frame.rows || 0,
|
|
705
761
|
sql: frame.sql || "",
|
|
706
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;
|
|
@@ -200,10 +204,14 @@
|
|
|
200
204
|
<div class="hint" id="wsTapStatus">未监听 Yearning 页面</div>
|
|
201
205
|
<div id="yearningTabList" style="margin-top:6px;"></div>
|
|
202
206
|
<div id="csvSection" style="margin-top:8px;display:none;">
|
|
203
|
-
<div
|
|
207
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
|
|
208
|
+
<span class="section-title" style="margin:0;">CSV 导出记录</span>
|
|
209
|
+
<button class="copy-btn" id="btnCsvClear" style="background:#e53935;">清空</button>
|
|
210
|
+
</div>
|
|
204
211
|
<div id="csvList"></div>
|
|
205
|
-
<div class="hint"
|
|
212
|
+
<div class="hint">点击记录下载 · 表名_库_数据源_日期.csv</div>
|
|
206
213
|
</div>
|
|
214
|
+
<button class="scan" id="btnCopyPrompt" style="margin-top:8px;background:#7c4dff;">📋 复制 Agent 使用 Prompt</button>
|
|
207
215
|
</div>
|
|
208
216
|
|
|
209
217
|
<div class="section">
|
package/files/extension/popup.js
CHANGED
|
@@ -144,17 +144,22 @@ const csvList = document.getElementById('csvList');
|
|
|
144
144
|
function refreshCsvList() {
|
|
145
145
|
chrome.runtime.sendMessage({ type: 'CSV_LIST' }, (res) => {
|
|
146
146
|
if (chrome.runtime.lastError || !res || !res.ok) return;
|
|
147
|
-
const
|
|
148
|
-
csvSection.style.display =
|
|
147
|
+
const all = res.exports || [];
|
|
148
|
+
csvSection.style.display = all.length ? 'block' : 'none';
|
|
149
|
+
// 只显示最近 5 条,popup 不超长(存储仍保留 20 条)
|
|
150
|
+
const exports = all.slice(0, 5);
|
|
149
151
|
csvList.innerHTML = exports.map(e => {
|
|
150
152
|
const time = new Date(e.time).toLocaleTimeString();
|
|
151
153
|
return `<div class="csv-item" data-id="${e.id}" title="${escapeHtml(e.sql || e.name)}">
|
|
152
154
|
<span class="csv-name">📄 ${escapeHtml(e.name)}</span>
|
|
153
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>
|
|
154
157
|
</div>`;
|
|
155
158
|
}).join('');
|
|
156
159
|
csvList.querySelectorAll('.csv-item').forEach(el => {
|
|
157
|
-
el.onclick = () => {
|
|
160
|
+
el.onclick = (ev) => {
|
|
161
|
+
// 点复制按钮不触发行点击(下载)
|
|
162
|
+
if (ev.target.closest('.csv-prompt-btn')) return;
|
|
158
163
|
chrome.runtime.sendMessage({ type: 'CSV_DOWNLOAD', id: Number(el.dataset.id) }, (r) => {
|
|
159
164
|
if (chrome.runtime.lastError || !r || !r.ok) {
|
|
160
165
|
console.warn('重新下载失败:', r && r.msg);
|
|
@@ -162,10 +167,61 @@ function refreshCsvList() {
|
|
|
162
167
|
});
|
|
163
168
|
};
|
|
164
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
|
+
});
|
|
165
186
|
});
|
|
166
187
|
}
|
|
167
188
|
refreshCsvList();
|
|
168
189
|
|
|
190
|
+
// 清空 CSV 导出记录
|
|
191
|
+
document.getElementById('btnCsvClear').onclick = () => {
|
|
192
|
+
chrome.runtime.sendMessage({ type: 'CSV_CLEAR' }, () => refreshCsvList());
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// 复制 Agent 使用 Prompt(粘给任意 AI 助手即可用桥接查 Yearning)
|
|
196
|
+
const AGENT_PROMPT = `你的机器上已装好 Terminal Bridge(Yearning SQL 桥接)。请按以下方式查询数据库:
|
|
197
|
+
|
|
198
|
+
## 查询(Yearning)
|
|
199
|
+
cd ~/.terminal-bridge/proxy && node yr-example.mjs "SELECT ...;" [超时ms]
|
|
200
|
+
|
|
201
|
+
返回 JSON:{ ok, output(结果 JSON: field 列定义 + data 数据行), error }。
|
|
202
|
+
|
|
203
|
+
## 说明
|
|
204
|
+
- SQL 会自动注入到用户浏览器里选中的 Yearning 页面并点「查询」,结果自动返回
|
|
205
|
+
- 前置:用户已在 Yearning 页面选择数据库,且插件 popup 已「监听当前 Yearning 页」
|
|
206
|
+
- 多个 Yearning 页面时:用户在 popup 列表点选目标页面(显示 数据源 · 数据库)
|
|
207
|
+
- 报 database-not-selected = 页面未选数据库,请让用户先选
|
|
208
|
+
- 报 timeout = 查询超时或页面未监听,让用户确认 popup 状态
|
|
209
|
+
- 只读查询即可;每次查询的结果会自动出现在插件 popup 的「CSV 导出记录」里供用户下载
|
|
210
|
+
|
|
211
|
+
## 终端命令(JumpServer/Arthas,同一代理)
|
|
212
|
+
cd ~/.terminal-bridge/proxy && node client-example.mjs "linux 命令" [超时ms]
|
|
213
|
+
多层引号命令加 --b64 参数下发。`;
|
|
214
|
+
|
|
215
|
+
document.getElementById('btnCopyPrompt').onclick = () => {
|
|
216
|
+
navigator.clipboard.writeText(AGENT_PROMPT).then(() => {
|
|
217
|
+
const btn = document.getElementById('btnCopyPrompt');
|
|
218
|
+
btn.textContent = '✓ 已复制,粘贴给 Agent 即可';
|
|
219
|
+
setTimeout(() => { btn.textContent = '📋 复制 Agent 使用 Prompt'; }, 2000);
|
|
220
|
+
}).catch(() => {
|
|
221
|
+
console.warn('复制失败');
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
169
225
|
// ============== 代理控制 ==============
|
|
170
226
|
const proxyDot = document.getElementById('proxyDot');
|
|
171
227
|
const proxyText = document.getElementById('proxyStatusText');
|
package/package.json
CHANGED