Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/docker-profile.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ test('secure defaults: loopback port, non-root user, no secret in the image, no
assert.match(compose, /^ {6}- "127\.0\.0\.1:\$\{FLYBOT_PORT:-3080\}:3080"$/m, 'the port is published on the host loopback only')
assert.match(compose, /^ {6}- data:\/data$/m, 'one data volume')
assert.match(compose, /^ {6}- DEEPSEEK_API_KEY$/m, 'the key is passed through, never written into the file')
for (const name of ['FLYBOT_SMTP_URL', 'FLYBOT_EMAIL_TO', 'FLYBOT_URL']) assert.match(compose, new RegExp(`^ {6}- ${name}$`, 'm'), `${name} is passed through, never written into the file (#355)`)
const dockerfile = read('Dockerfile')
assert.match(dockerfile, /^USER node$/m)
assert.match(dockerfile, /^VOLUME \["\/data"\]$/m)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/355-email-to-self.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FlyBot can email you, at your own address, when one of your Bots finishes, fails or waits for your approval: a short line plus a link back into FlyBot, at most one email every five minutes, with no chat or tool-call content. You approve only in FlyBot; replies to the email are not read. Set your own SMTP account with `FLYBOT_SMTP_URL`, `FLYBOT_EMAIL_TO` and `FLYBOT_URL` (see the [Docker guide](docs/docker.md)), then switch on Settings → General → **Email me**, which is off by default ([#355](https://github.com/ThinkFlowLab/FlyBot/issues/355)).
4 changes: 4 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ services:
# Passed only when set in your shell or in .env next to this file; otherwise save the
# key in the app (it is kept on the data volume).
- DEEPSEEK_API_KEY
# Email to yourself (#355), optional; set in .env, see docs/docker.md.
- FLYBOT_SMTP_URL
- FLYBOT_EMAIL_TO
- FLYBOT_URL
volumes:
- data:/data
volumes:
Expand Down
15 changes: 13 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,9 @@ var ZH = {
"Once more if it still waits after 15 min.": "等了 15 分钟还没批准时再提醒一次。",
Notifications: "通知",
"When your Bots finish, fail, or wait for your approval while you are in another chat or app.": "你的 Bot 完成、失败或等你批准时提醒你(你在别的对话或应用里时)。",
"Email me": "发邮件给我",
"The same news by email to {address}: a short line and a link back, at most every five minutes. Approve only in FlyBot.": "同样的提醒也发邮件到 {address}:一句话加一个回到 FlyBot 的链接,最多每五分钟一封。批准只能在 FlyBot 里进行。",
"Needs email set up on the server (see the Docker guide).": "需要先在服务器上设置邮件(见 Docker 指南)。",
"{first} and {second} are waiting for your approval": "{first}、{second} 等你批准",
"{first}, {second} and {count} more are waiting for your approval": "{first}、{second} 等 {total} 个等你批准",
"Waiting for your approval": "等你批准",
Expand Down Expand Up @@ -13873,7 +13876,7 @@ function GeneralPage({ roster, actions, run }) {
)
)
),
// One switch for every system notification (#353), then one per kind (#469).
// One switch for every system notification (#353), then one per kind (#469), then Email me (#355).
(0, import_react33.createElement)(
SetSection,
{ key: "notify", title: t("Notifications") },
Expand All @@ -13891,7 +13894,15 @@ function GeneralPage({ roster, actions, run }) {
disabled: prefs.notifications === false,
on: prefs.notify?.[kind] !== false,
onChange: (on) => run(actions.setPrefs({ notify: { [kind]: on } }))
}))
})),
// Email to yourself (#355): off by default, and only where the server has email set up.
(0, import_react33.createElement)(SwitchRow, {
label: t("Email me"),
hint: roster.email?.to ? t("The same news by email to {address}: a short line and a link back, at most every five minutes. Approve only in FlyBot.", { address: roster.email.to }) : t("Needs email set up on the server (see the Docker guide)."),
on: Boolean(roster.email?.to) && prefs.email === true,
disabled: !roster.email?.to,
onChange: (on) => run(actions.setPrefs({ email: on }))
})
)
];
}
Expand Down
3 changes: 2 additions & 1 deletion core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as tools from './src/host/tools.js'
import * as chatIndex from './src/host/chat-index.js'
import * as groups from './src/host/groups.js'
import * as approvals from './src/host/approvals.js'
import * as email from './src/host/email.js'
import * as resume from './src/host/resume.js'
import * as memory from './src/host/memory.js'
import * as parts from './src/host/parts.js'
Expand Down Expand Up @@ -49,7 +50,7 @@ export const inject = ['llm', 'tools', 'systemPrompt', 'sessionController', 'ses
// the functions other modules call; they call them through `rt` at run time, never during
// install. Listeners run and tools are listed in registration order, so this order holds:
// usage meters model requests after every listener that rewrites them.
const MODULES = [diagnostics, store, memberAuth, authz, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, chatIndex, groups, roomMembership, reach, tasks, approvals, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, channel]
const MODULES = [diagnostics, store, memberAuth, authz, models, permissions, turns, delivery, roster, folders, agents, prompt, timeContext, tools, chatIndex, groups, roomMembership, reach, tasks, approvals, email, resume, memory, parts, images, secrets, connectors, connectorAccess, capabilities, schedules, automationDefault, browser, usage, update, dshUpdate, hooks, remoteChat, channel]

export function apply(ctx, config = {}) {
const rt = createRuntime(ctx, config)
Expand Down
3 changes: 3 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,9 @@ const ZH = {
'Once more if it still waits after 15 min.': '等了 15 分钟还没批准时再提醒一次。',
Notifications: '通知',
'When your Bots finish, fail, or wait for your approval while you are in another chat or app.': '你的 Bot 完成、失败或等你批准时提醒你(你在别的对话或应用里时)。',
'Email me': '发邮件给我',
'The same news by email to {address}: a short line and a link back, at most every five minutes. Approve only in FlyBot.': '同样的提醒也发邮件到 {address}:一句话加一个回到 FlyBot 的链接,最多每五分钟一封。批准只能在 FlyBot 里进行。',
'Needs email set up on the server (see the Docker guide).': '需要先在服务器上设置邮件(见 Docker 指南)。',
'{first} and {second} are waiting for your approval': '{first}、{second} 等你批准',
'{first}, {second} and {count} more are waiting for your approval': '{first}、{second} 等 {total} 个等你批准',
'Waiting for your approval': '等你批准',
Expand Down
15 changes: 12 additions & 3 deletions core/src/client/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const NOTIFY_KINDS = [
['finished', 'Finished'],
['failed', 'Failed'],
]
// General is Appearance and Notifications; the switch to classic Agent mode is the account menu's (#393).
// General is Appearance and Notifications (system notifications, email to yourself); the switch to classic Agent mode is the account menu's (#393).
function GeneralPage({ roster, actions, run }) {
const prefs = roster.prefs ?? {}
const themes = allThemes()
Expand All @@ -69,7 +69,7 @@ function GeneralPage({ roster, actions, run }) {
})),
h('label', { className: 'bt-swatch bt-swatch-custom', title: t('Custom accent'), 'data-on': Boolean(prefs.accent) && !ACCENT_INKS.some(ink => INKS[ink][0] === prefs.accent) },
h('input', { type: 'color', value: custom, 'aria-label': t('Custom accent'), onChange: event => pickCustom(event.target.value) }))))),
// One switch for every system notification (#353), then one per kind (#469).
// One switch for every system notification (#353), then one per kind (#469), then Email me (#355).
h(SetSection, { key: 'notify', title: t('Notifications') },
h(SwitchRow, {
label: t('Notifications'), showLabel: false,
Expand All @@ -79,7 +79,16 @@ function GeneralPage({ roster, actions, run }) {
NOTIFY_KINDS.map(([kind, label, hint]) => h(SwitchRow, {
key: kind, label: t(label), hint: hint ? t(hint) : undefined, disabled: prefs.notifications === false,
on: prefs.notify?.[kind] !== false, onChange: on => run(actions.setPrefs({ notify: { [kind]: on } })),
}))),
})),
// Email to yourself (#355): off by default, and only where the server has email set up.
h(SwitchRow, {
label: t('Email me'),
hint: roster.email?.to
? t('The same news by email to {address}: a short line and a link back, at most every five minutes. Approve only in FlyBot.', { address: roster.email.to })
: t('Needs email set up on the server (see the Docker guide).'),
on: Boolean(roster.email?.to) && prefs.email === true, disabled: !roster.email?.to,
onChange: on => run(actions.setPrefs({ email: on })),
})),
]
}

Expand Down
2 changes: 2 additions & 0 deletions core/src/host/approvals.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export function install(rt) {
at: Date.now(),
}
pending.set(id, entry)
// Email to yourself, when switched on (email.js, #355).
rt.noticeOffApp?.('approval', entry)
rt.noteApproval?.(sessionId, { tool: entry.tool, reason: entry.reason.slice(0, 200) })
const settled = () => {
pending.delete(id)
Expand Down
1 change: 1 addition & 0 deletions core/src/host/authz.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export function install(rt) {
agents: alone() ? full.agents : [],
secrets: alone() ? full.secrets : [],
secretRequests: alone() ? full.secretRequests : {},
email: alone() ? full.email : null,
}
}
rt.ownedLive = (full, actor) => {
Expand Down
10 changes: 10 additions & 0 deletions core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export function install(rt) {
permissions: rt.permissionCatalog?.() ?? null,
modelsKey: modelsKey(),
prefs: state.prefs,
// Email to yourself (#355): the address when this server has email set up, else null.
email: rt.emailView?.() ?? null,
// Whether a Desktop window should serve Bot browser reads (see browser.js).
browser: rt.browserEnabled,
// False when config.memory turns memory off: no Memory row or panel.
Expand Down Expand Up @@ -177,6 +179,14 @@ export function install(rt) {
else if (body.notifications === false) state.prefs.notifications = false
else throw invalid('Notifications must be on or off')
}
// Email to yourself is off unless switched on, and only where the server has it set up (#355).
if (body.email !== undefined) {
if (body.email === null || body.email === false) delete state.prefs.email
else if (body.email !== true) throw invalid('Email must be on or off')
else if (!rt.emailView?.()) throw invalid('Email is not set up on this server')
else state.prefs.email = true
rt.emailSwitched?.(body.email === true)
}
// Each kind is on unless switched off on its own; only the off ones are kept (#469).
if (body.notify !== undefined) {
const kinds = body.notify
Expand Down
Loading
Loading