diff --git a/README.md b/README.md index efe52ae..56ffca7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# teams-alias +# Teams 别名插件 -Teams 别名插件 \ No newline at end of file +给 Microsoft Teams 好友的名称添加别名。 diff --git a/content.js b/content.js new file mode 100644 index 0000000..f414c96 --- /dev/null +++ b/content.js @@ -0,0 +1,428 @@ +const PERSON_ID_PREFIX = 'chat-topic-person-'; +const ICON_ID_PREFIX = "presence-pill-"; +const CHAT_ROSTER_PREFIX = "chat-roster-item-name-"; +const SUGGEST_PEOPLE_PREFIX = "AUTOSUGGEST_SUGGESTION_PEOPLE"; +const ROSTER_AVATAR_PREFIX = "roster-avatar-img-"; +const SERP_PEOPLE_CARD_PREFIX = "serp-people-card-content-"; +const PEOPLE_PICKER_PREFIX = "people-picker-entry-"; +const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-"; +let debounceTimer = null; +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) { + const key = id.replace(PERSON_ID_PREFIX, ""); + const result = await chrome.storage.local.get('aliases'); + const aliases = result.aliases || {}; + return aliases[key] || null; +} + +// 设置别名显示 + 按钮添加 +function applyAliasAndButton(el) { + const id = el.id; + if (!id || !id.startsWith(PERSON_ID_PREFIX)) return; + + const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`); + if (!existingBtn) { + const rect = el.getBoundingClientRect(); + + const button = document.createElement('button'); + button.textContent = '设置别名'; + button.style.position = 'fixed'; + button.style.left = `${rect.left + window.scrollX}px`; + button.style.top = `${rect.bottom + window.scrollY + 20}px`; + button.style.zIndex = '99999'; + button.style.padding = '4px 8px'; + button.style.fontSize = '12px'; + button.style.backgroundColor = '#0078d4'; + button.style.color = '#fff'; + button.style.border = 'none'; + button.style.borderRadius = '4px'; + button.style.cursor = 'pointer'; + button.setAttribute('data-floating-btn-for', id); + + button.addEventListener('click', async () => { + const current = (await getAlias(id.replace(PERSON_ID_PREFIX, ""))) || document.getElementById(id)?.textContent || ''; + const newAlias = prompt("请输入别名:", current); + // 如果是空字符串还是得进入 + 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); + } + + // 应用别名(异步) + getAlias(id.replace(PERSON_ID_PREFIX, "")).then(alias => { + if (alias && el.textContent !== alias) { + el.textContent = alias; + } + }); +} + +// 主要查找右侧消息列表中的名字并修改 +function applyRightChatAlias(el) { + let id = el.id; + if (!id || !id.startsWith(ICON_ID_PREFIX)) return; + + let parent = el; + // 向上查找 4 个父元素 + for (let i = 0; i < 4; i++) { + if (parent.parentElement) { + parent = parent.parentElement; + } else { + return; // 如果不足4层,就跳过 + } + } + + // 获取前一个兄弟元素 + const prevSibling = parent.previousElementSibling; + if (!prevSibling) return; + + // 向下查找第 4 个子元素(层级式) + let target = prevSibling; + for (let i = 0; i < 4; i++) { + if (target.children.length > 0) { + target = target.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足4层,跳过 + } + } + // 判断符合才修改 + if (target.getAttribute('data-tid') === 'message-author-name') { + getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => { + if (alias && target.textContent !== alias) { + target.textContent = alias; + } + }); + } +} + +// 主要查找左侧消息列表中的名字并修改 +function applyLeftChatAlias(el) { + let id = el.id; + if (!id || !id.startsWith(ICON_ID_PREFIX)) return; + + let parent = el; + // 向上查找 3 个父元素 + for (let i = 0; i < 3; i++) { + if (parent.parentElement) { + parent = parent.parentElement; + } else { + return; // 如果不足3层,就跳过 + } + } + + // 获取后一个兄弟元素 + const nextSibling = parent.nextElementSibling; + if (!nextSibling) return; + + // 向下查找第 2 个子元素(层级式) + let target = nextSibling; + for (let i = 0; i < 2; i++) { + if (target.children.length > 0) { + target = target.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足2层,跳过 + } + } + // 判断符合才修改 + if (target.getAttribute('data-tid') === 'chat-list-item-title') { + getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => { + if (alias && target.textContent !== alias) { + target.textContent = alias; + } + }); + } +} + +// 修改群组人员的名称 +function applyChatRosterAlias(el) { + let id = el.id; + if (!id || !id.startsWith(CHAT_ROSTER_PREFIX)) return; + + getAlias(id.replace(CHAT_ROSTER_PREFIX, "")).then(alias => { + if (alias && el.textContent !== alias) { + el.textContent = alias; + } + }); +} + +// 群组添加人员的别名 +function applyPeoplePickerAlias(el) { + let tid = el.getAttribute('data-tid'); + if (!tid || !tid.startsWith(PEOPLE_PICKER_PREFIX)) return; + + let child = el.children[1]; + // 向下查找第 3 个子元素(层级式) + for (let i = 0; i < 3; i++) { + if (child.children.length > 0) { + child = child.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足3层,跳过 + } + } + if (child.tagName.toLowerCase() === 'span') { + let id = "8:" + tid.replace(PEOPLE_PICKER_PREFIX, ""); + getAlias(id).then(alias => { + if (alias && child.textContent !== alias) { + child.textContent = alias; + } + }); + } +} + +// 群组添加人员时选中的人员 +function applyPeoplePickerSelectedAlias(el) { + let tid = el.getAttribute('data-tid'); + if (!tid || !tid.startsWith(PEOPLE_PICKER_SEL_PREFIX)) return; + + let id = "8:" + tid.replace(PEOPLE_PICKER_SEL_PREFIX, ""); + getAlias(id).then(alias => { + if (alias && el.textContent !== alias) { + el.textContent = alias; + } + }); +} + +// 追加搜索框中的人员别名 +function applySuggestPeopleAlias(el) { + let tid = el.getAttribute('data-tid'); + if (!tid || !tid.startsWith(SUGGEST_PEOPLE_PREFIX)) return; + + let child = el.children[1]; // 第二个子元素 + // 向下查找第 2 个子元素(层级式) + for (let i = 0; i < 2; i++) { + if (child.children.length > 0) { + child = child.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足2层,跳过 + } + } + if (child.getAttribute('data-tid') !== 'AUTOSUGGEST_SUGGESTION_TITLE') return; + + let id = tid.replace(SUGGEST_PEOPLE_PREFIX, ""); + getAlias(id).then(alias => { + if (alias) { + let lastSpan = child.lastElementChild; + if (lastSpan.id.startsWith("suggest-alias-attached")) { + if (lastSpan.textContent === `[${alias}]`) return; + lastSpan.textContent = `[${alias}]`; + } else { + const span = document.createElement('span'); + span.id = `suggest-alias-attached-${id}`; + span.textContent = `[${alias}]`; + span.style.marginLeft = '4px'; + span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b'; + child.appendChild(span); + } + } + }); +} + +// 追加人员搜索中的别名 +function applySerpPeopleAlias(el) { + let id = el.id; + if (!id || !id.startsWith(SERP_PEOPLE_CARD_PREFIX)) return; + + let child = el.children[2]; + // 向下查找第 4 个子元素(层级式) + for (let i = 0; i < 4; i++) { + if (child.children.length > 0) { + child = child.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足4层,跳过 + } + } + + id = id.replace(SERP_PEOPLE_CARD_PREFIX, ""); + getAlias(id).then(alias => { + if (alias) { + let lastSpan = child.lastElementChild; + if (lastSpan.id.startsWith("people-card-attached")) { + if (lastSpan.textContent === `[${alias}]`) return; + lastSpan.textContent = `[${alias}]`; + } else { + const span = document.createElement('span'); + span.id = `people-card-attached-${id}`; + span.textContent = `[${alias}]`; + span.style.marginLeft = '4px'; + span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b'; + child.appendChild(span); + } + } + }); +} + +// 修改通话中的人名 +function applyCallingAlias(el) { + if (el.getAttribute('data-cid') !== 'calling-participant-stream') return; + + let child = el.children[1]; + // 向下查找第 5 个子元素(层级式) + for (let i = 0; i < 5; i++) { + if (child.children.length > 0) { + child = child.children[0]; // 每层往下取第一个子元素 + } else { + return; // 不足5层,跳过 + } + } + if (child.tagName.toLowerCase() === 'span') { + getAlias(el.getAttribute('data-acc-element-id')).then(alias => { + if (alias && child.textContent !== alias) { + child.textContent = alias; + } + }); + } +} + +// 修改通话右侧人名的别名 +function applyRosterAvatarAlias(el) { + let id = el.id; + if (!id || !id.startsWith(ROSTER_AVATAR_PREFIX)) return; + + getAlias(id.replace(ROSTER_AVATAR_PREFIX, "")).then(alias => { + if (alias && el.textContent !== alias) { + el.textContent = alias; + } + }); +} + +// 查看此通话中的人员 +function applyPeopleInCall(el) { + let id = el.id; + if (!id || !id.startsWith(ICON_ID_PREFIX)) return; + + let parent = el; + // 向上查找 2 个父元素 + for (let i = 0; i < 2; i++) { + if (parent.parentElement) { + parent = parent.parentElement; + } else { + return; // 如果不足2层,就跳过 + } + } + // 一个是查看此通话中的参与者,一个是呼叫其他人加入 + if ([ + "audio_dropin_add_participants_dialog_renderer", + "audio-drop-in-live-roster" + ].includes(parent.getAttribute("data-tid"))) { + let target = parent.nextElementSibling; + if (!target) return; + if (target.tagName.toLowerCase() === "span") { + getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => { + if (alias && target.textContent !== alias) { + target.textContent = alias; + } + }); + } + } + +} + +// 回应表情的人员别名 +function applyReactionAlias(el) { + if (el.getAttribute('data-tid') !== 'diverse-reaction-user-list-item') return; + try { + const tabster = JSON.parse(el.getAttribute("data-tabster")); + + let child = el.children[1]; + let target = child.children[0]; + let id = tabster.observed.names[0]; + getAlias(id).then(alias => { + if (alias && target.textContent !== alias) { + target.textContent = alias; + } + }); + + } catch (error) {} +} + +// 查找所有目标元素应用别名和按钮 +function applyToAll() { + document.querySelectorAll('[data-floating-btn-for]').forEach(btn => btn.remove()); + + const allPersons = document.querySelectorAll(`[id^="${PERSON_ID_PREFIX}"]`); + allPersons.forEach(el => applyAliasAndButton(el)); + + const allIcons = document.querySelectorAll(`[id^="${ICON_ID_PREFIX}"]`); + allIcons.forEach(el => { + applyRightChatAlias(el); + applyLeftChatAlias(el); + applyPeopleInCall(el); + }); + + const allChatRoster = document.querySelectorAll(`[id^="${CHAT_ROSTER_PREFIX}"]`); + allChatRoster.forEach(el => applyChatRosterAlias(el)); + + const allSuggestPeople = document.querySelectorAll(`[data-tid^="${SUGGEST_PEOPLE_PREFIX}"]`); + allSuggestPeople.forEach(el => applySuggestPeopleAlias(el)); + + const allCalling = document.querySelectorAll(`[data-cid="calling-participant-stream"]`); + allCalling.forEach(el => applyCallingAlias(el)); + + const allRosterAvatar = document.querySelectorAll(`[id^="${ROSTER_AVATAR_PREFIX}"]`); + allRosterAvatar.forEach(el => applyRosterAvatarAlias(el)); + + const allSerpPeople = document.querySelectorAll(`[id^="${SERP_PEOPLE_CARD_PREFIX}"]`); + allSerpPeople.forEach(el => applySerpPeopleAlias(el)); + + const allPeoplePicker = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_PREFIX}"]`); + allPeoplePicker.forEach(el => applyPeoplePickerAlias(el)); + + const allPeoplePickerSelected = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_SEL_PREFIX}"]`); + allPeoplePickerSelected.forEach(el => applyPeoplePickerSelectedAlias(el)); + + const allReaction = document.querySelectorAll(`[data-tid="diverse-reaction-user-list-item"]`); + allReaction.forEach(el => applyReactionAlias(el)); +} + +// 初始化逻辑 +function init() { + const observer = new MutationObserver(() => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + if (isMutating) return; // 🧠 防止自己触发自己 + isMutating = true; + + applyToAll(); // 页面内容变动后重新应用 + + // 给浏览器一点时间完成 DOM 更新后再允许 observer 响应 + setTimeout(() => { + isMutating = false; + }, 500); // 至少比这个高才行,不然会一直触发 + }, 300); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + applyToAll(); // 初始执行 + + // 兜底:每 2 秒再扫一次(避免漏掉异步更新) + // setInterval(() => { + // applyToAll(); + // }, 2000); +} + +init(); diff --git a/icons/teams-alias-128.png b/icons/teams-alias-128.png new file mode 100644 index 0000000..a055236 Binary files /dev/null and b/icons/teams-alias-128.png differ diff --git a/icons/teams-alias-16.png b/icons/teams-alias-16.png new file mode 100644 index 0000000..1b5160a Binary files /dev/null and b/icons/teams-alias-16.png differ diff --git a/icons/teams-alias-32.png b/icons/teams-alias-32.png new file mode 100644 index 0000000..9024419 Binary files /dev/null and b/icons/teams-alias-32.png differ diff --git a/icons/teams-alias-48.png b/icons/teams-alias-48.png new file mode 100644 index 0000000..de2505a Binary files /dev/null and b/icons/teams-alias-48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..a010f07 --- /dev/null +++ b/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "Teams 别名管理", + "version": "0.1.0", + "icons": { + "16": "icons/teams-alias-16.png", + "32": "icons/teams-alias-32.png", + "48": "icons/teams-alias-48.png", + "128": "icons/teams-alias-128.png" + }, + "description": "给 Teams 好友设置别名", + "permissions": ["storage", "scripting"], + "host_permissions": ["https://teams.live.com/v2*"], + "content_scripts": [ + { + "matches": ["https://teams.live.com/v2*"], + "js": ["content.js"] + } + ], + "action": { + "default_popup": "popup.html" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..222a5bf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "teams-alias", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "teams-alias", + "version": "0.1.0", + "license": "GPL-3.0-only", + "dependencies": { + "chrome-types": "^0.1.347" + } + }, + "node_modules/chrome-types": { + "version": "0.1.347", + "resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.347.tgz", + "integrity": "sha512-c8lbQnYfghYJ8hQ6XivJzex3NJS7RAdcD81uGylnvrRqzF2QMei/wVhsE6+PsZDOipNxbFMxjhf242ZuGhYZzQ==", + "license": "Apache-2.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b93586a --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "teams-alias", + "version": "0.1.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "GPL-3.0-only", + "description": "", + "dependencies": { + "chrome-types": "^0.1.347" + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..0755758 --- /dev/null +++ b/popup.html @@ -0,0 +1,64 @@ + + +
+ + +