@wendongfly/myhi 1.3.193 → 1.3.195
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/dist/admin.html +51 -0
- package/dist/chat.html +1207 -336
- package/dist/index.html +9 -2
- package/dist/index.js +10 -10
- package/dist/index.min.js +178 -157
- package/dist/login.html +5 -0
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -294,6 +294,12 @@
|
|
|
294
294
|
@media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
|
|
295
295
|
#input-toolbar { display: flex; align-items: center; padding: 0.25rem 0.4rem 0.4rem; gap: 0.15rem; }
|
|
296
296
|
#input-toolbar .spacer { flex: 1; }
|
|
297
|
+
.inj-btn { flex-shrink: 0; height: 30px; border: 1px solid var(--yellow); background: rgba(210,153,34,0.12); color: var(--yellow); border-radius: 8px; padding: 0 0.6rem; font-size: 0.76rem; font-weight: 600; cursor: pointer; white-space: nowrap; align-self: center; }
|
|
298
|
+
.inj-btn:hover { background: rgba(210,153,34,0.22); }
|
|
299
|
+
.inj-tag { font-size: 0.66rem; margin-right: 0.35rem; color: var(--yellow); }
|
|
300
|
+
.inj-tag.seen { color: var(--green); }
|
|
301
|
+
.inj-tag.dropped { color: var(--red); }
|
|
302
|
+
.msg-inject .bubble { border: 1px dashed var(--yellow); }
|
|
297
303
|
.tb-btn { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; border: none; background: transparent; color: var(--muted); cursor: pointer; transition: all 0.15s; flex-shrink: 0; }
|
|
298
304
|
.tb-btn:hover { background: var(--surface2); color: var(--text); }
|
|
299
305
|
.tb-btn:active { transform: scale(0.92); }
|
|
@@ -448,6 +454,119 @@
|
|
|
448
454
|
.dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
|
|
449
455
|
.dr-btn:active { opacity: 0.75; }
|
|
450
456
|
.dr-src { color: var(--blue2); }
|
|
457
|
+
/* 技能面板 */
|
|
458
|
+
.skl-box { max-width: 640px !important; max-height: min(88vh, 820px); min-height: min(460px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
|
|
459
|
+
@media (min-width: 769px) { #skill-sheet.open { align-items: center; } #skill-sheet .skl-box { border-radius: 14px; } }
|
|
460
|
+
.skl-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.1rem 0.1rem 0.55rem; border-bottom: 1px solid var(--surface2); }
|
|
461
|
+
.skl-head b { font-size: 0.92rem; }
|
|
462
|
+
.skl-back { background: none; border: none; color: var(--text2); font-size: 1.3rem; line-height: 1; cursor: pointer; padding: 0 0.3rem; border-radius: 6px; }
|
|
463
|
+
.skl-back:hover { background: var(--surface2); }
|
|
464
|
+
.skl-view { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; padding: 0.55rem 0.1rem 0.2rem; }
|
|
465
|
+
.skl-tools { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.5rem; }
|
|
466
|
+
.skl-tool { border: 1px solid var(--border); background: var(--surface2); color: var(--text2); border-radius: 8px; padding: 0.3rem 0.6rem; font-size: 0.74rem; cursor: pointer; white-space: nowrap; position: relative; }
|
|
467
|
+
.skl-tool.pri { background: var(--purple); border-color: var(--purple); color: #fff; font-weight: 600; }
|
|
468
|
+
.skl-badge { position: absolute; top: -6px; right: -6px; background: var(--green); color: #fff; border-radius: 8px; font-size: 0.6rem; padding: 0 0.3rem; font-weight: 700; }
|
|
469
|
+
.skl-banner { background: rgba(46,160,67,0.1); color: var(--green); border-radius: 8px; padding: 0.4rem 0.6rem; font-size: 0.74rem; cursor: pointer; margin-bottom: 0.4rem; }
|
|
470
|
+
.skl-grp { font-size: 0.68rem; color: var(--muted); font-weight: 600; padding: 0.5rem 0.2rem 0.2rem; }
|
|
471
|
+
.skl-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.3rem; border-bottom: 1px solid var(--surface2); border-radius: 6px; }
|
|
472
|
+
.skl-item:hover { background: var(--surface2); }
|
|
473
|
+
.skl-main { flex: 1; min-width: 0; cursor: pointer; }
|
|
474
|
+
.skl-name { color: var(--purple2); font-weight: 600; font-size: 0.84rem; }
|
|
475
|
+
.skl-desc { font-size: 0.72rem; color: var(--muted); line-height: 1.45; margin-top: 0.1rem; word-break: break-word; }
|
|
476
|
+
.skl-tag { display: inline-block; margin-left: 0.35rem; font-size: 0.6rem; padding: 0.02rem 0.35rem; border-radius: 8px; border: 1px solid var(--border); color: var(--muted); vertical-align: 1px; }
|
|
477
|
+
.skl-tag.skill { border-color: var(--green); color: var(--green); }
|
|
478
|
+
.skl-tag.command { border-color: var(--purple2); color: var(--purple2); }
|
|
479
|
+
.skl-acts { display: flex; gap: 0.25rem; flex-shrink: 0; }
|
|
480
|
+
.skl-empty { text-align: center; color: var(--muted); padding: 1.6rem 0.5rem; font-size: 0.8rem; line-height: 1.7; }
|
|
481
|
+
.skl-foot { font-size: 0.68rem; color: var(--muted); line-height: 1.55; padding: 0.6rem 0.2rem 0; }
|
|
482
|
+
.skl-row { display: flex; gap: 0.4rem; align-items: center; margin-bottom: 0.35rem; flex-wrap: wrap; }
|
|
483
|
+
.skl-sel { flex: 1; min-width: 9rem; background: var(--bg); color: var(--text2); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem; font-size: 0.8rem; }
|
|
484
|
+
.skl-hint { font-size: 0.72rem; color: var(--muted); line-height: 1.6; }
|
|
485
|
+
.skl-hint code { background: var(--surface2); padding: 0 0.25rem; border-radius: 3px; }
|
|
486
|
+
.skl-lbl { font-size: 0.72rem; color: var(--text2); font-weight: 600; margin: 0.6rem 0 0.3rem; }
|
|
487
|
+
.skl-radios { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
|
488
|
+
.skl-radios label { flex: 1; min-width: 10rem; display: flex; gap: 0.4rem; align-items: flex-start; border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
|
|
489
|
+
.skl-radios label b { white-space: nowrap; }
|
|
490
|
+
.skl-radios label small { display: block; color: var(--muted); font-size: 0.68rem; margin-top: 0.1rem; }
|
|
491
|
+
.skl-radios input { margin-top: 0.2rem; }
|
|
492
|
+
.skl-card { border: 1px solid var(--border); border-radius: 9px; padding: 0.5rem 0.6rem; margin-bottom: 0.45rem; font-size: 0.8rem; }
|
|
493
|
+
.skl-share-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
|
|
494
|
+
@media (max-width: 480px) { .skl-share-grid { grid-template-columns: 1fr; } }
|
|
495
|
+
.skl-big { text-align: left; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 10px; padding: 0.55rem 0.65rem; cursor: pointer; display: grid; grid-template-columns: 1.6rem 1fr; column-gap: 0.3rem; align-items: center; }
|
|
496
|
+
.skl-big:hover { border-color: var(--purple); }
|
|
497
|
+
.skl-big span { grid-row: span 2; font-size: 1.1rem; }
|
|
498
|
+
.skl-big b { font-size: 0.8rem; }
|
|
499
|
+
.skl-big small { color: var(--muted); font-size: 0.66rem; line-height: 1.4; }
|
|
500
|
+
.skl-out { margin-top: 0.5rem; font-size: 0.76rem; color: var(--text2); line-height: 1.6; background: var(--surface2); border-radius: 8px; padding: 0.5rem 0.6rem; }
|
|
501
|
+
.skl-warn { background: rgba(248,81,73,0.1); color: var(--red); border-radius: 8px; padding: 0.45rem 0.6rem; font-size: 0.74rem; line-height: 1.55; margin: 0.4rem 0; }
|
|
502
|
+
.skl-ok { color: var(--green); font-size: 0.74rem; margin: 0.35rem 0; }
|
|
503
|
+
.skl-findings { margin: 0.35rem 0; display: flex; flex-direction: column; gap: 0.25rem; }
|
|
504
|
+
.skl-f { font-size: 0.72rem; border-radius: 6px; padding: 0.3rem 0.5rem; line-height: 1.5; }
|
|
505
|
+
.skl-f.danger { background: rgba(248,81,73,0.1); color: var(--red); }
|
|
506
|
+
.skl-f.warn { background: rgba(210,153,34,0.1); color: var(--yellow); }
|
|
507
|
+
.skl-f small { color: var(--muted); }
|
|
508
|
+
.skl-f code { display: block; margin-top: 0.15rem; font-size: 0.68rem; color: var(--text2); white-space: pre-wrap; word-break: break-all; }
|
|
509
|
+
.skl-files { margin: 0.4rem 0; font-size: 0.74rem; }
|
|
510
|
+
.skl-files summary { cursor: pointer; color: var(--blue2); }
|
|
511
|
+
.skl-files pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.45rem; font-size: 0.7rem; max-height: 14rem; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
|
512
|
+
.skl-fname { font-weight: 600; margin-top: 0.4rem; color: var(--text2); }
|
|
513
|
+
.skl-confirm { display: flex; gap: 0.4rem; align-items: center; font-size: 0.74rem; color: var(--text2); margin-top: 0.4rem; cursor: pointer; }
|
|
514
|
+
.skl-pick { display: flex; gap: 0.5rem; align-items: flex-start; padding: 0.45rem 0.2rem; border-bottom: 1px solid var(--surface2); cursor: pointer; }
|
|
515
|
+
.skl-pick input { margin-top: 0.2rem; }
|
|
516
|
+
.sk.skl-dot::after { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-left: 0.25rem; vertical-align: 2px; }
|
|
517
|
+
/* 草稿面板(队列) */
|
|
518
|
+
.ib-box { max-width: 640px !important; max-height: min(86vh, 760px); min-height: min(420px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
|
|
519
|
+
@media (min-width: 769px) { #inbox-sheet.open { align-items: center; } #inbox-sheet .ib-box { border-radius: 14px; } }
|
|
520
|
+
.ib-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.1rem 0.1rem 0.55rem; border-bottom: 1px solid var(--surface2); }
|
|
521
|
+
.ib-head > b { font-size: 0.92rem; white-space: nowrap; }
|
|
522
|
+
.ib-tabs { display: flex; gap: 2px; background: var(--surface2); border-radius: 8px; padding: 2px; }
|
|
523
|
+
.ib-tabs button { border: none; background: none; color: var(--muted); font-size: 0.74rem; padding: 0.25rem 0.65rem; border-radius: 6px; cursor: pointer; white-space: nowrap; }
|
|
524
|
+
.ib-tabs button.on { background: var(--surface); color: var(--text); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,0.12); }
|
|
525
|
+
.ib-x { margin-left: auto; background: none; border: none; color: var(--muted); font-size: 1rem; cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 6px; }
|
|
526
|
+
.ib-x:hover { background: var(--surface2); color: var(--text); }
|
|
527
|
+
.ib-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
528
|
+
.ib-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.1rem 0.35rem; flex-wrap: wrap; }
|
|
529
|
+
.ib-hint { flex: 1; min-width: 8em; font-size: 0.68rem; color: var(--muted); line-height: 1.45; }
|
|
530
|
+
.ib-autoall { display: flex; align-items: center; gap: 0.4rem; position: relative; font-size: 0.72rem; color: var(--text2); cursor: pointer; white-space: nowrap; }
|
|
531
|
+
/* 开关样式:比「复选框 + ▶」直观,一眼看出开 / 关 */
|
|
532
|
+
.ib-autoall input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
533
|
+
.ib-sw { position: relative; width: 1.9rem; height: 1.05rem; border-radius: 1rem; background: var(--border2, var(--border)); transition: background 0.15s; flex-shrink: 0; }
|
|
534
|
+
.ib-sw::after { content: ''; position: absolute; top: 0.125rem; left: 0.125rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: transform 0.15s; }
|
|
535
|
+
.ib-autoall input:checked + .ib-sw { background: var(--green); }
|
|
536
|
+
.ib-autoall input:checked + .ib-sw::after { transform: translateX(0.85rem); }
|
|
537
|
+
.ib-autoall input:indeterminate + .ib-sw { background: rgba(46,160,67,0.45); }
|
|
538
|
+
.ib-autoall input:focus-visible + .ib-sw { outline: 2px solid var(--green); outline-offset: 2px; }
|
|
539
|
+
.ib-paused { display: flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; color: var(--yellow); background: rgba(210,153,34,0.10); border-radius: 8px; padding: 0.35rem 0.55rem; margin-bottom: 0.3rem; }
|
|
540
|
+
.ib-paused span { flex: 1; }
|
|
541
|
+
.ib-list { flex: 1; min-height: 3rem; overflow-y: auto; scrollbar-width: thin; }
|
|
542
|
+
.ib-item { display: flex; gap: 0.45rem; align-items: flex-start; padding: 0.5rem 0.3rem; border-bottom: 1px solid var(--surface2); border-radius: 6px; }
|
|
543
|
+
.ib-item.sel { background: rgba(46,160,67,0.08); }
|
|
544
|
+
.ib-item.drag-over-top { box-shadow: inset 0 2px 0 var(--green); }
|
|
545
|
+
.ib-item.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--green); }
|
|
546
|
+
.ib-item.dragging { opacity: 0.4; }
|
|
547
|
+
.ib-handle { cursor: grab; color: var(--dim, var(--muted)); font-size: 0.8rem; padding-top: 0.1rem; user-select: none; letter-spacing: -2px; }
|
|
548
|
+
@media (hover: none) { .ib-handle { display: none; } }
|
|
549
|
+
.ib-no { flex-shrink: 0; min-width: 1.35rem; height: 1.35rem; border-radius: 50%; background: var(--surface2); color: var(--text2); font-size: 0.68rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
|
|
550
|
+
.ib-no.auto { background: var(--green); color: #fff; }
|
|
551
|
+
.ib-item input[type=checkbox] { width: 1.05rem; height: 1.05rem; margin-top: 0.15rem; flex-shrink: 0; }
|
|
552
|
+
.ib-main { flex: 1; min-width: 0; }
|
|
553
|
+
.ib-text { color: var(--text); font-size: 0.82rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; cursor: text; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; border-radius: 4px; }
|
|
554
|
+
.ib-text:hover { background: var(--surface2); }
|
|
555
|
+
.ib-edit textarea { width: 100%; box-sizing: border-box; min-height: 5.5em; background: var(--bg); color: var(--text); border: 1px solid var(--green); border-radius: 7px; padding: 0.4rem 0.5rem; font: inherit; font-size: 0.82rem; resize: vertical; outline: none; }
|
|
556
|
+
.ib-edit div { display: flex; gap: 0.4rem; justify-content: flex-end; margin-top: 0.3rem; align-items: center; }
|
|
557
|
+
.ib-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.3rem 0.5rem; margin-top: 0.3rem; font-size: 0.66rem; color: var(--muted); }
|
|
558
|
+
.ib-chip { border: 1px solid var(--border); background: none; color: var(--muted); border-radius: 10px; padding: 0.05rem 0.45rem; font-size: 0.64rem; cursor: pointer; }
|
|
559
|
+
.ib-chip.on { border-color: var(--green); color: var(--green); font-weight: 600; }
|
|
560
|
+
.ib-acts { display: flex; gap: 0.25rem; margin-left: auto; }
|
|
561
|
+
.ib-ic { border: none; background: var(--surface2); color: var(--text2); border-radius: 6px; font-size: 0.7rem; padding: 0.22rem 0.45rem; cursor: pointer; }
|
|
562
|
+
.ib-ic:disabled { opacity: 0.35; cursor: default; }
|
|
563
|
+
.ib-selbar { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; padding: 0.45rem 0.2rem; border-top: 1px solid var(--surface2); font-size: 0.74rem; }
|
|
564
|
+
.ib-selbar span { margin-right: auto; color: var(--text2); }
|
|
565
|
+
.ib-compose { border-top: 1px solid var(--surface2); padding: 0.5rem 0.1rem 0; }
|
|
566
|
+
.ib-compose textarea { width: 100%; box-sizing: border-box; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.55rem; font-size: 0.82rem; resize: vertical; min-height: 2.6em; }
|
|
567
|
+
.ib-compose-row { display: flex; align-items: center; gap: 0.55rem; margin-top: 0.4rem; flex-wrap: wrap; }
|
|
568
|
+
.ib-link { color: var(--blue2); cursor: pointer; font-size: 0.7rem; }
|
|
569
|
+
.ib-grp { font-size: 0.7rem; color: var(--blue2); font-weight: 600; padding: 0.55rem 0.2rem 0.15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
451
570
|
.dr-sess { font-size: 0.68rem; color: var(--blue2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
452
571
|
.mem-file { margin-bottom: 0.5rem; border: 1px solid var(--surface2); border-radius: 8px; overflow: hidden; }
|
|
453
572
|
.mem-file-header { display: flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.6rem; background: var(--surface2); cursor: pointer; font-size: 0.75rem; color: var(--blue); user-select: none; }
|
|
@@ -488,6 +607,44 @@
|
|
|
488
607
|
#qr-box .close-btn { margin-top: 1rem; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 1.5rem; cursor: pointer; font-size: 0.9rem; }
|
|
489
608
|
#status-overlay { position: fixed; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(13,17,23,0.85); color: var(--muted); font-size: 0.9rem; z-index: 50; }
|
|
490
609
|
#status-overlay.hidden { display: none; }
|
|
610
|
+
|
|
611
|
+
/* ── 💡 问一问:选中文字浮出的小工具条 + 旁边的问答小窗(不进当前会话) ── */
|
|
612
|
+
#ask-pop { position: fixed; z-index: 60; display: none; gap: 2px; padding: 3px; background: var(--surface); border: 1px solid var(--border2, var(--border)); border-radius: 9px; box-shadow: 0 6px 20px rgba(0,0,0,0.25); }
|
|
613
|
+
#ask-pop.show { display: flex; }
|
|
614
|
+
#ask-pop button { background: none; border: none; color: var(--text); font-size: 0.78rem; padding: 0.35rem 0.6rem; border-radius: 6px; cursor: pointer; white-space: nowrap; }
|
|
615
|
+
#ask-pop button:hover { background: var(--surface2); }
|
|
616
|
+
#ask-pop button.pri { color: var(--green); font-weight: 600; }
|
|
617
|
+
#ask-backdrop { display: none; position: fixed; inset: 0; z-index: 69; background: rgba(0,0,0,0.4); }
|
|
618
|
+
#ask-panel { position: fixed; z-index: 70; top: 0; right: 0; bottom: 0; width: min(420px, 100vw); background: var(--surface); border-left: 1px solid var(--border); box-shadow: -8px 0 30px rgba(0,0,0,0.18);
|
|
619
|
+
display: flex; flex-direction: column; transform: translateX(105%); transition: transform 0.2s ease; }
|
|
620
|
+
#ask-panel.open { transform: none; }
|
|
621
|
+
.ask-head { display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.8rem 0.9rem 0.6rem; border-bottom: 1px solid var(--border); }
|
|
622
|
+
.ask-head b { font-size: 0.95rem; }
|
|
623
|
+
.ask-sub { font-size: 0.7rem; color: var(--muted); margin-top: 0.15rem; }
|
|
624
|
+
.ask-x { margin-left: auto; background: none; border: none; color: var(--muted); font-size: 1rem; cursor: pointer; padding: 0.1rem 0.35rem; border-radius: 6px; }
|
|
625
|
+
.ask-x:hover { background: var(--surface2); color: var(--text); }
|
|
626
|
+
#ask-body { flex: 1; overflow-y: auto; padding: 0.7rem 0.9rem; font-size: 0.84rem; line-height: 1.7; }
|
|
627
|
+
.ask-quote { border-left: 3px solid var(--green); background: var(--bg); padding: 0.4rem 0.6rem; border-radius: 0 6px 6px 0; color: var(--text2, var(--text)); font-size: 0.78rem; white-space: pre-wrap; word-break: break-word; max-height: 8.5em; overflow-y: auto; margin-bottom: 0.7rem; }
|
|
628
|
+
.ask-q { margin: 0.7rem 0 0.3rem; text-align: right; }
|
|
629
|
+
.ask-q span { display: inline-block; background: var(--surface2); border-radius: 10px; padding: 0.3rem 0.65rem; font-size: 0.8rem; text-align: left; white-space: pre-wrap; }
|
|
630
|
+
.ask-a { padding: 0.1rem 0; }
|
|
631
|
+
.ask-a p { margin: 0.3rem 0; } .ask-a ul, .ask-a ol { margin: 0.3rem 0; padding-left: 1.2rem; }
|
|
632
|
+
.ask-a h1, .ask-a h2, .ask-a h3 { font-size: 0.88rem; margin: 0.5rem 0 0.3rem; }
|
|
633
|
+
.ask-a code { background: var(--surface2); padding: 0.05rem 0.3rem; border-radius: 4px; font-size: 0.78rem; font-family: 'SF Mono','Consolas',monospace; }
|
|
634
|
+
.ask-a pre { background: var(--bg); border: 1px solid var(--border); padding: 0.5rem 0.6rem; border-radius: 6px; overflow-x: auto; } .ask-a pre code { background: none; padding: 0; }
|
|
635
|
+
.ask-a.err { color: var(--red); font-size: 0.8rem; }
|
|
636
|
+
.ask-acts { display: flex; gap: 0.4rem; margin: 0.35rem 0 0.2rem; }
|
|
637
|
+
.ask-acts button { background: var(--bg); border: 1px solid var(--border); color: var(--text2, var(--text)); border-radius: 7px; padding: 0.22rem 0.6rem; font-size: 0.74rem; cursor: pointer; }
|
|
638
|
+
.ask-acts button.pri { border-color: var(--green); color: var(--green); font-weight: 600; }
|
|
639
|
+
.ask-foot { border-top: 1px solid var(--border); padding: 0.55rem 0.7rem calc(0.55rem + env(safe-area-inset-bottom)); display: flex; gap: 0.45rem; align-items: flex-end; }
|
|
640
|
+
#ask-input { flex: 1; resize: none; max-height: 7em; min-height: 2.3em; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 9px; padding: 0.45rem 0.6rem; font: inherit; font-size: 0.84rem; outline: none; }
|
|
641
|
+
#ask-input:focus { border-color: var(--green); }
|
|
642
|
+
#ask-send { flex-shrink: 0; background: var(--green); color: #fff; border: none; border-radius: 9px; padding: 0.5rem 0.8rem; font-size: 0.8rem; cursor: pointer; }
|
|
643
|
+
#ask-send.stop { background: var(--surface2); color: var(--text); }
|
|
644
|
+
@media (max-width: 768px) {
|
|
645
|
+
#ask-panel { top: auto; left: 0; width: auto; height: 78vh; border-left: none; border-top: 1px solid var(--border); border-radius: 16px 16px 0 0; transform: translateY(105%); }
|
|
646
|
+
#ask-panel.open + #ask-backdrop, #ask-backdrop.show { display: block; }
|
|
647
|
+
}
|
|
491
648
|
</style>
|
|
492
649
|
</head>
|
|
493
650
|
<body>
|
|
@@ -567,6 +724,8 @@
|
|
|
567
724
|
<button class="tb-btn" id="send-btn" onclick="sendCommand()" title="发送">
|
|
568
725
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
|
|
569
726
|
</button>
|
|
727
|
+
<!-- ⚡ 插话:AI 忙时把输入直接送进正在干的这一轮(不打断,AI 下一步看到);存草稿则等干完再发 -->
|
|
728
|
+
<button id="inject-btn" class="inj-btn" style="display:none" onclick="injectFromInput()" title="不打断 AI:马上把这条送过去,AI 在下一步就会看到并调整。 适合补充、提醒、微调方向;方向完全错了请先点 ■ 停止">⚡ 插话</button>
|
|
570
729
|
<!-- AI 忙时顶替发送键:这轮还没完(可能在跑子代理),先把想法存成草稿 -->
|
|
571
730
|
<button class="tb-btn" id="draft-btn" style="display:none;color:var(--yellow)" onclick="saveDraftFromInput('busy')" title="AI 还在跑,先存为草稿,干完再发">
|
|
572
731
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
@@ -880,44 +1039,61 @@
|
|
|
880
1039
|
</div>
|
|
881
1040
|
</div>
|
|
882
1041
|
|
|
883
|
-
<!--
|
|
1042
|
+
<!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务。
|
|
1043
|
+
本任务的草稿是一个有顺序的队列:可拖动 / ↑↓ 排序、就地编辑、多选合并 / 一起取回,开「依次自动发送」后每干完一轮发下一条 -->
|
|
884
1044
|
<div id="inbox-sheet" class="action-sheet">
|
|
885
1045
|
<div class="action-sheet-backdrop" onclick="closeInboxSheet()"></div>
|
|
886
|
-
<div class="action-sheet-box
|
|
887
|
-
<div class="
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
<div style="border-top:1px solid var(--surface2);padding:0.5rem 0.2rem">
|
|
893
|
-
<textarea id="inbox-new-text" rows="3" spellcheck="false" placeholder="写下临时想法…(截图可直接 Ctrl+V 粘进来)"
|
|
894
|
-
style="width:100%;box-sizing:border-box;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:0.5rem;font-size:0.82rem;resize:vertical"></textarea>
|
|
895
|
-
<div id="inbox-new-img" style="display:none;align-items:center;gap:0.4rem;margin-top:0.35rem;font-size:0.7rem;color:var(--text2)">
|
|
896
|
-
<span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
|
|
897
|
-
<button class="dr-btn" onclick="clearInboxNewImage()">移除</button>
|
|
1046
|
+
<div class="action-sheet-box ib-box">
|
|
1047
|
+
<div class="ib-head">
|
|
1048
|
+
<b>📥 草稿</b>
|
|
1049
|
+
<div class="ib-tabs">
|
|
1050
|
+
<button data-ibt="this" class="on" onclick="ibTab('this')">本任务 <span id="ib-n-this"></span></button>
|
|
1051
|
+
<button data-ibt="others" onclick="ibTab('others')">其它任务 <span id="inbox-others-count"></span></button>
|
|
898
1052
|
</div>
|
|
899
|
-
<
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
<
|
|
904
|
-
|
|
1053
|
+
<button class="ib-x" onclick="closeInboxSheet()" title="关闭">✕</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div id="ib-pane-this" class="ib-pane">
|
|
1056
|
+
<div class="ib-bar">
|
|
1057
|
+
<span id="ib-hint" class="ib-hint"></span>
|
|
1058
|
+
<button id="ib-sel-btn" class="dr-btn" onclick="ibToggleSelect()" title="勾选几条后合并、一起取回或删除">☑ 多选</button>
|
|
1059
|
+
<label id="ib-autoall" class="ib-autoall" title="打开后:每干完一轮,自动按顺序发出下一条(每条都是一轮真实的模型调用)。上一轮被中断或出错时会暂停,等你确认再继续。">
|
|
1060
|
+
<input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"><span class="ib-sw"></span>依次自动发送
|
|
905
1061
|
</label>
|
|
906
|
-
<button class="dr-btn take" style="margin-left:auto;padding:0.35rem 1rem" onclick="addDraftHere()">+ 记下来</button>
|
|
907
1062
|
</div>
|
|
908
|
-
<div style="
|
|
909
|
-
<
|
|
1063
|
+
<div id="ib-paused" class="ib-paused" style="display:none">
|
|
1064
|
+
<span>⏸ 自动发送暂停中:上一轮被中断或出错时不会自动往下发</span>
|
|
1065
|
+
<button class="dr-btn take" onclick="ibResume()">继续发送</button>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div id="inbox-this" class="ib-list"></div>
|
|
1068
|
+
<div id="ib-selbar" class="ib-selbar" style="display:none">
|
|
1069
|
+
<span>已选 <b id="ib-sel-n">0</b> 条</span>
|
|
1070
|
+
<button class="dr-btn" onclick="ibMergeSelected(false)" title="按列表顺序拼成一条(自动编号),可再编辑;之后也能撤销合并">🔗 合并</button>
|
|
1071
|
+
<button class="dr-btn take" onclick="ibMergeSelected(true)" title="合并后直接放进消息框">一起取回</button>
|
|
1072
|
+
<button class="dr-btn" onclick="ibDeleteSelected()" style="color:var(--red)">删除</button>
|
|
1073
|
+
<button class="dr-btn" onclick="ibToggleSelect(false)">取消</button>
|
|
1074
|
+
</div>
|
|
1075
|
+
<!-- 就地新增:不必先在消息框打字再"提取",直接在这儿写 -->
|
|
1076
|
+
<div class="ib-compose">
|
|
1077
|
+
<textarea id="inbox-new-text" rows="2" spellcheck="false" placeholder="记下一个想法…(截图可直接 Ctrl+V 粘进来)"></textarea>
|
|
1078
|
+
<div id="inbox-new-img" style="display:none;align-items:center;gap:0.4rem;margin-top:0.3rem;font-size:0.7rem;color:var(--text2)">
|
|
1079
|
+
<span>📷</span><span id="inbox-new-img-name" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
|
|
1080
|
+
<button class="dr-btn" onclick="clearInboxNewImage()">移除</button>
|
|
1081
|
+
</div>
|
|
1082
|
+
<div class="ib-compose-row">
|
|
1083
|
+
<button class="dr-btn" onclick="document.getElementById('inbox-new-file').click()" title="也可以直接 Ctrl+V 粘贴截图">📷</button>
|
|
1084
|
+
<input type="file" id="inbox-new-file" accept="image/*" style="display:none" onchange="uploadInboxImage(this.files[0]); this.value='';">
|
|
1085
|
+
<label style="display:flex;align-items:center;gap:0.3rem;font-size:0.7rem;color:var(--muted);cursor:pointer" title="这条也参与自动发送">
|
|
1086
|
+
<input type="checkbox" id="inbox-new-autosend" style="width:0.95rem;height:0.95rem"> ⚡ 自动
|
|
1087
|
+
</label>
|
|
1088
|
+
<a onclick="saveDraftFromInput('manual')" class="ib-link" title="把下方消息框里已写的内容存为草稿">存消息框内容</a>
|
|
1089
|
+
<button class="dr-btn take" style="margin-left:auto;padding:0.35rem 0.95rem" onclick="addDraftHere()">+ 记下来</button>
|
|
1090
|
+
</div>
|
|
910
1091
|
</div>
|
|
911
1092
|
</div>
|
|
912
|
-
<div
|
|
913
|
-
<
|
|
914
|
-
|
|
915
|
-
<div id="inbox-this" style="overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;min-height:3rem;max-height:34vh"></div>
|
|
916
|
-
<div style="font-size:0.66rem;color:var(--muted);padding:0.4rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
|
|
917
|
-
<a onclick="toggleInboxOthers()" style="color:var(--blue2);cursor:pointer">▸ 其它任务的草稿 <span id="inbox-others-count"></span></a>
|
|
1093
|
+
<div id="ib-pane-others" class="ib-pane" style="display:none">
|
|
1094
|
+
<div class="ib-hint" style="padding:0.1rem 0.2rem 0.4rem">其它任务里记的草稿。点「改投」挪到本任务。</div>
|
|
1095
|
+
<div id="inbox-others" class="ib-list"></div>
|
|
918
1096
|
</div>
|
|
919
|
-
<div id="inbox-others" style="display:none;overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;max-height:26vh"></div>
|
|
920
|
-
<button class="action-sheet-cancel" onclick="closeInboxSheet()">关闭</button>
|
|
921
1097
|
</div>
|
|
922
1098
|
</div>
|
|
923
1099
|
|
|
@@ -1052,6 +1228,25 @@
|
|
|
1052
1228
|
</div>
|
|
1053
1229
|
</div>
|
|
1054
1230
|
|
|
1231
|
+
<!-- 💡 问一问:选中文字后的小工具条 + 问答小窗。问答不进当前会话,不打扰正在干活的 AI -->
|
|
1232
|
+
<div id="ask-pop">
|
|
1233
|
+
<button class="pri" onmousedown="event.preventDefault()" onclick="askFromSelection(false)">💡 解释</button>
|
|
1234
|
+
<button onmousedown="event.preventDefault()" onclick="askFromSelection(true)">❓ 问一问…</button>
|
|
1235
|
+
<button onmousedown="event.preventDefault()" onclick="askCopySelection()">📋 复制</button>
|
|
1236
|
+
</div>
|
|
1237
|
+
<div id="ask-panel" role="dialog" aria-label="问一问">
|
|
1238
|
+
<div class="ask-head">
|
|
1239
|
+
<div><b>💡 问一问</b><div class="ask-sub">这里的问答不会打扰正在干活的 AI,放心问</div></div>
|
|
1240
|
+
<button class="ask-x" onclick="closeAsk()" title="关闭 (Esc)">✕</button>
|
|
1241
|
+
</div>
|
|
1242
|
+
<div id="ask-body"></div>
|
|
1243
|
+
<div class="ask-foot">
|
|
1244
|
+
<textarea id="ask-input" rows="1" placeholder="继续问…(Enter 发送)"></textarea>
|
|
1245
|
+
<button id="ask-send" onclick="askSendOrStop()">发送</button>
|
|
1246
|
+
</div>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div id="ask-backdrop" onclick="closeAsk()"></div>
|
|
1249
|
+
|
|
1055
1250
|
<!-- 清除:先说清楚会发生什么,免得误以为出了故障 -->
|
|
1056
1251
|
<div id="clear-sheet" class="action-sheet">
|
|
1057
1252
|
<div class="action-sheet-backdrop" onclick="closeClearSheet()"></div>
|
|
@@ -1071,59 +1266,113 @@
|
|
|
1071
1266
|
|
|
1072
1267
|
<div id="skill-sheet" class="action-sheet">
|
|
1073
1268
|
<div class="action-sheet-backdrop" onclick="closeSkillSheet()"></div>
|
|
1074
|
-
<div class="action-sheet-box
|
|
1075
|
-
<div class="
|
|
1076
|
-
<
|
|
1077
|
-
<
|
|
1078
|
-
|
|
1079
|
-
<button onclick="showSkillImport()" style="background:var(--surface2);color:var(--text2);border:1px solid var(--border);border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">Git</button>
|
|
1080
|
-
<button onclick="showSkillEditor()" style="background:var(--purple);color:#fff;border:none;border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">+ 新建</button>
|
|
1081
|
-
</div>
|
|
1269
|
+
<div class="action-sheet-box skl-box">
|
|
1270
|
+
<div class="skl-head">
|
|
1271
|
+
<button id="skl-back" class="skl-back" onclick="sklView('list')" style="display:none" title="返回">‹</button>
|
|
1272
|
+
<b id="skill-sheet-title">技能</b>
|
|
1273
|
+
<button class="ib-x" onclick="closeSkillSheet()" title="关闭">✕</button>
|
|
1082
1274
|
</div>
|
|
1083
|
-
<!--
|
|
1084
|
-
<div id="
|
|
1085
|
-
<div
|
|
1275
|
+
<!-- 列表 -->
|
|
1276
|
+
<div id="skl-v-list" class="skl-view">
|
|
1277
|
+
<div class="skl-tools">
|
|
1278
|
+
<button class="skl-tool pri" onclick="sklView('gen')" title="把本次任务里的做法提炼成技能,以后一点就能复用">✨ 从本次任务生成</button>
|
|
1279
|
+
<button class="skl-tool" onclick="sklView('import')" title="粘贴分享码 / 分享链接,或选择技能文件">📥 导入</button>
|
|
1280
|
+
<button class="skl-tool" onclick="sklView('team')" title="团队技能仓库:订阅、检查更新">👥 团队仓库<span id="skl-upd-badge" class="skl-badge" style="display:none"></span></button>
|
|
1281
|
+
<button class="skl-tool" onclick="showSkillMarket()">🛒 市场</button>
|
|
1282
|
+
<button class="skl-tool" onclick="showSkillEditor()">+ 手写</button>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div id="skl-upd-banner" class="skl-banner" style="display:none" onclick="sklView('team')"></div>
|
|
1285
|
+
<div id="skill-list-view" class="skl-list"></div>
|
|
1286
|
+
<div class="skl-foot">技能 = 预先写好的一段做事方法。<b>手动技能</b>点快捷栏按钮(或输入 /名称)运行;<b>自动技能</b>不用点,AI 遇到合适的场景会自己用。</div>
|
|
1086
1287
|
</div>
|
|
1087
|
-
<!--
|
|
1088
|
-
<div id="
|
|
1089
|
-
<div
|
|
1090
|
-
<input id="skill-name" class="slash-inp" placeholder="
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1288
|
+
<!-- 编辑 -->
|
|
1289
|
+
<div id="skl-v-edit" class="skl-view" style="display:none">
|
|
1290
|
+
<div class="skl-row">
|
|
1291
|
+
<input id="skill-name" class="slash-inp" placeholder="名称(英文,如 deploy-test)" autocomplete="off" style="flex:1">
|
|
1292
|
+
</div>
|
|
1293
|
+
<div class="skl-row">
|
|
1294
|
+
<select id="skill-kind" class="slash-inp skl-sel" onchange="sklKindChanged()">
|
|
1295
|
+
<option value="command">手动技能(/命令,点按钮运行)</option>
|
|
1296
|
+
<option value="skill">自动技能(AI 遇到合适场景自己用)</option>
|
|
1297
|
+
</select>
|
|
1298
|
+
<select id="skill-scope" class="slash-inp skl-sel">
|
|
1299
|
+
<option value="project">只在本项目</option>
|
|
1300
|
+
<option value="user">本机所有项目</option>
|
|
1094
1301
|
</select>
|
|
1095
|
-
<textarea id="skill-content" class="slash-inp" placeholder="# 技能标题 技能描述和指令内容..." rows="10" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5"></textarea>
|
|
1096
1302
|
</div>
|
|
1097
|
-
<
|
|
1098
|
-
<
|
|
1303
|
+
<textarea id="skill-content" class="slash-inp" rows="14" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5;width:100%;box-sizing:border-box"></textarea>
|
|
1304
|
+
<div id="skill-edit-hint" class="skl-hint"></div>
|
|
1305
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.5rem" onclick="saveSkill()">保存</button>
|
|
1099
1306
|
</div>
|
|
1100
|
-
<!--
|
|
1101
|
-
<div id="
|
|
1102
|
-
<div style="
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
<
|
|
1108
|
-
|
|
1109
|
-
|
|
1307
|
+
<!-- 从本次任务生成 -->
|
|
1308
|
+
<div id="skl-v-gen" class="skl-view" style="display:none">
|
|
1309
|
+
<div class="skl-hint" style="margin-bottom:0.5rem">让正在干活的 AI 把<b>本次任务里的做法</b>提炼成可复用的技能:项目特有的路径、名字会改成参数,不会写进密码等敏感信息。AI <b>先出草稿给你确认</b>,确认后才保存。</div>
|
|
1310
|
+
<textarea id="skl-gen-hint" class="slash-inp" rows="3" placeholder="想沉淀成什么技能?例如「把今天这套部署到测试环境的流程做成技能」。可以留空,让 AI 自己判断" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
|
|
1311
|
+
<div class="skl-lbl">技能类型</div>
|
|
1312
|
+
<div class="skl-radios">
|
|
1313
|
+
<label><input type="radio" name="skl-gen-kind" value="command" checked> <b>手动技能</b><small>点按钮 / 输入 /名称 才运行,最直观</small></label>
|
|
1314
|
+
<label><input type="radio" name="skl-gen-kind" value="skill"> <b>自动技能</b><small>AI 遇到合适场景自己用,可带辅助脚本</small></label>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div class="skl-lbl">保存到</div>
|
|
1317
|
+
<div class="skl-radios">
|
|
1318
|
+
<label><input type="radio" name="skl-gen-scope" value="project" checked> <b>只在本项目</b><small>跟着项目目录走</small></label>
|
|
1319
|
+
<label><input type="radio" name="skl-gen-scope" value="user"> <b>本机所有项目</b><small>这台电脑上的任务都能用</small></label>
|
|
1320
|
+
</div>
|
|
1321
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.6rem" onclick="genSkillFromTask()">✨ 让 AI 起草</button>
|
|
1322
|
+
</div>
|
|
1323
|
+
<!-- 分享 -->
|
|
1324
|
+
<div id="skl-v-share" class="skl-view" style="display:none">
|
|
1325
|
+
<div id="skl-share-info" class="skl-card"></div>
|
|
1326
|
+
<div id="skl-share-warn" class="skl-warn" style="display:none"></div>
|
|
1327
|
+
<div class="skl-share-grid">
|
|
1328
|
+
<button class="skl-big" onclick="sklShareDownload()"><span>⬇</span><b>下载技能文件</b><small>微信 / 邮件发给对方,对方「📥 导入」</small></button>
|
|
1329
|
+
<button class="skl-big" onclick="sklShareCode()"><span>📋</span><b>复制分享码</b><small>一段文本,粘贴给对方导入</small></button>
|
|
1330
|
+
<button class="skl-big" onclick="sklShareLink()"><span>🔗</span><b>生成分享链接</b><small>同一局域网 / Tailscale 的 myhi 直接导入,24 小时有效</small></button>
|
|
1331
|
+
<button class="skl-big" onclick="sklSharePublishForm()"><span>👥</span><b>发布到团队仓库</b><small>推到 Git 仓库,订阅的人会收到更新</small></button>
|
|
1332
|
+
</div>
|
|
1333
|
+
<div id="skl-share-out" class="skl-out" style="display:none"></div>
|
|
1334
|
+
<div id="skl-pub-form" style="display:none;margin-top:0.5rem">
|
|
1335
|
+
<input id="skl-pub-url" class="slash-inp" placeholder="仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
|
|
1336
|
+
<div class="skl-row">
|
|
1337
|
+
<input id="skl-pub-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
|
|
1338
|
+
<input id="skl-pub-pass" class="slash-inp" type="password" placeholder="Token(需要写权限)" autocomplete="off" style="flex:1">
|
|
1339
|
+
<input id="skl-pub-branch" class="slash-inp" placeholder="分支(默认)" autocomplete="off" style="width:6.5rem">
|
|
1110
1340
|
</div>
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1113
|
-
|
|
1341
|
+
<button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklPublish()">发布</button>
|
|
1342
|
+
</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
<!-- 导入 -->
|
|
1345
|
+
<div id="skl-v-import" class="skl-view" style="display:none">
|
|
1346
|
+
<textarea id="skl-imp-text" class="slash-inp" rows="3" placeholder="粘贴分享码(MYHI-SKILL:…)或分享链接(…/s/skill/…)" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
|
|
1347
|
+
<div class="skl-row" style="margin-top:0.35rem">
|
|
1348
|
+
<button class="dr-btn" onclick="document.getElementById('skl-imp-file').click()">📄 选择技能文件</button>
|
|
1349
|
+
<input type="file" id="skl-imp-file" accept=".myhiskill,.json,.md" style="display:none" onchange="sklImportFile(this.files[0]); this.value='';">
|
|
1350
|
+
<button class="dr-btn take" style="margin-left:auto" onclick="sklImportPreview()">预览</button>
|
|
1351
|
+
</div>
|
|
1352
|
+
<div id="skl-imp-preview"></div>
|
|
1353
|
+
</div>
|
|
1354
|
+
<!-- 团队仓库 -->
|
|
1355
|
+
<div id="skl-v-team" class="skl-view" style="display:none">
|
|
1356
|
+
<div id="skl-subs"></div>
|
|
1357
|
+
<div class="skl-lbl" style="margin-top:0.6rem">添加团队仓库</div>
|
|
1358
|
+
<input id="skill-git-url" class="slash-inp" placeholder="Git 仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
|
|
1359
|
+
<div class="skl-row">
|
|
1360
|
+
<input id="skill-git-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
|
|
1361
|
+
<input id="skill-git-pass" class="slash-inp" type="password" placeholder="密码 / Token(可选)" autocomplete="off" style="flex:1">
|
|
1362
|
+
</div>
|
|
1363
|
+
<div class="skl-row">
|
|
1364
|
+
<select id="skill-import-scope" class="slash-inp skl-sel">
|
|
1365
|
+
<option value="user">装到本机所有项目</option>
|
|
1366
|
+
<option value="project">只装到本项目</option>
|
|
1114
1367
|
</select>
|
|
1368
|
+
<button class="dr-btn take" onclick="scanGitSkills()">扫描</button>
|
|
1115
1369
|
</div>
|
|
1116
|
-
<div id="skill-import-preview"
|
|
1117
|
-
<div id="skill-import-scan-btn"></div>
|
|
1118
|
-
<button id="skill-import-install-btn" class="action-sheet-cancel" style="display:none;background:var(--green);color:#000;font-weight:600;margin-bottom:0.4rem" onclick="installGitSkills()">安装选中的技能</button>
|
|
1119
|
-
<button class="action-sheet-cancel" onclick="backToSkillList()">返回列表</button>
|
|
1370
|
+
<div id="skill-import-preview"></div>
|
|
1120
1371
|
</div>
|
|
1121
|
-
<!--
|
|
1122
|
-
<div id="
|
|
1372
|
+
<!-- 市场 -->
|
|
1373
|
+
<div id="skl-v-market" class="skl-view" style="display:none">
|
|
1123
1374
|
<div id="skill-market-list"></div>
|
|
1124
|
-
<button class="action-sheet-cancel" onclick="backToSkillList()" style="margin-top:0.5rem">返回列表</button>
|
|
1125
1375
|
</div>
|
|
1126
|
-
<button id="skill-close-btn" class="action-sheet-cancel" onclick="closeSkillSheet()">关闭</button>
|
|
1127
1376
|
</div>
|
|
1128
1377
|
</div>
|
|
1129
1378
|
|
|
@@ -1730,14 +1979,20 @@
|
|
|
1730
1979
|
}
|
|
1731
1980
|
|
|
1732
1981
|
// ── 消息渲染 ──────────────────────────────────
|
|
1733
|
-
|
|
1982
|
+
// ⚡ 插话气泡上的小标记:等 AI 下一步看到 → AI 已看到 / 未送达
|
|
1983
|
+
function injectTagHtml(inj) {
|
|
1984
|
+
if (!inj) return '';
|
|
1985
|
+
const st = inj.state || 'pending';
|
|
1986
|
+
const txt = st === 'seen' ? '⚡ 插话 · AI 已看到' : st === 'dropped' ? '⚡ 插话 · 未送达' + (inj.reason ? ':' + inj.reason : '') : '⚡ 插话 · 等 AI 下一步看到';
|
|
1987
|
+
return `<span class="inj-tag ${st}" data-inject-tag="${escHtml(inj.id || '')}">${escHtml(txt)}</span>`;
|
|
1988
|
+
}
|
|
1989
|
+
function addInputMessage(text, inject) {
|
|
1734
1990
|
endStream(); endToolGroup();
|
|
1735
1991
|
_userScrolledUp = false; hideNewMsgHint(); // 用户主动发消息 → 回到底部看回复
|
|
1736
|
-
_streamedTextThisCall = false;
|
|
1737
|
-
_textRenderedThisTurn = false;
|
|
1992
|
+
if (!inject) { _streamedTextThisCall = false; _textRenderedThisTurn = false; } // 插话并进当前这一轮,不算新一轮
|
|
1738
1993
|
const msg = document.createElement('div');
|
|
1739
|
-
msg.className = 'msg msg-input';
|
|
1740
|
-
msg.innerHTML = `<div><div class="bubble">${escHtml(text)}</div><div class="meta">${formatTime(new Date())}</div></div>`;
|
|
1994
|
+
msg.className = 'msg msg-input' + (inject ? ' msg-inject' : '');
|
|
1995
|
+
msg.innerHTML = `<div><div class="bubble">${escHtml(text)}</div><div class="meta">${injectTagHtml(inject)}${formatTime(new Date())}</div></div>`;
|
|
1741
1996
|
chatArea.appendChild(msg);
|
|
1742
1997
|
trimMessages();
|
|
1743
1998
|
scrollToBottom();
|
|
@@ -2191,9 +2446,9 @@
|
|
|
2191
2446
|
if (sel) { parent.appendChild(sel); return; }
|
|
2192
2447
|
if (syntheticUserKind(msg.content)) return;
|
|
2193
2448
|
const el = document.createElement('div');
|
|
2194
|
-
el.className = 'msg msg-input';
|
|
2449
|
+
el.className = 'msg msg-input' + (msg.inject ? ' msg-inject' : '');
|
|
2195
2450
|
const full = msg.timestamp ? new Date(msg.timestamp).toLocaleString('zh-CN') : '';
|
|
2196
|
-
el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta" title="${escHtml(full)}">${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
|
|
2451
|
+
el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta" title="${escHtml(full)}">${injectTagHtml(msg.inject)}${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
|
|
2197
2452
|
parent.appendChild(el);
|
|
2198
2453
|
return;
|
|
2199
2454
|
}
|
|
@@ -2552,8 +2807,11 @@
|
|
|
2552
2807
|
// busy 时发送键让位给「存草稿」键;回到 idle 再提示有没有待发草稿
|
|
2553
2808
|
const draftBtn = document.getElementById('draft-btn');
|
|
2554
2809
|
if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
|
|
2810
|
+
try { updateInjectBtn(); } catch {}
|
|
2555
2811
|
// 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
|
|
2556
2812
|
try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
|
|
2813
|
+
try { if (!busy && _sklGenPending) { const sc = _sklGenPending; _sklGenPending = false; loadSkills(); fetch('/api/skills/changed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: SESSION_ID, scope: sc }) }).catch(() => {}); } } catch {}
|
|
2814
|
+
try { if (!busy) _ibIdleSince = Date.now(); if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); } catch {}
|
|
2557
2815
|
try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
|
|
2558
2816
|
}
|
|
2559
2817
|
_postStatus(state);
|
|
@@ -2668,7 +2926,7 @@
|
|
|
2668
2926
|
const btn = document.createElement('button');
|
|
2669
2927
|
btn.textContent = '返回首页';
|
|
2670
2928
|
btn.style.cssText = 'margin-top:1rem;padding:0.5rem 1.5rem;border:1px solid var(--muted);border-radius:8px;background:transparent;color:var(--text2);cursor:pointer;font-size:0.9rem';
|
|
2671
|
-
btn.onclick = () =>
|
|
2929
|
+
btn.onclick = () => goHome();
|
|
2672
2930
|
overlay.appendChild(btn);
|
|
2673
2931
|
}
|
|
2674
2932
|
}
|
|
@@ -2766,7 +3024,7 @@
|
|
|
2766
3024
|
lastControlHolder = holder || null;
|
|
2767
3025
|
});
|
|
2768
3026
|
socket.on('control-denied', ({ reason }) => addStatusMessage('无法获取控制: ' + reason));
|
|
2769
|
-
socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() =>
|
|
3027
|
+
socket.on('kicked', ({ reason }) => { addStatusMessage(reason || '你已被管理员踢出'); setTimeout(() => goHome(), 2000); });
|
|
2770
3028
|
|
|
2771
3029
|
// ── Agent 模式事件 ──────────────────────────────
|
|
2772
3030
|
socket.on('agent:message', (msg) => {
|
|
@@ -2781,6 +3039,9 @@
|
|
|
2781
3039
|
} else if (msg.subtype === 'interrupted') {
|
|
2782
3040
|
addStatusMessage('查询已中断');
|
|
2783
3041
|
setWorkState('idle');
|
|
3042
|
+
} else if (msg.subtype === 'inject_state') {
|
|
3043
|
+
document.querySelectorAll(`[data-inject-tag="${CSS.escape(msg.id || '')}"]`).forEach(t => { t.outerHTML = injectTagHtml({ id: msg.id, state: msg.state, reason: msg.reason }); });
|
|
3044
|
+
if (msg.state === 'dropped' && msg.reason) addStatusMessage('⚡ ' + msg.reason);
|
|
2784
3045
|
} else if (msg.subtype === 'info' && msg.message) {
|
|
2785
3046
|
addStatusMessage(msg.message);
|
|
2786
3047
|
// 同步计划模式状态到按钮
|
|
@@ -2811,7 +3072,7 @@
|
|
|
2811
3072
|
const sel = buildSyntheticUserEl(msg.content);
|
|
2812
3073
|
if (sel) addStatusMessage(sel.textContent);
|
|
2813
3074
|
} else if (!kind) {
|
|
2814
|
-
addInputMessage(msg.content);
|
|
3075
|
+
addInputMessage(msg.content, msg.inject);
|
|
2815
3076
|
}
|
|
2816
3077
|
// caveat/noise:纯噪音,不渲染
|
|
2817
3078
|
}
|
|
@@ -2870,6 +3131,8 @@
|
|
|
2870
3131
|
// 关键:idle 放最后——渲染最终答复的 addAssistantMessage 会把状态置回 'working',
|
|
2871
3132
|
// 若在其之前置 idle 会被顶回「执行中」+ 取消按钮不复位。渲染全部完成后再回 idle。
|
|
2872
3133
|
setWorkState('idle');
|
|
3134
|
+
// 还有插话没被看到:AI 收尾后马上拿它开新一轮,界面别停在空闲
|
|
3135
|
+
if (msg.injectPending) { setWorkState('thinking'); showThinking(); }
|
|
2873
3136
|
// 收尾水位提示:server 在 result 上附带了刚从 transcript 反查的精确水位
|
|
2874
3137
|
// (ctxTokens——claude 2.1.292 起 stream 的 assistant usage 被清零,前端自己算不了)。
|
|
2875
3138
|
// 旧 server 无此字段时退回 _lastCtxTokens(老 CLI 的 assistant 流仍带真实 usage)。
|
|
@@ -3340,6 +3603,49 @@
|
|
|
3340
3603
|
cmdInput.focus();
|
|
3341
3604
|
}
|
|
3342
3605
|
|
|
3606
|
+
// ── ⚡ 插话 ─────────────────────────────────────────────
|
|
3607
|
+
// AI 忙时把输入(或一条草稿)直接送进正在干的这一轮:claude 在下一步(两次工具调用之间)看到它并调整,
|
|
3608
|
+
// 不打断、不重启。服务端靠 claude 的回显确认「AI 已看到」,气泡上的标记随之更新。
|
|
3609
|
+
function canInjectNow() {
|
|
3610
|
+
return currentSession?.mode === 'agent' && !!currentSession.injectable && workState !== 'idle' && canTakeControl() && !_askCtlId && !_planCtl;
|
|
3611
|
+
}
|
|
3612
|
+
window.updateInjectBtn = function() {
|
|
3613
|
+
const b = document.getElementById('inject-btn');
|
|
3614
|
+
if (b) b.style.display = canInjectNow() ? '' : 'none';
|
|
3615
|
+
};
|
|
3616
|
+
function injectEmit(payload, onOk) {
|
|
3617
|
+
if (!isController) {
|
|
3618
|
+
if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
3619
|
+
else { addStatusMessage('其他用户控制中,无法插话'); return; }
|
|
3620
|
+
}
|
|
3621
|
+
socket.emit('agent:inject', payload, (r) => {
|
|
3622
|
+
if (!r?.ok) { addStatusMessage('⚡ 插话没发出去:' + (r?.error || '未知原因')); return; }
|
|
3623
|
+
onOk && onOk(r);
|
|
3624
|
+
});
|
|
3625
|
+
}
|
|
3626
|
+
window.injectFromInput = function() {
|
|
3627
|
+
const text = cmdInput.value.trim();
|
|
3628
|
+
if (!text && !_pendingImage && !_pendingFile) { addStatusMessage('先在输入框里写下要补充的话,再点 ⚡ 插话'); cmdInput.focus(); return; }
|
|
3629
|
+
if (!canInjectNow()) { sendCommand(); return; }
|
|
3630
|
+
const imgPath = _pendingImage?.path || null;
|
|
3631
|
+
const filePath = _pendingFile?.path || null;
|
|
3632
|
+
const fileName = _pendingFile?.name || '';
|
|
3633
|
+
const display = imgPath ? `📷 ${text || '分析图片'}` : filePath ? `📎 ${fileName}${text ? ' ' + text : ''}` : text;
|
|
3634
|
+
const prompt = imgPath
|
|
3635
|
+
? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${text || '描述图片内容'}`
|
|
3636
|
+
: filePath
|
|
3637
|
+
? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${text || '分析这个文件'}`
|
|
3638
|
+
: text;
|
|
3639
|
+
injectEmit({ prompt, display }, () => {
|
|
3640
|
+
cmdInput.value = ''; cmdInput.style.height = 'auto';
|
|
3641
|
+
clearPendingImage(); clearPendingFile();
|
|
3642
|
+
cmdInput.focus();
|
|
3643
|
+
});
|
|
3644
|
+
};
|
|
3645
|
+
window.injectDraft = function(id) {
|
|
3646
|
+
injectEmit({ draftId: id }, async () => { closeInboxSheet(); await refreshInbox(); });
|
|
3647
|
+
};
|
|
3648
|
+
|
|
3343
3649
|
// ── @ 文件引用补全 ─────────────────────────────────────────
|
|
3344
3650
|
// 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
|
|
3345
3651
|
// 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
|
|
@@ -3601,6 +3907,7 @@
|
|
|
3601
3907
|
window.toggleControl = function() { if (isController) socket.emit('release-control', { sessionId: SESSION_ID }); else if (canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); };
|
|
3602
3908
|
|
|
3603
3909
|
function updateControlUI(controlling, holderName) {
|
|
3910
|
+
try { updateInjectBtn(); } catch {}
|
|
3604
3911
|
isController = controlling;
|
|
3605
3912
|
const badge = document.getElementById('control-badge');
|
|
3606
3913
|
if (controlling) {
|
|
@@ -3624,7 +3931,7 @@
|
|
|
3624
3931
|
// ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
|
|
3625
3932
|
// 快捷栏按钮说明:帮助面板和鼠标悬停提示共用这一份,加按钮时在这里补一句
|
|
3626
3933
|
const SK_HELP = {
|
|
3627
|
-
draft: ['📥 草稿', 'AI
|
|
3934
|
+
draft: ['📥 草稿', 'AI 忙时先把想法记下来排队:可拖动 / ↑↓ 排顺序、点文字直接改、多选合并或一起取回;开「依次自动发送」后每干完一轮自动发下一条;AI 忙时可「⚡ 插入」当前任务。'],
|
|
3628
3935
|
folder: ['📂 文件夹', '在这台电脑上用资源管理器打开本任务目录(后台/服务模式下不可用)。'],
|
|
3629
3936
|
model: ['模型', '切换 AI 模型。复杂任务用强模型,简单活换快模型更省;被安全检测误拦时也可换个模型再试。'],
|
|
3630
3937
|
plan: ['计划', '先让 AI 只调研、出方案、不改文件;你审批后再执行。再点一次「退出计划」可按方案执行或仅退出。'],
|
|
@@ -3636,7 +3943,7 @@
|
|
|
3636
3943
|
followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
|
|
3637
3944
|
git: ['提交', '查看改动并 git 提交 / 推送。'],
|
|
3638
3945
|
memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
|
|
3639
|
-
skills: ['技能', '
|
|
3946
|
+
skills: ['技能', '把做事方法存成技能,以后一点就能复用:可从本次任务一键生成,分享给其他 myhi(文件 / 分享码 / 链接 / 团队仓库),导入前自动做安全检查。'],
|
|
3640
3947
|
cost: ['费用', '查看本次会话的 token 用量与花费。'],
|
|
3641
3948
|
};
|
|
3642
3949
|
document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
|
|
@@ -3657,7 +3964,8 @@
|
|
|
3657
3964
|
<b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
|
|
3658
3965
|
<b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
|
|
3659
3966
|
<b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
|
|
3660
|
-
<b>AI 忙时</b
|
|
3967
|
+
<b>AI 忙时</b>:想补充或提醒,点输入框右边「⚡ 插话」——不打断,AI 下一步就会看到并调整(气泡上显示「AI 已看到」);不急的就回车「存草稿」排队,草稿里可排顺序、编辑、合并,开「依次自动发送」后每干完一轮自动发下一条,AI 忙时也能把草稿「⚡ 插入」当前任务。<br>
|
|
3968
|
+
<b>看不懂就问</b>:在聊天里<b>选中</b>看不懂的词或一段话(手机长按选中),点「💡 解释」或「❓ 问一问」,答案在旁边小窗里,可以继续追问;这些问答<b>不会打扰正在干活的 AI</b>,想让它知道就点「👉 告诉 AI」。<br>
|
|
3661
3969
|
<b>AI 提问 / 交方案</b>:AI 需要你拿主意时会弹出选项,选一个即可,也可「跳过,让 AI 自己决定」;收起后输入框上方「❓ AI 在等你回答」可再打开。<br>
|
|
3662
3970
|
<b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。
|
|
3663
3971
|
${sec('顶栏')}
|
|
@@ -4277,7 +4585,10 @@
|
|
|
4277
4585
|
|
|
4278
4586
|
// ── 辅助 ──────────────────────────────────────
|
|
4279
4587
|
function updateViewers(count) { document.getElementById('viewer-count').textContent = count > 1 ? `${count} 人在线` : ''; }
|
|
4280
|
-
|
|
4588
|
+
// 回首页走顶层窗口:电脑工作台里会话页是嵌在首页右侧的,只在嵌入框里跳 '/' 会把整个首页套进右侧
|
|
4589
|
+
function goHome() { try { (window.top || window).location.href = '/'; } catch { window.location.href = '/'; } }
|
|
4590
|
+
window.goHome = goHome;
|
|
4591
|
+
window.goBack = function() { goHome(); };
|
|
4281
4592
|
|
|
4282
4593
|
// ── 快速切换会话(免退出到首页)──────────────────────────
|
|
4283
4594
|
function switchRelTime(ms) {
|
|
@@ -4356,7 +4667,7 @@
|
|
|
4356
4667
|
const close = () => overlay.remove();
|
|
4357
4668
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
4358
4669
|
box.querySelector('#sw-cancel').onclick = close;
|
|
4359
|
-
box.querySelector('#sw-home').onclick = () => {
|
|
4670
|
+
box.querySelector('#sw-home').onclick = () => { goHome(); };
|
|
4360
4671
|
const bindRows = () => box.querySelectorAll('[data-sid]').forEach(row => row.onclick = () => {
|
|
4361
4672
|
const sid = row.dataset.sid;
|
|
4362
4673
|
if (sid && sid !== SESSION_ID) window.location.href = '/terminal/' + sid;
|
|
@@ -4641,6 +4952,7 @@
|
|
|
4641
4952
|
});
|
|
4642
4953
|
// 输入框上方的「AI 在等你」提示条:弹窗被收起后从这里再打开
|
|
4643
4954
|
function renderPendingBar() {
|
|
4955
|
+
try { updateInjectBtn(); } catch {}
|
|
4644
4956
|
const bar = document.getElementById('pending-bar');
|
|
4645
4957
|
if (!bar) return;
|
|
4646
4958
|
const txt = _planCtl ? '📋 AI 提交了方案,等你审批' : _askCtlId ? '❓ AI 在等你回答一个问题' : '';
|
|
@@ -5052,16 +5364,14 @@
|
|
|
5052
5364
|
const cb = document.getElementById('inbox-new-autosend');
|
|
5053
5365
|
if (cb) cb.checked = false;
|
|
5054
5366
|
clearInboxNewImage();
|
|
5367
|
+
_ibSelMode = false; _ibSel.clear(); _ibEditing = null;
|
|
5368
|
+
ibTab('this');
|
|
5055
5369
|
await refreshInbox(false);
|
|
5056
5370
|
renderInbox();
|
|
5057
5371
|
};
|
|
5058
5372
|
window.closeInboxSheet = function() {
|
|
5059
5373
|
document.getElementById('inbox-sheet').classList.remove('open');
|
|
5060
5374
|
};
|
|
5061
|
-
window.toggleInboxOthers = function() {
|
|
5062
|
-
const box = document.getElementById('inbox-others');
|
|
5063
|
-
box.style.display = box.style.display === 'none' ? '' : 'none';
|
|
5064
|
-
};
|
|
5065
5375
|
|
|
5066
5376
|
function draftPreview(d) {
|
|
5067
5377
|
const t = (d.text || '').replace(/\s+/g, ' ').trim();
|
|
@@ -5086,48 +5396,230 @@
|
|
|
5086
5396
|
return sameDay ? hm : `${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${hm}`;
|
|
5087
5397
|
}
|
|
5088
5398
|
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5399
|
+
// ── 草稿面板(队列)─────────────────────────────
|
|
5400
|
+
let _ibSelMode = false; // 多选模式
|
|
5401
|
+
const _ibSel = new Set(); // 选中的草稿 id
|
|
5402
|
+
let _ibEditing = null; // 正在就地编辑的草稿 id
|
|
5403
|
+
let _ibIdleSince = Date.now(); // 最近一次变空闲的时刻:刚干完的几秒里自动接力还没发,别急着报「暂停」
|
|
5404
|
+
let _ibPauseTimer = null;
|
|
5405
|
+
const ibEsc = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
5093
5406
|
|
|
5407
|
+
window.ibTab = function(t) {
|
|
5408
|
+
document.querySelectorAll('.ib-tabs button').forEach(b => b.classList.toggle('on', b.dataset.ibt === t));
|
|
5409
|
+
document.getElementById('ib-pane-this').style.display = t === 'this' ? '' : 'none';
|
|
5410
|
+
document.getElementById('ib-pane-others').style.display = t === 'others' ? '' : 'none';
|
|
5411
|
+
};
|
|
5412
|
+
|
|
5413
|
+
function renderInbox() {
|
|
5094
5414
|
const mine = myDrafts();
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5415
|
+
for (const id of [..._ibSel]) if (!mine.some(d => d.id === id)) _ibSel.delete(id);
|
|
5416
|
+
document.getElementById('ib-n-this').textContent = mine.length ? `(${mine.length})` : '';
|
|
5417
|
+
const autoN = mine.filter(d => d.autoSend).length;
|
|
5418
|
+
const allAuto = mine.length > 0 && autoN === mine.length;
|
|
5419
|
+
const autoAll = document.getElementById('ib-auto-all');
|
|
5420
|
+
autoAll.checked = allAuto;
|
|
5421
|
+
autoAll.indeterminate = autoN > 0 && !allAuto;
|
|
5422
|
+
document.getElementById('ib-hint').textContent = !mine.length ? '还没有草稿。AI 忙时想到什么,先记在这里。'
|
|
5423
|
+
: autoN ? `共 ${mine.length} 条,${autoN} 条会按顺序自动发送(绿色序号),每干完一轮发下一条`
|
|
5424
|
+
: `共 ${mine.length} 条,按顺序排队。AI 干完后点「取回」发送`;
|
|
5425
|
+
const selBtn = document.getElementById('ib-sel-btn');
|
|
5426
|
+
selBtn.textContent = _ibSelMode ? '完成' : '☑ 多选';
|
|
5427
|
+
selBtn.style.display = mine.length > 1 || _ibSelMode ? '' : 'none';
|
|
5428
|
+
// 多选时只管勾选和批量操作,自动发送开关先藏起来;没草稿时也不显示
|
|
5429
|
+
document.getElementById('ib-autoall').style.display = _ibSelMode || !mine.length ? 'none' : '';
|
|
5430
|
+
|
|
5431
|
+
// 暂停提示:有待自动发送的草稿,会话却已空闲一阵(上一轮被中断 / 出错时不会自动往下发)
|
|
5432
|
+
clearTimeout(_ibPauseTimer);
|
|
5433
|
+
const idleFor = Date.now() - _ibIdleSince;
|
|
5434
|
+
const paused = autoN > 0 && workState === 'idle' && idleFor > 6000;
|
|
5435
|
+
if (autoN > 0 && workState === 'idle' && !paused) _ibPauseTimer = setTimeout(() => { if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); }, 6100 - idleFor);
|
|
5436
|
+
document.getElementById('ib-paused').style.display = paused ? 'flex' : 'none';
|
|
5437
|
+
|
|
5438
|
+
const n = mine.length;
|
|
5439
|
+
document.getElementById('inbox-this').innerHTML = n ? mine.map((d, i) => {
|
|
5440
|
+
const editing = _ibEditing === d.id;
|
|
5441
|
+
const att = [d.images?.length ? `📷${d.images.length > 1 ? '×' + d.images.length : ''}` : '', d.files?.length ? `📎${d.files.length > 1 ? '×' + d.files.length : ''}` : ''].filter(Boolean).join(' ');
|
|
5442
|
+
const lead = _ibSelMode
|
|
5443
|
+
? `<input type="checkbox" ${_ibSel.has(d.id) ? 'checked' : ''} onchange="ibPick('${d.id}', this.checked)">`
|
|
5444
|
+
: `<span class="ib-handle" title="拖动排序" onmousedown="ibArmDrag(this)">⋮⋮</span>`;
|
|
5445
|
+
const body = editing
|
|
5446
|
+
? `<div class="ib-edit"><textarea id="ib-edit-ta" spellcheck="false">${ibEsc(d.text || '')}</textarea>
|
|
5447
|
+
<div><span style="font-size:0.64rem;color:var(--muted);margin-right:auto">Ctrl+Enter 保存 · Esc 取消</span>
|
|
5448
|
+
<button class="dr-btn" onclick="ibEditCancel()">取消</button><button class="dr-btn take" onclick="ibEditSave('${d.id}')">保存</button></div></div>`
|
|
5449
|
+
: `<div class="ib-text" onclick="${_ibSelMode ? `ibPick('${d.id}')` : `ibEdit('${d.id}')`}" title="${_ibSelMode ? '点选' : '点击编辑'}">${ibEsc((att ? att + ' ' : '') + ((d.text || '').trim() || '(仅附件)'))}</div>`;
|
|
5450
|
+
const meta = editing ? '' : `<div class="ib-meta">
|
|
5451
|
+
<span title="${ibEsc(new Date(d.createdAt).toLocaleString())}">🕒 ${draftTime(d.createdAt)}</span>
|
|
5100
5452
|
${d.source === 'busy' ? '<span class="dr-src">忙时自动存</span>' : ''}
|
|
5101
|
-
<
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
<button class="dr-btn" onclick="
|
|
5109
|
-
</span
|
|
5110
|
-
</div
|
|
5111
|
-
|
|
5112
|
-
|
|
5453
|
+
${d.mergedFrom?.length ? `<span class="dr-src">由 ${d.mergedFrom.length} 条合并 · <a style="cursor:pointer;text-decoration:underline" onclick="ibUnmerge('${d.id}')">撤销合并</a></span>` : ''}
|
|
5454
|
+
<button class="ib-chip ${d.autoSend ? 'on' : ''}" onclick="toggleAutoSend('${d.id}', ${!d.autoSend})" title="${d.autoSend ? '这条会在轮到它时自动发出。点一下取消' : '点一下:轮到这条时自动发出'}">⚡ ${d.autoSend ? '自动' : '手动'}</button>
|
|
5455
|
+
${_ibSelMode ? '' : `<span class="ib-acts">
|
|
5456
|
+
<button class="ib-ic" ${i === 0 ? 'disabled' : ''} onclick="ibMove('${d.id}', -1)" title="上移">↑</button>
|
|
5457
|
+
<button class="ib-ic" ${i === n - 1 ? 'disabled' : ''} onclick="ibMove('${d.id}', 1)" title="下移">↓</button>
|
|
5458
|
+
<button class="ib-ic" onclick="ibDelete('${d.id}')" title="删除">🗑</button>
|
|
5459
|
+
${canInjectNow() ? `<button class="dr-btn" style="color:var(--yellow);border:1px solid var(--yellow);background:rgba(210,153,34,0.12)" onclick="injectDraft('${d.id}')" title="AI 正在干活:不打断,马上把这条送过去,AI 下一步就会看到">⚡ 插入</button>` : ''}
|
|
5460
|
+
<button class="dr-btn take" onclick="useDraft('${d.id}')" title="放回消息框,草稿随即出队">取回</button>
|
|
5461
|
+
</span>`}
|
|
5462
|
+
</div>`;
|
|
5463
|
+
return `<div class="ib-item ${_ibSel.has(d.id) ? 'sel' : ''}" data-id="${d.id}">${lead}<span class="ib-no ${d.autoSend ? 'auto' : ''}" title="${d.autoSend ? '第 ' + (i + 1) + ' 条 · 自动发送' : '第 ' + (i + 1) + ' 条'}">${i + 1}</span><div class="ib-main">${body}${meta}</div></div>`;
|
|
5464
|
+
}).join('') : '<div style="color:var(--muted);font-size:0.75rem;padding:1.2rem 0.5rem;text-align:center">本任务暂无草稿</div>';
|
|
5465
|
+
|
|
5466
|
+
const selbar = document.getElementById('ib-selbar');
|
|
5467
|
+
selbar.style.display = _ibSelMode ? 'flex' : 'none';
|
|
5468
|
+
document.getElementById('ib-sel-n').textContent = _ibSel.size;
|
|
5469
|
+
selbar.querySelectorAll('button').forEach((b, k) => { if (k < 3) b.disabled = k < 2 ? _ibSel.size < (k === 0 ? 2 : 1) : !_ibSel.size; });
|
|
5470
|
+
|
|
5471
|
+
if (_ibEditing) {
|
|
5472
|
+
const ta = document.getElementById('ib-edit-ta');
|
|
5473
|
+
if (ta && document.activeElement !== ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); }
|
|
5474
|
+
if (ta) ta.onkeydown = (e) => {
|
|
5475
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); ibEditCancel(); }
|
|
5476
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); ibEditSave(_ibEditing); }
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
ibBindDrag();
|
|
5113
5480
|
|
|
5481
|
+
// 其它任务:按任务分组
|
|
5114
5482
|
const others = otherDrafts();
|
|
5115
|
-
document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5483
|
+
document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '';
|
|
5484
|
+
const groups = new Map();
|
|
5485
|
+
for (const d of others) { const k = d.sessionId; if (!groups.has(k)) groups.set(k, []); groups.get(k).push(d); }
|
|
5486
|
+
document.getElementById('inbox-others').innerHTML = others.length ? [...groups.values()].map(g => `
|
|
5487
|
+
<div class="ib-grp">${ibEsc(g[0].sessionTitle || '(未知任务)')}${g[0].projectName ? ' · ' + ibEsc(g[0].projectName) : ''}${g[0].sessionAlive ? '' : ' <span style="color:var(--muted);font-weight:400">(已结束)</span>'}</div>
|
|
5488
|
+
${g.map(d => `<div class="ib-item"><div class="ib-main">
|
|
5489
|
+
<div class="ib-text" style="cursor:default;color:var(--text2)">${ibEsc(draftPreview(d))}</div>
|
|
5490
|
+
<div class="ib-meta"><span>🕒 ${draftTime(d.createdAt)} · ${draftAge(d.createdAt)}</span>
|
|
5491
|
+
<span class="ib-acts"><button class="ib-ic" onclick="deleteDraft('${d.id}')" title="删除">🗑</button>
|
|
5492
|
+
<button class="dr-btn" onclick="reassignDraft('${d.id}')" title="挪到本任务,排到队尾">改投到本任务</button></span></div>
|
|
5493
|
+
</div></div>`).join('')}`).join('')
|
|
5494
|
+
: '<div style="color:var(--muted);font-size:0.75rem;padding:1.2rem 0.5rem;text-align:center">没有其它任务的草稿</div>';
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5497
|
+
async function ibReload() { await refreshInbox(); renderInbox(); }
|
|
5498
|
+
async function ibPost(url, body) {
|
|
5499
|
+
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|
5500
|
+
const d = await r.json().catch(() => ({}));
|
|
5501
|
+
if (!r.ok || d.error) throw new Error(d.error || ('HTTP ' + r.status));
|
|
5502
|
+
return d;
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
// 就地编辑
|
|
5506
|
+
window.ibEdit = function(id) { _ibEditing = id; renderInbox(); };
|
|
5507
|
+
window.ibEditCancel = function() { _ibEditing = null; renderInbox(); };
|
|
5508
|
+
window.ibEditSave = async function(id) {
|
|
5509
|
+
const ta = document.getElementById('ib-edit-ta');
|
|
5510
|
+
const text = ta ? ta.value : '';
|
|
5511
|
+
const d = myDrafts().find(x => x.id === id);
|
|
5512
|
+
if (!text.trim() && !d?.images?.length && !d?.files?.length) { addStatusMessage('草稿不能为空;不要了可以删除'); return; }
|
|
5513
|
+
try {
|
|
5514
|
+
await fetch('/api/inbox/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
|
|
5515
|
+
} catch { addStatusMessage('保存失败,请重试'); return; }
|
|
5516
|
+
_ibEditing = null;
|
|
5517
|
+
await ibReload();
|
|
5518
|
+
};
|
|
5519
|
+
|
|
5520
|
+
// 排序:↑↓(手机)/ 拖动(电脑)都汇成「新顺序」提交
|
|
5521
|
+
async function ibSaveOrder(ids) {
|
|
5522
|
+
const byId = new Map(_inboxCache.drafts.map(d => [d.id, d]));
|
|
5523
|
+
// 先本地生效再提交,拖动 / 连点 ↑↓ 不卡
|
|
5524
|
+
const mine = ids.map(id => byId.get(id)).filter(Boolean);
|
|
5525
|
+
_inboxCache.drafts = _inboxCache.drafts.filter(d => d.sessionId !== SESSION_ID).concat(mine);
|
|
5526
|
+
renderInbox();
|
|
5527
|
+
try { await ibPost('/api/inbox/reorder', { sessionId: SESSION_ID, ids }); } catch (e) { addStatusMessage('保存顺序失败:' + e.message); }
|
|
5528
|
+
await ibReload();
|
|
5529
|
+
}
|
|
5530
|
+
window.ibMove = function(id, dir) {
|
|
5531
|
+
const ids = myDrafts().map(d => d.id);
|
|
5532
|
+
const i = ids.indexOf(id), j = i + dir;
|
|
5533
|
+
if (i < 0 || j < 0 || j >= ids.length) return;
|
|
5534
|
+
[ids[i], ids[j]] = [ids[j], ids[i]];
|
|
5535
|
+
ibSaveOrder(ids);
|
|
5536
|
+
};
|
|
5537
|
+
window.ibArmDrag = function(h) { const it = h.closest('.ib-item'); if (it) it.draggable = true; };
|
|
5538
|
+
let _ibDragId = null;
|
|
5539
|
+
function ibBindDrag() {
|
|
5540
|
+
document.querySelectorAll('#inbox-this .ib-item').forEach(it => {
|
|
5541
|
+
it.ondragstart = (e) => { _ibDragId = it.dataset.id; it.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; try { e.dataTransfer.setData('text/plain', _ibDragId); } catch {} };
|
|
5542
|
+
it.ondragend = () => { it.draggable = false; it.classList.remove('dragging'); _ibDragId = null; document.querySelectorAll('.ib-item').forEach(x => x.classList.remove('drag-over-top', 'drag-over-bottom')); };
|
|
5543
|
+
it.ondragover = (e) => {
|
|
5544
|
+
if (!_ibDragId || it.dataset.id === _ibDragId) return;
|
|
5545
|
+
e.preventDefault();
|
|
5546
|
+
const r = it.getBoundingClientRect(), top = e.clientY < r.top + r.height / 2;
|
|
5547
|
+
it.classList.toggle('drag-over-top', top); it.classList.toggle('drag-over-bottom', !top);
|
|
5548
|
+
};
|
|
5549
|
+
it.ondragleave = () => it.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
5550
|
+
it.ondrop = (e) => {
|
|
5551
|
+
e.preventDefault();
|
|
5552
|
+
if (!_ibDragId || it.dataset.id === _ibDragId) return;
|
|
5553
|
+
const top = it.classList.contains('drag-over-top');
|
|
5554
|
+
const ids = myDrafts().map(d => d.id).filter(x => x !== _ibDragId);
|
|
5555
|
+
ids.splice(ids.indexOf(it.dataset.id) + (top ? 0 : 1), 0, _ibDragId);
|
|
5556
|
+
ibSaveOrder(ids);
|
|
5557
|
+
};
|
|
5558
|
+
});
|
|
5129
5559
|
}
|
|
5130
5560
|
|
|
5561
|
+
// 多选
|
|
5562
|
+
window.ibToggleSelect = function(on) {
|
|
5563
|
+
_ibSelMode = typeof on === 'boolean' ? on : !_ibSelMode;
|
|
5564
|
+
_ibSel.clear(); _ibEditing = null;
|
|
5565
|
+
renderInbox();
|
|
5566
|
+
};
|
|
5567
|
+
window.ibPick = function(id, on) {
|
|
5568
|
+
if (on === undefined) on = !_ibSel.has(id);
|
|
5569
|
+
on ? _ibSel.add(id) : _ibSel.delete(id);
|
|
5570
|
+
renderInbox();
|
|
5571
|
+
};
|
|
5572
|
+
window.ibMergeSelected = async function(take) {
|
|
5573
|
+
const ids = myDrafts().map(d => d.id).filter(id => _ibSel.has(id)); // 按列表顺序
|
|
5574
|
+
if (!ids.length) return;
|
|
5575
|
+
if (ids.length === 1) { if (take) { _ibSelMode = false; _ibSel.clear(); return useDraft(ids[0]); } return; }
|
|
5576
|
+
let merged;
|
|
5577
|
+
try { merged = (await ibPost('/api/inbox/merge', { ids })).draft; }
|
|
5578
|
+
catch (e) { addStatusMessage('合并失败:' + e.message); return; }
|
|
5579
|
+
_ibSelMode = false; _ibSel.clear();
|
|
5580
|
+
await refreshInbox();
|
|
5581
|
+
if (take) return useDraft(merged.id);
|
|
5582
|
+
_ibEditing = merged.id;
|
|
5583
|
+
renderInbox();
|
|
5584
|
+
addStatusMessage(`📥 已把 ${ids.length} 条合并成一条,可以直接改;不满意可点「撤销合并」`);
|
|
5585
|
+
};
|
|
5586
|
+
window.ibDeleteSelected = async function() {
|
|
5587
|
+
const ids = [..._ibSel];
|
|
5588
|
+
if (!ids.length || !confirm(`删除选中的 ${ids.length} 条草稿?`)) return;
|
|
5589
|
+
for (const id of ids) { try { await fetch('/api/inbox/' + id, { method: 'DELETE' }); } catch {} }
|
|
5590
|
+
_ibSelMode = false; _ibSel.clear();
|
|
5591
|
+
await ibReload();
|
|
5592
|
+
};
|
|
5593
|
+
window.ibDelete = async function(id) {
|
|
5594
|
+
const d = myDrafts().find(x => x.id === id);
|
|
5595
|
+
if (d && (d.text || '').trim().length > 20 && !confirm('删除这条草稿?\n\n' + d.text.trim().slice(0, 80))) return;
|
|
5596
|
+
await deleteDraft(id);
|
|
5597
|
+
};
|
|
5598
|
+
window.ibUnmerge = async function(id) {
|
|
5599
|
+
try { await ibPost('/api/inbox/' + id + '/unmerge'); } catch (e) { addStatusMessage('撤销合并失败:' + e.message); return; }
|
|
5600
|
+
_ibEditing = null;
|
|
5601
|
+
await ibReload();
|
|
5602
|
+
addStatusMessage('📥 已撤销合并,原来的几条已放回');
|
|
5603
|
+
};
|
|
5604
|
+
|
|
5605
|
+
// 依次自动发送:整队开 / 关
|
|
5606
|
+
window.ibAutoAll = async function(on) {
|
|
5607
|
+
const mine = myDrafts();
|
|
5608
|
+
const targets = mine.filter(d => !!d.autoSend !== on);
|
|
5609
|
+
if (on && !confirm(`按顺序自动发送 ${mine.length} 条草稿?\n\n每干完一轮自动发下一条,共约 ${mine.length} 轮模型调用。\n上一轮被中断或出错时会暂停,等你确认再继续。`)) { renderInbox(); return; }
|
|
5610
|
+
for (const d of targets) {
|
|
5611
|
+
try { await fetch('/api/inbox/' + d.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ autoSend: on }) }); } catch {}
|
|
5612
|
+
}
|
|
5613
|
+
await ibReload();
|
|
5614
|
+
addStatusMessage(on ? (workState === 'idle' ? '📥 已开启依次自动发送,马上发出第 1 条' : '📥 已开启依次自动发送:本轮干完后从第 1 条开始') : '📥 已关闭自动发送');
|
|
5615
|
+
};
|
|
5616
|
+
window.ibResume = async function() {
|
|
5617
|
+
try { await ibPost('/api/inbox/resume', { sessionId: SESSION_ID }); } catch (e) { addStatusMessage('继续失败:' + e.message); return; }
|
|
5618
|
+
_ibIdleSince = Date.now();
|
|
5619
|
+
renderInbox();
|
|
5620
|
+
addStatusMessage('📥 继续按顺序发送草稿');
|
|
5621
|
+
};
|
|
5622
|
+
|
|
5131
5623
|
// ── 抽屉内就地新增草稿(不必先在消息框打字再"提取")──────────
|
|
5132
5624
|
let _inboxNewImage = null; // { path, name }
|
|
5133
5625
|
|
|
@@ -5215,13 +5707,16 @@
|
|
|
5215
5707
|
window.useDraft = async function(id) {
|
|
5216
5708
|
const d = _inboxCache.drafts.find(x => x.id === id);
|
|
5217
5709
|
if (!d) return;
|
|
5218
|
-
|
|
5710
|
+
// 消息框一次只能挂一张图 / 一个文件:合并来的多余附件以路径写进文字里,AI 照样会读
|
|
5711
|
+
const extra = [...(d.images || []).slice(1).map(a => `另附图片 ${a.path},请用 Read 工具读取。`),
|
|
5712
|
+
...(d.files || []).slice(d.images?.length ? 0 : 1).map(a => `另附文件 ${a.path}(原文件名 ${a.name}),请读取分析。`)];
|
|
5713
|
+
cmdInput.value = (d.text || '') + (extra.length ? '\n' + extra.join('\n') : '');
|
|
5219
5714
|
cmdInput.style.height = 'auto';
|
|
5220
5715
|
cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
|
|
5221
5716
|
clearPendingImage();
|
|
5222
5717
|
clearPendingFile();
|
|
5223
5718
|
if (d.images?.length) setPendingImage(d.images[0].path, '');
|
|
5224
|
-
if (d.files?.length) {
|
|
5719
|
+
else if (d.files?.length) {
|
|
5225
5720
|
_pendingFile = { path: d.files[0].path, name: d.files[0].name };
|
|
5226
5721
|
const fp = document.getElementById('file-preview');
|
|
5227
5722
|
const nameEl = document.getElementById('file-preview-name');
|
|
@@ -5551,6 +6046,7 @@
|
|
|
5551
6046
|
const customs = _customSkills.filter(s => s.source !== 'builtin');
|
|
5552
6047
|
// 内置技能:始终逐个显示(青绿色)
|
|
5553
6048
|
const builtinEl = document.getElementById('sk-builtin-skills');
|
|
6049
|
+
loadSubs(); // 团队仓库有更新时快捷栏「技能」按钮带个绿点
|
|
5554
6050
|
builtinEl.innerHTML = builtins.map(s =>
|
|
5555
6051
|
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--teal)">/${escHtml(s.name)}</button>`
|
|
5556
6052
|
).join('');
|
|
@@ -5559,14 +6055,15 @@
|
|
|
5559
6055
|
if (!customs.length) { customEl.innerHTML = ''; return; }
|
|
5560
6056
|
if (customs.length <= 3) {
|
|
5561
6057
|
customEl.innerHTML = customs.map(s =>
|
|
5562
|
-
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--purple2)"
|
|
6058
|
+
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}', false, '${s.kind || 'command'}')" title="${escHtml(s.desc)}${s.kind === 'skill' ? '(自动技能:AI 也会在合适时自己用)' : ''}" style="color:var(--purple2)">${s.kind === 'skill' ? '🧠' : '/'}${escHtml(s.name)}</button>`
|
|
5563
6059
|
).join('');
|
|
5564
6060
|
} else {
|
|
5565
6061
|
customEl.innerHTML = `<button class="sk sk-claude" onclick="openCmdSheet()" style="color:var(--purple2)">/命令 (${customs.length})</button>`;
|
|
5566
6062
|
}
|
|
5567
6063
|
} catch {}
|
|
5568
6064
|
}
|
|
5569
|
-
window.runSkill = function(name, skipPrompt) {
|
|
6065
|
+
window.runSkill = function(name, skipPrompt, kind) {
|
|
6066
|
+
_pendingSkillKind = kind || _customSkills.find(s => s.name === name)?.kind || 'command';
|
|
5570
6067
|
// 直接运行(从弹窗确认后调用 skipPrompt=true)
|
|
5571
6068
|
if (skipPrompt) {
|
|
5572
6069
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
@@ -5577,10 +6074,10 @@
|
|
|
5577
6074
|
return;
|
|
5578
6075
|
}
|
|
5579
6076
|
// 先弹窗让用户补充要求
|
|
5580
|
-
const skill = _customSkills.find(s => s.name === name);
|
|
6077
|
+
const skill = _customSkills.find(s => s.name === name && (s.kind || 'command') === _pendingSkillKind);
|
|
5581
6078
|
openSkillRunSheet(name, skill?.desc);
|
|
5582
6079
|
};
|
|
5583
|
-
let _pendingSkillName = null;
|
|
6080
|
+
let _pendingSkillName = null, _pendingSkillKind = 'command';
|
|
5584
6081
|
window.openSkillRunSheet = function(name, desc) {
|
|
5585
6082
|
_pendingSkillName = name;
|
|
5586
6083
|
const sheet = document.getElementById('skill-run-sheet');
|
|
@@ -5602,6 +6099,13 @@
|
|
|
5602
6099
|
closeSkillRunSheet();
|
|
5603
6100
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
5604
6101
|
const displayCmd = args ? `/${name} ${args}` : `/${name}`;
|
|
6102
|
+
if (_pendingSkillKind === 'skill') {
|
|
6103
|
+
// 自动技能(Agent 技能)没有 /命令:明确请 AI 用它
|
|
6104
|
+
socket.emit('agent:query', { prompt: `请使用「${name}」技能${args ? '完成:' + args : '。'}` });
|
|
6105
|
+
addInputMessage(`🧠 ${name}${args ? ' ' + args : ''}`);
|
|
6106
|
+
showThinking();
|
|
6107
|
+
return;
|
|
6108
|
+
}
|
|
5605
6109
|
if (skill?.source === 'builtin') {
|
|
5606
6110
|
// 内置技能不在 .claude/commands/ 里,需拉取内容后直接发送为 prompt
|
|
5607
6111
|
try {
|
|
@@ -5634,8 +6138,8 @@
|
|
|
5634
6138
|
const filtered = _customSkills.filter(s =>
|
|
5635
6139
|
s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
|
|
5636
6140
|
listEl.innerHTML = filtered.length
|
|
5637
|
-
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}')">
|
|
5638
|
-
<div><div style="color:var(--purple3)"
|
|
6141
|
+
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}', '${s.kind || 'command'}')">
|
|
6142
|
+
<div><div style="color:var(--purple3)">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
|
|
5639
6143
|
</div>`).join('')
|
|
5640
6144
|
: '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>';
|
|
5641
6145
|
};
|
|
@@ -5647,9 +6151,9 @@
|
|
|
5647
6151
|
window.closeCmdSheet = function() {
|
|
5648
6152
|
document.getElementById('cmd-sheet').classList.remove('open');
|
|
5649
6153
|
};
|
|
5650
|
-
window.runSkillAndClose = function(name) {
|
|
6154
|
+
window.runSkillAndClose = function(name, kind) {
|
|
5651
6155
|
closeCmdSheet();
|
|
5652
|
-
runSkill(name);
|
|
6156
|
+
runSkill(name, false, kind);
|
|
5653
6157
|
};
|
|
5654
6158
|
// 加入会话后加载 skill
|
|
5655
6159
|
socket.on('joined', () => {
|
|
@@ -5661,252 +6165,440 @@
|
|
|
5661
6165
|
if (document.getElementById('inbox-sheet')?.classList.contains('open')) renderInbox();
|
|
5662
6166
|
});
|
|
5663
6167
|
|
|
5664
|
-
// ──
|
|
5665
|
-
|
|
5666
|
-
|
|
6168
|
+
// ── 技能管理(见 src/skills.js)────────────────────────
|
|
6169
|
+
// 手动技能 = Claude 的斜杠命令(.claude/commands/名.md);自动技能 = Agent 技能(.claude/skills/名/SKILL.md,AI 自己调用)。
|
|
6170
|
+
// ✨ 从本次任务生成(AI 起草、确认后保存)、📤 分享(文件 / 分享码 / 链接 / 团队仓库)、📥 导入(先预览 + 安全扫描)、👥 团队仓库订阅。
|
|
6171
|
+
let _editingSkill = null; // 编辑中的 { name, source, kind },null = 新建
|
|
6172
|
+
let _sklAll = [], _sklDirs = null, _sklShare = null, _sklImp = null, _gitSkills = [], _sklSubs = [];
|
|
6173
|
+
const SKL_TITLES = { list: '技能', edit: '编辑技能', gen: '✨ 从本次任务生成技能', share: '📤 分享技能', import: '📥 导入技能', team: '👥 团队技能仓库', market: '🛒 技能市场' };
|
|
6174
|
+
const sklEsc = (s) => escHtml(String(s ?? '')).replace(/"/g, '"').replace(/'/g, ''');
|
|
6175
|
+
const sklKindLabel = (k) => k === 'skill' ? '自动' : '手动';
|
|
6176
|
+
const sklScopeLabel = (s) => s === 'project' ? '本项目' : s === 'user' ? '本机' : '内置';
|
|
6177
|
+
async function sklJson(url, opts) {
|
|
6178
|
+
const r = await fetch(url, opts);
|
|
6179
|
+
const d = await r.json().catch(() => ({}));
|
|
6180
|
+
if (!r.ok || d.ok === false || d.error) { const e = new Error(d.error || ('HTTP ' + r.status)); e.data = d; throw e; }
|
|
6181
|
+
return d;
|
|
6182
|
+
}
|
|
6183
|
+
const sklPost = (url, body) => sklJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|
6184
|
+
|
|
6185
|
+
window.sklView = function(v) {
|
|
6186
|
+
for (const k of Object.keys(SKL_TITLES)) { const el = document.getElementById('skl-v-' + k); if (el) el.style.display = k === v ? '' : 'none'; }
|
|
6187
|
+
document.getElementById('skill-sheet-title').textContent = SKL_TITLES[v] || '技能';
|
|
6188
|
+
document.getElementById('skl-back').style.display = v === 'list' ? 'none' : '';
|
|
6189
|
+
if (v === 'list') refreshSkillList();
|
|
6190
|
+
if (v === 'team') renderSubs();
|
|
6191
|
+
if (v === 'import') { document.getElementById('skl-imp-preview').innerHTML = ''; setTimeout(() => document.getElementById('skl-imp-text').focus(), 80); }
|
|
6192
|
+
};
|
|
5667
6193
|
window.openSkillSheet = async function() {
|
|
5668
6194
|
document.getElementById('skill-sheet').classList.add('open');
|
|
5669
|
-
|
|
5670
|
-
document.getElementById('skill-edit-view').style.display = 'none';
|
|
5671
|
-
document.getElementById('skill-close-btn').style.display = '';
|
|
5672
|
-
await refreshSkillList();
|
|
5673
|
-
};
|
|
5674
|
-
window.closeSkillSheet = function() {
|
|
5675
|
-
document.getElementById('skill-sheet').classList.remove('open');
|
|
6195
|
+
sklView('list');
|
|
5676
6196
|
};
|
|
6197
|
+
window.closeSkillSheet = function() { document.getElementById('skill-sheet').classList.remove('open'); };
|
|
6198
|
+
window.backToSkillList = function() { sklView('list'); };
|
|
5677
6199
|
|
|
5678
6200
|
async function refreshSkillList() {
|
|
5679
6201
|
const listEl = document.getElementById('skill-list-view');
|
|
5680
|
-
listEl.innerHTML = '<div
|
|
6202
|
+
listEl.innerHTML = '<div class="skl-empty">加载中…</div>';
|
|
5681
6203
|
try {
|
|
5682
|
-
const
|
|
5683
|
-
|
|
5684
|
-
const
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
return;
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
6204
|
+
const data = await sklJson(`/api/skills?sessionId=${SESSION_ID}`);
|
|
6205
|
+
_sklAll = data.skills || []; _sklDirs = data.dirs || null;
|
|
6206
|
+
const own = _sklAll.filter(s => s.source !== 'builtin');
|
|
6207
|
+
const group = (src, title) => {
|
|
6208
|
+
const items = own.filter(s => s.source === src);
|
|
6209
|
+
if (!items.length) return '';
|
|
6210
|
+
return `<div class="skl-grp">${title}</div>` + items.map(s => `
|
|
6211
|
+
<div class="skl-item">
|
|
6212
|
+
<div class="skl-main" onclick="editSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="点击编辑">
|
|
6213
|
+
<div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${s.fileCount > 1 ? `<span class="skl-tag">${s.fileCount} 个文件</span>` : ''}</div>
|
|
6214
|
+
<div class="skl-desc">${escHtml(s.desc || '(没有说明)')}</div>
|
|
6215
|
+
</div>
|
|
6216
|
+
<div class="skl-acts">
|
|
6217
|
+
<button class="ib-ic" onclick="openSkillShare('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="分享给其他 myhi">📤</button>
|
|
6218
|
+
<button class="ib-ic" onclick="deleteSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="删除">🗑</button>
|
|
6219
|
+
</div>
|
|
6220
|
+
</div>`).join('');
|
|
6221
|
+
};
|
|
6222
|
+
listEl.innerHTML = own.length ? group('project', '本项目') + group('user', '本机所有项目')
|
|
6223
|
+
: '<div class="skl-empty">还没有技能。<br>做完一件以后还会再做的事,点上面「✨ 从本次任务生成」,下次一点就能复用。</div>';
|
|
5698
6224
|
} catch (e) {
|
|
5699
|
-
listEl.innerHTML = `<div style="
|
|
6225
|
+
listEl.innerHTML = `<div class="skl-empty" style="color:var(--red)">加载失败:${escHtml(e.message)}</div>`;
|
|
5700
6226
|
}
|
|
6227
|
+
sklCheckSubsQuietly();
|
|
5701
6228
|
}
|
|
5702
6229
|
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
document.getElementById('skill-close-btn').style.display = 'none';
|
|
5708
|
-
document.getElementById('skill-sheet-title').textContent = name ? `编辑 /${name}` : '新建技能';
|
|
5709
|
-
document.getElementById('skill-name').value = name || '';
|
|
5710
|
-
document.getElementById('skill-name').disabled = !!name;
|
|
5711
|
-
document.getElementById('skill-scope').value = scope || 'user';
|
|
5712
|
-
document.getElementById('skill-content').value = content || '# 技能标题\n\n在此输入技能指令...\n';
|
|
6230
|
+
// ── 编辑 / 手写 ──
|
|
6231
|
+
const SKL_TPL = {
|
|
6232
|
+
command: '# 技能标题(一句话说明做什么)\n\n## 目标\n\n## 步骤\n1. \n2. \n\n## 注意事项\n\n(运行时用户补充的内容会替换下面的 $ARGUMENTS)\n$ARGUMENTS\n',
|
|
6233
|
+
skill: '---\nname: {{name}}\ndescription: 什么时候该用这个技能(写清楚场景,AI 据此判断要不要用)\n---\n\n# 技能标题\n\n## 步骤\n1. \n2. \n\n## 注意事项\n',
|
|
5713
6234
|
};
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
6235
|
+
window.sklKindChanged = function() {
|
|
6236
|
+
const kind = document.getElementById('skill-kind').value;
|
|
6237
|
+
const ta = document.getElementById('skill-content');
|
|
6238
|
+
if (!_editingSkill && (!ta.value.trim() || Object.values(SKL_TPL).some(t => ta.value === t.replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill')))) {
|
|
6239
|
+
ta.value = SKL_TPL[kind].replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill');
|
|
6240
|
+
}
|
|
6241
|
+
document.getElementById('skill-edit-hint').innerHTML = (kind === 'skill'
|
|
6242
|
+
? '自动技能:名字只能用<b>小写字母、数字和连字符</b>(如 weekly-report);开头 <code>description</code> 写清楚<b>什么时候用</b>,AI 遇到这类场景会自己调用。辅助脚本等其他文件放在同目录(可让 AI 帮你加)。'
|
|
6243
|
+
: '手动技能:名字用英文、数字、- _ .(a:b 可分组);第一行标题会显示成说明;<code>$ARGUMENTS</code> 会换成运行时你补充的内容。') + '<div id="skl-name-fix"></div>';
|
|
6244
|
+
sklNameCheck();
|
|
5722
6245
|
};
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
document.getElementById('
|
|
5727
|
-
|
|
5728
|
-
document.getElementById('skill-
|
|
5729
|
-
document.getElementById('skill-
|
|
5730
|
-
|
|
5731
|
-
|
|
6246
|
+
// 自动技能名字不合规:实时提示并给出改好的名字(一点就用)
|
|
6247
|
+
const sklSlug = (n) => (String(n || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').replace(/anthropic|claude/g, 'ai').slice(0, 64).replace(/-+$/, '') || 'my-skill');
|
|
6248
|
+
window.sklNameCheck = function() {
|
|
6249
|
+
const box = document.getElementById('skl-name-fix');
|
|
6250
|
+
if (!box) return;
|
|
6251
|
+
const n = document.getElementById('skill-name').value.trim();
|
|
6252
|
+
const kind = document.getElementById('skill-kind').value;
|
|
6253
|
+
const bad = kind === 'skill' && n && (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(n) || n.length > 64 || /anthropic|claude/.test(n));
|
|
6254
|
+
box.innerHTML = bad ? `<span style="color:var(--yellow)">⚠️ 这个名字 Claude 可能认不出来,</span><a style="color:var(--blue2);cursor:pointer" onclick="document.getElementById('skill-name').value='${sklSlug(n)}';sklNameCheck()">改成 ${sklSlug(n)}</a>` : '';
|
|
6255
|
+
};
|
|
6256
|
+
document.getElementById('skill-name')?.addEventListener('input', () => sklNameCheck());
|
|
6257
|
+
window.showSkillEditor = function(name, scope, content, kind) {
|
|
6258
|
+
_editingSkill = name ? { name, source: scope, kind: kind || 'command' } : null;
|
|
6259
|
+
sklView('edit');
|
|
6260
|
+
document.getElementById('skill-sheet-title').textContent = name ? `编辑 ${kind === 'skill' ? '🧠 ' : '/'}${name}` : '手写新技能';
|
|
6261
|
+
const n = document.getElementById('skill-name'); n.value = name || ''; n.disabled = !!name;
|
|
6262
|
+
document.getElementById('skill-kind').value = kind || 'command';
|
|
6263
|
+
document.getElementById('skill-kind').disabled = !!name;
|
|
6264
|
+
document.getElementById('skill-scope').value = scope || 'project';
|
|
6265
|
+
document.getElementById('skill-scope').disabled = !!name;
|
|
6266
|
+
document.getElementById('skill-content').value = content || '';
|
|
6267
|
+
sklKindChanged();
|
|
6268
|
+
};
|
|
6269
|
+
window.editSkill = async function(name, source, kind) {
|
|
6270
|
+
try {
|
|
6271
|
+
const d = await sklJson(`/api/skills/content?sessionId=${SESSION_ID}&name=${encodeURIComponent(name)}&source=${source}&kind=${kind}`);
|
|
6272
|
+
showSkillEditor(name, source, d.content || '', kind);
|
|
6273
|
+
} catch (e) { addStatusMessage('打开失败:' + e.message); }
|
|
5732
6274
|
};
|
|
5733
|
-
|
|
5734
6275
|
window.saveSkill = async function() {
|
|
5735
6276
|
const name = document.getElementById('skill-name').value.trim();
|
|
5736
6277
|
const scope = document.getElementById('skill-scope').value;
|
|
6278
|
+
const kind = document.getElementById('skill-kind').value;
|
|
5737
6279
|
const content = document.getElementById('skill-content').value;
|
|
5738
6280
|
if (!name) { addStatusMessage('请输入技能名称'); return; }
|
|
5739
6281
|
if (!content.trim()) { addStatusMessage('请输入技能内容'); return; }
|
|
5740
6282
|
try {
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
6283
|
+
await sklPost(`/api/skills?sessionId=${SESSION_ID}`, { name, scope, content, kind });
|
|
6284
|
+
addStatusMessage(`技能 ${kind === 'skill' ? '🧠 ' : '/'}${name} 已保存`);
|
|
6285
|
+
sklView('list'); loadSkills();
|
|
6286
|
+
} catch (e) { addStatusMessage('保存失败:' + e.message); }
|
|
6287
|
+
};
|
|
6288
|
+
window.deleteSkill = async function(name, source, kind) {
|
|
6289
|
+
if (!confirm(`确定删除技能「${name}」?`)) return;
|
|
6290
|
+
try {
|
|
6291
|
+
await sklJson(`/api/skills?sessionId=${SESSION_ID}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, scope: source, kind }) });
|
|
6292
|
+
addStatusMessage(`技能「${name}」已删除`);
|
|
6293
|
+
refreshSkillList(); loadSkills();
|
|
6294
|
+
} catch (e) { addStatusMessage('删除失败:' + e.message); }
|
|
6295
|
+
};
|
|
6296
|
+
|
|
6297
|
+
// ── ✨ 从本次任务生成:交给当前任务的 AI(它最清楚刚才怎么做成的),先出草稿、确认后才写文件 ──
|
|
6298
|
+
window.genSkillFromTask = function() {
|
|
6299
|
+
if (currentSession?.mode !== 'agent') return addStatusMessage('只有 AI 任务里可以生成技能');
|
|
6300
|
+
if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成技能');
|
|
6301
|
+
const hint = document.getElementById('skl-gen-hint').value.trim();
|
|
6302
|
+
const kind = document.querySelector('input[name=skl-gen-kind]:checked').value;
|
|
6303
|
+
const scope = document.querySelector('input[name=skl-gen-scope]:checked').value;
|
|
6304
|
+
const dirs = _sklDirs?.[scope];
|
|
6305
|
+
const dir = dirs ? (kind === 'skill' ? dirs.skill : dirs.command) : (scope === 'project' ? (kind === 'skill' ? '.claude/skills' : '.claude/commands') : (kind === 'skill' ? '~/.claude/skills' : '~/.claude/commands'));
|
|
6306
|
+
const fmt = kind === 'skill'
|
|
6307
|
+
? `类型:Agent 技能(AI 遇到合适场景会自己调用)。在 ${dir}/<名称>/ 下建 SKILL.md:开头 YAML frontmatter 写 name(同目录名)和 description(用中文写清楚「什么场景该用这个技能」,AI 靠它判断);正文写目标、分步骤做法、注意事项、完成标准。需要辅助脚本就放在同一目录,并在 SKILL.md 里写明怎么用。`
|
|
6308
|
+
: `类型:斜杠命令(用户点按钮或输入 /名称 运行)。文件 ${dir}/<名称>.md:第一行是「# 中文标题」(myhi 拿它当说明),正文写目标、前提、分步骤做法、注意事项、完成标准;每次运行需要用户补充的内容用 $ARGUMENTS 表示。`;
|
|
6309
|
+
const prompt = [
|
|
6310
|
+
`请把本次会话里我们完成的工作提炼成一个可以复用的 Claude Code 技能${hint ? ',我的想法:' + hint : '(你判断最值得沉淀的那套做法)'}。`,
|
|
6311
|
+
fmt,
|
|
6312
|
+
kind === 'skill' ? '名称只能用小写字母、数字和连字符(如 deploy-test,不超过 64 个字符,不能含 anthropic / claude),目录名和 SKILL.md 里的 name 要一致。' : '名称用小写英文和连字符(如 deploy-test)。',
|
|
6313
|
+
'把本项目特有的路径、名字、账号改成参数或说明,让它在别的项目、别的电脑上也能用;不要写入任何密码、token、密钥或客户的真实敏感信息。',
|
|
6314
|
+
'先把完整草稿(含名称和每个文件的内容)贴出来问我是否保存(可以直接保存 / 我要修改);我确认之后再写入文件。在我确认之前不要创建或修改任何文件。如果同名技能已经存在,先告诉我。',
|
|
6315
|
+
].join('\n');
|
|
6316
|
+
closeSkillSheet();
|
|
6317
|
+
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6318
|
+
const display = `✨ 把本次任务的做法生成${kind === 'skill' ? '自动' : '手动'}技能(${scope === 'project' ? '本项目' : '本机所有项目'})${hint ? ':' + hint : ''}`;
|
|
6319
|
+
addInputMessage(display);
|
|
6320
|
+
socket.emit('agent:query', { prompt, display });
|
|
6321
|
+
showThinking();
|
|
6322
|
+
_sklGenPending = scope;
|
|
6323
|
+
};
|
|
6324
|
+
let _sklGenPending = false; // 生成中(存范围):这轮结束刷新快捷栏的技能按钮,并让相关任务重新加载技能
|
|
6325
|
+
|
|
6326
|
+
// ── 📤 分享 ──
|
|
6327
|
+
window.openSkillShare = function(name, source, kind) {
|
|
6328
|
+
_sklShare = { name, source, kind, force: false };
|
|
6329
|
+
sklView('share');
|
|
6330
|
+
document.getElementById('skl-share-info').innerHTML = `<b>${kind === 'skill' ? '🧠 ' : '/'}${escHtml(name)}</b> <span class="skl-tag ${kind}">${sklKindLabel(kind)}</span><span class="skl-tag">${sklScopeLabel(source)}</span>`;
|
|
6331
|
+
document.getElementById('skl-share-warn').style.display = 'none';
|
|
6332
|
+
document.getElementById('skl-share-out').style.display = 'none';
|
|
6333
|
+
document.getElementById('skl-pub-form').style.display = 'none';
|
|
6334
|
+
};
|
|
6335
|
+
function sklShareFail(e, retry) {
|
|
6336
|
+
const w = document.getElementById('skl-share-warn');
|
|
6337
|
+
if (e.data?.secrets) {
|
|
6338
|
+
w.innerHTML = `⚠️ ${escHtml(e.message)}<div style="margin-top:0.35rem;display:flex;gap:0.4rem"><button class="dr-btn" onclick="editSkill('${sklEsc(_sklShare.name)}','${_sklShare.source}','${_sklShare.kind}')">去编辑</button><button class="dr-btn" id="skl-force-btn">确认没有敏感信息,继续</button></div>`;
|
|
6339
|
+
w.style.display = '';
|
|
6340
|
+
document.getElementById('skl-force-btn').onclick = () => { _sklShare.force = true; w.style.display = 'none'; retry(); };
|
|
6341
|
+
} else addStatusMessage('分享失败:' + e.message);
|
|
6342
|
+
}
|
|
6343
|
+
const sklShareBody = () => ({ sessionId: SESSION_ID, name: _sklShare.name, source: _sklShare.source, kind: _sklShare.kind, force: _sklShare.force });
|
|
6344
|
+
function sklOut(html) { const o = document.getElementById('skl-share-out'); o.innerHTML = html; o.style.display = ''; }
|
|
6345
|
+
async function sklCopy(text, ok) {
|
|
6346
|
+
try { await navigator.clipboard.writeText(text); addStatusMessage(ok); }
|
|
6347
|
+
catch { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); addStatusMessage(ok); } catch {} ta.remove(); }
|
|
6348
|
+
}
|
|
6349
|
+
window.sklShareDownload = async function() {
|
|
6350
|
+
try {
|
|
6351
|
+
const d = await sklPost('/api/skills/export', sklShareBody());
|
|
6352
|
+
const blob = new Blob([JSON.stringify(d.pkg, null, 2)], { type: 'application/json' });
|
|
6353
|
+
const a = document.createElement('a');
|
|
6354
|
+
a.href = URL.createObjectURL(blob); a.download = d.pkg.name.replace(/:/g, '-') + '.myhiskill';
|
|
6355
|
+
document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
|
|
6356
|
+
sklOut('✓ 已下载 <b>' + escHtml(a.download) + '</b>。发给对方后,在对方 myhi 的「技能 → 📥 导入」里选择这个文件。');
|
|
6357
|
+
} catch (e) { sklShareFail(e, sklShareDownload); }
|
|
6358
|
+
};
|
|
6359
|
+
window.sklShareCode = async function() {
|
|
6360
|
+
try {
|
|
6361
|
+
const d = await sklPost('/api/skills/export', sklShareBody());
|
|
6362
|
+
await sklCopy(d.code, '分享码已复制');
|
|
6363
|
+
sklOut(`✓ 分享码已复制(${d.code.length} 字符)。对方在「技能 → 📥 导入」里粘贴即可。<textarea class="slash-inp" rows="3" readonly style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.7rem" onclick="this.select()">${escHtml(d.code)}</textarea>`);
|
|
6364
|
+
} catch (e) { sklShareFail(e, sklShareCode); }
|
|
6365
|
+
};
|
|
6366
|
+
window.sklShareLink = async function() {
|
|
6367
|
+
try {
|
|
6368
|
+
const d = await sklPost('/api/skills/share-link', sklShareBody());
|
|
6369
|
+
await sklCopy(d.url, '分享链接已复制');
|
|
6370
|
+
sklOut(`✓ 链接已复制,<b>${new Date(d.expires).toLocaleString()}</b> 前有效。对方在「📥 导入」里粘贴(两台 myhi 要能互相访问):<input class="slash-inp" readonly value="${sklEsc(d.url)}" style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.72rem" onclick="this.select()">`);
|
|
6371
|
+
} catch (e) { sklShareFail(e, sklShareLink); }
|
|
6372
|
+
};
|
|
6373
|
+
window.sklSharePublishForm = function() {
|
|
6374
|
+
const f = document.getElementById('skl-pub-form');
|
|
6375
|
+
f.style.display = f.style.display === 'none' ? '' : 'none';
|
|
6376
|
+
const last = JSON.parse(localStorage.getItem('myhi_skl_pub') || 'null') || (_sklSubs[0] ? { url: _sklSubs[0].url, username: _sklSubs[0].username, branch: _sklSubs[0].branch } : {});
|
|
6377
|
+
if (!document.getElementById('skl-pub-url').value) {
|
|
6378
|
+
document.getElementById('skl-pub-url').value = last.url || '';
|
|
6379
|
+
document.getElementById('skl-pub-user').value = last.username || '';
|
|
6380
|
+
document.getElementById('skl-pub-branch').value = last.branch || '';
|
|
5753
6381
|
}
|
|
5754
6382
|
};
|
|
6383
|
+
window.sklPublish = async function() {
|
|
6384
|
+
const url = document.getElementById('skl-pub-url').value.trim();
|
|
6385
|
+
if (!url) { addStatusMessage('请填写团队仓库地址'); return; }
|
|
6386
|
+
const body = { ...sklShareBody(), url, username: document.getElementById('skl-pub-user').value.trim(), password: document.getElementById('skl-pub-pass').value, branch: document.getElementById('skl-pub-branch').value.trim() };
|
|
6387
|
+
sklOut('发布中…(克隆仓库、提交、推送)');
|
|
6388
|
+
try {
|
|
6389
|
+
const d = await sklPost('/api/skills/publish', body);
|
|
6390
|
+
localStorage.setItem('myhi_skl_pub', JSON.stringify({ url, username: body.username, branch: body.branch }));
|
|
6391
|
+
sklOut(d.unchanged ? '仓库里已经是这个版本,不用重复发布。' : '✓ 已发布到团队仓库。订阅了这个仓库的 myhi「检查更新」时会看到。');
|
|
6392
|
+
} catch (e) { document.getElementById('skl-share-out').style.display = 'none'; sklShareFail(e, sklPublish); }
|
|
6393
|
+
};
|
|
5755
6394
|
|
|
5756
|
-
|
|
5757
|
-
|
|
6395
|
+
// ── 📥 导入:分享码 / 分享链接 / 文件 → 预览 + 安全扫描 → 选范围安装 ──
|
|
6396
|
+
window.sklImportFile = async function(file) {
|
|
6397
|
+
if (!file) return;
|
|
6398
|
+
const text = await file.text();
|
|
6399
|
+
let body;
|
|
6400
|
+
if (/\.md$/i.test(file.name)) {
|
|
6401
|
+
// 直接给了一个 .md:按手动技能处理,名称取文件名
|
|
6402
|
+
const name = file.name.replace(/\.md$/i, '').replace(/[^\w.-]+/g, '-');
|
|
6403
|
+
body = { pkg: { myhiSkill: 1, kind: 'command', name, files: [{ path: name + '.md', content: text }] } };
|
|
6404
|
+
} else if (text.trim().startsWith('MYHI-SKILL:')) body = { code: text };
|
|
6405
|
+
else body = { pkg: text };
|
|
6406
|
+
sklImportPreview(body);
|
|
6407
|
+
};
|
|
6408
|
+
window.sklImportPreview = async function(body) {
|
|
6409
|
+
const box = document.getElementById('skl-imp-preview');
|
|
6410
|
+
if (!body) {
|
|
6411
|
+
const t = document.getElementById('skl-imp-text').value.trim();
|
|
6412
|
+
if (!t) { addStatusMessage('先粘贴分享码 / 分享链接,或选择技能文件'); return; }
|
|
6413
|
+
body = /^https?:\/\/\S+\/s\/skill\/[\w-]+/.test(t) ? { url: t.match(/https?:\/\/\S+\/s\/skill\/[\w-]+/)[0] } : { code: t };
|
|
6414
|
+
}
|
|
6415
|
+
box.innerHTML = '<div class="skl-empty">读取中…</div>';
|
|
5758
6416
|
try {
|
|
5759
|
-
const
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
6417
|
+
const d = await sklPost('/api/skills/preview', { sessionId: SESSION_ID, ...body });
|
|
6418
|
+
_sklImp = d;
|
|
6419
|
+
const p = d.pkg;
|
|
6420
|
+
const danger = d.findings.filter(f => f.level === 'danger'), warn = d.findings.filter(f => f.level === 'warn');
|
|
6421
|
+
box.innerHTML = `
|
|
6422
|
+
<div class="skl-card">
|
|
6423
|
+
<div><b>${p.kind === 'skill' ? '🧠 ' : '/'}${escHtml(p.name)}</b> <span class="skl-tag ${p.kind}">${sklKindLabel(p.kind)}</span>${p.from ? `<span class="skl-tag">来自 ${escHtml(p.from)}</span>` : ''}</div>
|
|
6424
|
+
<div class="skl-desc" style="margin-top:0.2rem">${escHtml(p.desc || '(没有说明)')}</div>
|
|
6425
|
+
${p.renamedFrom ? `<div class="skl-hint" style="color:var(--yellow)">原名「${escHtml(p.renamedFrom)}」不符合自动技能的命名规范,安装时改成「${escHtml(p.name)}」</div>` : ''}
|
|
6426
|
+
</div>
|
|
6427
|
+
${sklFindingsHtml(d.findings)}
|
|
6428
|
+
<details class="skl-files"><summary>查看全部内容(${p.files.length} 个文件)——安装前建议过一遍</summary>
|
|
6429
|
+
${p.files.map(f => `<div class="skl-fname">${escHtml(f.path)}</div><pre>${escHtml(f.content)}</pre>`).join('')}
|
|
6430
|
+
</details>
|
|
6431
|
+
<div class="skl-row" style="margin-top:0.5rem">
|
|
6432
|
+
<select id="skl-imp-scope" class="slash-inp skl-sel" onchange="sklImpExists()">
|
|
6433
|
+
<option value="project">只装到本项目</option>
|
|
6434
|
+
<option value="user">装到本机所有项目</option>
|
|
6435
|
+
</select>
|
|
6436
|
+
</div>
|
|
6437
|
+
<div id="skl-imp-exists" class="skl-hint"></div>
|
|
6438
|
+
${danger.length ? '<label class="skl-confirm"><input type="checkbox" id="skl-imp-ack"> 我已看过上面标红的内容,确认这个技能来源可信</label>' : ''}
|
|
6439
|
+
<button class="action-sheet-cancel" style="background:${danger.length ? 'var(--red)' : 'var(--green)'};color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklInstall()">${danger.length ? '⚠️ 仍然安装' : '安装'}</button>`;
|
|
6440
|
+
// 带脚本的技能默认只装本项目
|
|
6441
|
+
document.getElementById('skl-imp-scope').value = p.kind === 'skill' && p.files.length > 1 ? 'project' : (warn.length || danger.length ? 'project' : 'user');
|
|
6442
|
+
sklImpExists();
|
|
5769
6443
|
} catch (e) {
|
|
5770
|
-
|
|
6444
|
+
box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`;
|
|
5771
6445
|
}
|
|
5772
6446
|
};
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
window.
|
|
5778
|
-
|
|
5779
|
-
document.getElementById('
|
|
5780
|
-
document.getElementById('
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
document.getElementById('
|
|
5785
|
-
|
|
5786
|
-
document.getElementById('
|
|
5787
|
-
|
|
5788
|
-
|
|
6447
|
+
function sklFindingsHtml(findings) {
|
|
6448
|
+
if (!findings?.length) return '<div class="skl-ok">✓ 安全检查没发现可疑写法</div>';
|
|
6449
|
+
return '<div class="skl-findings">' + findings.map(f => `<div class="skl-f ${f.level}">${f.level === 'danger' ? '⛔' : '⚠️'} ${escHtml(f.why)}${f.file ? ` <small>${escHtml(f.file)}${f.line ? ' 第 ' + f.line + ' 行' : ''}</small>` : ''}${f.text ? `<code>${escHtml(f.text)}</code>` : ''}</div>`).join('') + '</div>';
|
|
6450
|
+
}
|
|
6451
|
+
window.sklImpExists = function() {
|
|
6452
|
+
if (!_sklImp) return;
|
|
6453
|
+
const sc = document.getElementById('skl-imp-scope').value;
|
|
6454
|
+
document.getElementById('skl-imp-exists').textContent = _sklImp.exists?.[sc] ? `⚠️ ${sc === 'project' ? '本项目' : '本机'}已经有同名技能,安装会覆盖它` : '';
|
|
6455
|
+
};
|
|
6456
|
+
window.sklInstall = async function() {
|
|
6457
|
+
if (!_sklImp) return;
|
|
6458
|
+
const ack = document.getElementById('skl-imp-ack');
|
|
6459
|
+
if (ack && !ack.checked) { addStatusMessage('这个技能里有标红的可疑内容:确认来源可信后先勾选确认'); return; }
|
|
6460
|
+
const scope = document.getElementById('skl-imp-scope').value;
|
|
6461
|
+
try {
|
|
6462
|
+
await sklPost('/api/skills/install', { sessionId: SESSION_ID, pkg: _sklImp.pkg, scope });
|
|
6463
|
+
addStatusMessage(`已安装技能「${_sklImp.pkg.name}」(${scope === 'project' ? '本项目' : '本机所有项目'}),下一轮 AI 就能用`);
|
|
6464
|
+
_sklImp = null; document.getElementById('skl-imp-text').value = '';
|
|
6465
|
+
sklView('list'); loadSkills();
|
|
6466
|
+
} catch (e) { addStatusMessage('安装失败:' + e.message); }
|
|
5789
6467
|
};
|
|
5790
6468
|
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
6469
|
+
// ── 👥 团队仓库:订阅列表 / 检查更新 / 扫描安装 ──
|
|
6470
|
+
async function loadSubs() { try { _sklSubs = (await sklJson('/api/skills/subs')).subs || []; } catch { _sklSubs = []; } updateSubBadge(); return _sklSubs; }
|
|
6471
|
+
function updateSubBadge() {
|
|
6472
|
+
const n = _sklSubs.reduce((a, s) => a + (s.updates?.length || 0), 0);
|
|
6473
|
+
const b = document.getElementById('skl-upd-badge'), ban = document.getElementById('skl-upd-banner');
|
|
6474
|
+
if (b) { b.textContent = n; b.style.display = n ? '' : 'none'; }
|
|
6475
|
+
if (ban) { ban.textContent = `👥 团队仓库有 ${n} 个技能更新 / 新技能 · 点这里查看`; ban.style.display = n ? '' : 'none'; }
|
|
6476
|
+
const skBtn = document.querySelector('#sk-claude-agent .sk[data-sk="skills"]');
|
|
6477
|
+
if (skBtn) skBtn.classList.toggle('skl-dot', n > 0);
|
|
6478
|
+
}
|
|
6479
|
+
let _sklAutoChecked = false;
|
|
6480
|
+
async function sklCheckSubsQuietly() {
|
|
6481
|
+
await loadSubs();
|
|
6482
|
+
if (_sklAutoChecked) return;
|
|
6483
|
+
_sklAutoChecked = true;
|
|
6484
|
+
for (const s of _sklSubs) {
|
|
6485
|
+
if (Date.now() - (s.lastCheck || 0) < 6 * 3600 * 1000) continue; // 6 小时内查过就不再查
|
|
6486
|
+
try { await sklPost(`/api/skills/subs/${s.id}/check`); } catch {}
|
|
6487
|
+
}
|
|
6488
|
+
await loadSubs();
|
|
6489
|
+
}
|
|
6490
|
+
const maskUrl = (u) => String(u || '').replace(/\/\/[^@/]+@/, '//');
|
|
6491
|
+
async function renderSubs() {
|
|
6492
|
+
const box = document.getElementById('skl-subs');
|
|
6493
|
+
await loadSubs();
|
|
6494
|
+
box.innerHTML = _sklSubs.length ? '<div class="skl-lbl">已订阅</div>' + _sklSubs.map(s => `
|
|
6495
|
+
<div class="skl-card skl-sub">
|
|
6496
|
+
<div style="display:flex;align-items:center;gap:0.4rem">
|
|
6497
|
+
<div style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${sklEsc(maskUrl(s.url))}">${escHtml(maskUrl(s.url).replace(/^https?:\/\//, ''))}</div>
|
|
6498
|
+
<span class="skl-tag">${s.scope === 'project' ? '本项目' : '本机'}</span>
|
|
6499
|
+
</div>
|
|
6500
|
+
<div class="skl-desc">已装 ${Object.keys(s.installed || {}).length} 个 · ${s.lastCheck ? '上次检查 ' + new Date(s.lastCheck).toLocaleString() : '还没检查过'}${s.updates?.length ? ` · <b style="color:var(--green)">${s.updates.length} 个更新</b>` : ''}</div>
|
|
6501
|
+
<div class="skl-row" style="margin-top:0.35rem">
|
|
6502
|
+
<button class="dr-btn take" onclick="sklSubCheck('${s.id}')">检查更新</button>
|
|
6503
|
+
<button class="dr-btn" style="margin-left:auto" onclick="sklSubRemove('${s.id}')">取消订阅</button>
|
|
6504
|
+
</div>
|
|
6505
|
+
<div id="skl-sub-out-${s.id}"></div>
|
|
6506
|
+
</div>`).join('') : '<div class="skl-hint">还没有订阅团队仓库。团队把技能放在一个 Git 仓库里(commands/、skills/ 目录),大家订阅后就能一起用、收到更新;也可以在技能的「📤 分享」里发布到仓库。</div>';
|
|
6507
|
+
}
|
|
6508
|
+
function sklPackListHtml(list, withStatus) {
|
|
6509
|
+
return list.map((s, i) => `
|
|
6510
|
+
<label class="skl-pick">
|
|
6511
|
+
<input type="checkbox" ${s.findings?.some(f => f.level === 'danger') ? '' : 'checked'} data-key="${sklEsc(s.kind + ':' + s.name)}">
|
|
6512
|
+
<div style="min-width:0;flex:1">
|
|
6513
|
+
<div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${withStatus ? `<span class="skl-tag ${s.status === 'new' ? 'skill' : ''}">${s.status === 'new' ? '新技能' : '有更新'}</span>` : ''}</div>
|
|
6514
|
+
<div class="skl-desc">${escHtml(s.desc || '')}</div>
|
|
6515
|
+
${s.findings?.length ? sklFindingsHtml(s.findings) : ''}
|
|
6516
|
+
</div>
|
|
6517
|
+
</label>`).join('');
|
|
6518
|
+
}
|
|
6519
|
+
window.sklSubCheck = async function(id) {
|
|
6520
|
+
const out = document.getElementById('skl-sub-out-' + id);
|
|
6521
|
+
out.innerHTML = '<div class="skl-empty">检查中…</div>';
|
|
6522
|
+
try {
|
|
6523
|
+
const d = await sklPost(`/api/skills/subs/${id}/check`);
|
|
6524
|
+
await loadSubs();
|
|
6525
|
+
if (!d.updates.length) { out.innerHTML = '<div class="skl-ok">✓ 已是最新</div>'; return; }
|
|
6526
|
+
out.innerHTML = sklPackListHtml(d.updates, true) + `<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklSubUpdate('${id}')">更新 / 安装选中的</button>`;
|
|
6527
|
+
} catch (e) { out.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
|
|
6528
|
+
};
|
|
6529
|
+
window.sklSubUpdate = async function(id) {
|
|
6530
|
+
const names = [...document.querySelectorAll(`#skl-sub-out-${id} input[type=checkbox]:checked`)].map(c => c.dataset.key);
|
|
6531
|
+
if (!names.length) { addStatusMessage('请至少选一个'); return; }
|
|
6532
|
+
try {
|
|
6533
|
+
const d = await sklPost('/api/skills/repo/install', { sessionId: SESSION_ID, subId: id, names });
|
|
6534
|
+
addStatusMessage(`已更新 ${d.installed.length} 个技能`);
|
|
6535
|
+
loadSkills(); renderSubs();
|
|
6536
|
+
} catch (e) { addStatusMessage('更新失败:' + e.message); }
|
|
6537
|
+
};
|
|
6538
|
+
window.sklSubRemove = async function(id) {
|
|
6539
|
+
if (!confirm('取消订阅这个仓库?已经装好的技能会保留。')) return;
|
|
6540
|
+
try { await sklJson(`/api/skills/subs/${id}`, { method: 'DELETE' }); } catch {}
|
|
6541
|
+
renderSubs();
|
|
6542
|
+
};
|
|
6543
|
+
document.getElementById('skill-git-url')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); scanGitSkills(); } });
|
|
5795
6544
|
window.scanGitSkills = async function() {
|
|
5796
6545
|
const url = document.getElementById('skill-git-url').value.trim();
|
|
5797
6546
|
if (!url) { addStatusMessage('请输入 Git 仓库地址'); return; }
|
|
5798
|
-
const
|
|
5799
|
-
|
|
5800
|
-
document.getElementById('skill-import-preview').style.display = 'none';
|
|
5801
|
-
document.getElementById('skill-import-install-btn').style.display = 'none';
|
|
6547
|
+
const box = document.getElementById('skill-import-preview');
|
|
6548
|
+
box.innerHTML = '<div class="skl-empty">正在克隆并扫描…</div>';
|
|
5802
6549
|
try {
|
|
5803
|
-
const
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
const data = await resp.json();
|
|
5811
|
-
statusEl.innerHTML = '';
|
|
5812
|
-
if (data.error) { addStatusMessage(data.error); return; }
|
|
5813
|
-
_gitSkills = data.skills || [];
|
|
5814
|
-
if (!_gitSkills.length) {
|
|
5815
|
-
addStatusMessage(data.message || '未找到技能文件');
|
|
5816
|
-
return;
|
|
5817
|
-
}
|
|
5818
|
-
const preview = document.getElementById('skill-import-preview');
|
|
5819
|
-
preview.innerHTML = _gitSkills.map((s, i) => `
|
|
5820
|
-
<label style="display:flex;align-items:flex-start;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--surface2);cursor:pointer">
|
|
5821
|
-
<input type="checkbox" checked data-skill-idx="${i}" style="margin-top:0.15rem">
|
|
5822
|
-
<div style="min-width:0;flex:1">
|
|
5823
|
-
<div style="color:var(--purple2);font-weight:500">/${escHtml(s.name)}</div>
|
|
5824
|
-
<div style="color:var(--muted);font-size:0.72rem">${escHtml(s.desc)}</div>
|
|
5825
|
-
</div>
|
|
5826
|
-
</label>
|
|
5827
|
-
`).join('');
|
|
5828
|
-
preview.style.display = '';
|
|
5829
|
-
document.getElementById('skill-import-install-btn').style.display = '';
|
|
5830
|
-
} catch (e) {
|
|
5831
|
-
addStatusMessage('扫描失败: ' + e.message);
|
|
5832
|
-
statusEl.innerHTML = '';
|
|
5833
|
-
}
|
|
6550
|
+
const d = await sklPost('/api/skills/repo/scan', { url, username: document.getElementById('skill-git-user').value.trim() || undefined, password: document.getElementById('skill-git-pass').value || undefined });
|
|
6551
|
+
_gitSkills = d.skills || [];
|
|
6552
|
+
if (!_gitSkills.length) { box.innerHTML = '<div class="skl-hint">这个仓库里没找到技能(commands/*.md、skills/*/SKILL.md)</div>'; return; }
|
|
6553
|
+
box.innerHTML = sklPackListHtml(_gitSkills, false) +
|
|
6554
|
+
'<label class="skl-confirm"><input type="checkbox" id="skl-git-sub" checked> 订阅这个仓库:以后有更新 / 新技能时提示我</label>' +
|
|
6555
|
+
'<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="installGitSkills()">安装选中的技能</button>';
|
|
6556
|
+
} catch (e) { box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
|
|
5834
6557
|
};
|
|
5835
|
-
|
|
5836
6558
|
window.installGitSkills = async function() {
|
|
5837
|
-
const
|
|
5838
|
-
|
|
5839
|
-
const
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
method: 'POST',
|
|
5852
|
-
headers: { 'Content-Type': 'application/json' },
|
|
5853
|
-
body: JSON.stringify({ name: s.name, scope, content: s.content }),
|
|
5854
|
-
});
|
|
5855
|
-
const data = await resp.json();
|
|
5856
|
-
if (data.error) fail++;
|
|
5857
|
-
else ok++;
|
|
5858
|
-
} catch { fail++; }
|
|
5859
|
-
}
|
|
5860
|
-
addStatusMessage(`安装完成: ${ok} 个成功${fail ? `, ${fail} 个失败` : ''}`);
|
|
5861
|
-
btn.textContent = '安装选中的技能';
|
|
5862
|
-
btn.disabled = false;
|
|
5863
|
-
backToSkillList();
|
|
5864
|
-
loadSkills();
|
|
6559
|
+
const names = [...document.querySelectorAll('#skill-import-preview input[data-key]:checked')].map(c => c.dataset.key);
|
|
6560
|
+
if (!names.length) { addStatusMessage('请至少选择一个技能'); return; }
|
|
6561
|
+
const dangerSel = _gitSkills.filter(s => names.includes(s.kind + ':' + s.name) && s.findings?.some(f => f.level === 'danger'));
|
|
6562
|
+
if (dangerSel.length && !confirm(`选中的技能里有 ${dangerSel.length} 个带标红的可疑内容(${dangerSel.map(s => s.name).join('、')})。确认来源可信、仍然安装?`)) return;
|
|
6563
|
+
try {
|
|
6564
|
+
const d = await sklPost('/api/skills/repo/install', {
|
|
6565
|
+
sessionId: SESSION_ID, names, scope: document.getElementById('skill-import-scope').value,
|
|
6566
|
+
url: document.getElementById('skill-git-url').value.trim(), username: document.getElementById('skill-git-user').value.trim() || undefined,
|
|
6567
|
+
password: document.getElementById('skill-git-pass').value || undefined, subscribe: document.getElementById('skl-git-sub')?.checked,
|
|
6568
|
+
});
|
|
6569
|
+
addStatusMessage(`已安装 ${d.installed.length} 个技能${d.sub ? ',并订阅了这个仓库' : ''}`);
|
|
6570
|
+
document.getElementById('skill-import-preview').innerHTML = '';
|
|
6571
|
+
loadSkills(); renderSubs();
|
|
6572
|
+
} catch (e) { addStatusMessage('安装失败:' + e.message); }
|
|
5865
6573
|
};
|
|
5866
6574
|
|
|
5867
|
-
// ──
|
|
6575
|
+
// ── 🛒 技能市场(社区仓库,走团队仓库的扫描安装,默认不订阅)──
|
|
5868
6576
|
const SKILL_MARKET = [
|
|
5869
|
-
{ name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+'
|
|
5870
|
-
{ name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+'
|
|
5871
|
-
{ name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+'
|
|
5872
|
-
{ name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+'
|
|
5873
|
-
{ name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+'
|
|
5874
|
-
{ name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+'
|
|
6577
|
+
{ name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+' },
|
|
6578
|
+
{ name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+' },
|
|
6579
|
+
{ name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+' },
|
|
6580
|
+
{ name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+' },
|
|
6581
|
+
{ name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+' },
|
|
6582
|
+
{ name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+' },
|
|
5875
6583
|
];
|
|
5876
|
-
|
|
5877
6584
|
window.showSkillMarket = function() {
|
|
5878
|
-
|
|
5879
|
-
document.getElementById('skill-
|
|
5880
|
-
document.getElementById('skill-import-view').style.display = 'none';
|
|
5881
|
-
document.getElementById('skill-market-view').style.display = '';
|
|
5882
|
-
document.getElementById('skill-close-btn').style.display = 'none';
|
|
5883
|
-
document.getElementById('skill-sheet-title').textContent = '技能市场';
|
|
5884
|
-
|
|
5885
|
-
const list = document.getElementById('skill-market-list');
|
|
5886
|
-
list.innerHTML = '<div style="font-size:0.72rem;color:var(--muted);padding:0.3rem 0.5rem;border-bottom:1px solid var(--surface2)">热门社区技能仓库 · 点击安装</div>' +
|
|
6585
|
+
sklView('market');
|
|
6586
|
+
document.getElementById('skill-market-list').innerHTML = '<div class="skl-hint">热门社区技能仓库 · 点一个扫描,选择要装的(安装前会做安全检查)</div>' +
|
|
5887
6587
|
SKILL_MARKET.map((m, i) => `
|
|
5888
|
-
<div class="
|
|
5889
|
-
<div
|
|
6588
|
+
<div class="skl-item" style="cursor:pointer" onclick="installFromMarket(${i})">
|
|
6589
|
+
<div class="skl-main">
|
|
5890
6590
|
<div style="font-weight:500">${escHtml(m.name)} <span style="color:var(--muted);font-size:0.7rem">★ ${m.stars}</span></div>
|
|
5891
|
-
<div class="desc">${escHtml(m.desc)}</div>
|
|
6591
|
+
<div class="skl-desc">${escHtml(m.desc)}</div>
|
|
5892
6592
|
<div style="color:var(--blue);font-size:0.68rem">${escHtml(m.repo)}</div>
|
|
5893
6593
|
</div>
|
|
5894
|
-
</div
|
|
5895
|
-
|
|
5896
|
-
'<div style="padding:0.6rem 0.5rem;font-size:0.72rem;color:var(--muted);border-top:1px solid var(--surface2)">' +
|
|
5897
|
-
'更多: <a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a>' +
|
|
5898
|
-
' · <a href="https://skillsmp.com" target="_blank" style="color:var(--blue)">skillsmp.com</a>' +
|
|
5899
|
-
' · <a href="https://www.skillhub.club" target="_blank" style="color:var(--blue)">skillhub.club</a></div>';
|
|
6594
|
+
</div>`).join('') +
|
|
6595
|
+
'<div class="skl-hint" style="margin-top:0.5rem">更多:<a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a></div>';
|
|
5900
6596
|
};
|
|
5901
|
-
|
|
5902
6597
|
window.installFromMarket = async function(idx) {
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
// 复用 Git 安装流程
|
|
5906
|
-
showSkillImport();
|
|
5907
|
-
document.getElementById('skill-git-url').value = url;
|
|
5908
|
-
// 自动触发扫描
|
|
6598
|
+
sklView('team');
|
|
6599
|
+
document.getElementById('skill-git-url').value = `https://github.com/${SKILL_MARKET[idx].repo}.git`;
|
|
5909
6600
|
await scanGitSkills();
|
|
6601
|
+
const sub = document.getElementById('skl-git-sub'); if (sub) sub.checked = false;
|
|
5910
6602
|
};
|
|
5911
6603
|
|
|
5912
6604
|
// ── Git 提交推送 ──────────────────────────────
|
|
@@ -6129,10 +6821,189 @@
|
|
|
6129
6821
|
].join('');
|
|
6130
6822
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6131
6823
|
addInputMessage('📘 让 AI 生成项目说明');
|
|
6132
|
-
socket.emit('agent:query', { prompt });
|
|
6824
|
+
socket.emit('agent:query', { prompt, display: '📘 让 AI 生成项目说明' });
|
|
6133
6825
|
showThinking();
|
|
6134
6826
|
};
|
|
6135
6827
|
|
|
6828
|
+
// ── 💡 问一问 ─────────────────────────────────────
|
|
6829
|
+
// 在聊天里选中一段文字 → 浮出「解释 / 问一问 / 复制」;回答在旁边小窗里流式出现,可以继续追问。
|
|
6830
|
+
// 服务端为每次提问起一个一次性的 claude(不带工具、不落会话),正在干活的 AI 看不到这些问答;
|
|
6831
|
+
// 想让它知道就点「👉 告诉 AI」,把要点放进输入框由用户确认后发送。
|
|
6832
|
+
const askPop = document.getElementById('ask-pop');
|
|
6833
|
+
const askPanel = document.getElementById('ask-panel');
|
|
6834
|
+
const askBody = document.getElementById('ask-body');
|
|
6835
|
+
const askInput = document.getElementById('ask-input');
|
|
6836
|
+
const askSendBtn = document.getElementById('ask-send');
|
|
6837
|
+
const _askTouch = window.matchMedia('(hover: none)');
|
|
6838
|
+
if (_askTouch.matches) askInput.placeholder = '继续问…';
|
|
6839
|
+
let _askSel = null; // 当前选区 { text, msgEl }
|
|
6840
|
+
let _askThread = null; // { selection, context, userAsk, items: [{ q, a, err, done }] }
|
|
6841
|
+
let _askReq = null; // 正在等回答的 reqId
|
|
6842
|
+
const askClip = (s, n) => { s = String(s || '').trim(); return s.length > n ? s.slice(0, n) + '…' : s; };
|
|
6843
|
+
|
|
6844
|
+
function askHidePop() { askPop.classList.remove('show'); _askSel = null; }
|
|
6845
|
+
function askCheckSelection() {
|
|
6846
|
+
if (currentSession?.mode !== 'agent') return askHidePop();
|
|
6847
|
+
const sel = window.getSelection();
|
|
6848
|
+
const text = sel && !sel.isCollapsed ? sel.toString().trim() : '';
|
|
6849
|
+
if (!text || !sel.rangeCount) return askHidePop();
|
|
6850
|
+
const node = sel.anchorNode && (sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement);
|
|
6851
|
+
const msgEl = node && node.closest && node.closest('#chat-area .msg');
|
|
6852
|
+
if (!msgEl) return askHidePop();
|
|
6853
|
+
_askSel = { text, msgEl };
|
|
6854
|
+
askPop.classList.add('show');
|
|
6855
|
+
const pw = askPop.offsetWidth, ph = askPop.offsetHeight;
|
|
6856
|
+
if (_askTouch.matches) {
|
|
6857
|
+
// 手机:系统自带的选择菜单在选区上方,工具条放到输入框上方,互不遮挡
|
|
6858
|
+
const ia = document.getElementById('input-area').getBoundingClientRect();
|
|
6859
|
+
askPop.style.left = Math.max(8, (window.innerWidth - pw) / 2) + 'px';
|
|
6860
|
+
askPop.style.top = Math.max(8, ia.top - ph - 10) + 'px';
|
|
6861
|
+
} else {
|
|
6862
|
+
const r = sel.getRangeAt(0).getBoundingClientRect();
|
|
6863
|
+
let top = r.top - ph - 8; if (top < 56) top = r.bottom + 8;
|
|
6864
|
+
askPop.style.left = Math.min(window.innerWidth - pw - 8, Math.max(8, r.left + r.width / 2 - pw / 2)) + 'px';
|
|
6865
|
+
askPop.style.top = top + 'px';
|
|
6866
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
let _askSelTimer = null;
|
|
6869
|
+
document.addEventListener('selectionchange', () => { clearTimeout(_askSelTimer); _askSelTimer = setTimeout(askCheckSelection, 250); });
|
|
6870
|
+
chatArea.addEventListener('scroll', () => { if (askPop.classList.contains('show')) askCheckSelection(); }, { passive: true });
|
|
6871
|
+
|
|
6872
|
+
// 选区所在消息的上下文:消息原文(太长就截选区附近)+ 用户在它之前说的那句需求
|
|
6873
|
+
function askContextOf(msgEl, selText) {
|
|
6874
|
+
let ctx = (msgEl.innerText || '').trim();
|
|
6875
|
+
if (ctx.length > 3000) {
|
|
6876
|
+
const i = Math.max(0, ctx.indexOf(selText.slice(0, 50)));
|
|
6877
|
+
ctx = ctx.slice(Math.max(0, i - 1500), i + 1500);
|
|
6878
|
+
}
|
|
6879
|
+
let userAsk = '';
|
|
6880
|
+
for (let el = msgEl.previousElementSibling; el; el = el.previousElementSibling) {
|
|
6881
|
+
if (el.classList.contains('msg-input')) { userAsk = (el.innerText || '').trim(); break; }
|
|
6882
|
+
}
|
|
6883
|
+
return { context: ctx, userAsk };
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
window.askFromSelection = function(custom) {
|
|
6887
|
+
if (!_askSel) return;
|
|
6888
|
+
const { text, msgEl } = _askSel;
|
|
6889
|
+
const { context, userAsk } = askContextOf(msgEl, text);
|
|
6890
|
+
askHidePop();
|
|
6891
|
+
try { window.getSelection().removeAllRanges(); } catch {}
|
|
6892
|
+
askCancel();
|
|
6893
|
+
_askThread = { selection: text, context, userAsk, items: [] };
|
|
6894
|
+
openAsk();
|
|
6895
|
+
if (custom) { renderAsk(); askInput.focus(); }
|
|
6896
|
+
else askSend('');
|
|
6897
|
+
};
|
|
6898
|
+
window.askCopySelection = function() {
|
|
6899
|
+
if (!_askSel) return;
|
|
6900
|
+
const t = _askSel.text;
|
|
6901
|
+
(navigator.clipboard?.writeText(t) || Promise.reject()).then(() => addStatusMessage('已复制'), () => {
|
|
6902
|
+
const ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta); ta.select();
|
|
6903
|
+
try { document.execCommand('copy'); addStatusMessage('已复制'); } catch {} ta.remove();
|
|
6904
|
+
});
|
|
6905
|
+
askHidePop();
|
|
6906
|
+
};
|
|
6907
|
+
|
|
6908
|
+
function openAsk() {
|
|
6909
|
+
askPanel.classList.add('open');
|
|
6910
|
+
document.getElementById('ask-backdrop').classList.toggle('show', window.innerWidth <= 768);
|
|
6911
|
+
}
|
|
6912
|
+
window.closeAsk = function() {
|
|
6913
|
+
askPanel.classList.remove('open');
|
|
6914
|
+
document.getElementById('ask-backdrop').classList.remove('show');
|
|
6915
|
+
};
|
|
6916
|
+
function askCancel() {
|
|
6917
|
+
if (_askReq) { socket.emit('aside:cancel'); _askReq = null; }
|
|
6918
|
+
if (_askThread) _askThread.items.forEach(it => { if (!it.done) { it.done = true; if (!it.a) it.err = '已停止'; } });
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
function renderAsk() {
|
|
6922
|
+
const t = _askThread;
|
|
6923
|
+
if (!t) { askBody.innerHTML = ''; return; }
|
|
6924
|
+
let html = `<div class="ask-quote">${escHtml(askClip(t.selection, 600))}</div>`;
|
|
6925
|
+
t.items.forEach((it, i) => {
|
|
6926
|
+
html += `<div class="ask-q"><span>${escHtml(it.q || '解释一下这段是什么意思')}</span></div>`;
|
|
6927
|
+
if (it.err && !it.a) html += `<div class="ask-a err">⚠️ ${escHtml(it.err)}</div>`;
|
|
6928
|
+
else html += `<div class="ask-a md" data-i="${i}">${it.a ? renderMarkdown(it.a) : '<span style="color:var(--muted)">思考中…</span>'}</div>`;
|
|
6929
|
+
if (it.err && it.a) html += `<div class="ask-a err">⚠️ ${escHtml(it.err)}</div>`;
|
|
6930
|
+
if (it.done && it.a) {
|
|
6931
|
+
html += `<div class="ask-acts">${canTakeControl() ? `<button class="pri" onclick="askTellAI(${i})" title="把这段问答的要点放进输入框,你确认后再发给正在干活的 AI">👉 告诉 AI</button>` : ''}`
|
|
6932
|
+
+ `<button onclick="askCopyAnswer(${i})">📋 复制</button></div>`;
|
|
6933
|
+
}
|
|
6934
|
+
});
|
|
6935
|
+
if (!t.items.length) html += '<div style="color:var(--muted);font-size:0.78rem">在下面输入你想问的,比如「这是什么意思?」「为什么要这样做?」「和 XX 有什么区别?」</div>';
|
|
6936
|
+
askBody.innerHTML = html;
|
|
6937
|
+
askBody.scrollTop = askBody.scrollHeight;
|
|
6938
|
+
const busy = !!_askReq;
|
|
6939
|
+
askSendBtn.textContent = busy ? '停止' : '发送';
|
|
6940
|
+
askSendBtn.classList.toggle('stop', busy);
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6943
|
+
function askSend(q) {
|
|
6944
|
+
const t = _askThread;
|
|
6945
|
+
if (!t) return;
|
|
6946
|
+
const history = t.items.filter(it => it.done && it.a).map(it => ({ q: it.q || '解释一下这段是什么意思', a: it.a }));
|
|
6947
|
+
const it = { q, a: '', err: '', done: false };
|
|
6948
|
+
t.items.push(it);
|
|
6949
|
+
_askReq = 'a' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
6950
|
+
it.reqId = _askReq;
|
|
6951
|
+
socket.emit('aside:ask', { reqId: _askReq, selection: t.selection, context: t.context, userAsk: t.userAsk, question: q, history });
|
|
6952
|
+
renderAsk();
|
|
6953
|
+
}
|
|
6954
|
+
window.askSendOrStop = function() {
|
|
6955
|
+
if (_askReq) { askCancel(); renderAsk(); return; }
|
|
6956
|
+
const q = askInput.value.trim();
|
|
6957
|
+
if (!q) { askInput.focus(); return; }
|
|
6958
|
+
if (!_askThread) _askThread = { selection: '', context: '', userAsk: '', items: [] };
|
|
6959
|
+
askInput.value = ''; askInput.style.height = '';
|
|
6960
|
+
askSend(q);
|
|
6961
|
+
};
|
|
6962
|
+
askInput.addEventListener('keydown', (e) => {
|
|
6963
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !_askTouch.matches) { e.preventDefault(); askSendOrStop(); }
|
|
6964
|
+
else if (e.key === 'Escape') { e.preventDefault(); closeAsk(); }
|
|
6965
|
+
});
|
|
6966
|
+
askInput.addEventListener('input', () => { askInput.style.height = 'auto'; askInput.style.height = Math.min(askInput.scrollHeight, 150) + 'px'; });
|
|
6967
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && askPanel.classList.contains('open') && document.activeElement !== askInput) closeAsk(); });
|
|
6968
|
+
|
|
6969
|
+
const askItemOf = (reqId) => _askThread?.items.find(it => it.reqId === reqId);
|
|
6970
|
+
socket.on('aside:delta', ({ reqId, text }) => {
|
|
6971
|
+
const it = askItemOf(reqId);
|
|
6972
|
+
if (!it || it.done) return;
|
|
6973
|
+
it.a = text;
|
|
6974
|
+
const el = askBody.querySelector(`.ask-a[data-i="${_askThread.items.indexOf(it)}"]`);
|
|
6975
|
+
if (el) { const atBottom = askBody.scrollHeight - askBody.scrollTop - askBody.clientHeight < 40; el.innerHTML = renderMarkdown(text); if (atBottom) askBody.scrollTop = askBody.scrollHeight; }
|
|
6976
|
+
else renderAsk();
|
|
6977
|
+
});
|
|
6978
|
+
socket.on('aside:done', ({ reqId, text, error, partial }) => {
|
|
6979
|
+
const it = askItemOf(reqId);
|
|
6980
|
+
if (_askReq === reqId) _askReq = null;
|
|
6981
|
+
if (!it || it.done) { renderAsk(); return; }
|
|
6982
|
+
it.done = true;
|
|
6983
|
+
if (text) it.a = text;
|
|
6984
|
+
else { it.a = partial || it.a; it.err = error || '没有拿到回答,稍后再试'; }
|
|
6985
|
+
renderAsk();
|
|
6986
|
+
});
|
|
6987
|
+
|
|
6988
|
+
// 👉 告诉 AI:把问答要点放进输入框(不直接发),用户补上自己的想法后再发给干活的 AI
|
|
6989
|
+
window.askTellAI = function(i) {
|
|
6990
|
+
const t = _askThread, it = t?.items[i];
|
|
6991
|
+
if (!it) return;
|
|
6992
|
+
const firstPara = askClip(String(it.a).split(/\n\s*\n/)[0].replace(/[#*`>]/g, ''), 200);
|
|
6993
|
+
const about = t.selection ? `关于你刚才说的「${askClip(t.selection.replace(/\s+/g, ' '), 60)}」,` : '';
|
|
6994
|
+
const asked = it.q ? `我问了一下「${askClip(it.q, 60)}」,` : '';
|
|
6995
|
+
cmdInput.value = `${about}${asked}了解到:${firstPara}\n我的想法是:`;
|
|
6996
|
+
cmdInput.dispatchEvent(new Event('input'));
|
|
6997
|
+
if (window.innerWidth <= 768) closeAsk();
|
|
6998
|
+
cmdInput.focus();
|
|
6999
|
+
cmdInput.setSelectionRange(cmdInput.value.length, cmdInput.value.length);
|
|
7000
|
+
addStatusMessage('已放进输入框,补上你的想法后发送给 AI');
|
|
7001
|
+
};
|
|
7002
|
+
window.askCopyAnswer = function(i) {
|
|
7003
|
+
const a = _askThread?.items[i]?.a;
|
|
7004
|
+
if (a) navigator.clipboard?.writeText(a).then(() => addStatusMessage('已复制回答'), () => {});
|
|
7005
|
+
};
|
|
7006
|
+
|
|
6136
7007
|
window.analyzeMemory = function() {
|
|
6137
7008
|
const scopeTxt = _memScope === 'all' ? '所有项目 + 全局' : '本项目';
|
|
6138
7009
|
const prompt = [
|
|
@@ -6149,7 +7020,7 @@
|
|
|
6149
7020
|
if (currentSession?.mode === 'agent') {
|
|
6150
7021
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
6151
7022
|
addInputMessage('🤖 让 AI 过一遍记忆(找过时/重复/废弃)');
|
|
6152
|
-
socket.emit('agent:query', { prompt });
|
|
7023
|
+
socket.emit('agent:query', { prompt, display: '🤖 让 AI 过一遍记忆(找过时/重复/废弃)' });
|
|
6153
7024
|
showThinking();
|
|
6154
7025
|
} else {
|
|
6155
7026
|
addStatusMessage('该功能仅在 agent(Claude) 会话可用');
|