Merge pull request #31 from wagesj45/codex/implement-applyairules-helper-and-context-menu
Add context menu helper to apply AI rules
This commit is contained in:
commit
ac90af149e
3 changed files with 65 additions and 35 deletions
|
@ -20,6 +20,7 @@ message meets a specified criterion.
|
||||||
- **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page.
|
- **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page.
|
||||||
- **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service.
|
- **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service.
|
||||||
- **Automatic rules** – create rules that tag or move new messages based on AI classification.
|
- **Automatic rules** – create rules that tag or move new messages based on AI classification.
|
||||||
|
- **Context menu** – apply AI rules to selected messages from the message list or display.
|
||||||
- **Packaging script** – `build-xpi.ps1` builds an XPI ready for installation.
|
- **Packaging script** – `build-xpi.ps1` builds an XPI ready for installation.
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
@ -100,6 +101,7 @@ Sortana requests the following Thunderbird permissions:
|
||||||
- `messagesUpdate` – change message properties such as tags and junk status.
|
- `messagesUpdate` – change message properties such as tags and junk status.
|
||||||
- `messagesTagsList` – retrieve existing message tags for rule actions.
|
- `messagesTagsList` – retrieve existing message tags for rule actions.
|
||||||
- `accountsRead` – list accounts and folders for move actions.
|
- `accountsRead` – list accounts and folders for move actions.
|
||||||
|
- `menus` – add context menu commands.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,47 @@ async function sha256Hex(str) {
|
||||||
return Array.from(new Uint8Array(buf), b => b.toString(16).padStart(2, '0')).join('');
|
return Array.from(new Uint8Array(buf), b => b.toString(16).padStart(2, '0')).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyAiRules(idsInput) {
|
||||||
|
const ids = Array.isArray(idsInput) ? idsInput : [idsInput];
|
||||||
|
if (!ids.length) return;
|
||||||
|
|
||||||
|
if (!aiRules.length) {
|
||||||
|
const { aiRules: stored } = await browser.storage.local.get("aiRules");
|
||||||
|
aiRules = Array.isArray(stored) ? stored.map(r => {
|
||||||
|
if (r.actions) return r;
|
||||||
|
const actions = [];
|
||||||
|
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
||||||
|
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
||||||
|
return { criterion: r.criterion, actions };
|
||||||
|
}) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const msg of ids) {
|
||||||
|
const id = msg?.id ?? msg;
|
||||||
|
try {
|
||||||
|
const full = await messenger.messages.getFull(id);
|
||||||
|
const text = full?.parts?.[0]?.body || "";
|
||||||
|
for (const rule of aiRules) {
|
||||||
|
const cacheKey = await sha256Hex(`${id}|${rule.criterion}`);
|
||||||
|
const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey);
|
||||||
|
if (matched) {
|
||||||
|
for (const act of (rule.actions || [])) {
|
||||||
|
if (act.type === 'tag' && act.tagKey) {
|
||||||
|
await messenger.messages.update(id, { tags: [act.tagKey] });
|
||||||
|
} else if (act.type === 'move' && act.folder) {
|
||||||
|
await messenger.messages.move([id], act.folder);
|
||||||
|
} else if (act.type === 'junk') {
|
||||||
|
await messenger.messages.update(id, { junk: !!act.junk });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.aiLog("failed to apply AI rules", { level: 'error' }, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
logger = await import(browser.runtime.getURL("logger.js"));
|
logger = await import(browser.runtime.getURL("logger.js"));
|
||||||
try {
|
try {
|
||||||
|
@ -47,6 +88,24 @@ async function sha256Hex(str) {
|
||||||
|
|
||||||
logger.aiLog("background.js loaded – ready to classify", {debug: true});
|
logger.aiLog("background.js loaded – ready to classify", {debug: true});
|
||||||
|
|
||||||
|
browser.menus.create({
|
||||||
|
id: "apply-ai-rules-list",
|
||||||
|
title: "Apply AI Rules",
|
||||||
|
contexts: ["message_list"],
|
||||||
|
});
|
||||||
|
browser.menus.create({
|
||||||
|
id: "apply-ai-rules-display",
|
||||||
|
title: "Apply AI Rules",
|
||||||
|
contexts: ["message_display"],
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.menus.onClicked.addListener(async info => {
|
||||||
|
if (info.menuItemId === "apply-ai-rules-list" || info.menuItemId === "apply-ai-rules-display") {
|
||||||
|
const ids = info.selectedMessages?.ids || (info.messageId ? [info.messageId] : []);
|
||||||
|
await applyAiRules(ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Listen for messages from UI/devtools
|
// Listen for messages from UI/devtools
|
||||||
browser.runtime.onMessage.addListener(async (msg) => {
|
browser.runtime.onMessage.addListener(async (msg) => {
|
||||||
logger.aiLog("onMessage received", {debug: true}, msg);
|
logger.aiLog("onMessage received", {debug: true}, msg);
|
||||||
|
@ -77,40 +136,8 @@ async function sha256Hex(str) {
|
||||||
if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) {
|
if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) {
|
||||||
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
|
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
|
||||||
logger.aiLog("onNewMailReceived", {debug: true}, messages);
|
logger.aiLog("onNewMailReceived", {debug: true}, messages);
|
||||||
if (!aiRules.length) {
|
const ids = (messages?.messages || messages || []).map(m => m.id ?? m);
|
||||||
const { aiRules: stored } = await browser.storage.local.get("aiRules");
|
await applyAiRules(ids);
|
||||||
aiRules = Array.isArray(stored) ? stored.map(r => {
|
|
||||||
if (r.actions) return r;
|
|
||||||
const actions = [];
|
|
||||||
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
|
||||||
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
|
||||||
return { criterion: r.criterion, actions };
|
|
||||||
}) : [];
|
|
||||||
}
|
|
||||||
for (const msg of (messages?.messages || messages || [])) {
|
|
||||||
const id = msg.id ?? msg;
|
|
||||||
try {
|
|
||||||
const full = await messenger.messages.getFull(id);
|
|
||||||
const text = full?.parts?.[0]?.body || "";
|
|
||||||
for (const rule of aiRules) {
|
|
||||||
const cacheKey = await sha256Hex(`${id}|${rule.criterion}`);
|
|
||||||
const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey);
|
|
||||||
if (matched) {
|
|
||||||
for (const act of (rule.actions || [])) {
|
|
||||||
if (act.type === 'tag' && act.tagKey) {
|
|
||||||
await messenger.messages.update(id, {tags: [act.tagKey]});
|
|
||||||
} else if (act.type === 'move' && act.folder) {
|
|
||||||
await messenger.messages.move([id], act.folder);
|
|
||||||
} else if (act.type === 'junk') {
|
|
||||||
await messenger.messages.update(id, {junk: !!act.junk});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.aiLog("failed to classify new mail", {level: 'error'}, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.aiLog("messenger.messages API unavailable, skipping new mail listener", { level: 'warn' });
|
logger.aiLog("messenger.messages API unavailable, skipping new mail listener", { level: 'warn' });
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"messagesMove",
|
"messagesMove",
|
||||||
"messagesUpdate",
|
"messagesUpdate",
|
||||||
"messagesTagsList",
|
"messagesTagsList",
|
||||||
"accountsRead"
|
"accountsRead",
|
||||||
|
"menus"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue