Files
teams-alias/content.js
Julian Freeman 856f4c01c9 support export
2026-04-10 21:53:55 -04:00

1398 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const PERSON_ID_PREFIX = 'chat-topic-person-';
const ICON_ID_PREFIX = "presence-pill-";
const CHAT_ROSTER_PREFIX = "chat-roster-item-name-";
const CHAT_TITLE_PREFIX = "title-chat-list-item_";
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-";
const CHAT_CATEGORY_STORAGE_KEY = "chatCategories";
const CHAT_CACHE_STORAGE_KEY = "chatCategoryChatCache";
const CHAT_ORGANIZER_ROOT_ID = "teams-alias-chat-organizer-root";
const CHAT_ORGANIZER_STYLE_ID = "teams-alias-chat-organizer-style";
const CHAT_ORGANIZER_BUTTON_ID = "teams-alias-chat-organizer-button";
let debounceTimer = null;
let isMutating = false;
let organizerStateLoaded = false;
let organizerSaveTimer = null;
const chatOrganizerState = {
categories: [],
chatCache: {},
visibleChats: {},
isModalOpen: false,
selectedCategoryId: "",
feedback: "",
draftCategoryName: "",
draftChatId: "",
visibleChatsExpanded: false,
expandedCategoryIds: {},
createPanelOpen: false,
addPanelOpen: false
};
function getOrganizerRoot() {
return document.getElementById(CHAT_ORGANIZER_ROOT_ID);
}
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function normalizeChatId(rawValue) {
if (!rawValue) return "";
let value = String(rawValue).trim();
if (value.startsWith(CHAT_TITLE_PREFIX)) {
value = value.slice(CHAT_TITLE_PREFIX.length);
}
return value.startsWith("19:") ? value : "";
}
function createCategoryId() {
return `cat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function setOrganizerFeedback(message) {
chatOrganizerState.feedback = message || "";
renderChatOrganizer();
}
function scheduleOrganizerStateSave() {
clearTimeout(organizerSaveTimer);
organizerSaveTimer = setTimeout(async () => {
await chrome.storage.local.set({
[CHAT_CATEGORY_STORAGE_KEY]: chatOrganizerState.categories,
[CHAT_CACHE_STORAGE_KEY]: chatOrganizerState.chatCache
});
}, 300);
}
async function loadChatOrganizerState() {
if (organizerStateLoaded) return;
const result = await chrome.storage.local.get([
CHAT_CATEGORY_STORAGE_KEY,
CHAT_CACHE_STORAGE_KEY
]);
chatOrganizerState.categories = Array.isArray(result[CHAT_CATEGORY_STORAGE_KEY])
? result[CHAT_CATEGORY_STORAGE_KEY]
: [];
chatOrganizerState.chatCache = result[CHAT_CACHE_STORAGE_KEY] || {};
chatOrganizerState.selectedCategoryId = chatOrganizerState.categories[0]?.id || "";
organizerStateLoaded = true;
}
function getCategoryById(categoryId) {
return chatOrganizerState.categories.find(category => category.id === categoryId) || null;
}
function getChatMeta(chatId) {
return chatOrganizerState.visibleChats[chatId] || chatOrganizerState.chatCache[chatId] || null;
}
function inferAvatarFallback(name) {
const base = (name || "?").trim();
return base.slice(0, 2).toUpperCase();
}
function findChatTitleElement(chatId) {
const normalizedChatId = normalizeChatId(chatId);
if (!normalizedChatId) return null;
return document.getElementById(`${CHAT_TITLE_PREFIX}${normalizedChatId}`);
}
function findClickableChatElement(chatId) {
const titleElement = findChatTitleElement(chatId);
if (!titleElement) return null;
const clickable = titleElement.closest('button, a, [role="button"], [role="link"], [role="listitem"]');
if (clickable) return clickable;
let current = titleElement;
for (let depth = 0; depth < 8 && current; depth += 1) {
if (typeof current.click === "function") {
return current;
}
current = current.parentElement;
}
return titleElement;
}
function deepLinkToChat(chatId) {
const normalizedChatId = normalizeChatId(chatId);
if (!normalizedChatId) return false;
const candidates = [
`${window.location.origin}/l/chat/${encodeURIComponent(normalizedChatId)}/conversations`,
`https://teams.microsoft.com/l/chat/${encodeURIComponent(normalizedChatId)}/conversations`
];
for (const url of candidates) {
try {
window.location.href = url;
return true;
} catch (error) {}
}
return false;
}
function openChatById(chatId) {
const clickableElement = findClickableChatElement(chatId);
if (clickableElement) {
clickableElement.click();
return true;
}
return deepLinkToChat(chatId);
}
function extractChatAvatar(titleElement) {
const rowRoot = titleElement.closest('[role="listitem"], button, a, [role="button"]') || titleElement.parentElement;
if (!rowRoot) return "";
const avatarImage = rowRoot.querySelector("img");
if (avatarImage?.src) {
return avatarImage.src;
}
const avatarCandidate = rowRoot.querySelector('[style*="background-image"]');
const inlineStyle = avatarCandidate?.style?.backgroundImage || "";
const matched = inlineStyle.match(/url\(["']?(.*?)["']?\)/);
return matched?.[1] || "";
}
function scanVisibleChats() {
const titleElements = document.querySelectorAll(`[id^="${CHAT_TITLE_PREFIX}"]`);
let changed = false;
const nextVisibleChats = {};
titleElements.forEach(titleElement => {
const chatId = normalizeChatId(titleElement.id);
if (!chatId) return;
const name = titleElement.textContent?.trim() || chatOrganizerState.chatCache[chatId]?.name || chatId;
const avatarUrl = extractChatAvatar(titleElement) || chatOrganizerState.chatCache[chatId]?.avatarUrl || "";
const chatMeta = {
id: chatId,
name,
avatarUrl,
updatedAt: Date.now()
};
nextVisibleChats[chatId] = chatMeta;
const cached = chatOrganizerState.chatCache[chatId];
if (!cached || cached.name !== chatMeta.name || cached.avatarUrl !== chatMeta.avatarUrl) {
chatOrganizerState.chatCache[chatId] = chatMeta;
changed = true;
}
});
const visibleIds = Object.keys(nextVisibleChats);
const previousVisibleIds = Object.keys(chatOrganizerState.visibleChats);
if (visibleIds.length !== previousVisibleIds.length || visibleIds.some(id => !chatOrganizerState.visibleChats[id])) {
changed = true;
}
chatOrganizerState.visibleChats = nextVisibleChats;
if (changed) {
scheduleOrganizerStateSave();
renderChatOrganizer();
}
}
function addChatToCategory(categoryId, rawChatId) {
const category = getCategoryById(categoryId);
const chatId = normalizeChatId(rawChatId);
if (!category) {
setOrganizerFeedback("请选择一个分类。");
return;
}
if (!chatId) {
setOrganizerFeedback("聊天 ID 格式无效,必须以 19: 开头。");
return;
}
if (category.chatIds.includes(chatId)) {
setOrganizerFeedback("该聊天已存在于当前分类。");
return;
}
category.chatIds.push(chatId);
const meta = getChatMeta(chatId);
if (meta) {
chatOrganizerState.chatCache[chatId] = {
...chatOrganizerState.chatCache[chatId],
...meta,
updatedAt: Date.now()
};
} else {
chatOrganizerState.chatCache[chatId] = {
id: chatId,
name: chatId,
avatarUrl: "",
updatedAt: Date.now()
};
}
scheduleOrganizerStateSave();
setOrganizerFeedback("已加入分类。");
}
function removeChatFromCategory(categoryId, chatId) {
const category = getCategoryById(categoryId);
if (!category) return;
category.chatIds = category.chatIds.filter(id => id !== chatId);
scheduleOrganizerStateSave();
setOrganizerFeedback("已移除聊天。");
}
function createCategory(name) {
const trimmedName = (name || "").trim();
if (!trimmedName) {
setOrganizerFeedback("分类名称不能为空。");
return;
}
const category = {
id: createCategoryId(),
name: trimmedName,
chatIds: []
};
chatOrganizerState.categories.push(category);
chatOrganizerState.expandedCategoryIds[category.id] = true;
chatOrganizerState.selectedCategoryId = category.id;
scheduleOrganizerStateSave();
setOrganizerFeedback("分类已创建。");
}
function deleteCategory(categoryId) {
chatOrganizerState.categories = chatOrganizerState.categories.filter(category => category.id !== categoryId);
delete chatOrganizerState.expandedCategoryIds[categoryId];
if (chatOrganizerState.selectedCategoryId === categoryId) {
chatOrganizerState.selectedCategoryId = chatOrganizerState.categories[0]?.id || "";
}
scheduleOrganizerStateSave();
setOrganizerFeedback("分类已删除。");
}
function isCategoryExpanded(categoryId) {
return Boolean(chatOrganizerState.expandedCategoryIds[categoryId]);
}
function toggleCategoryExpanded(categoryId) {
chatOrganizerState.expandedCategoryIds[categoryId] = !isCategoryExpanded(categoryId);
renderChatOrganizer();
}
function toggleVisibleChatsExpanded() {
chatOrganizerState.visibleChatsExpanded = !chatOrganizerState.visibleChatsExpanded;
renderChatOrganizer();
}
function toggleCreatePanel() {
chatOrganizerState.createPanelOpen = !chatOrganizerState.createPanelOpen;
if (chatOrganizerState.createPanelOpen) {
chatOrganizerState.addPanelOpen = false;
}
renderChatOrganizer();
}
function toggleAddPanel() {
chatOrganizerState.addPanelOpen = !chatOrganizerState.addPanelOpen;
if (chatOrganizerState.addPanelOpen) {
chatOrganizerState.createPanelOpen = false;
}
renderChatOrganizer();
}
function normalizeImportedCategories(rawCategories) {
if (!Array.isArray(rawCategories)) return [];
return rawCategories
.map(category => {
const name = String(category?.name || "").trim();
if (!name) return null;
const chatIds = Array.isArray(category?.chatIds)
? Array.from(new Set(category.chatIds.map(chatId => normalizeChatId(chatId)).filter(Boolean)))
: [];
return {
id: String(category?.id || createCategoryId()),
name,
chatIds
};
})
.filter(Boolean);
}
function normalizeImportedChatCache(rawChatCache) {
if (!rawChatCache || typeof rawChatCache !== "object") return {};
const nextCache = {};
Object.entries(rawChatCache).forEach(([rawChatId, meta]) => {
const chatId = normalizeChatId(rawChatId);
if (!chatId) return;
nextCache[chatId] = {
id: chatId,
name: String(meta?.name || chatId),
avatarUrl: String(meta?.avatarUrl || ""),
updatedAt: Number(meta?.updatedAt || Date.now())
};
});
return nextCache;
}
function exportChatCategories() {
const payload = {
version: 1,
exportedAt: new Date().toISOString(),
categories: chatOrganizerState.categories,
chatCache: chatOrganizerState.chatCache
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `teams-chat-categories-${timestamp}.json`;
anchor.click();
URL.revokeObjectURL(url);
setOrganizerFeedback("分类已导出。");
}
async function importChatCategoriesFromFile(file) {
if (!file) return;
try {
const content = await file.text();
const parsed = JSON.parse(content);
const importedCategories = normalizeImportedCategories(parsed?.categories);
const importedChatCache = normalizeImportedChatCache(parsed?.chatCache);
if (!importedCategories.length) {
setOrganizerFeedback("导入失败:文件里没有有效分类。");
return;
}
chatOrganizerState.categories = importedCategories;
chatOrganizerState.chatCache = {
...chatOrganizerState.chatCache,
...importedChatCache
};
chatOrganizerState.expandedCategoryIds = {};
importedCategories.forEach(category => {
chatOrganizerState.expandedCategoryIds[category.id] = true;
});
chatOrganizerState.selectedCategoryId = importedCategories[0]?.id || "";
scheduleOrganizerStateSave();
setOrganizerFeedback(`已导入 ${importedCategories.length} 个分类。`);
} catch (error) {
console.error("Teams Alias: 导入分类失败", error);
setOrganizerFeedback("导入失败:文件格式不是有效的 JSON。");
}
}
function renderChatCard(chatId, categoryId) {
const meta = getChatMeta(chatId) || { id: chatId, name: chatId, avatarUrl: "" };
const avatar = meta.avatarUrl
? `<img src="${escapeHtml(meta.avatarUrl)}" alt="" class="teams-alias-chat-avatar-image">`
: `<span class="teams-alias-chat-avatar-fallback">${escapeHtml(inferAvatarFallback(meta.name))}</span>`;
return `
<div class="teams-alias-chat-card">
<button class="teams-alias-chat-open" data-action="open-chat" data-chat-id="${escapeHtml(chatId)}">
<span class="teams-alias-chat-avatar">${avatar}</span>
<span class="teams-alias-chat-main">
<span class="teams-alias-chat-name">${escapeHtml(meta.name || chatId)}</span>
<span class="teams-alias-chat-id">${escapeHtml(chatId)}</span>
</span>
</button>
<button class="teams-alias-chat-remove" data-action="remove-chat" data-category-id="${escapeHtml(categoryId)}" data-chat-id="${escapeHtml(chatId)}">移除</button>
</div>
`;
}
function renderVisibleChatCandidate(chatId) {
const meta = chatOrganizerState.visibleChats[chatId];
if (!meta) return "";
const avatar = meta.avatarUrl
? `<img src="${escapeHtml(meta.avatarUrl)}" alt="" class="teams-alias-chat-avatar-image">`
: `<span class="teams-alias-chat-avatar-fallback">${escapeHtml(inferAvatarFallback(meta.name))}</span>`;
return `
<div class="teams-alias-visible-chat">
<span class="teams-alias-chat-avatar">${avatar}</span>
<span class="teams-alias-chat-main">
<span class="teams-alias-chat-name">${escapeHtml(meta.name || chatId)}</span>
<span class="teams-alias-chat-id">${escapeHtml(chatId)}</span>
</span>
<button data-action="quick-add" data-chat-id="${escapeHtml(chatId)}">加入分类</button>
</div>
`;
}
function ensureChatOrganizerUI() {
if (!document.body || getOrganizerRoot()) return;
if (!document.getElementById(CHAT_ORGANIZER_STYLE_ID)) {
const style = document.createElement("style");
style.id = CHAT_ORGANIZER_STYLE_ID;
style.textContent = `
#${CHAT_ORGANIZER_BUTTON_ID} {
position: fixed;
top: 12px;
left: 12px;
z-index: 2147483644;
border: none;
border-radius: 999px;
background: #2563eb;
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 10px 14px;
cursor: pointer;
box-shadow: 0 10px 30px rgba(37, 99, 235, 0.25);
}
#${CHAT_ORGANIZER_ROOT_ID} {
position: fixed;
inset: 0;
z-index: 2147483643;
display: none;
}
#${CHAT_ORGANIZER_ROOT_ID}.is-open {
display: block;
}
.teams-alias-overlay {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.4);
backdrop-filter: blur(3px);
}
.teams-alias-modal {
position: absolute;
top: 64px;
left: 24px;
width: min(920px, calc(100vw - 48px));
max-height: calc(100vh - 88px);
overflow: auto;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.22);
color: #0f172a;
font-family: "Segoe UI", sans-serif;
}
.teams-alias-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 20px 12px;
border-bottom: 1px solid #e5e7eb;
}
.teams-alias-modal-header h2 {
margin: 0;
font-size: 18px;
}
.teams-alias-close {
border: none;
background: #e2e8f0;
color: #0f172a;
border-radius: 999px;
width: 32px;
height: 32px;
cursor: pointer;
}
.teams-alias-modal-body {
padding: 16px 20px 20px;
display: grid;
gap: 16px;
}
.teams-alias-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 16px 20px 0;
}
.teams-alias-toolbar-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.teams-alias-panel {
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 14px;
background: #f8fafc;
}
.teams-alias-panel h3 {
margin: 0 0 12px;
font-size: 14px;
}
.teams-alias-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.teams-alias-row input,
.teams-alias-row select {
min-width: 180px;
flex: 1;
border: 1px solid #cbd5e1;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
background: #fff;
}
.teams-alias-row button,
.teams-alias-visible-chat button,
.teams-alias-chat-remove {
border: none;
border-radius: 10px;
padding: 9px 12px;
font-size: 13px;
cursor: pointer;
background: #2563eb;
color: #fff;
}
.teams-alias-primary-button {
border: none;
border-radius: 10px;
padding: 9px 14px;
font-size: 13px;
cursor: pointer;
background: #2563eb;
color: #fff;
font-weight: 600;
}
.teams-alias-secondary-button {
border: 1px solid #cbd5e1;
border-radius: 10px;
padding: 9px 14px;
font-size: 13px;
cursor: pointer;
background: #fff;
color: #0f172a;
font-weight: 600;
}
.teams-alias-category-list,
.teams-alias-visible-list,
.teams-alias-chat-list {
display: grid;
gap: 10px;
}
.teams-alias-category-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
border: 1px solid #dbeafe;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.teams-alias-section-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.teams-alias-section-header h3 {
margin: 0;
}
.teams-alias-ghost-button,
.teams-alias-category-actions button {
border: 1px solid #cbd5e1;
border-radius: 10px;
padding: 8px 12px;
font-size: 12px;
background: #fff;
color: #0f172a;
cursor: pointer;
}
.teams-alias-category-actions {
display: flex;
gap: 8px;
align-items: center;
}
.teams-alias-category-meta {
display: grid;
gap: 4px;
}
.teams-alias-category-name {
font-size: 14px;
font-weight: 600;
}
.teams-alias-category-count,
.teams-alias-chat-id,
.teams-alias-feedback {
font-size: 12px;
color: #475569;
}
.teams-alias-chat-card,
.teams-alias-visible-chat {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 10px;
background: #fff;
}
.teams-alias-chat-open {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
border: none;
padding: 0;
background: transparent;
cursor: pointer;
color: inherit;
text-align: left;
}
.teams-alias-chat-avatar {
width: 36px;
height: 36px;
border-radius: 999px;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
background: #dbeafe;
color: #1d4ed8;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.teams-alias-chat-avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.teams-alias-chat-main {
min-width: 0;
display: grid;
gap: 3px;
}
.teams-alias-chat-name {
font-size: 13px;
font-weight: 600;
color: #0f172a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.teams-alias-empty {
color: #64748b;
font-size: 13px;
}
.teams-alias-collapsed {
display: none;
}
@media (max-width: 900px) {
.teams-alias-modal {
left: 12px;
right: 12px;
width: auto;
top: 56px;
}
.teams-alias-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
`;
document.head.appendChild(style);
}
const organizerButton = document.createElement("button");
organizerButton.id = CHAT_ORGANIZER_BUTTON_ID;
organizerButton.textContent = "聊天分类";
organizerButton.addEventListener("click", () => {
chatOrganizerState.isModalOpen = true;
renderChatOrganizer();
});
const root = document.createElement("div");
root.id = CHAT_ORGANIZER_ROOT_ID;
root.addEventListener("click", event => {
const actionElement = event.target.closest("[data-action]");
if (event.target.classList.contains("teams-alias-overlay")) {
chatOrganizerState.isModalOpen = false;
renderChatOrganizer();
return;
}
if (!actionElement) return;
const action = actionElement.getAttribute("data-action");
if (action === "close-modal") {
chatOrganizerState.isModalOpen = false;
renderChatOrganizer();
return;
}
if (action === "create-category") {
const input = root.querySelector('[data-role="new-category-name"]');
createCategory(input?.value || "");
chatOrganizerState.draftCategoryName = "";
chatOrganizerState.createPanelOpen = false;
return;
}
if (action === "delete-category") {
deleteCategory(actionElement.getAttribute("data-category-id"));
return;
}
if (action === "toggle-create-panel") {
toggleCreatePanel();
return;
}
if (action === "toggle-add-panel") {
toggleAddPanel();
return;
}
if (action === "export-categories") {
exportChatCategories();
return;
}
if (action === "trigger-import-categories") {
root.querySelector('[data-role="import-categories-input"]')?.click();
return;
}
if (action === "toggle-category") {
toggleCategoryExpanded(actionElement.getAttribute("data-category-id"));
return;
}
if (action === "toggle-visible-chats") {
toggleVisibleChatsExpanded();
return;
}
if (action === "add-chat-by-id") {
const input = root.querySelector('[data-role="chat-id-input"]');
addChatToCategory(chatOrganizerState.selectedCategoryId, input?.value || "");
chatOrganizerState.draftChatId = "";
renderChatOrganizer();
return;
}
if (action === "quick-add") {
addChatToCategory(chatOrganizerState.selectedCategoryId, actionElement.getAttribute("data-chat-id"));
return;
}
if (action === "remove-chat") {
removeChatFromCategory(
actionElement.getAttribute("data-category-id"),
actionElement.getAttribute("data-chat-id")
);
return;
}
if (action === "open-chat") {
const chatId = actionElement.getAttribute("data-chat-id");
const opened = openChatById(chatId);
setOrganizerFeedback(opened ? "已尝试打开聊天。" : "未找到该聊天,且 deep link 跳转失败。");
}
});
root.addEventListener("change", event => {
const target = event.target;
if (target.matches('[data-role="category-select"]')) {
chatOrganizerState.selectedCategoryId = target.value;
renderChatOrganizer();
return;
}
if (target.matches('[data-role="import-categories-input"]')) {
const file = target.files?.[0];
importChatCategoriesFromFile(file);
target.value = "";
}
});
root.addEventListener("input", event => {
const target = event.target;
if (target.matches('[data-role="new-category-name"]')) {
chatOrganizerState.draftCategoryName = target.value;
return;
}
if (target.matches('[data-role="chat-id-input"]')) {
chatOrganizerState.draftChatId = target.value;
}
});
document.body.appendChild(organizerButton);
document.body.appendChild(root);
renderChatOrganizer();
}
function renderChatOrganizer() {
const root = getOrganizerRoot();
if (!root) return;
root.classList.toggle("is-open", chatOrganizerState.isModalOpen);
if (!chatOrganizerState.isModalOpen) {
root.innerHTML = "";
return;
}
const categoryOptions = chatOrganizerState.categories.map(category => `
<option value="${escapeHtml(category.id)}" ${category.id === chatOrganizerState.selectedCategoryId ? "selected" : ""}>
${escapeHtml(category.name)}
</option>
`).join("");
const categoriesMarkup = chatOrganizerState.categories.length
? chatOrganizerState.categories.map(category => `
<div class="teams-alias-category-item">
<div class="teams-alias-category-meta">
<span class="teams-alias-category-name">${escapeHtml(category.name)}</span>
<span class="teams-alias-category-count">${category.chatIds.length} 个聊天</span>
</div>
<div class="teams-alias-category-actions">
<button data-action="toggle-category" data-category-id="${escapeHtml(category.id)}">${isCategoryExpanded(category.id) ? "收起" : "展开"}</button>
<button data-action="delete-category" data-category-id="${escapeHtml(category.id)}">删除分类</button>
</div>
</div>
<div class="teams-alias-chat-list ${isCategoryExpanded(category.id) ? "" : "teams-alias-collapsed"}">
${category.chatIds.length ? category.chatIds.map(chatId => renderChatCard(chatId, category.id)).join("") : '<div class="teams-alias-empty">当前分类还没有聊天。</div>'}
</div>
`).join("")
: '<div class="teams-alias-empty">还没有分类,先创建一个。</div>';
const visibleChatIds = Object.keys(chatOrganizerState.visibleChats);
const visibleChatsMarkup = visibleChatIds.length
? visibleChatIds.map(chatId => renderVisibleChatCandidate(chatId)).join("")
: '<div class="teams-alias-empty">当前页面还没有识别到左侧聊天列表。</div>';
root.innerHTML = `
<div class="teams-alias-overlay"></div>
<div class="teams-alias-modal" role="dialog" aria-modal="true" aria-label="聊天分类管理">
<div class="teams-alias-modal-header">
<h2>聊天分类管理</h2>
<button class="teams-alias-close" data-action="close-modal">×</button>
</div>
<div class="teams-alias-toolbar">
<div class="teams-alias-toolbar-actions">
<button class="teams-alias-primary-button" data-action="toggle-create-panel">${chatOrganizerState.createPanelOpen ? "收起新建" : "新建分类"}</button>
<button class="teams-alias-primary-button" data-action="toggle-add-panel">${chatOrganizerState.addPanelOpen ? "收起添加" : "添加聊天"}</button>
<button class="teams-alias-secondary-button" data-action="export-categories">导出分类</button>
<button class="teams-alias-secondary-button" data-action="trigger-import-categories">导入分类</button>
</div>
<input type="file" accept="application/json,.json" data-role="import-categories-input" class="teams-alias-collapsed">
</div>
<div class="teams-alias-modal-body">
<div class="teams-alias-panel ${chatOrganizerState.createPanelOpen ? "" : "teams-alias-collapsed"}">
<h3>新建分类</h3>
<div class="teams-alias-row">
<input type="text" placeholder="例如:重点客户 / 项目群" data-role="new-category-name" value="${escapeHtml(chatOrganizerState.draftCategoryName)}">
<button data-action="create-category">创建</button>
</div>
</div>
<div class="teams-alias-panel ${chatOrganizerState.addPanelOpen ? "" : "teams-alias-collapsed"}">
<div class="teams-alias-section-header">
<h3>添加聊天</h3>
<button class="teams-alias-ghost-button" data-action="toggle-visible-chats">${chatOrganizerState.visibleChatsExpanded ? "收起识别列表" : "展开识别列表"}</button>
</div>
<div class="teams-alias-row">
<select data-role="category-select">
<option value="">请选择分类</option>
${categoryOptions}
</select>
<input type="text" placeholder="输入 19: 开头的聊天 ID" data-role="chat-id-input" value="${escapeHtml(chatOrganizerState.draftChatId)}">
<button data-action="add-chat-by-id">按 ID 添加</button>
</div>
<div class="teams-alias-feedback">${escapeHtml(chatOrganizerState.feedback || "点击聊天卡片会在原 Teams 界面中打开会话。")}</div>
<div class="teams-alias-visible-list ${chatOrganizerState.visibleChatsExpanded ? "" : "teams-alias-collapsed"}">${visibleChatsMarkup}</div>
</div>
<div class="teams-alias-panel">
<div class="teams-alias-section-header">
<h3>分类内容</h3>
<span class="teams-alias-feedback">${chatOrganizerState.categories.length} 个分类</span>
</div>
<div class="teams-alias-category-list">${categoriesMarkup}</div>
</div>
</div>
</div>
`;
}
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 rawId = id.replace(PERSON_ID_PREFIX, "");
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
if (!existingBtn) {
const rect = el.getBoundingClientRect();
const button = document.createElement('button');
button.textContent = '显示ID'; // 改为显示ID
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 = '#6c757d'; // 灰色,表示只是信息查看
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', () => {
// 弹窗显示 ID方便复制
prompt("该用户的账号ID为 (请复制):", rawId);
});
document.body.appendChild(button);
}
// 应用别名(异步)
getAlias(rawId).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;
// 向上查找 4 个父元素
for (let i = 0; i < 4; i++) {
if (parent.parentElement) {
parent = parent.parentElement;
} else {
return; // 如果不足4层就跳过
}
}
// 获取后一个兄弟元素
const nextSibling = parent.nextElementSibling;
if (!nextSibling) return;
// 向下查找第 7 个子元素(层级式)
let target = nextSibling;
for (let i = 0; i < 7; i++) {
if (target.children.length > 0) {
target = target.children[0]; // 每层往下取第一个子元素
} else {
return; // 不足7层跳过
}
}
// 判断符合才修改
if (target.id.startsWith('title-chat-list-item')) {
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));
ensureChatOrganizerUI();
scanVisibleChats();
}
// 初始化逻辑
function init() {
loadChatOrganizerState().then(() => {
ensureChatOrganizerUI();
scanVisibleChats();
});
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,
});
// 自动同步检查
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 秒再扫一次(避免漏掉异步更新)
// setInterval(() => {
// applyToAll();
// }, 2000);
}
init();