@costrict/notify 1.0.5

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/.env.example ADDED
@@ -0,0 +1,21 @@
1
+ # Notification Channel Configuration
2
+ # ==================================
3
+
4
+ # System Desktop Notification
5
+ # Default: disabled
6
+ NOTIFY_ENABLE_SYSTEM=false
7
+
8
+ # Bark Notification (iOS/macOS push notifications)
9
+ # Default: disabled
10
+ NOTIFY_ENABLE_BARK=false
11
+ BARK_URL=https://api.day.app/YOUR_BARK_KEY
12
+
13
+ # WeChat Work (企微) Webhook Notification
14
+ # Default: disabled
15
+ NOTIFY_ENABLE_WECOM=false
16
+
17
+ # Option 1: Configure with full webhook URL
18
+ # WECOM_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY
19
+
20
+ # Option 2: Configure with KEY only (URL will use default: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=)
21
+ WECOM_WEBHOOK_KEY=YOUR_KEY
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # CoStrict-System-Notify
2
+
3
+ Desktop notification plugin for CoStrict.
4
+
5
+ ## Features
6
+
7
+ Monitors human intervention events and sends notifications via multiple channels when:
8
+
9
+ - **Permission requests** - Tool execution requires user permission
10
+ - **Question asked** - AI needs user input
11
+ - **Session idle** - AI waiting for user input
12
+
13
+ ## Notification Channels
14
+
15
+ ### System Notification (Default Disabled)
16
+ Desktop notifications using `node-notifier` - works across Windows, macOS, and Linux.
17
+
18
+ ### Bark Notification
19
+ Push notifications via Bark service (iOS/macOS).
20
+
21
+ ### WeChat Work Webhook (Default Disabled)
22
+ Push notifications via WeChat Work (企微) webhook service.
23
+
24
+ ## Configuration
25
+
26
+ Configure notification channels using environment variables:
27
+
28
+ ```bash
29
+ # Enable/disable notification channels (default: all disabled)
30
+ NOTIFY_ENABLE_SYSTEM=true # System notification (default: false)
31
+ NOTIFY_ENABLE_BARK=false # Bark notification (default: false)
32
+ NOTIFY_ENABLE_WECOM=false # WeChat Work notification (default: false)
33
+
34
+ # Bark configuration (required if Bark enabled)
35
+ BARK_URL="https://api.day.app/YOUR_BARK_KEY"
36
+
37
+ # WeChat Work configuration (required if WeChat Work enabled)
38
+ # Option 1: Full webhook URL
39
+ WECOM_WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
40
+
41
+ # Option 2: Just the KEY (URL will use default base)
42
+ WECOM_WEBHOOK_KEY="YOUR_KEY"
43
+ ```
44
+
45
+ ### Examples
46
+
47
+ **Enable system notifications**:
48
+ ```bash
49
+ export NOTIFY_ENABLE_SYSTEM=true
50
+ ```
51
+
52
+ **Enable Bark notifications**:
53
+ ```bash
54
+ export NOTIFY_ENABLE_BARK=true
55
+ export BARK_URL="https://api.day.app/YOUR_BARK_KEY"
56
+ ```
57
+
58
+ **Enable multiple channels**:
59
+ ```bash
60
+ export NOTIFY_ENABLE_SYSTEM=true
61
+ export NOTIFY_ENABLE_BARK=true
62
+ export BARK_URL="https://api.day.app/YOUR_BARK_KEY"
63
+ ```
64
+
65
+ **Enable WeChat Work notifications** (with KEY only):
66
+ ```bash
67
+ export NOTIFY_ENABLE_WECOM=true
68
+ export WECOM_WEBHOOK_KEY="YOUR_KEY"
69
+ ```
70
+
71
+ **Enable WeChat Work notifications** (with full URL):
72
+ ```bash
73
+ export NOTIFY_ENABLE_WECOM=true
74
+ export WECOM_WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
75
+ ```
76
+
77
+ **Enable all channels**:
78
+ ```bash
79
+ export NOTIFY_ENABLE_SYSTEM=true
80
+ export NOTIFY_ENABLE_BARK=true
81
+ export NOTIFY_ENABLE_WECOM=true
82
+ export BARK_URL="https://api.day.app/YOUR_BARK_KEY"
83
+ export WECOM_WEBHOOK_KEY="YOUR_KEY"
84
+ ```
85
+
86
+ ## Architecture
87
+
88
+ The plugin implements a single hook:
89
+
90
+ - **intervention.required hook** - Receives notification events and displays desktop notifications
91
+
92
+ The filtering logic for idle events (main session vs sub-agent) is handled by the TDD plugin which triggers this hook. This plugin simply displays whatever notifications it receives.
93
+
94
+ ## Installation
95
+
96
+ ### Add to CoStrict Config
97
+
98
+ Add to your `~/.config/costrict/config.json`:
99
+
100
+ ```json
101
+ {
102
+ "plugin": ["@costrict/notify"]
103
+ }
104
+ ```
105
+
106
+ ### Build Plugin
107
+
108
+ ```bash
109
+ cd D:/DEV/costrict-notify
110
+ bun install
111
+ ```
112
+
113
+ No build step required - pure JavaScript implementation.
114
+
115
+ ### Configuration Template
116
+
117
+ Copy `.env.example` to create your own environment configuration:
118
+
119
+ ```bash
120
+ cp .env.example .env
121
+ # Edit .env with your notification settings
122
+ ```
123
+
124
+ ## Project Structure
125
+
126
+ ```
127
+ costrict-notify/
128
+ ├── src/
129
+ │ └── index.js # Main plugin code with multi-channel support
130
+ ├── package.json # Dependencies and scripts
131
+ ├── .env.example # Environment configuration template
132
+ ├── .gitignore # Git ignore rules
133
+ ├── LICENSE # MIT License
134
+ └── README.md # This file
135
+ ```
136
+
137
+ ## Notification Details
138
+
139
+ ### Permission Notification
140
+
141
+ - **Trigger**: `intervention.required` hook (type: "permission")
142
+ - **Title**: "需要权限"
143
+ - **Message**: Permission request message
144
+
145
+ ### Question Notification
146
+
147
+ - **Trigger**: `intervention.required` hook (type: "question")
148
+ - **Title**: "问题"
149
+ - **Message**: First question text
150
+
151
+ ### Idle Notification
152
+
153
+ - **Trigger**: `intervention.required` hook (type: "idle")
154
+ - **Title**: "会话空闲"
155
+ - **Message**: "AI 正在等待您的输入"
156
+ - **Filtered**: Only for main sessions (handled by TDD plugin)
157
+
158
+ ## Technical Details
159
+
160
+ - Uses `node-notifier` v10.0.1 for cross-platform desktop notifications
161
+ - Supports multiple notification channels (System, Bark, WeChat Work)
162
+ - Configurable via environment variables
163
+ - Implements `intervention.required` hook
164
+ - Pure JavaScript - no build step required
165
+ - Compatible with CoStrict plugin system
166
+ - Smart filtering delegated to TDD plugin
167
+
168
+ ## License
169
+
170
+ MIT
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@costrict/notify",
3
+ "version": "1.0.5",
4
+ "description": "Multi-channel notification plugin for CoStrict - supports system desktop, Bark, and WeChat Work notifications",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "opencode",
10
+ "costrict",
11
+ "plugin",
12
+ "notification",
13
+ "desktop",
14
+ "bark",
15
+ "wecom",
16
+ "wechat",
17
+ "webhook"
18
+ ],
19
+ "author": "",
20
+ "scripts": {
21
+ "test": "node test/notify.test.js"
22
+ },
23
+ "dependencies": {
24
+ "@opencode-ai/plugin": "*",
25
+ "node-notifier": "^10.0.1"
26
+ },
27
+ "peerDependencies": {
28
+ "@opencode-ai/plugin": "*"
29
+ }
30
+ }
package/src/index.js ADDED
@@ -0,0 +1,239 @@
1
+ let notifierInstance = null;
2
+
3
+ async function loadNotifier() {
4
+ if (notifierInstance) {
5
+ return notifierInstance;
6
+ }
7
+
8
+ try {
9
+ const notifierModule = await import("node-notifier");
10
+ const notifier = notifierModule.default || notifierModule;
11
+ return notifier;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ class SystemNotifier {
18
+ async notify(title, message, options = {}) {
19
+ const notifier = await loadNotifier();
20
+ if (!notifier) {
21
+ throw new Error("System notifier not available");
22
+ }
23
+
24
+ return new Promise((resolve, reject) => {
25
+ notifier.notify(
26
+ {
27
+ title,
28
+ message,
29
+ timeout: options.timeout || 5,
30
+ icon: "nothing",
31
+ appID: "CoStrict",
32
+ wait: false,
33
+ },
34
+ (err, response) => {
35
+ if (err) {
36
+ reject(err);
37
+ } else {
38
+ resolve(response);
39
+ }
40
+ }
41
+ );
42
+ });
43
+ }
44
+ }
45
+
46
+ class BarkNotifier {
47
+ constructor() {
48
+ this.url = process.env.BARK_URL;
49
+ }
50
+
51
+ async notify(title, message, options = {}) {
52
+ if (!this.url) {
53
+ throw new Error("Bark URL not configured");
54
+ }
55
+
56
+ let url = `${this.url}/${encodeURIComponent(title)}/${encodeURIComponent(message)}`;
57
+ const params = {};
58
+
59
+ if (options.timeout) {
60
+ params.timeout = options.timeout;
61
+ }
62
+
63
+ if (Object.keys(params).length > 0) {
64
+ url += '?' + new URLSearchParams(params).toString();
65
+ }
66
+
67
+ await fetch(url);
68
+ }
69
+ }
70
+
71
+ class WecomNotifier {
72
+ constructor() {
73
+ const webhookUrl = process.env.WECOM_WEBHOOK_URL;
74
+ const webhookKey = process.env.WECOM_WEBHOOK_KEY;
75
+
76
+ if (webhookUrl) {
77
+ this.webhookUrl = webhookUrl;
78
+ } else if (webhookKey) {
79
+ this.webhookUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${webhookKey}`;
80
+ } else {
81
+ this.webhookUrl = null;
82
+ }
83
+ }
84
+
85
+ async notify(title, message, options = {}) {
86
+ if (!this.webhookUrl) {
87
+ throw new Error("Wecom webhook URL or KEY not configured");
88
+ }
89
+
90
+ const content = `${title}\n${message}`;
91
+
92
+ await fetch(this.webhookUrl, {
93
+ method: "POST",
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ },
97
+ body: JSON.stringify({
98
+ msgtype: "text",
99
+ text: {
100
+ content: content,
101
+ },
102
+ }),
103
+ });
104
+ }
105
+ }
106
+
107
+ async function sendNotification(title, message, options = {}) {
108
+ const enableSystem = process.env.NOTIFY_ENABLE_SYSTEM === "true";
109
+ const enableBark = process.env.NOTIFY_ENABLE_BARK === "true";
110
+ const enableWecom = process.env.NOTIFY_ENABLE_WECOM === "true";
111
+
112
+ const results = [];
113
+
114
+ if (enableSystem) {
115
+ try {
116
+ const systemNotifier = new SystemNotifier();
117
+ await systemNotifier.notify(title, message, options);
118
+ results.push({ type: "system", success: true });
119
+ } catch (error) {
120
+ results.push({ type: "system", success: false, error: error.message });
121
+ }
122
+ }
123
+
124
+ if (enableBark) {
125
+ try {
126
+ const barkNotifier = new BarkNotifier();
127
+ await barkNotifier.notify(title, message, options);
128
+ results.push({ type: "bark", success: true });
129
+ } catch (error) {
130
+ results.push({ type: "bark", success: false, error: error.message });
131
+ }
132
+ }
133
+
134
+ if (enableWecom) {
135
+ try {
136
+ const wecomNotifier = new WecomNotifier();
137
+ await wecomNotifier.notify(title, message, options);
138
+ results.push({ type: "wecom", success: true });
139
+ } catch (error) {
140
+ results.push({ type: "wecom", success: false, error: error.message });
141
+ }
142
+ }
143
+
144
+ const hasSuccess = results.some((r) => r.success);
145
+ return hasSuccess;
146
+ }
147
+
148
+ export async function CoStrictSystemNotifyPlugin(ctx) {
149
+ if (notifierInstance) {
150
+ notifierInstance = null;
151
+ }
152
+
153
+ const { client } = ctx;
154
+
155
+ return {
156
+ "intervention.required": async (input, output) => {
157
+ const { type, data, sessionID } = input;
158
+
159
+ let title = "CoStrict";
160
+ let message = "";
161
+ let timeout = 5;
162
+
163
+ let sessionTitle = "任务";
164
+ let userMessage = "";
165
+ let latestMessage = "";
166
+
167
+ if (sessionID && client?.session) {
168
+ try {
169
+ const session = await client.session.get({ path: { id: sessionID } });
170
+ if (session?.data?.title) {
171
+ sessionTitle = session.data.title.length > 50 ? session.data.title.slice(0, 50) + "..." : session.data.title;
172
+ }
173
+
174
+ const result = await client.session.messages({ path: { id: sessionID }, query: { limit: 10 } });
175
+ const data = result.data ?? [];
176
+ const lastUserMsg = data.findLast(m => m.info?.role === "user");
177
+ if (lastUserMsg?.parts) {
178
+ const textPart = lastUserMsg.parts.find(p => p.type === "text");
179
+ if (textPart?.text) {
180
+ userMessage = textPart.text.length > 100 ? textPart.text.slice(0, 100) + "..." : textPart.text;
181
+ }
182
+ }
183
+ const latestNonUserMsg = data.findLast(m => m.info?.role !== "user");
184
+ if (latestNonUserMsg?.parts) {
185
+ const textPart = latestNonUserMsg.parts.find(p => p.type === "text");
186
+ if (textPart?.text) {
187
+ latestMessage = textPart.text.length > 100 ? textPart.text.slice(0, 100) + "..." : textPart.text;
188
+ }
189
+ }
190
+ } catch (err) {
191
+ console.error("Failed to get session messages:", err);
192
+ }
193
+ }
194
+
195
+ let formattedMessage = "";
196
+
197
+ switch (type) {
198
+ case "permission":
199
+ title = "需要权限";
200
+ const permission = data.permission || "";
201
+ const pattern = Array.isArray(data.patterns) && data.patterns.length > 0 ? data.patterns[0] : "";
202
+ const permissionMessage = permission ? `[${permission}] ${pattern}` : (pattern || "工具需要权限才能执行");
203
+ formattedMessage = [
204
+ `会话标题:${sessionTitle}`,
205
+ `权限请求:${permissionMessage}`
206
+ ].join("\n");
207
+ break;
208
+
209
+ case "question":
210
+ title = "问题";
211
+ const firstQuestion =
212
+ Array.isArray(data.questions) && data.questions.length > 0
213
+ ? data.questions[0].question
214
+ : "请回答问题";
215
+ formattedMessage = [
216
+ `会话标题:${sessionTitle}`,
217
+ `待回答问题:${firstQuestion}`
218
+ ].join("\n");
219
+ break;
220
+
221
+ case "idle":
222
+ title = "会话空闲";
223
+ formattedMessage = [
224
+ `会话标题:${sessionTitle}`,
225
+ userMessage ? `用户上一条提问内容:${userMessage}` : "用户上一条提问内容:(无)",
226
+ latestMessage ? `最新一条消息:${latestMessage}` : "最新一条消息:(无)"
227
+ ].join("\n");
228
+ break;
229
+ }
230
+
231
+ try {
232
+ const success = await sendNotification(title, formattedMessage, { timeout });
233
+ output.handled = success;
234
+ } catch {
235
+ output.handled = false;
236
+ }
237
+ },
238
+ };
239
+ }
@@ -0,0 +1,30 @@
1
+ const barkUrl = process.env.BARK_URL;
2
+ const title = "CoStrict 验证测试";
3
+ const message = "Bark 通知功能正常工作";
4
+
5
+ console.log("Testing Bark API directly...\n");
6
+ console.log("BARK_URL:", barkUrl);
7
+ console.log("Title:", title);
8
+ console.log("Message:", message);
9
+ console.log();
10
+
11
+ if (!barkUrl) {
12
+ console.log("✗ BARK_URL not configured");
13
+ process.exit(1);
14
+ }
15
+
16
+ try {
17
+ let url = `${barkUrl}/${encodeURIComponent(title)}/${encodeURIComponent(message)}`;
18
+ url += '?timeout=5';
19
+ console.log("Sending request to:", url);
20
+
21
+ const response = await fetch(url);
22
+
23
+ console.log("\nResponse status:", response.status);
24
+ const data = await response.json();
25
+ console.log("Response data:", JSON.stringify(data, null, 2));
26
+ console.log("\n✓ Bark notification sent successfully!");
27
+ } catch (error) {
28
+ console.log("\n✗ Bark notification failed:", error.message);
29
+ process.exit(1);
30
+ }
@@ -0,0 +1,83 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ const srcPath = path.join(__dirname, "..", "src", "index.js");
8
+
9
+ const moduleContent = await fs.readFile(srcPath, "utf-8");
10
+
11
+ const mockProcess = {
12
+ env: {
13
+ BARK_URL: process.env.BARK_URL,
14
+ NOTIFY_ENABLE_BARK: process.env.NOTIFY_ENABLE_BARK,
15
+ NOTIFY_ENABLE_SYSTEM: process.env.NOTIFY_ENABLE_SYSTEM,
16
+ },
17
+ };
18
+
19
+ const mockImport = async (module) => {
20
+ return {};
21
+ };
22
+
23
+ const loadNotifierFunction = new Function(
24
+ "mockProcess",
25
+ "mockImport",
26
+ `
27
+ ${moduleContent
28
+ .replace(/export async function/g, "async function")
29
+ .replace(/process\.env/g, "mockProcess.env")
30
+ .replace(/await import\(/g, "await mockImport(")}
31
+
32
+ return {
33
+ BarkNotifier,
34
+ sendNotification
35
+ };
36
+ `
37
+ );
38
+
39
+ const { BarkNotifier, sendNotification } = loadNotifierFunction(mockProcess, mockImport);
40
+
41
+ console.log("Testing Bark notification with real API...\n");
42
+ console.log("Environment variables:");
43
+ console.log(" BARK_URL:", mockProcess.env.BARK_URL);
44
+ console.log(" NOTIFY_ENABLE_BARK:", mockProcess.env.NOTIFY_ENABLE_BARK);
45
+ console.log();
46
+
47
+ try {
48
+ const barkNotifier = new BarkNotifier();
49
+
50
+ console.log("Sending test notification via Bark...");
51
+ console.log(" Title: CoStrict 测试通知");
52
+ console.log(" Message: 这是一条测试消息");
53
+
54
+ await barkNotifier.notify(
55
+ "CoStrict 测试通知",
56
+ "这是一条测试消息",
57
+ { timeout: 5 }
58
+ );
59
+
60
+ console.log("\n✓ Bark notification sent successfully!");
61
+
62
+ console.log("\nTesting sendNotification with Bark enabled...");
63
+
64
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "false";
65
+ mockProcess.env.NOTIFY_ENABLE_BARK = "true";
66
+
67
+ const result = await sendNotification(
68
+ "CoStrict 测试通知 2",
69
+ "通过 sendNotification 发送",
70
+ { timeout: 5 }
71
+ );
72
+
73
+ if (result) {
74
+ console.log("✓ sendNotification with Bark: Success!");
75
+ } else {
76
+ console.log("✗ sendNotification with Bark: Failed");
77
+ process.exit(1);
78
+ }
79
+
80
+ } catch (error) {
81
+ console.log("\n✗ Bark notification failed:", error.message);
82
+ process.exit(1);
83
+ }
@@ -0,0 +1,256 @@
1
+ import assert from "node:assert";
2
+ import { promises as fs } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ const srcPath = path.join(__dirname, "..", "src", "index.js");
9
+
10
+ const moduleContent = await fs.readFile(srcPath, "utf-8");
11
+
12
+ const exports = {};
13
+
14
+ const mockProcess = {
15
+ env: {},
16
+ };
17
+
18
+ const mockImport = async (module) => {
19
+ if (module === "node-notifier") {
20
+ return {
21
+ notify: (options, callback) => {
22
+ callback(null, "success");
23
+ },
24
+ };
25
+ }
26
+ return {};
27
+ };
28
+
29
+ const loadNotifierFunction = new Function(
30
+ "mockProcess",
31
+ "mockImport",
32
+ `
33
+ ${moduleContent
34
+ .replace(/export async function/g, "async function")
35
+ .replace(/process\.env/g, "mockProcess.env")
36
+ .replace(/await import\(/g, "await mockImport(")}
37
+
38
+ return {
39
+ SystemNotifier,
40
+ BarkNotifier,
41
+ WecomNotifier,
42
+ sendNotification,
43
+ CoStrictSystemNotifyPlugin
44
+ };
45
+
46
+ `
47
+ );
48
+
49
+ const { SystemNotifier, BarkNotifier, WecomNotifier, sendNotification, CoStrictSystemNotifyPlugin } =
50
+ loadNotifierFunction(mockProcess, mockImport);
51
+
52
+ console.log("Running SystemNotifier tests...");
53
+
54
+ const systemNotifier = new SystemNotifier();
55
+
56
+ try {
57
+ await systemNotifier.notify("Test Title", "Test Message");
58
+ console.log("✓ SystemNotifier.notify: Success");
59
+ } catch (error) {
60
+ console.log("✗ SystemNotifier.notify: Failed -", error.message);
61
+ process.exit(1);
62
+ }
63
+
64
+ console.log("\nRunning BarkNotifier tests...");
65
+
66
+ mockProcess.env.BARK_URL = "https://api.day.app/test";
67
+
68
+ const barkNotifier = new BarkNotifier();
69
+
70
+ try {
71
+ await barkNotifier.notify("Test Title", "Test Message");
72
+ console.log("✓ BarkNotifier.notify: Success");
73
+ } catch (error) {
74
+ console.log("✗ BarkNotifier.notify: Failed -", error.message);
75
+ process.exit(1);
76
+ }
77
+
78
+ try {
79
+ mockProcess.env.BARK_URL = undefined;
80
+ const barkNotifierNoUrl = new BarkNotifier();
81
+ await barkNotifierNoUrl.notify("Test Title", "Test Message");
82
+ console.log("✗ BarkNotifier without URL: Should have thrown error");
83
+ process.exit(1);
84
+ } catch (error) {
85
+ if (error.message === "Bark URL not configured") {
86
+ console.log("✓ BarkNotifier without URL: Correctly throws error");
87
+ } else {
88
+ console.log("✗ BarkNotifier without URL: Wrong error -", error.message);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ console.log("\nRunning WecomNotifier tests...");
94
+
95
+ mockProcess.env.WECOM_WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test";
96
+
97
+ const wecomNotifier = new WecomNotifier();
98
+
99
+ try {
100
+ await wecomNotifier.notify("Test Title", "Test Message");
101
+ console.log("✓ WecomNotifier.notify (with URL): Success");
102
+ } catch (error) {
103
+ console.log("✗ WecomNotifier.notify (with URL): Failed -", error.message);
104
+ process.exit(1);
105
+ }
106
+
107
+ try {
108
+ mockProcess.env.WECOM_WEBHOOK_URL = undefined;
109
+ const wecomNotifierNoUrl = new WecomNotifier();
110
+ await wecomNotifierNoUrl.notify("Test Title", "Test Message");
111
+ console.log("✗ WecomNotifier without URL: Should have thrown error");
112
+ process.exit(1);
113
+ } catch (error) {
114
+ if (error.message === "Wecom webhook URL or KEY not configured") {
115
+ console.log("✓ WecomNotifier without URL: Correctly throws error");
116
+ } else {
117
+ console.log("✗ WecomNotifier without URL: Wrong error -", error.message);
118
+ process.exit(1);
119
+ }
120
+ }
121
+
122
+ mockProcess.env.WECOM_WEBHOOK_KEY = "test-key-123";
123
+
124
+ const wecomNotifierWithKey = new WecomNotifier();
125
+
126
+ try {
127
+ await wecomNotifierWithKey.notify("Test Title", "Test Message");
128
+ console.log("✓ WecomNotifier.notify (with KEY): Success");
129
+ } catch (error) {
130
+ console.log("✗ WecomNotifier.notify (with KEY): Failed -", error.message);
131
+ process.exit(1);
132
+ }
133
+
134
+ try {
135
+ mockProcess.env.WECOM_WEBHOOK_KEY = undefined;
136
+ const wecomNotifierNoKey = new WecomNotifier();
137
+ await wecomNotifierNoKey.notify("Test Title", "Test Message");
138
+ console.log("✗ WecomNotifier without KEY: Should have thrown error");
139
+ process.exit(1);
140
+ } catch (error) {
141
+ if (error.message === "Wecom webhook URL or KEY not configured") {
142
+ console.log("✓ WecomNotifier without KEY: Correctly throws error");
143
+ } else {
144
+ console.log("✗ WecomNotifier without KEY: Wrong error -", error.message);
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ console.log("\nRunning sendNotification tests...");
150
+
151
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "true";
152
+ mockProcess.env.NOTIFY_ENABLE_BARK = "false";
153
+
154
+ try {
155
+ const result = await sendNotification("Test Title", "Test Message");
156
+ assert.strictEqual(result, true, "sendNotification should return true");
157
+ console.log("✓ sendNotification (system only): Success");
158
+ } catch (error) {
159
+ console.log("✗ sendNotification (system only): Failed -", error.message);
160
+ process.exit(1);
161
+ }
162
+
163
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "false";
164
+ mockProcess.env.NOTIFY_ENABLE_BARK = "true";
165
+ mockProcess.env.BARK_URL = "https://api.day.app/test";
166
+
167
+ try {
168
+ const result = await sendNotification("Test Title", "Test Message");
169
+ assert.strictEqual(result, true, "sendNotification should return true");
170
+ console.log("✓ sendNotification (bark only): Success");
171
+ } catch (error) {
172
+ console.log("✗ sendNotification (bark only): Failed -", error.message);
173
+ process.exit(1);
174
+ }
175
+
176
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "true";
177
+ mockProcess.env.NOTIFY_ENABLE_BARK = "true";
178
+
179
+ try {
180
+ const result = await sendNotification("Test Title", "Test Message");
181
+ assert.strictEqual(result, true, "sendNotification should return true");
182
+ console.log("✓ sendNotification (both): Success");
183
+ } catch (error) {
184
+ console.log("✗ sendNotification (both): Failed -", error.message);
185
+ process.exit(1);
186
+ }
187
+
188
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "false";
189
+ mockProcess.env.NOTIFY_ENABLE_BARK = "false";
190
+ mockProcess.env.NOTIFY_ENABLE_WECOM = "true";
191
+ mockProcess.env.WECOM_WEBHOOK_KEY = "test-key-123";
192
+
193
+ try {
194
+ const result = await sendNotification("Test Title", "Test Message");
195
+ assert.strictEqual(result, true, "sendNotification should return true");
196
+ console.log("✓ sendNotification (wecom only): Success");
197
+ } catch (error) {
198
+ console.log("✗ sendNotification (wecom only): Failed -", error.message);
199
+ process.exit(1);
200
+ }
201
+
202
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "true";
203
+ mockProcess.env.NOTIFY_ENABLE_BARK = "true";
204
+ mockProcess.env.NOTIFY_ENABLE_WECOM = "true";
205
+
206
+ try {
207
+ const result = await sendNotification("Test Title", "Test Message");
208
+ assert.strictEqual(result, true, "sendNotification should return true");
209
+ console.log("✓ sendNotification (all): Success");
210
+ } catch (error) {
211
+ console.log("✗ sendNotification (all): Failed -", error.message);
212
+ process.exit(1);
213
+ }
214
+
215
+ console.log("\nRunning CoStrictSystemNotifyPlugin tests...");
216
+
217
+ mockProcess.env.NOTIFY_ENABLE_SYSTEM = "true";
218
+
219
+ try {
220
+ const plugin = await CoStrictSystemNotifyPlugin();
221
+
222
+ const testCases = [
223
+ {
224
+ type: "permission",
225
+ data: { message: "Test permission message" },
226
+ expectedTitle: "需要权限",
227
+ expectedMessage: "Test permission message",
228
+ },
229
+ {
230
+ type: "question",
231
+ data: { questions: [{ question: "Test question?" }] },
232
+ expectedTitle: "问题",
233
+ expectedMessage: "Test question?",
234
+ },
235
+ {
236
+ type: "idle",
237
+ data: {},
238
+ expectedTitle: "会话空闲",
239
+ expectedMessage: "AI 正在等待您的输入",
240
+ },
241
+ ];
242
+
243
+ for (const testCase of testCases) {
244
+ const output = {};
245
+ await plugin["intervention.required"]({ ...testCase }, output);
246
+ assert.strictEqual(output.handled, true, "Plugin should handle the event");
247
+ console.log(
248
+ `✓ Plugin handles ${testCase.type} event: Success`
249
+ );
250
+ }
251
+ } catch (error) {
252
+ console.log("✗ CoStrictSystemNotifyPlugin: Failed -", error.message);
253
+ process.exit(1);
254
+ }
255
+
256
+ console.log("\n✓ All tests passed!");