dsh-issue2pr 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -3
- package/client.js +466 -76
- package/index.js +63 -0
- package/lib/agents.js +204 -0
- package/lib/stages/p6-coder.js +14 -7
- package/package.json +37 -8
package/client.js
CHANGED
|
@@ -80,6 +80,10 @@ window.__ModuleLoader__.load({
|
|
|
80
80
|
--err-bg:rgba(213,70,58,.10);
|
|
81
81
|
--code-bg:#10141d; --code-line:#1d2330;
|
|
82
82
|
--mono:'JetBrains Mono','IBM Plex Mono','Cascadia Code',Consolas,'Liberation Mono',monospace;
|
|
83
|
+
--fs-xs:11.5px; --fs-sm:12.5px; --fs-md:13.5px; --fs-lg:14.5px; /* 字号 scale:AI 面板段统一走 token */
|
|
84
|
+
/* 字体必须显式声明:不声明则继承宿主→系统默认字体(Win 雅黑/Mac 苹方/Linux Noto
|
|
85
|
+
度量差异大),同一字号在不同电脑上观感"一大一小"——跨平台栈保证同类字形 */
|
|
86
|
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans CJK SC','Source Han Sans SC',sans-serif;
|
|
83
87
|
color:var(--ink); font-size:14.5px; line-height:1.7;
|
|
84
88
|
flex:1; min-height:0; display:flex; flex-direction:column;
|
|
85
89
|
}
|
|
@@ -118,7 +122,7 @@ window.__ModuleLoader__.load({
|
|
|
118
122
|
hover 显色;双击恢复默认宽 */
|
|
119
123
|
.i2p-nav-grip{position:absolute;top:0;bottom:0;right:0;width:8px;z-index:5;cursor:col-resize;
|
|
120
124
|
border-radius:5px;background:transparent;transition:background .15s}
|
|
121
|
-
.i2p-nav-grip:hover,.i2p-nav-grip:focus-visible{background:color-mix(in srgb,var(--accent) 26%,transparent)}
|
|
125
|
+
.i2p-nav-grip:hover,.i2p-nav-grip:focus-visible{background:var(--hover);background:color-mix(in srgb,var(--accent) 26%,transparent)}
|
|
122
126
|
body.i2p-dragging{cursor:col-resize}
|
|
123
127
|
body.i2p-dragging *{cursor:col-resize!important}
|
|
124
128
|
body.i2p-dragging{user-select:none}
|
|
@@ -445,6 +449,38 @@ body.i2p-dragging{user-select:none}
|
|
|
445
449
|
.i2p .conn-row .cn-tok{font-family:var(--mono);font-size:11.5px;color:var(--muted)}
|
|
446
450
|
.i2p .conn-row .cn-act{margin-left:auto;display:flex;gap:6px}
|
|
447
451
|
|
|
452
|
+
/* ===== 委外智能体绑定卡(项目页 P6=claude / 配置页 P6 共用):多方式发现 + 测试门禁 ===== */
|
|
453
|
+
.i2p .agent-bind{border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-top:10px}
|
|
454
|
+
.i2p .agent-cands{display:flex;flex-direction:column;gap:5px;margin:8px 0 2px}
|
|
455
|
+
.i2p .agent-cand{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;
|
|
456
|
+
background:none;border:1px solid var(--line);border-radius:9px;padding:5px 10px;
|
|
457
|
+
transition:border-color .15s,background .15s;font-family:inherit;font-size:inherit;color:inherit}
|
|
458
|
+
.i2p .agent-cand:hover{background:var(--hover)}
|
|
459
|
+
.i2p .agent-cand.on{border-color:var(--accent);background:var(--nav-on)}
|
|
460
|
+
.i2p .agent-cand .ac-path{font-size:12.5px;color:var(--ink);min-width:0;word-break:break-all}
|
|
461
|
+
.i2p .agent-cand .ac-path.mono{font-family:var(--mono)}
|
|
462
|
+
.i2p .agent-cand .ac-meta{margin-left:auto;flex:none;display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted)}
|
|
463
|
+
.i2p .agent-cand .ac-src{border:1px solid var(--line);border-radius:4px;padding:0 5px;font-size:10.5px;white-space:nowrap}
|
|
464
|
+
.i2p .agent-cand .miss{color:var(--warn)}
|
|
465
|
+
.i2p .agent-gate{margin-top:8px;font-size:12.5px;line-height:1.6}
|
|
466
|
+
.i2p .agent-gate .ag-line{margin:0;font-weight:600}
|
|
467
|
+
.i2p .agent-gate.ok .ag-line{color:var(--good)}
|
|
468
|
+
.i2p .agent-gate.bad .ag-line{color:var(--err)}
|
|
469
|
+
.i2p .agent-gate .ag-steps{margin:4px 0 0;display:flex;gap:6px;flex-wrap:wrap}
|
|
470
|
+
.i2p .agent-gate .ag-step{font-family:var(--mono);font-size:11px;border-radius:4px;padding:0 6px;border:1px solid var(--line)}
|
|
471
|
+
.i2p .agent-gate .ag-step.ok{color:var(--good)}
|
|
472
|
+
.i2p .agent-gate .ag-step.bad{color:var(--err);border-color:currentColor}
|
|
473
|
+
.i2p .agent-gate .ag-hint{margin:6px 0 0;color:var(--warn)}
|
|
474
|
+
/* 发现扫描走马灯:滚动展示解析全过程(各来源阶段);标记位当前阶段文字轮播 */
|
|
475
|
+
.i2p .scan-ticker{margin:8px 0 0;overflow:hidden;white-space:nowrap;border:1px dashed var(--line-2);
|
|
476
|
+
border-radius:8px;background:var(--card)}
|
|
477
|
+
.i2p .scan-ticker .tk{display:inline-block;padding-left:100%;line-height:22px;font-size:12px;
|
|
478
|
+
color:var(--muted);animation:i2p-tk 12s linear infinite}
|
|
479
|
+
@keyframes i2p-tk{from{transform:translateX(0)}to{transform:translateX(-100%)}}
|
|
480
|
+
@media (prefers-reduced-motion:reduce){.i2p .scan-ticker .tk{animation:none;padding-left:8px}}
|
|
481
|
+
.i2p .scan-now{color:var(--accent)}
|
|
482
|
+
.i2p .scan-fail{margin:8px 0 0}
|
|
483
|
+
|
|
448
484
|
/* ---- 侧边栏入口按钮:1:1 复刻宿主设置按钮(settings-general VOzbGW_trigger 配方) ---- */
|
|
449
485
|
.i2p-entry{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;
|
|
450
486
|
color:var(--dsw-alias-label-primary);background:none;border:none;border-radius:12px;
|
|
@@ -478,21 +514,27 @@ body.i2p-dragging{user-select:none}
|
|
|
478
514
|
.i2p-ai button{font-family:inherit;cursor:pointer;color:inherit}
|
|
479
515
|
/* 入口 = 中性工具钮(非"AI 球"):描边消息图标,hover 才显底;开启态反色 */
|
|
480
516
|
.i2p-ai-fab{pointer-events:auto;position:absolute;top:64px;right:88px;width:30px;height:30px;border-radius:9px;
|
|
517
|
+
border:1px solid var(--line-2,#d8dae3);
|
|
481
518
|
border:1px solid color-mix(in srgb,#3aa0f5 24%,var(--line-2,#d8dae3));
|
|
519
|
+
background:var(--panel,#fff);
|
|
482
520
|
background:color-mix(in srgb,#3aa0f5 9%,var(--panel,#fff));color:var(--ink-2,#414351);
|
|
483
521
|
display:inline-flex;align-items:center;justify-content:center;
|
|
484
522
|
box-shadow:0 1px 4px rgba(9,12,20,.08);
|
|
485
523
|
transition:background .18s,color .18s,border-color .18s,transform .12s}
|
|
486
|
-
.i2p-ai-fab:hover{background:color-mix(in srgb,#3aa0f5 16%,var(--panel,#fff));color:var(--ink);transform:translateY(-1px)}
|
|
524
|
+
.i2p-ai-fab:hover{background:var(--hover);background:color-mix(in srgb,#3aa0f5 16%,var(--panel,#fff));color:var(--ink);transform:translateY(-1px)}
|
|
487
525
|
.i2p-ai-fab:active{transform:scale(.95)}
|
|
488
526
|
.i2p-ai-fab.on{background:var(--ink,#17181f);color:var(--card,#fff);border-color:var(--ink,#17181f)}
|
|
489
527
|
@keyframes i2p-ai-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
|
|
490
528
|
/* 面板:轻天蓝底(color-mix 混主题底色,浅色=淡天蓝/深色=深蓝调,官方 Markdown 文字两主题可读),
|
|
491
|
-
全部强调(边框/focus/hover/chip
|
|
529
|
+
全部强调(边框/focus/hover/chip 圆点)统一同一天蓝色系,避免杂色。
|
|
530
|
+
每处 color-mix 前都有一行传统语法兜底:旧内核(Chromium<111)把不认识的值整条丢弃、
|
|
531
|
+
保留兜底行——面板退化为"无天蓝染色但结构完整",而不是无背景无边框 */
|
|
492
532
|
.i2p-ai-panel{pointer-events:auto;position:absolute;top:60px;width:400px;height:calc(100% - 76px);
|
|
493
533
|
min-width:240px;min-height:200px;
|
|
494
534
|
display:flex;flex-direction:column;
|
|
535
|
+
background:var(--panel,#fff);
|
|
495
536
|
background:color-mix(in srgb,#3aa0f5 7%,var(--panel,#fff));
|
|
537
|
+
border:1px solid var(--line-2,#d8dae3);
|
|
496
538
|
border:1px solid color-mix(in srgb,#3aa0f5 20%,var(--line-2,#d8dae3));border-radius:12px;
|
|
497
539
|
box-shadow:0 6px 28px rgba(9,12,20,.12);overflow:hidden;
|
|
498
540
|
animation:i2p-ai-in .2s cubic-bezier(.2,.8,.2,1)}
|
|
@@ -507,12 +549,13 @@ body.i2p-dragging{user-select:none}
|
|
|
507
549
|
.i2p-ai-rz.ne{top:0;right:0;width:24px;height:24px;cursor:nesw-resize}
|
|
508
550
|
.i2p-ai-rz.sw{bottom:0;left:0;width:24px;height:24px;cursor:nesw-resize}
|
|
509
551
|
.i2p-ai-rz.se{bottom:0;right:0;width:24px;height:24px;cursor:nwse-resize}
|
|
510
|
-
.i2p-ai-rz:hover{background:color-mix(in srgb,#3aa0f5 35%,transparent)}
|
|
552
|
+
.i2p-ai-rz:hover{background:var(--hover);background:color-mix(in srgb,#3aa0f5 35%,transparent)}
|
|
511
553
|
/* 头部:标题 + 小字副题 + 关闭,细分割线(无图标) */
|
|
512
554
|
.i2p-ai-head{flex:none;display:flex;align-items:baseline;gap:8px;padding:11px 14px;cursor:move;
|
|
555
|
+
border-bottom:1px solid var(--line,#e5e6ec);
|
|
513
556
|
border-bottom:1px solid color-mix(in srgb,#3aa0f5 14%,var(--line,#e5e6ec))}
|
|
514
|
-
.i2p-ai-title{font-size:13.5px;font-weight:600;color:var(--ink);letter-spacing:.01em}
|
|
515
|
-
.i2p-ai-sub{font-size:11.5px;color:var(--muted)}
|
|
557
|
+
.i2p-ai-title{font-size:var(--fs-md,13.5px);font-weight:600;color:var(--ink);letter-spacing:.01em}
|
|
558
|
+
.i2p-ai-sub{font-size:var(--fs-xs,11.5px);color:var(--muted)}
|
|
516
559
|
.i2p-ai-close{margin-left:auto;align-self:center;width:26px;height:26px;border:none;border-radius:7px;background:none;
|
|
517
560
|
color:var(--muted);display:inline-flex;align-items:center;justify-content:center;
|
|
518
561
|
transition:background .15s,color .15s}
|
|
@@ -524,28 +567,51 @@ body.i2p-dragging{user-select:none}
|
|
|
524
567
|
.i2p-ai-msgs::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:6px}
|
|
525
568
|
/* 空态:一句能力说明 + 快捷问题(浅底单行钮,前缀天蓝圆点) */
|
|
526
569
|
.i2p-ai-empty{display:flex;flex-direction:column;gap:10px;padding-top:2px}
|
|
527
|
-
.i2p-ai-hint{margin:0;font-size:12.5px;color:var(--muted);line-height:1.6}
|
|
570
|
+
.i2p-ai-hint{margin:0;font-size:var(--fs-sm,12.5px);color:var(--muted);line-height:1.6}
|
|
528
571
|
.i2p-ai-chip{display:flex;align-items:center;gap:8px;text-align:left;
|
|
572
|
+
background:var(--card,#fff);
|
|
529
573
|
background:color-mix(in srgb,var(--card,#fff) 58%,transparent);
|
|
530
|
-
border:none;border-radius:8px;padding:7px 11px;font-size:
|
|
574
|
+
border:none;border-radius:8px;padding:7px 11px;font-size:var(--fs-md,13.5px);color:var(--ink-2);
|
|
531
575
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
532
576
|
transition:background .15s,color .15s}
|
|
533
577
|
.i2p-ai-chip::before{content:"";flex:none;width:5px;height:5px;border-radius:50%;
|
|
578
|
+
background:#3aa0f5;
|
|
534
579
|
background:color-mix(in srgb,#3aa0f5 70%,var(--muted,#82848f))}
|
|
535
|
-
.i2p-ai-chip:hover:not(:disabled){background:color-mix(in srgb,#3aa0f5 12%,var(--panel,#fff));color:var(--ink)}
|
|
580
|
+
.i2p-ai-chip:hover:not(:disabled){background:var(--hover);background:color-mix(in srgb,#3aa0f5 12%,var(--panel,#fff));color:var(--ink)}
|
|
536
581
|
.i2p-ai-chip:disabled{opacity:.5;cursor:default}
|
|
537
582
|
/* 用户问题:半透明卡色块(在蓝底上呈更浅的蓝白),全宽不挤字 */
|
|
538
|
-
.i2p-ai-m{max-width:100%;font-size:13.5px;line-height:1.65}
|
|
539
|
-
.i2p-ai-m.user .i2p-ai-t{background:
|
|
583
|
+
.i2p-ai-m{max-width:100%;font-size:var(--fs-md,13.5px);line-height:1.65}
|
|
584
|
+
.i2p-ai-m.user .i2p-ai-t{background:var(--card,#fff);
|
|
585
|
+
background:color-mix(in srgb,var(--card,#fff) 62%,transparent);
|
|
586
|
+
border:1px solid var(--line,#e5e6ec);
|
|
540
587
|
border:1px solid color-mix(in srgb,#3aa0f5 12%,var(--line,#e5e6ec));
|
|
541
588
|
border-radius:10px;padding:8px 12px;color:var(--ink);white-space:pre-wrap;word-break:break-word}
|
|
542
589
|
.i2p-ai-m.assistant{overflow-wrap:break-word}
|
|
543
|
-
.i2p-ai-m.assistant.err .i2p-ai-t{color:var(--err);font-size:12.5px;
|
|
590
|
+
.i2p-ai-m.assistant.err .i2p-ai-t{color:var(--err);font-size:var(--fs-sm,12.5px);
|
|
544
591
|
background:var(--err-bg,rgba(213,70,58,.10));border-radius:8px;padding:7px 11px}
|
|
545
592
|
.i2p-ai-m .i2p-ai-t{white-space:pre-wrap;word-break:break-word}
|
|
546
|
-
.i2p-ai-md{font-size:13.5px;color:var(--ink)}
|
|
593
|
+
.i2p-ai-md{font-size:var(--fs-md,13.5px);color:var(--ink)}
|
|
547
594
|
.i2p-ai-md :first-child{margin-top:0}
|
|
548
595
|
.i2p-ai-md :last-child{margin-bottom:0}
|
|
596
|
+
/* 助手回答 Markdown:插件自持排版,不随宿主 .md 全局样式版本漂移——
|
|
597
|
+
".i2p-page .i2p-ai-md xxx" 的 specificity 压过宿主 ".md xxx" 类规则,所有电脑渲染一致 */
|
|
598
|
+
.i2p-page .i2p-ai-md p,.i2p-page .i2p-ai-md ul,.i2p-page .i2p-ai-md ol{margin:0 0 8px}
|
|
599
|
+
.i2p-page .i2p-ai-md li{margin:2px 0}
|
|
600
|
+
.i2p-page .i2p-ai-md h1,.i2p-page .i2p-ai-md h2,.i2p-page .i2p-ai-md h3,.i2p-page .i2p-ai-md h4{
|
|
601
|
+
margin:12px 0 6px;line-height:1.4;color:var(--ink)}
|
|
602
|
+
.i2p-page .i2p-ai-md h1{font-size:16px}
|
|
603
|
+
.i2p-page .i2p-ai-md h2{font-size:15px}
|
|
604
|
+
.i2p-page .i2p-ai-md h3{font-size:var(--fs-lg,14.5px)}
|
|
605
|
+
.i2p-page .i2p-ai-md h4{font-size:var(--fs-md,13.5px)}
|
|
606
|
+
.i2p-page .i2p-ai-md code{font-family:var(--mono);font-size:12.5px;background:var(--hover);
|
|
607
|
+
padding:1px 5px;border-radius:4px}
|
|
608
|
+
.i2p-page .i2p-ai-md pre{background:var(--code-bg);border-radius:8px;padding:10px 12px;overflow-x:auto}
|
|
609
|
+
.i2p-page .i2p-ai-md pre code{background:none;padding:0;font-size:12.5px;line-height:1.6;color:#e6edf3}
|
|
610
|
+
.i2p-page .i2p-ai-md table{border-collapse:collapse;margin:0 0 8px;font-size:var(--fs-sm,12.5px)}
|
|
611
|
+
.i2p-page .i2p-ai-md th,.i2p-page .i2p-ai-md td{border:1px solid var(--line);padding:4px 8px;text-align:left}
|
|
612
|
+
.i2p-page .i2p-ai-md blockquote{margin:0 0 8px;padding:2px 0 2px 10px;border-left:3px solid var(--line-2);color:var(--ink-2)}
|
|
613
|
+
.i2p-page .i2p-ai-md a{color:var(--accent)}
|
|
614
|
+
.i2p-page .i2p-ai-md hr{border:none;border-top:1px solid var(--line);margin:10px 0}
|
|
549
615
|
/* 流式等待:三点呼吸 */
|
|
550
616
|
@keyframes i2p-ai-dot{50%{opacity:.25}}
|
|
551
617
|
.i2p-ai-dots{display:inline-flex;gap:4px;padding:2px 0}
|
|
@@ -555,25 +621,29 @@ body.i2p-dragging{user-select:none}
|
|
|
555
621
|
.i2p-ai-dots i:nth-child(3){animation-delay:.4s}
|
|
556
622
|
/* 输入区:半透明卡色输入框 + 图标钮(生成中变停止,可中断) */
|
|
557
623
|
.i2p-ai-input{flex:none;display:flex;gap:8px;align-items:flex-end;padding:10px 12px 12px;
|
|
624
|
+
border-top:1px solid var(--line,#e5e6ec);
|
|
558
625
|
border-top:1px solid color-mix(in srgb,#3aa0f5 14%,var(--line,#e5e6ec))}
|
|
559
626
|
.i2p-ai-input textarea{flex:1;min-height:40px;max-height:120px;resize:none;
|
|
627
|
+
background:var(--card,#fff);
|
|
560
628
|
background:color-mix(in srgb,var(--card,#fff) 66%,transparent);
|
|
629
|
+
border:1px solid var(--line-2,#d8dae3);
|
|
561
630
|
border:1px solid color-mix(in srgb,#3aa0f5 16%,var(--line-2,#d8dae3));
|
|
562
|
-
border-radius:9px;color:var(--ink);padding:8px 11px;font-size:13.5px;
|
|
631
|
+
border-radius:9px;color:var(--ink);padding:8px 11px;font-size:var(--fs-md,13.5px);
|
|
563
632
|
line-height:1.55;font-family:inherit;transition:border-color .15s}
|
|
564
|
-
.i2p-ai-input textarea:focus{border-color:color-mix(in srgb,#3aa0f5 55%,var(--line-2,#d8dae3))}
|
|
633
|
+
.i2p-ai-input textarea:focus{border-color:var(--accent);border-color:color-mix(in srgb,#3aa0f5 55%,var(--line-2,#d8dae3))}
|
|
565
634
|
.i2p-ai-input textarea::placeholder{color:var(--muted)}
|
|
566
635
|
.i2p-ai-send{flex:none;width:38px;height:38px;border-radius:9px;
|
|
636
|
+
border:1px solid var(--line-2,#d8dae3);
|
|
567
637
|
border:1px solid color-mix(in srgb,#3aa0f5 30%,var(--line-2,#d8dae3));
|
|
638
|
+
background:var(--panel,#fff);
|
|
568
639
|
background:color-mix(in srgb,#3aa0f5 12%,var(--panel,#fff));color:var(--ink-2);
|
|
569
640
|
display:inline-flex;align-items:center;justify-content:center;
|
|
570
641
|
transition:background .15s,border-color .15s,color .15s,transform .1s}
|
|
571
|
-
.i2p-ai-send:hover:not(:disabled){border-color:color-mix(in srgb,#3aa0f5 55%,var(--line-2,#d8dae3));color:var(--ink)}
|
|
642
|
+
.i2p-ai-send:hover:not(:disabled){border-color:var(--accent);border-color:color-mix(in srgb,#3aa0f5 55%,var(--line-2,#d8dae3));color:var(--ink)}
|
|
572
643
|
.i2p-ai-send:active:not(:disabled){transform:scale(.95)}
|
|
573
644
|
.i2p-ai-send:disabled{opacity:.4;cursor:default}
|
|
574
645
|
@media (max-width:880px){
|
|
575
646
|
.i2p-ai-fab{right:88px;top:60px}
|
|
576
|
-
.i2p-ai-panel{width:min(360px,calc(100% - 100px))}
|
|
577
647
|
}
|
|
578
648
|
|
|
579
649
|
/* ===== 动效与质感层(丰富但克制:150-300ms、语义化、可关) ===== */
|
|
@@ -800,6 +870,34 @@ body.i2p-dragging{user-select:none}
|
|
|
800
870
|
return v;
|
|
801
871
|
}
|
|
802
872
|
|
|
873
|
+
/* ================================================================
|
|
874
|
+
* lastRun 记忆(按项目记最近选中的 Run):localStorage 快路径 + 服务端
|
|
875
|
+
* ui-state 兜底(与 i2p.proj 同双写策略)。宿主标签切换会销毁重建插件
|
|
876
|
+
* webview,选中 Run 的现场以此恢复,而不是每次都掉回最新 Run。
|
|
877
|
+
* ================================================================ */
|
|
878
|
+
const lastRunStore = {
|
|
879
|
+
map: (function () { try { return JSON.parse(localStorage.getItem("i2p.run") || "{}") || {}; } catch (e) { return {}; } })(),
|
|
880
|
+
get(slug) { return this.map[slug] || null; },
|
|
881
|
+
set(slug, runId) {
|
|
882
|
+
if (!slug) return;
|
|
883
|
+
if (runId) this.map[slug] = runId; else delete this.map[slug];
|
|
884
|
+
try { localStorage.setItem("i2p.run", JSON.stringify(this.map)); } catch (e) { /* 忽略 */ }
|
|
885
|
+
apiPost("/ui-state", { lastRunBySlug: runId ? { [slug]: runId } : { [slug]: null } })
|
|
886
|
+
.catch(function () { /* 兜底写失败静默:localStorage 仍有效 */ });
|
|
887
|
+
},
|
|
888
|
+
// 服务端兜底返回后并入(只补缺失键,不覆盖本地较新的记忆)
|
|
889
|
+
adopt(map) {
|
|
890
|
+
if (!map || typeof map !== "object") return;
|
|
891
|
+
for (const k of Object.keys(map)) if (!this.map[k] && map[k]) this.map[k] = map[k];
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/* ================================================================
|
|
896
|
+
* 运行页现场(选中阶段 / 二级菜单 / 产物文件):模块级 store,
|
|
897
|
+
* RunsPanel 每次渲染写回;切走一级标签卸载后重挂载,runId 一致则恢复
|
|
898
|
+
* ================================================================ */
|
|
899
|
+
const runsViewStore = { runId: null, selStage: null, stTab: "run", selArt: null };
|
|
900
|
+
|
|
803
901
|
/* ================================================================
|
|
804
902
|
* Git 托管连接辅助:host 解析(与服务端 lib/connections.js 对齐)。
|
|
805
903
|
* 支持 https / ssh:// / scp(git@host:path)三种形态。
|
|
@@ -945,15 +1043,144 @@ body.i2p-dragging{user-select:none}
|
|
|
945
1043
|
: h("button", { type: "button", className: "add-row", onClick: function () { setAdding(true); } }, Ic("plus", 12), " 添加连接"));
|
|
946
1044
|
}
|
|
947
1045
|
|
|
1046
|
+
// 委外智能体发现扫描阶段(与服务端 discoverAgents 探测顺序一致;走马灯滚动展示全过程)
|
|
1047
|
+
const AGENT_SCAN_STEPS = [
|
|
1048
|
+
"读取项目配置 stageConfig.P6.params.claudeBin",
|
|
1049
|
+
"检查环境变量 ISSUE2PR_CLAUDE_BIN",
|
|
1050
|
+
"扫描常见安装位置(.npm-global / .npm_global / AppData\\Roaming\\npm / .local\\bin)",
|
|
1051
|
+
"查询 npm 全局目录(npm config get prefix)",
|
|
1052
|
+
"PATH 查找 claude(where / which)",
|
|
1053
|
+
"候选去重合并 · 存在性校验",
|
|
1054
|
+
];
|
|
1055
|
+
const AGENT_SCAN_SHORT = ["读项目配置", "查环境变量", "扫常见位置", "查 npm 全局", "PATH 查找", "去重校验"];
|
|
1056
|
+
|
|
1057
|
+
/* 委外智能体绑定卡(项目页 P6=claude 与「配置」页 P6 共用):
|
|
1058
|
+
* 发现——五种来源(项目配置/环境变量/常见安装位置/npm 全局目录/PATH)+ 手动路径;
|
|
1059
|
+
* 门禁——定位 → 版本 → 认证微任务(一次极小真实调用,403 IP 白名单等在绑定时拦截而非 Run 中暴露)。
|
|
1060
|
+
* gate 状态由父组件持有(控制保存按钮):{key:"bin:<路径>"|"auto", status:"pass"|"fail", result}
|
|
1061
|
+
* 扫描过程走马灯可见:请求 20s 超时 / 404(旧版 node 半)显式报错 + 重试,不再无限「解析中…」。 */
|
|
1062
|
+
function AgentBindCard(props) {
|
|
1063
|
+
const p = props; // { bin, onBinChange, gate, onGate, toast, slug }
|
|
1064
|
+
const [found, setFound] = React.useState(null);
|
|
1065
|
+
// 扫描态:status scanning|done|fail;phase 驱动标记位阶段轮播与走马灯
|
|
1066
|
+
const [scan, setScan] = React.useState({ status: "scanning", phase: 0, error: "" });
|
|
1067
|
+
const [testing, setTesting] = React.useState(false);
|
|
1068
|
+
|
|
1069
|
+
const doDiscover = React.useCallback(function () {
|
|
1070
|
+
setFound(null);
|
|
1071
|
+
setScan({ status: "scanning", phase: 0, error: "" });
|
|
1072
|
+
const q = p.slug ? "?slug=" + encodeURIComponent(p.slug) : "";
|
|
1073
|
+
const req = apiGet("/agents/discover" + q).catch(function () { return null; });
|
|
1074
|
+
const guard = new Promise(function (r) { setTimeout(function () { r({ __timeout: true }); }, 20000); });
|
|
1075
|
+
Promise.race([req, guard]).then(function (r) {
|
|
1076
|
+
if (r && r.ok) {
|
|
1077
|
+
setFound({ agents: r.agents || [], resolved: r.resolved || "" });
|
|
1078
|
+
setScan({ status: "done", phase: 0, error: "" });
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const msg = !r ? "发现请求失败(网络错误)"
|
|
1082
|
+
: r.__timeout ? "发现请求超时(20 秒无响应)"
|
|
1083
|
+
: "发现接口不可用" + (r.message ? ":" + r.message : "");
|
|
1084
|
+
setScan({ status: "fail", phase: 0, error: msg + "——若刚升级插件,node 半需重启 DSH 生效后重试" });
|
|
1085
|
+
});
|
|
1086
|
+
}, [p.slug]);
|
|
1087
|
+
React.useEffect(function () { doDiscover(); }, [doDiscover]);
|
|
1088
|
+
// 扫描期间阶段轮播(标记位「读项目配置…」→「查环境变量…」→ …)
|
|
1089
|
+
React.useEffect(function () {
|
|
1090
|
+
if (scan.status !== "scanning") return;
|
|
1091
|
+
const t = setInterval(function () { setScan(function (s) { return Object.assign({}, s, { phase: s.phase + 1 }); }); }, 1200);
|
|
1092
|
+
return function () { clearInterval(t); };
|
|
1093
|
+
}, [scan.status]);
|
|
1094
|
+
|
|
1095
|
+
const cur = (p.bin || "").trim();
|
|
1096
|
+
const gateLive = p.gate && p.gate.key === (cur ? "bin:" + cur : "auto") ? p.gate : null;
|
|
1097
|
+
const runGate = function () {
|
|
1098
|
+
if (testing) return;
|
|
1099
|
+
setTesting(true);
|
|
1100
|
+
apiPost("/agents/test", cur ? { bin: cur } : {}).then(function (r) {
|
|
1101
|
+
setTesting(false);
|
|
1102
|
+
if (!r || typeof r.ok !== "boolean") { p.toast("门禁请求失败", "bad"); return; }
|
|
1103
|
+
// r.gate 缺失 = 接口 404/异常(常见于 UI 已新版而 node 半未随重启更新)——给出可操作文案
|
|
1104
|
+
const res = r.gate || { message: (r && r.message) ? ("接口异常:" + r.message) : "接口不可用——若刚升级插件,请重启 DSH 后重试" };
|
|
1105
|
+
p.onGate({ key: cur ? "bin:" + cur : "auto", status: r.ok ? "pass" : "fail", result: res });
|
|
1106
|
+
if (r.ok) p.toast("委外智能体门禁通过" + (r.gate && r.gate.version ? ":" + r.gate.version : ""));
|
|
1107
|
+
}).catch(function (e) { setTesting(false); p.toast("请求失败: " + e, "bad"); });
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
return h("div", { className: "agent-bind" },
|
|
1111
|
+
h("span", { className: "f-label" }, "委外智能体 · Claude Code(测试门禁通过后才能保存)"),
|
|
1112
|
+
h("p", { className: "hint-line", style: { margin: "6px 0 0" } },
|
|
1113
|
+
"P6「委托 Claude Code」由本机 claude CLI 无人值守执行。绑定 = 从候选选择或手动指定可执行文件;",
|
|
1114
|
+
"「测试门禁」真实跑一次极小调用(定位 → 版本 → 认证,费用可忽略)——403 IP 白名单 / 未登录这类错误在绑定时拦截,而不是等 Run 失败后才暴露。"),
|
|
1115
|
+
h("div", { className: "agent-cands", role: "radiogroup", "aria-label": "发现的 claude 安装" },
|
|
1116
|
+
h("button", {
|
|
1117
|
+
type: "button", key: "__auto__", className: "agent-cand" + (!cur ? " on" : ""),
|
|
1118
|
+
onClick: function () { p.onBinChange(""); },
|
|
1119
|
+
},
|
|
1120
|
+
h("span", { className: "ac-path" }, "自动探测"),
|
|
1121
|
+
h("span", { className: "ac-meta" },
|
|
1122
|
+
found ? "当前解析 " + (found.resolved || "claude")
|
|
1123
|
+
: scan.status === "fail" ? h("span", { className: "miss" }, "发现失败")
|
|
1124
|
+
: h("span", { className: "scan-now" }, AGENT_SCAN_SHORT[scan.phase % AGENT_SCAN_SHORT.length] + "…"))),
|
|
1125
|
+
(found ? found.agents : []).map(function (a, i) {
|
|
1126
|
+
return h("button", {
|
|
1127
|
+
type: "button", key: "a" + i,
|
|
1128
|
+
className: "agent-cand" + (cur && cur.toLowerCase() === a.path.toLowerCase() ? " on" : ""),
|
|
1129
|
+
onClick: function () { p.onBinChange(a.path); },
|
|
1130
|
+
},
|
|
1131
|
+
h("span", { className: "ac-path mono" }, a.path),
|
|
1132
|
+
h("span", { className: "ac-meta" },
|
|
1133
|
+
h("span", { className: "ac-src" }, a.label),
|
|
1134
|
+
a.exists ? "已就绪" : h("span", { className: "miss" }, "不存在")));
|
|
1135
|
+
})),
|
|
1136
|
+
// —— 解析过程走马灯(标记位下方通栏滚动)——
|
|
1137
|
+
scan.status === "scanning" ? h("div", { className: "scan-ticker", role: "status", "aria-live": "polite" },
|
|
1138
|
+
h("span", { className: "tk" }, "正在解析 claude 安装 ▸ " + AGENT_SCAN_STEPS.join(" ▸ ") + " ▸ ")) : null,
|
|
1139
|
+
scan.status === "done" && found ? h("p", { className: "hint-line", style: { margin: "8px 0 0" } },
|
|
1140
|
+
found.agents.length
|
|
1141
|
+
? "扫描完成:发现 " + found.agents.length + " 个候选 · 自动解析 → " + (found.resolved || "claude")
|
|
1142
|
+
: "扫描完成:未发现已安装的 claude——请手动指定完整路径(如 C:\\Users\\you\\AppData\\Roaming\\npm\\claude.cmd),或先安装 claude CLI。") : null,
|
|
1143
|
+
scan.status === "fail" ? h("div", { className: "scan-fail" },
|
|
1144
|
+
h("p", { className: "hint-line", style: { margin: 0, color: "var(--err)" } }, "✗ " + scan.error),
|
|
1145
|
+
h("button", { type: "button", className: "btn sm", style: { marginTop: 6 }, onClick: doDiscover }, "重试扫描")) : null,
|
|
1146
|
+
h("div", { className: "dyn-row", style: { marginTop: 6 } },
|
|
1147
|
+
h("input", {
|
|
1148
|
+
className: "f-input mono", value: p.bin || "",
|
|
1149
|
+
placeholder: "手动指定可执行文件(留空 = 自动探测)",
|
|
1150
|
+
"aria-label": "claude 可执行文件路径",
|
|
1151
|
+
onChange: function (e) { p.onBinChange(e.target.value); },
|
|
1152
|
+
}),
|
|
1153
|
+
h("button", {
|
|
1154
|
+
type: "button", className: "btn sm" + (gateLive && gateLive.status === "pass" ? "" : " pri"),
|
|
1155
|
+
disabled: testing, onClick: runGate,
|
|
1156
|
+
}, testing ? "测试中…" : "测试门禁")),
|
|
1157
|
+
testing ? h("p", { className: "hint-line", style: { margin: "6px 0 0" } },
|
|
1158
|
+
"门禁测试中:真实调用一次 claude(通常 5–30 秒),请勿关闭本页…") : null,
|
|
1159
|
+
gateLive ? (function () {
|
|
1160
|
+
const g = gateLive.result || {};
|
|
1161
|
+
return h("div", { className: "agent-gate " + (gateLive.status === "pass" ? "ok" : "bad") },
|
|
1162
|
+
h("p", { className: "ag-line" },
|
|
1163
|
+
gateLive.status === "pass"
|
|
1164
|
+
? "✓ 门禁通过" + (g.version ? " · " + g.version : "") + (g.ms != null ? " · " + (Math.round(g.ms / 100) / 10) + "s" : "")
|
|
1165
|
+
: "✗ 未通过:" + (g.message || "未知错误")),
|
|
1166
|
+
(g.steps || []).length ? h("p", { className: "ag-steps" }, g.steps.map(function (s, i) {
|
|
1167
|
+
return h("span", { key: i, className: "ag-step " + (s.ok ? "ok" : "bad") }, (s.ok ? "✓" : "✗") + s.name);
|
|
1168
|
+
})) : null,
|
|
1169
|
+
g.hint ? h("p", { className: "ag-hint" }, g.hint) : null);
|
|
1170
|
+
})() : null);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
948
1173
|
function ProjectsPanel(props) {
|
|
949
1174
|
const p = props;
|
|
950
1175
|
const [form, setForm] = React.useState(null);
|
|
951
1176
|
const [saving, setSaving] = React.useState(false);
|
|
952
1177
|
// 本地触发源存在性检查结果:uri → true/false(check-local 端点;onBlur 与载入时触发)
|
|
953
1178
|
const [trigCheck, setTrigCheck] = React.useState({});
|
|
1179
|
+
// 委外智能体测试门禁结果:{key:"bin:<路径>"|"auto", status:"pass"|"fail", result}(p6Mode=claude 时保存前置条件)
|
|
1180
|
+
const [agentGate, setAgentGate] = React.useState(null);
|
|
954
1181
|
|
|
955
1182
|
const blankForm = function () {
|
|
956
|
-
return { name: "", slug: "", repos: [{ uri: "" }], triggers: [], reviewMode: "every", p6Mode: "builtin", testCommand: "" };
|
|
1183
|
+
return { name: "", slug: "", repos: [{ uri: "" }], triggers: [], reviewMode: "every", p6Mode: "builtin", testCommand: "", claudeBin: "" };
|
|
957
1184
|
};
|
|
958
1185
|
|
|
959
1186
|
React.useEffect(function () {
|
|
@@ -969,8 +1196,11 @@ body.i2p-dragging{user-select:none}
|
|
|
969
1196
|
reviewMode: pr.reviewMode || "every",
|
|
970
1197
|
p6Mode: pr.p6Mode || "builtin",
|
|
971
1198
|
testCommand: pr.testCommand || "",
|
|
1199
|
+
// 委外智能体绑定:claudeBin 存于 stageConfig.P6.params(「配置」页同源),空 = 自动探测
|
|
1200
|
+
claudeBin: (pr.stageConfig && pr.stageConfig.P6 && pr.stageConfig.P6.params && pr.stageConfig.P6.params.claudeBin) || "",
|
|
972
1201
|
});
|
|
973
1202
|
setTrigCheck({});
|
|
1203
|
+
setAgentGate(null);
|
|
974
1204
|
(pr.triggers || []).forEach(function (t) { checkLocal(t.uri); }); // 载入即查本地触发源存在性
|
|
975
1205
|
}, [p.slug, p.projects]);
|
|
976
1206
|
|
|
@@ -986,8 +1216,18 @@ body.i2p-dragging{user-select:none}
|
|
|
986
1216
|
|
|
987
1217
|
const collect = function () {
|
|
988
1218
|
const f = form;
|
|
989
|
-
// stageConfig
|
|
1219
|
+
// stageConfig(「配置」页维护)与项目表单互不感知:原样透传,避免保存项目时丢失阶段配置;
|
|
1220
|
+
// 唯一例外 claudeBin——委外智能体绑定卡在本表单维护,合并进 P6.params(清空 = 移除覆盖恢复自动探测)
|
|
990
1221
|
const pr = p.projects ? p.projects.find(function (x) { return x.slug === p.slug; }) : null;
|
|
1222
|
+
const stageConfig = JSON.parse(JSON.stringify((pr && pr.stageConfig) || {}));
|
|
1223
|
+
const bin = (f.claudeBin || "").trim();
|
|
1224
|
+
if (bin || (stageConfig.P6 && stageConfig.P6.params && stageConfig.P6.params.claudeBin)) {
|
|
1225
|
+
const p6 = stageConfig.P6 || {};
|
|
1226
|
+
const params = Object.assign({}, p6.params);
|
|
1227
|
+
if (bin) params.claudeBin = bin; else delete params.claudeBin;
|
|
1228
|
+
if (Object.keys(params).length) p6.params = params; else delete p6.params;
|
|
1229
|
+
if (Object.keys(p6).length) stageConfig.P6 = p6; else delete stageConfig.P6;
|
|
1230
|
+
}
|
|
991
1231
|
return {
|
|
992
1232
|
name: (f.name || "").trim(),
|
|
993
1233
|
slug: (f.slug || "").trim(),
|
|
@@ -998,7 +1238,7 @@ body.i2p-dragging{user-select:none}
|
|
|
998
1238
|
reviewMode: f.reviewMode,
|
|
999
1239
|
p6Mode: f.p6Mode,
|
|
1000
1240
|
testCommand: (f.testCommand || "").trim(),
|
|
1001
|
-
stageConfig:
|
|
1241
|
+
stageConfig: stageConfig,
|
|
1002
1242
|
};
|
|
1003
1243
|
};
|
|
1004
1244
|
const validate = function (o) {
|
|
@@ -1007,11 +1247,21 @@ body.i2p-dragging{user-select:none}
|
|
|
1007
1247
|
if (o.repos.length === 0) return "至少填写一个 Git 仓库链接";
|
|
1008
1248
|
return null;
|
|
1009
1249
|
};
|
|
1250
|
+
// 委外智能体门禁是否对当前绑定有效:仅 p6Mode=claude 需要;改过路径后 key 不匹配即失效需重测
|
|
1251
|
+
const agentGateOk = function () {
|
|
1252
|
+
if (form.p6Mode !== "claude") return true;
|
|
1253
|
+
const cur = (form.claudeBin || "").trim();
|
|
1254
|
+
return !!(agentGate && agentGate.status === "pass" && agentGate.key === (cur ? "bin:" + cur : "auto"));
|
|
1255
|
+
};
|
|
1010
1256
|
|
|
1011
1257
|
const ensureSaved = function () {
|
|
1012
1258
|
const o = collect();
|
|
1013
1259
|
const err = validate(o);
|
|
1014
1260
|
if (err) { p.toast(err, "bad"); return Promise.resolve(null); }
|
|
1261
|
+
if (!agentGateOk()) {
|
|
1262
|
+
p.toast("P6 委托 Claude Code:请先通过「委外智能体」测试门禁再保存", "bad");
|
|
1263
|
+
return Promise.resolve(null);
|
|
1264
|
+
}
|
|
1015
1265
|
setSaving(true);
|
|
1016
1266
|
return apiPost("/projects", o).then(function (r) {
|
|
1017
1267
|
setSaving(false);
|
|
@@ -1169,8 +1419,7 @@ body.i2p-dragging{user-select:none}
|
|
|
1169
1419
|
if (pf && pf.claude && !pf.claude.ok && form.p6Mode === "claude") banners.push(h("div", { key: "claude", className: "callout warn", style: { marginBottom: 12 } },
|
|
1170
1420
|
h("h4", null, "未探测到 claude CLI"),
|
|
1171
1421
|
h("p", { style: { margin: 0 } },
|
|
1172
|
-
"尝试路径 ", h("code", null, pf.claude.path || "claude"), "
|
|
1173
|
-
"可在「配置」页 P6 阶段指定 claude 可执行文件。")));
|
|
1422
|
+
"尝试路径 ", h("code", null, pf.claude.path || "claude"), " 不存在。请在下方 P6 执行模式选「委托 Claude Code」后,用「委外智能体」卡片从扫描候选选择或手动指定路径,并通过测试门禁后保存。")));
|
|
1174
1423
|
// —— LLM 路由行:默认模型从哪来 + 几个阶段已覆盖(key 由 DSH 宿主管理,插件无法预检其有效性) ——
|
|
1175
1424
|
const llmRoute = pf && pf.llm
|
|
1176
1425
|
? (function () {
|
|
@@ -1246,6 +1495,12 @@ body.i2p-dragging{user-select:none}
|
|
|
1246
1495
|
{ value: "session", label: "交给 DSH 会话", hint: "真 workflow" },
|
|
1247
1496
|
{ value: "claude", label: "委托 Claude Code", hint: "claude CLI 自动执行" },
|
|
1248
1497
|
], form.p6Mode, function (v) { setField("p6Mode", v); }, "exec"))),
|
|
1498
|
+
// 委外智能体绑定(仅 claude 模式):多方式发现 + 测试门禁,通过后才能保存
|
|
1499
|
+
form.p6Mode === "claude" ? h(AgentBindCard, {
|
|
1500
|
+
key: "agent-bind", bin: form.claudeBin, gate: agentGate,
|
|
1501
|
+
onBinChange: function (v) { setField("claudeBin", v); },
|
|
1502
|
+
onGate: setAgentGate, toast: p.toast, slug: p.slug,
|
|
1503
|
+
}) : null,
|
|
1249
1504
|
h("div", { className: "field" },
|
|
1250
1505
|
h("label", { className: "f-label", htmlFor: "f-test" }, "测试命令(可选 · P8 使用)"),
|
|
1251
1506
|
h("input", {
|
|
@@ -1255,7 +1510,11 @@ body.i2p-dragging{user-select:none}
|
|
|
1255
1510
|
})),
|
|
1256
1511
|
h(ConnectionsCard, { connections: p.connections, toast: p.toast, reloadConnections: p.reloadConnections }),
|
|
1257
1512
|
h("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
1258
|
-
h("button", {
|
|
1513
|
+
h("button", {
|
|
1514
|
+
type: "submit", className: "btn pri",
|
|
1515
|
+
disabled: saving || !agentGateOk(),
|
|
1516
|
+
title: form.p6Mode === "claude" && !agentGateOk() ? "P6 委托 Claude Code 需先通过委外智能体测试门禁" : "",
|
|
1517
|
+
}, "保存配置"),
|
|
1259
1518
|
h("button", { type: "button", className: "btn", onClick: runFirst }, Ic("play"), " 用该项目发起 Run"),
|
|
1260
1519
|
form.slug && p.slug ? h("button", { type: "button", className: "btn danger", onClick: deleteProject }, Ic("trash"), " 删除项目") : null),
|
|
1261
1520
|
h("p", { className: "hint-line" },
|
|
@@ -1375,14 +1634,27 @@ body.i2p-dragging{user-select:none}
|
|
|
1375
1634
|
const artRef = React.useRef(null);
|
|
1376
1635
|
React.useEffect(function () { artRef.current = selArt; }, [selArt]);
|
|
1377
1636
|
|
|
1378
|
-
// run
|
|
1637
|
+
// run 切换:重置选择(二级菜单回「运行」);同一 Run 重挂载(切走一级标签再回来)
|
|
1638
|
+
// 则从模块级 runsViewStore 恢复现场(选中阶段 / 二级菜单 / 产物文件)
|
|
1379
1639
|
React.useEffect(function () {
|
|
1380
|
-
|
|
1640
|
+
const saved = runsViewStore.runId === p.runId ? runsViewStore : null;
|
|
1641
|
+
setSelArt(saved ? saved.selArt : null);
|
|
1642
|
+
artRef.current = saved ? saved.selArt : null; // 供产物自动选中效果保位(不抢占恢复的文件)
|
|
1643
|
+
setArtText(null); setComment(""); setRerunStage(null);
|
|
1644
|
+
setStTab(saved && saved.stTab ? saved.stTab : "run");
|
|
1381
1645
|
setReviews(null); setLedger(null); setEvents(null); setOpenEv(-1); evRef.current = null;
|
|
1382
|
-
if (
|
|
1646
|
+
if (saved && saved.selStage) setSelStage(saved.selStage);
|
|
1647
|
+
else if (p.run) setSelStage(p.run.current || "P1");
|
|
1383
1648
|
else setSelStage(null);
|
|
1384
1649
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1385
1650
|
}, [p.runId]);
|
|
1651
|
+
// 现场写回模块级 store(每次渲染同步;卸载后由重挂载恢复)
|
|
1652
|
+
React.useEffect(function () {
|
|
1653
|
+
runsViewStore.runId = p.runId; runsViewStore.selStage = selStage;
|
|
1654
|
+
runsViewStore.stTab = stTab; runsViewStore.selArt = selArt;
|
|
1655
|
+
});
|
|
1656
|
+
// 重挂载即拉一次最新状态(不等 3s 轮询):切回「运行」页立刻看到真实进度
|
|
1657
|
+
React.useEffect(function () { if (p.onChanged) p.onChanged(); }, []);
|
|
1386
1658
|
// 流水线停住(待复核/失败/停止/完成)时钉到 run.current
|
|
1387
1659
|
React.useEffect(function () {
|
|
1388
1660
|
if (!p.run) return;
|
|
@@ -1419,13 +1691,15 @@ body.i2p-dragging{user-select:none}
|
|
|
1419
1691
|
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1420
1692
|
};
|
|
1421
1693
|
|
|
1422
|
-
// 阶段或产物文件变化 →
|
|
1694
|
+
// 阶段或产物文件变化 → 选中产物并读取(恢复上次选中的文件;新产物落盘不抢占已选)
|
|
1423
1695
|
React.useEffect(function () {
|
|
1424
1696
|
if (!selStage || !p.runId) return;
|
|
1425
1697
|
const fs = listFiles(selStage);
|
|
1426
1698
|
if (!fs.length) { setSelArt(null); setArtText(null); return; }
|
|
1427
|
-
|
|
1428
|
-
|
|
1699
|
+
const keep = artRef.current && fs.some(function (f) { return f.path === artRef.current; });
|
|
1700
|
+
const pick = keep ? artRef.current : fs[0].path;
|
|
1701
|
+
setSelArt(pick);
|
|
1702
|
+
fetchArt(pick);
|
|
1429
1703
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1430
1704
|
}, [selStage, filesKey, p.runId]);
|
|
1431
1705
|
|
|
@@ -1554,7 +1828,8 @@ body.i2p-dragging{user-select:none}
|
|
|
1554
1828
|
r.id + "(" + c[1] + " · " + kindLabel(r.trigger && r.trigger.kind) + ")");
|
|
1555
1829
|
});
|
|
1556
1830
|
if (!options.length) {
|
|
1557
|
-
options.push(h("option", { key: "__none__", value: "" },
|
|
1831
|
+
options.push(h("option", { key: "__none__", value: "" },
|
|
1832
|
+
p.restoring ? "(恢复 Run 列表…)" : "(暂无 Run · 请先在「01 项目」发起)"));
|
|
1558
1833
|
}
|
|
1559
1834
|
|
|
1560
1835
|
const sDef = STAGES.find(function (x) { return x.id === selStage; });
|
|
@@ -1569,7 +1844,8 @@ body.i2p-dragging{user-select:none}
|
|
|
1569
1844
|
const stClaudeRun = selStage === "P6" && stStatus === "running" && !!(ee && ee.status === "running");
|
|
1570
1845
|
|
|
1571
1846
|
let viewHtml = '<span class="c">点击左侧任一阶段查看产物。</span>';
|
|
1572
|
-
if (!
|
|
1847
|
+
if (p.restoring && !p.run) viewHtml = '<span class="c">正在恢复运行状态…</span>';
|
|
1848
|
+
else if (!sDef) viewHtml = '<span class="c">点击左侧任一阶段查看产物。</span>';
|
|
1573
1849
|
else if (artText != null) viewHtml = renderView(artText, selArt);
|
|
1574
1850
|
else if (selArt) viewHtml = '<span class="c">读取中…</span>';
|
|
1575
1851
|
else if (!stStatus || stStatus === "pending") viewHtml = '<span class="c">该阶段尚未运行,暂无产物。</span>';
|
|
@@ -1601,6 +1877,8 @@ body.i2p-dragging{user-select:none}
|
|
|
1601
1877
|
return Object.keys(groups).sort().map(function (g) { return [g, groups[g]]; });
|
|
1602
1878
|
})();
|
|
1603
1879
|
const curProject = p.projects ? p.projects.find(function (x) { return x.slug === p.slug; }) : null;
|
|
1880
|
+
// 恢复期(重挂载后详情未到位):阶段徽章统一占位「…」,不渲染误导性的「未开始」
|
|
1881
|
+
const restoringAll = p.restoring && !p.run;
|
|
1604
1882
|
|
|
1605
1883
|
return h("div", null,
|
|
1606
1884
|
h("div", { className: "run-bar" },
|
|
@@ -1627,19 +1905,27 @@ body.i2p-dragging{user-select:none}
|
|
|
1627
1905
|
" 五个视图(配置可直接调本阶段参数);", h("b", null, "待复核"), " 时在「运行」视图通过或打回;",
|
|
1628
1906
|
h("b", null, "停止"), " = 当前阶段跑完即停;", h("b", null, "重跑"), " = 从所选阶段重新推进;",
|
|
1629
1907
|
h("b", null, "删除"), " = 移除整个 Run 目录。P10 为失败旁路(仅失败时执行)。"),
|
|
1908
|
+
// 失败分析(P10)结果横幅:后端写回 run.failureAnalysis 后台面化,
|
|
1909
|
+
// 失败现场直接给出分类与建议动作,不必再点开 09-failure-analysis.json
|
|
1910
|
+
runStatus === "failed" && p.run && p.run.failureAnalysis ? h("div", { className: "card", style: { margin: "0 0 10px", padding: "10px 14px" } },
|
|
1911
|
+
h("span", { className: "f-label" }, "失败分析(P10 已生成)"),
|
|
1912
|
+
h("div", { style: { marginTop: 6, fontSize: 12.5, lineHeight: 1.7 } },
|
|
1913
|
+
h("span", { className: "tg t-warn" }, p.run.failureAnalysis.category || "未分类"),
|
|
1914
|
+
" ",
|
|
1915
|
+
h("span", null, (p.run.failureAnalysis.detail || "") + "(建议:" + (p.run.failureAnalysis.action || "?") + ")"))) : null,
|
|
1630
1916
|
h("div", { className: "pipe-layout" },
|
|
1631
1917
|
h("div", { className: "steps", role: "list", "aria-label": "流水线阶段" },
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1918
|
+
STAGES.map(function (s) {
|
|
1919
|
+
const cur = p.run && p.run.stages ? p.run.stages[s.id] : null;
|
|
1920
|
+
const status = restoringAll ? "…" : (cur ? cur.status : "pending");
|
|
1921
|
+
// session/claude 模式的 P6 待复核 = 任务包等外部执行(claude 委托成功后显示正常"待复核")
|
|
1922
|
+
const eeRow = p.run ? p.run.externalExec : null;
|
|
1923
|
+
const ext = s.id === "P6" && status === "awaiting_review"
|
|
1924
|
+
&& !!(p.run && (p.run.p6Mode === "session" || p.run.p6Mode === "claude"))
|
|
1925
|
+
&& !(eeRow && eeRow.status === "done");
|
|
1926
|
+
// claude 委托执行中:P6 处于 running,徽标准明示由 claude 执行
|
|
1927
|
+
const claudeRun = s.id === "P6" && status === "running" && !!(eeRow && eeRow.status === "running");
|
|
1928
|
+
const c = restoringAll ? ["t-off", "…"] : claudeRun ? ["t-acc", "claude 执行中"] : ext ? ["t-warn", "等外部执行"] : tag(status);
|
|
1643
1929
|
const dur = cur ? fmtDur(cur.startedAt, cur.finishedAt) : "";
|
|
1644
1930
|
const extra = cur && cur.attempts ? " · 重试" + cur.attempts : "";
|
|
1645
1931
|
const xp = (ext || claudeRun) ? p.run.externalProgress : null;
|
|
@@ -1970,6 +2256,8 @@ body.i2p-dragging{user-select:none}
|
|
|
1970
2256
|
const p = props;
|
|
1971
2257
|
const [edit, setEdit] = React.useState(null);
|
|
1972
2258
|
const [saving, setSaving] = React.useState(false);
|
|
2259
|
+
// P6 委外智能体测试门禁(claude 模式保存前置条件;与项目页绑定卡同一套逻辑)
|
|
2260
|
+
const [p6Gate, setP6Gate] = React.useState(null);
|
|
1973
2261
|
|
|
1974
2262
|
// defaults / 项目变化 → 重建编辑态(生效值 = 用户覆盖 ?? 默认)
|
|
1975
2263
|
React.useEffect(function () {
|
|
@@ -2003,12 +2291,13 @@ body.i2p-dragging{user-select:none}
|
|
|
2003
2291
|
agent: (user.delegate && user.delegate.agent) || "",
|
|
2004
2292
|
brief: (user.delegate && user.delegate.brief) || "",
|
|
2005
2293
|
},
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2294
|
+
testCommand: (pr && pr.testCommand) || "",
|
|
2295
|
+
};
|
|
2296
|
+
});
|
|
2297
|
+
setEdit(next);
|
|
2298
|
+
setP6Gate(null); // 项目/默认值变化 → 绑定路径可能变,门禁结果作废待重测
|
|
2299
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2300
|
+
}, [p.defaults, p.projects, p.slug]);
|
|
2012
2301
|
|
|
2013
2302
|
if (!p.defaults || !p.projects) return h("p", { className: "empty-hint" }, "加载中…");
|
|
2014
2303
|
if (!p.slug) {
|
|
@@ -2107,7 +2396,18 @@ body.i2p-dragging{user-select:none}
|
|
|
2107
2396
|
};
|
|
2108
2397
|
};
|
|
2109
2398
|
|
|
2399
|
+
// P6 门禁是否对当前绑定有效(仅 P6 + claude 模式需要;改过路径 → key 失效需重测)
|
|
2400
|
+
const p6GateOk = function () {
|
|
2401
|
+
if (stageId !== "P6" || e.delegate.mode !== "claude") return true;
|
|
2402
|
+
const cur = (e.params.claudeBin || "").trim();
|
|
2403
|
+
return !!(p6Gate && p6Gate.status === "pass" && p6Gate.key === (cur ? "bin:" + cur : "auto"));
|
|
2404
|
+
};
|
|
2405
|
+
|
|
2110
2406
|
const save = function () {
|
|
2407
|
+
if (stageId === "P6" && e.delegate.mode === "claude" && !p6GateOk()) {
|
|
2408
|
+
p.toast("P6 委托 Claude Code:请先通过「委外智能体」测试门禁再保存", "bad");
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2111
2411
|
setSaving(true);
|
|
2112
2412
|
apiPost("/projects", collect()).then(function (r) {
|
|
2113
2413
|
setSaving(false);
|
|
@@ -2139,6 +2439,8 @@ body.i2p-dragging{user-select:none}
|
|
|
2139
2439
|
|
|
2140
2440
|
const promptKeys = Object.keys((dDef && dDef.prompts) || {});
|
|
2141
2441
|
const paramDefs = (dDef && dDef.params) || {};
|
|
2442
|
+
// P6 的 claudeBin 由上方「委外智能体」绑定卡维护(claude 模式时显示),通用参数网格不重复出
|
|
2443
|
+
const paramKeys = Object.keys(paramDefs).filter(function (k) { return !(stageId === "P6" && k === "claudeBin"); });
|
|
2142
2444
|
const roleLabel = { planner: "派单 Planner", coder: "编码 Coder", reviewer: "门控 Reviewer", desc: "PR 说明", gate: "Gate 评测" };
|
|
2143
2445
|
const effortOpts = [
|
|
2144
2446
|
{ value: "", label: "跟随全局", hint: "默认" },
|
|
@@ -2238,10 +2540,10 @@ body.i2p-dragging{user-select:none}
|
|
|
2238
2540
|
onChange: function (ev) { setSt({ testCommand: ev.target.value }); },
|
|
2239
2541
|
})) : null) : null,
|
|
2240
2542
|
// —— 阶段专属参数(本阶段工具行为:扫描上限/深读数/claude 路径等;开源场景不藏在代码里) ——
|
|
2241
|
-
|
|
2543
|
+
paramKeys.length ? h("div", { className: "field" },
|
|
2242
2544
|
h("span", { className: "f-label" }, "阶段专属参数(本阶段工具行为 · 留空 = 默认值)"),
|
|
2243
2545
|
h("div", { className: "field-row" },
|
|
2244
|
-
|
|
2546
|
+
paramKeys.map(function (k) {
|
|
2245
2547
|
const meta = paramDefs[k];
|
|
2246
2548
|
return h("div", { key: k, className: "field", style: { marginBottom: 0 } },
|
|
2247
2549
|
h("label", { className: "f-label", htmlFor: "cfg-p-" + stageId + "-" + k },
|
|
@@ -2260,7 +2562,7 @@ body.i2p-dragging{user-select:none}
|
|
|
2260
2562
|
// —— 委托外部智能体 ——
|
|
2261
2563
|
caps.delegate ? (function () {
|
|
2262
2564
|
if (stageId === "P6") {
|
|
2263
|
-
// P6:执行模式与项目页同源(builtin/session/claude
|
|
2565
|
+
// P6:执行模式与项目页同源(builtin/session/claude);claude 模式出绑定卡(发现 + 测试门禁)
|
|
2264
2566
|
return h("div", { className: "field" },
|
|
2265
2567
|
h("span", { className: "f-label", id: "cfg-p6-label" }, "执行模式 · 委托外部智能体"),
|
|
2266
2568
|
radioGroup("cfg-p6-label", [
|
|
@@ -2268,6 +2570,11 @@ body.i2p-dragging{user-select:none}
|
|
|
2268
2570
|
{ value: "session", label: "交给外部会话", hint: "生成任务包,人工交接" },
|
|
2269
2571
|
{ value: "claude", label: "委托 Claude Code", hint: "claude CLI 自动执行" },
|
|
2270
2572
|
], e.delegate.mode, function (v) { setDelegate({ mode: v }); }, "p6mode"),
|
|
2573
|
+
e.delegate.mode === "claude" ? h(AgentBindCard, {
|
|
2574
|
+
bin: e.params.claudeBin, gate: p6Gate,
|
|
2575
|
+
onBinChange: function (v) { setParam("claudeBin", v); },
|
|
2576
|
+
onGate: setP6Gate, toast: p.toast, slug: p.slug,
|
|
2577
|
+
}) : null,
|
|
2271
2578
|
h("p", { className: "hint-line", style: { margin: "8px 0 0" } },
|
|
2272
2579
|
"与「项目」页 P6 执行模式为同一配置(p6Mode);claude 模式任务包自动执行,产出补丁后回复核门。"));
|
|
2273
2580
|
}
|
|
@@ -2315,7 +2622,12 @@ body.i2p-dragging{user-select:none}
|
|
|
2315
2622
|
"每条 patch 可在「运行」页 P7 详情中逐条回滚。"))
|
|
2316
2623
|
: null,
|
|
2317
2624
|
h("div", { className: "cfg-foot" },
|
|
2318
|
-
h("button", {
|
|
2625
|
+
h("button", {
|
|
2626
|
+
type: "button", className: "btn pri",
|
|
2627
|
+
disabled: saving || !p6GateOk(),
|
|
2628
|
+
title: !p6GateOk() ? "P6 委托 Claude Code 需先通过委外智能体测试门禁" : "",
|
|
2629
|
+
onClick: save,
|
|
2630
|
+
},
|
|
2319
2631
|
Ic("check"), saving ? " 保存中…" : " 保存阶段配置"),
|
|
2320
2632
|
h("span", { className: "hint-line", style: { margin: 0 } },
|
|
2321
2633
|
"写入 …\\issue2pr\\projects\\" + p.slug + "\\project.json 的 stageConfig 字段")));
|
|
@@ -2415,7 +2727,14 @@ body.i2p-dragging{user-select:none}
|
|
|
2415
2727
|
* 主 Section:左侧导航 + 共享数据 + 3s 轮询
|
|
2416
2728
|
* ================================================================ */
|
|
2417
2729
|
function Section() {
|
|
2418
|
-
|
|
2730
|
+
// 一级导航记忆:宿主标签切换销毁重建 webview 后回到上次页面(而不是掉回「项目」)
|
|
2731
|
+
const [nav, setNav] = React.useState(function () {
|
|
2732
|
+
const v = localStorage.getItem("i2p.nav");
|
|
2733
|
+
return ["projects", "runs", "artifacts", "config", "guide"].indexOf(v) >= 0 ? v : "projects";
|
|
2734
|
+
});
|
|
2735
|
+
React.useEffect(function () {
|
|
2736
|
+
try { localStorage.setItem("i2p.nav", nav); } catch (e) { /* 忽略 */ }
|
|
2737
|
+
}, [nav]);
|
|
2419
2738
|
const [toast, setToast] = React.useState(null);
|
|
2420
2739
|
const [projects, setProjects] = React.useState(null);
|
|
2421
2740
|
// 选中记忆:进入时恢复上次选中的项目(无记忆则保持未选中)
|
|
@@ -2488,6 +2807,7 @@ body.i2p-dragging{user-select:none}
|
|
|
2488
2807
|
}
|
|
2489
2808
|
apiGet("/ui-state").then(function (r) {
|
|
2490
2809
|
if (!r || !r.ok) return;
|
|
2810
|
+
lastRunStore.adopt(r.state && r.state.lastRunBySlug); // lastRun 记忆兜底并入(仅补缺)
|
|
2491
2811
|
const lp = r.state && r.state.lastProject;
|
|
2492
2812
|
if (!lp || touchedRef.current || ctxRef.current.slug != null) return;
|
|
2493
2813
|
setSelSlug(lp);
|
|
@@ -2499,7 +2819,8 @@ body.i2p-dragging{user-select:none}
|
|
|
2499
2819
|
}, []);
|
|
2500
2820
|
React.useEffect(function () {
|
|
2501
2821
|
if (!toast) return;
|
|
2502
|
-
|
|
2822
|
+
// 失败类提示停留更久(长文案需要时间读完)
|
|
2823
|
+
const t = setTimeout(function () { setToast(null); }, toast.kind === "bad" ? 5200 : 2600);
|
|
2503
2824
|
return function () { clearTimeout(t); };
|
|
2504
2825
|
}, [toast]);
|
|
2505
2826
|
|
|
@@ -2518,7 +2839,8 @@ body.i2p-dragging{user-select:none}
|
|
|
2518
2839
|
if (!projects.some(function (x) { return x.slug === selSlug; })) selectSlug(null);
|
|
2519
2840
|
}, [selSlug, projects, selectSlug]);
|
|
2520
2841
|
|
|
2521
|
-
// slug 变化 → 重置 run 选择并加载 runs
|
|
2842
|
+
// slug 变化 → 重置 run 选择并加载 runs 摘要;恢复优先上次选中的 Run
|
|
2843
|
+
// (lastRun 记忆),无记忆/已删除才退回最新 Run
|
|
2522
2844
|
React.useEffect(function () {
|
|
2523
2845
|
setSelRunId(null); setRun(null); setTree(null);
|
|
2524
2846
|
if (!selSlug) { setRuns(null); return; }
|
|
@@ -2529,7 +2851,11 @@ body.i2p-dragging{user-select:none}
|
|
|
2529
2851
|
if (ctxRef.current.slug !== slug) return;
|
|
2530
2852
|
setRuns(r.runs || []);
|
|
2531
2853
|
setSelRunId(function (prev) {
|
|
2532
|
-
|
|
2854
|
+
if (prev) return prev;
|
|
2855
|
+
const list = r.runs || [];
|
|
2856
|
+
if (!list.length) return null;
|
|
2857
|
+
const remembered = lastRunStore.get(slug);
|
|
2858
|
+
return list.some(function (x) { return x.id === remembered; }) ? remembered : list[0].id;
|
|
2533
2859
|
});
|
|
2534
2860
|
});
|
|
2535
2861
|
}, [selSlug]);
|
|
@@ -2587,6 +2913,18 @@ body.i2p-dragging{user-select:none}
|
|
|
2587
2913
|
}
|
|
2588
2914
|
}, [selSlug, selRunId]);
|
|
2589
2915
|
|
|
2916
|
+
// 回到前台立即刷新(不等 3s 轮询):宿主标签切回 / 窗口聚焦即恢复现场。
|
|
2917
|
+
// 嵌入式 IAB 里 visibilitychange 与 focus 都可能不触发,两个都挂做双保险。
|
|
2918
|
+
React.useEffect(function () {
|
|
2919
|
+
const onVis = function () { if (document.visibilityState === "visible") refreshNow(); };
|
|
2920
|
+
document.addEventListener("visibilitychange", onVis);
|
|
2921
|
+
window.addEventListener("focus", refreshNow);
|
|
2922
|
+
return function () {
|
|
2923
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
2924
|
+
window.removeEventListener("focus", refreshNow);
|
|
2925
|
+
};
|
|
2926
|
+
}, [refreshNow]);
|
|
2927
|
+
|
|
2590
2928
|
const onSaved = React.useCallback(function (slug) {
|
|
2591
2929
|
selectSlug(slug);
|
|
2592
2930
|
loadProjects();
|
|
@@ -2596,6 +2934,7 @@ body.i2p-dragging{user-select:none}
|
|
|
2596
2934
|
const onRunDeleted = React.useCallback(function () {
|
|
2597
2935
|
setSelRunId(null); setRun(null); setTree(null);
|
|
2598
2936
|
const slug = selSlug;
|
|
2937
|
+
lastRunStore.set(slug, null); // 删除后清掉该项目 lastRun 记忆(恢复时退回最新 Run)
|
|
2599
2938
|
if (!slug) return;
|
|
2600
2939
|
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2601
2940
|
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
@@ -2603,19 +2942,52 @@ body.i2p-dragging{user-select:none}
|
|
|
2603
2942
|
}, [selSlug]);
|
|
2604
2943
|
|
|
2605
2944
|
const onProjectDeleted = React.useCallback(function () {
|
|
2945
|
+
lastRunStore.set(selSlug, null); // 项目删除后清 lastRun 记忆
|
|
2606
2946
|
selectSlug(null); setRuns(null); setSelRunId(null); setRun(null); setTree(null);
|
|
2607
2947
|
loadProjects();
|
|
2608
|
-
}, [loadProjects, selectSlug]);
|
|
2948
|
+
}, [loadProjects, selectSlug, selSlug]);
|
|
2609
2949
|
|
|
2610
2950
|
const onRunStarted = React.useCallback(function (slug, runId) {
|
|
2611
2951
|
selectSlug(slug);
|
|
2612
2952
|
setSelRunId(runId);
|
|
2953
|
+
lastRunStore.set(slug, runId); // 新发起的 Run 即为现场:重挂载恢复时优先回到它
|
|
2613
2954
|
setNav("runs");
|
|
2614
2955
|
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2615
2956
|
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
2616
2957
|
});
|
|
2617
2958
|
}, [selectSlug]);
|
|
2618
2959
|
|
|
2960
|
+
// 选中 Run 统一入口:写 lastRun 记忆(localStorage + 服务端兜底),重挂载恢复现场
|
|
2961
|
+
const selectRun = React.useCallback(function (id) {
|
|
2962
|
+
setSelRunId(id);
|
|
2963
|
+
if (ctxRef.current.slug) lastRunStore.set(ctxRef.current.slug, id || null);
|
|
2964
|
+
}, []);
|
|
2965
|
+
|
|
2966
|
+
// Run 状态跃迁显式提示:从执行态(running/awaiting_review)离开时 toast,
|
|
2967
|
+
// 切标签/合上工作台期间的后台失败/完成/待复核不再静默(首帧加载不算跃迁;
|
|
2968
|
+
// 按 {id,status} 记忆——切换到另一个 Run 不误报跃迁)
|
|
2969
|
+
const runStatusRef = React.useRef(null);
|
|
2970
|
+
React.useEffect(function () {
|
|
2971
|
+
const cur = run && run.id === selRunId ? { id: run.id, status: run.status } : null;
|
|
2972
|
+
const prev = runStatusRef.current;
|
|
2973
|
+
runStatusRef.current = cur;
|
|
2974
|
+
if (!cur || !prev || prev.id !== cur.id || prev.status === cur.status) return;
|
|
2975
|
+
if (prev.status !== "running" && prev.status !== "awaiting_review") return;
|
|
2976
|
+
const s = cur.status;
|
|
2977
|
+
if (s === "awaiting_review") toastFn("Run " + run.id + " 待复核:" + run.current + " 阶段等您确认", "warn");
|
|
2978
|
+
else if (s === "failed") {
|
|
2979
|
+
const st = run.stages ? run.stages[run.current] : null;
|
|
2980
|
+
toastFn("Run " + run.id + " 已失败(" + run.current + (st && st.error ? ":" + st.error : "") + ")", "bad");
|
|
2981
|
+
}
|
|
2982
|
+
else if (s === "completed") toastFn("Run " + run.id + " 已完成——全部阶段通过");
|
|
2983
|
+
else if (s === "stopped") toastFn("Run " + run.id + " 已停止", "warn");
|
|
2984
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2985
|
+
}, [run]);
|
|
2986
|
+
|
|
2987
|
+
// 恢复期(重挂载后 runs 列表/详情未到位):渲染「恢复中」而非误导性的「未开始」
|
|
2988
|
+
const restoring = !!selSlug && (runs == null
|
|
2989
|
+
|| (!!selRunId && run == null && !!(runs || []).some(function (x) { return x.id === selRunId; })));
|
|
2990
|
+
|
|
2619
2991
|
const awaitingCount = (runs || []).filter(function (r) { return r.status === "awaiting_review"; }).length;
|
|
2620
2992
|
|
|
2621
2993
|
const navDefs = [
|
|
@@ -2737,7 +3109,8 @@ body.i2p-dragging{user-select:none}
|
|
|
2737
3109
|
projects: projects,
|
|
2738
3110
|
defaults: stageDefaults,
|
|
2739
3111
|
toast: toastFn,
|
|
2740
|
-
|
|
3112
|
+
restoring: restoring,
|
|
3113
|
+
onPickRun: selectRun,
|
|
2741
3114
|
onChanged: refreshNow,
|
|
2742
3115
|
onDeleted: onRunDeleted,
|
|
2743
3116
|
onSaved: onSaved,
|
|
@@ -2807,29 +3180,22 @@ body.i2p-dragging{user-select:none}
|
|
|
2807
3180
|
const r2 = r != null ? r
|
|
2808
3181
|
: (oldX != null ? Math.max(4, Math.round(p.w - oldX - w2)) : AI_ANCHOR.right);
|
|
2809
3182
|
const t2 = oldT != null ? oldT : AI_ANCHOR.top;
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
};
|
|
2816
|
-
})(),
|
|
3183
|
+
// 恢复的矩形可能来自大屏(localStorage 按机器各存各的),先按当前页面收敛再启用
|
|
3184
|
+
return aiClampRect(
|
|
3185
|
+
Math.round(p.w - r2 - w2), t2, w2,
|
|
3186
|
+
h != null ? h : Math.max(200, Math.round(p.h - 16 - t2)));
|
|
3187
|
+
})(),
|
|
2817
3188
|
listeners: new Set(),
|
|
2818
|
-
set(v) { this.open = v; for (const fn of this.listeners) fn(); },
|
|
3189
|
+
set(v) { this.open = v; if (v) this.normalize(); for (const fn of this.listeners) fn(); },
|
|
2819
3190
|
// setRect:先 clamp 尺寸再 clamp 位置(面板始终完整落在工作台内);
|
|
2820
3191
|
// resize 的锚定边由调用方以推导后的 x/y 一并传入(西/北边拖动时对边固定)
|
|
2821
3192
|
setRect(patch, persist) {
|
|
2822
|
-
const p = aiPageRect();
|
|
2823
3193
|
const cur = this.rect;
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
let y = patch.y != null ? Math.round(patch.y) : cur.y;
|
|
2830
|
-
x = Math.max(4, Math.min(Math.max(4, p.w - w - 4), x));
|
|
2831
|
-
y = Math.max(44, Math.min(Math.max(44, p.h - h - 8), y));
|
|
2832
|
-
this.rect = { x: x, y: y, w: w, h: h };
|
|
3194
|
+
this.rect = aiClampRect(
|
|
3195
|
+
patch.x != null ? Math.round(patch.x) : cur.x,
|
|
3196
|
+
patch.y != null ? Math.round(patch.y) : cur.y,
|
|
3197
|
+
patch.w != null ? Math.round(patch.w) : cur.w,
|
|
3198
|
+
patch.h != null ? Math.round(patch.h) : cur.h);
|
|
2833
3199
|
if (persist) {
|
|
2834
3200
|
// 右上角相对坐标落盘:aiR=右缘偏移、aiT=顶部偏移;旧绝对键清除防干扰
|
|
2835
3201
|
try {
|
|
@@ -2850,6 +3216,15 @@ body.i2p-dragging{user-select:none}
|
|
|
2850
3216
|
for (const fn of this.listeners) fn();
|
|
2851
3217
|
},
|
|
2852
3218
|
resetRect() { this.setRect(aiDefaultRect(), true); },
|
|
3219
|
+
// 收敛当前矩形到页面内(不落盘,用户偏好不动):打开面板与页面几何变化时调用,
|
|
3220
|
+
// 修掉"大屏存下的尺寸到小屏溢出屏幕";矩形没变化时不触发监听
|
|
3221
|
+
normalize() {
|
|
3222
|
+
const cur = this.rect;
|
|
3223
|
+
const n = aiClampRect(cur.x, cur.y, cur.w, cur.h);
|
|
3224
|
+
if (n.x === cur.x && n.y === cur.y && n.w === cur.w && n.h === cur.h) return;
|
|
3225
|
+
this.rect = n;
|
|
3226
|
+
for (const fn of this.listeners) fn();
|
|
3227
|
+
},
|
|
2853
3228
|
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
|
|
2854
3229
|
};
|
|
2855
3230
|
function useAiState() {
|
|
@@ -2870,6 +3245,16 @@ body.i2p-dragging{user-select:none}
|
|
|
2870
3245
|
const r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
2871
3246
|
return r ? { w: r.width, h: r.height } : { w: window.innerWidth || 1280, h: window.innerHeight || 720 };
|
|
2872
3247
|
}
|
|
3248
|
+
// 矩形收敛(与 setRect 同边界):尺寸先 clamp(240..760/页宽-24、200..页高-60),
|
|
3249
|
+
// 位置后 clamp(面板完整落在工作台内)——恢复路径/打开面板/页面几何变化共用
|
|
3250
|
+
function aiClampRect(x, y, w, h) {
|
|
3251
|
+
const p = aiPageRect();
|
|
3252
|
+
w = Math.max(240, Math.min(Math.max(240, Math.min(760, p.w - 24)), w));
|
|
3253
|
+
h = Math.max(200, Math.min(Math.max(200, p.h - 60), h));
|
|
3254
|
+
x = Math.max(4, Math.min(Math.max(4, p.w - w - 4), x));
|
|
3255
|
+
y = Math.max(44, Math.min(Math.max(44, p.h - h - 8), y));
|
|
3256
|
+
return { x: x, y: y, w: w, h: h };
|
|
3257
|
+
}
|
|
2873
3258
|
// 默认窗口:右上角按 AI_ANCHOR 固定偏移锚定(窗口尺寸变化时相对位置不变)
|
|
2874
3259
|
function aiDefaultRect() {
|
|
2875
3260
|
const p = aiPageRect();
|
|
@@ -3184,6 +3569,11 @@ body.i2p-dragging{user-select:none}
|
|
|
3184
3569
|
const timer = setInterval(tick, 300);
|
|
3185
3570
|
return () => clearInterval(timer);
|
|
3186
3571
|
}, [open]);
|
|
3572
|
+
// 页面几何变化(宿主布局/窗口缩放,由上方 300ms 轮询驱动 rect)后把助手面板
|
|
3573
|
+
// 收敛回页面内;effect 在 DOM 提交后运行,aiPageRect 读到的已是新尺寸
|
|
3574
|
+
React.useEffect(function () {
|
|
3575
|
+
if (aiStore.open) aiStore.normalize();
|
|
3576
|
+
}, [rect]);
|
|
3187
3577
|
if (!open || rect === null) return null;
|
|
3188
3578
|
return h("div", {
|
|
3189
3579
|
className: "i2p-page", role: "region", "aria-label": "Issue2PR 工作台",
|