@hienlh/ppm 0.9.39 → 0.9.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/web/assets/{browser-tab-DnIsHiCc.js → browser-tab-Dm65lWLO.js} +1 -1
- package/dist/web/assets/{chat-tab-il6D4jql.js → chat-tab-rKXwCBEZ.js} +1 -1
- package/dist/web/assets/{code-editor-BUc1jBqm.js → code-editor-BD_hxR8Z.js} +1 -1
- package/dist/web/assets/{database-viewer-TjRo2b8_.js → database-viewer-RqbZkczM.js} +1 -1
- package/dist/web/assets/{diff-viewer-BMhCz0xk.js → diff-viewer-C-6EcVDG.js} +1 -1
- package/dist/web/assets/{extension-webview-DiVdlE2r.js → extension-webview-xZQrFpb0.js} +1 -1
- package/dist/web/assets/{git-graph-4eGJ8B1A.js → git-graph-Cndi59vr.js} +1 -1
- package/dist/web/assets/index-BpOBp5oT.js +30 -0
- package/dist/web/assets/keybindings-store-BE5y0cut.js +1 -0
- package/dist/web/assets/{markdown-renderer-IyEzLrC6.js → markdown-renderer-CViGEOCg.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CSynGGkJ.js → postgres-viewer-DjRZKruo.js} +1 -1
- package/dist/web/assets/{settings-tab-BdI4HhRa.js → settings-tab-DQ3eb1bU.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-C5mviyU5.js → sqlite-viewer-B8OuhoEV.js} +1 -1
- package/dist/web/assets/{terminal-tab-CDyC1grg.js → terminal-tab-D7K74k2B.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-DcVicB_i.js → use-monaco-theme-BEWkUA66.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/settings.ts +11 -16
- package/src/services/db.service.ts +6 -0
- package/src/services/telegram-notification.service.ts +44 -21
- package/src/types/config.ts +0 -2
- package/src/web/components/settings/ppmbot-settings-section.tsx +87 -2
- package/src/web/components/settings/settings-tab.tsx +6 -4
- package/dist/web/assets/index-BmcV1di6.js +0 -30
- package/dist/web/assets/keybindings-store--5T5hsAj.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { configService } from "./config.service.ts";
|
|
2
2
|
import { tunnelService } from "./tunnel.service.ts";
|
|
3
3
|
import { getLocalIp } from "../lib/network-utils.ts";
|
|
4
|
+
import { getApprovedPairedChats } from "./db.service.ts";
|
|
4
5
|
import type { TelegramConfig } from "../types/config.ts";
|
|
5
6
|
import type { NotificationPayload } from "./notification.service.ts";
|
|
6
7
|
|
|
@@ -12,12 +13,15 @@ function escapeHtml(str: string): string {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
class TelegramNotificationService {
|
|
15
|
-
/** Send notification to
|
|
16
|
+
/** Send notification to all approved paired chats. No-op if not configured. */
|
|
16
17
|
async send(payload: NotificationPayload): Promise<void> {
|
|
17
18
|
const config = configService.get("telegram") as TelegramConfig | undefined;
|
|
18
|
-
if (!config?.bot_token
|
|
19
|
+
if (!config?.bot_token) return;
|
|
19
20
|
if (!BOT_TOKEN_RE.test(config.bot_token)) return;
|
|
20
21
|
|
|
22
|
+
const approvedChats = getApprovedPairedChats();
|
|
23
|
+
if (approvedChats.length === 0) return;
|
|
24
|
+
|
|
21
25
|
const deviceName = (configService.get("device_name") as string) || "PPM";
|
|
22
26
|
const deepLink = this.buildDeepLink(payload);
|
|
23
27
|
|
|
@@ -27,31 +31,50 @@ class TelegramNotificationService {
|
|
|
27
31
|
text += `\n\n<a href="${deepLink}">Open in PPM</a>`;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
// Send to all approved paired chats in parallel
|
|
35
|
+
await Promise.allSettled(
|
|
36
|
+
approvedChats.map((chat) =>
|
|
37
|
+
this.callApi(config.bot_token, chat.telegram_chat_id, text),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
/** Send a test message. Returns { ok, error? } */
|
|
34
|
-
async sendTest(botToken: string
|
|
42
|
+
/** Send a test message to all approved paired chats. Returns { ok, error? } */
|
|
43
|
+
async sendTest(botToken: string): Promise<{ ok: boolean; error?: string }> {
|
|
35
44
|
if (!BOT_TOKEN_RE.test(botToken)) return { ok: false, error: "Invalid bot token format" };
|
|
45
|
+
|
|
46
|
+
const approvedChats = getApprovedPairedChats();
|
|
47
|
+
if (approvedChats.length === 0) {
|
|
48
|
+
return { ok: false, error: "No approved paired chats. Pair a device in PPMBot settings first." };
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
const deviceName = (configService.get("device_name") as string) || "PPM";
|
|
37
52
|
const text = `<b>${escapeHtml(deviceName)} — Test</b>\nTelegram notifications are working!`;
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
const results = await Promise.allSettled(
|
|
55
|
+
approvedChats.map(async (chat) => {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ chat_id: chat.telegram_chat_id, text, parse_mode: "HTML" }),
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
});
|
|
65
|
+
const json = (await res.json()) as { ok: boolean; description?: string };
|
|
66
|
+
if (!json.ok) throw new Error(json.description || "Unknown error");
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const failed = results.filter((r) => r.status === "rejected");
|
|
74
|
+
if (failed.length === results.length) {
|
|
75
|
+
return { ok: false, error: (failed[0] as PromiseRejectedResult).reason?.message || "All sends failed" };
|
|
54
76
|
}
|
|
77
|
+
return { ok: true };
|
|
55
78
|
}
|
|
56
79
|
|
|
57
80
|
private buildDeepLink(payload: NotificationPayload): string | null {
|
package/src/types/config.ts
CHANGED
|
@@ -6,7 +6,6 @@ export interface PushConfig {
|
|
|
6
6
|
|
|
7
7
|
export interface TelegramConfig {
|
|
8
8
|
bot_token: string;
|
|
9
|
-
chat_id: string;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
export interface PPMBotConfig {
|
|
@@ -99,7 +98,6 @@ export const DEFAULT_CONFIG: PpmConfig = {
|
|
|
99
98
|
},
|
|
100
99
|
telegram: {
|
|
101
100
|
bot_token: "",
|
|
102
|
-
chat_id: "",
|
|
103
101
|
},
|
|
104
102
|
clawbot: {
|
|
105
103
|
enabled: false,
|
|
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import { Switch } from "@/components/ui/switch";
|
|
5
5
|
import { api } from "@/lib/api-client";
|
|
6
|
-
import { Trash2, CheckCircle, Clock } from "lucide-react";
|
|
6
|
+
import { Trash2, CheckCircle, Clock, Send } from "lucide-react";
|
|
7
7
|
|
|
8
8
|
interface PPMBotConfig {
|
|
9
9
|
enabled: boolean;
|
|
@@ -16,6 +16,10 @@ interface PPMBotConfig {
|
|
|
16
16
|
debounce_ms: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
interface TelegramConfig {
|
|
20
|
+
bot_token: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
interface PairedChat {
|
|
20
24
|
id: number;
|
|
21
25
|
telegram_chat_id: string;
|
|
@@ -32,6 +36,11 @@ export function PPMBotSettingsSection() {
|
|
|
32
36
|
const [saving, setSaving] = useState(false);
|
|
33
37
|
const [status, setStatus] = useState<{ type: "ok" | "err"; msg: string } | null>(null);
|
|
34
38
|
|
|
39
|
+
// Bot token (from telegram config)
|
|
40
|
+
const [tokenInput, setTokenInput] = useState("");
|
|
41
|
+
const [tokenConfigured, setTokenConfigured] = useState(false);
|
|
42
|
+
const [tokenSaving, setTokenSaving] = useState(false);
|
|
43
|
+
|
|
35
44
|
const [enabled, setEnabled] = useState(false);
|
|
36
45
|
const [defaultProject, setDefaultProject] = useState("");
|
|
37
46
|
const [systemPrompt, setSystemPrompt] = useState("");
|
|
@@ -42,6 +51,7 @@ export function PPMBotSettingsSection() {
|
|
|
42
51
|
const [pairedChats, setPairedChats] = useState<PairedChat[]>([]);
|
|
43
52
|
const [approveCode, setApproveCode] = useState("");
|
|
44
53
|
const [approving, setApproving] = useState(false);
|
|
54
|
+
const [testing, setTesting] = useState(false);
|
|
45
55
|
|
|
46
56
|
const fetchPairedChats = useCallback(async () => {
|
|
47
57
|
try {
|
|
@@ -60,9 +70,28 @@ export function PPMBotSettingsSection() {
|
|
|
60
70
|
setShowThinking(data.show_thinking);
|
|
61
71
|
setDebounceMs(data.debounce_ms);
|
|
62
72
|
}).catch(() => {});
|
|
73
|
+
api.get<TelegramConfig>("/api/settings/telegram").then((data) => {
|
|
74
|
+
setTokenConfigured(!!data.bot_token);
|
|
75
|
+
}).catch(() => {});
|
|
63
76
|
fetchPairedChats();
|
|
64
77
|
}, [fetchPairedChats]);
|
|
65
78
|
|
|
79
|
+
const saveToken = async () => {
|
|
80
|
+
if (!tokenInput.trim()) return;
|
|
81
|
+
setTokenSaving(true);
|
|
82
|
+
setStatus(null);
|
|
83
|
+
try {
|
|
84
|
+
await api.put<TelegramConfig>("/api/settings/telegram", { bot_token: tokenInput });
|
|
85
|
+
setTokenConfigured(true);
|
|
86
|
+
setTokenInput("");
|
|
87
|
+
setStatus({ type: "ok", msg: "Bot token saved" });
|
|
88
|
+
} catch (e) {
|
|
89
|
+
setStatus({ type: "err", msg: (e as Error).message });
|
|
90
|
+
} finally {
|
|
91
|
+
setTokenSaving(false);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
66
95
|
const save = async () => {
|
|
67
96
|
setSaving(true);
|
|
68
97
|
setStatus(null);
|
|
@@ -110,10 +139,51 @@ export function PPMBotSettingsSection() {
|
|
|
110
139
|
}
|
|
111
140
|
};
|
|
112
141
|
|
|
142
|
+
const handleTestNotification = async () => {
|
|
143
|
+
setTesting(true);
|
|
144
|
+
setStatus(null);
|
|
145
|
+
try {
|
|
146
|
+
await api.post("/api/settings/telegram/test", {});
|
|
147
|
+
setStatus({ type: "ok", msg: "Test notification sent to all paired devices!" });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
setStatus({ type: "err", msg: (e as Error).message });
|
|
150
|
+
} finally {
|
|
151
|
+
setTesting(false);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
113
155
|
if (!config) return <p className="text-xs text-muted-foreground">Loading...</p>;
|
|
114
156
|
|
|
157
|
+
const approvedCount = pairedChats.filter((c) => c.status === "approved").length;
|
|
158
|
+
|
|
115
159
|
return (
|
|
116
160
|
<div className="space-y-4">
|
|
161
|
+
{/* Bot Token */}
|
|
162
|
+
<div className="space-y-1.5">
|
|
163
|
+
<label className="text-[11px] text-muted-foreground">Telegram Bot Token</label>
|
|
164
|
+
<div className="flex gap-1.5">
|
|
165
|
+
<Input
|
|
166
|
+
type="password"
|
|
167
|
+
placeholder={tokenConfigured ? "•••••• (saved)" : "123456:ABC-DEF..."}
|
|
168
|
+
value={tokenInput}
|
|
169
|
+
onChange={(e) => setTokenInput(e.target.value)}
|
|
170
|
+
className="h-7 text-xs flex-1"
|
|
171
|
+
/>
|
|
172
|
+
<Button
|
|
173
|
+
variant="outline"
|
|
174
|
+
size="sm"
|
|
175
|
+
className="h-7 text-xs shrink-0 cursor-pointer"
|
|
176
|
+
disabled={tokenSaving || !tokenInput.trim()}
|
|
177
|
+
onClick={saveToken}
|
|
178
|
+
>
|
|
179
|
+
{tokenSaving ? "..." : "Save"}
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
<p className="text-[10px] text-muted-foreground">
|
|
183
|
+
Create a bot via <b>@BotFather</b> on Telegram. Used for both chat and notifications.
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
117
187
|
{/* Enable/Disable */}
|
|
118
188
|
<div className="flex items-center justify-between">
|
|
119
189
|
<div>
|
|
@@ -130,6 +200,7 @@ export function PPMBotSettingsSection() {
|
|
|
130
200
|
<p className="text-xs font-medium">Paired Devices</p>
|
|
131
201
|
<p className="text-[10px] text-muted-foreground">
|
|
132
202
|
Send any message to the bot on Telegram to get a pairing code. Enter it below to approve.
|
|
203
|
+
Notifications are sent to all approved devices.
|
|
133
204
|
</p>
|
|
134
205
|
|
|
135
206
|
<div className="flex gap-2">
|
|
@@ -189,6 +260,20 @@ export function PPMBotSettingsSection() {
|
|
|
189
260
|
))}
|
|
190
261
|
</div>
|
|
191
262
|
)}
|
|
263
|
+
|
|
264
|
+
{/* Test notification button */}
|
|
265
|
+
{tokenConfigured && approvedCount > 0 && (
|
|
266
|
+
<Button
|
|
267
|
+
variant="outline"
|
|
268
|
+
size="sm"
|
|
269
|
+
className="h-7 text-xs gap-1 w-full cursor-pointer"
|
|
270
|
+
disabled={testing}
|
|
271
|
+
onClick={handleTestNotification}
|
|
272
|
+
>
|
|
273
|
+
<Send className="size-3" />
|
|
274
|
+
{testing ? "Sending..." : "Test Notification"}
|
|
275
|
+
</Button>
|
|
276
|
+
)}
|
|
192
277
|
</div>
|
|
193
278
|
|
|
194
279
|
{/* Default Project */}
|
|
@@ -201,7 +286,7 @@ export function PPMBotSettingsSection() {
|
|
|
201
286
|
className="h-7 text-xs"
|
|
202
287
|
/>
|
|
203
288
|
<p className="text-[10px] text-muted-foreground">
|
|
204
|
-
Project used when starting a new chat.
|
|
289
|
+
Project used when starting a new chat. Leave empty for default workspace (~/.ppm/bot/).
|
|
205
290
|
</p>
|
|
206
291
|
</div>
|
|
207
292
|
|
|
@@ -11,7 +11,6 @@ import { useSettingsStore, type Theme } from "@/stores/settings-store";
|
|
|
11
11
|
import { cn } from "@/lib/utils";
|
|
12
12
|
import { AISettingsSection } from "./ai-settings-section";
|
|
13
13
|
import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
|
|
14
|
-
import { TelegramSettingsSection } from "./telegram-settings-section";
|
|
15
14
|
import { ProxySettingsSection } from "./proxy-settings-section";
|
|
16
15
|
import { McpSettingsSection } from "./mcp-settings-section";
|
|
17
16
|
import { ExtensionManagerSection } from "./extension-manager-section";
|
|
@@ -276,10 +275,13 @@ function NotificationsContent({ isSubscribed, loading, permission, pushError, su
|
|
|
276
275
|
|
|
277
276
|
<Separator />
|
|
278
277
|
|
|
279
|
-
{/* Telegram */}
|
|
280
|
-
<section className="space-y-
|
|
278
|
+
{/* Telegram — redirect to PPMBot */}
|
|
279
|
+
<section className="space-y-1">
|
|
281
280
|
<h3 className="text-xs font-medium text-muted-foreground">Telegram</h3>
|
|
282
|
-
<
|
|
281
|
+
<p className="text-[11px] text-muted-foreground">
|
|
282
|
+
Telegram notifications are sent to all approved devices in PPMBot settings.
|
|
283
|
+
Configure your bot token and pair devices there.
|
|
284
|
+
</p>
|
|
283
285
|
</section>
|
|
284
286
|
</div>
|
|
285
287
|
);
|