Skip to content
Merged
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
36 changes: 36 additions & 0 deletions app/admin-api/notifications/bark/test/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getAdminSessionErrorResponse } from '@/lib/server/admin/session';
import { sendBarkNotification } from '@/lib/server/domain/credential-expiry-notifications';
import { getJsonBody } from '@/lib/server/shared/http';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

export const POST = async (request: Request): Promise<Response> => {
const authError = await getAdminSessionErrorResponse(request);

if (authError) return authError;

const body = await getJsonBody<{ token?: unknown }>(request);
const token = typeof body.token === 'string' ? body.token.trim() : '';

if (!token) {
return Response.json({ error: 'Bark token is required' }, { status: 400 });
}

try {
await sendBarkNotification({
body: '这是一条 CodeBuddy2API Bark 测试通知。',
title: 'CodeBuddy2API Bark 测试',
token,
});
return Response.json({ message: 'Test notification sent.' });
} catch (error) {
return Response.json(
{
error:
error instanceof Error ? error.message : 'Bark notification failed',
},
{ status: 502 },
);
}
};
4 changes: 2 additions & 2 deletions app/page-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ interface PollAuthResponse {

interface SettingsResponse {
labels?: Record<string, string>;
settings?: Record<string, string | number | null>;
settings?: Record<string, string | number | boolean | null>;
}

interface DebugResponse {
Expand Down Expand Up @@ -1414,7 +1414,7 @@ const AdminPageLayoutContent = ({

const result = await requestJson<{
message?: string;
settings?: Record<string, string | number | null>;
settings?: Record<string, string | number | boolean | null>;
}>('/admin-api/settings', {
body: JSON.stringify({
settings: settings.values,
Expand Down
128 changes: 126 additions & 2 deletions app/settings/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client';

import { Block, Flexbox, Input, TextArea } from '@lobehub/ui';
import { Button, Select } from '@lobehub/ui/base-ui';
import { Button, Select, Switch } from '@lobehub/ui/base-ui';
import { Table } from 'antd';
import type { TableColumnsType } from 'antd';
import { atom } from 'jotai';
import {
Layers3,
BellRing,
LoaderCircle,
RefreshCw,
Save,
Expand Down Expand Up @@ -67,7 +68,7 @@ export const createSettingsState = ({
};

export interface SettingsController {
onChange: (key: string, value: string) => void;
onChange: (key: string, value: string | boolean) => void;
onSave: () => void;
settings: SettingsState;
}
Expand Down Expand Up @@ -107,6 +108,10 @@ const settingsSelectOptions: Record<
{ label: 'true', value: 'true' },
{ label: 'false', value: 'false' },
],
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: [
{ label: 'true', value: 'true' },
{ label: 'false', value: 'false' },
],
};

/**
Expand Down Expand Up @@ -183,6 +188,11 @@ const DESKTOP_HIDDEN_CONFIG_KEYS = new Set([
'CODEBUDDY_ADMIN_PASSKEY_RP_ID',
]);

const CUSTOM_CONFIG_KEYS = new Set([
'CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED',
'CODEBUDDY_BARK_TOKEN',
]);

/**
* Maps a setting key to its helper text.
*
Expand Down Expand Up @@ -254,6 +264,114 @@ const SettingField = ({
);
};

const CredentialExpiryNotificationSettings = ({
onChange,
settings,
translations,
}: {
onChange: (key: string, value: string | boolean) => void;
settings: SettingsState;
translations: (key: string) => string;
}) => {
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<'success' | 'error' | null>(
null,
);
const token = String(settings.values.CODEBUDDY_BARK_TOKEN ?? '');
const enabled =
settings.values.CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED ===
true ||
settings.values.CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED ===
'true';

const testNotification = async () => {
setTesting(true);
setTestResult(null);

try {
const response = await fetch('/admin-api/notifications/bark/test', {
body: JSON.stringify({ token }),
headers: { 'Content-Type': 'application/json' },
method: 'POST',
});
setTestResult(response.ok ? 'success' : 'error');
} catch {
setTestResult('error');
} finally {
setTesting(false);
}
};

return (
<Block direction="vertical" gap={16} padding={24} variant="outlined">
<Flexbox align="center" gap={8} horizontal>
<BellRing size={18} strokeWidth={2} />
<h3 className="section-title">
{translations('settingsPanel.credentialExpiryNotificationsTitle')}
</h3>
</Flexbox>
<p className="text-secondary">
{translations('settingsPanel.credentialExpiryNotificationsDescription')}
</p>
<Flexbox align="center" distribution="space-between" horizontal>
<span className="font-medium">
{translations('settingsPanel.credentialExpiryNotificationsEnabled')}
</span>
<Switch
checked={enabled}
onChange={(checked) =>
onChange(
'CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED',
checked,
)
}
/>
</Flexbox>
<div>
<label
className="mb-2 block whitespace-normal break-words font-medium text-text-light dark:text-text-dark"
htmlFor="CODEBUDDY_BARK_TOKEN"
>
{translations('settingsPanel.barkToken')}
</label>
<Input
id="CODEBUDDY_BARK_TOKEN"
onChange={(event) =>
onChange('CODEBUDDY_BARK_TOKEN', event.target.value)
}
placeholder={translations('settingsPanel.barkTokenPlaceholder')}
type="password"
value={token}
/>
<p className="mt-2 text-secondary">
<a href="https://bark.day.app/" rel="noreferrer" target="_blank">
{translations('settingsPanel.barkLink')}
</a>
</p>
</div>
<Flexbox align="center" gap={12} horizontal wrap="wrap">
<Button
disabled={testing}
loading={testing}
onClick={() => void testNotification()}
>
{translations('settingsPanel.testBark')}
</Button>
{testResult === 'success' ? (
<span className="text-secondary">
{translations('settingsPanel.testBarkSuccess')}
</span>
) : null}
{testResult === 'error' ? (
<span className="text-secondary">
{translations('settingsPanel.testBarkFailed')}
</span>
) : null}
</Flexbox>
</Block>
);
};

/**
* The settings one backend needs, rendered under the picker that selected it.
*
Expand Down Expand Up @@ -709,6 +827,7 @@ const Settings = ({
// A backend's own settings are rendered by its picker, which
// knows which backend they belong to.
.filter(([settingKey]) => !BACKEND_CONFIG_KEYS.has(settingKey))
.filter(([settingKey]) => !CUSTOM_CONFIG_KEYS.has(settingKey))
.filter(([settingKey]) =>
desktop ? !DESKTOP_HIDDEN_CONFIG_KEYS.has(settingKey) : true,
)
Expand Down Expand Up @@ -763,6 +882,11 @@ const Settings = ({
</Button>
</Flexbox>
</Block>
<CredentialExpiryNotificationSettings
onChange={onChange}
settings={settings}
translations={translations}
/>
<Desktop />
<CredentialModels />
<Block direction="vertical" gap={16} padding={24} variant="outlined">
Expand Down
7 changes: 6 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,12 @@ const writeConsolePreferences = async (cookies: Cookie[]): Promise<void> => {

refreshMenus();

if (mainWindow && !mainWindow.isDestroyed() && consoleOrigin) {
if (
mainWindow &&
!mainWindow.isDestroyed() &&
consoleOrigin &&
(backend.mode === 'remote' || gateway !== null)
) {
mainWindow.reload();
}
};
Expand Down
3 changes: 3 additions & 0 deletions instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ export const register = async (): Promise<void> => {
await import('@/lib/server/domain/credential-models');
const { startAutoCheckinScheduler } =
await import('@/lib/server/domain/auto-checkin-scheduler');
const { startCredentialExpiryNotificationScheduler } =
await import('@/lib/server/domain/credential-expiry-notifications');

void refreshMissingCredentialModels();

// Only in the server process: a build or an edge runtime must not start a
// timer that would outlive the request it belongs to.
startAutoCheckinScheduler();
startCredentialExpiryNotificationScheduler();
};
20 changes: 20 additions & 0 deletions lib/server/domain/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface RuntimeConfig {
CODEBUDDY_INTERNET_ENVIRONMENT: 'ioa' | 'internal' | 'public';
CODEBUDDY_LOG_LEVEL: string;
CODEBUDDY_API_TIMEOUT_MINUTES: number;
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: boolean;
CODEBUDDY_BARK_TOKEN: string;
CODEBUDDY_WEB_SEARCH_BACKEND: string;
CODEBUDDY_WEB_FETCH_BACKEND: string;
CODEBUDDY_SEARXNG_URL: string;
Expand Down Expand Up @@ -68,6 +70,8 @@ const DEFAULT_CONFIG: RuntimeConfig = {
CODEBUDDY_INTERNET_ENVIRONMENT: 'ioa',
CODEBUDDY_LOG_LEVEL: 'INFO',
CODEBUDDY_API_TIMEOUT_MINUTES: DEFAULT_API_TIMEOUT_MINUTES,
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: false,
CODEBUDDY_BARK_TOKEN: '',
CODEBUDDY_WEB_SEARCH_BACKEND: 'searxng',
CODEBUDDY_WEB_FETCH_BACKEND: 'codebuddy',
CODEBUDDY_SEARXNG_URL: '',
Expand Down Expand Up @@ -96,6 +100,9 @@ const SETTING_LABELS_BY_LOCALE: Record<
CODEBUDDY_INTERNET_ENVIRONMENT: 'Network environment (internal/ioa/public)',
CODEBUDDY_LOG_LEVEL: 'Log level',
CODEBUDDY_API_TIMEOUT_MINUTES: 'API timeout, first token (minutes)',
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED:
'Credential expiry notifications',
CODEBUDDY_BARK_TOKEN: 'Bark device key',
CODEBUDDY_WEB_SEARCH_BACKEND: 'Web search backend',
CODEBUDDY_WEB_FETCH_BACKEND: 'Web fetch backend',
CODEBUDDY_SEARXNG_URL: 'SearXNG instance address (with port)',
Expand All @@ -119,6 +126,8 @@ const SETTING_LABELS_BY_LOCALE: Record<
CODEBUDDY_INTERNET_ENVIRONMENT: 'ネットワーク環境 (internal/ioa/public)',
CODEBUDDY_LOG_LEVEL: 'ログレベル',
CODEBUDDY_API_TIMEOUT_MINUTES: 'API タイムアウト・最初のトークン (分)',
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: '認証情報の有効期限通知',
CODEBUDDY_BARK_TOKEN: 'Bark デバイスキー',
CODEBUDDY_WEB_SEARCH_BACKEND: 'Web 検索バックエンド',
CODEBUDDY_WEB_FETCH_BACKEND: 'Web フェッチバックエンド',
CODEBUDDY_SEARXNG_URL: 'SearXNG インスタンスのアドレス(ポートを含む)',
Expand All @@ -141,6 +150,8 @@ const SETTING_LABELS_BY_LOCALE: Record<
CODEBUDDY_INTERNET_ENVIRONMENT: '网络环境 (internal/ioa/public)',
CODEBUDDY_LOG_LEVEL: '日志级别',
CODEBUDDY_API_TIMEOUT_MINUTES: 'API 超时时间,首个 token(分钟)',
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: '凭据过期通知',
CODEBUDDY_BARK_TOKEN: 'Bark Token',
CODEBUDDY_WEB_SEARCH_BACKEND: 'WebSearch 后端',
CODEBUDDY_WEB_FETCH_BACKEND: 'WebFetch 后端',
CODEBUDDY_SEARXNG_URL: 'SearXNG 实例地址(含端口)',
Expand Down Expand Up @@ -287,6 +298,15 @@ export const getActiveConfig = async (): Promise<RuntimeConfig> => {
persisted.CODEBUDDY_API_TIMEOUT_MINUTES ??
process.env.CODEBUDDY_API_TIMEOUT_MINUTES,
),
CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED: normalizeValue(
'CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED',
persisted.CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED ??
process.env.CODEBUDDY_CREDENTIAL_EXPIRY_NOTIFICATIONS_ENABLED,
),
CODEBUDDY_BARK_TOKEN: normalizeValue(
'CODEBUDDY_BARK_TOKEN',
persisted.CODEBUDDY_BARK_TOKEN ?? process.env.CODEBUDDY_BARK_TOKEN,
),
CODEBUDDY_WEB_SEARCH_BACKEND: normalizeValue(
'CODEBUDDY_WEB_SEARCH_BACKEND',
persisted.CODEBUDDY_WEB_SEARCH_BACKEND ??
Expand Down
Loading
Loading