diff --git a/.env.example b/.env.example deleted file mode 100644 index 32d6eab..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -NOCODB_TOKEN= -NOCODB_LIST_URL=https://nocodb.example.com/api/v2/tables/xxx/records -NOCODB_FIELD_ID=账号ID -NOCODB_FIELD_ALIAS=_最终输出 \ No newline at end of file diff --git a/README.md b/README.md index 93b6eaa..fbbf5e3 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ 给 Microsoft Teams 好友的名称添加别名。 -使用 NocoDB 数据库获取数据。修改 .env 之后别忘了修改 manifest.json 中的 host_permissions +在插件弹窗中导入 CSV 文件作为数据源。CSV 需要包含“账号ID”和“最终输出”两列,插件会用“账号ID”匹配 Teams 用户,并显示对应的“最终输出”。 diff --git a/content.js b/content.js index 8a6088f..d3e4c88 100644 --- a/content.js +++ b/content.js @@ -397,25 +397,6 @@ function init() { 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(); // 初始执行 // 兜底:每 2 秒再扫一次(避免漏掉异步更新) diff --git a/manifest.json b/manifest.json index 2ccd143..f299299 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Teams 别名管理", - "version": "0.2.0", + "version": "0.4.0", "icons": { "16": "icons/teams-alias-16.png", "32": "icons/teams-alias-32.png", @@ -9,15 +9,14 @@ "128": "icons/teams-alias-128.png" }, "description": "给 Teams 好友设置别名", - "permissions": ["storage", "scripting"], + "permissions": ["storage"], "host_permissions": [ - "https://teams.live.com/v2*", - "https://nocodb.example.com/*" + "https://teams.live.com/v2*" ], "content_scripts": [ { "matches": ["https://teams.live.com/v2*"], - "js": ["env.js", "utils.js", "content.js"] + "js": ["content.js"] } ], "action": { diff --git a/package-lock.json b/package-lock.json index 222a5bf..8f1847f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "teams-alias", - "version": "0.1.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "teams-alias", - "version": "0.1.0", + "version": "0.4.0", "license": "GPL-3.0-only", "dependencies": { "chrome-types": "^0.1.347" diff --git a/package.json b/package.json index 88dd897..620b244 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "teams-alias", - "version": "0.2.0", + "version": "0.4.0", "main": "index.js", "scripts": { - "build": "node scripts/generate-env.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/popup.html b/popup.html index 22e8cc7..fd3335b 100644 --- a/popup.html +++ b/popup.html @@ -8,7 +8,7 @@ background-color: #f9fafb; margin: 0; padding: 12px; - width: 120px; + width: 160px; color: #111827; } @@ -50,15 +50,25 @@ #fileInput { 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; + } Teams 别名管理

Teams 别名管理

- + + - - +
diff --git a/popup.js b/popup.js index dfa0d6b..5f95d0b 100644 --- a/popup.js +++ b/popup.js @@ -1,3 +1,137 @@ +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 () => { const { aliases } = await chrome.storage.local.get('aliases'); @@ -16,27 +150,3 @@ document.getElementById('export').addEventListener('click', async () => { 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; - } -}); \ No newline at end of file diff --git a/scripts/generate-env.js b/scripts/generate-env.js deleted file mode 100644 index 632b59f..0000000 --- a/scripts/generate-env.js +++ /dev/null @@ -1,37 +0,0 @@ -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); -} diff --git a/utils.js b/utils.js deleted file mode 100644 index 53b20f2..0000000 --- a/utils.js +++ /dev/null @@ -1,73 +0,0 @@ -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} { "账号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; // 向抛出以便调用者处理 - } -}