Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8482611011 | ||
|
|
7403a03246 | ||
|
|
856f4c01c9 | ||
|
|
5d872f7bf1 | ||
|
|
ce67ae56bd |
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
NOCODB_TOKEN=
|
||||||
|
NOCODB_LIST_URL=https://nocodb.example.com/api/v2/tables/xxx/records
|
||||||
|
NOCODB_FIELD_ID=账号ID
|
||||||
|
NOCODB_FIELD_ALIAS=_最终输出
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
给 Microsoft Teams 好友的名称添加别名。
|
给 Microsoft Teams 好友的名称添加别名。
|
||||||
|
|
||||||
在插件弹窗中导入 CSV 文件作为数据源。CSV 需要包含“账号ID”和“最终输出”两列,插件会用“账号ID”匹配 Teams 用户,并显示对应的“最终输出”。
|
使用 NocoDB 数据库获取数据。修改 .env 之后别忘了修改 manifest.json 中的 host_permissions
|
||||||
|
|
||||||
|
v0.3.0 之后支持给聊天分类。
|
||||||
|
|||||||
1091
content.js
1091
content.js
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,23 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Teams 别名管理",
|
"name": "Teams 别名管理",
|
||||||
"version": "0.4.0",
|
"version": "0.3.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",
|
||||||
"48": "icons/teams-alias-48.png",
|
"48": "icons/teams-alias-48.png",
|
||||||
"128": "icons/teams-alias-128.png"
|
"128": "icons/teams-alias-128.png"
|
||||||
},
|
},
|
||||||
"description": "给 Teams 好友设置别名",
|
"description": "给 Teams 好友设置别名以及聊天分类",
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage", "scripting"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://teams.live.com/v2*"
|
"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": {
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.4.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.4.0",
|
"version": "0.1.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chrome-types": "^0.1.347"
|
"chrome-types": "^0.1.347"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.4.0",
|
"version": "0.3.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": "",
|
||||||
|
|||||||
18
popup.html
18
popup.html
@@ -8,7 +8,7 @@
|
|||||||
background-color: #f9fafb;
|
background-color: #f9fafb;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
width: 160px;
|
width: 120px;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,25 +50,15 @@
|
|||||||
#fileInput {
|
#fileInput {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#status {
|
|
||||||
margin-top: 8px;
|
|
||||||
min-height: 15px;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.35;
|
|
||||||
color: #4b5563;
|
|
||||||
text-align: center;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<title>Teams 别名管理</title>
|
<title>Teams 别名管理</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>Teams 别名管理</h3>
|
<h3>Teams 别名管理</h3>
|
||||||
<input id="fileInput" type="file" accept=".csv,text/csv">
|
<button id="sync">立即同步</button>
|
||||||
<button id="import">导入 CSV</button>
|
|
||||||
<button id="export">导出</button>
|
<button id="export">导出</button>
|
||||||
<div id="status"></div>
|
<script src="env.js"></script>
|
||||||
|
<script src="utils.js"></script>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
158
popup.js
158
popup.js
@@ -1,137 +1,3 @@
|
|||||||
const REQUIRED_ID_FIELD = '账号ID';
|
|
||||||
const REQUIRED_ALIAS_FIELD = '最终输出';
|
|
||||||
|
|
||||||
function setStatus(message) {
|
|
||||||
document.getElementById('status').textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCsv(text) {
|
|
||||||
const rows = [];
|
|
||||||
let row = [];
|
|
||||||
let cell = '';
|
|
||||||
let inQuotes = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
const char = text[i];
|
|
||||||
const nextChar = text[i + 1];
|
|
||||||
|
|
||||||
if (char === '"') {
|
|
||||||
if (inQuotes && nextChar === '"') {
|
|
||||||
cell += '"';
|
|
||||||
i++;
|
|
||||||
} else {
|
|
||||||
inQuotes = !inQuotes;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === ',' && !inQuotes) {
|
|
||||||
row.push(cell);
|
|
||||||
cell = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((char === '\n' || char === '\r') && !inQuotes) {
|
|
||||||
if (char === '\r' && nextChar === '\n') i++;
|
|
||||||
row.push(cell);
|
|
||||||
if (row.some(value => value.trim() !== '')) rows.push(row);
|
|
||||||
row = [];
|
|
||||||
cell = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cell += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
row.push(cell);
|
|
||||||
if (row.some(value => value.trim() !== '')) rows.push(row);
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHeader(value) {
|
|
||||||
return value.replace(/^\uFEFF/, '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function aliasesFromCsv(text) {
|
|
||||||
const rows = parseCsv(text);
|
|
||||||
if (rows.length === 0) {
|
|
||||||
throw new Error('CSV 文件为空。');
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = rows[0].map(normalizeHeader);
|
|
||||||
const idIndex = headers.indexOf(REQUIRED_ID_FIELD);
|
|
||||||
const aliasIndex = headers.indexOf(REQUIRED_ALIAS_FIELD);
|
|
||||||
|
|
||||||
if (idIndex === -1 || aliasIndex === -1) {
|
|
||||||
throw new Error(`CSV 必须包含“${REQUIRED_ID_FIELD}”和“${REQUIRED_ALIAS_FIELD}”两列。`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const aliases = {};
|
|
||||||
|
|
||||||
rows.slice(1).forEach(row => {
|
|
||||||
const id = (row[idIndex] || '').trim();
|
|
||||||
const alias = (row[aliasIndex] || '').trim();
|
|
||||||
|
|
||||||
if (id && alias) {
|
|
||||||
aliases[id] = alias;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return aliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decodeCsvBuffer(buffer, encoding) {
|
|
||||||
return new TextDecoder(encoding).decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAliasesFromCsvBuffer(buffer) {
|
|
||||||
const encodings = ['utf-8', 'gb18030', 'gbk'];
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
for (const encoding of encodings) {
|
|
||||||
try {
|
|
||||||
return aliasesFromCsv(decodeCsvBuffer(buffer, encoding));
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入 CSV
|
|
||||||
document.getElementById('import').addEventListener('click', () => {
|
|
||||||
document.getElementById('fileInput').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('fileInput').addEventListener('change', async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const btn = document.getElementById('import');
|
|
||||||
const originalText = btn.textContent;
|
|
||||||
btn.textContent = '导入中...';
|
|
||||||
btn.disabled = true;
|
|
||||||
setStatus('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const buffer = await file.arrayBuffer();
|
|
||||||
const aliases = readAliasesFromCsvBuffer(buffer);
|
|
||||||
await chrome.storage.local.set({ aliases, lastImport: Date.now(), sourceFileName: file.name });
|
|
||||||
setStatus(`已导入 ${Object.keys(aliases).length} 条`);
|
|
||||||
alert(`导入成功!共导入 ${Object.keys(aliases).length} 条数据。`);
|
|
||||||
} catch (error) {
|
|
||||||
setStatus('导入失败');
|
|
||||||
alert(error.message || '导入失败,请检查 CSV 文件。');
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
btn.textContent = originalText;
|
|
||||||
btn.disabled = false;
|
|
||||||
event.target.value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 导出数据
|
// 导出数据
|
||||||
document.getElementById('export').addEventListener('click', async () => {
|
document.getElementById('export').addEventListener('click', async () => {
|
||||||
const { aliases } = await chrome.storage.local.get('aliases');
|
const { aliases } = await chrome.storage.local.get('aliases');
|
||||||
@@ -150,3 +16,27 @@ document.getElementById('export').addEventListener('click', async () => {
|
|||||||
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 手动同步
|
||||||
|
document.getElementById('sync').addEventListener('click', async () => {
|
||||||
|
const btn = document.getElementById('sync');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '同步中...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof fetchAliasesFromDB !== 'function') {
|
||||||
|
throw new Error("utils.js not loaded properly");
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliases = await fetchAliasesFromDB();
|
||||||
|
await chrome.storage.local.set({ aliases, lastSync: Date.now() });
|
||||||
|
alert(`同步成功!共获取 ${Object.keys(aliases).length} 条数据。`);
|
||||||
|
} catch (error) {
|
||||||
|
alert('同步失败,请检查网络或控制台日志。');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
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);
|
||||||
|
}
|
||||||
73
utils.js
Normal file
73
utils.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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 : "",
|
||||||
|
FIELD_ID: (typeof ENV_SECRETS !== 'undefined' && ENV_SECRETS.NOCODB_FIELD_ID) ? ENV_SECRETS.NOCODB_FIELD_ID : "账号ID",
|
||||||
|
FIELD_ALIAS: (typeof ENV_SECRETS !== 'undefined' && ENV_SECRETS.NOCODB_FIELD_ALIAS) ? ENV_SECRETS.NOCODB_FIELD_ALIAS : "_最终输出"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 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", `${NOCODB_CONFIG.FIELD_ID},${NOCODB_CONFIG.FIELD_ALIAS}`);
|
||||||
|
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[NOCODB_CONFIG.FIELD_ID] && row[NOCODB_CONFIG.FIELD_ALIAS]) {
|
||||||
|
// key = 账号ID, value = _最终输出
|
||||||
|
allAliases[row[NOCODB_CONFIG.FIELD_ID]] = row[NOCODB_CONFIG.FIELD_ALIAS];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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