speccore 5.84.2 → 5.85.0

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.
@@ -0,0 +1,765 @@
1
+ "use strict";
2
+ /**
3
+ * prompts - 提示词库管理命令
4
+ * 提供预置提示词模板和用户自定义提示词,支持搜索、分类、CRUD、复制
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.promptsCommand = promptsCommand;
8
+ const logger_1 = require("../utils/logger");
9
+ const fs_extra_1 = require("fs-extra");
10
+ const path_1 = require("path");
11
+ async function promptsCommand(options) {
12
+ try {
13
+ // 收集所有提示词数据
14
+ const presets = await loadPresets();
15
+ const userPrompts = await loadUserPrompts();
16
+ const allPrompts = [...presets, ...userPrompts];
17
+ // 生成 HTML 页面
18
+ const html = generatePromptsHtml(allPrompts);
19
+ const outputPath = options.output || (0, path_1.join)(process.cwd(), 'outputs', 'speccore-prompts.html');
20
+ await (0, fs_extra_1.ensureDir)((0, path_1.join)(process.cwd(), 'outputs'));
21
+ await (0, fs_extra_1.writeFile)(outputPath, html);
22
+ logger_1.logger.info('');
23
+ logger_1.logger.success('✅ 提示词库页面已生成!');
24
+ process.stdout.write(`✅ 页面已生成: file://${outputPath}\n`);
25
+ process.stdout.write(`[SPECCORE_PROMPTS: ${outputPath}]\n`);
26
+ }
27
+ catch (error) {
28
+ logger_1.logger.error(`生成提示词库失败: ${error}`);
29
+ }
30
+ }
31
+ async function loadPresets() {
32
+ const presetsDir = (0, path_1.join)(process.cwd(), '.speccore', 'prompts', 'presets');
33
+ const presets = [];
34
+ if (!(await (0, fs_extra_1.pathExists)(presetsDir)))
35
+ return presets;
36
+ try {
37
+ const files = await (0, fs_extra_1.readdir)(presetsDir);
38
+ for (const file of files.filter(f => f.endsWith('.json'))) {
39
+ const content = await (0, fs_extra_1.readFile)((0, path_1.join)(presetsDir, file), 'utf-8');
40
+ const items = JSON.parse(content);
41
+ presets.push(...items);
42
+ }
43
+ }
44
+ catch { }
45
+ return presets.sort((a, b) => (a.sort || 99) - (b.sort || 99));
46
+ }
47
+ async function loadUserPrompts() {
48
+ const userDir = (0, path_1.join)(process.cwd(), '.speccore', 'prompts', 'user');
49
+ const prompts = [];
50
+ if (!(await (0, fs_extra_1.pathExists)(userDir)))
51
+ return prompts;
52
+ try {
53
+ const files = await (0, fs_extra_1.readdir)(userDir);
54
+ for (const file of files.filter(f => f.endsWith('.json'))) {
55
+ const content = await (0, fs_extra_1.readFile)((0, path_1.join)(userDir, file), 'utf-8');
56
+ const items = JSON.parse(content);
57
+ prompts.push(...items.map((p) => ({ ...p, builtin: false })));
58
+ }
59
+ }
60
+ catch { }
61
+ return prompts.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
62
+ }
63
+ function generatePromptsHtml(prompts) {
64
+ const categories = [...new Set(prompts.map(p => p.category))];
65
+ const categoryLabels = {
66
+ iteration: '迭代',
67
+ analysis: '分析',
68
+ execute: '执行',
69
+ change: '变更',
70
+ custom: '我的'
71
+ };
72
+ return `<!DOCTYPE html>
73
+ <html lang="zh-CN">
74
+ <head>
75
+ <meta charset="UTF-8">
76
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
77
+ <title>SpecCore 提示词库</title>
78
+ <style>
79
+ * { margin: 0; padding: 0; box-sizing: border-box; }
80
+ body {
81
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
82
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
83
+ min-height: 100vh;
84
+ padding: 20px;
85
+ }
86
+ .container {
87
+ max-width: 1200px;
88
+ margin: 0 auto;
89
+ background: white;
90
+ border-radius: 16px;
91
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
92
+ overflow: hidden;
93
+ }
94
+ .header {
95
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
96
+ color: white;
97
+ padding: 30px;
98
+ text-align: center;
99
+ }
100
+ .header h1 { font-size: 28px; margin-bottom: 8px; }
101
+ .header p { opacity: 0.9; font-size: 14px; }
102
+ .toolbar {
103
+ padding: 20px 30px;
104
+ background: #f8f9fa;
105
+ border-bottom: 1px solid #e9ecef;
106
+ display: flex;
107
+ gap: 12px;
108
+ align-items: center;
109
+ flex-wrap: wrap;
110
+ }
111
+ .search-box {
112
+ flex: 1;
113
+ min-width: 200px;
114
+ padding: 10px 16px;
115
+ border: 2px solid #e9ecef;
116
+ border-radius: 8px;
117
+ font-size: 14px;
118
+ transition: all 0.2s;
119
+ }
120
+ .search-box:focus {
121
+ outline: none;
122
+ border-color: #667eea;
123
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
124
+ }
125
+ .category-tabs {
126
+ display: flex;
127
+ gap: 8px;
128
+ flex-wrap: wrap;
129
+ }
130
+ .tab {
131
+ padding: 8px 16px;
132
+ border-radius: 6px;
133
+ cursor: pointer;
134
+ font-size: 13px;
135
+ font-weight: 500;
136
+ transition: all 0.2s;
137
+ background: white;
138
+ border: 1px solid #e9ecef;
139
+ }
140
+ .tab:hover { background: #f1f3f5; }
141
+ .tab.active {
142
+ background: #667eea;
143
+ color: white;
144
+ border-color: #667eea;
145
+ }
146
+ .btn {
147
+ padding: 10px 20px;
148
+ border-radius: 8px;
149
+ border: none;
150
+ cursor: pointer;
151
+ font-size: 14px;
152
+ font-weight: 500;
153
+ transition: all 0.2s;
154
+ }
155
+ .btn-primary {
156
+ background: #667eea;
157
+ color: white;
158
+ }
159
+ .btn-primary:hover { background: #5568d3; }
160
+ .btn-secondary {
161
+ background: #e9ecef;
162
+ color: #495057;
163
+ }
164
+ .btn-secondary:hover { background: #dee2e6; }
165
+ .content {
166
+ padding: 30px;
167
+ }
168
+ .section-title {
169
+ font-size: 18px;
170
+ font-weight: 600;
171
+ margin-bottom: 16px;
172
+ color: #495057;
173
+ }
174
+ .prompt-grid {
175
+ display: grid;
176
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
177
+ gap: 16px;
178
+ margin-bottom: 30px;
179
+ align-items: stretch;
180
+ }
181
+ .prompt-card {
182
+ background: white;
183
+ border: 1px solid #e9ecef;
184
+ border-radius: 12px;
185
+ padding: 20px;
186
+ transition: all 0.2s;
187
+ cursor: pointer;
188
+ height: 100%;
189
+ display: flex;
190
+ flex-direction: column;
191
+ }
192
+ .prompt-card:hover {
193
+ border-color: #667eea;
194
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
195
+ transform: translateY(-2px);
196
+ }
197
+ .prompt-header {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 12px;
201
+ margin-bottom: 12px;
202
+ }
203
+ .prompt-icon {
204
+ font-size: 24px;
205
+ width: 40px;
206
+ height: 40px;
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ background: #f8f9fa;
211
+ border-radius: 8px;
212
+ }
213
+ .prompt-title {
214
+ flex: 1;
215
+ }
216
+ .prompt-title h3 {
217
+ font-size: 15px;
218
+ font-weight: 600;
219
+ color: #212529;
220
+ margin-bottom: 4px;
221
+ }
222
+ .prompt-title p {
223
+ font-size: 12px;
224
+ color: #868e96;
225
+ }
226
+ .prompt-preview {
227
+ background: #f8f9fa;
228
+ border-radius: 8px;
229
+ padding: 12px;
230
+ font-size: 13px;
231
+ color: #495057;
232
+ margin-bottom: 12px;
233
+ font-family: 'SF Mono', Monaco, monospace;
234
+ line-height: 1.5;
235
+ max-height: 80px;
236
+ overflow: hidden;
237
+ position: relative;
238
+ flex: 1;
239
+ }
240
+ .prompt-preview::after {
241
+ content: '';
242
+ position: absolute;
243
+ bottom: 0;
244
+ left: 0;
245
+ right: 0;
246
+ height: 30px;
247
+ background: linear-gradient(transparent, #f8f9fa);
248
+ }
249
+ .prompt-actions {
250
+ display: flex;
251
+ gap: 8px;
252
+ }
253
+ .prompt-actions .btn {
254
+ flex: 1;
255
+ padding: 8px 12px;
256
+ font-size: 13px;
257
+ }
258
+ .badge {
259
+ display: inline-block;
260
+ padding: 2px 8px;
261
+ border-radius: 4px;
262
+ font-size: 11px;
263
+ font-weight: 500;
264
+ }
265
+ .badge-builtin {
266
+ background: #e7f5ff;
267
+ color: #1971c2;
268
+ }
269
+ .badge-custom {
270
+ background: #fff3bf;
271
+ color: #e67700;
272
+ }
273
+ .empty-state {
274
+ text-align: center;
275
+ padding: 60px 20px;
276
+ color: #868e96;
277
+ }
278
+ .empty-state-icon {
279
+ font-size: 48px;
280
+ margin-bottom: 16px;
281
+ opacity: 0.5;
282
+ }
283
+ .modal {
284
+ display: none;
285
+ position: fixed;
286
+ top: 0;
287
+ left: 0;
288
+ right: 0;
289
+ bottom: 0;
290
+ background: rgba(0,0,0,0.5);
291
+ z-index: 1000;
292
+ align-items: center;
293
+ justify-content: center;
294
+ }
295
+ .modal.active { display: flex; }
296
+ .modal-content {
297
+ background: white;
298
+ border-radius: 12px;
299
+ width: 90%;
300
+ max-width: 600px;
301
+ max-height: 90vh;
302
+ overflow-y: auto;
303
+ }
304
+ .modal-header {
305
+ padding: 20px;
306
+ border-bottom: 1px solid #e9ecef;
307
+ display: flex;
308
+ justify-content: space-between;
309
+ align-items: center;
310
+ }
311
+ .modal-header h2 { font-size: 18px; }
312
+ .modal-close {
313
+ background: none;
314
+ border: none;
315
+ font-size: 24px;
316
+ cursor: pointer;
317
+ color: #868e96;
318
+ }
319
+ .modal-body { padding: 20px; }
320
+ .form-group { margin-bottom: 16px; }
321
+ .form-group label {
322
+ display: block;
323
+ font-size: 13px;
324
+ font-weight: 500;
325
+ margin-bottom: 6px;
326
+ color: #495057;
327
+ }
328
+ .form-group input,
329
+ .form-group textarea,
330
+ .form-group select {
331
+ width: 100%;
332
+ padding: 10px 12px;
333
+ border: 1px solid #e9ecef;
334
+ border-radius: 6px;
335
+ font-size: 14px;
336
+ }
337
+ .form-group textarea {
338
+ resize: vertical;
339
+ min-height: 100px;
340
+ font-family: 'SF Mono', Monaco, monospace;
341
+ }
342
+ .modal-footer {
343
+ padding: 20px;
344
+ border-top: 1px solid #e9ecef;
345
+ display: flex;
346
+ gap: 8px;
347
+ justify-content: flex-end;
348
+ }
349
+ .toast {
350
+ position: fixed;
351
+ bottom: 20px;
352
+ right: 20px;
353
+ background: #212529;
354
+ color: white;
355
+ padding: 12px 20px;
356
+ border-radius: 8px;
357
+ font-size: 14px;
358
+ z-index: 2000;
359
+ animation: slideIn 0.3s ease;
360
+ }
361
+ @keyframes slideIn {
362
+ from { transform: translateY(100px); opacity: 0; }
363
+ to { transform: translateY(0); opacity: 1; }
364
+ }
365
+ </style>
366
+ </head>
367
+ <body>
368
+ <div class="container">
369
+ <div class="header">
370
+ <h1>📋 SpecCore 提示词库</h1>
371
+ <p>预置模板 + 自定义提示词,一键复制,高效开发</p>
372
+ </div>
373
+
374
+ <div class="toolbar">
375
+ <input type="text" class="search-box" id="searchBox" placeholder="搜索提示词名称或内容...">
376
+ <div class="category-tabs">
377
+ <div class="tab active" data-category="all">全部</div>
378
+ ${categories.map(c => `<div class="tab" data-category="${c}">${categoryLabels[c] || c}</div>`).join('')}
379
+ </div>
380
+ <button class="btn btn-primary" onclick="openCreateModal()">+ 新建</button>
381
+ </div>
382
+
383
+ <div class="content" id="content">
384
+ <!-- 动态渲染 -->
385
+ </div>
386
+ </div>
387
+
388
+ <!-- 创建/编辑弹窗 -->
389
+ <div class="modal" id="editModal">
390
+ <div class="modal-content">
391
+ <div class="modal-header">
392
+ <h2 id="modalTitle">新建提示词</h2>
393
+ <button class="modal-close" onclick="closeModal()">&times;</button>
394
+ </div>
395
+ <div class="modal-body">
396
+ <div class="form-group">
397
+ <label>名称 *</label>
398
+ <input type="text" id="editName" placeholder="例如:周会汇报">
399
+ </div>
400
+ <div class="form-group">
401
+ <label>分类</label>
402
+ <select id="editCategory">
403
+ <option value="custom">我的</option>
404
+ <option value="iteration">迭代</option>
405
+ <option value="analysis">分析</option>
406
+ <option value="execute">执行</option>
407
+ <option value="change">变更</option>
408
+ </select>
409
+ </div>
410
+ <div class="form-group">
411
+ <label>图标</label>
412
+ <input type="text" id="editIcon" placeholder="例如:📝" value="📝">
413
+ </div>
414
+ <div class="form-group">
415
+ <label>描述</label>
416
+ <input type="text" id="editDescription" placeholder="简短描述这个提示词的用途">
417
+ </div>
418
+ <div class="form-group">
419
+ <label>提示词内容 *</label>
420
+ <textarea id="editPrompt" placeholder="输入提示词内容,使用 {参数名} 作为占位符"></textarea>
421
+ </div>
422
+ <div class="form-group">
423
+ <label>对应命令</label>
424
+ <input type="text" id="editCommand" placeholder="例如:speccore analyze -I Q2">
425
+ </div>
426
+ </div>
427
+ <div class="modal-footer">
428
+ <button class="btn btn-secondary" onclick="closeModal()">取消</button>
429
+ <button class="btn btn-primary" onclick="savePrompt()">保存</button>
430
+ </div>
431
+ </div>
432
+ </div>
433
+
434
+ <script>
435
+ const prompts = ${JSON.stringify(prompts, null, 2)};
436
+ let currentCategory = 'all';
437
+ let searchQuery = '';
438
+ let editingId = null;
439
+
440
+ // 初始化
441
+ document.addEventListener('DOMContentLoaded', () => {
442
+ render();
443
+ document.getElementById('searchBox').addEventListener('input', (e) => {
444
+ searchQuery = e.target.value.toLowerCase();
445
+ render();
446
+ });
447
+ document.querySelectorAll('.tab').forEach(tab => {
448
+ tab.addEventListener('click', () => {
449
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
450
+ tab.classList.add('active');
451
+ currentCategory = tab.dataset.category;
452
+ render();
453
+ });
454
+ });
455
+ });
456
+
457
+ function render() {
458
+ const filtered = prompts.filter(p => {
459
+ const matchCategory = currentCategory === 'all' || p.category === currentCategory;
460
+ const matchSearch = !searchQuery ||
461
+ p.name.toLowerCase().includes(searchQuery) ||
462
+ p.description.toLowerCase().includes(searchQuery) ||
463
+ p.prompt.toLowerCase().includes(searchQuery);
464
+ return matchCategory && matchSearch;
465
+ });
466
+
467
+ const builtin = filtered.filter(p => p.builtin);
468
+ const custom = filtered.filter(p => !p.builtin);
469
+
470
+ let html = '';
471
+
472
+ if (builtin.length > 0) {
473
+ html += '<h2 class="section-title">📌 预置模板</h2>';
474
+ html += '<div class="prompt-grid">';
475
+ html += builtin.map(p => renderCard(p)).join('');
476
+ html += '</div>';
477
+ }
478
+
479
+ if (custom.length > 0) {
480
+ html += '<h2 class="section-title">✏️ 我的提示词</h2>';
481
+ html += '<div class="prompt-grid">';
482
+ html += custom.map(p => renderCard(p)).join('');
483
+ html += '</div>';
484
+ }
485
+
486
+ if (filtered.length === 0) {
487
+ html = '<div class="empty-state"><div class="empty-state-icon">🔍</div><p>没有找到匹配的提示词</p></div>';
488
+ }
489
+
490
+ document.getElementById('content').innerHTML = html;
491
+ }
492
+
493
+ function renderCard(p) {
494
+ const badge = p.builtin
495
+ ? '<span class="badge badge-builtin">预置</span>'
496
+ : '<span class="badge badge-custom">自定义</span>';
497
+
498
+ return \`
499
+ <div class="prompt-card">
500
+ <div class="prompt-header">
501
+ <div class="prompt-icon">\${p.icon || '📝'}</div>
502
+ <div class="prompt-title">
503
+ <h3>\${p.name} \${badge}</h3>
504
+ <p>\${p.description || ''}</p>
505
+ </div>
506
+ </div>
507
+ <div class="prompt-preview">\${escapeHtml(p.prompt)}</div>
508
+ <div class="prompt-actions">
509
+ <button class="btn btn-primary" onclick="copyPrompt('\${p.id}')">📋 复制</button>
510
+ \${!p.builtin ? \`
511
+ <button class="btn btn-secondary" onclick="editPrompt('\${p.id}')">编辑</button>
512
+ <button class="btn btn-secondary" onclick="deletePrompt('\${p.id}')">删除</button>
513
+ \` : ''}
514
+ </div>
515
+ </div>
516
+ \`;
517
+ }
518
+
519
+ function escapeHtml(str) {
520
+ const div = document.createElement('div');
521
+ div.textContent = str;
522
+ return div.innerHTML;
523
+ }
524
+
525
+ function copyPrompt(id) {
526
+ const p = prompts.find(x => x.id === id);
527
+ if (!p) return;
528
+
529
+ // 如果有参数,显示自定义弹框
530
+ if (p.params && p.params.length > 0) {
531
+ showParamModal(p);
532
+ } else {
533
+ // 无参数,直接复制
534
+ const text = '/spec-ask "' + p.prompt + '"';
535
+ copyToClipboard(text);
536
+ }
537
+ }
538
+
539
+ function showParamModal(p) {
540
+ // 创建参数输入弹框
541
+ const modal = document.createElement('div');
542
+ modal.className = 'modal active';
543
+ modal.innerHTML = \`
544
+ <div class="modal-content" style="max-width: 600px;">
545
+ <div class="modal-header">
546
+ <h2>\${p.icon || '📝'} \${p.name}</h2>
547
+ <button class="modal-close" onclick="this.closest('.modal').remove()">&times;</button>
548
+ </div>
549
+ <div class="modal-body">
550
+ <p style="color: #868e96; font-size: 13px; margin-bottom: 16px;">\${p.description || '请填写以下参数'}</p>
551
+ \${p.params.map(param => \`
552
+ <div class="form-group">
553
+ <label>\${param.key} \${param.required ? '<span style="color: #e03131;">*</span>' : ''}</label>
554
+ <input type="text" id="param-\${param.key}" placeholder="\${param.placeholder || ''}" value="\${param.placeholder || ''}" oninput="updatePreview('\${p.id}')">
555
+ </div>
556
+ \`).join('')}
557
+ <div class="form-group" style="margin-top: 20px;">
558
+ <label style="font-weight: 600; color: #495057;"> 预览</label>
559
+ <div id="preview-box" style="background: #f8f9fa; border-radius: 8px; padding: 12px; font-size: 13px; color: #495057; line-height: 1.6; min-height: 60px; border: 1px solid #e9ecef;">
560
+ \${escapeHtml(p.prompt)}
561
+ </div>
562
+ </div>
563
+ </div>
564
+ <div class="modal-footer">
565
+ <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
566
+ <button class="btn btn-primary" onclick="submitParams('\${p.id}')">📋 复制</button>
567
+ </div>
568
+ </div>
569
+ \`;
570
+ document.body.appendChild(modal);
571
+
572
+ // 聚焦第一个输入框
573
+ setTimeout(() => {
574
+ const firstInput = modal.querySelector('input');
575
+ if (firstInput) firstInput.focus();
576
+ }, 100);
577
+ }
578
+
579
+ function updatePreview(id) {
580
+ const p = prompts.find(x => x.id === id);
581
+ if (!p) return;
582
+
583
+ let text = p.prompt;
584
+ p.params.forEach(param => {
585
+ const input = document.getElementById('param-' + param.key);
586
+ const value = input ? input.value.trim() : '';
587
+ if (value) {
588
+ text = text.replace(new RegExp('\\{' + param.key + '\\}', 'g'), value);
589
+ }
590
+ });
591
+
592
+ const previewBox = document.getElementById('preview-box');
593
+ if (previewBox) {
594
+ previewBox.innerHTML = '<strong>/spec-ask "</strong>' + escapeHtml(text) + '<strong>"</strong>';
595
+ }
596
+ }
597
+
598
+ function submitParams(id) {
599
+ const p = prompts.find(x => x.id === id);
600
+ if (!p) return;
601
+
602
+ let text = p.prompt;
603
+ let allFilled = true;
604
+
605
+ p.params.forEach(param => {
606
+ const input = document.getElementById('param-' + param.key);
607
+ const value = input ? input.value.trim() : '';
608
+
609
+ if (param.required && !value) {
610
+ allFilled = false;
611
+ input.style.borderColor = '#e03131';
612
+ } else {
613
+ input.style.borderColor = '#e9ecef';
614
+ text = text.replace(new RegExp('\\{' + param.key + '\\}', 'g'), value);
615
+ }
616
+ });
617
+
618
+ if (!allFilled) {
619
+ showToast('⚠️ 请填写必填参数');
620
+ return;
621
+ }
622
+
623
+ // 关闭弹框
624
+ const modal = document.querySelector('.modal.active');
625
+ if (modal) modal.remove();
626
+
627
+ // 复制
628
+ const finalText = '/spec-ask "' + text + '"';
629
+ copyToClipboard(finalText);
630
+ }
631
+
632
+ function copyToClipboard(text) {
633
+ navigator.clipboard.writeText(text).then(() => {
634
+ showToast('✅ 已复制到剪贴板');
635
+ }).catch(() => {
636
+ // 降级方案
637
+ const textarea = document.createElement('textarea');
638
+ textarea.value = text;
639
+ document.body.appendChild(textarea);
640
+ textarea.select();
641
+ document.execCommand('copy');
642
+ document.body.removeChild(textarea);
643
+ showToast('✅ 已复制到剪贴板');
644
+ });
645
+ }
646
+
647
+ function openCreateModal() {
648
+ editingId = null;
649
+ document.getElementById('modalTitle').textContent = '新建提示词';
650
+ document.getElementById('editName').value = '';
651
+ document.getElementById('editCategory').value = 'custom';
652
+ document.getElementById('editIcon').value = '📝';
653
+ document.getElementById('editDescription').value = '';
654
+ document.getElementById('editPrompt').value = '';
655
+ document.getElementById('editCommand').value = '';
656
+ document.getElementById('editModal').classList.add('active');
657
+ }
658
+
659
+ function editPrompt(id) {
660
+ const p = prompts.find(x => x.id === id);
661
+ if (!p || p.builtin) return;
662
+
663
+ editingId = id;
664
+ document.getElementById('modalTitle').textContent = '编辑提示词';
665
+ document.getElementById('editName').value = p.name;
666
+ document.getElementById('editCategory').value = p.category;
667
+ document.getElementById('editIcon').value = p.icon || '📝';
668
+ document.getElementById('editDescription').value = p.description || '';
669
+ document.getElementById('editPrompt').value = p.prompt;
670
+ document.getElementById('editCommand').value = p.command || '';
671
+ document.getElementById('editModal').classList.add('active');
672
+ }
673
+
674
+ function closeModal() {
675
+ document.getElementById('editModal').classList.remove('active');
676
+ }
677
+
678
+ function savePrompt() {
679
+ const name = document.getElementById('editName').value.trim();
680
+ const prompt = document.getElementById('editPrompt').value.trim();
681
+
682
+ if (!name || !prompt) {
683
+ alert('请填写名称和提示词内容');
684
+ return;
685
+ }
686
+
687
+ const data = {
688
+ id: editingId || 'custom-' + Date.now(),
689
+ name,
690
+ category: document.getElementById('editCategory').value,
691
+ icon: document.getElementById('editIcon').value || '📝',
692
+ description: document.getElementById('editDescription').value.trim(),
693
+ prompt,
694
+ command: document.getElementById('editCommand').value.trim(),
695
+ params: extractParams(prompt),
696
+ builtin: false,
697
+ sort: 99,
698
+ createdAt: editingId ? undefined : new Date().toISOString(),
699
+ updatedAt: new Date().toISOString()
700
+ };
701
+
702
+ // 保存到 localStorage
703
+ const stored = JSON.parse(localStorage.getItem('speccore-user-prompts') || '[]');
704
+ if (editingId) {
705
+ const idx = stored.findIndex(p => p.id === editingId);
706
+ if (idx >= 0) stored[idx] = { ...stored[idx], ...data };
707
+ } else {
708
+ stored.push(data);
709
+ }
710
+ localStorage.setItem('speccore-user-prompts', JSON.stringify(stored));
711
+
712
+ // 更新内存数据
713
+ if (editingId) {
714
+ const idx = prompts.findIndex(p => p.id === editingId);
715
+ if (idx >= 0) prompts[idx] = { ...prompts[idx], ...data };
716
+ } else {
717
+ prompts.push(data);
718
+ }
719
+
720
+ closeModal();
721
+ render();
722
+ showToast(editingId ? '✅ 已更新' : '✅ 已创建');
723
+ }
724
+
725
+ function deletePrompt(id) {
726
+ if (!confirm('确定要删除这个提示词吗?')) return;
727
+
728
+ const idx = prompts.findIndex(p => p.id === id);
729
+ if (idx >= 0) prompts.splice(idx, 1);
730
+
731
+ const stored = JSON.parse(localStorage.getItem('speccore-user-prompts') || '[]');
732
+ const filtered = stored.filter(p => p.id !== id);
733
+ localStorage.setItem('speccore-user-prompts', JSON.stringify(filtered));
734
+
735
+ render();
736
+ showToast('✅ 已删除');
737
+ }
738
+
739
+ function extractParams(text) {
740
+ const matches = text.match(/\{([^}]+)\}/g) || [];
741
+ return matches.map(m => ({
742
+ key: m.slice(1, -1),
743
+ placeholder: '',
744
+ required: false
745
+ }));
746
+ }
747
+
748
+ function showToast(msg) {
749
+ const toast = document.createElement('div');
750
+ toast.className = 'toast';
751
+ toast.textContent = msg;
752
+ document.body.appendChild(toast);
753
+ setTimeout(() => toast.remove(), 2000);
754
+ }
755
+
756
+ // 加载 localStorage 中的用户提示词
757
+ const userStored = JSON.parse(localStorage.getItem('speccore-user-prompts') || '[]');
758
+ userStored.forEach(p => {
759
+ if (!prompts.find(x => x.id === p.id)) prompts.push(p);
760
+ });
761
+ </script>
762
+ </body>
763
+ </html>`;
764
+ }
765
+ //# sourceMappingURL=prompts.js.map