skill-forge-hub 1.1.0 → 1.3.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.
Files changed (108) hide show
  1. package/CHANGELOG.en.md +18 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.en.md +3 -1
  4. package/README.md +3 -1
  5. package/SKILLS_CATALOG.md +119 -0
  6. package/annual-report/SKILL.md +3 -3
  7. package/annual-report/resources/reference.html +2 -2
  8. package/annual-report/resources/skillforge-guide.html +1 -0
  9. package/annual-report/resources/skillforge-guide.txt +1 -0
  10. package/annual-report/user-config.md +1 -1
  11. package/article-to-html/SKILL.md +1 -1
  12. package/article-to-html/resources/skillforge-guide.html +1 -0
  13. package/article-to-html/resources/skillforge-guide.txt +1 -0
  14. package/article-to-html/user-config.md +1 -1
  15. package/cli.js +54 -14
  16. package/commit-heatmap/SKILL.md +2 -2
  17. package/commit-heatmap/resources/skillforge-guide.html +1 -0
  18. package/commit-heatmap/resources/skillforge-guide.txt +1 -0
  19. package/commit-heatmap/user-config.md +1 -1
  20. package/daily-report/SKILL.md +2 -2
  21. package/daily-report/resources/skillforge-guide.html +1 -0
  22. package/daily-report/resources/skillforge-guide.txt +1 -0
  23. package/daily-report/user-config.md +1 -1
  24. package/data-prepper/SKILL.md +2 -2
  25. package/data-prepper/resources/skillforge-guide.html +1 -0
  26. package/data-prepper/resources/skillforge-guide.txt +1 -0
  27. package/data-prepper/user-config.md +1 -1
  28. package/decision-log/SKILL.md +2 -2
  29. package/decision-log/resources/skillforge-guide.html +1 -0
  30. package/decision-log/resources/skillforge-guide.txt +1 -0
  31. package/decision-log/user-config.md +1 -1
  32. package/deck-stage/SKILL.md +3 -10
  33. package/deck-stage/resources/skillforge-guide.html +1 -0
  34. package/deck-stage/resources/skillforge-guide.txt +1 -0
  35. package/docs/articles/ai-daily-report-correct-pose.md +39 -0
  36. package/docs/articles/ai-prompt-management-skill-pouch.md +37 -0
  37. package/docs/articles/csdn-skillforge-21-skills.md +89 -0
  38. package/docs/directory-submission.md +106 -0
  39. package/docs/install.en.md +3 -3
  40. package/docs/install.md +3 -3
  41. package/docs/usage-examples.en.md +1 -1
  42. package/docs/usage-examples.md +1 -1
  43. package/magic-deck/SKILL.md +4 -5
  44. package/magic-deck/resources/skillforge-guide.html +1 -0
  45. package/magic-deck/resources/skillforge-guide.txt +1 -0
  46. package/magic-deck/user-config.md +1 -1
  47. package/meeting-minutes/SKILL.md +1 -1
  48. package/meeting-minutes/resources/skillforge-guide.html +1 -0
  49. package/meeting-minutes/resources/skillforge-guide.txt +1 -0
  50. package/meeting-minutes/user-config.md +1 -1
  51. package/meta-forge/SKILL.md +2 -2
  52. package/meta-forge/resources/skillforge-guide.html +1 -0
  53. package/meta-forge/resources/skillforge-guide.txt +1 -0
  54. package/meta-forge/user-config.md +1 -1
  55. package/monthly-report/SKILL.md +1 -1
  56. package/monthly-report/resources/skillforge-guide.html +1 -0
  57. package/monthly-report/resources/skillforge-guide.txt +1 -0
  58. package/monthly-report/user-config.md +1 -1
  59. package/one-pager/SKILL.md +20 -8
  60. package/one-pager/resources/design-process.md +117 -0
  61. package/one-pager/resources/generate.md +19 -7
  62. package/one-pager/resources/html-rules.md +55 -10
  63. package/one-pager/resources/reference.html +1192 -111
  64. package/one-pager/resources/skillforge-guide.html +1 -0
  65. package/one-pager/resources/skillforge-guide.txt +1 -0
  66. package/one-pager/resources/usage-guide.md +18 -4
  67. package/one-pager/resources/user-template-example.html +63 -0
  68. package/one-pager/user-config.md +7 -4
  69. package/package.json +3 -2
  70. package/prompt-pouch/SKILL.md +4 -15
  71. package/prompt-pouch/resources/reference.html +388 -126
  72. package/prompt-pouch/resources/skillforge-guide.html +1 -0
  73. package/prompt-pouch/resources/skillforge-guide.txt +1 -0
  74. package/prompt-pouch/resources/usage-guide.md +4 -3
  75. package/quarterly-report/SKILL.md +1 -1
  76. package/quarterly-report/resources/skillforge-guide.html +1 -0
  77. package/quarterly-report/resources/skillforge-guide.txt +1 -0
  78. package/quarterly-report/user-config.md +1 -1
  79. package/release-notes/SKILL.md +2 -2
  80. package/release-notes/resources/skillforge-guide.html +1 -0
  81. package/release-notes/resources/skillforge-guide.txt +1 -0
  82. package/release-notes/user-config.md +1 -1
  83. package/skill-doctor/SKILL.md +2 -2
  84. package/skill-doctor/resources/skillforge-guide.html +1 -0
  85. package/skill-doctor/resources/skillforge-guide.txt +1 -0
  86. package/skill-doctor/user-config.md +1 -1
  87. package/spec-doc2spec/SKILL.md +3 -9
  88. package/spec-doc2spec/resources/skillforge-guide.html +1 -0
  89. package/spec-doc2spec/resources/skillforge-guide.txt +1 -0
  90. package/spec-doc2spec/user-config.md +1 -1
  91. package/spec-spec2doc/SKILL.md +3 -12
  92. package/spec-spec2doc/resources/skillforge-guide.html +1 -0
  93. package/spec-spec2doc/resources/skillforge-guide.txt +1 -0
  94. package/spec-spec2doc/user-config.md +1 -1
  95. package/team-report-collector/SKILL.md +2 -2
  96. package/team-report-collector/resources/skillforge-guide.html +1 -0
  97. package/team-report-collector/resources/skillforge-guide.txt +1 -0
  98. package/team-report-collector/user-config.md +1 -1
  99. package/template/SKILL.md +2 -2
  100. package/template/user-config.md +1 -1
  101. package/twin-docs/SKILL.md +2 -2
  102. package/twin-docs/resources/skillforge-guide.html +1 -0
  103. package/twin-docs/resources/skillforge-guide.txt +1 -0
  104. package/twin-docs/user-config.md +1 -1
  105. package/weekly-report/SKILL.md +2 -2
  106. package/weekly-report/resources/skillforge-guide.html +1 -0
  107. package/weekly-report/resources/skillforge-guide.txt +1 -0
  108. package/weekly-report/user-config.md +1 -1
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="sf-template" content="prompt-pouch@2">
6
+ <meta name="sf-template" content="prompt-pouch@10">
7
7
  <meta name="pouch-id" content="REF-POUCH-0000">
8
8
  <title>锦囊 Prompt Pouch — 提示词管理台</title>
9
9
  <style>
@@ -200,12 +200,13 @@ header{
200
200
  /* 侧栏 */
201
201
  aside{ width:200px; flex:0 0 auto;
202
202
  display:flex; flex-direction:column;
203
- position:sticky; top:80px; align-self:flex-start;
204
- height:calc(100vh - 100px);
203
+ align-self:flex-start; /* 自然流式随页面整体滚动:说明卡完整展示,页面滚到底即可读完 */
205
204
  }
206
- /* 侧栏分组滚动区:收藏 + 分组局部滚动,分组下方紧跟「+ 新建分组」,滚动流末尾附「锦囊用法」说明——滚动区占满「全部」与侧栏底缘之间的剩余屏幕高度(最大占满屏幕),说明被顶出首屏(滚到底可见);窄滚动条 */
205
+ /* 侧栏分组滚动区:收藏 + 分组局部滚动——滚动区最大高度 = 视口高度减去顶栏、「全部」行与底部「锦囊用法」说明卡(完整展示不裁剪),分组再多说明卡也完整可见;窄滚动条 */
207
206
  .nav-scroll{
208
- flex:1 1 auto; min-height:0; overflow-y:auto;
207
+ flex:0 0 auto; min-height:0;
208
+ max-height:calc(100vh - 360px);
209
+ overflow-y:auto;
209
210
  margin:0 -4px 0 0; padding-right:4px;
210
211
  scrollbar-width:thin; scrollbar-color:var(--border) transparent;
211
212
  }
@@ -231,8 +232,8 @@ aside{ width:200px; flex:0 0 auto;
231
232
  }
232
233
  .nav-item.on .cnt{ color:var(--gold); background:var(--gold-dim); }
233
234
  .side-tip{
234
- flex:0 0 auto;
235
- margin:16px 4px 0 0; padding:12px 14px; border-radius:10px; /* margin-right 补偿 nav-scroll 负 margin(-4px)的右侧裁切,虚线边框完整不裁;滚动流末尾,分组多时被顶出首屏 */
235
+ flex:0 0 auto; /* 完整展示不裁剪:分组滚动区为其留出空间,侧栏随页面整体滚动即可读完 */
236
+ margin:12px 0 0 0; padding:12px 14px 14px; border-radius:10px;
236
237
  border:1px dashed var(--border); font-size:11px; line-height:1.9; color:var(--muted);
237
238
  }
238
239
  .side-tip b{ color:var(--gold); font-weight:700; }
@@ -246,13 +247,38 @@ main{ flex:1; min-width:0; }
246
247
  font-weight:900;
247
248
  }
248
249
  .bar .total{ font-size:12px; color:var(--muted); }
249
- .sort-box{ margin-left:auto; display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); }
250
- .sort-box select{
250
+ .sort-box{ margin-left:auto; display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); position:relative; }
251
+ .sort-sel{
252
+ display:flex; align-items:center; gap:7px;
251
253
  background:var(--panel2); border:1px solid var(--border); color:var(--text);
252
254
  font-family:inherit; font-size:12px; letter-spacing:1px;
253
- border-radius:999px; padding:5px 12px; outline:none; cursor:pointer;
254
- }
255
- .sort-box select:focus{ border-color:var(--gold); }
255
+ border-radius:999px; padding:5px 13px; outline:none; cursor:pointer;
256
+ transition:border-color .15s ease;
257
+ }
258
+ .sort-sel:hover,.sort-sel.open{ border-color:var(--gold); }
259
+ .sort-arrow{ font-size:9px; color:var(--gold); line-height:1; transition:transform .2s ease; }
260
+ .sort-sel.open .sort-arrow{ transform:rotate(180deg); }
261
+ .sort-menu{
262
+ position:absolute; top:calc(100% + 6px); right:0; z-index:130;
263
+ display:flex; flex-direction:column; gap:2px; min-width:136px;
264
+ background:var(--panel2); border:1px solid var(--border); border-radius:12px;
265
+ padding:5px; box-shadow:0 12px 32px rgba(0,0,0,.45);
266
+ animation:sortMenuIn .16s ease;
267
+ }
268
+ .sort-menu[hidden]{ display:none; }
269
+ @keyframes sortMenuIn{ from{ opacity:0; transform:translateY(-4px); } to{ opacity:1; transform:none; } }
270
+ .sort-opt{
271
+ display:flex; align-items:center; gap:8px; width:100%;
272
+ background:transparent; border:none; border-radius:8px;
273
+ color:var(--text); font-family:inherit; font-size:12px; letter-spacing:1px;
274
+ padding:7px 10px; cursor:pointer; text-align:left;
275
+ transition:background .12s ease,color .12s ease;
276
+ }
277
+ .sort-opt:hover{ background:var(--gold-dim); color:var(--gold); }
278
+ .sort-check{ width:14px; font-size:11px; color:var(--gold); opacity:0; flex:0 0 auto; }
279
+ .sort-opt.on{ color:var(--gold); font-weight:600; }
280
+ .sort-opt.on .sort-check{ opacity:1; }
281
+ [data-theme="light"] .sort-menu{ box-shadow:0 10px 28px rgba(30,31,34,.16); }
256
282
  .sort-status{
257
283
  font-size:12px; color:var(--muted);
258
284
  display:flex; align-items:center; gap:10px;
@@ -449,6 +475,38 @@ mark.hl{ background:var(--gold-dim); color:var(--gold); border-radius:3px; paddi
449
475
  color:var(--muted); font-family:inherit; font-size:12px; line-height:1.7;
450
476
  }
451
477
  .fp-ops{ display:flex; justify-content:flex-end; gap:10px; }
478
+ /* ============ 统一确认弹框(替代浏览器原生弹框) ============ */
479
+ .cf-panel{
480
+ width:460px; max-width:92vw; max-height:82vh; display:flex; flex-direction:column;
481
+ background:linear-gradient(180deg, var(--panel) 0%, var(--panel2) 100%);
482
+ border:1px solid var(--border); border-radius:16px; padding:20px 22px;
483
+ box-shadow:0 20px 60px rgba(0,0,0,.5);
484
+ animation:cfPop .18s ease;
485
+ }
486
+ @keyframes cfPop{ from{ transform:translateY(10px) scale(.97); opacity:0; } to{ transform:none; opacity:1; } }
487
+ .cf-title{
488
+ font-size:15px; font-weight:900; letter-spacing:1px; margin-bottom:10px;
489
+ background:linear-gradient(90deg,var(--star),var(--gold));
490
+ -webkit-background-clip:text; background-clip:text; color:transparent;
491
+ }
492
+ .cf-body{
493
+ font-size:13px; line-height:1.9; color:var(--text); white-space:pre-wrap; word-break:break-word;
494
+ max-height:46vh; overflow-y:auto; padding-right:4px;
495
+ }
496
+ .cf-field{ display:flex; flex-direction:column; gap:4px; margin-top:12px; }
497
+ .cf-field label{ font-size:12px; font-weight:700; color:var(--gold); letter-spacing:.5px; }
498
+ .cf-field input{
499
+ width:100%; padding:9px 12px; border-radius:9px;
500
+ background:rgba(0,0,0,.25); border:1px solid var(--border); outline:none;
501
+ color:var(--text); font-family:inherit; font-size:13px; transition:border-color .18s ease, box-shadow .18s ease;
502
+ }
503
+ .cf-field input:focus{ border-color:var(--gold); box-shadow:0 0 0 3px var(--gold-dim); }
504
+ .cf-ops{ display:flex; justify-content:flex-end; gap:10px; margin-top:16px; }
505
+ .btn.danger{
506
+ background:linear-gradient(135deg, #a63d34, #e06c5b); color:#fff;
507
+ font-weight:900; border:none;
508
+ }
509
+ .btn.danger:hover{ box-shadow:0 0 16px rgba(224,108,91,.45); }
452
510
  /* 备份与数据安全 */
453
511
  .bk-wrap{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
454
512
  .bk-status{ font-size:12px; color:var(--muted); letter-spacing:1px; }
@@ -521,9 +579,10 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
521
579
  }
522
580
  /* 页面直写:侧栏分组管理 / 提示词表单 / toast */
523
581
  .nav-add{
524
- display:flex; align-items:center; justify-content:center; gap:6px;
525
- margin-top:10px; padding:10px 14px; border-radius:10px; cursor:pointer;
526
- font-size:13px; letter-spacing:1px; color:var(--gold);
582
+ flex:0 0 auto; /* 「全部」行右侧迷你 + 图标(新建分组),行内不占独立高度 */
583
+ width:22px; height:22px; margin-left:6px;
584
+ display:flex; align-items:center; justify-content:center;
585
+ border-radius:6px; cursor:pointer; font-size:14px; line-height:1; color:var(--gold);
527
586
  border:1px dashed var(--border); background:var(--gold-dim);
528
587
  transition:all .18s ease; user-select:none;
529
588
  }
@@ -636,13 +695,16 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
636
695
  <span class="tag-filter" id="tagFilterChip" style="display:none"><span class="tf-ico">🏷</span><b id="tagFilterName"></b><button class="tf-x" id="tagFilterClear" title="清除标签过滤">✕</button></span>
637
696
  <button class="btn primary" id="btnAddItem">+ 新增提示词</button>
638
697
  <div class="sort-box">
639
- <label for="sortSel">排序</label>
640
- <select id="sortSel" title="切换卡片排序方式">
641
- <option value="added">最新添加</option>
642
- <option value="recent">最近使用</option>
643
- <option value="freq">使用频率</option>
644
- <option value="manual">手动顺序</option>
645
- </select>
698
+ <label>排序</label>
699
+ <button class="sort-sel" id="sortSel" type="button" title="切换卡片排序方式">
700
+ <span class="sort-sel-txt" id="sortTxt">最新添加</span><span class="sort-arrow">▾</span>
701
+ </button>
702
+ <div class="sort-menu" id="sortMenu" hidden>
703
+ <button class="sort-opt on" data-mode="added" type="button"><span class="sort-check">✓</span>最新添加</button>
704
+ <button class="sort-opt" data-mode="recent" type="button"><span class="sort-check">✓</span>最近使用</button>
705
+ <button class="sort-opt" data-mode="freq" type="button"><span class="sort-check">✓</span>使用频率</button>
706
+ <button class="sort-opt" data-mode="manual" type="button"><span class="sort-check">✓</span>手动顺序</button>
707
+ </div>
646
708
  </div>
647
709
  <div class="sort-status" id="sortStatus">
648
710
  <button class="btn" id="btnSortCmd" disabled>📋 复制变更指令</button>
@@ -757,6 +819,22 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
757
819
  </div>
758
820
  </div>
759
821
 
822
+ <!-- 统一确认弹框(确认 / 提示 / 输入,替代浏览器原生弹框) -->
823
+ <div class="modal-mask" id="cfMask">
824
+ <div class="cf-panel">
825
+ <div class="cf-title" id="cfTitle">确认</div>
826
+ <div class="cf-body" id="cfBody"></div>
827
+ <div class="cf-field" id="cfFieldWrap" style="display:none">
828
+ <label id="cfLabel"></label>
829
+ <input id="cfInput" />
830
+ </div>
831
+ <div class="cf-ops">
832
+ <button class="btn" id="cfCancel">取消</button>
833
+ <button class="btn primary" id="cfOk">确定</button>
834
+ </div>
835
+ </div>
836
+ </div>
837
+
760
838
  <div class="toast" id="toast"></div>
761
839
 
762
840
  <footer>
@@ -812,6 +890,9 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
812
890
  var searchClear = document.getElementById('searchClear');
813
891
  var sortStatus = document.getElementById('sortStatus');
814
892
  var sortSel = document.getElementById('sortSel');
893
+ var sortMenu = document.getElementById('sortMenu');
894
+ var sortTxt = document.getElementById('sortTxt');
895
+ var SORT_LABELS = { added:'最新添加', recent:'最近使用', freq:'使用频率', manual:'手动顺序' };
815
896
  var btnSortCmd = document.getElementById('btnSortCmd');
816
897
  var btnMarkSaved = document.getElementById('btnMarkSaved');
817
898
  var bkStatus = document.getElementById('bkStatus');
@@ -944,16 +1025,15 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
944
1025
  var html = '';
945
1026
  var all = state.data.prompts.length;
946
1027
  var favN = state.data.prompts.filter(function(p){ return p.favorite; }).length;
947
- html += '<div class="nav-item' + (state.curGroup==='all'?' on':'') + '" data-g="all"><span class="icon">🗂️</span>全部<span class="cnt">' + all + '</span></div>';
1028
+ html += '<div class="nav-item' + (state.curGroup==='all'?' on':'') + '" data-g="all"><span class="icon">🗂️</span>全部<span class="cnt">' + all + '</span><span class="nav-add" id="navAdd" title="新建分组">+</span></div>';
948
1029
  html += '<div class="nav-scroll">';
949
1030
  html += '<div class="nav-item' + (state.curGroup==='fav'?' on':'') + '" data-g="fav"><span class="icon">⭐</span>收藏<span class="cnt">' + favN + '</span></div>';
950
1031
  state.data.groups.forEach(function(g){
951
1032
  var n = state.data.prompts.filter(function(p){ return p.group === g.id; }).length;
952
1033
  html += '<div class="nav-item' + (state.curGroup===g.id?' on':'') + '" data-g="' + g.id + '" draggable="true" title="拖拽调整分组顺序"><span class="icon">' + g.icon + '</span><span class="g-name">' + escapeHtml(g.name) + '</span><span class="cnt">' + n + '</span><span class="g-ops"><button class="g-edit" title="编辑分组">✎</button><button class="g-del" title="删除分组">🗑</button></span></div>';
953
1034
  });
954
- html += '<div class="nav-add" id="navAdd">+ 新建分组</div>';
955
- html += '<div class="side-tip"><b>锦囊用法</b><br>· 页面直接增删改:+ 新建分组 / + 新增提示词 / ✎ 编辑 / 🗑 删除(授权目录即写即存)<br>· 也可对 AI 说「把 XX 加到写作输出分组」<br>· 顶栏切换排序;⇄ 移组、拖卡片排序自动保存;拖分组条调整分组顺序<br>· 点卡片标签按标签筛选(再点取消);标签悬停 ✕ 删 / + 加;⤓ 导出 / ⤒ 导入 JSON 备份<br>· ⭐ 收藏常用 · 复制次数在卡片右上角</div>';
956
1035
  html += '</div>';
1036
+ html += '<div class="side-tip"><b>锦囊用法</b><br>· 页面直接增删改:+ 新建分组 / + 新增提示词 / ✎ 编辑 / 🗑 删除(授权目录即写即存)<br>· 也可对 AI 说「把 XX 加到写作输出分组」<br>· 顶栏切换排序;⇄ 移组、拖卡片排序自动保存;拖分组条调整分组顺序<br>· 点卡片标签按标签筛选(再点取消);标签悬停 ✕ 删 / + 加;⤓ 导出 / ⤒ 导入 JSON 备份<br>· ⭐ 收藏常用 · 复制次数在卡片右上角</div>';
957
1037
  sideNav.innerHTML = html;
958
1038
  sideNav.querySelectorAll('.nav-item').forEach(function(el){
959
1039
  el.addEventListener('click', function(){
@@ -1001,7 +1081,7 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1001
1081
  });
1002
1082
  });
1003
1083
  var addBtn = sideNav.querySelector('#navAdd');
1004
- if (addBtn) addBtn.addEventListener('click', function(){ openGroupForm(null); });
1084
+ if (addBtn) addBtn.addEventListener('click', function(e){ e.stopPropagation(); openGroupForm(null); });
1005
1085
  }
1006
1086
 
1007
1087
  function render(){
@@ -1198,7 +1278,12 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1198
1278
  persistUserState();
1199
1279
  scheduleBackup();
1200
1280
  }
1201
- function syncSortUI(){ if (sortSel) sortSel.value = state.sortMode; }
1281
+ function syncSortUI(){
1282
+ if (sortTxt) sortTxt.textContent = SORT_LABELS[state.sortMode] || SORT_LABELS.added;
1283
+ if (sortMenu) sortMenu.querySelectorAll('.sort-opt').forEach(function(o){
1284
+ o.classList.toggle('on', o.getAttribute('data-mode') === state.sortMode);
1285
+ });
1286
+ }
1202
1287
  function ensureManual(){
1203
1288
  if (state.sortMode === 'manual') return;
1204
1289
  state.sortMode = 'manual';
@@ -1279,10 +1364,30 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1279
1364
  if (!gpopEl.contains(e.target) && !onAnchor){ closeGpop(); }
1280
1365
  }
1281
1366
  });
1282
- sortSel.addEventListener('change', function(){
1283
- state.sortMode = sortSel.value;
1367
+ function toggleSortMenu(force){
1368
+ if (!sortMenu) return;
1369
+ var willOpen = (force !== undefined) ? force : sortMenu.hidden;
1370
+ sortMenu.hidden = !willOpen;
1371
+ sortSel.classList.toggle('open', willOpen);
1372
+ }
1373
+ sortSel.addEventListener('click', function(e){
1374
+ e.stopPropagation();
1375
+ toggleSortMenu();
1376
+ });
1377
+ sortMenu.addEventListener('click', function(e){
1378
+ var opt = e.target.closest('.sort-opt');
1379
+ if (!opt) return;
1380
+ state.sortMode = opt.getAttribute('data-mode');
1284
1381
  persistUserState();
1285
1382
  render();
1383
+ syncSortUI();
1384
+ toggleSortMenu(false);
1385
+ });
1386
+ document.addEventListener('click', function(e){
1387
+ if (!sortMenu.hidden){
1388
+ var onSel = e.target.closest && e.target.closest('#sortSel');
1389
+ if (!sortMenu.contains(e.target) && !onSel){ toggleSortMenu(false); }
1390
+ }
1286
1391
  });
1287
1392
 
1288
1393
  /* ---------- 模板插值({{变量}} / {变量}) ---------- */
@@ -1417,6 +1522,71 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1417
1522
  if (toastTimer) clearTimeout(toastTimer);
1418
1523
  toastTimer = setTimeout(function(){ el.className = 'toast'; }, 2800);
1419
1524
  }
1525
+ /* ---------- 统一确认弹框(替代原生 confirm/alert/prompt) ---------- */
1526
+ var cfMask = document.getElementById('cfMask');
1527
+ var cfTitle = document.getElementById('cfTitle');
1528
+ var cfBody = document.getElementById('cfBody');
1529
+ var cfFieldWrap = document.getElementById('cfFieldWrap');
1530
+ var cfLabel = document.getElementById('cfLabel');
1531
+ var cfInput = document.getElementById('cfInput');
1532
+ var cfOk = document.getElementById('cfOk');
1533
+ var cfCancel = document.getElementById('cfCancel');
1534
+ var cfPending = null; var cfMode = 'confirm';
1535
+ function cfClose(res){
1536
+ cfMask.classList.remove('open');
1537
+ if (cfPending){ cfPending(res); cfPending = null; }
1538
+ }
1539
+ function cfOpen(opts){
1540
+ cfMode = opts.mode || 'confirm';
1541
+ cfTitle.textContent = opts.title || '确认';
1542
+ cfBody.textContent = opts.body || '';
1543
+ if (cfMode === 'prompt'){
1544
+ cfFieldWrap.style.display = '';
1545
+ cfLabel.textContent = opts.label || '';
1546
+ cfInput.value = opts.def || '';
1547
+ cfInput.placeholder = opts.placeholder || '';
1548
+ } else {
1549
+ cfFieldWrap.style.display = 'none';
1550
+ }
1551
+ cfOk.textContent = opts.okText || (cfMode === 'alert' ? '知道了' : '确定');
1552
+ cfOk.classList.toggle('danger', !!opts.danger);
1553
+ cfCancel.textContent = opts.cancelText || '取消';
1554
+ cfCancel.style.display = (cfMode === 'alert') ? 'none' : '';
1555
+ cfMask.classList.add('open');
1556
+ setTimeout(function(){
1557
+ if (cfMode === 'prompt'){ cfInput.focus(); cfInput.select(); }
1558
+ else cfOk.focus();
1559
+ }, 60);
1560
+ return new Promise(function(res){ cfPending = res; });
1561
+ }
1562
+ cfOk.addEventListener('click', function(){
1563
+ cfClose({ ok: true, value: (cfMode === 'prompt') ? cfInput.value : null });
1564
+ });
1565
+ cfCancel.addEventListener('click', function(){ cfClose({ ok: false, canceled: true, value: null }); });
1566
+ cfMask.addEventListener('click', function(e){
1567
+ if (e.target === this) cfClose({ ok: false, canceled: true, value: null });
1568
+ });
1569
+ cfInput.addEventListener('keydown', function(e){
1570
+ if (e.key === 'Enter'){ e.preventDefault(); cfOk.click(); }
1571
+ });
1572
+ document.addEventListener('keydown', function(e){
1573
+ if (!cfMask.classList.contains('open')) return;
1574
+ if (e.key === 'Escape') cfClose({ ok: false, canceled: true, value: null });
1575
+ });
1576
+ function uiConfirm(title, body, opts){
1577
+ opts = opts || {};
1578
+ return cfOpen({
1579
+ mode: 'confirm', title: title, body: body,
1580
+ okText: opts.okText, cancelText: opts.cancelText, danger: opts.danger
1581
+ }).then(function(r){ return !!r.ok; });
1582
+ }
1583
+ function uiAlert(title, body){
1584
+ return cfOpen({ mode: 'alert', title: title, body: body });
1585
+ }
1586
+ function uiPrompt(title, label, def, placeholder){
1587
+ return cfOpen({ mode: 'prompt', title: title, label: label, def: def, placeholder: placeholder })
1588
+ .then(function(r){ return r.canceled ? null : r.value; });
1589
+ }
1420
1590
  function ensureWriteDir(){
1421
1591
  if (bkDirHandle) return Promise.resolve(true);
1422
1592
  return maybeAutoAuth().then(function(){ return !!bkDirHandle; });
@@ -1448,6 +1618,7 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1448
1618
  if (next === null) return false;
1449
1619
  return bkWrite('prompts.json', 'data', JSON.stringify(next, null, 2)).then(function(){
1450
1620
  state.data = next;
1621
+ syncMirror(next);
1451
1622
  applyUserState();
1452
1623
  state.dirty = false; state.moves = [];
1453
1624
  persistUserState(); refreshDirtyUI();
@@ -1484,6 +1655,7 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1484
1655
  });
1485
1656
  return bkWrite('prompts.json', 'data', JSON.stringify(latest, null, 2)).then(function(){
1486
1657
  state.data = latest;
1658
+ syncMirror(latest);
1487
1659
  applyUserState();
1488
1660
  state.dirty = false; state.moves = [];
1489
1661
  persistUserState(); refreshDirtyUI();
@@ -1613,14 +1785,16 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1613
1785
  }).then(function(ok){ if (ok) toast(editId ? '提示词已更新' : '提示词已入囊'); });
1614
1786
  }
1615
1787
  function delItem(p){
1616
- if (!window.confirm('确定删除「' + p.title + '」?删除前会自动备份,可从 ↩ 恢复。')) return;
1617
- writePromptsJson({
1618
- apply: function(data){
1619
- data.prompts = data.prompts.filter(function(x){ return x.id !== p.id; });
1620
- return data;
1621
- },
1622
- cmd: '请删除锦囊里「' + p.title + '」这条'
1623
- }).then(function(ok){ if (ok) toast('已删除「' + p.title + '」'); });
1788
+ uiConfirm('删除提示词', '确定删除「' + p.title + '」?删除前会自动备份,可从 ↩ 恢复。', { danger: true, okText: '删除' }).then(function(ok){
1789
+ if (!ok) return;
1790
+ writePromptsJson({
1791
+ apply: function(data){
1792
+ data.prompts = data.prompts.filter(function(x){ return x.id !== p.id; });
1793
+ return data;
1794
+ },
1795
+ cmd: '请删除锦囊里「' + p.title + '」这条'
1796
+ }).then(function(ok2){ if (ok2) toast('已删除「' + p.title + '」'); });
1797
+ });
1624
1798
  }
1625
1799
  /* 标签快捷操作:卡片标签悬停 ✕ 删 / + 加(授权目录即写即存,未授权生成指令) */
1626
1800
  function removeTag(p, t){
@@ -1642,21 +1816,22 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1642
1816
  }
1643
1817
  function quickAddTag(p){
1644
1818
  var cur = (p.tags || []).slice(0, 3);
1645
- var input = window.prompt('为「' + p.title + '」添加标签(多个用逗号分隔,最多 3 个):', cur.join(', '));
1646
- if (input === null) return;
1647
- var adds = input.split(/[,,]/).map(function(s){ return s.trim(); }).filter(Boolean);
1648
- if (!adds.length) return;
1649
- var merged = cur.concat(adds).slice(0, 3);
1650
- writePromptsJson({
1651
- apply: function(data){
1652
- var item = data.prompts.find(function(x){ return x.id === p.id; });
1653
- if (!item){ toast('该提示词已不存在,请刷新后重试', true); return null; }
1654
- item.tags = merged;
1655
- item.updatedAt = nowStamp();
1656
- return data;
1657
- },
1658
- cmd: '请把锦囊里「' + p.title + '」的标签更新为:' + merged.join(', ') + '(同时把 updatedAt 更新为当前时间 YYYY-MM-DD HH:mm:ss)'
1659
- }).then(function(ok){ if (ok) toast('标签已更新'); });
1819
+ uiPrompt('添加标签', '为「' + p.title + '」添加标签(多个用逗号分隔,最多 3 个):', cur.join(', '), '如:日报,工作').then(function(input){
1820
+ if (input === null) return;
1821
+ var adds = input.split(/[,,]/).map(function(s){ return s.trim(); }).filter(Boolean);
1822
+ if (!adds.length) return;
1823
+ var merged = cur.concat(adds).slice(0, 3);
1824
+ writePromptsJson({
1825
+ apply: function(data){
1826
+ var item = data.prompts.find(function(x){ return x.id === p.id; });
1827
+ if (!item){ toast('该提示词已不存在,请刷新后重试', true); return null; }
1828
+ item.tags = merged;
1829
+ item.updatedAt = nowStamp();
1830
+ return data;
1831
+ },
1832
+ cmd: '请把锦囊里「' + p.title + '」的标签更新为:' + merged.join(', ') + '(同时把 updatedAt 更新为当前时间 YYYY-MM-DD HH:mm:ss)'
1833
+ }).then(function(ok){ if (ok) toast('标签已更新'); });
1834
+ });
1660
1835
  }
1661
1836
  document.getElementById('itemCancel').addEventListener('click', closeItemForm);
1662
1837
  document.getElementById('itemSave').addEventListener('click', saveItem);
@@ -1726,15 +1901,17 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1726
1901
  function delGroup(g){
1727
1902
  var n = state.data.prompts.filter(function(p){ return p.group === g.id; }).length;
1728
1903
  if (n > 0){ toast('「' + g.name + '」内还有 ' + n + ' 条提示词,请先移出或删除', true); return; }
1729
- if (!window.confirm('确定删除分组「' + g.name + '」?')) return;
1730
- if (state.curGroup === g.id) state.curGroup = 'all';
1731
- writePromptsJson({
1732
- apply: function(data){
1733
- data.groups = data.groups.filter(function(x){ return x.id !== g.id; });
1734
- return data;
1735
- },
1736
- cmd: '请删除锦囊分组「' + g.name + '」'
1737
- }).then(function(ok){ if (ok) toast('分组「' + g.name + '」已删除'); });
1904
+ uiConfirm('删除分组', '确定删除分组「' + g.name + '」?', { danger: true, okText: '删除' }).then(function(ok){
1905
+ if (!ok) return;
1906
+ if (state.curGroup === g.id) state.curGroup = 'all';
1907
+ writePromptsJson({
1908
+ apply: function(data){
1909
+ data.groups = data.groups.filter(function(x){ return x.id !== g.id; });
1910
+ return data;
1911
+ },
1912
+ cmd: '请删除锦囊分组「' + g.name + '」'
1913
+ }).then(function(ok2){ if (ok2) toast('分组「' + g.name + '」已删除'); });
1914
+ });
1738
1915
  }
1739
1916
  function reorderGroups(moveId, beforeId){
1740
1917
  var ids = state.data.groups.map(function(g){ return g.id; });
@@ -1797,9 +1974,11 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1797
1974
  }
1798
1975
  } catch(err){ toast('JSON 解析失败,请确认文件内容', true); return; }
1799
1976
  var cur = (state.data && state.data.prompts) ? state.data.prompts.length : 0;
1800
- if (!window.confirm('导入将覆盖现有 ' + cur + ' 条数据为 ' + d.prompts.length + ' 条(导入前自动备份,可从 ↩ 恢复)。确定继续?')) return;
1801
- writePromptsJson({ apply: function(){ return d; }, cmd: '' }).then(function(ok){
1802
- if (ok) toast('已导入 ' + d.prompts.length + ' 条提示词');
1977
+ uiConfirm('覆盖导入', '导入将覆盖现有 ' + cur + ' 条数据为 ' + d.prompts.length + ' 条(导入前自动备份,可从 ↩ 恢复)。确定继续?', { danger: true, okText: '覆盖导入' }).then(function(ok){
1978
+ if (!ok) return;
1979
+ writePromptsJson({ apply: function(){ return d; }, cmd: '' }).then(function(ok2){
1980
+ if (ok2) toast('已导入 ' + d.prompts.length + ' 条提示词');
1981
+ });
1803
1982
  });
1804
1983
  };
1805
1984
  reader.readAsText(f, 'utf-8');
@@ -1868,6 +2047,7 @@ footer.open .ht-arrow{ transform:rotate(180deg); }
1868
2047
  if (e.key === 'Escape'){
1869
2048
  closeFillPanel();
1870
2049
  closeGpop();
2050
+ toggleSortMenu(false);
1871
2051
  closeItemForm();
1872
2052
  closeGroupForm();
1873
2053
  document.getElementById('bkMask').classList.remove('open');
@@ -2030,6 +2210,19 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2030
2210
  });
2031
2211
  });
2032
2212
  }
2213
+ /* 数据镜像:localStorage 存全量数据快照(file:// 下 IndexedDB 偶发不可用时兜底恢复,键按 pouch-id 隔离) */
2214
+ function syncMirror(data){
2215
+ try{ localStorage.setItem(SF_NS + '-mirror', JSON.stringify(data)); }catch(e){}
2216
+ }
2217
+ function readMirror(){
2218
+ try{
2219
+ var raw = localStorage.getItem(SF_NS + '-mirror');
2220
+ if (!raw) return null;
2221
+ var d = JSON.parse(raw);
2222
+ if (d && d.groups && d.prompts) return d;
2223
+ }catch(e){}
2224
+ return null;
2225
+ }
2033
2226
  /* 备份文件读:backup/ 优先,读不到回退根目录旧文件(旧目录结构兼容) */
2034
2227
  function readBk(name){
2035
2228
  return bkRead(name, 'backup').then(function(text){
@@ -2109,22 +2302,24 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2109
2302
  if (bkDirHandle) return Promise.resolve(true);
2110
2303
  if (!bkSupported() || bkDeclined || pendingAuth) return Promise.resolve(false);
2111
2304
  if (!state.data) return Promise.resolve(false);
2112
- var ok = window.confirm('锦囊自动备份\n\n数据一有变更就会自动备份,建议授权写入 prompt-pouch.html 所在目录(首次弹窗选一次,浏览器会记住)。\n\n点「确定」打开目录选择 → 选 prompt-pouch.html 所在文件夹即可;\n点「取消」则改用浏览器本地快照(保留最近 5 份,清浏览器数据会丢)。');
2113
- if (!ok){
2114
- bkDeclined = true;
2115
- try{ localStorage.setItem(SF_NS + '-bk-declined', '1'); }catch(e){}
2116
- return Promise.resolve(false);
2117
- }
2118
- pendingAuth = true; updateBkUI();
2119
- return window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
2120
- .then(function(h){
2121
- bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
2122
- try{ localStorage.removeItem(SF_NS + '-bk-declined'); }catch(e){}
2123
- idbSet('bkHandle', bkDirHandle);
2124
- updateBkUI();
2125
- return true;
2126
- })
2127
- .catch(function(){ pendingAuth = false; updateBkUI(); return false; });
2305
+ return uiConfirm('锦囊自动备份', '数据一有变更就会自动备份,建议授权写入 prompt-pouch.html 所在目录(首次选一次,浏览器会记住)。\n\n点「授权目录」打开目录选择 → 选 prompt-pouch.html 所在文件夹即可;\n点「暂不」则改用浏览器本地快照(保留最近 5 份,清浏览器数据会丢)。', { okText: '授权目录', cancelText: '暂不' }).then(function(ok){
2306
+ if (!ok){
2307
+ bkDeclined = true;
2308
+ try{ localStorage.setItem(SF_NS + '-bk-declined', '1'); }catch(e){}
2309
+ return false;
2310
+ }
2311
+ pendingAuth = true; updateBkUI();
2312
+ return window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
2313
+ .then(function(h){
2314
+ bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
2315
+ try{ localStorage.removeItem(SF_NS + '-bk-declined'); }catch(e){}
2316
+ idbSet('bkHandle', bkDirHandle);
2317
+ updateBkUI();
2318
+ reloadFromDirIfNeeded();
2319
+ return true;
2320
+ })
2321
+ .catch(function(){ pendingAuth = false; updateBkUI(); return false; });
2322
+ });
2128
2323
  }
2129
2324
  function updateBkUI(){
2130
2325
  if (!bkSupported()){
@@ -2168,18 +2363,55 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2168
2363
  pendingAuth = false;
2169
2364
  }).catch(function(){});
2170
2365
  }
2366
+ /* file:// 直开补洞:fetch 被浏览器拦截读不到数据文件时,授权目录后直读 data/prompts.json 替换示例/快照 */
2367
+ function reloadFromDirIfNeeded(){
2368
+ if (!bkDirHandle || bootMode === 'live') return;
2369
+ readPouch().then(function(text){
2370
+ if (text === null) return;
2371
+ var data = JSON.parse(text);
2372
+ state.data = data;
2373
+ idbSet('lastData', data);
2374
+ syncMirror(data);
2375
+ bootMode = 'live';
2376
+ cacheBadge.style.display = 'none';
2377
+ applyUserState();
2378
+ renderNav(); render();
2379
+ toast('✓ 已直读 data/prompts.json,你的分组与提示词已载入');
2380
+ }).catch(function(){});
2381
+ }
2382
+ /* 「🔓 恢复数据」:已授权过目录但刷新后权限需重新确认(file:// 权限不跨会话持久)→ 一键 requestPermission 后直读,免重新选目录 */
2383
+ function restoreAuth(){
2384
+ if (!bkDirHandle || typeof bkDirHandle.requestPermission !== 'function') return;
2385
+ bkDirHandle.requestPermission({ mode: 'readwrite' }).then(function(st){
2386
+ if (st === 'granted'){
2387
+ pendingAuth = false; updateBkUI();
2388
+ reloadFromDirIfNeeded();
2389
+ } else {
2390
+ toast('未获授权:点「💾 授权目录」重新选择目录', true);
2391
+ }
2392
+ }).catch(function(){ toast('权限恢复失败,请重新授权目录', true); });
2393
+ }
2394
+ function updateBootBadge(){
2395
+ if (bootMode === 'live' || !bkDirHandle || !bootRendered) return;
2396
+ var link = ' <a id="bootRestore" style="cursor:pointer;text-decoration:underline">🔓 恢复数据</a>';
2397
+ cacheBadge.innerHTML = (bootMode === 'cache'
2398
+ ? '⚠️ 快照模式:当前显示上次快照,目录权限需确认后直读最新数据'
2399
+ : '📌 示例模式:检测到曾授权过目录,点击确认权限即可载入你的数据') + link;
2400
+ cacheBadge.style.display = '';
2401
+ document.getElementById('bootRestore').addEventListener('click', restoreAuth);
2402
+ }
2171
2403
  btnBkEnable.addEventListener('click', function(){
2172
2404
  if (bkDirHandle && !pendingAuth){
2173
2405
  writeBackupNow().then(function(ok){ toast(ok ? '✓ 备份完成 · backup/pouch-backup.json' : '备份失败,请重新授权目录', !ok); });
2174
2406
  return;
2175
2407
  }
2176
2408
  if (!bkSupported()){
2177
- alert('当前浏览器不支持「选择目录」授权(Chrome / Edge 可用)。\n本页已自动使用本地快照备份(保留最近 5 份,随浏览器数据走);\n也可随时对 AI 说「导出锦囊数据」生成备份文件备用。');
2409
+ uiAlert('目录授权不可用', '当前浏览器不支持「选择目录」授权(Chrome / Edge 可用)。\n本页已自动使用本地快照备份(保留最近 5 份,随浏览器数据走);\n也可随时对 AI 说「导出锦囊数据」生成备份文件备用。');
2178
2410
  return;
2179
2411
  }
2180
2412
  if (bkDirHandle && pendingAuth){
2181
2413
  bkDirHandle.requestPermission({ mode: 'readwrite' }).then(function(st){
2182
- if (st === 'granted'){ pendingAuth = false; updateBkUI(); writeBackupNow(); }
2414
+ if (st === 'granted'){ pendingAuth = false; updateBkUI(); writeBackupNow(); reloadFromDirIfNeeded(); }
2183
2415
  else bkStatus.textContent = '未获授权,备份暂停';
2184
2416
  }).catch(function(){});
2185
2417
  return;
@@ -2191,21 +2423,25 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2191
2423
  idbSet('bkHandle', bkDirHandle);
2192
2424
  updateBkUI();
2193
2425
  writeBackupNow();
2426
+ reloadFromDirIfNeeded();
2194
2427
  })
2195
2428
  .catch(function(){});
2196
2429
  });
2197
2430
  btnBkChange.addEventListener('click', function(){
2198
2431
  if (!bkSupported()){ updateBkUI(); return; }
2199
- if (!window.confirm('更换备份目录\n\n建议仍选 prompt-pouch.html 所在目录:页面新增/编辑的分组与提示词、自动备份都写在这个目录,选别处会造成页面与数据文件不一致。\n\n确定继续选择目录?')) return;
2200
- window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
2201
- .then(function(h){
2202
- bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
2203
- try{ localStorage.removeItem(SF_NS + '-bk-declined'); }catch(e){}
2204
- idbSet('bkHandle', bkDirHandle);
2205
- updateBkUI();
2206
- writeBackupNow();
2207
- })
2208
- .catch(function(){});
2432
+ uiConfirm('更换备份目录', '建议仍选 prompt-pouch.html 所在目录:页面新增/编辑的分组与提示词、自动备份都写在这个目录,选别处会造成页面与数据文件不一致。\n\n确定继续选择目录?', { okText: '选择目录' }).then(function(ok){
2433
+ if (!ok) return;
2434
+ window.showDirectoryPicker({ id: 'pouch-backup', mode: 'readwrite' })
2435
+ .then(function(h){
2436
+ bkDirHandle = h; pendingAuth = false; bkDeclined = false; subDirCache = {};
2437
+ try{ localStorage.removeItem(SF_NS + '-bk-declined'); }catch(e){}
2438
+ idbSet('bkHandle', bkDirHandle);
2439
+ updateBkUI();
2440
+ writeBackupNow();
2441
+ reloadFromDirIfNeeded();
2442
+ })
2443
+ .catch(function(){});
2444
+ });
2209
2445
  });
2210
2446
  btnBkRestore.addEventListener('click', function(){
2211
2447
  document.getElementById('bkList').innerHTML = '<div class="bk-empty">读取备份清单…</div>';
@@ -2256,31 +2492,33 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2256
2492
  }
2257
2493
  function doRestore(content){
2258
2494
  var snap;
2259
- try{ snap = JSON.parse(content); }catch(e){ alert('备份文件损坏,无法恢复。'); return; }
2495
+ try{ snap = JSON.parse(content); }catch(e){ uiAlert('恢复失败', '备份文件损坏,无法恢复。'); return; }
2260
2496
  if (!snap || !snap.data || !Array.isArray(snap.data.groups) || !Array.isArray(snap.data.prompts)){
2261
- alert('备份结构不完整,无法恢复。'); return;
2497
+ uiAlert('恢复失败', '备份结构不完整,无法恢复。'); return;
2262
2498
  }
2263
- var ok = window.confirm('将用这份备份覆盖当前数据:\n\n· 备份时间:' + fmtTime(snap.time)
2499
+ uiConfirm('恢复备份', '将用这份备份覆盖当前数据:\n\n· 备份时间:' + fmtTime(snap.time)
2264
2500
  + '\n· 内容:' + snap.counts.groups + ' 分组 / ' + snap.counts.prompts + ' 条提示词\n\n'
2265
- + '恢复前会先把当前数据自动另存一份新备份,可随时再恢复回来。\n\n确定恢复?');
2266
- if (!ok) return;
2267
- // 先把当前状态固化进备份(目录备份滚入 .prev.json / 快照环记一份),再应用所选备份
2268
- (bkDirHandle ? writeBackupNow() : writeSnapNow()).then(function(){
2269
- state.data = snap.data;
2270
- applyUserStateFrom(snap.userState || {});
2271
- state.dirty = false; state.moves = [];
2272
- persistUserState();
2273
- renderNav(); render();
2274
- document.getElementById('bkMask').classList.remove('open');
2275
- if (bkDirHandle){
2276
- return bkWrite('prompts.json', JSON.stringify(snap.data, null, 2)).then(function(){
2277
- toast('✓ 已恢复并写入 prompts.json,刷新即见');
2278
- }).catch(function(){
2279
- toast('已恢复;写入 prompts.json 失败,请重新授权目录', true);
2280
- });
2281
- }
2282
- markDirty();
2283
- toast('✓ 已恢复;未授权目录——点「📋 复制变更指令」交给 AI 落盘,或先「💾 授权目录备份」');
2501
+ + '恢复前会先把当前数据自动另存一份新备份,可随时再恢复回来。\n\n确定恢复?', { danger: true, okText: '恢复' }).then(function(ok){
2502
+ if (!ok) return;
2503
+ // 先把当前状态固化进备份(目录备份滚入 .prev.json / 快照环记一份),再应用所选备份
2504
+ (bkDirHandle ? writeBackupNow() : writeSnapNow()).then(function(){
2505
+ state.data = snap.data;
2506
+ syncMirror(snap.data);
2507
+ applyUserStateFrom(snap.userState || {});
2508
+ state.dirty = false; state.moves = [];
2509
+ persistUserState();
2510
+ renderNav(); render();
2511
+ document.getElementById('bkMask').classList.remove('open');
2512
+ if (bkDirHandle){
2513
+ return bkWrite('prompts.json', JSON.stringify(snap.data, null, 2)).then(function(){
2514
+ toast('✓ 已恢复并写入 prompts.json,刷新即见');
2515
+ }).catch(function(){
2516
+ toast('已恢复;写入 prompts.json 失败,请重新授权目录', true);
2517
+ });
2518
+ }
2519
+ markDirty();
2520
+ toast('✓ 已恢复;未授权目录——点「📋 复制变更指令」交给 AI 落盘,或先「💾 授权目录备份」');
2521
+ });
2284
2522
  });
2285
2523
  }
2286
2524
  document.getElementById('bkCancelBtn').addEventListener('click', function(){
@@ -2303,7 +2541,7 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2303
2541
  render();
2304
2542
  if (bootMode === 'cache') cacheBadge.style.display = '';
2305
2543
  if (bootMode === 'demo'){
2306
- cacheBadge.textContent = '📌 示例模式:未读到 data/prompts.json,当前展示系统自带示例提示词(file:// 直开或数据文件尚未生成)。经 HTTP 打开页面并生成数据文件后即为你的提示词,示例可随时删除。';
2544
+ cacheBadge.textContent = '📌 示例模式:未读到 data/prompts.json,当前展示系统自带示例。若你有自己的数据文件:点顶部「💾 授权目录」选 prompt-pouch.html 所在文件夹即可直读载入;或经 HTTP 打开页面(file:// 直开会被浏览器拦截文件读取)。';
2307
2545
  cacheBadge.style.display = '';
2308
2546
  }
2309
2547
  }
@@ -2319,26 +2557,49 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2319
2557
  .then(function(data){
2320
2558
  state.data = data;
2321
2559
  idbSet('lastData', data);
2560
+ syncMirror(data);
2322
2561
  bootMode = 'live';
2323
2562
  firstRender();
2324
2563
  })
2325
2564
  .catch(function(){
2326
- idbGet('lastData').then(function(cached){
2327
- if (cached){
2328
- state.data = cached;
2329
- bootMode = 'cache';
2330
- firstRender();
2331
- } else {
2332
- state.data = DEFAULT_POUCH;
2333
- bootMode = 'demo';
2334
- firstRender();
2335
- }
2565
+ // file:// 场景:fetch 被浏览器拦截 → 已授权目录则直读 data/prompts.json(File System Access API)
2566
+ return dirHandleReady.then(function(){
2567
+ return readPouch().then(function(text){
2568
+ if (text !== null) return JSON.parse(text);
2569
+ throw new Error('no-fs');
2570
+ });
2571
+ }).then(function(data){
2572
+ state.data = data;
2573
+ idbSet('lastData', data);
2574
+ syncMirror(data);
2575
+ bootMode = 'live';
2576
+ firstRender();
2577
+ }).catch(function(){
2578
+ idbGet('lastData').then(function(cached){
2579
+ if (cached){
2580
+ state.data = cached;
2581
+ bootMode = 'cache';
2582
+ firstRender();
2583
+ } else {
2584
+ var mirror = readMirror();
2585
+ if (mirror){
2586
+ state.data = mirror;
2587
+ bootMode = 'cache';
2588
+ firstRender();
2589
+ } else {
2590
+ state.data = DEFAULT_POUCH;
2591
+ bootMode = 'demo';
2592
+ firstRender();
2593
+ }
2594
+ }
2595
+ });
2336
2596
  });
2337
2597
  });
2338
2598
  // 本地态并行:可用则应用收藏与排序并刷新备份区(不阻塞数据展示)
2599
+ var dirHandleReady = idbOpen().then(function(){ return loadDirHandle(); }).catch(function(){});
2339
2600
  idbOpen()
2340
2601
  .then(loadLocalState)
2341
- .then(loadDirHandle)
2602
+ .then(function(){ return dirHandleReady; })
2342
2603
  .catch(function(){})
2343
2604
  .then(function(){
2344
2605
  applyUserState();
@@ -2346,6 +2607,7 @@ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(POUC
2346
2607
  refreshDirtyUI();
2347
2608
  if (bootRendered){ renderNav(); render(); }
2348
2609
  updateBkUI();
2610
+ updateBootBadge();
2349
2611
  });
2350
2612
  })();
2351
2613