使用外部数据库
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
NOCODB_TOKEN=
|
||||||
|
NOCODB_LIST_URL=https://nocodb.example.com/api/v2/tables/xxx/records
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -79,6 +79,7 @@ web_modules/
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
env.js
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
55
content.js
55
content.js
@@ -9,19 +9,6 @@ const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-";
|
|||||||
let debounceTimer = null;
|
let debounceTimer = null;
|
||||||
let isMutating = false;
|
let isMutating = false;
|
||||||
|
|
||||||
async function saveAlias(id, alias) {
|
|
||||||
const key = id.replace(PERSON_ID_PREFIX, "");
|
|
||||||
const result = await chrome.storage.local.get('aliases');
|
|
||||||
const aliases = result.aliases || {};
|
|
||||||
// 新的别名为空就删除
|
|
||||||
if (alias) {
|
|
||||||
aliases[key] = alias;
|
|
||||||
} else {
|
|
||||||
delete aliases[key];
|
|
||||||
}
|
|
||||||
await chrome.storage.local.set({ aliases });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAlias(id) {
|
async function getAlias(id) {
|
||||||
const key = id.replace(PERSON_ID_PREFIX, "");
|
const key = id.replace(PERSON_ID_PREFIX, "");
|
||||||
const result = await chrome.storage.local.get('aliases');
|
const result = await chrome.storage.local.get('aliases');
|
||||||
@@ -34,44 +21,37 @@ function applyAliasAndButton(el) {
|
|||||||
const id = el.id;
|
const id = el.id;
|
||||||
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
const rawId = id.replace(PERSON_ID_PREFIX, "");
|
||||||
|
|
||||||
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
||||||
if (!existingBtn) {
|
if (!existingBtn) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = '设置别名';
|
button.textContent = '显示ID'; // 改为显示ID
|
||||||
button.style.position = 'fixed';
|
button.style.position = 'fixed';
|
||||||
button.style.left = `${rect.left + window.scrollX}px`;
|
button.style.left = `${rect.left + window.scrollX}px`;
|
||||||
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
||||||
button.style.zIndex = '99999';
|
button.style.zIndex = '99999';
|
||||||
button.style.padding = '4px 8px';
|
button.style.padding = '4px 8px';
|
||||||
button.style.fontSize = '12px';
|
button.style.fontSize = '12px';
|
||||||
button.style.backgroundColor = '#0078d4';
|
button.style.backgroundColor = '#6c757d'; // 灰色,表示只是信息查看
|
||||||
button.style.color = '#fff';
|
button.style.color = '#fff';
|
||||||
button.style.border = 'none';
|
button.style.border = 'none';
|
||||||
button.style.borderRadius = '4px';
|
button.style.borderRadius = '4px';
|
||||||
button.style.cursor = 'pointer';
|
button.style.cursor = 'pointer';
|
||||||
button.setAttribute('data-floating-btn-for', id);
|
button.setAttribute('data-floating-btn-for', id);
|
||||||
|
|
||||||
button.addEventListener('click', async () => {
|
button.addEventListener('click', () => {
|
||||||
const current = (await getAlias(id.replace(PERSON_ID_PREFIX, ""))) || document.getElementById(id)?.textContent || '';
|
// 弹窗显示 ID,方便复制
|
||||||
const newAlias = prompt("请输入别名:", current);
|
prompt("该用户的账号ID为 (请复制):", rawId);
|
||||||
// 如果是空字符串还是得进入
|
|
||||||
if (newAlias !== null) {
|
|
||||||
const el = document.getElementById(id); // 🔥 重新获取最新的元素
|
|
||||||
if (el && newAlias) el.textContent = newAlias.trim();
|
|
||||||
await saveAlias(id.replace(PERSON_ID_PREFIX, ""), newAlias.trim());
|
|
||||||
}
|
|
||||||
if (newAlias === "") {
|
|
||||||
alert("别名已删除");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(button);
|
document.body.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用别名(异步)
|
// 应用别名(异步)
|
||||||
getAlias(id.replace(PERSON_ID_PREFIX, "")).then(alias => {
|
getAlias(rawId).then(alias => {
|
||||||
if (alias && el.textContent !== alias) {
|
if (alias && el.textContent !== alias) {
|
||||||
el.textContent = alias;
|
el.textContent = alias;
|
||||||
}
|
}
|
||||||
@@ -417,6 +397,25 @@ function init() {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 自动同步检查
|
||||||
|
chrome.storage.local.get('lastSync').then(({ lastSync }) => {
|
||||||
|
const now = Date.now();
|
||||||
|
// 24 小时 = 86400000 ms
|
||||||
|
if (!lastSync || (now - lastSync > 86400000)) {
|
||||||
|
console.log("Teams Alias: 正在进行后台同步...");
|
||||||
|
if (typeof fetchAliasesFromDB === 'function') {
|
||||||
|
fetchAliasesFromDB().then(aliases => {
|
||||||
|
chrome.storage.local.set({ aliases, lastSync: now });
|
||||||
|
console.log("Teams Alias: 自动同步成功");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Teams Alias: 自动同步失败", err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Teams Alias: fetchAliasesFromDB 未定义,无法同步。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
applyToAll(); // 初始执行
|
applyToAll(); // 初始执行
|
||||||
|
|
||||||
// 兜底:每 2 秒再扫一次(避免漏掉异步更新)
|
// 兜底:每 2 秒再扫一次(避免漏掉异步更新)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Teams 别名管理",
|
"name": "Teams 别名管理",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/teams-alias-16.png",
|
"16": "icons/teams-alias-16.png",
|
||||||
"32": "icons/teams-alias-32.png",
|
"32": "icons/teams-alias-32.png",
|
||||||
@@ -10,11 +10,14 @@
|
|||||||
},
|
},
|
||||||
"description": "给 Teams 好友设置别名",
|
"description": "给 Teams 好友设置别名",
|
||||||
"permissions": ["storage", "scripting"],
|
"permissions": ["storage", "scripting"],
|
||||||
"host_permissions": ["https://teams.live.com/v2*"],
|
"host_permissions": [
|
||||||
|
"https://teams.live.com/v2*",
|
||||||
|
"https://nocodb.example.com/*"
|
||||||
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://teams.live.com/v2*"],
|
"matches": ["https://teams.live.com/v2*"],
|
||||||
"js": ["content.js"]
|
"js": ["env.js", "utils.js", "content.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"action": {
|
"action": {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build": "node scripts/generate-env.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>Teams 别名管理</h3>
|
<h3>Teams 别名管理</h3>
|
||||||
|
<button id="sync">立即同步</button>
|
||||||
<button id="export">导出</button>
|
<button id="export">导出</button>
|
||||||
<button id="import">覆盖导入</button>
|
<script src="env.js"></script>
|
||||||
<button id="update-import">合并导入</button>
|
<script src="utils.js"></script>
|
||||||
<input type="file" id="fileInput" accept="application/json" />
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
56
popup.js
56
popup.js
@@ -17,48 +17,26 @@ document.getElementById('export').addEventListener('click', async () => {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通用导入处理函数
|
// 手动同步
|
||||||
function handleFileImport(mode = 'overwrite') {
|
document.getElementById('sync').addEventListener('click', async () => {
|
||||||
const fileInput = document.getElementById('fileInput');
|
const btn = document.getElementById('sync');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '同步中...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
const handler = async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async function (event) {
|
|
||||||
try {
|
try {
|
||||||
const importedData = JSON.parse(event.target.result);
|
if (typeof fetchAliasesFromDB !== 'function') {
|
||||||
if (typeof importedData !== 'object' || importedData === null) {
|
throw new Error("utils.js not loaded properly");
|
||||||
alert('文件格式错误');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { aliases: existingAliases = {} } = await chrome.storage.local.get('aliases');
|
const aliases = await fetchAliasesFromDB();
|
||||||
|
await chrome.storage.local.set({ aliases, lastSync: Date.now() });
|
||||||
const updatedAliases = (mode === 'merge')
|
alert(`同步成功!共获取 ${Object.keys(aliases).length} 条数据。`);
|
||||||
? { ...importedData, ...existingAliases } // 保留现有的不被导入的覆盖
|
} catch (error) {
|
||||||
: importedData;
|
alert('同步失败,请检查网络或控制台日志。');
|
||||||
|
console.error(error);
|
||||||
await chrome.storage.local.set({ aliases: updatedAliases });
|
|
||||||
|
|
||||||
alert(mode === 'merge' ? '导入并合并成功' : '导入成功');
|
|
||||||
} catch (err) {
|
|
||||||
alert('无法解析 JSON 文件');
|
|
||||||
} finally {
|
} finally {
|
||||||
fileInput.removeEventListener('change', handler);
|
btn.textContent = originalText;
|
||||||
fileInput.value = ''; // 重置 input 以便后续重复导入同一文件
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', handler, { once: true });
|
|
||||||
fileInput.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入(覆盖)
|
|
||||||
document.getElementById('import').addEventListener('click', () => handleFileImport('overwrite'));
|
|
||||||
|
|
||||||
// 更新导入(合并)
|
|
||||||
document.getElementById('update-import').addEventListener('click', () => handleFileImport('merge'));
|
|
||||||
37
scripts/generate-env.js
Normal file
37
scripts/generate-env.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const envPath = path.join(__dirname, '..', '.env');
|
||||||
|
const outputPath = path.join(__dirname, '..', 'env.js');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let envContent = '';
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
envContent = fs.readFileSync(envPath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVars = {};
|
||||||
|
envContent.split('\n').forEach(line => {
|
||||||
|
const match = line.match(/^\s*([\w_]+)\s*=\s*(.*)?\s*$/);
|
||||||
|
if (match) {
|
||||||
|
const key = match[1];
|
||||||
|
let value = match[2] || '';
|
||||||
|
// Remove quotes if present
|
||||||
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'" ) && value.endsWith("'" ))) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
envVars[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileContent = `// Auto-generated by scripts/generate-env.js
|
||||||
|
const ENV_SECRETS = ${JSON.stringify(envVars, null, 4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, fileContent);
|
||||||
|
console.log('Successfully generated env.js');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating env.js:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
71
utils.js
Normal file
71
utils.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
const NOCODB_CONFIG = {
|
||||||
|
// Token and URL are loaded from env.js (ENV_SECRETS global variable)
|
||||||
|
TOKEN: (typeof ENV_SECRETS !== 'undefined' && ENV_SECRETS.NOCODB_TOKEN) ? ENV_SECRETS.NOCODB_TOKEN : "",
|
||||||
|
LIST_URL: (typeof ENV_SECRETS !== 'undefined' && ENV_SECRETS.NOCODB_LIST_URL) ? ENV_SECRETS.NOCODB_LIST_URL : ""
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 NocoDB 获取所有别名记录并整理为键值对
|
||||||
|
* @returns {Promise<Object>} { "账号ID": "别名", ... }
|
||||||
|
*/
|
||||||
|
async function fetchAliasesFromDB() {
|
||||||
|
const listUrl = NOCODB_CONFIG.LIST_URL;
|
||||||
|
const headers = {
|
||||||
|
"xc-token": NOCODB_CONFIG.TOKEN,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!listUrl || !NOCODB_CONFIG.TOKEN) {
|
||||||
|
console.error("Teams Alias: 缺少配置 (URL 或 Token),请检查 .env 文件。");
|
||||||
|
throw new Error("Missing configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
let allAliases = {};
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 1000;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
// 构建带参数的 URL
|
||||||
|
const url = new URL(listUrl);
|
||||||
|
url.searchParams.append("limit", limit);
|
||||||
|
url.searchParams.append("fields", "账号ID,_最终输出");
|
||||||
|
url.searchParams.append("offset", offset);
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), { headers });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`获取数据失败: ${response.status} ${response.statusText}`);
|
||||||
|
throw new Error("网络请求失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const rows = data.list || [];
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理数据
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row["账号ID"] && row["_最终输出"]) {
|
||||||
|
// key = 账号ID, value = _最终输出
|
||||||
|
allAliases[row["账号ID"]] = row["_最终输出"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rows.length < limit) {
|
||||||
|
break; // 已获取所有数据
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`同步完成,共获取 ${Object.keys(allAliases).length} 条别名记录。`);
|
||||||
|
return allAliases;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("同步别名数据时出错:", error);
|
||||||
|
throw error; // 向抛出以便调用者处理
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user