dsh-issue2pr 0.1.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/LICENSE +21 -0
- package/README.md +291 -0
- package/client.js +3227 -0
- package/cordis.patch.yml +4 -0
- package/docs/assets/banner.svg +59 -0
- package/docs/assets/pipeline.svg +175 -0
- package/index.js +658 -0
- package/lib/assistant.js +137 -0
- package/lib/connections.js +114 -0
- package/lib/llm.js +159 -0
- package/lib/pipeline.js +151 -0
- package/lib/stageConfig.js +313 -0
- package/lib/stages/helpers.js +127 -0
- package/lib/stages/index.js +16 -0
- package/lib/stages/p1-issue-analyzer.js +18 -0
- package/lib/stages/p10-failure.js +21 -0
- package/lib/stages/p11-pr-builder.js +24 -0
- package/lib/stages/p2-search.js +18 -0
- package/lib/stages/p3-code-understanding.js +24 -0
- package/lib/stages/p4-hypothesis.js +17 -0
- package/lib/stages/p5-planner.js +17 -0
- package/lib/stages/p6-coder.js +226 -0
- package/lib/stages/p7-patch.js +73 -0
- package/lib/stages/p8-test-runner.js +44 -0
- package/lib/stages/p9-reviewer.js +41 -0
- package/lib/store.js +153 -0
- package/package.json +30 -0
package/client.js
ADDED
|
@@ -0,0 +1,3227 @@
|
|
|
1
|
+
// client.js — Issue2PR 工作台(项目 / 运行 / 产物 / 说明)
|
|
2
|
+
// 设计语言:原生 DSH 工具风——配色/交互令牌取宿主 --dsw-alias-*(自动适配明暗主题),
|
|
3
|
+
// 数据(id/路径/产物/耗时)用 JetBrains Mono,图标全 SVG 描边(无 emoji),
|
|
4
|
+
// 布局靠左、密度紧凑、每个区块自带一行中文说明。数据源 /issue2pr/api/*。
|
|
5
|
+
// 零 npm 依赖:React 与官方 UI 原语(MarkdownText 等)均来自宿主模块表(require),样式注入单个 <style> 标签。
|
|
6
|
+
window.__ModuleLoader__.load({
|
|
7
|
+
id: "dsh-issue2pr",
|
|
8
|
+
factory: (require) => {
|
|
9
|
+
var module = { exports: {} };
|
|
10
|
+
var exports = module.exports;
|
|
11
|
+
const React = require("react");
|
|
12
|
+
const { MarkdownText } = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
13
|
+
const h = React.createElement;
|
|
14
|
+
const zh = { nav: "Issue2PR" };
|
|
15
|
+
const en = { nav: "Issue2PR" };
|
|
16
|
+
const API = "/issue2pr/api";
|
|
17
|
+
|
|
18
|
+
// ---------- 常量:11 阶段(与 lib/pipeline.js STAGES 对齐;P10 为失败旁路,不在 MAIN_FLOW) ----------
|
|
19
|
+
// about:阶段职责说明(「运行」页阶段详情·说明 tab 展示;输入 → 做什么 → 输出 → 何时介入)
|
|
20
|
+
const STAGES = [
|
|
21
|
+
{ id: "P1", name: "IssueAnalyzer", desc: "Issue → 结构化契约", art: "01-issue-analysis.json", key: false,
|
|
22
|
+
about: "把触发源原文(需求文档或 Issue)提炼为机器可读的结构化契约:现象、触发条件、影响模块、可验证成功标准、约束与风险等级。后续所有阶段都以该契约为源头,跑偏多因 P1 契约含糊——打回时优先补「成功标准」。" },
|
|
23
|
+
{ id: "P2", name: "Search Layer", desc: "候选文件 + 证据", art: "02-search-candidates.json", key: false,
|
|
24
|
+
about: "扫描仓库文件清单,结合 P1 契约选出候选修改文件,每条附选择理由(SearchEvidence)与置信度,另列测试候选与待探索项。仓库过大时受「扫描上限」参数约束;候选质量直接决定 P3 的理解深度。" },
|
|
25
|
+
{ id: "P3", name: "Code Understanding", desc: "调用链与修改点", art: "03-code-understanding.md", key: false,
|
|
26
|
+
about: "按 P2 候选置信度取前 N 个文件读真实源码,分析关键函数、调用方与潜在修改点,产出中文理解报告。报告是假设与规划的依据;「深读文件数」「单文件读取上限」两个参数控制上下文规模。" },
|
|
27
|
+
{ id: "P4", name: "Hypothesis", desc: "可验证根因假设", art: "04-hypotheses.json", key: false,
|
|
28
|
+
about: "把 P3 报告转为可验证的根因假设:每条必须携带证据、验证文件与验证方法,禁止「我看着像」式结论。多假设并存时由 P5 规划验证顺序。" },
|
|
29
|
+
{ id: "P5", name: "Planner", desc: "TaskGraph 规划 · 复核门", art: "05-task-graph.json", key: true,
|
|
30
|
+
about: "把修复任务拆成有依赖关系的 TaskGraph:每节点有输入产物、输出产物、成功标准与风险,并指定复核门与 PR 门。这是第一个关键复核门——规划错了后面全白跑,建议人工核对节点拆分与依赖。" },
|
|
31
|
+
{ id: "P6", name: "代码优化", desc: "多智能体协同 · 复核门", art: "06-implementation/", key: true,
|
|
32
|
+
about: "按 TaskGraph 实施代码修改:内置模式为 Planner 派单 → 并行 Coder(TDD/最小 diff)→ Reviewer 门控;也可委托外部会话或 Claude Code CLI(执行模式三选一)。产物为逐节点 unified diff + coder-report,P7 落盘前不碰你的仓库。" },
|
|
33
|
+
{ id: "P7", name: "Patch Pipeline", desc: "版本校验 → 落盘 + ledger", art: "ledger/patch-ledger.jsonl", key: false,
|
|
34
|
+
about: "确定性 patch 应用管线(不调用大模型):校验基线版本 → 逐条 git apply 落盘 → 写 patch ledger。每条 patch 可在运行页逐条回滚;基线不匹配会拒绝应用,防止盲覆盖。" },
|
|
35
|
+
{ id: "P8", name: "TestRunner", desc: "沙箱真实执行", art: "07-test-report.json", key: false,
|
|
36
|
+
about: "在仓库真实执行测试命令(项目配置的 testCommand 或自动探测 npm test),完整输出落盘 08-test-output.txt,报告只留末尾。结果必须来自真实执行——这是流水线的铁律。" },
|
|
37
|
+
{ id: "P9", name: "Reviewer", desc: "三维门控审查 · 复核门", art: "08-review-report.json", key: true,
|
|
38
|
+
about: "独立 Reviewer 读真实 diff 与测试报告做三维门控:①Diff 范围(过大/越权/遗漏调用方)②API 与安全 ③测试补强与说明忠实。测试通过 ≠ 可合并;verdict=fail 会打回 P6。" },
|
|
39
|
+
{ id: "P10", name: "FailureClassifier", desc: "失败旁路 · 仅失败时执行", art: "09-failure-analysis.json", key: false, bypass: true,
|
|
40
|
+
about: "失败旁路(仅在任一阶段失败时执行):把失败归入六类——实现错误/根因错误/测试选择/环境缺失/权限被拒/反复失败,并给出处理路径(replan/rollback/escalate)。分类决定重跑策略,避免盲目重试。" },
|
|
41
|
+
{ id: "P11", name: "PRBuilder + Eval", desc: "PR 说明 + Gate 评测 · 复核门", art: "10-pr-description.md", key: true,
|
|
42
|
+
about: "双角色收尾:PR 说明忠实反映修改与验证过程(背景/根因/修改点/验证证据/风险,禁止夸大);Gate 按六项判定——ROOT 根因有证据 / PATCH 干净应用 / TEST 无回归 / DIFF 可审查 / DESC 说明忠实 / ACCEPT 门控通过。全部 pass 才算交付。" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// 状态 → tag 样式(映射宿主语义状态色:成功/警告/错误/进行中)
|
|
46
|
+
const TAG = {
|
|
47
|
+
approved: ["t-good", "已通过"],
|
|
48
|
+
awaiting_review: ["t-warn", "待复核"],
|
|
49
|
+
running: ["t-acc", "运行中"],
|
|
50
|
+
failed: ["t-err", "失败"],
|
|
51
|
+
stopped: ["t-off", "已停止"],
|
|
52
|
+
pending: ["t-off", "未开始"],
|
|
53
|
+
completed: ["t-good", "已完成"],
|
|
54
|
+
};
|
|
55
|
+
function tag(status) { return TAG[status] || TAG.pending; }
|
|
56
|
+
|
|
57
|
+
/* ================================================================
|
|
58
|
+
* 样式(全部选择器 .i2p 前缀;令牌取宿主 --dsw-alias-*,带回退值)
|
|
59
|
+
* ================================================================ */
|
|
60
|
+
const css = `
|
|
61
|
+
/* 别名变量同时挂在 .i2p-page 上:AssistantDock(悬浮助手)挂在 .i2p-page 下、
|
|
62
|
+
在 Section 的 .i2p 容器外,变量只定义在 .i2p 会整体解析失败(深色主题下全透明) */
|
|
63
|
+
.i2p,.i2p-page{
|
|
64
|
+
--ink:var(--dsw-alias-label-primary,#17181f);
|
|
65
|
+
--ink-2:var(--dsw-alias-label-secondary,#414351);
|
|
66
|
+
--muted:var(--dsw-alias-label-tertiary,#82848f);
|
|
67
|
+
--caption:var(--dsw-alias-label-caption,#9b9da8);
|
|
68
|
+
--line:var(--dsw-alias-border-l1,#e5e6ec);
|
|
69
|
+
--line-2:var(--dsw-alias-border-l2,#d8dae3);
|
|
70
|
+
--card:var(--dsw-alias-bg-base,#ffffff);
|
|
71
|
+
--panel:var(--dsw-alias-bg-layer-2,#ffffff);
|
|
72
|
+
--accent:var(--dsw-alias-state-business-primary,#2a5ddc);
|
|
73
|
+
--hover:var(--dsw-alias-interactive-bg-hover,rgba(9,12,20,.055));
|
|
74
|
+
--nav-on:var(--dsw-specific-sidebar-nav-item-active,var(--dsw-alias-interactive-bg-hover,rgba(9,12,20,.09)));
|
|
75
|
+
--good:var(--dsw-alias-state-success-primary,#1c7c43);
|
|
76
|
+
--good-bg:var(--dsw-alias-state-success-tertiary,rgba(28,124,67,.12));
|
|
77
|
+
--warn:var(--dsw-alias-state-warn-label,#9a6700);
|
|
78
|
+
--warn-bg:var(--dsw-alias-state-warn-tertiary,rgba(154,103,0,.12));
|
|
79
|
+
--err:var(--dsw-alias-state-error-primary,#d5463a);
|
|
80
|
+
--err-bg:rgba(213,70,58,.10);
|
|
81
|
+
--code-bg:#10141d; --code-line:#1d2330;
|
|
82
|
+
--mono:'JetBrains Mono','IBM Plex Mono','Cascadia Code',Consolas,'Liberation Mono',monospace;
|
|
83
|
+
color:var(--ink); font-size:14.5px; line-height:1.7;
|
|
84
|
+
flex:1; min-height:0; display:flex; flex-direction:column;
|
|
85
|
+
}
|
|
86
|
+
.i2p *{box-sizing:border-box}
|
|
87
|
+
.i2p button{font-family:inherit;cursor:pointer;color:inherit}
|
|
88
|
+
.i2p button:focus-visible,.i2p input:focus-visible,.i2p textarea:focus-visible,.i2p select:focus-visible{
|
|
89
|
+
outline:2px solid var(--accent);outline-offset:1px;
|
|
90
|
+
}
|
|
91
|
+
.i2p pre code,.i2p pre{font-family:var(--mono)}
|
|
92
|
+
|
|
93
|
+
/* ===== 主体:左侧导航列 + 内容区(一律靠左) ===== */
|
|
94
|
+
.i2p-body{flex:1;min-height:0;display:flex;align-items:stretch}
|
|
95
|
+
.i2p-nav{flex:none;width:var(--i2p-nav-w,240px);border-right:1px solid var(--line);padding:18px 14px 18px 14px;
|
|
96
|
+
display:flex;flex-direction:column;gap:16px;position:relative;overflow:hidden;
|
|
97
|
+
transition:width .2s cubic-bezier(.2,.8,.2,1)}
|
|
98
|
+
.i2p-nav.no-anim{transition:none}
|
|
99
|
+
.i2p-nav-title{display:flex;align-items:center;gap:8px;min-width:0;padding:0 12px}
|
|
100
|
+
.i2p-nav-title .tt{flex:none;font-size:16px;font-weight:600;color:var(--ink)}
|
|
101
|
+
.i2p-nav-list{display:flex;flex-direction:column;gap:2px;list-style:none;padding:0;margin:0}
|
|
102
|
+
.i2p-nav-cell{display:flex;align-items:center;gap:8px;height:38px;padding:0 12px;border:none;
|
|
103
|
+
background:none;border-radius:10px;font-size:14.5px;color:var(--ink-2);text-align:left;width:100%;
|
|
104
|
+
position:relative;cursor:pointer;
|
|
105
|
+
animation:i2p-cell-in .3s cubic-bezier(.2,.8,.2,1) both;animation-delay:calc(var(--i,0)*45ms);
|
|
106
|
+
transition:background .14s,color .14s}
|
|
107
|
+
.i2p-nav-cell svg{flex:none}
|
|
108
|
+
.i2p-nav-cell::before{content:"";position:absolute;left:0;top:26%;bottom:26%;width:2.5px;border-radius:3px;
|
|
109
|
+
background:var(--accent);transform:scaleY(0);transform-origin:center;
|
|
110
|
+
transition:transform .18s cubic-bezier(.2,.8,.2,1)}
|
|
111
|
+
.i2p-nav-cell:hover{background:var(--hover)}
|
|
112
|
+
.i2p-nav-cell.on{background:var(--nav-on);color:var(--ink);font-weight:600}
|
|
113
|
+
.i2p-nav-cell.on::before{transform:scaleY(1)}
|
|
114
|
+
.i2p-nav-cell .cnt{margin-left:auto;font-family:var(--mono);font-size:11.5px;color:var(--muted);transition:color .14s}
|
|
115
|
+
.i2p-nav-cell .cnt.alert{color:var(--warn);font-weight:600;animation:i2p-breathe 1.8s ease-in-out infinite}
|
|
116
|
+
.i2p-nav-note{margin-top:auto;padding:0 12px;font-size:12.5px;color:var(--muted);line-height:1.6}
|
|
117
|
+
/* 拖宽手柄:贴 nav 右缘内侧(勿伸出——nav overflow:hidden 会裁掉导致点不到),
|
|
118
|
+
hover 显色;双击恢复默认宽 */
|
|
119
|
+
.i2p-nav-grip{position:absolute;top:0;bottom:0;right:0;width:8px;z-index:5;cursor:col-resize;
|
|
120
|
+
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)}
|
|
122
|
+
body.i2p-dragging{cursor:col-resize}
|
|
123
|
+
body.i2p-dragging *{cursor:col-resize!important}
|
|
124
|
+
body.i2p-dragging{user-select:none}
|
|
125
|
+
/* 收起态(激活「收起侧边栏」)= 图标栏 rail:菜单项仅图标,标题文字/计数/说明/拖宽隐藏 */
|
|
126
|
+
.i2p-nav.rail{width:52px;padding-left:8px;padding-right:8px}
|
|
127
|
+
.i2p-nav.rail .i2p-nav-title{padding:0;justify-content:center}
|
|
128
|
+
.i2p-nav.rail .i2p-nav-title .tt{display:none}
|
|
129
|
+
.i2p-nav.rail .i2p-nav-cell{justify-content:center;padding:0;gap:0}
|
|
130
|
+
.i2p-nav.rail .i2p-nav-cell .lb,.i2p-nav.rail .i2p-nav-cell .cnt{display:none}
|
|
131
|
+
.i2p-nav.rail .i2p-nav-note{display:none}
|
|
132
|
+
.i2p-nav.rail .i2p-nav-grip{display:none}
|
|
133
|
+
.i2p-main{flex:1;min-width:0;overflow-y:auto;padding:24px 32px 48px}
|
|
134
|
+
@media (max-width:880px){
|
|
135
|
+
.i2p-body{flex-direction:column}
|
|
136
|
+
.i2p-nav{width:auto;flex-direction:row;flex-wrap:wrap;align-items:center;gap:8px;
|
|
137
|
+
border-right:none;border-bottom:1px solid var(--line);padding:10px 14px}
|
|
138
|
+
.i2p-nav-grip{display:none}
|
|
139
|
+
.i2p-nav.rail{width:auto;padding-left:14px;padding-right:14px}
|
|
140
|
+
.i2p-nav-list{flex-direction:row;flex-wrap:wrap}
|
|
141
|
+
.i2p-nav-note{display:none}
|
|
142
|
+
.i2p-main{padding:16px 16px 32px}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ===== 区块标题(工具式:标题 + 一行说明) ===== */
|
|
146
|
+
.i2p .sec{margin-bottom:6px}
|
|
147
|
+
.i2p .sec h2{font-size:17px;font-weight:600;margin:0 0 2px;color:var(--ink)}
|
|
148
|
+
.i2p .sec .sub{margin:0 0 16px;font-size:13.5px;color:var(--muted)}
|
|
149
|
+
.i2p .sec .sub b{color:var(--ink-2);font-weight:600}
|
|
150
|
+
|
|
151
|
+
/* ===== 卡片 / callout ===== */
|
|
152
|
+
.i2p .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
|
|
153
|
+
.i2p .card h4{font-size:13px;text-transform:none;letter-spacing:0;color:var(--muted);
|
|
154
|
+
margin:0 0 10px;font-weight:600}
|
|
155
|
+
.i2p .card p{margin:0 0 8px;font-size:14px}
|
|
156
|
+
.i2p .card ul{margin:6px 0 0;padding-left:18px;font-size:14px}
|
|
157
|
+
.i2p .card ul li{margin-bottom:4px}
|
|
158
|
+
.i2p .callout{border:1px solid var(--line);border-left:3px solid var(--accent);background:var(--card);
|
|
159
|
+
border-radius:0 10px 10px 0;padding:12px 14px}
|
|
160
|
+
.i2p .callout h4{margin:0 0 8px}
|
|
161
|
+
.i2p .callout.acc{border-left-color:var(--accent)}
|
|
162
|
+
.i2p .callout.warn{border-left-color:var(--warn);background:var(--warn-bg);border-color:transparent;border-left-color:var(--warn)}
|
|
163
|
+
.i2p .callout.warn h4{color:var(--warn)}
|
|
164
|
+
.i2p .callout.good{border-left-color:var(--good);background:var(--good-bg);border-color:transparent;border-left-color:var(--good)}
|
|
165
|
+
.i2p .callout.good h4{color:var(--good)}
|
|
166
|
+
.i2p .callout.err{border-left-color:var(--err);background:var(--err-bg);border-color:transparent;border-left-color:var(--err)}
|
|
167
|
+
.i2p .callout.err h4{color:var(--err)}
|
|
168
|
+
|
|
169
|
+
/* ===== 表格 ===== */
|
|
170
|
+
.i2p .tbl{width:100%;border-collapse:collapse;font-size:13.5px;margin:12px 0}
|
|
171
|
+
.i2p .tbl th{border-bottom:1px solid var(--line-2);font-size:12px;color:var(--muted);
|
|
172
|
+
font-weight:600;text-align:left;padding:6px 10px}
|
|
173
|
+
.i2p .tbl td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
174
|
+
.i2p .tbl tr:last-child td{border-bottom:none}
|
|
175
|
+
.i2p .tbl code{font-family:var(--mono);background:var(--hover);padding:1px 6px;border-radius:4px;
|
|
176
|
+
font-size:12px;color:var(--ink-2)}
|
|
177
|
+
|
|
178
|
+
/* ===== 状态 tag ===== */
|
|
179
|
+
.i2p .tg{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11.5px;
|
|
180
|
+
padding:1px 8px;border-radius:5px;letter-spacing:.02em;border:1px solid;white-space:nowrap;vertical-align:middle}
|
|
181
|
+
.i2p .tg.t-good{color:var(--good);border-color:transparent;background:var(--good-bg)}
|
|
182
|
+
.i2p .tg.t-warn{color:var(--warn);border-color:transparent;background:var(--warn-bg)}
|
|
183
|
+
.i2p .tg.t-err{color:var(--err);border-color:transparent;background:var(--err-bg)}
|
|
184
|
+
.i2p .tg.t-acc{color:var(--accent);border-color:var(--accent);background:none}
|
|
185
|
+
.i2p .tg.t-off{color:var(--muted);border-color:var(--line-2);background:none}
|
|
186
|
+
.i2p .tg.run::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;
|
|
187
|
+
background:var(--accent);animation:i2p-pulse 1.1s ease-in-out infinite}
|
|
188
|
+
@keyframes i2p-pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
|
189
|
+
@media (prefers-reduced-motion:reduce){.i2p .tg.run::before{animation:none}}
|
|
190
|
+
|
|
191
|
+
/* ===== 按钮 ===== */
|
|
192
|
+
.i2p .btn{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 12px;border-radius:8px;
|
|
193
|
+
border:1px solid var(--line-2);background:none;color:var(--ink-2);font-size:13.5px;transition:background .15s,border-color .15s}
|
|
194
|
+
.i2p .btn:hover{background:var(--hover);color:var(--ink)}
|
|
195
|
+
.i2p .btn.pri{background:var(--ink);border-color:var(--ink);color:var(--panel);font-weight:600}
|
|
196
|
+
.i2p .btn.pri:hover{opacity:.88;background:var(--ink)}
|
|
197
|
+
.i2p .btn.danger{border-color:transparent;color:var(--err)}
|
|
198
|
+
.i2p .btn.danger:hover{background:var(--err-bg)}
|
|
199
|
+
.i2p .btn:disabled{opacity:.45;cursor:not-allowed}
|
|
200
|
+
.i2p .btn.sm{height:26px;padding:0 9px;font-size:12.5px;gap:5px}
|
|
201
|
+
|
|
202
|
+
/* ===== 表单 ===== */
|
|
203
|
+
.i2p label.f-label,span.f-label{display:block;font-size:12px;color:var(--muted);margin:0 0 5px;font-weight:600}
|
|
204
|
+
.i2p .f-input,.i2p .f-select{width:100%;background:var(--card);border:1px solid var(--line-2);
|
|
205
|
+
border-radius:8px;color:var(--ink);padding:6px 10px;font-size:14px;font-family:inherit;transition:border-color .15s}
|
|
206
|
+
.i2p textarea.f-input{resize:vertical}
|
|
207
|
+
.i2p .f-input.mono,.i2p .f-select.mono{font-family:var(--mono);font-size:13px}
|
|
208
|
+
.i2p .f-input:focus,.i2p .f-select:focus{border-color:var(--accent)}
|
|
209
|
+
.i2p .f-input::placeholder{color:var(--caption)}
|
|
210
|
+
.i2p .field{margin-bottom:13px}
|
|
211
|
+
.i2p .field-row{display:grid;grid-template-columns:1fr 1fr;gap:13px}
|
|
212
|
+
@media (max-width:760px){.i2p .field-row{grid-template-columns:1fr}}
|
|
213
|
+
.i2p .dyn-row{display:flex;gap:6px;margin-bottom:6px;align-items:center}
|
|
214
|
+
.i2p .dyn-row .f-input{flex:1}
|
|
215
|
+
.i2p .dyn-row .rm{flex:none;display:inline-flex;align-items:center;justify-content:center;color:var(--muted);
|
|
216
|
+
border:1px solid var(--line-2);background:none;border-radius:8px;width:30px;height:30px}
|
|
217
|
+
.i2p .dyn-row .rm:hover{color:var(--err);border-color:var(--err)}
|
|
218
|
+
.i2p .add-row{display:inline-flex;align-items:center;gap:5px;background:none;border:1px dashed var(--line-2);
|
|
219
|
+
color:var(--accent);border-radius:8px;padding:4px 11px;font-size:13px;margin-top:2px}
|
|
220
|
+
.i2p .add-row:hover{border-color:var(--accent)}
|
|
221
|
+
.i2p .run-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;
|
|
222
|
+
border-radius:8px;border:1px solid var(--good);color:var(--good);background:none}
|
|
223
|
+
.i2p .run-btn:hover{background:var(--good-bg)}
|
|
224
|
+
.i2p fieldset.trig-src{border:1px solid var(--line);border-radius:10px;padding:12px 14px 14px;margin:0 0 13px}
|
|
225
|
+
.i2p .trig-head{font-size:14px;font-weight:600;color:var(--ink-2);margin:10px 0 7px;
|
|
226
|
+
display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
227
|
+
.i2p .trig-head .hint{font-weight:400;font-size:12.5px;color:var(--muted)}
|
|
228
|
+
.i2p .trig-tag{font-family:var(--mono);font-size:11px;color:var(--accent);border:1px solid var(--accent);
|
|
229
|
+
border-radius:4px;padding:1px 6px;background:none}
|
|
230
|
+
.i2p .trig-tag.issue{color:var(--warn);border-color:var(--warn)}
|
|
231
|
+
|
|
232
|
+
/* 单选组(chip 式) */
|
|
233
|
+
.i2p .radio-group{display:flex;gap:8px;flex-wrap:wrap}
|
|
234
|
+
.i2p .radio-chip{display:flex;align-items:center;gap:7px;border:1px solid var(--line-2);border-radius:8px;
|
|
235
|
+
padding:5px 11px;font-size:13.5px;color:var(--ink-2);cursor:pointer;user-select:none;background:var(--card)}
|
|
236
|
+
.i2p .radio-chip input{accent-color:var(--accent);margin:0}
|
|
237
|
+
.i2p .radio-chip.on{border-color:var(--accent);color:var(--accent)}
|
|
238
|
+
.i2p .radio-chip .hint{font-size:12px;color:var(--muted)}
|
|
239
|
+
|
|
240
|
+
/* ===== 项目列表 ===== */
|
|
241
|
+
.i2p .proj-list{display:flex;flex-direction:column;gap:6px}
|
|
242
|
+
.i2p .proj-item{text-align:left;background:none;border:1px solid var(--line);border-radius:10px;
|
|
243
|
+
padding:9px 12px;color:var(--ink);transition:border-color .15s,background .15s}
|
|
244
|
+
.i2p .proj-item:hover{border-color:var(--line-2);background:var(--hover)}
|
|
245
|
+
.i2p .proj-item.on{border-color:var(--accent)}
|
|
246
|
+
.i2p .proj-item .p-name{font-weight:600;font-size:14px}
|
|
247
|
+
.i2p .proj-item .p-meta{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-top:2px}
|
|
248
|
+
|
|
249
|
+
/* ===== 运行页布局 ===== */
|
|
250
|
+
.i2p .run-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
|
|
251
|
+
.i2p .run-bar .f-select{width:auto;min-width:300px}
|
|
252
|
+
.i2p .pipe-layout{display:grid;grid-template-columns:330px minmax(0,1fr);gap:16px;align-items:start;margin-top:14px}
|
|
253
|
+
@media (max-width:960px){.i2p .pipe-layout{grid-template-columns:1fr}}
|
|
254
|
+
|
|
255
|
+
.i2p .run-hint{font-size:13.5px;color:var(--muted);margin:0 0 4px}
|
|
256
|
+
.i2p .run-hint b{color:var(--ink-2);font-weight:600}
|
|
257
|
+
|
|
258
|
+
/* ===== 阶段时间线(行式列表 + 状态点) ===== */
|
|
259
|
+
.i2p .steps{display:flex;flex-direction:column;gap:1px}
|
|
260
|
+
.i2p .step-row{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;
|
|
261
|
+
width:100%;text-align:left;background:none;border:none;padding:7px 9px;border-radius:10px;transition:background .15s}
|
|
262
|
+
.i2p .step-row:hover{background:var(--hover)}
|
|
263
|
+
.i2p .step-row.on{background:var(--nav-on)}
|
|
264
|
+
.i2p .sdot{width:10px;height:10px;border-radius:50%;border:2px solid var(--line-2);background:none;flex:none}
|
|
265
|
+
.i2p .step-row.approved .sdot{border-color:var(--good);background:var(--good)}
|
|
266
|
+
.i2p .step-row.failed .sdot,.i2p .step-row.stopped .sdot{border-color:var(--err);background:var(--err)}
|
|
267
|
+
.i2p .step-row.awaiting_review .sdot{border-color:var(--warn);background:var(--warn)}
|
|
268
|
+
.i2p .step-row.running .sdot{border-color:var(--accent);background:var(--accent);animation:i2p-pulse 1.1s ease-in-out infinite}
|
|
269
|
+
.i2p .step-row.bypass .sdot{border-style:dashed}
|
|
270
|
+
@media (prefers-reduced-motion:reduce){.i2p .step-row.running .sdot{animation:none}}
|
|
271
|
+
.i2p .s-main{min-width:0;display:flex;flex-direction:column;gap:1px}
|
|
272
|
+
.i2p .s-name{font-size:13.5px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
273
|
+
.i2p .step-row.on .s-name{color:var(--accent)}
|
|
274
|
+
.i2p .s-desc{font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
275
|
+
.i2p .s-side{display:flex;flex-direction:column;align-items:flex-end;gap:3px}
|
|
276
|
+
.i2p .s-dur{font-family:var(--mono);font-size:11px;color:var(--muted)}
|
|
277
|
+
|
|
278
|
+
/* ===== 阶段详情 ===== */
|
|
279
|
+
.i2p .detail-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}
|
|
280
|
+
.i2p .detail-head h3{font-size:15.5px;font-weight:600;margin:0}
|
|
281
|
+
.i2p .detail-head .path{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-top:3px;word-break:break-all}
|
|
282
|
+
.i2p .a-tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
|
|
283
|
+
.i2p .a-tab{background:none;border:1px solid var(--line);color:var(--ink-2);border-radius:6px;
|
|
284
|
+
padding:2px 9px;font-size:12.5px;font-family:var(--mono)}
|
|
285
|
+
.i2p .a-tab.on{border-color:var(--accent);color:var(--accent)}
|
|
286
|
+
|
|
287
|
+
/* ===== 代码 / 产物预览(固定深底,等价嵌入式终端) ===== */
|
|
288
|
+
.i2p pre.view{background:var(--code-bg);color:#e8e6df;padding:13px 15px;border-radius:10px;overflow-x:auto;
|
|
289
|
+
margin:0;font-family:var(--mono);font-size:13px;line-height:1.6;max-height:420px;overflow-y:auto;
|
|
290
|
+
border:1px solid var(--code-line)}
|
|
291
|
+
.i2p pre.view .k{color:#c2a26f}
|
|
292
|
+
.i2p pre.view .s{color:#9bc28b}
|
|
293
|
+
.i2p pre.view .c{color:#7a7785;font-style:italic}
|
|
294
|
+
.i2p pre.view .hunk{color:#d9a55b}
|
|
295
|
+
.i2p pre.view .add{color:#9bc28b;background:rgba(155,194,139,.13);display:inline-block;width:100%}
|
|
296
|
+
.i2p pre.view .del{color:#e0a49c;background:rgba(213,70,58,.18);display:inline-block;width:100%}
|
|
297
|
+
|
|
298
|
+
/* .md 产物走宿主官方 MarkdownText(全套样式在宿主全局样式表,与聊天区 1:1);此处只做滚动容器 */
|
|
299
|
+
.i2p .md-view{max-height:420px;overflow-y:auto;padding:2px}
|
|
300
|
+
|
|
301
|
+
/* ===== 复核门 / 复核历史 ===== */
|
|
302
|
+
.i2p .review-actions{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-top:4px}
|
|
303
|
+
.i2p .review-actions textarea{flex:1;min-width:220px;min-height:60px}
|
|
304
|
+
.i2p .gate-note{font-family:var(--mono);font-size:11.5px;color:var(--muted);margin-top:10px}
|
|
305
|
+
.i2p .rev-item{display:grid;grid-template-columns:auto auto 1fr;gap:8px;align-items:baseline;
|
|
306
|
+
padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}
|
|
307
|
+
.i2p .rev-item:last-child{border-bottom:none}
|
|
308
|
+
.i2p .rev-item .at{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap}
|
|
309
|
+
.i2p .rev-item .cm{color:var(--ink-2)}
|
|
310
|
+
|
|
311
|
+
/* ===== 过程事件(LLM / git / 测试 / 工具调用) ===== */
|
|
312
|
+
.i2p .ev-head{display:flex;align-items:baseline;gap:8px;margin-top:14px;flex-wrap:wrap}
|
|
313
|
+
.i2p .ev-head .f-label{margin:0}
|
|
314
|
+
.i2p .ev-head .hint{font-size:12px;color:var(--muted)}
|
|
315
|
+
.i2p .ev-list{border:1px solid var(--line);border-radius:10px;margin-top:6px;max-height:280px;overflow-y:auto}
|
|
316
|
+
.i2p .ev-row{display:grid;grid-template-columns:auto auto minmax(0,1fr) auto auto;gap:8px;align-items:center;
|
|
317
|
+
width:100%;text-align:left;background:none;border:none;border-bottom:1px solid var(--line);padding:6px 10px;font-size:13px}
|
|
318
|
+
.i2p .ev-row:hover{background:var(--hover)}
|
|
319
|
+
.i2p .ev-time{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap}
|
|
320
|
+
.i2p .ev-kind{font-family:var(--mono);font-size:9.5px;padding:1px 6px;border-radius:4px;border:1px solid;white-space:nowrap}
|
|
321
|
+
.i2p .ev-kind.k-llm{color:var(--accent);border-color:var(--accent)}
|
|
322
|
+
.i2p .ev-kind.k-git{color:var(--good);border-color:var(--good)}
|
|
323
|
+
.i2p .ev-kind.k-test{color:var(--warn);border-color:var(--warn)}
|
|
324
|
+
.i2p .ev-kind.k-tool,.i2p .ev-kind.k-stage,.i2p .ev-kind.k-info{color:var(--muted);border-color:var(--line-2)}
|
|
325
|
+
.i2p .ev-name{color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
326
|
+
font-family:var(--mono);font-size:12.5px;text-align:left}
|
|
327
|
+
.i2p .ev-ms{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap}
|
|
328
|
+
.i2p .ev-ok{width:7px;height:7px;border-radius:50%;background:var(--good)}
|
|
329
|
+
.i2p .ev-row.bad .ev-ok{background:var(--err)}
|
|
330
|
+
.i2p .ev-empty{padding:8px 12px;font-size:13px;color:var(--muted)}
|
|
331
|
+
.i2p pre.ev-detail{margin:0;border-top:1px solid var(--line);border-radius:0;max-height:220px;
|
|
332
|
+
white-space:pre-wrap;word-break:break-word}
|
|
333
|
+
|
|
334
|
+
/* ===== P7 ledger ===== */
|
|
335
|
+
.i2p .ledger-row{display:grid;grid-template-columns:auto 1fr auto auto;gap:10px;align-items:center;
|
|
336
|
+
padding:6px 0;border-bottom:1px solid var(--line);font-size:13.5px}
|
|
337
|
+
.i2p .ledger-row:last-child{border-bottom:none}
|
|
338
|
+
.i2p .ledger-row .no{font-family:var(--mono);font-size:11.5px;color:var(--muted)}
|
|
339
|
+
.i2p .ledger-row .pf{font-family:var(--mono);font-size:12.5px;color:var(--ink-2);word-break:break-all}
|
|
340
|
+
.i2p .ledger-row .at{font-family:var(--mono);font-size:11.5px;color:var(--muted);white-space:nowrap}
|
|
341
|
+
|
|
342
|
+
/* ===== 产物 tab ===== */
|
|
343
|
+
.i2p .art-layout{display:grid;grid-template-columns:300px minmax(0,1fr);gap:16px;align-items:start;margin-top:14px}
|
|
344
|
+
@media (max-width:900px){.i2p .art-layout{grid-template-columns:1fr}}
|
|
345
|
+
.i2p .tree{font-family:var(--mono);font-size:14.5px;color:var(--ink-2);overflow-x:auto}
|
|
346
|
+
.i2p .tree .dir{display:flex;width:100%;text-align:left;background:var(--hover);border:none;color:var(--ink);font-weight:600;
|
|
347
|
+
font-family:inherit;font-size:inherit;padding:4px 8px;border-radius:6px;cursor:pointer}
|
|
348
|
+
.i2p .tree .dir:hover{background:var(--nav-on)}
|
|
349
|
+
.i2p .tree .file{display:flex;justify-content:space-between;gap:8px;width:100%;text-align:left;background:none;
|
|
350
|
+
border:none;color:var(--ink-2);padding:4px 8px;border-radius:6px;font-family:inherit;font-size:inherit}
|
|
351
|
+
.i2p .tree .file:hover{background:var(--hover)}
|
|
352
|
+
.i2p .tree .file.on{background:var(--nav-on);color:var(--accent)}
|
|
353
|
+
.i2p .tree .ts{color:var(--muted);font-size:12px;flex:none}
|
|
354
|
+
|
|
355
|
+
/* ===== 说明页 ===== */
|
|
356
|
+
.i2p .guide .lede{font-size:14.5px;color:var(--ink-2);max-width:760px;line-height:1.7;margin:0 0 16px}
|
|
357
|
+
.i2p ol.tight{padding-left:20px;margin:8px 0 16px;font-size:14px}
|
|
358
|
+
.i2p ol.tight li{margin-bottom:5px}
|
|
359
|
+
.i2p ol.tight li b{color:var(--ink-2)}
|
|
360
|
+
.i2p .grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:0 0 12px}
|
|
361
|
+
@media (max-width:760px){.i2p .grid-2{grid-template-columns:1fr}}
|
|
362
|
+
.i2p .hint-line{font-size:13px;color:var(--muted);margin:12px 0 0}
|
|
363
|
+
.i2p .hint-line code{font-family:var(--mono);background:var(--hover);padding:1px 6px;border-radius:4px;font-size:12px;color:var(--ink-2)}
|
|
364
|
+
.i2p .empty-hint{color:var(--muted);font-size:14px;padding:14px 0}
|
|
365
|
+
|
|
366
|
+
/* ===== 阶段详情二级菜单(项目/运行/产物/配置/说明,聚焦当前阶段) ===== */
|
|
367
|
+
.i2p .stg-tabs{display:flex;gap:2px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:2px 0 14px}
|
|
368
|
+
.i2p .stg-tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--ink-2);
|
|
369
|
+
padding:6px 12px 9px;font-size:13.5px;transition:color .14s,border-color .14s;margin-bottom:-1px}
|
|
370
|
+
.i2p .stg-tab:hover{color:var(--ink)}
|
|
371
|
+
.i2p .stg-tab.on{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}
|
|
372
|
+
.i2p .stg-tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
373
|
+
/* 阶段产物 tab:左文件列表(目录分组)+ 右预览 */
|
|
374
|
+
.i2p .stg-art{display:grid;grid-template-columns:280px minmax(0,1fr);gap:12px;align-items:start}
|
|
375
|
+
@media (max-width:760px){.i2p .stg-art{grid-template-columns:1fr}}
|
|
376
|
+
.i2p .stg-fgroup{font-family:var(--mono);font-size:11px;color:var(--muted);padding:8px 8px 3px;
|
|
377
|
+
text-transform:none;letter-spacing:.02em}
|
|
378
|
+
.i2p .stg-fgroup:first-child{padding-top:0}
|
|
379
|
+
.i2p .stg-file{display:flex;justify-content:space-between;gap:8px;width:100%;text-align:left;background:none;
|
|
380
|
+
border:none;color:var(--ink-2);padding:4px 8px;border-radius:6px;font-family:var(--mono);font-size:12.5px}
|
|
381
|
+
.i2p .stg-file:hover{background:var(--hover)}
|
|
382
|
+
.i2p .stg-file.on{background:var(--nav-on);color:var(--accent)}
|
|
383
|
+
.i2p .stg-file .ts{color:var(--muted);font-size:11px;flex:none}
|
|
384
|
+
/* 阶段项目 tab:键值信息行 */
|
|
385
|
+
.i2p .kv{display:grid;grid-template-columns:150px minmax(0,1fr);gap:7px 14px;font-size:13.5px}
|
|
386
|
+
@media (max-width:600px){.i2p .kv{grid-template-columns:1fr}}
|
|
387
|
+
.i2p .kv .k{color:var(--muted)}
|
|
388
|
+
.i2p .kv .v{color:var(--ink);min-width:0;word-break:break-all}
|
|
389
|
+
.i2p .kv .v.mono{font-family:var(--mono);font-size:12.5px}
|
|
390
|
+
|
|
391
|
+
/* ===== 配置页 ===== */
|
|
392
|
+
.i2p .cfg-layout{display:grid;grid-template-columns:330px minmax(0,1fr);gap:16px;align-items:start;margin-top:14px}
|
|
393
|
+
@media (max-width:960px){.i2p .cfg-layout{grid-template-columns:1fr}}
|
|
394
|
+
/* 阶段选择行:已自定义 → 名字后跟一枚小蓝点;委托开启 → 右侧"委"标 */
|
|
395
|
+
.i2p .cfg-row{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;
|
|
396
|
+
width:100%;text-align:left;background:none;border:none;padding:7px 9px;border-radius:10px;transition:background .15s}
|
|
397
|
+
.i2p .cfg-row:hover{background:var(--hover)}
|
|
398
|
+
.i2p .cfg-row.on{background:var(--nav-on)}
|
|
399
|
+
.i2p .cfg-row .cdot{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--accent);
|
|
400
|
+
margin-left:6px;vertical-align:2px}
|
|
401
|
+
.i2p .cfg-row .wtg{font-family:var(--mono);font-size:10px;color:var(--warn);border:1px solid var(--warn);
|
|
402
|
+
border-radius:4px;padding:0 4px;white-space:nowrap}
|
|
403
|
+
/* 提示词编辑:等宽小字号 textarea + 角色头(角色名 + 默认值标记 + 恢复默认按钮) */
|
|
404
|
+
.i2p .pr-head{display:flex;align-items:center;gap:8px;margin:12px 0 5px;flex-wrap:wrap}
|
|
405
|
+
.i2p .pr-head:first-child{margin-top:0}
|
|
406
|
+
.i2p .pr-head .f-label{margin:0}
|
|
407
|
+
.i2p .pr-head .mark{font-family:var(--mono);font-size:10.5px;color:var(--accent);border:1px solid var(--accent);
|
|
408
|
+
border-radius:4px;padding:0 5px}
|
|
409
|
+
.i2p textarea.pr-input{width:100%;min-height:88px;resize:vertical;background:var(--card);border:1px solid var(--line-2);
|
|
410
|
+
border-radius:8px;color:var(--ink);padding:7px 10px;font-size:12.5px;line-height:1.6;
|
|
411
|
+
font-family:var(--mono);transition:border-color .15s}
|
|
412
|
+
.i2p textarea.pr-input:focus{border-color:var(--accent)}
|
|
413
|
+
/* 委托开关行 */
|
|
414
|
+
.i2p .dlg-row{display:flex;align-items:flex-start;gap:10px;margin-bottom:10px}
|
|
415
|
+
.i2p .dlg-row input[type=checkbox]{accent-color:var(--accent);margin:3px 0 0;flex:none}
|
|
416
|
+
.i2p .dlg-row .lbl{font-size:14px;color:var(--ink);font-weight:600}
|
|
417
|
+
.i2p .dlg-row .sub{font-size:12.5px;color:var(--muted);line-height:1.55}
|
|
418
|
+
/* 文件导入按钮(外部智能体简介导入) */
|
|
419
|
+
.i2p .imp-label{display:inline-flex;align-items:center;gap:5px;background:none;border:1px dashed var(--line-2);
|
|
420
|
+
color:var(--accent);border-radius:8px;padding:4px 11px;font-size:13px;cursor:pointer}
|
|
421
|
+
.i2p .imp-label:hover{border-color:var(--accent)}
|
|
422
|
+
.i2p .imp-label input{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0 0 0 0)}
|
|
423
|
+
.i2p .cfg-foot{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px}
|
|
424
|
+
|
|
425
|
+
/* ===== toast ===== */
|
|
426
|
+
.i2p-toast{position:fixed;bottom:26px;left:36px;background:var(--dsw-alias-bg-layer-2,#fff);
|
|
427
|
+
border:1px solid var(--dsw-alias-border-l1,#e5e6ec);border-left:3px solid var(--dsw-alias-state-success-primary,#1c7c43);
|
|
428
|
+
color:var(--dsw-alias-label-primary,#17181f);padding:9px 16px;border-radius:10px;font-size:14px;
|
|
429
|
+
z-index:2100;max-width:70%;box-shadow:0 4px 18px rgba(9,12,20,.14)}
|
|
430
|
+
.i2p-toast.t-bad{border-left-color:var(--dsw-alias-state-error-primary,#d5463a)}
|
|
431
|
+
.i2p-toast.t-warn{border-left-color:var(--dsw-alias-state-warn-label,#9a6700)}
|
|
432
|
+
|
|
433
|
+
/* ===== Git 托管连接(项目页):host 徽章 + 连接列表 ===== */
|
|
434
|
+
.i2p .host-badge{display:inline-flex;align-items:center;gap:5px;font-size:12px;padding:1px 9px;
|
|
435
|
+
border-radius:999px;border:1px solid var(--line);color:var(--muted);max-width:100%}
|
|
436
|
+
.i2p .host-badge.ok{color:var(--good);border-color:transparent;background:var(--good-bg)}
|
|
437
|
+
.i2p .host-badge.warn{color:var(--warn);border-color:transparent;background:var(--warn-bg)}
|
|
438
|
+
.i2p .host-badge .hb-host{font-family:var(--mono);font-size:11.5px}
|
|
439
|
+
.i2p .repo-row-2{display:flex;gap:6px;margin:-2px 0 8px;align-items:center;flex-wrap:wrap}
|
|
440
|
+
.i2p .conn-list{display:flex;flex-direction:column;gap:6px;margin:8px 0 10px}
|
|
441
|
+
.i2p .conn-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--line);
|
|
442
|
+
border-radius:10px;flex-wrap:wrap}
|
|
443
|
+
.i2p .conn-row .cn-kind{flex:none;font-size:12px;font-weight:600;color:var(--accent)}
|
|
444
|
+
.i2p .conn-row .cn-host{font-family:var(--mono);font-size:12.5px;color:var(--ink)}
|
|
445
|
+
.i2p .conn-row .cn-tok{font-family:var(--mono);font-size:11.5px;color:var(--muted)}
|
|
446
|
+
.i2p .conn-row .cn-act{margin-left:auto;display:flex;gap:6px}
|
|
447
|
+
|
|
448
|
+
/* ---- 侧边栏入口按钮:1:1 复刻宿主设置按钮(settings-general VOzbGW_trigger 配方) ---- */
|
|
449
|
+
.i2p-entry{box-sizing:border-box;cursor:pointer;width:calc(100% + 4px);height:42px;
|
|
450
|
+
color:var(--dsw-alias-label-primary);background:none;border:none;border-radius:12px;
|
|
451
|
+
flex:none;align-items:center;gap:8px;margin:4px -2px;padding:0 10px 0 8px;
|
|
452
|
+
font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden;text-align:left}
|
|
453
|
+
.i2p-entry:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
454
|
+
.i2p-entry:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
455
|
+
.i2p-entry.rail{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;margin:8px 0 10px;padding:0}
|
|
456
|
+
.i2p-entry-label{white-space:nowrap;overflow:hidden}
|
|
457
|
+
|
|
458
|
+
/* ---- 主区整页工作台:贴合 conversation 列(JS 实时测量 rect 定位,随
|
|
459
|
+
sidebar 拖动/窗口缩放跟随),无遮罩非模态,视觉上即"右边的框" ---- */
|
|
460
|
+
.i2p-page{position:fixed;display:flex;flex-direction:column;overflow:hidden;
|
|
461
|
+
background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary)}
|
|
462
|
+
.i2p-head{flex:none;display:flex;align-items:center;justify-content:center;gap:14px;position:relative;
|
|
463
|
+
height:52px;padding:0 10px 0 20px;border-bottom:1px solid var(--dsw-alias-border-l1,#e5e6ec)}
|
|
464
|
+
.i2p-head .brand{display:flex;align-items:baseline;gap:10px;min-width:0}
|
|
465
|
+
.i2p-head .brand b{font-size:16px;font-weight:600}
|
|
466
|
+
.i2p-head .brand span{font-family:var(--mono);font-size:11.5px;color:var(--dsw-alias-label-tertiary,#82848f);letter-spacing:.08em}
|
|
467
|
+
.i2p-head .om{font-family:var(--mono);font-size:11.5px;color:var(--dsw-alias-label-tertiary,#82848f)}
|
|
468
|
+
/* right:78px 避开宿主右上角的悬浮按钮排(better-sidebar:底部面板/侧边栏展开钮占右缘约 70px) */
|
|
469
|
+
.i2p-close{position:absolute;right:78px;top:50%;transform:translateY(-50%);
|
|
470
|
+
display:inline-flex;align-items:center;justify-content:center;cursor:pointer;width:28px;height:28px;
|
|
471
|
+
color:var(--dsw-alias-label-primary);background:none;border:none;border-radius:28px}
|
|
472
|
+
.i2p-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
473
|
+
.i2p-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
474
|
+
.i2p-page-body{flex:1;min-height:0;display:flex}
|
|
475
|
+
|
|
476
|
+
/* ===== 悬浮智能助手(瑞士极简:去气泡、去 AI 符号,天蓝只做强调色系) ===== */
|
|
477
|
+
.i2p-ai{position:absolute;inset:0;pointer-events:none;z-index:9999}
|
|
478
|
+
.i2p-ai button{font-family:inherit;cursor:pointer;color:inherit}
|
|
479
|
+
/* 入口 = 中性工具钮(非"AI 球"):描边消息图标,hover 才显底;开启态反色 */
|
|
480
|
+
.i2p-ai-fab{pointer-events:auto;position:absolute;top:64px;right:88px;width:30px;height:30px;border-radius:9px;
|
|
481
|
+
border:1px solid color-mix(in srgb,#3aa0f5 24%,var(--line-2,#d8dae3));
|
|
482
|
+
background:color-mix(in srgb,#3aa0f5 9%,var(--panel,#fff));color:var(--ink-2,#414351);
|
|
483
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
484
|
+
box-shadow:0 1px 4px rgba(9,12,20,.08);
|
|
485
|
+
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)}
|
|
487
|
+
.i2p-ai-fab:active{transform:scale(.95)}
|
|
488
|
+
.i2p-ai-fab.on{background:var(--ink,#17181f);color:var(--card,#fff);border-color:var(--ink,#17181f)}
|
|
489
|
+
@keyframes i2p-ai-in{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
|
|
490
|
+
/* 面板:轻天蓝底(color-mix 混主题底色,浅色=淡天蓝/深色=深蓝调,官方 Markdown 文字两主题可读),
|
|
491
|
+
全部强调(边框/focus/hover/chip 圆点)统一同一天蓝色系,避免杂色 */
|
|
492
|
+
.i2p-ai-panel{pointer-events:auto;position:absolute;top:60px;width:400px;height:calc(100% - 76px);
|
|
493
|
+
min-width:240px;min-height:200px;
|
|
494
|
+
display:flex;flex-direction:column;
|
|
495
|
+
background:color-mix(in srgb,#3aa0f5 7%,var(--panel,#fff));
|
|
496
|
+
border:1px solid color-mix(in srgb,#3aa0f5 20%,var(--line-2,#d8dae3));border-radius:12px;
|
|
497
|
+
box-shadow:0 6px 28px rgba(9,12,20,.12);overflow:hidden;
|
|
498
|
+
animation:i2p-ai-in .2s cubic-bezier(.2,.8,.2,1)}
|
|
499
|
+
/* 自由窗口 resize 层:四边+四角,hover 显天蓝;角命中区 24×24(WCAG 2.2 目标尺寸),
|
|
500
|
+
贴面板内侧(外伸会被 overflow:hidden 裁剪点不到);双击任一处恢复默认位置与尺寸 */
|
|
501
|
+
.i2p-ai-rz{position:absolute;z-index:4;touch-action:none}
|
|
502
|
+
.i2p-ai-rz.n{top:0;left:24px;right:24px;height:7px;cursor:ns-resize}
|
|
503
|
+
.i2p-ai-rz.s{bottom:0;left:24px;right:24px;height:7px;cursor:ns-resize}
|
|
504
|
+
.i2p-ai-rz.e{right:0;top:24px;bottom:24px;width:7px;cursor:ew-resize}
|
|
505
|
+
.i2p-ai-rz.w{left:0;top:24px;bottom:24px;width:7px;cursor:ew-resize}
|
|
506
|
+
.i2p-ai-rz.nw{top:0;left:0;width:24px;height:24px;cursor:nwse-resize}
|
|
507
|
+
.i2p-ai-rz.ne{top:0;right:0;width:24px;height:24px;cursor:nesw-resize}
|
|
508
|
+
.i2p-ai-rz.sw{bottom:0;left:0;width:24px;height:24px;cursor:nesw-resize}
|
|
509
|
+
.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)}
|
|
511
|
+
/* 头部:标题 + 小字副题 + 关闭,细分割线(无图标) */
|
|
512
|
+
.i2p-ai-head{flex:none;display:flex;align-items:baseline;gap:8px;padding:11px 14px;cursor:move;
|
|
513
|
+
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)}
|
|
516
|
+
.i2p-ai-close{margin-left:auto;align-self:center;width:26px;height:26px;border:none;border-radius:7px;background:none;
|
|
517
|
+
color:var(--muted);display:inline-flex;align-items:center;justify-content:center;
|
|
518
|
+
transition:background .15s,color .15s}
|
|
519
|
+
.i2p-ai-close:hover{background:var(--hover);color:var(--ink)}
|
|
520
|
+
/* 消息区:助手回答平铺(与宿主聊天区同语言),用户问题轻底块 */
|
|
521
|
+
.i2p-ai-msgs{flex:1;min-height:0;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:12px;
|
|
522
|
+
scrollbar-width:thin;scrollbar-color:var(--line-2) transparent}
|
|
523
|
+
.i2p-ai-msgs::-webkit-scrollbar{width:6px}
|
|
524
|
+
.i2p-ai-msgs::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:6px}
|
|
525
|
+
/* 空态:一句能力说明 + 快捷问题(浅底单行钮,前缀天蓝圆点) */
|
|
526
|
+
.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}
|
|
528
|
+
.i2p-ai-chip{display:flex;align-items:center;gap:8px;text-align:left;
|
|
529
|
+
background:color-mix(in srgb,var(--card,#fff) 58%,transparent);
|
|
530
|
+
border:none;border-radius:8px;padding:7px 11px;font-size:13px;color:var(--ink-2);
|
|
531
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
|
532
|
+
transition:background .15s,color .15s}
|
|
533
|
+
.i2p-ai-chip::before{content:"";flex:none;width:5px;height:5px;border-radius:50%;
|
|
534
|
+
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)}
|
|
536
|
+
.i2p-ai-chip:disabled{opacity:.5;cursor:default}
|
|
537
|
+
/* 用户问题:半透明卡色块(在蓝底上呈更浅的蓝白),全宽不挤字 */
|
|
538
|
+
.i2p-ai-m{max-width:100%;font-size:13.5px;line-height:1.65}
|
|
539
|
+
.i2p-ai-m.user .i2p-ai-t{background:color-mix(in srgb,var(--card,#fff) 62%,transparent);
|
|
540
|
+
border:1px solid color-mix(in srgb,#3aa0f5 12%,var(--line,#e5e6ec));
|
|
541
|
+
border-radius:10px;padding:8px 12px;color:var(--ink);white-space:pre-wrap;word-break:break-word}
|
|
542
|
+
.i2p-ai-m.assistant{overflow-wrap:break-word}
|
|
543
|
+
.i2p-ai-m.assistant.err .i2p-ai-t{color:var(--err);font-size:12.5px;
|
|
544
|
+
background:var(--err-bg,rgba(213,70,58,.10));border-radius:8px;padding:7px 11px}
|
|
545
|
+
.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)}
|
|
547
|
+
.i2p-ai-md :first-child{margin-top:0}
|
|
548
|
+
.i2p-ai-md :last-child{margin-bottom:0}
|
|
549
|
+
/* 流式等待:三点呼吸 */
|
|
550
|
+
@keyframes i2p-ai-dot{50%{opacity:.25}}
|
|
551
|
+
.i2p-ai-dots{display:inline-flex;gap:4px;padding:2px 0}
|
|
552
|
+
.i2p-ai-dots i{width:6px;height:6px;border-radius:50%;background:var(--muted);
|
|
553
|
+
animation:i2p-ai-dot 1.2s ease-in-out infinite}
|
|
554
|
+
.i2p-ai-dots i:nth-child(2){animation-delay:.2s}
|
|
555
|
+
.i2p-ai-dots i:nth-child(3){animation-delay:.4s}
|
|
556
|
+
/* 输入区:半透明卡色输入框 + 图标钮(生成中变停止,可中断) */
|
|
557
|
+
.i2p-ai-input{flex:none;display:flex;gap:8px;align-items:flex-end;padding:10px 12px 12px;
|
|
558
|
+
border-top:1px solid color-mix(in srgb,#3aa0f5 14%,var(--line,#e5e6ec))}
|
|
559
|
+
.i2p-ai-input textarea{flex:1;min-height:40px;max-height:120px;resize:none;
|
|
560
|
+
background:color-mix(in srgb,var(--card,#fff) 66%,transparent);
|
|
561
|
+
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;
|
|
563
|
+
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))}
|
|
565
|
+
.i2p-ai-input textarea::placeholder{color:var(--muted)}
|
|
566
|
+
.i2p-ai-send{flex:none;width:38px;height:38px;border-radius:9px;
|
|
567
|
+
border:1px solid color-mix(in srgb,#3aa0f5 30%,var(--line-2,#d8dae3));
|
|
568
|
+
background:color-mix(in srgb,#3aa0f5 12%,var(--panel,#fff));color:var(--ink-2);
|
|
569
|
+
display:inline-flex;align-items:center;justify-content:center;
|
|
570
|
+
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)}
|
|
572
|
+
.i2p-ai-send:active:not(:disabled){transform:scale(.95)}
|
|
573
|
+
.i2p-ai-send:disabled{opacity:.4;cursor:default}
|
|
574
|
+
@media (max-width:880px){
|
|
575
|
+
.i2p-ai-fab{right:88px;top:60px}
|
|
576
|
+
.i2p-ai-panel{width:min(360px,calc(100% - 100px))}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* ===== 动效与质感层(丰富但克制:150-300ms、语义化、可关) ===== */
|
|
580
|
+
/* 整页进场:淡入 + 轻微上浮 */
|
|
581
|
+
@keyframes i2p-in{from{opacity:0;transform:translateY(10px) scale(.997)}to{opacity:1;transform:none}}
|
|
582
|
+
.i2p-page{animation:i2p-in .22s cubic-bezier(.2,.8,.2,1)}
|
|
583
|
+
/* 目录项逐个滑入(stagger,CSS 变量 --i 由 JSX 提供) */
|
|
584
|
+
@keyframes i2p-cell-in{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
|
|
585
|
+
/* 待复核计数呼吸提醒 */
|
|
586
|
+
@keyframes i2p-breathe{50%{opacity:.5}}
|
|
587
|
+
/* 运行中状态徽章呼吸(与 .sdot 脉冲呼应) */
|
|
588
|
+
.i2p .tg.run{animation:i2p-breathe 1.6s ease-in-out infinite}
|
|
589
|
+
/* 卡片:hover 浮起一线 + 边框加重 */
|
|
590
|
+
.i2p .card{transition:border-color .16s ease,box-shadow .16s ease,transform .16s ease}
|
|
591
|
+
.i2p .card:hover{border-color:var(--line-2);transform:translateY(-1px);box-shadow:0 2px 12px rgba(9,12,20,.05)}
|
|
592
|
+
/* 按钮:按压回弹 */
|
|
593
|
+
.i2p .btn{cursor:pointer;transition:background .15s,border-color .15s,color .15s,transform .1s}
|
|
594
|
+
.i2p .btn:active{transform:scale(.97)}
|
|
595
|
+
/* 表格行 hover 扫过 */
|
|
596
|
+
.i2p .tbl tbody tr{transition:background .12s}
|
|
597
|
+
.i2p .tbl tbody tr:hover{background:var(--hover)}
|
|
598
|
+
/* 项目卡:hover 边框亮起 */
|
|
599
|
+
.i2p .proj-item{cursor:pointer;transition:border-color .16s,background .16s,transform .16s}
|
|
600
|
+
.i2p .proj-item:hover{border-color:var(--line-2);transform:translateY(-1px)}
|
|
601
|
+
/* 收起/展开侧边栏按钮(标题行左侧,仅图标;文字含义交给 aria-label/title) */
|
|
602
|
+
.i2p-nav-collapse{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;
|
|
603
|
+
border:none;border-radius:8px;background:none;color:var(--muted);cursor:pointer;
|
|
604
|
+
transition:background .14s,color .14s}
|
|
605
|
+
.i2p-nav-collapse:hover{background:var(--hover);color:var(--ink)}
|
|
606
|
+
.i2p-nav-collapse:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
607
|
+
/* 空状态:虚线卡片化,引导感 */
|
|
608
|
+
.i2p .empty-hint{border:1.5px dashed var(--line-2);border-radius:12px;padding:22px 16px;text-align:center}
|
|
609
|
+
/* 细滚动条 */
|
|
610
|
+
.i2p-main::-webkit-scrollbar,.i2p-nav::-webkit-scrollbar{width:6px;height:6px}
|
|
611
|
+
.i2p-main::-webkit-scrollbar-thumb,.i2p-nav::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:6px}
|
|
612
|
+
.i2p-main::-webkit-scrollbar-thumb:hover,.i2p-nav::-webkit-scrollbar-thumb:hover{background:var(--muted)}
|
|
613
|
+
.i2p-main::-webkit-scrollbar-track,.i2p-nav::-webkit-scrollbar-track{background:transparent}
|
|
614
|
+
.i2p-main{scrollbar-width:thin;scrollbar-color:var(--line-2) transparent}
|
|
615
|
+
/* 尊重系统减弱动效 */
|
|
616
|
+
@media (prefers-reduced-motion:reduce){
|
|
617
|
+
.i2p-page,.i2p-nav-cell,.i2p-nav{animation:none!important;transition:none!important}
|
|
618
|
+
.i2p .card,.i2p .btn,.i2p .proj-item,.i2p .tbl tbody tr,.i2p .tg.run,
|
|
619
|
+
.i2p-nav-cell .cnt.alert,.i2p-nav-grip,.i2p-nav-collapse{animation:none!important;transition:none!important}
|
|
620
|
+
.i2p-ai-panel,.i2p-ai-fab,.i2p-ai-send,.i2p-ai-dots i{animation:none!important;transition:none!important}
|
|
621
|
+
}
|
|
622
|
+
`;
|
|
623
|
+
|
|
624
|
+
const tagId = "dsh-issue2pr/styles";
|
|
625
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
626
|
+
const tag = document.createElement("style");
|
|
627
|
+
tag.dataset.plugin = "dsh-issue2pr";
|
|
628
|
+
tag.dataset.pluginCss = tagId;
|
|
629
|
+
tag.textContent = css;
|
|
630
|
+
document.head.appendChild(tag);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* ================================================================
|
|
634
|
+
* 工具函数
|
|
635
|
+
* ================================================================ */
|
|
636
|
+
function esc(s) {
|
|
637
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
638
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// JSON 顺序词法高亮(key 金 / 字符串绿 / 注释灰)
|
|
643
|
+
function jsonHtml(text) {
|
|
644
|
+
const src = String(text);
|
|
645
|
+
const re = /("(?:\\.|[^"\\])*")(\s*:)?|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
646
|
+
const out = [];
|
|
647
|
+
let last = 0, m;
|
|
648
|
+
while ((m = re.exec(src))) {
|
|
649
|
+
out.push(esc(src.slice(last, m.index)));
|
|
650
|
+
if (m[0][0] === "/") out.push('<span class="c">' + esc(m[0]) + "</span>");
|
|
651
|
+
else if (m[2]) out.push('<span class="k">' + esc(m[1]) + "</span>" + esc(m[2]));
|
|
652
|
+
else out.push('<span class="s">' + esc(m[1]) + "</span>");
|
|
653
|
+
last = m.index + m[0].length;
|
|
654
|
+
}
|
|
655
|
+
out.push(esc(src.slice(last)));
|
|
656
|
+
return out.join("");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// diff 着色(hunk 头 / 增 / 删)
|
|
660
|
+
function diffHtml(text) {
|
|
661
|
+
return String(text).split("\n").map(function (line) {
|
|
662
|
+
if (/^(\+\+\+|---|@@)/.test(line)) return '<span class="hunk">' + esc(line) + "</span>";
|
|
663
|
+
if (/^\+/.test(line)) return '<span class="add">' + esc(line) + "</span>";
|
|
664
|
+
if (/^-/.test(line)) return '<span class="del">' + esc(line) + "</span>";
|
|
665
|
+
return esc(line);
|
|
666
|
+
}).join("\n");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function renderView(text, name) {
|
|
670
|
+
const n = String(name || "").toLowerCase();
|
|
671
|
+
if (/\.json$/.test(n)) return jsonHtml(text);
|
|
672
|
+
if (/\.(diff|patch)$/.test(n)) return diffHtml(text);
|
|
673
|
+
if (/\.md$/.test(n)) return esc(text);
|
|
674
|
+
if (/^(diff |--- |\+\+\+ )/.test(text)) return diffHtml(text);
|
|
675
|
+
return esc(text);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function reviewModeLabel(m) {
|
|
679
|
+
return ({ every: "每阶段都停", "key-only": "只停关键门", auto: "全自动" })[m] || String(m || "");
|
|
680
|
+
}
|
|
681
|
+
function p6ModeLabel(m) {
|
|
682
|
+
return ({ builtin: "插件内多智能体", session: "交给 DSH 会话", claude: "委托 Claude Code" })[m] || String(m || "");
|
|
683
|
+
}
|
|
684
|
+
function kindLabel(k) { return k === "issue" ? "Issue" : "需求"; }
|
|
685
|
+
function fmtClock(iso) {
|
|
686
|
+
if (!iso) return "";
|
|
687
|
+
const d = new Date(iso);
|
|
688
|
+
if (isNaN(d.getTime())) return "";
|
|
689
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
690
|
+
return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
|
|
691
|
+
}
|
|
692
|
+
const EV_KIND = { llm: "LLM", git: "git", test: "测试", tool: "工具", stage: "阶段", info: "信息" };
|
|
693
|
+
function fmtTime(iso) {
|
|
694
|
+
if (!iso) return "";
|
|
695
|
+
const d = new Date(iso);
|
|
696
|
+
if (isNaN(d.getTime())) return String(iso).slice(0, 19);
|
|
697
|
+
const p = (x) => String(x).padStart(2, "0");
|
|
698
|
+
return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
|
|
699
|
+
}
|
|
700
|
+
function fmtDur(startIso, endIso) {
|
|
701
|
+
if (!startIso || !endIso) return "";
|
|
702
|
+
const ms = new Date(endIso) - new Date(startIso);
|
|
703
|
+
if (!(ms >= 0)) return "";
|
|
704
|
+
if (ms < 1000) return ms + "ms";
|
|
705
|
+
if (ms < 60000) return (ms / 1000).toFixed(1) + "s";
|
|
706
|
+
if (ms < 3600000) return Math.floor(ms / 60000) + "m" + Math.round(ms % 60000 / 1000) + "s";
|
|
707
|
+
return (ms / 3600000).toFixed(1) + "h";
|
|
708
|
+
}
|
|
709
|
+
function fmtSize(n) {
|
|
710
|
+
if (n == null) return "";
|
|
711
|
+
if (n < 1024) return n + " B";
|
|
712
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
713
|
+
return (n / 1024 / 1024).toFixed(1) + " MB";
|
|
714
|
+
}
|
|
715
|
+
function apiGet(path) {
|
|
716
|
+
return fetch(API + path).then(function (r) { return r.json().catch(function () { return {}; }); });
|
|
717
|
+
}
|
|
718
|
+
function apiPost(path, body) {
|
|
719
|
+
return fetch(API + path, {
|
|
720
|
+
method: "POST",
|
|
721
|
+
headers: { "Content-Type": "application/json" },
|
|
722
|
+
body: JSON.stringify(body || {}),
|
|
723
|
+
}).then(function (r) { return r.json().catch(function () { return {}; }); });
|
|
724
|
+
}
|
|
725
|
+
function apiDelete(path) {
|
|
726
|
+
return fetch(API + path, { method: "DELETE" })
|
|
727
|
+
.then(function (r) { return r.json().catch(function () { return {}; }); });
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// SVG 描边图标(24 网格 lucide 路径,按 width/height 缩放;stroke 继承 currentColor)
|
|
731
|
+
const ICONS = {
|
|
732
|
+
play: '<path d="M7 4.5v15l12-7.5z" fill="currentColor" stroke="none"/>',
|
|
733
|
+
stop: '<rect x="6" y="6" width="12" height="12" rx="1.5" fill="currentColor" stroke="none"/>',
|
|
734
|
+
redo: '<path d="M3 2v6h6"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L3 8"/>',
|
|
735
|
+
folder: '<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/>',
|
|
736
|
+
trash: '<path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
|
737
|
+
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
738
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
739
|
+
plus: '<path d="M12 5v14"/><path d="M5 12h14"/>',
|
|
740
|
+
collapse: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m16 9-3 3 3 3"/>',
|
|
741
|
+
expand: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="m13 9 3 3-3 3"/>',
|
|
742
|
+
box: '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
|
|
743
|
+
book: '<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>',
|
|
744
|
+
sliders: '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
|
|
745
|
+
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>',
|
|
746
|
+
reset: '<path d="M3 2v6h6"/><path d="M3 13a9 9 0 1 0 3-7.7L3 8"/>',
|
|
747
|
+
sparkle: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
|
|
748
|
+
send: '<path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/>',
|
|
749
|
+
msg: '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',
|
|
750
|
+
};
|
|
751
|
+
function Ic(name, size) {
|
|
752
|
+
return h("svg", {
|
|
753
|
+
width: size || 14, height: size || 14, viewBox: "0 0 24 24",
|
|
754
|
+
fill: "none", stroke: "currentColor", strokeWidth: 2,
|
|
755
|
+
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
756
|
+
dangerouslySetInnerHTML: { __html: ICONS[name] || "" },
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/* ================================================================
|
|
761
|
+
* 工作台目录列几何(宽窄/显隐):模块级 store,localStorage 记忆,
|
|
762
|
+
* WorkbenchPage 头部按钮与 Section 目录列共享
|
|
763
|
+
* ================================================================ */
|
|
764
|
+
const navStore = {
|
|
765
|
+
open: localStorage.getItem("i2p.navOpen") !== "0",
|
|
766
|
+
width: (function (v) { return v >= 180 && v <= 460 ? v : 240; })(parseInt(localStorage.getItem("i2p.navW") || "", 10)),
|
|
767
|
+
listeners: new Set(),
|
|
768
|
+
setOpen(v) {
|
|
769
|
+
this.open = v;
|
|
770
|
+
localStorage.setItem("i2p.navOpen", v ? "1" : "0");
|
|
771
|
+
this.emit();
|
|
772
|
+
},
|
|
773
|
+
setWidth(w, persist) {
|
|
774
|
+
this.width = Math.max(180, Math.min(460, Math.round(w)));
|
|
775
|
+
if (persist) localStorage.setItem("i2p.navW", String(this.width));
|
|
776
|
+
this.emit();
|
|
777
|
+
},
|
|
778
|
+
emit() { for (const fn of this.listeners) fn(); },
|
|
779
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
|
|
780
|
+
};
|
|
781
|
+
function useNavState() {
|
|
782
|
+
const [s, setS] = React.useState({ open: navStore.open, width: navStore.width });
|
|
783
|
+
React.useEffect(() => navStore.subscribe(() => setS({ open: navStore.open, width: navStore.width })), []);
|
|
784
|
+
return s;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/* ================================================================
|
|
788
|
+
* 视图位置(当前页面 / 选中项目 / 选中 Run):模块级 store,
|
|
789
|
+
* Section 写入、悬浮智能助手读取("聚焦现在的页面")
|
|
790
|
+
* ================================================================ */
|
|
791
|
+
const viewStore = {
|
|
792
|
+
nav: "projects", slug: null, runId: null,
|
|
793
|
+
listeners: new Set(),
|
|
794
|
+
set(patch) { Object.assign(this, patch); for (const fn of this.listeners) fn(); },
|
|
795
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
|
|
796
|
+
};
|
|
797
|
+
function useViewState() {
|
|
798
|
+
const [v, setV] = React.useState({ nav: viewStore.nav, slug: viewStore.slug, runId: viewStore.runId });
|
|
799
|
+
React.useEffect(() => viewStore.subscribe(() => setV({ nav: viewStore.nav, slug: viewStore.slug, runId: viewStore.runId })), []);
|
|
800
|
+
return v;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* ================================================================
|
|
804
|
+
* Git 托管连接辅助:host 解析(与服务端 lib/connections.js 对齐)。
|
|
805
|
+
* 支持 https / ssh:// / scp(git@host:path)三种形态。
|
|
806
|
+
* ================================================================ */
|
|
807
|
+
function hostOfUri(uri) {
|
|
808
|
+
const s = String(uri || "").trim();
|
|
809
|
+
const m = s.match(/^(?:https?|ssh|git):\/\/(?:[^@\/]+@)?([^\/:?#]+)/i);
|
|
810
|
+
if (m) return m[1].toLowerCase();
|
|
811
|
+
const scp = s.match(/^git@([^\/:?#]+):/);
|
|
812
|
+
return scp ? scp[1].toLowerCase() : null;
|
|
813
|
+
}
|
|
814
|
+
function isSshUri(uri) {
|
|
815
|
+
const s = String(uri || "").trim();
|
|
816
|
+
return /^(ssh|git):\/\//i.test(s) || /^git@[^\/:?#]+:/i.test(s);
|
|
817
|
+
}
|
|
818
|
+
// 本地绝对路径触发源(Windows 盘符 / POSIX / ~ 开头且非 URL)
|
|
819
|
+
function isLocalPath(uri) {
|
|
820
|
+
const s = String(uri || "").trim();
|
|
821
|
+
return !!s && !/^https?:\/\//i.test(s) && /^([a-zA-Z]:[\\/]|\/|~)/.test(s);
|
|
822
|
+
}
|
|
823
|
+
const LLM_SOURCE_LABEL = { host: "宿主默认模型", plugin: "插件配置", default: "插件内置兜底", stage: "阶段覆盖" };
|
|
824
|
+
|
|
825
|
+
/* ================================================================
|
|
826
|
+
* 01 项目
|
|
827
|
+
* ================================================================ */
|
|
828
|
+
// —— Git 托管连接卡(项目页内嵌,数据全局共享):连接列表 + 添加/测试/删除 ——
|
|
829
|
+
// GitHub/GitLab 用 Access Token;CodeArts 用「个人设置 → HTTPS 密码」的 用户名+密码。
|
|
830
|
+
// token 明文存于 ~/.dsh/issue2pr/connections.json(与本机环境变量同级安全)。
|
|
831
|
+
const CONN_KIND_META = {
|
|
832
|
+
github: { label: "GitHub", host: "github.com", hostFixed: true, needsUser: false, tokenLabel: "Access Token", hint: "GitHub → Settings → Developer settings → Personal access tokens" },
|
|
833
|
+
gitlab: { label: "GitLab", host: "gitlab.com", hostFixed: false, needsUser: false, tokenLabel: "Access Token", hint: "自建实例请改 host;token 需 read_api + read_repository 权限" },
|
|
834
|
+
codearts: { label: "CodeArts", host: "", hostFixed: false, needsUser: true, tokenLabel: "HTTPS 密码", hint: "华为云 CodeArts 仓库页右上角 → 个人设置 → HTTPS 密码;用户名形如 租户名/IAM用户名" },
|
|
835
|
+
};
|
|
836
|
+
function ConnectionsCard(props) {
|
|
837
|
+
const p = props;
|
|
838
|
+
const [draft, setDraft] = React.useState({ kind: "github", host: "", token: "", username: "" });
|
|
839
|
+
const [adding, setAdding] = React.useState(false);
|
|
840
|
+
const [busy, setBusy] = React.useState(false);
|
|
841
|
+
const meta = CONN_KIND_META[draft.kind] || CONN_KIND_META.github;
|
|
842
|
+
const list = p.connections || [];
|
|
843
|
+
|
|
844
|
+
const setD = function (k, v) { setDraft(function (d) { return Object.assign({}, d, { [k]: v }); }); };
|
|
845
|
+
// 保存用的 host:github/gitlab 留空时由服务端补默认域名
|
|
846
|
+
const payload = function () {
|
|
847
|
+
const host = meta.hostFixed ? meta.host : (draft.host || "").trim() || meta.host;
|
|
848
|
+
return { kind: draft.kind, host: host, token: (draft.token || "").trim(), username: (draft.username || "").trim() };
|
|
849
|
+
};
|
|
850
|
+
const testDraft = function () {
|
|
851
|
+
const o = payload();
|
|
852
|
+
if (!o.token) { p.toast("请先填写" + meta.tokenLabel, "bad"); return; }
|
|
853
|
+
setBusy(true);
|
|
854
|
+
apiPost("/connections/test", o).then(function (r) {
|
|
855
|
+
setBusy(false);
|
|
856
|
+
if (r && r.ok) p.toast(meta.label + " 已连接" + (r.account ? ":@" + r.account : ""));
|
|
857
|
+
else p.toast((r && r.message) || "测试失败", "bad");
|
|
858
|
+
}).catch(function (e) { setBusy(false); p.toast("请求失败: " + e, "bad"); });
|
|
859
|
+
};
|
|
860
|
+
const save = function () {
|
|
861
|
+
const o = payload();
|
|
862
|
+
if (!o.token) { p.toast("请先填写" + meta.tokenLabel, "bad"); return; }
|
|
863
|
+
setBusy(true);
|
|
864
|
+
apiPost("/connections", o).then(function (r) {
|
|
865
|
+
setBusy(false);
|
|
866
|
+
if (!r || !r.ok) { p.toast((r && r.message) || "保存失败", "bad"); return; }
|
|
867
|
+
p.toast("已保存 " + (r.connection ? r.connection.host : o.host) + " 连接");
|
|
868
|
+
p.reloadConnections();
|
|
869
|
+
setAdding(false);
|
|
870
|
+
setDraft({ kind: "github", host: "", token: "", username: "" });
|
|
871
|
+
}).catch(function (e) { setBusy(false); p.toast("请求失败: " + e, "bad"); });
|
|
872
|
+
};
|
|
873
|
+
const testSaved = function (c) {
|
|
874
|
+
setBusy(true);
|
|
875
|
+
apiPost("/connections/test", { id: c.id }).then(function (r) {
|
|
876
|
+
setBusy(false);
|
|
877
|
+
if (r && r.ok) p.toast(c.host + " 已连接" + (r.account ? ":" + r.account : ""));
|
|
878
|
+
else p.toast((r && r.message) || "测试失败", "bad");
|
|
879
|
+
}).catch(function (e) { setBusy(false); p.toast("请求失败: " + e, "bad"); });
|
|
880
|
+
};
|
|
881
|
+
const remove = function (c) {
|
|
882
|
+
if (!window.confirm("删除 " + c.host + " 的连接?使用该域名的私有仓库将无法克隆。")) return;
|
|
883
|
+
apiDelete("/connections/" + encodeURIComponent(c.id)).then(function (r) {
|
|
884
|
+
if (r && r.ok) { p.toast("已删除 " + c.host + " 连接"); p.reloadConnections(); }
|
|
885
|
+
else p.toast((r && r.message) || "删除失败", "bad");
|
|
886
|
+
}).catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
return h("div", { className: "field" },
|
|
890
|
+
h("span", { className: "f-label" }, "Git 托管连接(全局 · 所有项目共用)"),
|
|
891
|
+
h("p", { className: "hint-line" },
|
|
892
|
+
"私有仓库克隆与 Issue 抓取(GitHub / GitLab / 华为云 CodeArts)用这里配置的凭据;",
|
|
893
|
+
"按仓库地址的域名自动匹配,SSH 地址则走本机密钥。凭据明文存于 ",
|
|
894
|
+
h("code", null, "…\\issue2pr\\connections.json"), "。"),
|
|
895
|
+
list.length ? h("div", { className: "conn-list" }, list.map(function (c) {
|
|
896
|
+
const km = CONN_KIND_META[c.kind] || {};
|
|
897
|
+
return h("div", { key: c.id, className: "conn-row" },
|
|
898
|
+
h("span", { className: "cn-kind" }, km.label || c.kind),
|
|
899
|
+
h("span", { className: "cn-host" }, c.host),
|
|
900
|
+
h("span", { className: "cn-tok" }, c.token + (c.username ? " · " + c.username : "")),
|
|
901
|
+
h("span", { className: "cn-act" },
|
|
902
|
+
c.kind !== "codearts" ? h("button", {
|
|
903
|
+
type: "button", className: "btn sm", disabled: busy,
|
|
904
|
+
onClick: function () { testSaved(c); },
|
|
905
|
+
}, "测试") : h("span", { className: "hint" }, "在上方仓库行点「测试」"),
|
|
906
|
+
h("button", {
|
|
907
|
+
type: "button", className: "btn sm danger", disabled: busy,
|
|
908
|
+
onClick: function () { remove(c); },
|
|
909
|
+
}, "删除")));
|
|
910
|
+
})) : h("p", { className: "hint-line" }, "尚无连接——只用公开仓库(或本机 SSH 密钥)时无需配置。"),
|
|
911
|
+
adding ? h("div", { style: { border: "1px solid var(--line)", borderRadius: 10, padding: "10px 12px", marginTop: 6 } },
|
|
912
|
+
h("div", { className: "radio-group", role: "radiogroup", "aria-label": "托管类型" },
|
|
913
|
+
Object.keys(CONN_KIND_META).map(function (k) {
|
|
914
|
+
const on = draft.kind === k;
|
|
915
|
+
return h("label", { key: k, className: "radio-chip" + (on ? " on" : "") },
|
|
916
|
+
h("input", { type: "radio", name: "conn-kind", value: k, checked: on, onChange: function () { setD("kind", k); } }),
|
|
917
|
+
CONN_KIND_META[k].label);
|
|
918
|
+
})),
|
|
919
|
+
h("p", { className: "hint-line" }, meta.hint),
|
|
920
|
+
meta.hostFixed ? h("p", { className: "hint-line" }, "域名固定为 ", h("code", null, meta.host)) : h("div", { className: "field", style: { marginTop: 6 } },
|
|
921
|
+
h("label", { className: "f-label", htmlFor: "conn-host" }, meta.needsUser ? "域名(CodeArts 仓库 HTTPS 地址的域名,因区域/实例而异)" : "域名(自建实例请修改,公网默认 " + meta.host + ")"),
|
|
922
|
+
h("input", {
|
|
923
|
+
className: "f-input mono", id: "conn-host", value: draft.host,
|
|
924
|
+
placeholder: meta.host || "如 codehub.devcloud.cn-north-4.huaweicloud.com",
|
|
925
|
+
onChange: function (e) { setD("host", e.target.value); },
|
|
926
|
+
})),
|
|
927
|
+
h("div", { className: "field", style: { marginTop: 6 } },
|
|
928
|
+
h("label", { className: "f-label", htmlFor: "conn-user" }, meta.needsUser ? "HTTPS 用户名(租户名/IAM用户名)" : "HTTPS 用户名(可选)"),
|
|
929
|
+
h("input", {
|
|
930
|
+
className: "f-input mono", id: "conn-user", value: draft.username,
|
|
931
|
+
placeholder: meta.needsUser ? "如 mytenant/iamuser" : "通常留空即可",
|
|
932
|
+
onChange: function (e) { setD("username", e.target.value); },
|
|
933
|
+
})),
|
|
934
|
+
h("div", { className: "field", style: { marginTop: 6 } },
|
|
935
|
+
h("label", { className: "f-label", htmlFor: "conn-token" }, meta.tokenLabel),
|
|
936
|
+
h("input", {
|
|
937
|
+
className: "f-input mono", id: "conn-token", type: "password", value: draft.token,
|
|
938
|
+
placeholder: meta.tokenLabel, "aria-label": meta.tokenLabel,
|
|
939
|
+
onChange: function (e) { setD("token", e.target.value); },
|
|
940
|
+
})),
|
|
941
|
+
h("div", { style: { display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" } },
|
|
942
|
+
h("button", { type: "button", className: "btn sm", disabled: busy, onClick: testDraft }, "测试连接"),
|
|
943
|
+
h("button", { type: "button", className: "btn sm pri", disabled: busy, onClick: save }, "保存"),
|
|
944
|
+
h("button", { type: "button", className: "btn sm", disabled: busy, onClick: function () { setAdding(false); } }, "取消")))
|
|
945
|
+
: h("button", { type: "button", className: "add-row", onClick: function () { setAdding(true); } }, Ic("plus", 12), " 添加连接"));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function ProjectsPanel(props) {
|
|
949
|
+
const p = props;
|
|
950
|
+
const [form, setForm] = React.useState(null);
|
|
951
|
+
const [saving, setSaving] = React.useState(false);
|
|
952
|
+
// 本地触发源存在性检查结果:uri → true/false(check-local 端点;onBlur 与载入时触发)
|
|
953
|
+
const [trigCheck, setTrigCheck] = React.useState({});
|
|
954
|
+
|
|
955
|
+
const blankForm = function () {
|
|
956
|
+
return { name: "", slug: "", repos: [{ uri: "" }], triggers: [], reviewMode: "every", p6Mode: "builtin", testCommand: "" };
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
React.useEffect(function () {
|
|
960
|
+
const pr = p.projects ? p.projects.find(function (x) { return x.slug === p.slug; }) : null;
|
|
961
|
+
if (!pr) { setForm(blankForm()); return; }
|
|
962
|
+
setForm({
|
|
963
|
+
name: pr.name || "",
|
|
964
|
+
slug: pr.slug || "",
|
|
965
|
+
repos: (pr.repos || []).map(function (r) {
|
|
966
|
+
return typeof r === "string" ? { uri: r } : { uri: (r && r.uri) || "" };
|
|
967
|
+
}),
|
|
968
|
+
triggers: (pr.triggers || []).map(function (t) { return { kind: t.kind, uri: (t.uri || "") }; }),
|
|
969
|
+
reviewMode: pr.reviewMode || "every",
|
|
970
|
+
p6Mode: pr.p6Mode || "builtin",
|
|
971
|
+
testCommand: pr.testCommand || "",
|
|
972
|
+
});
|
|
973
|
+
setTrigCheck({});
|
|
974
|
+
(pr.triggers || []).forEach(function (t) { checkLocal(t.uri); }); // 载入即查本地触发源存在性
|
|
975
|
+
}, [p.slug, p.projects]);
|
|
976
|
+
|
|
977
|
+
if (!form) return h("p", { className: "empty-hint" }, "加载中…");
|
|
978
|
+
|
|
979
|
+
const setField = function (k, v) { setForm(function (f) { return Object.assign({}, f, { [k]: v }); }); };
|
|
980
|
+
const setRepo = function (i, uri) { setForm(function (f) { const r = f.repos.slice(); r[i] = { uri: uri }; return Object.assign({}, f, { repos: r }); }); };
|
|
981
|
+
const rmRepo = function (i) { setForm(function (f) { if (f.repos.length <= 1) return f; const r = f.repos.slice(); r.splice(i, 1); return Object.assign({}, f, { repos: r }); }); };
|
|
982
|
+
const addRepo = function () { setForm(function (f) { return Object.assign({}, f, { repos: f.repos.concat([{ uri: "" }]) }); }); };
|
|
983
|
+
const setTrig = function (i, uri) { setForm(function (f) { const t = f.triggers.slice(); t[i] = Object.assign({}, t[i], { uri: uri }); return Object.assign({}, f, { triggers: t }); }); };
|
|
984
|
+
const rmTrig = function (i) { setForm(function (f) { const t = f.triggers.slice(); t.splice(i, 1); return Object.assign({}, f, { triggers: t }); }); };
|
|
985
|
+
const addTrig = function (kind) { setForm(function (f) { return Object.assign({}, f, { triggers: f.triggers.concat([{ kind: kind, uri: "" }]) }); }); };
|
|
986
|
+
|
|
987
|
+
const collect = function () {
|
|
988
|
+
const f = form;
|
|
989
|
+
// stageConfig(「配置」页维护)与项目表单互不感知:原样透传,避免保存项目时丢失阶段配置
|
|
990
|
+
const pr = p.projects ? p.projects.find(function (x) { return x.slug === p.slug; }) : null;
|
|
991
|
+
return {
|
|
992
|
+
name: (f.name || "").trim(),
|
|
993
|
+
slug: (f.slug || "").trim(),
|
|
994
|
+
repos: f.repos.map(function (r) { return (r.uri || "").trim(); }).filter(Boolean).map(function (uri) { return { uri: uri }; }),
|
|
995
|
+
triggers: f.triggers
|
|
996
|
+
.map(function (t) { return { kind: t.kind, uri: (t.uri || "").trim() }; })
|
|
997
|
+
.filter(function (t) { return t.uri; }),
|
|
998
|
+
reviewMode: f.reviewMode,
|
|
999
|
+
p6Mode: f.p6Mode,
|
|
1000
|
+
testCommand: (f.testCommand || "").trim(),
|
|
1001
|
+
stageConfig: (pr && pr.stageConfig) || {},
|
|
1002
|
+
};
|
|
1003
|
+
};
|
|
1004
|
+
const validate = function (o) {
|
|
1005
|
+
if (!o.name) return "请填写项目名称";
|
|
1006
|
+
if (!/^[a-z0-9-]+$/.test(o.slug)) return "slug 只允许小写字母/数字/连字符";
|
|
1007
|
+
if (o.repos.length === 0) return "至少填写一个 Git 仓库链接";
|
|
1008
|
+
return null;
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const ensureSaved = function () {
|
|
1012
|
+
const o = collect();
|
|
1013
|
+
const err = validate(o);
|
|
1014
|
+
if (err) { p.toast(err, "bad"); return Promise.resolve(null); }
|
|
1015
|
+
setSaving(true);
|
|
1016
|
+
return apiPost("/projects", o).then(function (r) {
|
|
1017
|
+
setSaving(false);
|
|
1018
|
+
if (!r || !r.ok) { p.toast((r && r.message) || "保存失败", "bad"); return null; }
|
|
1019
|
+
p.onSaved(o.slug);
|
|
1020
|
+
return o.slug;
|
|
1021
|
+
}).catch(function (e) { setSaving(false); p.toast("请求失败: " + e, "bad"); return null; });
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
const startRun = function (kind, uri) {
|
|
1025
|
+
const u = (uri || "").trim();
|
|
1026
|
+
if (!u) { p.toast("请先填写该触发源路径", "bad"); return; }
|
|
1027
|
+
ensureSaved().then(function (slug) {
|
|
1028
|
+
if (!slug) return;
|
|
1029
|
+
// 轻预检(不阻断):主仓库是 https 且无匹配连接 → 私有仓库会克隆失败,提前亮黄;
|
|
1030
|
+
// 公开仓库匿名可克隆,不受影响,所以只提示不拦截
|
|
1031
|
+
const o = collect();
|
|
1032
|
+
const mainUri = o.repos.length ? o.repos[0].uri : "";
|
|
1033
|
+
if (/^https?:\/\//i.test(mainUri)) {
|
|
1034
|
+
const host = hostOfUri(mainUri);
|
|
1035
|
+
const has = p.connections && p.connections.some(function (c) { return c.host === host; });
|
|
1036
|
+
if (host && !has) p.toast("提示:" + host + " 未配置连接,私有仓库将无法克隆(公开仓库不受影响)", "warn");
|
|
1037
|
+
}
|
|
1038
|
+
apiPost("/projects/" + slug + "/runs", { kind: kind, uri: u }).then(function (r) {
|
|
1039
|
+
if (!r || !r.ok) { p.toast((r && r.message) || "发起 Run 失败", "bad"); return; }
|
|
1040
|
+
p.onRunStarted(slug, r.runId);
|
|
1041
|
+
}).catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1042
|
+
});
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const runFirst = function () {
|
|
1046
|
+
const o = collect();
|
|
1047
|
+
if (o.triggers.length === 0) { p.toast("请先添加触发源(需求文档或 Issue)", "bad"); return; }
|
|
1048
|
+
startRun(o.triggers[0].kind, o.triggers[0].uri);
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// —— 仓库行 host 徽章:按域名匹配连接(匹配=已连接;https 未匹配=黄牌提示;SSH=本机密钥) ——
|
|
1052
|
+
const connOf = function (uri) {
|
|
1053
|
+
const host = hostOfUri(uri);
|
|
1054
|
+
if (!host || !p.connections) return null;
|
|
1055
|
+
return p.connections.find(function (c) { return c.host === host; }) || null;
|
|
1056
|
+
};
|
|
1057
|
+
const repoBadge = function (uri) {
|
|
1058
|
+
const u = (uri || "").trim();
|
|
1059
|
+
if (!u) return null;
|
|
1060
|
+
if (isSshUri(u)) return h("span", { className: "host-badge", title: "SSH 地址使用本机密钥认证" }, "SSH · 本机密钥");
|
|
1061
|
+
const host = hostOfUri(u);
|
|
1062
|
+
if (!host) return null;
|
|
1063
|
+
const c = connOf(u);
|
|
1064
|
+
if (c) return h("span", { className: "host-badge ok", title: "已配置该域名的连接凭据" },
|
|
1065
|
+
Ic("check", 11), h("span", { className: "hb-host" }, host), "已连接" + (c.username ? " · " + c.username : ""));
|
|
1066
|
+
return h("span", { className: "host-badge warn", title: "私有仓库需要凭据才能克隆;在下方「Git 托管连接」添加后自动匹配" },
|
|
1067
|
+
h("span", { className: "hb-host" }, host), "未配置连接");
|
|
1068
|
+
};
|
|
1069
|
+
const testRepo = function (uri) {
|
|
1070
|
+
const u = (uri || "").trim();
|
|
1071
|
+
if (!u) { p.toast("请先填写仓库地址", "bad"); return; }
|
|
1072
|
+
p.toast("正在测试连通(git ls-remote)…");
|
|
1073
|
+
apiPost("/connections/test-repo", { uri: u }).then(function (r) {
|
|
1074
|
+
if (r && r.ok) p.toast("仓库可达:" + r.message);
|
|
1075
|
+
else p.toast("仓库不可达:" + ((r && r.message) || "未知错误"), "bad");
|
|
1076
|
+
}).catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1077
|
+
};
|
|
1078
|
+
// —— 本地触发源存在性(check-local 端点):失焦时检查,结果随 uri 键缓存 ——
|
|
1079
|
+
// (函数声明:表单载入 effect 也要调它,且 smoke 的 mock effect 同步执行需提升)
|
|
1080
|
+
function checkLocal(uri) {
|
|
1081
|
+
const u = (uri || "").trim();
|
|
1082
|
+
if (!u || !isLocalPath(u)) return;
|
|
1083
|
+
apiPost("/check-local", { path: u }).then(function (r) {
|
|
1084
|
+
if (r && r.ok) setTrigCheck(function (m) { return Object.assign({}, m, { [u]: !!r.exists }); });
|
|
1085
|
+
}).catch(function () { /* 检查失败静默:发起 Run 时服务端仍会硬校验 */ });
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const deleteProject = function () {
|
|
1089
|
+
const slug = form.slug;
|
|
1090
|
+
if (!slug) { p.toast("新建中的项目无需删除", "bad"); return; }
|
|
1091
|
+
if (!window.confirm("确认删除项目 " + slug + "?\n其全部 Run 产物与本地 repo 克隆都会被删除,不可恢复。")) return;
|
|
1092
|
+
apiDelete("/projects/" + slug + "?confirm=" + encodeURIComponent(slug))
|
|
1093
|
+
.then(function (r) {
|
|
1094
|
+
if (r && r.ok) { p.toast("项目已删除"); p.onDeletedProject(); }
|
|
1095
|
+
else p.toast((r && r.message) || "删除失败", "bad");
|
|
1096
|
+
})
|
|
1097
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const radioGroup = function (labelId, opts, value, onChange, name) {
|
|
1101
|
+
return h("div", { className: "radio-group", role: "radiogroup", "aria-labelledby": labelId },
|
|
1102
|
+
opts.map(function (o) {
|
|
1103
|
+
const on = value === o.value;
|
|
1104
|
+
return h("label", { key: o.value, className: "radio-chip" + (on ? " on" : "") },
|
|
1105
|
+
h("input", { type: "radio", name: name, value: o.value, checked: on, onChange: function () { onChange(o.value); } }),
|
|
1106
|
+
o.label,
|
|
1107
|
+
h("span", { className: "hint" }, o.hint));
|
|
1108
|
+
}));
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
const trigGroup = function (kind, title, tagCls, tagText, hint) {
|
|
1112
|
+
const rows = [];
|
|
1113
|
+
form.triggers.forEach(function (t, i) {
|
|
1114
|
+
if (t.kind !== kind) return;
|
|
1115
|
+
const checked = isLocalPath(t.uri) && trigCheck[(t.uri || "").trim()] !== undefined
|
|
1116
|
+
? trigCheck[(t.uri || "").trim()] : null;
|
|
1117
|
+
rows.push(h("div", { key: "tr" + i, className: "dyn-row" },
|
|
1118
|
+
h("input", {
|
|
1119
|
+
className: "f-input mono", value: t.uri,
|
|
1120
|
+
placeholder: kind === "requirement" ? "D:\\path\\to\\spec.md 或 https://…" : "D:\\issues\\xxx.md 或 issue 链接,格式不限",
|
|
1121
|
+
"aria-label": title + " " + (i + 1),
|
|
1122
|
+
onChange: function (e) { setTrig(i, e.target.value); },
|
|
1123
|
+
onBlur: function (e) { checkLocal(e.target.value); },
|
|
1124
|
+
}),
|
|
1125
|
+
checked !== null ? h("span", {
|
|
1126
|
+
className: "host-badge " + (checked ? "ok" : "warn"),
|
|
1127
|
+
title: checked ? "发起 Run 前会重新读取该文件" : "文件不存在:发起 Run 时将报「触发文档不存在」",
|
|
1128
|
+
}, checked ? "文件存在" : "不存在") : null,
|
|
1129
|
+
h("button", {
|
|
1130
|
+
type: "button", className: "run-btn",
|
|
1131
|
+
title: "用此" + title + "发起 Run", "aria-label": "用此" + title + "发起 Run",
|
|
1132
|
+
onClick: function () { startRun(kind, t.uri); },
|
|
1133
|
+
}, Ic("play", 12)),
|
|
1134
|
+
h("button", {
|
|
1135
|
+
type: "button", className: "rm", "aria-label": "删除该行",
|
|
1136
|
+
onClick: function () { rmTrig(i); },
|
|
1137
|
+
}, Ic("x", 12))));
|
|
1138
|
+
});
|
|
1139
|
+
return h("div", { className: "trig-head-group", key: kind },
|
|
1140
|
+
h("div", { className: "trig-head" },
|
|
1141
|
+
h("span", { className: "trig-tag" + (tagCls ? " " + tagCls : "") }, tagText),
|
|
1142
|
+
title,
|
|
1143
|
+
h("span", { className: "hint" }, hint)),
|
|
1144
|
+
rows,
|
|
1145
|
+
h("button", { type: "button", className: "add-row", onClick: function () { addTrig(kind); } },
|
|
1146
|
+
Ic("plus", 12), " 添加" + (kind === "issue" ? " Issue" : "需求文档")));
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
const listItems = (p.projects || []).map(function (pr) {
|
|
1150
|
+
const on = pr.slug === p.slug;
|
|
1151
|
+
return h("button", {
|
|
1152
|
+
key: pr.slug, className: "proj-item" + (on ? " on" : ""), role: "option",
|
|
1153
|
+
"aria-selected": on ? "true" : "false",
|
|
1154
|
+
onClick: function () { p.onSelectProject(pr.slug); },
|
|
1155
|
+
},
|
|
1156
|
+
h("div", { className: "p-name" }, pr.name),
|
|
1157
|
+
h("div", { className: "p-meta" },
|
|
1158
|
+
(pr.repos || []).length + " 仓库 · " + (pr.triggers || []).length + " 触发源"));
|
|
1159
|
+
});
|
|
1160
|
+
listItems.push(h("button", { key: "__new__", className: "add-row", onClick: function () { p.onSelectProject(null); } },
|
|
1161
|
+
Ic("plus", 12), " 新建项目"));
|
|
1162
|
+
|
|
1163
|
+
// —— 环境健康横幅(preflight 只读探测):git 缺失 / claude CLI 未就绪(仅 p6Mode=claude 时才相关) ——
|
|
1164
|
+
const pf = p.preflight;
|
|
1165
|
+
const banners = [];
|
|
1166
|
+
if (pf && pf.git && !pf.git.ok) banners.push(h("div", { key: "git", className: "callout err", style: { marginBottom: 12 } },
|
|
1167
|
+
h("h4", null, "未检测到 git"),
|
|
1168
|
+
h("p", { style: { margin: 0 } }, "克隆仓库(P2 前)与应用补丁(P7)都依赖 git 命令。请安装 git 并加入 PATH 后刷新本页。")));
|
|
1169
|
+
if (pf && pf.claude && !pf.claude.ok && form.p6Mode === "claude") banners.push(h("div", { key: "claude", className: "callout warn", style: { marginBottom: 12 } },
|
|
1170
|
+
h("h4", null, "未探测到 claude CLI"),
|
|
1171
|
+
h("p", { style: { margin: 0 } },
|
|
1172
|
+
"尝试路径 ", h("code", null, pf.claude.path || "claude"), " 不存在——P6「委托 Claude Code」发起后将回退为等待人工。",
|
|
1173
|
+
"可在「配置」页 P6 阶段指定 claude 可执行文件。")));
|
|
1174
|
+
// —— LLM 路由行:默认模型从哪来 + 几个阶段已覆盖(key 由 DSH 宿主管理,插件无法预检其有效性) ——
|
|
1175
|
+
const llmRoute = pf && pf.llm
|
|
1176
|
+
? (function () {
|
|
1177
|
+
const ovN = pf.llm.overrides ? Object.keys(pf.llm.overrides).length : 0;
|
|
1178
|
+
return h("p", { className: "hint-line", style: { margin: "0 0 14px" } },
|
|
1179
|
+
"LLM 默认 ", h("code", null, (pf.llm.provider || "?") + " / " + (pf.llm.model || "?")),
|
|
1180
|
+
"(来源:" + (LLM_SOURCE_LABEL[pf.llm.source] || pf.llm.source) + (ovN > 0 ? " · 本项目 " + ovN + " 个阶段已覆盖模型" : "") + ");",
|
|
1181
|
+
ovN > 0 ? "覆盖清单见「配置」页。" : "各阶段可在「配置」页按阶段覆盖。", "模型凭据由 DSH 宿主管理。");
|
|
1182
|
+
})()
|
|
1183
|
+
: null;
|
|
1184
|
+
|
|
1185
|
+
return h("div", null,
|
|
1186
|
+
banners,
|
|
1187
|
+
h("div", { className: "callout acc", style: { marginBottom: 16 } },
|
|
1188
|
+
h("h4", null, "三步上手"),
|
|
1189
|
+
h("p", { style: { margin: 0 } },
|
|
1190
|
+
h("b", null, "① 填项目名与仓库"), " → ", h("b", null, "② 添加触发源(需求文档 / Issue)"),
|
|
1191
|
+
" → ", h("b", null, "③ 点 ▶ 发起 Run"), ",随后到「运行」跟进 11 阶段流水线。")),
|
|
1192
|
+
llmRoute,
|
|
1193
|
+
h("div", { style: { display: "grid", gridTemplateColumns: "240px minmax(0,1fr)", gap: 16, alignItems: "start" } },
|
|
1194
|
+
h("div", { className: "proj-list", role: "listbox", "aria-label": "项目列表" }, listItems),
|
|
1195
|
+
h("form", {
|
|
1196
|
+
className: "card", onSubmit: function (e) { e.preventDefault(); ensureSaved(); },
|
|
1197
|
+
},
|
|
1198
|
+
h("h4", null, "项目配置"),
|
|
1199
|
+
h("div", { className: "field-row" },
|
|
1200
|
+
h("div", { className: "field" },
|
|
1201
|
+
h("label", { className: "f-label", htmlFor: "f-name" }, "项目名称"),
|
|
1202
|
+
h("input", { className: "f-input", id: "f-name", value: form.name, onChange: function (e) { setField("name", e.target.value); } })),
|
|
1203
|
+
h("div", { className: "field" },
|
|
1204
|
+
h("label", { className: "f-label", htmlFor: "f-slug" }, "目录名(slug · 产物根下的项目文件夹)"),
|
|
1205
|
+
h("input", { className: "f-input mono", id: "f-slug", value: form.slug, onChange: function (e) { setField("slug", e.target.value); } }))),
|
|
1206
|
+
h("div", { className: "field" },
|
|
1207
|
+
h("label", { className: "f-label" }, "Git 仓库链接(可多个 · 第一个为主仓库,发起 Run 时自动 clone)"),
|
|
1208
|
+
form.repos.map(function (r, i) {
|
|
1209
|
+
const badge = repoBadge(r.uri);
|
|
1210
|
+
return h("div", { key: i },
|
|
1211
|
+
h("div", { className: "dyn-row" },
|
|
1212
|
+
h("input", {
|
|
1213
|
+
className: "f-input mono", value: r.uri, placeholder: "https://github.com/org/repo.git",
|
|
1214
|
+
"aria-label": "仓库链接 " + (i + 1),
|
|
1215
|
+
onChange: function (e) { setRepo(i, e.target.value); },
|
|
1216
|
+
}),
|
|
1217
|
+
h("button", {
|
|
1218
|
+
type: "button", className: "btn sm",
|
|
1219
|
+
title: "测试连通(git ls-remote,私有仓库验证已配置的连接凭据)",
|
|
1220
|
+
"aria-label": "测试仓库 " + (i + 1) + " 连通",
|
|
1221
|
+
onClick: function () { testRepo(r.uri); },
|
|
1222
|
+
}, "测试"),
|
|
1223
|
+
h("button", {
|
|
1224
|
+
type: "button", className: "rm", "aria-label": "删除该行",
|
|
1225
|
+
onClick: function () { rmRepo(i); },
|
|
1226
|
+
}, Ic("x", 12))),
|
|
1227
|
+
badge ? h("div", { className: "repo-row-2" }, badge) : null);
|
|
1228
|
+
}),
|
|
1229
|
+
h("button", { type: "button", className: "add-row", onClick: addRepo }, Ic("plus", 12), " 添加仓库")),
|
|
1230
|
+
h("fieldset", { className: "trig-src" },
|
|
1231
|
+
h("span", { className: "f-label", style: { display: "block" } }, "触发源 · 需求文档与 Issue 均可单独发起 Run(可多个,格式不限)"),
|
|
1232
|
+
trigGroup("requirement", "需求文档", "", "需求", "本地路径或 URL"),
|
|
1233
|
+
trigGroup("issue", "Issue 文档", "issue", "Issue", "md / txt / 工单导出 / issue 链接…")),
|
|
1234
|
+
h("div", { className: "field-row" },
|
|
1235
|
+
h("div", { className: "field" },
|
|
1236
|
+
h("span", { className: "f-label", id: "rg-review-label" }, "人工复核模式"),
|
|
1237
|
+
radioGroup("rg-review-label", [
|
|
1238
|
+
{ value: "every", label: "每阶段都停", hint: "默认" },
|
|
1239
|
+
{ value: "key-only", label: "只停关键门", hint: "P5/P6/P9/P11" },
|
|
1240
|
+
{ value: "auto", label: "全自动", hint: "事后可打回" },
|
|
1241
|
+
], form.reviewMode, function (v) { setField("reviewMode", v); }, "review")),
|
|
1242
|
+
h("div", { className: "field" },
|
|
1243
|
+
h("span", { className: "f-label", id: "rg-exec-label" }, "P6 代码优化执行模式"),
|
|
1244
|
+
radioGroup("rg-exec-label", [
|
|
1245
|
+
{ value: "builtin", label: "插件内多智能体", hint: "Planner/Coder/Reviewer" },
|
|
1246
|
+
{ value: "session", label: "交给 DSH 会话", hint: "真 workflow" },
|
|
1247
|
+
{ value: "claude", label: "委托 Claude Code", hint: "claude CLI 自动执行" },
|
|
1248
|
+
], form.p6Mode, function (v) { setField("p6Mode", v); }, "exec"))),
|
|
1249
|
+
h("div", { className: "field" },
|
|
1250
|
+
h("label", { className: "f-label", htmlFor: "f-test" }, "测试命令(可选 · P8 使用)"),
|
|
1251
|
+
h("input", {
|
|
1252
|
+
className: "f-input mono", id: "f-test", value: form.testCommand,
|
|
1253
|
+
placeholder: "如 npm test / python -m pytest …",
|
|
1254
|
+
onChange: function (e) { setField("testCommand", e.target.value); },
|
|
1255
|
+
})),
|
|
1256
|
+
h(ConnectionsCard, { connections: p.connections, toast: p.toast, reloadConnections: p.reloadConnections }),
|
|
1257
|
+
h("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
1258
|
+
h("button", { type: "submit", className: "btn pri", disabled: saving }, "保存配置"),
|
|
1259
|
+
h("button", { type: "button", className: "btn", onClick: runFirst }, Ic("play"), " 用该项目发起 Run"),
|
|
1260
|
+
form.slug && p.slug ? h("button", { type: "button", className: "btn danger", onClick: deleteProject }, Ic("trash"), " 删除项目") : null),
|
|
1261
|
+
h("p", { className: "hint-line" },
|
|
1262
|
+
"配置写入 ", h("code", null, "…\\issue2pr\\projects\\" + (form.slug || "<slug>") + "\\project.json"), "。"))));
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
/* ================================================================
|
|
1266
|
+
* 02 运行
|
|
1267
|
+
* ================================================================ */
|
|
1268
|
+
// 键值信息行(项目 / 说明 tab 共用):左灰标签右值,靠左两列
|
|
1269
|
+
function kvRow(k, v, mono) {
|
|
1270
|
+
return h(React.Fragment, null,
|
|
1271
|
+
h("span", { className: "k" }, k),
|
|
1272
|
+
h("span", { className: "v" + (mono ? " mono" : "") }, v));
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// 阶段契约卡:上游输入 → 输出产物 → 输出契约(「配置」表单与「运行」页阶段详情共用;
|
|
1276
|
+
// 数据源 defaults.stages[id].delegateSpec——内置执行与委托外部智能体同受此约束;
|
|
1277
|
+
// P7/P8 为确定性执行阶段,无 LLM 输出契约)
|
|
1278
|
+
function StageContractCard(props) {
|
|
1279
|
+
const d = props.defaults && props.defaults.stages ? props.defaults.stages[props.stageId] : null;
|
|
1280
|
+
const spec = d && d.delegateSpec;
|
|
1281
|
+
return h("div", { className: "field" },
|
|
1282
|
+
h("span", { className: "f-label" }, "阶段契约 · 输入 → 输出(内置执行与委托外部智能体同受约束)"),
|
|
1283
|
+
spec
|
|
1284
|
+
? h("div", { className: "kv" },
|
|
1285
|
+
spec.inputs ? kvRow("上游输入", (Array.isArray(spec.inputs) ? spec.inputs : [spec.inputs]).join("、"), true) : null,
|
|
1286
|
+
kvRow("输出产物", spec.output, true),
|
|
1287
|
+
kvRow("输出契约", spec.contract, true))
|
|
1288
|
+
: h("p", { className: "hint-line", style: { margin: 0 } },
|
|
1289
|
+
"确定性执行阶段(不调用大模型),无 LLM 输出契约。"));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// 阶段详情 · 项目 tab:本 Run 所属项目的只读上下文(阶段执行读的配置)
|
|
1293
|
+
function StageProjectCard(props) {
|
|
1294
|
+
const pr = props.project;
|
|
1295
|
+
if (!pr) {
|
|
1296
|
+
return h("p", { className: "empty-hint" },
|
|
1297
|
+
"当前未选择项目。请先在左侧项目列表(或「项目」页)选择一个项目。");
|
|
1298
|
+
}
|
|
1299
|
+
return h("div", null,
|
|
1300
|
+
h("p", { className: "run-hint", style: { margin: "0 0 12px" } },
|
|
1301
|
+
"本 Run 所属项目的配置——阶段执行读的就是这些值:复核模式决定每阶段是否停顿,",
|
|
1302
|
+
"P6 执行模式决定代码修改由谁完成,测试命令供 P8 使用。编辑请到「项目」页。"),
|
|
1303
|
+
h("div", { className: "kv" },
|
|
1304
|
+
kvRow("项目", (pr.name || "") + "(slug " + (pr.slug || "") + ")"),
|
|
1305
|
+
kvRow("Git 仓库", (pr.repos || []).map(function (r, i) {
|
|
1306
|
+
return h("div", { key: i }, r.uri || "");
|
|
1307
|
+
}) || "(无)", true),
|
|
1308
|
+
kvRow("触发源", (pr.triggers || []).length
|
|
1309
|
+
? (pr.triggers || []).map(function (t, i) {
|
|
1310
|
+
return h("div", { key: i }, kindLabel(t.kind) + " · " + (t.uri || ""));
|
|
1311
|
+
})
|
|
1312
|
+
: "(无)", true),
|
|
1313
|
+
kvRow("人工复核模式", reviewModeLabel(pr.reviewMode) + "(" + (pr.reviewMode || "every") + ")"),
|
|
1314
|
+
kvRow("P6 执行模式", p6ModeLabel(pr.p6Mode) + "(" + (pr.p6Mode || "builtin") + ")"),
|
|
1315
|
+
kvRow("测试命令", pr.testCommand ? pr.testCommand : "(未配置 · P8 自动探测 npm test)", !!pr.testCommand)),
|
|
1316
|
+
h("div", { style: { marginTop: 14 } },
|
|
1317
|
+
h("button", { type: "button", className: "btn sm", onClick: props.onGoProjects },
|
|
1318
|
+
Ic("folder", 12), " 到「项目」页编辑")));
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// 阶段详情 · 说明 tab:职责 / 输入输出 / 委托契约 / 专属参数表
|
|
1322
|
+
function StageGuideCard(props) {
|
|
1323
|
+
const s = STAGES.find(function (x) { return x.id === props.stageId; });
|
|
1324
|
+
if (!s) return null;
|
|
1325
|
+
const d = props.defaults && props.defaults.stages ? props.defaults.stages[s.id] : null;
|
|
1326
|
+
const caps = (d && d.caps) || {};
|
|
1327
|
+
const params = (d && d.params) || {};
|
|
1328
|
+
const paramKeys = Object.keys(params);
|
|
1329
|
+
return h("div", null,
|
|
1330
|
+
h("p", { className: "run-hint", style: { margin: "0 0 12px" } }, s.about),
|
|
1331
|
+
h("div", { className: "kv", style: { marginBottom: 14 } },
|
|
1332
|
+
kvRow("职责", s.desc),
|
|
1333
|
+
kvRow("产物", s.art, true),
|
|
1334
|
+
kvRow("复核门", s.key ? "是 · 产物就绪需人工通过后推进" : s.bypass ? "否 · 失败旁路,仅失败时执行" : "否 · 产物就绪自动推进"),
|
|
1335
|
+
kvRow("能力", [
|
|
1336
|
+
caps.route ? "可调模型/思考深度" : null,
|
|
1337
|
+
caps.exec ? "可调超时" + (caps.test ? "/测试命令" : "/maxTokens") : null,
|
|
1338
|
+
caps.delegate ? "可委托外部智能体" : null,
|
|
1339
|
+
!caps.route && !caps.delegate ? "确定性执行(不调用大模型)" : null,
|
|
1340
|
+
].filter(Boolean).join(" · ")),
|
|
1341
|
+
d && d.delegateSpec ? kvRow("委托产出", d.delegateSpec.output, true) : null,
|
|
1342
|
+
d && d.delegateSpec ? kvRow("输出契约", d.delegateSpec.contract, true) : null),
|
|
1343
|
+
h("span", { className: "f-label" }, "本阶段专属参数"),
|
|
1344
|
+
paramKeys.length ? h("table", { className: "tbl", style: { margin: "6px 0 0" } },
|
|
1345
|
+
h("thead", null, h("tr", null,
|
|
1346
|
+
h("th", null, "参数"), h("th", null, "默认值"), h("th", null, "说明"))),
|
|
1347
|
+
h("tbody", null, paramKeys.map(function (k) {
|
|
1348
|
+
const meta = params[k];
|
|
1349
|
+
return h("tr", { key: k },
|
|
1350
|
+
h("td", null, h("code", null, k)),
|
|
1351
|
+
h("td", null, h("code", null, meta.type === "string" && !meta.def ? "(自动探测)" : String(meta.def) + (meta.unit ? " " + meta.unit : ""))),
|
|
1352
|
+
h("td", null, meta.label + "——" + meta.hint));
|
|
1353
|
+
})))
|
|
1354
|
+
: h("p", { className: "hint-line", style: { margin: "4px 0 0" } },
|
|
1355
|
+
"本阶段没有专属参数;通用项(提示词 / 模型 / 思考深度 / 超时 / 委托)在「配置」tab 调整。"),
|
|
1356
|
+
h("p", { className: "hint-line" },
|
|
1357
|
+
"参数与提示词的当前生效值都可以在「配置」tab 修改;保存后对进行中的 Run 从下一阶段起生效。"));
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function RunsPanel(props) {
|
|
1361
|
+
const p = props;
|
|
1362
|
+
const [rerunStage, setRerunStage] = React.useState(null);
|
|
1363
|
+
const [selStage, setSelStage] = React.useState(null);
|
|
1364
|
+
// 阶段详情二级菜单(与工作台一级目录同名同序:项目/运行/产物/配置/说明,聚焦当前阶段)
|
|
1365
|
+
const [stTab, setStTab] = React.useState("run");
|
|
1366
|
+
const [selArt, setSelArt] = React.useState(null);
|
|
1367
|
+
const [artText, setArtText] = React.useState(null);
|
|
1368
|
+
const [comment, setComment] = React.useState("");
|
|
1369
|
+
const [reviews, setReviews] = React.useState(null);
|
|
1370
|
+
const [ledger, setLedger] = React.useState(null);
|
|
1371
|
+
const [events, setEvents] = React.useState(null);
|
|
1372
|
+
const [openEv, setOpenEv] = React.useState(-1);
|
|
1373
|
+
const evRef = React.useRef(null);
|
|
1374
|
+
const commentRef = React.useRef(null);
|
|
1375
|
+
const artRef = React.useRef(null);
|
|
1376
|
+
React.useEffect(function () { artRef.current = selArt; }, [selArt]);
|
|
1377
|
+
|
|
1378
|
+
// run 切换:重置选择(二级菜单回「运行」)
|
|
1379
|
+
React.useEffect(function () {
|
|
1380
|
+
setSelArt(null); setArtText(null); setComment(""); setRerunStage(null); setStTab("run");
|
|
1381
|
+
setReviews(null); setLedger(null); setEvents(null); setOpenEv(-1); evRef.current = null;
|
|
1382
|
+
if (p.run) setSelStage(p.run.current || "P1");
|
|
1383
|
+
else setSelStage(null);
|
|
1384
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1385
|
+
}, [p.runId]);
|
|
1386
|
+
// 流水线停住(待复核/失败/停止/完成)时钉到 run.current
|
|
1387
|
+
React.useEffect(function () {
|
|
1388
|
+
if (!p.run) return;
|
|
1389
|
+
if (["awaiting_review", "failed", "completed", "stopped"].indexOf(p.run.status) >= 0) {
|
|
1390
|
+
setSelStage(p.run.current || null);
|
|
1391
|
+
if (!rerunStage) setRerunStage(p.run.current || null);
|
|
1392
|
+
} else if (!selStage) {
|
|
1393
|
+
setSelStage(p.run.current || null);
|
|
1394
|
+
}
|
|
1395
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1396
|
+
}, [p.run && p.run.status, p.run && p.run.current, p.runId]);
|
|
1397
|
+
|
|
1398
|
+
// 阶段产物文件列表(来自 run 产物树,按阶段产物根过滤)
|
|
1399
|
+
const listFiles = function (stageId) {
|
|
1400
|
+
const s = STAGES.find(function (x) { return x.id === stageId; });
|
|
1401
|
+
if (!s || !p.tree) return [];
|
|
1402
|
+
const root = s.art;
|
|
1403
|
+
return p.tree.filter(function (f) {
|
|
1404
|
+
return f.path === root || f.path.indexOf(root + "/") === 0;
|
|
1405
|
+
});
|
|
1406
|
+
};
|
|
1407
|
+
const files = listFiles(selStage);
|
|
1408
|
+
const filesKey = selStage + "|" + files.map(function (f) { return f.path; }).join(",");
|
|
1409
|
+
|
|
1410
|
+
const fetchArt = function (path) {
|
|
1411
|
+
artRef.current = path;
|
|
1412
|
+
setArtText(null);
|
|
1413
|
+
apiGet("/projects/" + p.slug + "/runs/" + p.runId + "/artifact?path=" + encodeURIComponent(path))
|
|
1414
|
+
.then(function (r) {
|
|
1415
|
+
if (artRef.current !== path) return; // 乱序守卫
|
|
1416
|
+
if (r && r.ok) setArtText(r.text);
|
|
1417
|
+
else p.toast((r && r.message) || "读取产物失败", "bad");
|
|
1418
|
+
})
|
|
1419
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// 阶段或产物文件变化 → 自动选中第一个产物并读取
|
|
1423
|
+
React.useEffect(function () {
|
|
1424
|
+
if (!selStage || !p.runId) return;
|
|
1425
|
+
const fs = listFiles(selStage);
|
|
1426
|
+
if (!fs.length) { setSelArt(null); setArtText(null); return; }
|
|
1427
|
+
setSelArt(fs[0].path);
|
|
1428
|
+
fetchArt(fs[0].path);
|
|
1429
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1430
|
+
}, [selStage, filesKey, p.runId]);
|
|
1431
|
+
|
|
1432
|
+
// 复核历史:reviews/*.json 全量读取(文件名含时间戳,倒序 = 最新在前)
|
|
1433
|
+
React.useEffect(function () {
|
|
1434
|
+
if (!p.tree || !p.runId) { setReviews(null); return; }
|
|
1435
|
+
const revs = p.tree.filter(function (f) { return f.path.indexOf("reviews/") === 0; })
|
|
1436
|
+
.sort().reverse();
|
|
1437
|
+
if (!revs.length) { setReviews([]); return; }
|
|
1438
|
+
Promise.all(revs.map(function (f) {
|
|
1439
|
+
return apiGet("/projects/" + p.slug + "/runs/" + p.runId + "/artifact?path=" + encodeURIComponent(f.path))
|
|
1440
|
+
.then(function (r) {
|
|
1441
|
+
if (!r || !r.ok) return null;
|
|
1442
|
+
try { return JSON.parse(r.text); } catch (e) { return null; }
|
|
1443
|
+
}).catch(function () { return null; });
|
|
1444
|
+
})).then(function (list) { setReviews(list.filter(Boolean)); });
|
|
1445
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1446
|
+
}, [p.tree, p.runId]);
|
|
1447
|
+
|
|
1448
|
+
// P7:解析 patch ledger(每行一个 JSON:applied 记录 / rollback 记录)
|
|
1449
|
+
React.useEffect(function () {
|
|
1450
|
+
if (selStage !== "P7" || !p.tree || !p.runId) { setLedger(null); return; }
|
|
1451
|
+
const ent = p.tree.find(function (f) { return f.path === "ledger/patch-ledger.jsonl"; });
|
|
1452
|
+
if (!ent) { setLedger([]); return; }
|
|
1453
|
+
apiGet("/projects/" + p.slug + "/runs/" + p.runId + "/artifact?path=" + encodeURIComponent(ent.path))
|
|
1454
|
+
.then(function (r) {
|
|
1455
|
+
if (!r || !r.ok) { setLedger([]); return; }
|
|
1456
|
+
const rows = String(r.text).split("\n").filter(Boolean).map(function (line, i) {
|
|
1457
|
+
try { return { no: i, ...JSON.parse(line) }; } catch (e) { return { no: i, raw: line }; }
|
|
1458
|
+
});
|
|
1459
|
+
setLedger(rows);
|
|
1460
|
+
}).catch(function () { setLedger([]); });
|
|
1461
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1462
|
+
}, [selStage, p.tree, p.runId]);
|
|
1463
|
+
|
|
1464
|
+
// 过程事件:trace/events.jsonl(mtime 未变不重拉;轮询刷新 = 近实时跟进)
|
|
1465
|
+
React.useEffect(function () {
|
|
1466
|
+
if (!p.tree || !p.runId) { setEvents(null); return; }
|
|
1467
|
+
const ent = p.tree.find(function (f) { return f.path === "trace/events.jsonl"; });
|
|
1468
|
+
if (!ent) { setEvents([]); return; }
|
|
1469
|
+
const key = p.runId + "|" + ent.mtimeMs;
|
|
1470
|
+
if (evRef.current === key) return;
|
|
1471
|
+
evRef.current = key;
|
|
1472
|
+
apiGet("/projects/" + p.slug + "/runs/" + p.runId + "/artifact?path=" + encodeURIComponent("trace/events.jsonl"))
|
|
1473
|
+
.then(function (r) {
|
|
1474
|
+
if (evRef.current !== key) return; // 乱序守卫
|
|
1475
|
+
if (!r || !r.ok) { setEvents([]); return; }
|
|
1476
|
+
const list = String(r.text).split("\n").filter(Boolean).map(function (line, i) {
|
|
1477
|
+
try { return Object.assign(JSON.parse(line), { no: i }); } catch (e) { return null; }
|
|
1478
|
+
}).filter(Boolean);
|
|
1479
|
+
setEvents(list);
|
|
1480
|
+
}).catch(function () { setEvents([]); });
|
|
1481
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1482
|
+
}, [p.tree, p.runId]);
|
|
1483
|
+
|
|
1484
|
+
const review = function (decision) {
|
|
1485
|
+
if (!p.runId) return;
|
|
1486
|
+
if (decision === "reject" && !(comment || "").trim()) {
|
|
1487
|
+
p.toast("打回需要填写复核意见", "bad");
|
|
1488
|
+
if (commentRef.current) commentRef.current.focus();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/review", { decision: decision, comment: comment })
|
|
1492
|
+
.then(function (r) {
|
|
1493
|
+
if (r && r.ok) {
|
|
1494
|
+
p.toast(r.message || (decision === "approve" ? "已通过" : "已打回"));
|
|
1495
|
+
setComment("");
|
|
1496
|
+
p.onChanged();
|
|
1497
|
+
} else {
|
|
1498
|
+
p.toast((r && r.message) || "复核提交失败", "bad");
|
|
1499
|
+
}
|
|
1500
|
+
})
|
|
1501
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1502
|
+
};
|
|
1503
|
+
|
|
1504
|
+
// —— Run 控制:停止 / 回退重跑 / 删除 / 打开目录 ——
|
|
1505
|
+
const runStatus = p.run && p.run.status;
|
|
1506
|
+
const stopRun = function () {
|
|
1507
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/stop", {})
|
|
1508
|
+
.then(function (r) {
|
|
1509
|
+
p.toast((r && r.message) || (r && r.ok ? "已停止" : "停止失败"), r && r.ok ? "ok" : "bad");
|
|
1510
|
+
if (r && r.ok) p.onChanged();
|
|
1511
|
+
})
|
|
1512
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1513
|
+
};
|
|
1514
|
+
const rerunFrom = function () {
|
|
1515
|
+
if (!rerunStage) { p.toast("请选择要回退到的阶段", "bad"); return; }
|
|
1516
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/rerun", { stage: rerunStage })
|
|
1517
|
+
.then(function (r) {
|
|
1518
|
+
p.toast((r && r.message) || (r && r.ok ? "已重跑" : "重跑失败"), r && r.ok ? "ok" : "bad");
|
|
1519
|
+
if (r && r.ok) p.onChanged();
|
|
1520
|
+
})
|
|
1521
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1522
|
+
};
|
|
1523
|
+
const deleteRun = function () {
|
|
1524
|
+
if (!window.confirm("确认删除 Run " + p.runId + "?产物目录将一并删除,不可恢复。")) return;
|
|
1525
|
+
apiDelete("/projects/" + p.slug + "/runs/" + p.runId)
|
|
1526
|
+
.then(function (r) {
|
|
1527
|
+
if (r && r.ok) { p.toast("已删除"); p.onDeleted(); }
|
|
1528
|
+
else p.toast((r && r.message) || "删除失败", "bad");
|
|
1529
|
+
})
|
|
1530
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1531
|
+
};
|
|
1532
|
+
const openDir = function () {
|
|
1533
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/open", {})
|
|
1534
|
+
.then(function (r) {
|
|
1535
|
+
if (r && r.ok) p.toast(r.message || "已打开产物目录");
|
|
1536
|
+
else p.toast((r && r.message) || "打开失败", "bad");
|
|
1537
|
+
})
|
|
1538
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1539
|
+
};
|
|
1540
|
+
const rollback = function (lineNo) {
|
|
1541
|
+
if (!window.confirm("回滚 ledger 第 " + lineNo + " 行的 patch?\n只反向撤销该 patch,不影响其他修改。")) return;
|
|
1542
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/rollback", { lineNo: lineNo })
|
|
1543
|
+
.then(function (r) {
|
|
1544
|
+
if (r && r.ok) { p.toast("已回滚 patch #" + lineNo); p.onChanged(); }
|
|
1545
|
+
else p.toast((r && r.message) || "回滚失败", "bad");
|
|
1546
|
+
})
|
|
1547
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
// —— 渲染 ——
|
|
1551
|
+
const options = (p.runs && p.runs.length ? p.runs : []).map(function (r) {
|
|
1552
|
+
const c = tag(r.status);
|
|
1553
|
+
return h("option", { key: r.id, value: r.id },
|
|
1554
|
+
r.id + "(" + c[1] + " · " + kindLabel(r.trigger && r.trigger.kind) + ")");
|
|
1555
|
+
});
|
|
1556
|
+
if (!options.length) {
|
|
1557
|
+
options.push(h("option", { key: "__none__", value: "" }, "(暂无 Run · 请先在「01 项目」发起)"));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const sDef = STAGES.find(function (x) { return x.id === selStage; });
|
|
1561
|
+
const st = p.run && p.run.stages ? p.run.stages[selStage] : null;
|
|
1562
|
+
const stStatus = st ? st.status : null;
|
|
1563
|
+
// session/claude 模式的 P6 待复核 = 任务包等外部执行(按 p6Mode 推导,兼容旧 run)
|
|
1564
|
+
// claude 委托成功(externalExec.status=done)后实施已完成,走正常"待复核";执行中则显示"claude 执行中"
|
|
1565
|
+
const ee = p.run ? p.run.externalExec : null;
|
|
1566
|
+
const stExternal = selStage === "P6" && stStatus === "awaiting_review"
|
|
1567
|
+
&& !!(p.run && (p.run.p6Mode === "session" || p.run.p6Mode === "claude"))
|
|
1568
|
+
&& !(ee && ee.status === "done");
|
|
1569
|
+
const stClaudeRun = selStage === "P6" && stStatus === "running" && !!(ee && ee.status === "running");
|
|
1570
|
+
|
|
1571
|
+
let viewHtml = '<span class="c">点击左侧任一阶段查看产物。</span>';
|
|
1572
|
+
if (!sDef) viewHtml = '<span class="c">点击左侧任一阶段查看产物。</span>';
|
|
1573
|
+
else if (artText != null) viewHtml = renderView(artText, selArt);
|
|
1574
|
+
else if (selArt) viewHtml = '<span class="c">读取中…</span>';
|
|
1575
|
+
else if (!stStatus || stStatus === "pending") viewHtml = '<span class="c">该阶段尚未运行,暂无产物。</span>';
|
|
1576
|
+
else if (stStatus === "failed") viewHtml = '<span class="c">该阶段执行失败:' + esc(st.error || "未知错误") + "</span>";
|
|
1577
|
+
else viewHtml = '<span class="c">该阶段暂无文件产物(执行中或产物为目录)。</span>';
|
|
1578
|
+
// .md 产物改走宿主官方 MarkdownText(替代源码文本)
|
|
1579
|
+
const artMd = artText != null && /\.md$/i.test(selArt || "");
|
|
1580
|
+
|
|
1581
|
+
const showReview = stStatus === "awaiting_review";
|
|
1582
|
+
const stageEvents = (events || []).filter(function (e) { return e.stage === selStage; });
|
|
1583
|
+
const reached = STAGES.filter(function (s) {
|
|
1584
|
+
const x = p.run && p.run.stages ? p.run.stages[s.id] : null;
|
|
1585
|
+
return x && x.status !== "pending";
|
|
1586
|
+
}).map(function (s) {
|
|
1587
|
+
return h("option", { key: s.id, value: s.id }, s.id + " · " + s.name);
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
// 产物 tab:阶段文件按目录分组(patches/ 等子目录平铺为组;"·" = 产物根)
|
|
1591
|
+
const artGroups = (function () {
|
|
1592
|
+
if (!sDef) return [];
|
|
1593
|
+
const root = sDef.art;
|
|
1594
|
+
const groups = {};
|
|
1595
|
+
files.forEach(function (f) {
|
|
1596
|
+
const sub = root.charAt(root.length - 1) === "/" ? f.path.slice(root.length) : f.path.split("/").pop();
|
|
1597
|
+
const seg = sub.split("/");
|
|
1598
|
+
const g = seg.length > 1 ? seg[0] + "/" : "·";
|
|
1599
|
+
(groups[g] || (groups[g] = [])).push(f);
|
|
1600
|
+
});
|
|
1601
|
+
return Object.keys(groups).sort().map(function (g) { return [g, groups[g]]; });
|
|
1602
|
+
})();
|
|
1603
|
+
const curProject = p.projects ? p.projects.find(function (x) { return x.slug === p.slug; }) : null;
|
|
1604
|
+
|
|
1605
|
+
return h("div", null,
|
|
1606
|
+
h("div", { className: "run-bar" },
|
|
1607
|
+
h("label", { className: "f-label", htmlFor: "run-select", style: { margin: 0 } }, "当前 Run"),
|
|
1608
|
+
h("select", {
|
|
1609
|
+
className: "f-select mono", id: "run-select", value: p.runId || "",
|
|
1610
|
+
onChange: function (e) { p.onPickRun(e.target.value); },
|
|
1611
|
+
}, options),
|
|
1612
|
+
runStatus ? h("span", { className: "tg " + tag(runStatus)[0] + (runStatus === "running" ? " run" : "") }, tag(runStatus)[1]) : null,
|
|
1613
|
+
p.runId && (runStatus === "running" || runStatus === "awaiting_review")
|
|
1614
|
+
? h("button", { type: "button", className: "btn sm", title: "当前阶段执行完即停", onClick: stopRun }, Ic("stop", 12), " 停止") : null,
|
|
1615
|
+
p.runId && runStatus && runStatus !== "running" && reached.length
|
|
1616
|
+
? h("span", { style: { display: "inline-flex", gap: 6, alignItems: "center" } },
|
|
1617
|
+
h("select", {
|
|
1618
|
+
className: "f-select mono", style: { minWidth: 170 }, value: rerunStage || "",
|
|
1619
|
+
"aria-label": "回退到的阶段",
|
|
1620
|
+
onChange: function (e) { setRerunStage(e.target.value); },
|
|
1621
|
+
}, reached),
|
|
1622
|
+
h("button", { type: "button", className: "btn sm", title: "该阶段及其后全部重跑", onClick: rerunFrom }, Ic("redo", 12), " 重跑")) : null,
|
|
1623
|
+
p.runId ? h("button", { type: "button", className: "btn sm", onClick: openDir }, Ic("folder", 13), " 打开目录") : null,
|
|
1624
|
+
p.runId ? h("button", { type: "button", className: "btn sm danger", onClick: deleteRun }, Ic("trash", 12), " 删除") : null),
|
|
1625
|
+
h("p", { className: "run-hint" },
|
|
1626
|
+
"点击阶段查看详情——每个阶段内含 ", h("b", null, "项目 / 运行 / 产物 / 配置 / 说明"),
|
|
1627
|
+
" 五个视图(配置可直接调本阶段参数);", h("b", null, "待复核"), " 时在「运行」视图通过或打回;",
|
|
1628
|
+
h("b", null, "停止"), " = 当前阶段跑完即停;", h("b", null, "重跑"), " = 从所选阶段重新推进;",
|
|
1629
|
+
h("b", null, "删除"), " = 移除整个 Run 目录。P10 为失败旁路(仅失败时执行)。"),
|
|
1630
|
+
h("div", { className: "pipe-layout" },
|
|
1631
|
+
h("div", { className: "steps", role: "list", "aria-label": "流水线阶段" },
|
|
1632
|
+
STAGES.map(function (s) {
|
|
1633
|
+
const cur = p.run && p.run.stages ? p.run.stages[s.id] : null;
|
|
1634
|
+
const status = cur ? cur.status : "pending";
|
|
1635
|
+
// session/claude 模式的 P6 待复核 = 任务包等外部执行(claude 委托成功后显示正常"待复核")
|
|
1636
|
+
const eeRow = p.run ? p.run.externalExec : null;
|
|
1637
|
+
const ext = s.id === "P6" && status === "awaiting_review"
|
|
1638
|
+
&& !!(p.run && (p.run.p6Mode === "session" || p.run.p6Mode === "claude"))
|
|
1639
|
+
&& !(eeRow && eeRow.status === "done");
|
|
1640
|
+
// claude 委托执行中:P6 处于 running,徽标准明示由 claude 执行
|
|
1641
|
+
const claudeRun = s.id === "P6" && status === "running" && !!(eeRow && eeRow.status === "running");
|
|
1642
|
+
const c = claudeRun ? ["t-acc", "claude 执行中"] : ext ? ["t-warn", "等外部执行"] : tag(status);
|
|
1643
|
+
const dur = cur ? fmtDur(cur.startedAt, cur.finishedAt) : "";
|
|
1644
|
+
const extra = cur && cur.attempts ? " · 重试" + cur.attempts : "";
|
|
1645
|
+
const xp = (ext || claudeRun) ? p.run.externalProgress : null;
|
|
1646
|
+
const extMeta = (ext || claudeRun)
|
|
1647
|
+
? "patch " + (xp ? xp.patches : 0) + "/" + (xp && xp.tasks != null ? xp.tasks : "?") + (xp && xp.report ? " · report 已写" : "")
|
|
1648
|
+
: "";
|
|
1649
|
+
return h("button", {
|
|
1650
|
+
key: s.id, role: "listitem",
|
|
1651
|
+
className: "step-row " + status + (s.bypass ? " bypass" : "") + (s.id === selStage ? " on" : ""),
|
|
1652
|
+
onClick: function () { setSelStage(s.id); },
|
|
1653
|
+
},
|
|
1654
|
+
h("span", { className: "sdot" }),
|
|
1655
|
+
h("span", { className: "s-main" },
|
|
1656
|
+
h("span", { className: "s-name" }, s.id + " · " + s.name),
|
|
1657
|
+
h("span", { className: "s-desc" }, s.desc + " · " + s.art)),
|
|
1658
|
+
h("span", { className: "s-side" },
|
|
1659
|
+
h("span", { className: "tg " + c[0] + (status === "running" ? " run" : "") }, c[1]),
|
|
1660
|
+
(extMeta || dur || extra) ? h("span", { className: "s-dur" }, extMeta || (dur + extra)) : null));
|
|
1661
|
+
})),
|
|
1662
|
+
h("div", { className: "card" },
|
|
1663
|
+
h("div", { className: "detail-head" },
|
|
1664
|
+
h("div", null,
|
|
1665
|
+
h("h3", null, sDef ? sDef.id + " · " + sDef.name : "阶段详情"),
|
|
1666
|
+
h("div", { className: "path" },
|
|
1667
|
+
"产物:" + (p.runId ? "runs\\" + p.runId + "\\" + (sDef ? sDef.art.replace(/\//g, "\\") : "") : ""))),
|
|
1668
|
+
h("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
1669
|
+
stStatus ? h("span", { className: "tg " + tag(stStatus)[0] + (stClaudeRun ? " run" : "") }, stClaudeRun ? "claude 执行中" : stExternal ? "等外部执行" : tag(stStatus)[1]) : null)),
|
|
1670
|
+
// 二级菜单条:五项与工作台一级目录同名同序,全部聚焦当前阶段
|
|
1671
|
+
// (运行=过程与复核;项目=本 Run 项目上下文;产物=本阶段文件树;配置=本阶段全量参数;说明=职责契约)
|
|
1672
|
+
sDef ? h("div", { className: "stg-tabs", role: "tablist", "aria-label": "阶段视图" },
|
|
1673
|
+
[["run", "运行"], ["project", "项目"], ["artifacts", "产物"], ["config", "配置"], ["guide", "说明"]].map(function (t) {
|
|
1674
|
+
return h("button", {
|
|
1675
|
+
key: t[0], role: "tab", className: "stg-tab" + (stTab === t[0] ? " on" : ""),
|
|
1676
|
+
"aria-selected": stTab === t[0] ? "true" : "false",
|
|
1677
|
+
onClick: function () { setStTab(t[0]); },
|
|
1678
|
+
}, t[1]);
|
|
1679
|
+
})) : null,
|
|
1680
|
+
stTab === "run" ? h("div", null,
|
|
1681
|
+
// —— 阶段契约(本阶段输入 → 输出约定;过程与复核见下方) ——
|
|
1682
|
+
h(StageContractCard, { stageId: selStage, defaults: p.defaults }),
|
|
1683
|
+
files.length ? h("div", { className: "a-tabs" },
|
|
1684
|
+
files.map(function (f) {
|
|
1685
|
+
return h("button", {
|
|
1686
|
+
key: f.path,
|
|
1687
|
+
className: "a-tab" + (f.path === selArt ? " on" : ""),
|
|
1688
|
+
onClick: function () { setSelArt(f.path); fetchArt(f.path); },
|
|
1689
|
+
}, f.path.split("/").pop());
|
|
1690
|
+
})) : null,
|
|
1691
|
+
artMd
|
|
1692
|
+
? h("div", { className: "md-view", tabIndex: 0 }, h(MarkdownText, { text: artText }))
|
|
1693
|
+
: h("pre", { className: "view", tabIndex: 0, dangerouslySetInnerHTML: { __html: viewHtml } }),
|
|
1694
|
+
// 过程事件:本阶段的 LLM 调用 / git / 测试 / 工具调用(点击行展开详情)
|
|
1695
|
+
events != null && sDef ? h("div", { style: { marginTop: 14 } },
|
|
1696
|
+
h("div", { className: "ev-head" },
|
|
1697
|
+
h("span", { className: "f-label" }, "过程 · 本阶段调用记录"),
|
|
1698
|
+
h("span", { className: "hint" },
|
|
1699
|
+
"含大模型调用(蓝)、git(绿)、测试(黄)、工具(灰);点击行展开 prompt / 输出详情")),
|
|
1700
|
+
stageEvents.length ? h("div", { className: "ev-list", role: "log", "aria-label": "阶段过程事件" },
|
|
1701
|
+
stageEvents.slice().reverse().map(function (e) {
|
|
1702
|
+
const open = openEv === e.no;
|
|
1703
|
+
return h(React.Fragment, { key: e.no },
|
|
1704
|
+
h("button", {
|
|
1705
|
+
className: "ev-row" + (e.ok ? "" : " bad"),
|
|
1706
|
+
onClick: function () { setOpenEv(open ? -1 : e.no); },
|
|
1707
|
+
"aria-expanded": open ? "true" : "false",
|
|
1708
|
+
},
|
|
1709
|
+
h("span", { className: "ev-time" }, fmtClock(e.at)),
|
|
1710
|
+
h("span", { className: "ev-kind k-" + (e.kind || "info") }, EV_KIND[e.kind] || e.kind),
|
|
1711
|
+
h("span", { className: "ev-name" }, e.name),
|
|
1712
|
+
h("span", { className: "ev-ms" }, e.ms != null ? (e.ms >= 1000 ? (e.ms / 1000).toFixed(1) + "s" : e.ms + "ms") : ""),
|
|
1713
|
+
h("span", { className: "ev-ok", title: e.ok ? "成功" : "失败" })),
|
|
1714
|
+
open && e.detail ? h("pre", { className: "view ev-detail", tabIndex: 0,
|
|
1715
|
+
dangerouslySetInnerHTML: { __html: esc(e.detail) } }) : null);
|
|
1716
|
+
}))
|
|
1717
|
+
: h("div", { className: "ev-list" },
|
|
1718
|
+
h("p", { className: "ev-empty" }, "该阶段暂无调用记录(执行后自动写入 trace/events.jsonl)。"))) : null,
|
|
1719
|
+
// P7 专属:patch ledger 逐条回滚
|
|
1720
|
+
(selStage === "P7" && ledger && ledger.length) ? h("div", { className: "callout acc", style: { marginTop: 14 } },
|
|
1721
|
+
h("h4", null, "Patch Ledger · 逐条回滚"),
|
|
1722
|
+
h("p", { style: { margin: "0 0 6px", fontSize: 13.5 } },
|
|
1723
|
+
"回滚只反向撤销该条 Agent patch,不动你的其他修改;回滚会追加记录到 ledger。"),
|
|
1724
|
+
ledger.map(function (row) {
|
|
1725
|
+
if (row.rollbackOf != null) {
|
|
1726
|
+
return h("div", { key: "r" + row.no, className: "ledger-row" },
|
|
1727
|
+
h("span", { className: "no" }, "#" + row.no),
|
|
1728
|
+
h("span", { className: "pf" }, h("span", { className: "tg t-err" }, "回滚记录"), " 撤销了 #" + row.rollbackOf),
|
|
1729
|
+
h("span", { className: "at" }, fmtTime(row.at)),
|
|
1730
|
+
h("span", null));
|
|
1731
|
+
}
|
|
1732
|
+
return h("div", { key: "r" + row.no, className: "ledger-row" },
|
|
1733
|
+
h("span", { className: "no" }, "#" + row.no),
|
|
1734
|
+
h("span", { className: "pf" }, row.patch || row.raw || "?"),
|
|
1735
|
+
h("span", { className: "at" }, fmtTime(row.appliedAt)),
|
|
1736
|
+
h("button", { type: "button", className: "btn sm danger", onClick: function () { rollback(row.no); } }, "回滚"));
|
|
1737
|
+
})) : null,
|
|
1738
|
+
// P6 专属:外部执行(claude 委托)状态卡——执行中实时进度 / 完成统计(耗时·轮次·费用·总结)/ 失败原因
|
|
1739
|
+
(selStage === "P6" && ee) ? (function () {
|
|
1740
|
+
const running = ee.status === "running";
|
|
1741
|
+
const xpRow = p.run ? p.run.externalProgress : null;
|
|
1742
|
+
const stats = ee.stats || null;
|
|
1743
|
+
const elapsed = running && ee.startedAt ? fmtDur(ee.startedAt, new Date().toISOString())
|
|
1744
|
+
: fmtDur(ee.startedAt, ee.finishedAt);
|
|
1745
|
+
const statLine = stats && (stats.turns != null || stats.costUsd != null || stats.durationMs != null)
|
|
1746
|
+
? (stats.durationMs != null ? Math.round(stats.durationMs / 60000) + " 分钟" : "")
|
|
1747
|
+
+ (stats.turns != null ? " · " + stats.turns + " 轮" : "")
|
|
1748
|
+
+ (stats.costUsd != null ? " · $" + Number(stats.costUsd).toFixed(2) : "")
|
|
1749
|
+
: (elapsed ? elapsed : "");
|
|
1750
|
+
return h("div", { className: "callout acc", style: { marginTop: 14 } },
|
|
1751
|
+
h("h4", null, "外部执行 · Claude Code"),
|
|
1752
|
+
h("p", { style: { margin: "0 0 6px", fontSize: 13.5 } },
|
|
1753
|
+
h("span", { className: "tg " + (running ? "t-acc run" : ee.status === "done" ? "t-good" : "t-err") },
|
|
1754
|
+
running ? "执行中" : ee.status === "done" ? "已完成" : ee.status === "failed" ? "失败" : "未启动"),
|
|
1755
|
+
" · " + (running ? "已用 " + (elapsed || "—") : (statLine ? "耗时 " + statLine : "—")),
|
|
1756
|
+
running && xpRow ? h("span", null, " · patch ", h("b", null, String(xpRow.patches) + "/" + (xpRow.tasks != null ? xpRow.tasks : "?")) + (xpRow.report ? " · report 已写" : "")) : null),
|
|
1757
|
+
h("p", { style: { margin: 0, fontSize: 12.5, wordBreak: "break-all" } },
|
|
1758
|
+
"bin ", h("span", { style: { fontFamily: "var(--mono)" } }, String(ee.bin || "")),
|
|
1759
|
+
ee.startedAt ? " · 开始 " + fmtClock(ee.startedAt) : "",
|
|
1760
|
+
ee.finishedAt ? " · 结束 " + fmtClock(ee.finishedAt) : ""),
|
|
1761
|
+
stats && stats.result
|
|
1762
|
+
? h("p", { style: { margin: "6px 0 0", fontSize: 13 } },
|
|
1763
|
+
h("span", { className: "f-label" }, "claude 总结"), h("br", null),
|
|
1764
|
+
h("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5 } }, stats.result))
|
|
1765
|
+
: null);
|
|
1766
|
+
})() : null,
|
|
1767
|
+
// 复核门(session 模式的 P6:先外部执行任务包,未就绪不能通过)
|
|
1768
|
+
showReview ? (function () {
|
|
1769
|
+
const xp = p.run ? p.run.externalProgress : null;
|
|
1770
|
+
const extReady = !stExternal || !!(xp && (xp.patches > 0 || xp.report));
|
|
1771
|
+
return h("div", { className: "callout warn", style: { marginTop: 14 } },
|
|
1772
|
+
stExternal
|
|
1773
|
+
? h("div", null,
|
|
1774
|
+
h("h4", null, ee && (ee.status === "failed" || ee.status === "skipped")
|
|
1775
|
+
? "P6 · Claude Code 执行未成功,待人工接管"
|
|
1776
|
+
: "P6 session 模式 · 任务包待外部执行"),
|
|
1777
|
+
h("p", { style: { margin: "0 0 6px", fontSize: 13.5 } },
|
|
1778
|
+
"本阶段只生成了任务包,代码修改还没有发生。流程:把 ",
|
|
1779
|
+
h("span", { style: { fontFamily: "var(--mono)" } }, "runs\\…\\06-implementation\\session-task.md"),
|
|
1780
|
+
" 交给 DSH 会话执行 → 会话产出 ",
|
|
1781
|
+
h("span", { style: { fontFamily: "var(--mono)" } }, "patches\\*.diff"),
|
|
1782
|
+
" 与 ", h("span", { style: { fontFamily: "var(--mono)" } }, "coder-report.json"),
|
|
1783
|
+
" → 回到这里通过复核。"),
|
|
1784
|
+
h("p", { style: { margin: "0 0 8px", fontSize: 13.5 } },
|
|
1785
|
+
"当前进度:patch ", h("b", null, String(xp ? xp.patches : 0) + "/" + (xp && xp.tasks != null ? xp.tasks : "?")),
|
|
1786
|
+
" · report ", h("b", null, xp && xp.report ? "已写" : "未写"),
|
|
1787
|
+
extReady ? "" : "(未就绪,不能通过)"),
|
|
1788
|
+
(ee && (ee.status === "failed" || ee.status === "skipped"))
|
|
1789
|
+
? h("p", { style: { margin: "0 0 8px", fontSize: 13 } },
|
|
1790
|
+
"Claude Code 未产出补丁:",
|
|
1791
|
+
h("span", { style: { fontFamily: "var(--mono)" } }, String(ee.error || "无详情")),
|
|
1792
|
+
"(详见 06-implementation\\external-exec.log)")
|
|
1793
|
+
: null)
|
|
1794
|
+
: h("p", { style: { margin: "0 0 8px", fontSize: 13.5 } },
|
|
1795
|
+
"该阶段产物已生成。通过后进入下一阶段;打回将把意见注入本阶段重跑(决定写入 ",
|
|
1796
|
+
h("span", { style: { fontFamily: "var(--mono)" } }, "reviews\\"), ")。"),
|
|
1797
|
+
h("div", { className: "review-actions" },
|
|
1798
|
+
h("textarea", {
|
|
1799
|
+
className: "f-input", ref: commentRef, value: comment,
|
|
1800
|
+
placeholder: "复核意见(打回必填,通过可选)…", "aria-label": "复核意见",
|
|
1801
|
+
onChange: function (e) { setComment(e.target.value); },
|
|
1802
|
+
}),
|
|
1803
|
+
h("button", {
|
|
1804
|
+
className: "btn pri", disabled: !extReady,
|
|
1805
|
+
title: extReady ? "" : "patches/ 为空且无 coder-report.json:先在外部 DSH 会话执行任务包",
|
|
1806
|
+
onClick: function () { review("approve"); },
|
|
1807
|
+
}, Ic("check"), " 通过,继续"),
|
|
1808
|
+
h("button", { className: "btn danger", onClick: function () { review("reject"); } }, Ic("x"), " 打回重跑")));
|
|
1809
|
+
})() : null,
|
|
1810
|
+
// 复核历史
|
|
1811
|
+
(reviews && reviews.length) ? h("div", { style: { marginTop: 14 } },
|
|
1812
|
+
h("span", { className: "f-label" }, "复核历史"),
|
|
1813
|
+
reviews.map(function (r, i) {
|
|
1814
|
+
return h("div", { key: i, className: "rev-item" },
|
|
1815
|
+
h("span", { className: "tg " + (r.decision === "approve" ? "t-good" : "t-err") },
|
|
1816
|
+
(r.decision === "approve" ? "通过" : "打回") + " " + (r.stage || "")),
|
|
1817
|
+
h("span", { className: "at" }, fmtTime(r.at)),
|
|
1818
|
+
h("span", { className: "cm" }, r.comment || "无意见"));
|
|
1819
|
+
})) : null) : null,
|
|
1820
|
+
// —— 项目 tab:本 Run 的项目上下文(阶段执行读的就是这些配置) ——
|
|
1821
|
+
stTab === "project" ? h(StageProjectCard, { project: curProject, onGoProjects: p.onGoProjects }) : null,
|
|
1822
|
+
// —— 产物 tab:本阶段文件树(目录分组)+ 预览 ——
|
|
1823
|
+
stTab === "artifacts" && sDef ? h("div", { className: "stg-art" },
|
|
1824
|
+
h("div", null,
|
|
1825
|
+
artGroups.length ? artGroups.map(function (g) {
|
|
1826
|
+
return h(React.Fragment, { key: g[0] },
|
|
1827
|
+
h("div", { className: "stg-fgroup" }, g[0] === "·" ? "阶段产物" : g[0]),
|
|
1828
|
+
g[1].map(function (f) {
|
|
1829
|
+
return h("button", {
|
|
1830
|
+
key: f.path, className: "stg-file" + (f.path === selArt ? " on" : ""),
|
|
1831
|
+
onClick: function () { setSelArt(f.path); fetchArt(f.path); },
|
|
1832
|
+
},
|
|
1833
|
+
h("span", null, f.path.split("/").pop()),
|
|
1834
|
+
h("span", { className: "ts" }, fmtSize(f.size)));
|
|
1835
|
+
}));
|
|
1836
|
+
}) : h("p", { className: "empty-hint" }, "该阶段暂无产物文件(执行后自动生成)。")),
|
|
1837
|
+
artMd && artText != null
|
|
1838
|
+
? h("div", { className: "md-view", tabIndex: 0 }, h(MarkdownText, { text: artText }))
|
|
1839
|
+
: h("pre", { className: "view", tabIndex: 0, dangerouslySetInnerHTML: { __html: viewHtml } })) : null,
|
|
1840
|
+
// —— 配置 tab:本阶段全量参数(「配置」页表单嵌入,无左列) ——
|
|
1841
|
+
stTab === "config" ? h(StageConfigPanel, {
|
|
1842
|
+
embedded: true, defaults: p.defaults, projects: p.projects, slug: p.slug,
|
|
1843
|
+
selStage: selStage, toast: p.toast, onSaved: p.onSaved,
|
|
1844
|
+
}) : null,
|
|
1845
|
+
// —— 说明 tab:职责 / 契约 / 专属参数 ——
|
|
1846
|
+
stTab === "guide" ? h(StageGuideCard, { stageId: selStage, defaults: p.defaults }) : null)));
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/* ================================================================
|
|
1850
|
+
* 03 产物
|
|
1851
|
+
* ================================================================ */
|
|
1852
|
+
function ArtifactsPanel(props) {
|
|
1853
|
+
const p = props;
|
|
1854
|
+
const [selFile, setSelFile] = React.useState(null);
|
|
1855
|
+
const [fileText, setFileText] = React.useState(null);
|
|
1856
|
+
// 目录折叠态:键为目录相对路径(如 "ledger"),存在即折叠
|
|
1857
|
+
const [collapsed, setCollapsed] = React.useState({});
|
|
1858
|
+
const fileRef = React.useRef(null);
|
|
1859
|
+
React.useEffect(function () { fileRef.current = selFile; }, [selFile]);
|
|
1860
|
+
|
|
1861
|
+
React.useEffect(function () {
|
|
1862
|
+
setSelFile(null); setFileText(null); setCollapsed({});
|
|
1863
|
+
}, [p.runId]);
|
|
1864
|
+
|
|
1865
|
+
if (!p.runId) {
|
|
1866
|
+
return h("p", { className: "empty-hint" },
|
|
1867
|
+
"请先在「01 项目」发起 Run,或在「02 运行」选择一个 Run,再查看其产物树。");
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
const pick = function (path) {
|
|
1871
|
+
fileRef.current = path;
|
|
1872
|
+
setSelFile(path); setFileText(null);
|
|
1873
|
+
apiGet("/projects/" + p.slug + "/runs/" + p.runId + "/artifact?path=" + encodeURIComponent(path))
|
|
1874
|
+
.then(function (r) {
|
|
1875
|
+
if (fileRef.current !== path) return; // 乱序守卫
|
|
1876
|
+
if (r && r.ok) setFileText(r.text);
|
|
1877
|
+
else p.toast((r && r.message) || "读取产物失败", "bad");
|
|
1878
|
+
})
|
|
1879
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
const openDir = function () {
|
|
1883
|
+
apiPost("/projects/" + p.slug + "/runs/" + p.runId + "/open", {})
|
|
1884
|
+
.then(function (r) {
|
|
1885
|
+
if (r && r.ok) p.toast(r.message || "已打开产物目录");
|
|
1886
|
+
else p.toast((r && r.message) || "打开失败", "bad");
|
|
1887
|
+
})
|
|
1888
|
+
.catch(function (e) { p.toast("请求失败: " + e, "bad"); });
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
const indentStyle = function (depth) { return depth ? { paddingLeft: 8 + 16 * depth } : null; };
|
|
1892
|
+
|
|
1893
|
+
const renderNode = function (node, depth, prefix, out) {
|
|
1894
|
+
Object.keys(node.children).sort().forEach(function (k) {
|
|
1895
|
+
const c = node.children[k];
|
|
1896
|
+
const dirPath = prefix ? prefix + "/" + k : k;
|
|
1897
|
+
const open = !collapsed[dirPath];
|
|
1898
|
+
out.push(h("button", {
|
|
1899
|
+
key: dirPath,
|
|
1900
|
+
className: "dir",
|
|
1901
|
+
style: indentStyle(depth),
|
|
1902
|
+
"aria-expanded": String(open),
|
|
1903
|
+
onClick: function () {
|
|
1904
|
+
setCollapsed(function (prev) {
|
|
1905
|
+
const next = { ...prev };
|
|
1906
|
+
if (next[dirPath]) delete next[dirPath]; else next[dirPath] = true;
|
|
1907
|
+
return next;
|
|
1908
|
+
});
|
|
1909
|
+
},
|
|
1910
|
+
}, (open ? "▾ " : "▸ ") + k + "/"));
|
|
1911
|
+
if (open) renderNode(c, depth + 1, dirPath, out);
|
|
1912
|
+
});
|
|
1913
|
+
node.files.sort(function (a, b) { return a.path.localeCompare(b.path); }).forEach(function (f) {
|
|
1914
|
+
const on = selFile === f.path;
|
|
1915
|
+
out.push(h("button", {
|
|
1916
|
+
key: f.path,
|
|
1917
|
+
className: "file" + (on ? " on" : ""),
|
|
1918
|
+
style: indentStyle(depth),
|
|
1919
|
+
onClick: function () { pick(f.path); },
|
|
1920
|
+
},
|
|
1921
|
+
h("span", null, f.path.split("/").pop()),
|
|
1922
|
+
h("span", { className: "ts" }, fmtSize(f.size))));
|
|
1923
|
+
});
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
const root = { name: "", children: {}, files: [] };
|
|
1927
|
+
(p.tree || []).forEach(function (f) {
|
|
1928
|
+
const segs = f.path.split("/");
|
|
1929
|
+
let node = root;
|
|
1930
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
1931
|
+
node = node.children[segs[i]] || (node.children[segs[i]] = { name: segs[i], children: {}, files: [] });
|
|
1932
|
+
}
|
|
1933
|
+
node.files.push(f);
|
|
1934
|
+
});
|
|
1935
|
+
const treeItems = [];
|
|
1936
|
+
renderNode(root, 0, "", treeItems);
|
|
1937
|
+
|
|
1938
|
+
const viewHtml = fileText != null
|
|
1939
|
+
? renderView(fileText, selFile)
|
|
1940
|
+
: (selFile ? '<span class="c">读取中…</span>' : '<span class="c">点击左侧文件预览内容。</span>');
|
|
1941
|
+
const fileMd = fileText != null && /\.md$/i.test(selFile || "");
|
|
1942
|
+
|
|
1943
|
+
return h("div", null,
|
|
1944
|
+
h("p", { className: "run-hint" },
|
|
1945
|
+
"当前 Run:", h("b", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, p.runId),
|
|
1946
|
+
"(在「02 运行」切换;.md 产物按 Markdown 排版渲染;以下为该 Run 的完整产物目录,含 trace / reviews / ledger)"),
|
|
1947
|
+
h("div", { className: "art-layout" },
|
|
1948
|
+
h("div", { className: "card" },
|
|
1949
|
+
p.tree && p.tree.length
|
|
1950
|
+
? h("div", { className: "tree", "aria-label": "产物目录树" }, treeItems)
|
|
1951
|
+
: h("p", { className: "empty-hint" }, "该 Run 暂无产物(可能尚未开始)。")),
|
|
1952
|
+
h("div", { className: "card" },
|
|
1953
|
+
h("div", { className: "detail-head" },
|
|
1954
|
+
h("div", null,
|
|
1955
|
+
h("h3", { style: { fontSize: 16 } }, selFile ? selFile.split("/").pop() : "产物预览"),
|
|
1956
|
+
h("div", { className: "path" },
|
|
1957
|
+
"~/.dsh/issue2pr/projects\\" + p.slug + "\\runs\\" + p.runId + (selFile ? "\\" + selFile.replace(/\//g, "\\") : ""))),
|
|
1958
|
+
h("button", { type: "button", className: "btn sm", onClick: openDir }, Ic("folder", 13), " 打开所在目录")),
|
|
1959
|
+
fileMd
|
|
1960
|
+
? h("div", { className: "md-view", tabIndex: 0, style: { maxHeight: 520 } }, h(MarkdownText, { text: fileText }))
|
|
1961
|
+
: h("pre", { className: "view", tabIndex: 0, style: { maxHeight: 520 }, dangerouslySetInnerHTML: { __html: viewHtml } }))));
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/* ================================================================
|
|
1965
|
+
* 04 配置 · 每阶段参数(提示词 / 模型 / 思考深度 / 超时 / 委托外部智能体)
|
|
1966
|
+
* 数据流:默认值来自 GET /stage-defaults(node 半 STAGE_DEFS);
|
|
1967
|
+
* 用户覆盖存 project.json 的 stageConfig 字段;P6 执行模式与项目页 p6Mode 同源。
|
|
1968
|
+
* ================================================================ */
|
|
1969
|
+
function StageConfigPanel(props) {
|
|
1970
|
+
const p = props;
|
|
1971
|
+
const [edit, setEdit] = React.useState(null);
|
|
1972
|
+
const [saving, setSaving] = React.useState(false);
|
|
1973
|
+
|
|
1974
|
+
// defaults / 项目变化 → 重建编辑态(生效值 = 用户覆盖 ?? 默认)
|
|
1975
|
+
React.useEffect(function () {
|
|
1976
|
+
if (!p.defaults || !p.projects) return;
|
|
1977
|
+
const pr = p.projects.find(function (x) { return x.slug === p.slug; }) || null;
|
|
1978
|
+
const sc = (pr && pr.stageConfig) || {};
|
|
1979
|
+
const next = {};
|
|
1980
|
+
STAGES.forEach(function (s) {
|
|
1981
|
+
const def = p.defaults.stages[s.id];
|
|
1982
|
+
const user = sc[s.id] || {};
|
|
1983
|
+
const prompts = {};
|
|
1984
|
+
Object.keys((def && def.prompts) || {}).forEach(function (k) {
|
|
1985
|
+
prompts[k] = (user.prompts && user.prompts[k] != null) ? user.prompts[k] : def.prompts[k];
|
|
1986
|
+
});
|
|
1987
|
+
// 专属参数:字符串编辑态(空串 = 跟默认;placeholder 展示默认值)
|
|
1988
|
+
const params = {};
|
|
1989
|
+
Object.keys((def && def.params) || {}).forEach(function (k) {
|
|
1990
|
+
const uv = user.params && user.params[k];
|
|
1991
|
+
params[k] = uv != null && uv !== "" ? String(uv) : "";
|
|
1992
|
+
});
|
|
1993
|
+
next[s.id] = {
|
|
1994
|
+
prompts,
|
|
1995
|
+
params,
|
|
1996
|
+
provider: user.provider || "",
|
|
1997
|
+
model: user.model || "",
|
|
1998
|
+
reasoningEffort: user.reasoningEffort || "",
|
|
1999
|
+
timeoutMin: user.timeoutMs ? String(Math.round(user.timeoutMs / 60000)) : "",
|
|
2000
|
+
maxTokens: user.maxTokens ? String(user.maxTokens) : "",
|
|
2001
|
+
delegate: {
|
|
2002
|
+
mode: s.id === "P6" ? (pr ? (pr.p6Mode || "builtin") : "builtin") : ((user.delegate && user.delegate.mode) || "off"),
|
|
2003
|
+
agent: (user.delegate && user.delegate.agent) || "",
|
|
2004
|
+
brief: (user.delegate && user.delegate.brief) || "",
|
|
2005
|
+
},
|
|
2006
|
+
testCommand: (pr && pr.testCommand) || "",
|
|
2007
|
+
};
|
|
2008
|
+
});
|
|
2009
|
+
setEdit(next);
|
|
2010
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2011
|
+
}, [p.defaults, p.projects, p.slug]);
|
|
2012
|
+
|
|
2013
|
+
if (!p.defaults || !p.projects) return h("p", { className: "empty-hint" }, "加载中…");
|
|
2014
|
+
if (!p.slug) {
|
|
2015
|
+
return h("p", { className: "empty-hint" },
|
|
2016
|
+
"请先在「项目」保存一个项目,再为它的 11 个阶段分别配置参数。");
|
|
2017
|
+
}
|
|
2018
|
+
if (!edit) return h("p", { className: "empty-hint" }, "加载中…");
|
|
2019
|
+
|
|
2020
|
+
const stageId = p.selStage || "P1";
|
|
2021
|
+
const sDef = STAGES.find(function (x) { return x.id === stageId; });
|
|
2022
|
+
const dDef = p.defaults.stages[stageId];
|
|
2023
|
+
const caps = (dDef && dDef.caps) || {};
|
|
2024
|
+
const e = edit[stageId];
|
|
2025
|
+
|
|
2026
|
+
const setSt = function (patch) {
|
|
2027
|
+
setEdit(function (prev) {
|
|
2028
|
+
const next = Object.assign({}, prev);
|
|
2029
|
+
next[stageId] = Object.assign({}, prev[stageId], patch);
|
|
2030
|
+
return next;
|
|
2031
|
+
});
|
|
2032
|
+
};
|
|
2033
|
+
const setPrompt = function (key, val) {
|
|
2034
|
+
setSt({ prompts: Object.assign({}, e.prompts, { [key || ""]: val }) });
|
|
2035
|
+
};
|
|
2036
|
+
const setParam = function (key, val) {
|
|
2037
|
+
setSt({ params: Object.assign({}, e.params, { [key]: val }) });
|
|
2038
|
+
};
|
|
2039
|
+
const setDelegate = function (patch) {
|
|
2040
|
+
setSt({ delegate: Object.assign({}, e.delegate, patch) });
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const radioGroup = function (labelId, opts, value, onChange, name) {
|
|
2044
|
+
return h("div", { className: "radio-group", role: "radiogroup", "aria-labelledby": labelId },
|
|
2045
|
+
opts.map(function (o) {
|
|
2046
|
+
const on = value === o.value;
|
|
2047
|
+
return h("label", { key: o.value, className: "radio-chip" + (on ? " on" : "") },
|
|
2048
|
+
h("input", { type: "radio", name: name + "-" + stageId, value: o.value, checked: on, onChange: function () { onChange(o.value); } }),
|
|
2049
|
+
o.label,
|
|
2050
|
+
h("span", { className: "hint" }, o.hint));
|
|
2051
|
+
}));
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
// —— 收集保存:与默认相同的项不落盘,保持 project.json 干净 ——
|
|
2055
|
+
const collect = function () {
|
|
2056
|
+
const pr = p.projects.find(function (x) { return x.slug === p.slug; }) || {};
|
|
2057
|
+
const stageConfig = {};
|
|
2058
|
+
STAGES.forEach(function (s) {
|
|
2059
|
+
const def = p.defaults.stages[s.id];
|
|
2060
|
+
const st = edit[s.id];
|
|
2061
|
+
const out = {};
|
|
2062
|
+
const prompts = {};
|
|
2063
|
+
let prDirty = false;
|
|
2064
|
+
Object.keys((def && def.prompts) || {}).forEach(function (k) {
|
|
2065
|
+
if (st.prompts[k] !== def.prompts[k]) { prompts[k] = st.prompts[k]; prDirty = true; }
|
|
2066
|
+
});
|
|
2067
|
+
if (prDirty) out.prompts = prompts;
|
|
2068
|
+
if (st.provider.trim()) out.provider = st.provider.trim();
|
|
2069
|
+
if (st.model.trim()) out.model = st.model.trim();
|
|
2070
|
+
if (st.reasoningEffort) out.reasoningEffort = st.reasoningEffort;
|
|
2071
|
+
const tmin = parseInt(st.timeoutMin, 10);
|
|
2072
|
+
if (tmin > 0) out.timeoutMs = tmin * 60000;
|
|
2073
|
+
const mt = parseInt(st.maxTokens, 10);
|
|
2074
|
+
if (mt > 0) out.maxTokens = mt;
|
|
2075
|
+
if (s.id !== "P6" && st.delegate.mode === "session") {
|
|
2076
|
+
const d = { mode: "session" };
|
|
2077
|
+
if (st.delegate.agent.trim()) d.agent = st.delegate.agent.trim();
|
|
2078
|
+
if (st.delegate.brief.trim()) d.brief = st.delegate.brief.trim();
|
|
2079
|
+
out.delegate = d;
|
|
2080
|
+
}
|
|
2081
|
+
// 专属参数:与默认相同的项不落盘(保持 project.json 干净)
|
|
2082
|
+
const pdef = (def && def.params) || {};
|
|
2083
|
+
const po = {};
|
|
2084
|
+
let pDirty = false;
|
|
2085
|
+
Object.keys(pdef).forEach(function (k) {
|
|
2086
|
+
const raw = String(st.params[k] == null ? "" : st.params[k]).trim();
|
|
2087
|
+
if (!raw) return;
|
|
2088
|
+
if (pdef[k].type === "string") {
|
|
2089
|
+
if (raw !== pdef[k].def) { po[k] = raw; pDirty = true; }
|
|
2090
|
+
} else {
|
|
2091
|
+
const n = parseInt(raw, 10);
|
|
2092
|
+
if (n > 0 && n !== pdef[k].def) { po[k] = n; pDirty = true; }
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
if (pDirty) out.params = po;
|
|
2096
|
+
if (Object.keys(out).length) stageConfig[s.id] = out;
|
|
2097
|
+
});
|
|
2098
|
+
return {
|
|
2099
|
+
name: pr.name || "",
|
|
2100
|
+
slug: pr.slug || "",
|
|
2101
|
+
repos: pr.repos || [],
|
|
2102
|
+
triggers: pr.triggers || [],
|
|
2103
|
+
reviewMode: pr.reviewMode || "every",
|
|
2104
|
+
p6Mode: edit.P6.delegate.mode, // P6 执行模式与项目页同源(builtin/session/claude)
|
|
2105
|
+
testCommand: (edit.P8.testCommand || "").trim(),
|
|
2106
|
+
stageConfig,
|
|
2107
|
+
};
|
|
2108
|
+
};
|
|
2109
|
+
|
|
2110
|
+
const save = function () {
|
|
2111
|
+
setSaving(true);
|
|
2112
|
+
apiPost("/projects", collect()).then(function (r) {
|
|
2113
|
+
setSaving(false);
|
|
2114
|
+
if (r && r.ok) { p.toast("阶段配置已保存(下一阶段起生效)"); p.onSaved(p.slug); }
|
|
2115
|
+
else p.toast((r && r.message) || "保存失败", "bad");
|
|
2116
|
+
}).catch(function (err) { setSaving(false); p.toast("请求失败: " + err, "bad"); });
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
// 阶段是否已自定义(左列蓝点):提示词偏离默认 / 路由覆盖 / 超时或 maxTokens 覆盖 / 专属参数覆盖 / 委托开启
|
|
2120
|
+
const isCustomized = function (sid) {
|
|
2121
|
+
const def = p.defaults.stages[sid];
|
|
2122
|
+
const st = edit[sid];
|
|
2123
|
+
if (!def || !st) return false;
|
|
2124
|
+
if (sid === "P6" && st.delegate.mode !== "builtin") return true;
|
|
2125
|
+
if (sid !== "P6" && st.delegate.mode === "session") return true;
|
|
2126
|
+
if (st.provider.trim() || st.model.trim() || st.reasoningEffort) return true;
|
|
2127
|
+
if (parseInt(st.timeoutMin, 10) > 0 || parseInt(st.maxTokens, 10) > 0) return true;
|
|
2128
|
+
const pdef = def.params || {};
|
|
2129
|
+
const pCustom = Object.keys(pdef).some(function (k) {
|
|
2130
|
+
const raw = String(st.params[k] == null ? "" : st.params[k]).trim();
|
|
2131
|
+
if (!raw) return false;
|
|
2132
|
+
if (pdef[k].type === "string") return raw !== pdef[k].def;
|
|
2133
|
+
const n = parseInt(raw, 10);
|
|
2134
|
+
return n > 0 && n !== pdef[k].def;
|
|
2135
|
+
});
|
|
2136
|
+
if (pCustom) return true;
|
|
2137
|
+
return Object.keys((def.prompts) || {}).some(function (k) { return st.prompts[k] !== def.prompts[k]; });
|
|
2138
|
+
};
|
|
2139
|
+
|
|
2140
|
+
const promptKeys = Object.keys((dDef && dDef.prompts) || {});
|
|
2141
|
+
const paramDefs = (dDef && dDef.params) || {};
|
|
2142
|
+
const roleLabel = { planner: "派单 Planner", coder: "编码 Coder", reviewer: "门控 Reviewer", desc: "PR 说明", gate: "Gate 评测" };
|
|
2143
|
+
const effortOpts = [
|
|
2144
|
+
{ value: "", label: "跟随全局", hint: "默认" },
|
|
2145
|
+
{ value: "low", label: "浅", hint: "low" },
|
|
2146
|
+
{ value: "medium", label: "中", hint: "medium" },
|
|
2147
|
+
{ value: "high", label: "深", hint: "high" },
|
|
2148
|
+
];
|
|
2149
|
+
|
|
2150
|
+
// 导入外部智能体简介(.md/.txt 文件 → brief)
|
|
2151
|
+
const importBrief = function (file) {
|
|
2152
|
+
if (!file) return;
|
|
2153
|
+
const rd = new FileReader();
|
|
2154
|
+
rd.onload = function () {
|
|
2155
|
+
setDelegate({ brief: String(rd.result || "") });
|
|
2156
|
+
p.toast("已导入 " + file.name + " 到附加要求");
|
|
2157
|
+
};
|
|
2158
|
+
rd.onerror = function () { p.toast("文件读取失败", "bad"); };
|
|
2159
|
+
rd.readAsText(file, "utf8");
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
// 右侧表单内容(配置页独立用 / 运行页「配置」tab 嵌入用共用)
|
|
2163
|
+
const cardContent = h("div", null,
|
|
2164
|
+
p.embedded ? null : h("div", { className: "detail-head" },
|
|
2165
|
+
h("div", null,
|
|
2166
|
+
h("h3", null, sDef.id + " · " + sDef.name),
|
|
2167
|
+
h("div", { className: "path" },
|
|
2168
|
+
sDef.desc + " · 产物 " + sDef.art + (dDef && dDef.delegateSpec ? " · 委托产出就绪判定:" + dDef.delegateSpec.output : "")))),
|
|
2169
|
+
// —— 阶段契约(先看本阶段输入 → 输出约定,再调下面的参数) ——
|
|
2170
|
+
h(StageContractCard, { stageId: stageId, defaults: p.defaults }),
|
|
2171
|
+
// —— 提示词(P7 无;P6 三角色 / P11 双角色) ——
|
|
2172
|
+
promptKeys.length ? h("div", { className: "field" },
|
|
2173
|
+
h("span", { className: "f-label" }, "提示词(system)"),
|
|
2174
|
+
promptKeys.map(function (k) {
|
|
2175
|
+
const isDefault = e.prompts[k] === dDef.prompts[k];
|
|
2176
|
+
return h("div", { key: k || "_" },
|
|
2177
|
+
h("div", { className: "pr-head" },
|
|
2178
|
+
promptKeys.length > 1 ? h("span", { className: "f-label" }, roleLabel[k] || k) : null,
|
|
2179
|
+
h("span", { className: "mark" }, isDefault ? "默认" : "已自定义"),
|
|
2180
|
+
!isDefault ? h("button", {
|
|
2181
|
+
type: "button", className: "btn sm", title: "恢复内置默认提示词",
|
|
2182
|
+
onClick: function () { setPrompt(k, dDef.prompts[k]); },
|
|
2183
|
+
}, Ic("reset", 12), " 恢复默认") : null),
|
|
2184
|
+
h("textarea", {
|
|
2185
|
+
className: "pr-input", value: e.prompts[k], spellCheck: false,
|
|
2186
|
+
"aria-label": "提示词" + (promptKeys.length > 1 ? " · " + (roleLabel[k] || k) : ""),
|
|
2187
|
+
onChange: function (ev) { setPrompt(k, ev.target.value); },
|
|
2188
|
+
}));
|
|
2189
|
+
})) : null,
|
|
2190
|
+
// —— 模型 · 思考深度 ——
|
|
2191
|
+
caps.route ? h("div", { className: "field" },
|
|
2192
|
+
h("span", { className: "f-label" }, "模型 · 思考深度(留空 = 跟随全局宿主默认模型)"),
|
|
2193
|
+
h("div", { className: "field-row" },
|
|
2194
|
+
h("div", { className: "field", style: { marginBottom: 0 } },
|
|
2195
|
+
h("label", { className: "f-label", htmlFor: "cfg-prov" }, "Provider"),
|
|
2196
|
+
h("input", {
|
|
2197
|
+
className: "f-input mono", id: "cfg-prov", value: e.provider,
|
|
2198
|
+
placeholder: "跟随全局(如 deepseek-official)",
|
|
2199
|
+
onChange: function (ev) { setSt({ provider: ev.target.value }); },
|
|
2200
|
+
})),
|
|
2201
|
+
h("div", { className: "field", style: { marginBottom: 0 } },
|
|
2202
|
+
h("label", { className: "f-label", htmlFor: "cfg-model" }, "模型 ID"),
|
|
2203
|
+
h("input", {
|
|
2204
|
+
className: "f-input mono", id: "cfg-model", value: e.model,
|
|
2205
|
+
placeholder: "跟随全局(如 deepseek-v4-pro)",
|
|
2206
|
+
onChange: function (ev) { setSt({ model: ev.target.value }); },
|
|
2207
|
+
}))),
|
|
2208
|
+
h("div", { style: { marginTop: 10 } },
|
|
2209
|
+
h("span", { className: "f-label", id: "cfg-eff-label" }, "思考深度(reasoningEffort)"),
|
|
2210
|
+
radioGroup("cfg-eff-label", effortOpts, e.reasoningEffort,
|
|
2211
|
+
function (v) { setSt({ reasoningEffort: v }); }, "eff"))) : null,
|
|
2212
|
+
// —— 执行参数(超时 / maxTokens;P8 = 测试超时 + 测试命令) ——
|
|
2213
|
+
caps.exec ? h("div", { className: "field" },
|
|
2214
|
+
h("span", { className: "f-label" }, "执行参数(留空 = 默认)"),
|
|
2215
|
+
h("div", { className: "field-row" },
|
|
2216
|
+
h("div", { className: "field", style: { marginBottom: 0 } },
|
|
2217
|
+
h("label", { className: "f-label", htmlFor: "cfg-timeout" },
|
|
2218
|
+
stageId === "P8" ? "测试超时(分钟)" : "LLM 调用超时(分钟)"),
|
|
2219
|
+
h("input", {
|
|
2220
|
+
className: "f-input mono", id: "cfg-timeout", type: "number", min: 1,
|
|
2221
|
+
value: e.timeoutMin, placeholder: "默认 " + (stageId === "P8"
|
|
2222
|
+
? Math.round(p.defaults.testTimeoutMs / 60000)
|
|
2223
|
+
: Math.round(p.defaults.llmTimeoutMs / 60000)) + " 分钟",
|
|
2224
|
+
onChange: function (ev) { setSt({ timeoutMin: ev.target.value }); },
|
|
2225
|
+
})),
|
|
2226
|
+
stageId !== "P8" ? h("div", { className: "field", style: { marginBottom: 0 } },
|
|
2227
|
+
h("label", { className: "f-label", htmlFor: "cfg-maxtok" }, "maxTokens"),
|
|
2228
|
+
h("input", {
|
|
2229
|
+
className: "f-input mono", id: "cfg-maxtok", type: "number", min: 1024,
|
|
2230
|
+
value: e.maxTokens, placeholder: "默认 " + p.defaults.maxTokens,
|
|
2231
|
+
onChange: function (ev) { setSt({ maxTokens: ev.target.value }); },
|
|
2232
|
+
})) : null),
|
|
2233
|
+
caps.test ? h("div", { style: { marginTop: 10 } },
|
|
2234
|
+
h("label", { className: "f-label", htmlFor: "cfg-test" }, "测试命令(留空 = 自动探测 npm test)"),
|
|
2235
|
+
h("input", {
|
|
2236
|
+
className: "f-input mono", id: "cfg-test", value: e.testCommand,
|
|
2237
|
+
placeholder: "如 npm test / python -m pytest …",
|
|
2238
|
+
onChange: function (ev) { setSt({ testCommand: ev.target.value }); },
|
|
2239
|
+
})) : null) : null,
|
|
2240
|
+
// —— 阶段专属参数(本阶段工具行为:扫描上限/深读数/claude 路径等;开源场景不藏在代码里) ——
|
|
2241
|
+
Object.keys(paramDefs).length ? h("div", { className: "field" },
|
|
2242
|
+
h("span", { className: "f-label" }, "阶段专属参数(本阶段工具行为 · 留空 = 默认值)"),
|
|
2243
|
+
h("div", { className: "field-row" },
|
|
2244
|
+
Object.keys(paramDefs).map(function (k) {
|
|
2245
|
+
const meta = paramDefs[k];
|
|
2246
|
+
return h("div", { key: k, className: "field", style: { marginBottom: 0 } },
|
|
2247
|
+
h("label", { className: "f-label", htmlFor: "cfg-p-" + stageId + "-" + k },
|
|
2248
|
+
meta.label + (meta.unit ? "(" + meta.unit + ")" : "")),
|
|
2249
|
+
h("input", {
|
|
2250
|
+
className: "f-input mono", id: "cfg-p-" + stageId + "-" + k,
|
|
2251
|
+
type: meta.type === "string" ? "text" : "number", min: 1,
|
|
2252
|
+
value: e.params[k],
|
|
2253
|
+
placeholder: meta.type === "string"
|
|
2254
|
+
? (meta.def ? "默认 " + meta.def : "自动探测")
|
|
2255
|
+
: "默认 " + meta.def + (meta.unit ? " " + meta.unit : ""),
|
|
2256
|
+
onChange: function (ev) { setParam(k, ev.target.value); },
|
|
2257
|
+
}),
|
|
2258
|
+
h("p", { className: "hint-line", style: { margin: "5px 0 0" } }, meta.hint));
|
|
2259
|
+
}))) : null,
|
|
2260
|
+
// —— 委托外部智能体 ——
|
|
2261
|
+
caps.delegate ? (function () {
|
|
2262
|
+
if (stageId === "P6") {
|
|
2263
|
+
// P6:执行模式与项目页同源(builtin/session/claude)
|
|
2264
|
+
return h("div", { className: "field" },
|
|
2265
|
+
h("span", { className: "f-label", id: "cfg-p6-label" }, "执行模式 · 委托外部智能体"),
|
|
2266
|
+
radioGroup("cfg-p6-label", [
|
|
2267
|
+
{ value: "builtin", label: "插件内多智能体", hint: "Planner/Coder/Reviewer" },
|
|
2268
|
+
{ value: "session", label: "交给外部会话", hint: "生成任务包,人工交接" },
|
|
2269
|
+
{ value: "claude", label: "委托 Claude Code", hint: "claude CLI 自动执行" },
|
|
2270
|
+
], e.delegate.mode, function (v) { setDelegate({ mode: v }); }, "p6mode"),
|
|
2271
|
+
h("p", { className: "hint-line", style: { margin: "8px 0 0" } },
|
|
2272
|
+
"与「项目」页 P6 执行模式为同一配置(p6Mode);claude 模式任务包自动执行,产出补丁后回复核门。"));
|
|
2273
|
+
}
|
|
2274
|
+
const on = e.delegate.mode === "session";
|
|
2275
|
+
return h("div", { className: "field" },
|
|
2276
|
+
h("span", { className: "f-label" }, "委托外部智能体(ACP)"),
|
|
2277
|
+
h("div", { className: "dlg-row" },
|
|
2278
|
+
h("input", {
|
|
2279
|
+
type: "checkbox", id: "cfg-dlg-on", checked: on,
|
|
2280
|
+
onChange: function (ev) { setDelegate({ mode: ev.target.checked ? "session" : "off" }); },
|
|
2281
|
+
}),
|
|
2282
|
+
h("label", { htmlFor: "cfg-dlg-on", style: { flex: 1 } },
|
|
2283
|
+
h("span", { className: "lbl" }, "本阶段不调用插件内 LLM,生成任务包交外部智能体执行"),
|
|
2284
|
+
h("div", { className: "sub" },
|
|
2285
|
+
"开启后产出 ", h("span", { style: { fontFamily: "var(--mono)" } }, "delegate/" + stageId + "-task.md"),
|
|
2286
|
+
"(含上游输入、输出契约" + (dDef && dDef.delegateSpec ? ",产出 " + dDef.delegateSpec.output + " 落盘后复核门可放行" : "") + ")。"))),
|
|
2287
|
+
on ? h("div", null,
|
|
2288
|
+
h("div", { className: "field" },
|
|
2289
|
+
h("label", { className: "f-label", htmlFor: "cfg-agent" }, "外部智能体(名称/型号,写入任务包抬头)"),
|
|
2290
|
+
h("input", {
|
|
2291
|
+
className: "f-input mono", id: "cfg-agent", value: e.delegate.agent,
|
|
2292
|
+
placeholder: "如 claude-code / codex-cli / 自定义智能体名",
|
|
2293
|
+
onChange: function (ev) { setDelegate({ agent: ev.target.value }); },
|
|
2294
|
+
})),
|
|
2295
|
+
h("div", { className: "field" },
|
|
2296
|
+
h("label", { className: "f-label", htmlFor: "cfg-brief" }, "附加要求(随任务包下发给智能体)"),
|
|
2297
|
+
h("textarea", {
|
|
2298
|
+
className: "f-input", id: "cfg-brief", value: e.delegate.brief,
|
|
2299
|
+
placeholder: "对该智能体的额外要求、产出格式、注意事项…(也可从下方导入)",
|
|
2300
|
+
onChange: function (ev) { setDelegate({ brief: ev.target.value }); },
|
|
2301
|
+
}),
|
|
2302
|
+
h("label", { className: "imp-label", style: { marginTop: 6 } },
|
|
2303
|
+
Ic("upload", 12), " 导入外部智能体简介(.md / .txt)",
|
|
2304
|
+
h("input", {
|
|
2305
|
+
type: "file", accept: ".md,.txt,.markdown",
|
|
2306
|
+
onChange: function (ev) { importBrief(ev.target.files && ev.target.files[0]); ev.target.value = ""; },
|
|
2307
|
+
})))) : null);
|
|
2308
|
+
})() : null,
|
|
2309
|
+
// —— P7:确定性执行,无参数 ——
|
|
2310
|
+
!promptKeys.length && !caps.route && !caps.exec && !caps.delegate
|
|
2311
|
+
? h("div", { className: "callout", style: { marginTop: 0 } },
|
|
2312
|
+
h("h4", null, "本阶段无可配置参数"),
|
|
2313
|
+
h("p", { style: { margin: 0, fontSize: 13.5 } },
|
|
2314
|
+
"P7 是确定性 patch 应用管线(版本校验 → 逐条落盘 → 写 ledger),不调用大模型;",
|
|
2315
|
+
"每条 patch 可在「运行」页 P7 详情中逐条回滚。"))
|
|
2316
|
+
: null,
|
|
2317
|
+
h("div", { className: "cfg-foot" },
|
|
2318
|
+
h("button", { type: "button", className: "btn pri", disabled: saving, onClick: save },
|
|
2319
|
+
Ic("check"), saving ? " 保存中…" : " 保存阶段配置"),
|
|
2320
|
+
h("span", { className: "hint-line", style: { margin: 0 } },
|
|
2321
|
+
"写入 …\\issue2pr\\projects\\" + p.slug + "\\project.json 的 stageConfig 字段")));
|
|
2322
|
+
|
|
2323
|
+
if (p.embedded) {
|
|
2324
|
+
// 运行页「配置」tab:运行页已有阶段头与左列,这里只出表单体
|
|
2325
|
+
return h("div", null,
|
|
2326
|
+
h("p", { className: "run-hint", style: { margin: "0 0 10px" } },
|
|
2327
|
+
"本阶段全量参数:", h("b", null, "提示词 / 模型 / 思考深度 / 超时 / 专属参数 / 委托"),
|
|
2328
|
+
";留空 = 用默认值。保存对进行中的 Run 从下一阶段起生效;阶段切换用左侧时间线。"),
|
|
2329
|
+
cardContent);
|
|
2330
|
+
}
|
|
2331
|
+
return h("div", null,
|
|
2332
|
+
h("p", { className: "run-hint" },
|
|
2333
|
+
"按阶段覆盖 ", h("b", null, "提示词 / 模型 / 思考深度 / 超时 / 专属参数 / 委托外部智能体"),
|
|
2334
|
+
";留空 = 用默认值。保存写入 project.json,对进行中的 Run 从下一阶段起生效。「运行」页各阶段详情也可直接配置。"),
|
|
2335
|
+
h("div", { className: "cfg-layout" },
|
|
2336
|
+
h("div", { className: "steps", role: "list", "aria-label": "阶段列表" },
|
|
2337
|
+
STAGES.map(function (s) {
|
|
2338
|
+
const customized = isCustomized(s.id);
|
|
2339
|
+
const dlg = edit[s.id] && edit[s.id].delegate.mode !== "off"
|
|
2340
|
+
&& (s.id !== "P6" ? true : edit.P6.delegate.mode !== "builtin");
|
|
2341
|
+
return h("button", {
|
|
2342
|
+
key: s.id, role: "listitem",
|
|
2343
|
+
className: "cfg-row" + (s.id === stageId ? " on" : ""),
|
|
2344
|
+
onClick: function () { p.onSelectStage(s.id); },
|
|
2345
|
+
},
|
|
2346
|
+
h("span", { className: "sdot" }),
|
|
2347
|
+
h("span", { className: "s-main" },
|
|
2348
|
+
h("span", { className: "s-name" }, s.id + " · " + s.name,
|
|
2349
|
+
customized ? h("span", { className: "cdot", title: "已自定义" }) : null),
|
|
2350
|
+
h("span", { className: "s-desc" }, s.desc)),
|
|
2351
|
+
h("span", { className: "s-side" },
|
|
2352
|
+
dlg ? h("span", { className: "wtg" }, "委托") : null));
|
|
2353
|
+
})),
|
|
2354
|
+
h("div", { className: "card" }, cardContent)));
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/* ================================================================
|
|
2358
|
+
* 05 说明
|
|
2359
|
+
* ================================================================ */
|
|
2360
|
+
function GuidePanel() {
|
|
2361
|
+
return h("div", { className: "guide" },
|
|
2362
|
+
h("p", { className: "lede" },
|
|
2363
|
+
"从一条 Issue 到一份可合并 PR 的可验证流水线:11 个阶段逐级推进,每阶段产出结构化产物,",
|
|
2364
|
+
"关键门需人工复核,失败自动分类、可回退可回滚。交付物 = 产物 + 测试 + PR 说明,而非一段回答。"),
|
|
2365
|
+
h("div", { className: "card" },
|
|
2366
|
+
h("h4", null, "五条规则"),
|
|
2367
|
+
h("ol", { className: "tight", style: { margin: 0 } },
|
|
2368
|
+
h("li", null, h("b", null, "不可跳步"), " — 分析、检索、理解、假设、规划、修改、测试、审查、评测,一步不省。"),
|
|
2369
|
+
h("li", null, h("b", null, "可定位"), " — 任何失败都落到具体阶段与错误信息。"),
|
|
2370
|
+
h("li", null, h("b", null, "可回滚"), " — Agent 引入的 patch 逐条可逆(P7 ledger)。"),
|
|
2371
|
+
h("li", null, h("b", null, "可审查"), " — 每个阶段都有可检查的产物文件。"),
|
|
2372
|
+
h("li", null, h("b", null, "可复现"), " — trace/spans.jsonl 记录每阶段耗时与决策。"))),
|
|
2373
|
+
h("table", { className: "tbl" },
|
|
2374
|
+
h("thead", null, h("tr", null,
|
|
2375
|
+
h("th", null, "阶段"), h("th", null, "职责"), h("th", null, "产物"), h("th", null, "复核门"))),
|
|
2376
|
+
h("tbody", null,
|
|
2377
|
+
STAGES.map(function (s) {
|
|
2378
|
+
return h("tr", { key: s.id },
|
|
2379
|
+
h("td", null, h("code", null, s.id)),
|
|
2380
|
+
h("td", null, s.name + (s.bypass ? h("span", { className: "tg t-warn", style: { marginLeft: 6 } }, "失败旁路") : null)),
|
|
2381
|
+
h("td", null, h("code", null, s.art)),
|
|
2382
|
+
h("td", null, s.key ? "是" : "—"));
|
|
2383
|
+
}))),
|
|
2384
|
+
h("div", { className: "grid-2" },
|
|
2385
|
+
h("div", { className: "callout acc" },
|
|
2386
|
+
h("h4", null, "复核模式"),
|
|
2387
|
+
h("ul", { style: { margin: 0, paddingLeft: 18, fontSize: 13.5 } },
|
|
2388
|
+
h("li", null, h("b", null, "每阶段都停"), " — 每阶段产物都等人工通过(默认)"),
|
|
2389
|
+
h("li", null, h("b", null, "只停关键门"), " — P5 规划 / P6 代码 / P9 审查 / P11 PR"),
|
|
2390
|
+
h("li", null, h("b", null, "全自动"), " — 不停顿,事后仍可打回重跑"))),
|
|
2391
|
+
h("div", { className: "callout warn" },
|
|
2392
|
+
h("h4", null, "失败怎么办"),
|
|
2393
|
+
h("ul", { style: { margin: 0, paddingLeft: 18, fontSize: 13.5 } },
|
|
2394
|
+
h("li", null, "阶段失败 → 自动经 P10 写失败分类,然后停住"),
|
|
2395
|
+
h("li", null, "「重跑」从任一已到达阶段回退重跑"),
|
|
2396
|
+
h("li", null, "P7 ledger 支持逐条回滚单个 patch"))),
|
|
2397
|
+
h("div", { className: "callout acc" },
|
|
2398
|
+
h("h4", null, "按阶段调参"),
|
|
2399
|
+
h("ul", { style: { margin: 0, paddingLeft: 18, fontSize: 13.5 } },
|
|
2400
|
+
h("li", null, "「运行」页每个阶段详情内有「配置」视图,直接调本阶段参数;「配置」页可集中管理全部阶段"),
|
|
2401
|
+
h("li", null, "可调:提示词 / 模型 / 思考深度 / 超时 / 阶段专属参数(扫描上限、深读数、claude 路径等)/ 委托外部智能体"),
|
|
2402
|
+
h("li", null, "可把任一 LLM 阶段委托外部智能体(生成任务包,产出落盘后回复核门)"),
|
|
2403
|
+
h("li", null, "留空一律回落内置默认;保存后对进行中的 Run 下一阶段起生效")))),
|
|
2404
|
+
h("div", { className: "card" },
|
|
2405
|
+
h("h4", null, "数据落盘位置"),
|
|
2406
|
+
h("p", { style: { margin: 0 } },
|
|
2407
|
+
"项目配置、Run 状态与全部产物都在 ", h("code", { style: { fontFamily: "var(--mono)", background: "var(--hover)", padding: "1px 6px", borderRadius: 4, fontSize: 11.5, color: "var(--ink-2)" } }, "~/.dsh/issue2pr/projects/<slug>/"),
|
|
2408
|
+
" 下:", h("code", { style: { fontFamily: "var(--mono)", background: "var(--hover)", padding: "1px 6px", borderRadius: 4, fontSize: 11.5, color: "var(--ink-2)" } }, "project.json"),
|
|
2409
|
+
" 配置、", h("code", { style: { fontFamily: "var(--mono)", background: "var(--hover)", padding: "1px 6px", borderRadius: 4, fontSize: 11.5, color: "var(--ink-2)" } }, "runs/<id>/run.json"),
|
|
2410
|
+
" 状态、", h("code", { style: { fontFamily: "var(--mono)", background: "var(--hover)", padding: "1px 6px", borderRadius: 4, fontSize: 11.5, color: "var(--ink-2)" } }, "repo/"),
|
|
2411
|
+
" 本地克隆。删除项目 / Run 即删除对应目录。")));
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/* ================================================================
|
|
2415
|
+
* 主 Section:左侧导航 + 共享数据 + 3s 轮询
|
|
2416
|
+
* ================================================================ */
|
|
2417
|
+
function Section() {
|
|
2418
|
+
const [nav, setNav] = React.useState("projects");
|
|
2419
|
+
const [toast, setToast] = React.useState(null);
|
|
2420
|
+
const [projects, setProjects] = React.useState(null);
|
|
2421
|
+
// 选中记忆:进入时恢复上次选中的项目(无记忆则保持未选中)
|
|
2422
|
+
const [selSlug, setSelSlug] = React.useState(function () { return localStorage.getItem("i2p.proj") || null; });
|
|
2423
|
+
const [runs, setRuns] = React.useState(null);
|
|
2424
|
+
const [selRunId, setSelRunId] = React.useState(null);
|
|
2425
|
+
const [run, setRun] = React.useState(null);
|
|
2426
|
+
const [tree, setTree] = React.useState(null);
|
|
2427
|
+
const [cfgStage, setCfgStage] = React.useState("P1");
|
|
2428
|
+
const [stageDefaults, setStageDefaults] = React.useState(null);
|
|
2429
|
+
// Git 托管连接(全局)+ 环境预检(git/claude/LLM 路由):项目页台面化数据源
|
|
2430
|
+
const [connections, setConnections] = React.useState(null);
|
|
2431
|
+
const [preflight, setPreflight] = React.useState(null);
|
|
2432
|
+
|
|
2433
|
+
// 视图位置同步进模块级 store(悬浮智能助手据此聚焦当前页面/项目/Run)
|
|
2434
|
+
React.useEffect(function () {
|
|
2435
|
+
viewStore.set({ nav: nav, slug: selSlug, runId: selRunId });
|
|
2436
|
+
}, [nav, selSlug, selRunId]);
|
|
2437
|
+
|
|
2438
|
+
// 阶段默认值与能力表(配置页数据源;一次拉取)
|
|
2439
|
+
React.useEffect(function () {
|
|
2440
|
+
apiGet("/stage-defaults").then(function (r) {
|
|
2441
|
+
if (r && r.ok) setStageDefaults(r.defaults);
|
|
2442
|
+
}).catch(function () { /* 配置页会显示加载中 */ });
|
|
2443
|
+
}, []);
|
|
2444
|
+
|
|
2445
|
+
// Git 托管连接(全局共享;增删改后由 loadConnections 刷新)
|
|
2446
|
+
const loadConnections = React.useCallback(function () {
|
|
2447
|
+
apiGet("/connections").then(function (r) {
|
|
2448
|
+
if (r && r.ok) setConnections(r.connections || []);
|
|
2449
|
+
}).catch(function () { /* 静默:徽章显示未配置 */ });
|
|
2450
|
+
}, []);
|
|
2451
|
+
React.useEffect(function () { loadConnections(); }, [loadConnections]);
|
|
2452
|
+
|
|
2453
|
+
// 环境预检(只读):随选中项目刷新(覆盖清单/claudeBin 与项目配置相关),保存后也刷新
|
|
2454
|
+
const loadPreflight = React.useCallback(function () {
|
|
2455
|
+
const q = selSlug ? "?slug=" + encodeURIComponent(selSlug) : "";
|
|
2456
|
+
apiGet("/preflight" + q).then(function (r) {
|
|
2457
|
+
if (r && r.ok) setPreflight(r.preflight);
|
|
2458
|
+
}).catch(function () { /* 静默:无横幅即视为健康 */ });
|
|
2459
|
+
}, [selSlug]);
|
|
2460
|
+
React.useEffect(function () { loadPreflight(); }, [loadPreflight]);
|
|
2461
|
+
|
|
2462
|
+
// 竞态守卫键:始终反映最新的 slug/runId,异步响应到达时比对,
|
|
2463
|
+
// 不匹配则丢弃(防止旧项目的 runs/run/tree 响应覆盖新选择)
|
|
2464
|
+
const ctxRef = React.useRef({ slug: null, runId: null });
|
|
2465
|
+
ctxRef.current.slug = selSlug;
|
|
2466
|
+
ctxRef.current.runId = selRunId;
|
|
2467
|
+
|
|
2468
|
+
// 选中项目统一入口:双写记忆——localStorage(快路径,同会话零延迟)
|
|
2469
|
+
// + 服务端 ui-state 文件(兜底:宿主 webview 重启后 localStorage 不持久)
|
|
2470
|
+
const touchedRef = React.useRef(false);
|
|
2471
|
+
const selectSlug = React.useCallback(function (slug) {
|
|
2472
|
+
touchedRef.current = true;
|
|
2473
|
+
setSelSlug(slug);
|
|
2474
|
+
if (slug) localStorage.setItem("i2p.proj", slug);
|
|
2475
|
+
else localStorage.removeItem("i2p.proj");
|
|
2476
|
+
apiPost("/ui-state", { lastProject: slug || null })
|
|
2477
|
+
.catch(function () { /* 兜底写失败静默:localStorage 仍有效 */ });
|
|
2478
|
+
}, []);
|
|
2479
|
+
|
|
2480
|
+
// 进入时记忆恢复的兜底:localStorage 无记忆(宿主重启清空 / 换 origin)→
|
|
2481
|
+
// 拉服务端 ui-state 恢复上次选中;有记忆则反向同步到服务端,保证下次重启后兜底可用。
|
|
2482
|
+
// 异步返回时用户已手动选过(touched)或已处于选中态则不覆盖。
|
|
2483
|
+
React.useEffect(function () {
|
|
2484
|
+
const local = localStorage.getItem("i2p.proj");
|
|
2485
|
+
if (local) {
|
|
2486
|
+
apiPost("/ui-state", { lastProject: local }).catch(function () { /* 同步失败静默 */ });
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
apiGet("/ui-state").then(function (r) {
|
|
2490
|
+
if (!r || !r.ok) return;
|
|
2491
|
+
const lp = r.state && r.state.lastProject;
|
|
2492
|
+
if (!lp || touchedRef.current || ctxRef.current.slug != null) return;
|
|
2493
|
+
setSelSlug(lp);
|
|
2494
|
+
}).catch(function () { /* 兜底不可用:保持未选中 */ });
|
|
2495
|
+
}, []);
|
|
2496
|
+
|
|
2497
|
+
const toastFn = React.useCallback(function (msg, kind) {
|
|
2498
|
+
setToast({ msg: msg, kind: kind || "ok" });
|
|
2499
|
+
}, []);
|
|
2500
|
+
React.useEffect(function () {
|
|
2501
|
+
if (!toast) return;
|
|
2502
|
+
const t = setTimeout(function () { setToast(null); }, 2600);
|
|
2503
|
+
return function () { clearTimeout(t); };
|
|
2504
|
+
}, [toast]);
|
|
2505
|
+
|
|
2506
|
+
// 项目列表(挂载 / 保存后)
|
|
2507
|
+
const loadProjects = React.useCallback(function () {
|
|
2508
|
+
apiGet("/projects").then(function (r) {
|
|
2509
|
+
if (r && r.ok) setProjects(r.projects);
|
|
2510
|
+
else toastFn((r && r.message) || "项目列表加载失败", "bad");
|
|
2511
|
+
}).catch(function (e) { toastFn("请求失败: " + e, "bad"); });
|
|
2512
|
+
}, [toastFn]);
|
|
2513
|
+
React.useEffect(function () { loadProjects(); }, [loadProjects]);
|
|
2514
|
+
|
|
2515
|
+
// 记忆选中的项目若已不存在(被他处删除),回到未选中状态
|
|
2516
|
+
React.useEffect(function () {
|
|
2517
|
+
if (selSlug == null || projects == null) return;
|
|
2518
|
+
if (!projects.some(function (x) { return x.slug === selSlug; })) selectSlug(null);
|
|
2519
|
+
}, [selSlug, projects, selectSlug]);
|
|
2520
|
+
|
|
2521
|
+
// slug 变化 → 重置 run 选择并加载 runs 摘要
|
|
2522
|
+
React.useEffect(function () {
|
|
2523
|
+
setSelRunId(null); setRun(null); setTree(null);
|
|
2524
|
+
if (!selSlug) { setRuns(null); return; }
|
|
2525
|
+
const slug = selSlug;
|
|
2526
|
+
setRuns(null);
|
|
2527
|
+
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2528
|
+
if (!r || !r.ok) return;
|
|
2529
|
+
if (ctxRef.current.slug !== slug) return;
|
|
2530
|
+
setRuns(r.runs || []);
|
|
2531
|
+
setSelRunId(function (prev) {
|
|
2532
|
+
return prev || (r.runs && r.runs.length ? r.runs[0].id : null);
|
|
2533
|
+
});
|
|
2534
|
+
});
|
|
2535
|
+
}, [selSlug]);
|
|
2536
|
+
|
|
2537
|
+
// run 详细 + 产物树(切换 run 时;stale 守卫防旧响应覆盖新选择)
|
|
2538
|
+
React.useEffect(function () {
|
|
2539
|
+
if (!selSlug || !selRunId) { setRun(null); setTree(null); return; }
|
|
2540
|
+
let stale = false;
|
|
2541
|
+
apiGet("/projects/" + selSlug + "/runs/" + selRunId).then(function (r) {
|
|
2542
|
+
if (stale) return;
|
|
2543
|
+
if (r && r.id) setRun(r);
|
|
2544
|
+
else if (r && r.ok === false) setRun(null);
|
|
2545
|
+
});
|
|
2546
|
+
apiGet("/projects/" + selSlug + "/runs/" + selRunId + "/tree").then(function (r) {
|
|
2547
|
+
if (!stale) setTree(r && r.ok ? r.files : null);
|
|
2548
|
+
});
|
|
2549
|
+
return function () { stale = true; };
|
|
2550
|
+
}, [selSlug, selRunId]);
|
|
2551
|
+
|
|
2552
|
+
// 3s 轮询 run 摘要 / 详细 / 产物树(卸载时清理;响应按最新 slug/runId 守卫)
|
|
2553
|
+
React.useEffect(function () {
|
|
2554
|
+
if (!selSlug) return;
|
|
2555
|
+
const slug = selSlug, runId = selRunId;
|
|
2556
|
+
const timer = setInterval(function () {
|
|
2557
|
+
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2558
|
+
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
2559
|
+
});
|
|
2560
|
+
if (runId) {
|
|
2561
|
+
apiGet("/projects/" + slug + "/runs/" + runId).then(function (r) {
|
|
2562
|
+
if (r && r.id && ctxRef.current.slug === slug && ctxRef.current.runId === runId) setRun(r);
|
|
2563
|
+
});
|
|
2564
|
+
apiGet("/projects/" + slug + "/runs/" + runId + "/tree").then(function (r) {
|
|
2565
|
+
if (r && r.ok && ctxRef.current.slug === slug && ctxRef.current.runId === runId) setTree(r.files);
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
}, 3000);
|
|
2569
|
+
return function () { clearInterval(timer); };
|
|
2570
|
+
}, [selSlug, selRunId]);
|
|
2571
|
+
|
|
2572
|
+
// 动作后立即刷新(不等轮询;响应同样按最新 slug/runId 守卫)
|
|
2573
|
+
const refreshNow = React.useCallback(function () {
|
|
2574
|
+
const slug = selSlug, runId = selRunId;
|
|
2575
|
+
if (slug) {
|
|
2576
|
+
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2577
|
+
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
if (slug && runId) {
|
|
2581
|
+
apiGet("/projects/" + slug + "/runs/" + runId).then(function (r) {
|
|
2582
|
+
if (r && r.id && ctxRef.current.slug === slug && ctxRef.current.runId === runId) setRun(r);
|
|
2583
|
+
});
|
|
2584
|
+
apiGet("/projects/" + slug + "/runs/" + runId + "/tree").then(function (r) {
|
|
2585
|
+
if (r && r.ok && ctxRef.current.slug === slug && ctxRef.current.runId === runId) setTree(r.files);
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}, [selSlug, selRunId]);
|
|
2589
|
+
|
|
2590
|
+
const onSaved = React.useCallback(function (slug) {
|
|
2591
|
+
selectSlug(slug);
|
|
2592
|
+
loadProjects();
|
|
2593
|
+
loadPreflight(); // 阶段模型覆盖 / claudeBin 配置变化 → 预检行与横幅即时更新
|
|
2594
|
+
}, [loadProjects, selectSlug, loadPreflight]);
|
|
2595
|
+
|
|
2596
|
+
const onRunDeleted = React.useCallback(function () {
|
|
2597
|
+
setSelRunId(null); setRun(null); setTree(null);
|
|
2598
|
+
const slug = selSlug;
|
|
2599
|
+
if (!slug) return;
|
|
2600
|
+
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2601
|
+
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
2602
|
+
});
|
|
2603
|
+
}, [selSlug]);
|
|
2604
|
+
|
|
2605
|
+
const onProjectDeleted = React.useCallback(function () {
|
|
2606
|
+
selectSlug(null); setRuns(null); setSelRunId(null); setRun(null); setTree(null);
|
|
2607
|
+
loadProjects();
|
|
2608
|
+
}, [loadProjects, selectSlug]);
|
|
2609
|
+
|
|
2610
|
+
const onRunStarted = React.useCallback(function (slug, runId) {
|
|
2611
|
+
selectSlug(slug);
|
|
2612
|
+
setSelRunId(runId);
|
|
2613
|
+
setNav("runs");
|
|
2614
|
+
apiGet("/projects/" + slug + "/runs").then(function (r) {
|
|
2615
|
+
if (r && r.ok && ctxRef.current.slug === slug) setRuns(r.runs || []);
|
|
2616
|
+
});
|
|
2617
|
+
}, [selectSlug]);
|
|
2618
|
+
|
|
2619
|
+
const awaitingCount = (runs || []).filter(function (r) { return r.status === "awaiting_review"; }).length;
|
|
2620
|
+
|
|
2621
|
+
const navDefs = [
|
|
2622
|
+
{ id: "projects", label: "项目", icon: "folder", meta: projects != null ? String(projects.length) : "", alert: false },
|
|
2623
|
+
{ id: "runs", label: "运行", icon: "play", meta: runs != null ? (awaitingCount > 0 ? awaitingCount + " 待复核" : String(runs.length)) : "", alert: awaitingCount > 0 },
|
|
2624
|
+
{ id: "artifacts", label: "产物", icon: "box", meta: "", alert: false },
|
|
2625
|
+
{ id: "config", label: "配置", icon: "sliders", meta: "", alert: false },
|
|
2626
|
+
{ id: "guide", label: "说明", icon: "book", meta: "", alert: false },
|
|
2627
|
+
];
|
|
2628
|
+
|
|
2629
|
+
const navKey = function (e, i) {
|
|
2630
|
+
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
|
|
2631
|
+
e.preventDefault();
|
|
2632
|
+
const next = (i + (e.key === "ArrowRight" ? 1 : -1) + navDefs.length) % navDefs.length;
|
|
2633
|
+
navDefs[next] && setNav(navDefs[next].id);
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
const navState = useNavState();
|
|
2637
|
+
|
|
2638
|
+
const navEls = navDefs.map(function (t, i) {
|
|
2639
|
+
return h("li", { key: t.id },
|
|
2640
|
+
h("button", {
|
|
2641
|
+
className: "i2p-nav-cell" + (nav === t.id ? " on" : ""), role: "tab",
|
|
2642
|
+
"aria-selected": nav === t.id ? "true" : "false",
|
|
2643
|
+
style: { "--i": i }, title: t.label,
|
|
2644
|
+
onClick: function () { setNav(t.id); },
|
|
2645
|
+
onKeyDown: function (e) { navKey(e, i); },
|
|
2646
|
+
}, Ic(t.icon, 15), h("span", { className: "lb" }, t.label),
|
|
2647
|
+
t.meta ? h("span", { className: "cnt" + (t.alert ? " alert" : "") }, t.meta) : null));
|
|
2648
|
+
});
|
|
2649
|
+
|
|
2650
|
+
// 目录列拖宽:原生事件委托(React 合成事件在宿主注入环境下不可靠),
|
|
2651
|
+
// pointer 拖动 + 双击手柄恢复默认宽(拖动的单指针替代路径)
|
|
2652
|
+
React.useEffect(function () {
|
|
2653
|
+
const gripOf = function (t) { return t && t.closest ? t.closest(".i2p-nav-grip") : null; };
|
|
2654
|
+
const down = function (e) {
|
|
2655
|
+
const grip = gripOf(e.target);
|
|
2656
|
+
if (!grip || e.button !== 0) return;
|
|
2657
|
+
const navEl = grip.parentElement;
|
|
2658
|
+
if (!navEl) return;
|
|
2659
|
+
e.preventDefault();
|
|
2660
|
+
const rect = navEl.getBoundingClientRect();
|
|
2661
|
+
document.body.classList.add("i2p-dragging");
|
|
2662
|
+
navEl.classList.add("no-anim");
|
|
2663
|
+
const move = function (ev) { navStore.setWidth(ev.clientX - rect.left, false); };
|
|
2664
|
+
const up = function () {
|
|
2665
|
+
window.removeEventListener("pointermove", move);
|
|
2666
|
+
window.removeEventListener("pointerup", up);
|
|
2667
|
+
document.body.classList.remove("i2p-dragging");
|
|
2668
|
+
navEl.classList.remove("no-anim");
|
|
2669
|
+
navStore.setWidth(navStore.width, true);
|
|
2670
|
+
};
|
|
2671
|
+
window.addEventListener("pointermove", move);
|
|
2672
|
+
window.addEventListener("pointerup", up);
|
|
2673
|
+
};
|
|
2674
|
+
const dbl = function (e) {
|
|
2675
|
+
if (gripOf(e.target)) navStore.setWidth(240, true);
|
|
2676
|
+
};
|
|
2677
|
+
document.addEventListener("pointerdown", down);
|
|
2678
|
+
document.addEventListener("dblclick", dbl);
|
|
2679
|
+
return function () {
|
|
2680
|
+
document.removeEventListener("pointerdown", down);
|
|
2681
|
+
document.removeEventListener("dblclick", dbl);
|
|
2682
|
+
};
|
|
2683
|
+
}, []);
|
|
2684
|
+
|
|
2685
|
+
const secSub = {
|
|
2686
|
+
projects: "配置仓库与触发源——所有 Run 从这里发起。",
|
|
2687
|
+
runs: "11 阶段流水线——逐段查看产物、逐段推进。",
|
|
2688
|
+
artifacts: "Run 的完整产物目录——证据链都在这里。",
|
|
2689
|
+
config: "逐阶段覆盖提示词、模型、思考深度、超时与委托外部智能体。",
|
|
2690
|
+
guide: "工作原理与阶段速查。",
|
|
2691
|
+
}[nav];
|
|
2692
|
+
|
|
2693
|
+
return h("div", { className: "i2p" },
|
|
2694
|
+
h("div", { className: "i2p-body" },
|
|
2695
|
+
h("nav", {
|
|
2696
|
+
className: "i2p-nav" + (navState.open ? "" : " rail"), "aria-label": "工作台目录",
|
|
2697
|
+
style: { "--i2p-nav-w": navState.width + "px" },
|
|
2698
|
+
},
|
|
2699
|
+
h("div", { className: "i2p-nav-title" },
|
|
2700
|
+
h("button", {
|
|
2701
|
+
type: "button", className: "i2p-nav-collapse",
|
|
2702
|
+
onClick: function () { navStore.setOpen(!navState.open); },
|
|
2703
|
+
"aria-expanded": navState.open ? "true" : "false",
|
|
2704
|
+
"aria-label": navState.open ? "收起侧边栏" : "展开侧边栏",
|
|
2705
|
+
title: navState.open ? "收起侧边栏" : "展开侧边栏",
|
|
2706
|
+
}, Ic(navState.open ? "collapse" : "expand", 15)),
|
|
2707
|
+
h("span", { className: "tt" }, "Issue2PR")),
|
|
2708
|
+
h("ul", { className: "i2p-nav-list", role: "tablist" }, navEls),
|
|
2709
|
+
h("p", { className: "i2p-nav-note" },
|
|
2710
|
+
"状态每 3 秒自动刷新;产物目录可通过「打开目录」在文件管理器中查看。"),
|
|
2711
|
+
h("div", {
|
|
2712
|
+
className: "i2p-nav-grip", role: "separator", "aria-orientation": "vertical",
|
|
2713
|
+
"aria-label": "拖动调整目录列宽度(双击恢复默认)", title: "拖动调整宽度 · 双击恢复默认",
|
|
2714
|
+
})),
|
|
2715
|
+
h("main", { className: "i2p-main" },
|
|
2716
|
+
h("div", { className: "sec" },
|
|
2717
|
+
h("h2", null, nav === "projects" ? "项目" : nav === "runs" ? "运行" : nav === "artifacts" ? "产物" : nav === "config" ? "配置" : "说明"),
|
|
2718
|
+
h("p", { className: "sub" }, secSub)),
|
|
2719
|
+
nav === "projects" ? h(ProjectsPanel, {
|
|
2720
|
+
projects: projects,
|
|
2721
|
+
slug: selSlug,
|
|
2722
|
+
toast: toastFn,
|
|
2723
|
+
onSelectProject: selectSlug,
|
|
2724
|
+
onSaved: onSaved,
|
|
2725
|
+
onRunStarted: onRunStarted,
|
|
2726
|
+
onDeletedProject: onProjectDeleted,
|
|
2727
|
+
connections: connections,
|
|
2728
|
+
preflight: preflight,
|
|
2729
|
+
reloadConnections: loadConnections,
|
|
2730
|
+
}) : null,
|
|
2731
|
+
nav === "runs" ? h(RunsPanel, {
|
|
2732
|
+
slug: selSlug,
|
|
2733
|
+
runId: selRunId,
|
|
2734
|
+
runs: runs,
|
|
2735
|
+
run: run,
|
|
2736
|
+
tree: tree,
|
|
2737
|
+
projects: projects,
|
|
2738
|
+
defaults: stageDefaults,
|
|
2739
|
+
toast: toastFn,
|
|
2740
|
+
onPickRun: function (id) { setSelRunId(id); },
|
|
2741
|
+
onChanged: refreshNow,
|
|
2742
|
+
onDeleted: onRunDeleted,
|
|
2743
|
+
onSaved: onSaved,
|
|
2744
|
+
onGoProjects: function () { setNav("projects"); },
|
|
2745
|
+
}) : null,
|
|
2746
|
+
nav === "artifacts" ? h(ArtifactsPanel, {
|
|
2747
|
+
slug: selSlug,
|
|
2748
|
+
runId: selRunId,
|
|
2749
|
+
tree: tree,
|
|
2750
|
+
toast: toastFn,
|
|
2751
|
+
}) : null,
|
|
2752
|
+
nav === "config" ? h(StageConfigPanel, {
|
|
2753
|
+
defaults: stageDefaults,
|
|
2754
|
+
projects: projects,
|
|
2755
|
+
slug: selSlug,
|
|
2756
|
+
selStage: cfgStage,
|
|
2757
|
+
onSelectStage: setCfgStage,
|
|
2758
|
+
toast: toastFn,
|
|
2759
|
+
onSaved: onSaved,
|
|
2760
|
+
}) : null,
|
|
2761
|
+
nav === "guide" ? h(GuidePanel, null) : null)),
|
|
2762
|
+
toast ? h("div", {
|
|
2763
|
+
className: "i2p-toast" + (toast.kind === "bad" ? " t-bad" : toast.kind === "warn" ? " t-warn" : ""),
|
|
2764
|
+
role: "status", "aria-live": "polite",
|
|
2765
|
+
}, toast.msg) : null);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/* ================================================================
|
|
2769
|
+
* 工作台开关(sidebar.footer.action 入口 ↔ conversation.input.dock 面板共享)
|
|
2770
|
+
* ================================================================ */
|
|
2771
|
+
const panelStore = {
|
|
2772
|
+
open: false,
|
|
2773
|
+
listeners: new Set(),
|
|
2774
|
+
set(v) { this.open = v; for (const fn of this.listeners) fn(); },
|
|
2775
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
|
|
2776
|
+
};
|
|
2777
|
+
function usePanelOpen() {
|
|
2778
|
+
const [open, setOpen] = React.useState(panelStore.open);
|
|
2779
|
+
React.useEffect(() => panelStore.subscribe(() => setOpen(panelStore.open)), []);
|
|
2780
|
+
return open;
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
/* ================================================================
|
|
2784
|
+
* 悬浮智能助手(伴随工作台全部五个页面):右上角悬浮球 + 右侧滑出对话栏。
|
|
2785
|
+
* 后端 /assistant/ask 流式 JSONL({"delta"}… {"done"|"error"}),
|
|
2786
|
+
* 上下文(页面位置/项目/Run/最近事件)由后端每次现读
|
|
2787
|
+
* ================================================================ */
|
|
2788
|
+
// 初始(重置)位置固定值:面板右上角锚定工作台右上角内侧——右偏 78px
|
|
2789
|
+
// (避宿主右缘悬浮按钮排约 70px)、顶偏 60px(工作台头部下方)
|
|
2790
|
+
const AI_ANCHOR = { right: 78, top: 60 };
|
|
2791
|
+
const aiStore = {
|
|
2792
|
+
open: false,
|
|
2793
|
+
// 自由窗口矩形:运行时 x/y/w/h(相对工作台左上);**持久化/恢复一律用右上角相对坐标**
|
|
2794
|
+
// (aiR=面板右缘距工作台右缘、aiT=面板顶距工作台顶)——窗口尺寸变化后面板与
|
|
2795
|
+
// 右缘的相对位置不漂移。兼容旧字段(aiX 绝对坐标/aiTop)按当前页宽换算迁移
|
|
2796
|
+
rect: (function () {
|
|
2797
|
+
const d = aiDefaultRect();
|
|
2798
|
+
const r = clampInt(localStorage.getItem("i2p.aiR"), 0, 2000, null);
|
|
2799
|
+
const t = clampInt(localStorage.getItem("i2p.aiT"), 44, 2000, null);
|
|
2800
|
+
const oldT = t != null ? t : clampInt(localStorage.getItem("i2p.aiTop"), 44, 2000, null);
|
|
2801
|
+
const w = clampInt(localStorage.getItem("i2p.aiW"), 240, 760, null);
|
|
2802
|
+
const h = clampInt(localStorage.getItem("i2p.aiH"), 200, 1800, null);
|
|
2803
|
+
const oldX = clampInt(localStorage.getItem("i2p.aiX"), 0, 4000, null);
|
|
2804
|
+
if (r == null && oldT == null && w == null && h == null && oldX == null) return d;
|
|
2805
|
+
const p = aiPageRect();
|
|
2806
|
+
const w2 = w != null ? w : d.w;
|
|
2807
|
+
const r2 = r != null ? r
|
|
2808
|
+
: (oldX != null ? Math.max(4, Math.round(p.w - oldX - w2)) : AI_ANCHOR.right);
|
|
2809
|
+
const t2 = oldT != null ? oldT : AI_ANCHOR.top;
|
|
2810
|
+
return {
|
|
2811
|
+
x: Math.round(p.w - r2 - w2),
|
|
2812
|
+
y: t2,
|
|
2813
|
+
w: w2,
|
|
2814
|
+
h: h != null ? h : Math.max(200, Math.round(p.h - 16 - t2)),
|
|
2815
|
+
};
|
|
2816
|
+
})(),
|
|
2817
|
+
listeners: new Set(),
|
|
2818
|
+
set(v) { this.open = v; for (const fn of this.listeners) fn(); },
|
|
2819
|
+
// setRect:先 clamp 尺寸再 clamp 位置(面板始终完整落在工作台内);
|
|
2820
|
+
// resize 的锚定边由调用方以推导后的 x/y 一并传入(西/北边拖动时对边固定)
|
|
2821
|
+
setRect(patch, persist) {
|
|
2822
|
+
const p = aiPageRect();
|
|
2823
|
+
const cur = this.rect;
|
|
2824
|
+
let w = patch.w != null ? Math.round(patch.w) : cur.w;
|
|
2825
|
+
let h = patch.h != null ? Math.round(patch.h) : cur.h;
|
|
2826
|
+
w = Math.max(240, Math.min(Math.max(240, Math.min(760, p.w - 24)), w));
|
|
2827
|
+
h = Math.max(200, Math.min(Math.max(200, p.h - 60), h));
|
|
2828
|
+
let x = patch.x != null ? Math.round(patch.x) : cur.x;
|
|
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 };
|
|
2833
|
+
if (persist) {
|
|
2834
|
+
// 右上角相对坐标落盘:aiR=右缘偏移、aiT=顶部偏移;旧绝对键清除防干扰
|
|
2835
|
+
try {
|
|
2836
|
+
localStorage.setItem("i2p.aiR", String(Math.round(p.w - x - w)));
|
|
2837
|
+
localStorage.setItem("i2p.aiT", String(y));
|
|
2838
|
+
localStorage.setItem("i2p.aiW", String(w));
|
|
2839
|
+
localStorage.setItem("i2p.aiH", String(h));
|
|
2840
|
+
localStorage.removeItem("i2p.aiX");
|
|
2841
|
+
localStorage.removeItem("i2p.aiY");
|
|
2842
|
+
localStorage.removeItem("i2p.aiTop");
|
|
2843
|
+
} catch (e) { /* webview localStorage 可能被禁,服务端兜底仍在 */ }
|
|
2844
|
+
fetch(API + "/ui-state", {
|
|
2845
|
+
method: "POST",
|
|
2846
|
+
headers: { "Content-Type": "application/json" },
|
|
2847
|
+
body: JSON.stringify({ aiR: Math.round(p.w - x - w), aiT: y, aiW: w, aiH: h }),
|
|
2848
|
+
}).catch(function () { /* 兜底失败静默 */ });
|
|
2849
|
+
}
|
|
2850
|
+
for (const fn of this.listeners) fn();
|
|
2851
|
+
},
|
|
2852
|
+
resetRect() { this.setRect(aiDefaultRect(), true); },
|
|
2853
|
+
subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
|
|
2854
|
+
};
|
|
2855
|
+
function useAiState() {
|
|
2856
|
+
const [s, setS] = React.useState({ open: aiStore.open, rect: aiStore.rect });
|
|
2857
|
+
React.useEffect(() => aiStore.subscribe(() => setS({ open: aiStore.open, rect: aiStore.rect })), []);
|
|
2858
|
+
return s;
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
const AI_CHIPS = ["现在的运行到哪一步了?", "P1-P11 各做什么?", "这个 Run 为什么会失败?"];
|
|
2862
|
+
|
|
2863
|
+
function clampInt(v, lo, hi, def) {
|
|
2864
|
+
const n = parseInt(v, 10);
|
|
2865
|
+
return Number.isFinite(n) && n >= lo && n <= hi ? n : def;
|
|
2866
|
+
}
|
|
2867
|
+
// 工作台 page 的可用尺寸(查询失败退回视口近似——smoke/preview 环境无真实 rect)
|
|
2868
|
+
function aiPageRect() {
|
|
2869
|
+
const el = document.querySelector(".i2p-page");
|
|
2870
|
+
const r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
2871
|
+
return r ? { w: r.width, h: r.height } : { w: window.innerWidth || 1280, h: window.innerHeight || 720 };
|
|
2872
|
+
}
|
|
2873
|
+
// 默认窗口:右上角按 AI_ANCHOR 固定偏移锚定(窗口尺寸变化时相对位置不变)
|
|
2874
|
+
function aiDefaultRect() {
|
|
2875
|
+
const p = aiPageRect();
|
|
2876
|
+
return {
|
|
2877
|
+
x: Math.max(4, Math.round(p.w - AI_ANCHOR.right - 400)),
|
|
2878
|
+
y: AI_ANCHOR.top, w: 400,
|
|
2879
|
+
h: Math.max(200, Math.round(p.h - 76)),
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function AssistantDock() {
|
|
2884
|
+
const ai = useAiState();
|
|
2885
|
+
const open = ai.open;
|
|
2886
|
+
const view = useViewState();
|
|
2887
|
+
const [msgs, setMsgs] = React.useState([]); // {role:"user"|"assistant", text, err?}
|
|
2888
|
+
const [input, setInput] = React.useState("");
|
|
2889
|
+
const [busy, setBusy] = React.useState(false);
|
|
2890
|
+
const acRef = React.useRef(null);
|
|
2891
|
+
const scrollRef = React.useRef(null);
|
|
2892
|
+
const taRef = React.useRef(null);
|
|
2893
|
+
|
|
2894
|
+
// Esc 只关助手面板:capture 阶段拦截,避免穿透到工作台的「Esc 关工作台」
|
|
2895
|
+
React.useEffect(function () {
|
|
2896
|
+
if (!open) return undefined;
|
|
2897
|
+
const onKey = function (e) {
|
|
2898
|
+
if (e.key === "Escape") { e.stopPropagation(); aiStore.set(false); }
|
|
2899
|
+
};
|
|
2900
|
+
window.addEventListener("keydown", onKey, true);
|
|
2901
|
+
return function () { window.removeEventListener("keydown", onKey, true); };
|
|
2902
|
+
}, [open]);
|
|
2903
|
+
|
|
2904
|
+
// 打开时聚焦输入框;新消息滚动到底
|
|
2905
|
+
React.useEffect(function () {
|
|
2906
|
+
if (open && taRef.current && taRef.current.focus) taRef.current.focus();
|
|
2907
|
+
}, [open]);
|
|
2908
|
+
React.useEffect(function () {
|
|
2909
|
+
const el = scrollRef.current;
|
|
2910
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
2911
|
+
}, [msgs]);
|
|
2912
|
+
|
|
2913
|
+
const send = React.useCallback(async function (q) {
|
|
2914
|
+
const question = String(q != null ? q : input).trim();
|
|
2915
|
+
if (!question || busy) return;
|
|
2916
|
+
setInput("");
|
|
2917
|
+
const hist = msgs.slice(-6)
|
|
2918
|
+
.filter(function (m) { return m.text && !m.err; })
|
|
2919
|
+
.map(function (m) { return { role: m.role, text: m.text }; });
|
|
2920
|
+
setMsgs(function (ms) { return ms.concat([{ role: "user", text: question }, { role: "assistant", text: "" }]); });
|
|
2921
|
+
setBusy(true);
|
|
2922
|
+
if (acRef.current) acRef.current.abort();
|
|
2923
|
+
const ac = new AbortController();
|
|
2924
|
+
acRef.current = ac;
|
|
2925
|
+
const patchLast = function (fn) {
|
|
2926
|
+
setMsgs(function (ms) {
|
|
2927
|
+
if (!ms.length || ms[ms.length - 1].role !== "assistant") return ms;
|
|
2928
|
+
const next = ms.slice();
|
|
2929
|
+
next[next.length - 1] = fn(next[next.length - 1]);
|
|
2930
|
+
return next;
|
|
2931
|
+
});
|
|
2932
|
+
};
|
|
2933
|
+
try {
|
|
2934
|
+
const r = await fetch(API + "/assistant/ask", {
|
|
2935
|
+
method: "POST",
|
|
2936
|
+
headers: { "Content-Type": "application/json" },
|
|
2937
|
+
body: JSON.stringify({ question: question, history: hist, focus: { nav: view.nav, slug: view.slug, runId: view.runId } }),
|
|
2938
|
+
signal: ac.signal,
|
|
2939
|
+
});
|
|
2940
|
+
if (!r.ok || !r.body) throw new Error("HTTP " + r.status);
|
|
2941
|
+
const reader = r.body.getReader();
|
|
2942
|
+
const dec = new TextDecoder();
|
|
2943
|
+
let buf = "";
|
|
2944
|
+
for (;;) {
|
|
2945
|
+
const rd = await reader.read();
|
|
2946
|
+
if (rd.done) break;
|
|
2947
|
+
buf += dec.decode(rd.value, { stream: true });
|
|
2948
|
+
const lines = buf.split("\n");
|
|
2949
|
+
buf = lines.pop();
|
|
2950
|
+
for (const line of lines) {
|
|
2951
|
+
if (!line.trim()) continue;
|
|
2952
|
+
let ev = null;
|
|
2953
|
+
try { ev = JSON.parse(line); } catch (e) { continue; }
|
|
2954
|
+
if (ev.error) throw new Error(ev.error);
|
|
2955
|
+
if (ev.delta) {
|
|
2956
|
+
const d = ev.delta;
|
|
2957
|
+
patchLast(function (m) { return { role: "assistant", text: m.text + d }; });
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
patchLast(function (m) { return m.text ? m : { role: "assistant", text: "(空回答)", err: true }; });
|
|
2962
|
+
} catch (e) {
|
|
2963
|
+
if (ac.signal.aborted) {
|
|
2964
|
+
// 被新请求或工作台关闭取代:消息列表归新请求/卸载流程管,这里不动
|
|
2965
|
+
} else {
|
|
2966
|
+
const msg = String((e && e.message) || e);
|
|
2967
|
+
patchLast(function (m) { return { role: "assistant", text: m.text || msg, err: !m.text }; });
|
|
2968
|
+
}
|
|
2969
|
+
} finally {
|
|
2970
|
+
setBusy(false);
|
|
2971
|
+
if (acRef.current === ac) acRef.current = null;
|
|
2972
|
+
}
|
|
2973
|
+
}, [input, busy, msgs, view]);
|
|
2974
|
+
|
|
2975
|
+
// 工作台关闭(组件卸载)时中断进行中的生成
|
|
2976
|
+
React.useEffect(function () {
|
|
2977
|
+
return function () { if (acRef.current) acRef.current.abort(); };
|
|
2978
|
+
}, []);
|
|
2979
|
+
|
|
2980
|
+
// 面板窗口矩形兜底:宿主 webview 的 localStorage 不跨软件重启——本地无记忆时
|
|
2981
|
+
// 从服务端 ui-state 恢复(右上角相对坐标 aiR/aiT;兼容旧 aiX/aiY/aiTop 按当前
|
|
2982
|
+
// 页宽换算);本地有记忆时反向同步服务端(防旧值空窗)
|
|
2983
|
+
React.useEffect(function () {
|
|
2984
|
+
apiGet("/ui-state").then(function (r) {
|
|
2985
|
+
if (!r || !r.ok || !r.state) return;
|
|
2986
|
+
const local = ["i2p.aiR", "i2p.aiT", "i2p.aiW", "i2p.aiH", "i2p.aiX", "i2p.aiTop"].some(function (k) { return localStorage.getItem(k) != null; });
|
|
2987
|
+
const p = aiPageRect();
|
|
2988
|
+
if (local) {
|
|
2989
|
+
fetch(API + "/ui-state", {
|
|
2990
|
+
method: "POST",
|
|
2991
|
+
headers: { "Content-Type": "application/json" },
|
|
2992
|
+
body: JSON.stringify({ aiR: Math.round(p.w - aiStore.rect.x - aiStore.rect.w), aiT: aiStore.rect.y, aiW: aiStore.rect.w, aiH: aiStore.rect.h }),
|
|
2993
|
+
}).catch(function () { /* 兜底失败静默 */ });
|
|
2994
|
+
} else if (r.state.aiR != null || r.state.aiX != null || r.state.aiW != null || r.state.aiTop != null) {
|
|
2995
|
+
const w2 = r.state.aiW != null ? r.state.aiW : aiStore.rect.w;
|
|
2996
|
+
const r2 = r.state.aiR != null ? r.state.aiR
|
|
2997
|
+
: (r.state.aiX != null ? Math.max(4, Math.round(p.w - r.state.aiX - w2)) : AI_ANCHOR.right);
|
|
2998
|
+
aiStore.setRect({
|
|
2999
|
+
x: Math.round(p.w - r2 - w2),
|
|
3000
|
+
y: r.state.aiT != null ? r.state.aiT : r.state.aiTop,
|
|
3001
|
+
w: r.state.aiW, h: r.state.aiH,
|
|
3002
|
+
}, false);
|
|
3003
|
+
}
|
|
3004
|
+
}).catch(function () { /* 兜底失败静默 */ });
|
|
3005
|
+
}, []);
|
|
3006
|
+
|
|
3007
|
+
// 自由窗口拖拽:resize 边/角(含双向角点)+ 标题栏拖动移动。
|
|
3008
|
+
// 原生事件委托(宿主注入环境里 React 合成事件对 pointer 不可靠);
|
|
3009
|
+
// 双击 resize 区或标题栏 = 恢复默认位置与尺寸(WCAG 2.2 拖拽的单指针替代路径)
|
|
3010
|
+
React.useEffect(function () {
|
|
3011
|
+
const hitOf = function (t) {
|
|
3012
|
+
if (!t || !t.closest) return null;
|
|
3013
|
+
return t.closest(".i2p-ai-rz") || t.closest(".i2p-ai-head");
|
|
3014
|
+
};
|
|
3015
|
+
const down = function (e) {
|
|
3016
|
+
if (e.button !== 0) return;
|
|
3017
|
+
const hit = hitOf(e.target);
|
|
3018
|
+
if (!hit) return;
|
|
3019
|
+
// 标题栏上的按钮(关闭等)不触发拖动
|
|
3020
|
+
if (hit.classList.contains("i2p-ai-head") && e.target.closest && e.target.closest("button")) return;
|
|
3021
|
+
const page = hit.closest(".i2p-page");
|
|
3022
|
+
if (!page) return;
|
|
3023
|
+
e.preventDefault();
|
|
3024
|
+
const mode = hit.classList.contains("i2p-ai-head") ? "move"
|
|
3025
|
+
: ((hit.className.match(/i2p-ai-rz\s+(\S+)/) || [])[1] || "");
|
|
3026
|
+
if (!mode) return;
|
|
3027
|
+
const start = {
|
|
3028
|
+
x: e.clientX, y: e.clientY,
|
|
3029
|
+
rect: { x: aiStore.rect.x, y: aiStore.rect.y, w: aiStore.rect.w, h: aiStore.rect.h },
|
|
3030
|
+
};
|
|
3031
|
+
document.body.classList.add("i2p-dragging");
|
|
3032
|
+
document.body.style.cursor = mode === "move" ? "move"
|
|
3033
|
+
: (/n|s/.test(mode) && /e|w/.test(mode) ? (/nw|se/.test(mode) ? "nwse-resize" : "nesw-resize")
|
|
3034
|
+
: (/n|s/.test(mode) ? "ns-resize" : "ew-resize"));
|
|
3035
|
+
const move = function (ev) {
|
|
3036
|
+
const dx = ev.clientX - start.x, dy = ev.clientY - start.y;
|
|
3037
|
+
if (mode === "move") {
|
|
3038
|
+
aiStore.setRect({ x: start.rect.x + dx, y: start.rect.y + dy }, false);
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
let w = start.rect.w, h = start.rect.h, x = start.rect.x, y = start.rect.y;
|
|
3042
|
+
if (mode.indexOf("e") >= 0) w = start.rect.w + dx;
|
|
3043
|
+
if (mode.indexOf("s") >= 0) h = start.rect.h + dy;
|
|
3044
|
+
if (mode.indexOf("w") >= 0) { w = start.rect.w - dx; x = start.rect.x + dx; }
|
|
3045
|
+
if (mode.indexOf("n") >= 0) { h = start.rect.h - dy; y = start.rect.y + dy; }
|
|
3046
|
+
aiStore.setRect({ x: x, y: y, w: w, h: h }, false);
|
|
3047
|
+
};
|
|
3048
|
+
const up = function () {
|
|
3049
|
+
window.removeEventListener("pointermove", move);
|
|
3050
|
+
window.removeEventListener("pointerup", up);
|
|
3051
|
+
document.body.style.cursor = "";
|
|
3052
|
+
document.body.classList.remove("i2p-dragging");
|
|
3053
|
+
aiStore.setRect({}, true); // 拖动结束落盘(双写)
|
|
3054
|
+
};
|
|
3055
|
+
window.addEventListener("pointermove", move);
|
|
3056
|
+
window.addEventListener("pointerup", up);
|
|
3057
|
+
};
|
|
3058
|
+
const dbl = function (e) {
|
|
3059
|
+
if (hitOf(e.target)) aiStore.resetRect();
|
|
3060
|
+
};
|
|
3061
|
+
document.addEventListener("pointerdown", down);
|
|
3062
|
+
document.addEventListener("dblclick", dbl);
|
|
3063
|
+
return function () {
|
|
3064
|
+
document.removeEventListener("pointerdown", down);
|
|
3065
|
+
document.removeEventListener("dblclick", dbl);
|
|
3066
|
+
};
|
|
3067
|
+
}, []);
|
|
3068
|
+
|
|
3069
|
+
return h("div", { className: "i2p-ai" },
|
|
3070
|
+
h("button", {
|
|
3071
|
+
type: "button", className: "i2p-ai-fab" + (open ? " on" : ""),
|
|
3072
|
+
onClick: function () { aiStore.set(!open); },
|
|
3073
|
+
"aria-expanded": open ? "true" : "false",
|
|
3074
|
+
"aria-label": open ? "关闭智能助手" : "智能助手(问答与运行状态)",
|
|
3075
|
+
title: open ? "关闭智能助手" : "智能助手 · 问运行状态、阶段含义、失败原因",
|
|
3076
|
+
}, Ic("msg", 15)),
|
|
3077
|
+
open ? h("aside", {
|
|
3078
|
+
className: "i2p-ai-panel", role: "complementary", "aria-label": "智能助手",
|
|
3079
|
+
style: { left: ai.rect.x + "px", top: ai.rect.y + "px", width: ai.rect.w + "px", height: ai.rect.h + "px" },
|
|
3080
|
+
},
|
|
3081
|
+
["n", "s", "e", "w", "nw", "ne", "sw", "se"].map(function (d) {
|
|
3082
|
+
return h("div", { key: d, className: "i2p-ai-rz " + d, title: "拖动调整大小 · 双击恢复默认" });
|
|
3083
|
+
}),
|
|
3084
|
+
h("div", { className: "i2p-ai-head", title: "拖动移动位置 · 双击恢复默认" },
|
|
3085
|
+
h("span", { className: "i2p-ai-title" }, "智能助手"),
|
|
3086
|
+
h("span", { className: "i2p-ai-sub" }, "问答 · 运行状态"),
|
|
3087
|
+
h("button", {
|
|
3088
|
+
type: "button", className: "i2p-ai-close", onClick: function () { aiStore.set(false); },
|
|
3089
|
+
"aria-label": "关闭(Esc)", title: "关闭(Esc)",
|
|
3090
|
+
}, Ic("x", 14))),
|
|
3091
|
+
h("div", { className: "i2p-ai-msgs", ref: scrollRef },
|
|
3092
|
+
msgs.length === 0 ? h("div", { className: "i2p-ai-empty" },
|
|
3093
|
+
h("p", { className: "i2p-ai-hint" }, "我能看到你的项目、Run 进度与最近事件——直接问。"),
|
|
3094
|
+
h("div", { className: "i2p-ai-chips" },
|
|
3095
|
+
AI_CHIPS.map(function (c) {
|
|
3096
|
+
return h("button", {
|
|
3097
|
+
key: c, type: "button", className: "i2p-ai-chip", title: c,
|
|
3098
|
+
disabled: busy, onClick: function () { send(c); },
|
|
3099
|
+
}, c);
|
|
3100
|
+
}))) : null,
|
|
3101
|
+
msgs.map(function (m, i) {
|
|
3102
|
+
const waiting = busy && i === msgs.length - 1 && m.role === "assistant" && !m.err && !m.text;
|
|
3103
|
+
return h("div", { key: i, className: "i2p-ai-m " + m.role + (m.err ? " err" : "") },
|
|
3104
|
+
m.role === "assistant" && !m.err
|
|
3105
|
+
? h("div", { className: "i2p-ai-md" },
|
|
3106
|
+
waiting ? h("span", { className: "i2p-ai-dots" }, h("i"), h("i"), h("i"))
|
|
3107
|
+
: h(MarkdownText, { text: m.text }))
|
|
3108
|
+
: h("span", { className: "i2p-ai-t" }, m.text));
|
|
3109
|
+
})),
|
|
3110
|
+
h("div", { className: "i2p-ai-input" },
|
|
3111
|
+
h("textarea", {
|
|
3112
|
+
ref: taRef, value: input, rows: 2,
|
|
3113
|
+
placeholder: busy ? "回答生成中…" : "问运行状态、阶段含义、失败原因…(Enter 发送)",
|
|
3114
|
+
onChange: function (e) { setInput(e.target.value); },
|
|
3115
|
+
onKeyDown: function (e) {
|
|
3116
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
|
|
3117
|
+
},
|
|
3118
|
+
}),
|
|
3119
|
+
h("button", {
|
|
3120
|
+
type: "button", className: "i2p-ai-send", disabled: !busy && !input.trim(),
|
|
3121
|
+
onClick: function () { if (busy) { if (acRef.current) acRef.current.abort(); return; } send(); },
|
|
3122
|
+
"aria-label": busy ? "中断生成" : "发送(Enter)", title: busy ? "中断生成" : "发送(Enter)",
|
|
3123
|
+
}, Ic(busy ? "stop" : "send", 15)))) : null);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// 入口图标:Issue→PR 分支(描边、currentColor,随宿主主题)
|
|
3127
|
+
function EntryGlyph(props) {
|
|
3128
|
+
return h("svg", Object.assign({
|
|
3129
|
+
viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5,
|
|
3130
|
+
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
3131
|
+
}, props),
|
|
3132
|
+
h("circle", { cx: 4.5, cy: 3.5, r: 2 }),
|
|
3133
|
+
h("circle", { cx: 4.5, cy: 12.5, r: 2 }),
|
|
3134
|
+
h("circle", { cx: 11.5, cy: 6.5, r: 2 }),
|
|
3135
|
+
h("path", { d: "M4.5 5.5v5M11.5 8.5c0 2.8-3.2 2.4-4.7 3.2" }));
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// 侧边栏底部入口按钮:1:1 复刻宿主设置按钮几何与交互
|
|
3139
|
+
// (宽列 = 42px 整行·图标+文字靠左;收起列 = 36×36 圆形·仅图标;点击开/关工作台)
|
|
3140
|
+
function FooterEntry(props) {
|
|
3141
|
+
const wide = !!(props && props.wide);
|
|
3142
|
+
const open = usePanelOpen();
|
|
3143
|
+
return h("button", {
|
|
3144
|
+
type: "button", className: "i2p-entry" + (wide ? "" : " rail"),
|
|
3145
|
+
onClick: () => panelStore.set(!open),
|
|
3146
|
+
"aria-expanded": open ? "true" : "false",
|
|
3147
|
+
},
|
|
3148
|
+
h(EntryGlyph, { width: wide ? 16 : 18, height: wide ? 16 : 18 }),
|
|
3149
|
+
wide ? h("span", { className: "i2p-entry-label" }, "Issue2PR") : null);
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// 主区整页工作台(shell.overlay 槽位):贴合 conversation 列 rect,
|
|
3153
|
+
// 未开启时渲染 null;sidebar 拖动/窗口缩放实时跟随
|
|
3154
|
+
function WorkbenchPage() {
|
|
3155
|
+
const open = usePanelOpen();
|
|
3156
|
+
const closeRef = React.useRef(null);
|
|
3157
|
+
const [rect, setRect] = React.useState(null);
|
|
3158
|
+
React.useEffect(() => {
|
|
3159
|
+
if (!open) return undefined;
|
|
3160
|
+
const onKey = (e) => { if (e.key === "Escape") panelStore.set(false); };
|
|
3161
|
+
window.addEventListener("keydown", onKey);
|
|
3162
|
+
if (closeRef.current) closeRef.current.focus();
|
|
3163
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
3164
|
+
}, [open]);
|
|
3165
|
+
React.useLayoutEffect(() => {
|
|
3166
|
+
if (!open) { setRect(null); return undefined; }
|
|
3167
|
+
// conversation 槽位 anchor 的父级即主内容列(CenterColumn)。
|
|
3168
|
+
// 用轻量轮询贴合而非 resize/ResizeObserver:嵌入式 webview(IAB)
|
|
3169
|
+
// 里两者都可能不触发;300ms 轮询一次 getBoundingClientRect 且值
|
|
3170
|
+
// 不变不 setState,顺带覆盖 sidebar 拖动、details 开关等一切变化
|
|
3171
|
+
const anchor = document.querySelector('[data-slot="conversation"]');
|
|
3172
|
+
const host = anchor && anchor.parentElement;
|
|
3173
|
+
if (!host) return undefined;
|
|
3174
|
+
let last = "";
|
|
3175
|
+
const tick = () => {
|
|
3176
|
+
const r = host.getBoundingClientRect();
|
|
3177
|
+
const key = r.left + "," + r.top + "," + r.width + "," + r.height;
|
|
3178
|
+
if (key !== last) {
|
|
3179
|
+
last = key;
|
|
3180
|
+
setRect({ left: r.left, top: r.top, width: r.width, height: r.height });
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
tick();
|
|
3184
|
+
const timer = setInterval(tick, 300);
|
|
3185
|
+
return () => clearInterval(timer);
|
|
3186
|
+
}, [open]);
|
|
3187
|
+
if (!open || rect === null) return null;
|
|
3188
|
+
return h("div", {
|
|
3189
|
+
className: "i2p-page", role: "region", "aria-label": "Issue2PR 工作台",
|
|
3190
|
+
style: {
|
|
3191
|
+
left: rect.left + "px", top: rect.top + "px",
|
|
3192
|
+
width: rect.width + "px", height: rect.height + "px",
|
|
3193
|
+
},
|
|
3194
|
+
},
|
|
3195
|
+
h("div", { className: "i2p-head" },
|
|
3196
|
+
h("span", { className: "brand" },
|
|
3197
|
+
h(EntryGlyph, { width: 16, height: 16, style: { alignSelf: "center" } }),
|
|
3198
|
+
h("b", null, "Issue2PR"),
|
|
3199
|
+
h("span", null, "ISSUE → PR"),
|
|
3200
|
+
h("span", { className: "om" }, "v0.3")),
|
|
3201
|
+
h("button", {
|
|
3202
|
+
type: "button", className: "i2p-close", ref: closeRef,
|
|
3203
|
+
onClick: () => panelStore.set(false), "aria-label": "关闭(Esc)",
|
|
3204
|
+
}, Ic("x", 15))),
|
|
3205
|
+
h("div", { className: "i2p-page-body" }, h(Section)),
|
|
3206
|
+
h(AssistantDock));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
/* ================================================================
|
|
3210
|
+
* apply:主入口 = 侧边栏底部按钮 + 主区整页工作台;
|
|
3211
|
+
* settings.section 保留为次级入口
|
|
3212
|
+
* ================================================================ */
|
|
3213
|
+
function apply(ctx) {
|
|
3214
|
+
const t = ctx.locale.bind("issue2pr");
|
|
3215
|
+
ctx.effect(() => ctx.locale.register("issue2pr", { zh, en }), "issue2pr: dictionaries");
|
|
3216
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register(
|
|
3217
|
+
{ name: "settings.section", id: "issue2pr", order: 17, label: () => t("nav"), locale: "issue2pr" }, Section));
|
|
3218
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
|
|
3219
|
+
{ name: "sidebar.footer.action", id: "issue2pr", order: 30 }, FooterEntry), "issue2pr: sidebar entry");
|
|
3220
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register(
|
|
3221
|
+
{ name: "shell.overlay", id: "issue2pr", order: 90 }, WorkbenchPage), "issue2pr: main-area page");
|
|
3222
|
+
}
|
|
3223
|
+
exports.apply = apply;
|
|
3224
|
+
exports.inject = ["slots", "locale"];
|
|
3225
|
+
return module.exports;
|
|
3226
|
+
}
|
|
3227
|
+
});
|