@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 +21 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +30 -0
- package/src/index.js +239 -0
- package/test/bark-direct.test.js +30 -0
- package/test/bark-real.test.js +83 -0
- package/test/notify.test.js +256 -0
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!");
|