ai-engineering-init 1.3.1 → 1.3.4
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/.claude/hooks/lib/notify.js +310 -0
- package/.claude/hooks/skill-forced-eval.js +2 -1
- package/.claude/hooks/stop.js +5 -30
- package/.claude/notify-config.json +9 -0
- package/.claude/skills/add-skill/SKILL.md +205 -116
- package/.claude/skills/git-workflow/SKILL.md +27 -5
- package/.claude/skills/leniu-report-customization/SKILL.md +42 -2
- package/.claude/skills/leniu-report-standard-customization/SKILL.md +551 -0
- package/.codex/skills/add-skill/SKILL.md +205 -116
- package/.codex/skills/leniu-report-customization/SKILL.md +42 -2
- package/.codex/skills/leniu-report-standard-customization/SKILL.md +551 -0
- package/.cursor/hooks/cursor-skill-eval.js +14 -11
- package/.cursor/hooks/lib/notify.js +310 -0
- package/.cursor/hooks/stop.js +5 -34
- package/.cursor/rules/skill-activation.mdc +96 -0
- package/.cursor/skills/add-skill/SKILL.md +205 -116
- package/.cursor/skills/leniu-report-customization/SKILL.md +42 -2
- package/.cursor/skills/leniu-report-standard-customization/SKILL.md +551 -0
- package/AGENTS.md +1 -0
- package/bin/index.js +8 -2
- package/package.json +1 -1
- package/.claude/skills/leniu-java-code-style/SKILL.md +0 -510
- package/.codex/skills/leniu-java-code-style/SKILL.md +0 -510
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 智能完成通知模块
|
|
3
|
+
* 支持音效、系统通知弹窗、TTS 语音播报三种方式的任意组合
|
|
4
|
+
* 跨平台兼容:macOS / Windows / Linux
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
|
|
13
|
+
// ── 默认配置 ─────────────────────────────────────────────────────────────
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
notify: {
|
|
16
|
+
methods: ['sound'],
|
|
17
|
+
sound: { enabled: true, file: 'completed.wav' },
|
|
18
|
+
notification: { enabled: false, title: 'AI 任务完成' },
|
|
19
|
+
tts: { enabled: false, prefix: '任务完成', maxLength: 80 },
|
|
20
|
+
summary: { maxLength: 100, strategy: 'first-sentence', stripMarkdown: true },
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ── 工具函数 ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** 深度合并对象(src 补充到 dest,dest 已有的键保留) */
|
|
27
|
+
function deepMerge(dest, src) {
|
|
28
|
+
const result = { ...dest };
|
|
29
|
+
for (const [key, value] of Object.entries(src)) {
|
|
30
|
+
if (key in result && typeof result[key] === 'object' && result[key] !== null
|
|
31
|
+
&& !Array.isArray(result[key]) && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
32
|
+
result[key] = deepMerge(result[key], value);
|
|
33
|
+
} else if (!(key in result)) {
|
|
34
|
+
result[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 带超时的 spawn,返回 Promise */
|
|
41
|
+
function spawnWithTimeout(cmd, args, opts, timeoutMs) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
try {
|
|
44
|
+
const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], ...opts });
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
47
|
+
resolve(false);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
child.on('close', () => { clearTimeout(timer); resolve(true); });
|
|
50
|
+
child.on('error', () => { clearTimeout(timer); resolve(false); });
|
|
51
|
+
} catch {
|
|
52
|
+
resolve(false);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 1. readStdin ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** 读取 hook stdin JSON(2s 超时,失败返回 null) */
|
|
60
|
+
function readStdin() {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
// 非 TTY 且有 pipe 数据时才读取
|
|
63
|
+
if (process.stdin.isTTY) {
|
|
64
|
+
resolve(null);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let data = '';
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
process.stdin.removeAllListeners();
|
|
71
|
+
process.stdin.destroy();
|
|
72
|
+
resolve(null);
|
|
73
|
+
}, 2000);
|
|
74
|
+
|
|
75
|
+
process.stdin.setEncoding('utf8');
|
|
76
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
77
|
+
process.stdin.on('end', () => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
try {
|
|
80
|
+
resolve(JSON.parse(data));
|
|
81
|
+
} catch {
|
|
82
|
+
resolve(null);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
process.stdin.on('error', () => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
resolve(null);
|
|
88
|
+
});
|
|
89
|
+
process.stdin.resume();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── 2. loadConfig ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/** 加载配置文件,按查找顺序:工作区 → 全局 → 内置默认值 */
|
|
96
|
+
function loadConfig(cwd) {
|
|
97
|
+
const homeDir = os.homedir();
|
|
98
|
+
const candidates = [
|
|
99
|
+
path.join(cwd, '.claude', 'notify-config.json'),
|
|
100
|
+
path.join(cwd, '.cursor', 'notify-config.json'),
|
|
101
|
+
path.join(homeDir, '.claude', 'notify-config.json'),
|
|
102
|
+
path.join(homeDir, '.cursor', 'notify-config.json'),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const configPath of candidates) {
|
|
106
|
+
try {
|
|
107
|
+
if (fs.existsSync(configPath)) {
|
|
108
|
+
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
109
|
+
// 用户配置优先,缺失字段用默认值补充
|
|
110
|
+
return deepMerge(userConfig, DEFAULT_CONFIG);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// 配置文件解析失败,继续尝试下一个
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 3. extractSummary ────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/** 从 last_assistant_message 提取摘要 */
|
|
123
|
+
function extractSummary(message, config) {
|
|
124
|
+
if (!message || typeof message !== 'string') return '';
|
|
125
|
+
|
|
126
|
+
const summaryConfig = config.notify.summary;
|
|
127
|
+
let text = message;
|
|
128
|
+
|
|
129
|
+
// 去除 markdown 标记
|
|
130
|
+
if (summaryConfig.stripMarkdown) {
|
|
131
|
+
text = text
|
|
132
|
+
.replace(/```[\s\S]*?```/g, '') // 代码块
|
|
133
|
+
.replace(/`[^`]+`/g, '') // 行内代码
|
|
134
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // 链接 → 文字
|
|
135
|
+
.replace(/^#{1,6}\s+.*$/gm, '') // 整行标题(含文字)
|
|
136
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // 粗体
|
|
137
|
+
.replace(/\*([^*]+)\*/g, '$1') // 斜体
|
|
138
|
+
.replace(/~~([^~]+)~~/g, '$1') // 删除线
|
|
139
|
+
.replace(/^[-*+]\s+/gm, '') // 无序列表标记
|
|
140
|
+
.replace(/^\d+\.\s+/gm, '') // 有序列表标记
|
|
141
|
+
.replace(/^>\s+/gm, '') // 引用
|
|
142
|
+
.replace(/\n{2,}/g, '\n') // 多余空行
|
|
143
|
+
.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!text) return '';
|
|
147
|
+
|
|
148
|
+
const maxLen = summaryConfig.maxLength || 100;
|
|
149
|
+
const strategy = summaryConfig.strategy || 'first-sentence';
|
|
150
|
+
|
|
151
|
+
let result = '';
|
|
152
|
+
|
|
153
|
+
switch (strategy) {
|
|
154
|
+
case 'first-sentence': {
|
|
155
|
+
// 匹配第一个句末标点(中文标点后无需空格,英文句号需后跟空格或结尾)
|
|
156
|
+
const match = text.match(/^(.+?(?:[。!?!?]|\.(?:\s|$)))/);
|
|
157
|
+
result = match ? match[1] : text.split('\n')[0];
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'first-line': {
|
|
161
|
+
result = text.split('\n')[0];
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'truncate':
|
|
165
|
+
default: {
|
|
166
|
+
result = text;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 截断到 maxLength
|
|
172
|
+
if (result.length > maxLen) {
|
|
173
|
+
result = result.substring(0, maxLen - 3) + '...';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 4. playSound ─────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** 跨平台播放音效(5s 超时) */
|
|
182
|
+
function playSound(cwd, fileName) {
|
|
183
|
+
const homeDir = os.homedir();
|
|
184
|
+
const candidates = [
|
|
185
|
+
path.join(cwd, '.claude', 'audio', fileName),
|
|
186
|
+
path.join(cwd, '.cursor', 'audio', fileName),
|
|
187
|
+
path.join(homeDir, '.claude', 'audio', fileName),
|
|
188
|
+
path.join(homeDir, '.cursor', 'audio', fileName),
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const audioFile = candidates.find(f => {
|
|
192
|
+
try { return fs.existsSync(f); } catch { return false; }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!audioFile) return Promise.resolve(false);
|
|
196
|
+
|
|
197
|
+
const platform = process.platform;
|
|
198
|
+
|
|
199
|
+
if (platform === 'darwin') {
|
|
200
|
+
return spawnWithTimeout('afplay', [audioFile], {}, 5000);
|
|
201
|
+
} else if (platform === 'win32') {
|
|
202
|
+
return spawnWithTimeout('powershell', [
|
|
203
|
+
'-c', `(New-Object Media.SoundPlayer '${audioFile.replace(/'/g, "''")}').PlaySync()`,
|
|
204
|
+
], {}, 5000);
|
|
205
|
+
} else {
|
|
206
|
+
// Linux: 尝试 aplay → paplay
|
|
207
|
+
return spawnWithTimeout('aplay', [audioFile], {}, 5000).then(ok => {
|
|
208
|
+
if (!ok) return spawnWithTimeout('paplay', [audioFile], {}, 5000);
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── 5. sendNotification ──────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/** 跨平台系统通知弹窗(2s 超时) */
|
|
217
|
+
function sendNotification(title, body) {
|
|
218
|
+
const safeTitle = (title || '').replace(/"/g, '\\"');
|
|
219
|
+
const safeBody = (body || '').replace(/"/g, '\\"');
|
|
220
|
+
const platform = process.platform;
|
|
221
|
+
|
|
222
|
+
if (platform === 'darwin') {
|
|
223
|
+
return spawnWithTimeout('osascript', [
|
|
224
|
+
'-e', `display notification "${safeBody}" with title "${safeTitle}"`,
|
|
225
|
+
], {}, 2000);
|
|
226
|
+
} else if (platform === 'win32') {
|
|
227
|
+
const ps = `
|
|
228
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;
|
|
229
|
+
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);
|
|
230
|
+
$textNodes = $template.GetElementsByTagName('text');
|
|
231
|
+
$textNodes.Item(0).AppendChild($template.CreateTextNode('${safeTitle}')) | Out-Null;
|
|
232
|
+
$textNodes.Item(1).AppendChild($template.CreateTextNode('${safeBody}')) | Out-Null;
|
|
233
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($template);
|
|
234
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('AI Assistant').Show($toast);
|
|
235
|
+
`.replace(/\n\s*/g, ' ');
|
|
236
|
+
return spawnWithTimeout('powershell', ['-c', ps], {}, 2000);
|
|
237
|
+
} else {
|
|
238
|
+
// Linux: notify-send
|
|
239
|
+
return spawnWithTimeout('notify-send', [safeTitle, safeBody], {}, 2000);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── 6. speakTTS ──────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/** 跨平台语音播报(7s 超时) */
|
|
246
|
+
function speakTTS(text, config) {
|
|
247
|
+
if (!text) return Promise.resolve(false);
|
|
248
|
+
|
|
249
|
+
const prefix = config.notify.tts.prefix || '';
|
|
250
|
+
const fullText = prefix ? `${prefix},${text}` : text;
|
|
251
|
+
const maxLen = config.notify.tts.maxLength || 80;
|
|
252
|
+
const spoken = fullText.length > maxLen ? fullText.substring(0, maxLen) : fullText;
|
|
253
|
+
|
|
254
|
+
const platform = process.platform;
|
|
255
|
+
|
|
256
|
+
if (platform === 'darwin') {
|
|
257
|
+
return spawnWithTimeout('say', [spoken], {}, 7000);
|
|
258
|
+
} else if (platform === 'win32') {
|
|
259
|
+
const safeTxt = spoken.replace(/'/g, "''");
|
|
260
|
+
return spawnWithTimeout('powershell', [
|
|
261
|
+
'-c', `Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('${safeTxt}')`,
|
|
262
|
+
], {}, 7000);
|
|
263
|
+
} else {
|
|
264
|
+
// Linux: spd-say → espeak
|
|
265
|
+
return spawnWithTimeout('spd-say', [spoken], {}, 7000).then(ok => {
|
|
266
|
+
if (!ok) return spawnWithTimeout('espeak', [spoken], {}, 7000);
|
|
267
|
+
return true;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── 7. run(主入口)──────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
/** 主入口:读配置 → 读 stdin → 提取摘要 → 并行执行通知 */
|
|
275
|
+
async function run(cwd) {
|
|
276
|
+
const config = loadConfig(cwd);
|
|
277
|
+
const methods = config.notify.methods || ['sound'];
|
|
278
|
+
|
|
279
|
+
// 读取 stdin(Claude/Cursor hook 通过 stdin 传入 JSON)
|
|
280
|
+
const stdinData = await readStdin();
|
|
281
|
+
const lastMessage = stdinData && stdinData.last_assistant_message
|
|
282
|
+
? stdinData.last_assistant_message
|
|
283
|
+
: null;
|
|
284
|
+
|
|
285
|
+
// 提取摘要
|
|
286
|
+
const summary = lastMessage ? extractSummary(lastMessage, config) : '';
|
|
287
|
+
|
|
288
|
+
// 并行执行各通知方式
|
|
289
|
+
const tasks = [];
|
|
290
|
+
|
|
291
|
+
if (methods.includes('sound') && config.notify.sound.enabled !== false) {
|
|
292
|
+
tasks.push(playSound(cwd, config.notify.sound.file || 'completed.wav'));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (methods.includes('notification') && config.notify.notification.enabled !== false) {
|
|
296
|
+
const title = config.notify.notification.title || 'AI 任务完成';
|
|
297
|
+
const body = summary || '回答已完成';
|
|
298
|
+
tasks.push(sendNotification(title, body));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (methods.includes('tts') && config.notify.tts.enabled !== false) {
|
|
302
|
+
const ttsText = summary || '回答已完成';
|
|
303
|
+
tasks.push(speakTTS(ttsText, config));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 等待所有通知完成(任何失败都静默忽略)
|
|
307
|
+
await Promise.allSettled(tasks);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { run, readStdin, loadConfig, extractSummary, playSound, sendNotification, speakTTS };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* UserPromptSubmit Hook - 强制技能评估 (跨平台版本)
|
|
4
4
|
* 功能: 开发场景下,将 Skills 激活率从约 25% 提升到 90% 以上
|
|
5
5
|
*
|
|
6
|
-
* 适配项目:
|
|
6
|
+
* 适配项目: leniu (纯后端项目)
|
|
7
7
|
* 架构: 三层架构 (Controller → Service → Mapper)
|
|
8
8
|
* 包名: org.dromara.*
|
|
9
9
|
*/
|
|
@@ -107,6 +107,7 @@ const instructions = `## 强制技能激活流程(必须执行)
|
|
|
107
107
|
- leniu-java-task: 定时任务/XXL-Job/@XxlJob/TenantLoader/任务调度/分布式定时
|
|
108
108
|
- leniu-java-total-line: 合计行/totalLine/报表合计/SUM合计/ReportBaseTotalVO/合计查询
|
|
109
109
|
- leniu-report-customization: 定制报表/汇总报表/report_order_info/report_order_detail/report_account_flow/退款汇总/消费金额统计/订单报表/流水报表
|
|
110
|
+
- leniu-report-standard-customization: 标准版报表/core-report/report_refund/report_refund_detail/经营分析/营业额分析/用户活跃度/菜品排行/操作员统计/账户日结/钱包消费汇总/商户消费汇总/ReportOrderConsumeService/ReportAccountConsumeService
|
|
110
111
|
- leniu-crud-development: CRUD/增删改查/新建模块/Business层/Service/Mapper/Controller/分页查询/LeRequest/PageDTO/PageVO/事务管理
|
|
111
112
|
- leniu-database-ops: 数据库/SQL/建表/双库/商户库/系统库/审计字段/crby/crtime/del_flag
|
|
112
113
|
- leniu-utils-toolkit: 工具类/BeanUtil/StrUtil/CollUtil/ObjectUtil/RedisUtil/JacksonUtil/LeBeanUtil
|
package/.claude/hooks/stop.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Stop Hook - Claude 回答结束时触发
|
|
4
|
-
* 功能: nul 文件清理 +
|
|
4
|
+
* 功能: nul 文件清理 + 智能完成通知(音效/系统通知/TTS)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { execSync } = require('child_process');
|
|
10
9
|
|
|
11
10
|
// 清理可能误创建的 nul 文件(Windows 下 > nul 可能创建该文件)
|
|
12
11
|
const findAndDeleteNul = (dir, depth = 0) => {
|
|
@@ -28,31 +27,7 @@ const findAndDeleteNul = (dir, depth = 0) => {
|
|
|
28
27
|
};
|
|
29
28
|
findAndDeleteNul(process.cwd());
|
|
30
29
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const platform = process.platform;
|
|
36
|
-
if (platform === 'darwin') {
|
|
37
|
-
execSync(`afplay "${audioFile}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
38
|
-
} else if (platform === 'win32') {
|
|
39
|
-
execSync(`powershell -c "(New-Object Media.SoundPlayer '${audioFile.replace(/'/g, "''")}').PlaySync()"`, {
|
|
40
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
41
|
-
});
|
|
42
|
-
} else if (platform === 'linux') {
|
|
43
|
-
try {
|
|
44
|
-
execSync(`aplay "${audioFile}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
45
|
-
} catch {
|
|
46
|
-
try {
|
|
47
|
-
execSync(`paplay "${audioFile}"`, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
48
|
-
} catch {
|
|
49
|
-
// 播放失败,静默忽略
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch {
|
|
55
|
-
// 播放失败时静默忽略
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
process.exit(0);
|
|
30
|
+
// 执行智能通知
|
|
31
|
+
require('./lib/notify.js').run(process.cwd())
|
|
32
|
+
.then(() => process.exit(0))
|
|
33
|
+
.catch(() => process.exit(0));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"notify": {
|
|
3
|
+
"methods": ["sound"],
|
|
4
|
+
"sound": { "enabled": true, "file": "completed.wav" },
|
|
5
|
+
"notification": { "enabled": false, "title": "AI 任务完成" },
|
|
6
|
+
"tts": { "enabled": false, "prefix": "任务完成", "maxLength": 80 },
|
|
7
|
+
"summary": { "maxLength": 100, "strategy": "first-sentence", "stripMarkdown": true }
|
|
8
|
+
}
|
|
9
|
+
}
|