5fd0d3c148
pay-server 自带 /_test 测试页:创建订单→显示收款地址+精确金额(可复制)
+过期倒计时→轮询状态到 paid。同源、无 CORS/CSP 摩擦,不碰营销站安全头;
纯调 POST /order + GET /order/{id},无私钥无密钥。生产收银台后续走独角数卡门面。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1733 lines
86 KiB
HTML
1733 lines
86 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>feature+windows — 项目 TODO</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body {
|
||
margin: 0; padding: 0 0 80px;
|
||
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
color: #1f2933; background: #f7f9fb; line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
header {
|
||
background: linear-gradient(135deg, #0A1F3B 0%, #15407D 100%);
|
||
color: #fff; padding: 36px 32px 28px;
|
||
}
|
||
header h1 { margin: 0 0 6px; font-size: 24px; font-weight: 700; }
|
||
header .header-meta { color: #ADC9EA; font-size: 13px; margin-top: 4px; }
|
||
.stats { display: flex; gap: 16px; margin-top: 14px; flex-wrap: wrap; }
|
||
.stat-pill {
|
||
background: rgba(255,255,255,0.12); border-radius: 20px;
|
||
padding: 4px 14px; font-size: 13px; color: #fff;
|
||
}
|
||
.stat-pill strong { font-size: 18px; font-weight: 700; margin-right: 2px; }
|
||
|
||
/* ── 筛选栏 ── */
|
||
.filter-bar {
|
||
position: sticky; top: 0; z-index: 10;
|
||
background: #fff; border-bottom: 1px solid #e4e9ef;
|
||
padding: 10px 28px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
|
||
}
|
||
.filter-group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||
.filter-label { font-size: 12px; color: #7b8794; white-space: nowrap; }
|
||
.filter-chip, .filter-btn {
|
||
padding: 3px 12px; border-radius: 14px; border: 1.5px solid #d1d9e6;
|
||
background: #f7f9fb; color: #52606d; font-size: 12.5px; cursor: pointer;
|
||
transition: all .15s; white-space: nowrap;
|
||
}
|
||
.filter-chip:hover, .filter-btn:hover { border-color: #2563AC; color: #2563AC; }
|
||
.filter-chip.active, .filter-btn.active {
|
||
background: #2563AC; border-color: #2563AC; color: #fff;
|
||
}
|
||
.filter-sep { width: 1px; height: 20px; background: #e4e9ef; margin: 0 4px; }
|
||
|
||
/* ── 内容区 ── */
|
||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px; }
|
||
.section-block { margin-bottom: 8px; }
|
||
.section-title {
|
||
font-size: 15px; font-weight: 700;
|
||
padding: 10px 14px; border-radius: 8px;
|
||
display: flex; align-items: center; gap: 8px; cursor: pointer;
|
||
margin: 20px 0 8px; user-select: none;
|
||
}
|
||
.section-title .s-count {
|
||
font-size: 12px; font-weight: 500; padding: 1px 8px; border-radius: 10px;
|
||
background: rgba(0,0,0,0.08);
|
||
}
|
||
.section-title .s-arrow { margin-left: auto; font-size: 12px; color: inherit; opacity: .6; }
|
||
|
||
/* 状态主题色 */
|
||
.st-open { background: #eff6ff; color: #1e40af; border-left: 4px solid #3b82f6; }
|
||
.st-doing { background: #fff7ed; color: #9a3412; border-left: 4px solid #f97316; }
|
||
.st-done { background: #fefce8; color: #854d0e; border-left: 4px solid #eab308; }
|
||
.st-accepted{ background: #f0fdf4; color: #166534; border-left: 4px solid #22c55e; }
|
||
|
||
ul.todo-list { list-style: none; margin: 0; padding: 0; }
|
||
.todo-card {
|
||
background: #fff; border: 1px solid #e4e9ef; border-radius: 10px;
|
||
padding: 14px 18px; margin-bottom: 10px;
|
||
border-left-width: 4px;
|
||
}
|
||
.todo-card.s-open { border-left-color: #3b82f6; }
|
||
.todo-card.s-doing { border-left-color: #f97316; }
|
||
.todo-card.s-done { border-left-color: #eab308; }
|
||
.todo-card.s-accepted { border-left-color: #22c55e; background: #f0fdf4; opacity: .8; }
|
||
|
||
.card-header { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
|
||
.item-id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 700; font-size: 13px; color: #2563AC; background: #eef4fb; border: 1px solid #d3e2f3; border-radius: 6px; padding: 1px 7px; flex: 0 0 auto; }
|
||
.item-title { font-weight: 600; font-size: 15px; flex: 1; }
|
||
.todo-card.s-accepted .item-title { text-decoration: line-through; color: #52606d; }
|
||
.card-badges { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
||
.item-desc { font-size: 13.5px; color: #52606d; margin-top: 6px; }
|
||
.item-desc code {
|
||
background: #f0f2f5; padding: 1px 5px; border-radius: 3px;
|
||
font-family: "JetBrains Mono", monospace; font-size: 12.5px; color: #b91c1c;
|
||
}
|
||
.card-footer { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
|
||
.tag-row { display: flex; flex-wrap: wrap; gap: 6px; flex: 1; }
|
||
.item-meta { display: flex; flex-wrap: wrap; gap: 8px; font-size: 12px; color: #8aa3c4; }
|
||
.meta-date { white-space: nowrap; }
|
||
|
||
/* ── 标签样式 ── */
|
||
.tag { display: inline-block; padding: 2px 9px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
||
.t-block { background: #fde8e8; color: #b91c1c; }
|
||
.t-high { background: #fef0d8; color: #9a6700; }
|
||
.t-low { background: #e6f0fb; color: #1d4ed8; }
|
||
.t-tag { background: #efefef; color: #555; cursor: pointer; transition: .12s; }
|
||
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
|
||
.ver-badge { font-weight: 700; color: #1f7a44; }
|
||
.sub-progress-badge { background: #f0f4ff; color: #3730a3; }
|
||
.owner-user{background:rgba(232,192,122,.16);border:1px solid #e8c07a;color:#e8c07a}
|
||
.owner-agent{background:rgba(126,224,162,.14);border:1px solid #7ee0a2;color:#7ee0a2}
|
||
|
||
/* 状态徽章 */
|
||
.status-badge { font-size: 11.5px; }
|
||
.s-open { background: #dbeafe; color: #1e40af; }
|
||
.s-doing { background: #ffedd5; color: #9a3412; }
|
||
.s-done { background: #fef9c3; color: #854d0e; }
|
||
.s-accepted { background: #dcfce7; color: #166534; }
|
||
|
||
/* ── 折叠 ── */
|
||
.section-list-wrap.collapsed { display: none; }
|
||
.empty-tip { font-size: 14px; color: #8aa3c4; margin: 8px 0 24px; }
|
||
.hidden { display: none !important; }
|
||
|
||
/* ── 改动等级徽章 ── */
|
||
.tier-1 { background: #ede9fe; color: #6d28d9; }
|
||
.tier-2 { background: #ccfbf1; color: #0f766e; }
|
||
.tier-3 { background: #f1f5f9; color: #475569; }
|
||
|
||
/* ── 拒绝按钮 & 拒绝原因 ── */
|
||
.reject-btn {
|
||
padding: 3px 10px; border-radius: 6px; border: 1.5px solid #dc2626;
|
||
background: #fff; color: #dc2626; font-size: 12px; font-weight: 600;
|
||
cursor: pointer; transition: all .15s; white-space: nowrap;
|
||
}
|
||
.reject-btn:hover { background: #dc2626; color: #fff; }
|
||
.reject-note {
|
||
margin: 8px 0 4px; padding: 7px 12px; border-radius: 6px;
|
||
background: #fef2f2; border: 1px solid #fecaca;
|
||
font-size: 13px; color: #b91c1c; line-height: 1.5;
|
||
}
|
||
.reject-date { color: #ef9999; font-size: 12px; }
|
||
|
||
/* ── 确认闸(方案/改动说明)── */
|
||
.gate-block { margin: 10px 0 4px; padding: 10px 14px; border-radius: 8px; font-size: 13px; }
|
||
.gate-pending { background: #fff7ed; border: 1px solid #fdba74; }
|
||
.gate-granted { background: #f0fdf4; border: 1px solid #bbf7d0; }
|
||
.gate-info { background: #f1f5f9; border: 1px solid #e2e8f0; }
|
||
.gate-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
|
||
.gate-badge { padding: 2px 9px; border-radius: 10px; font-size: 11.5px; font-weight: 700; white-space: nowrap; }
|
||
.gate-badge.pending { background: #f97316; color: #fff; }
|
||
.gate-badge.granted { background: #22c55e; color: #fff; }
|
||
.gate-badge.info { background: #94a3b8; color: #fff; }
|
||
.gate-kind { font-size: 12px; color: #52606d; }
|
||
.gate-date { font-size: 12px; color: #8aa3c4; margin-left: auto; }
|
||
.gate-note { color: #52606d; margin: 4px 0; white-space: pre-wrap; }
|
||
.gate-ref { font-size: 12.5px; color: #52606d; margin-top: 4px; }
|
||
.gate-note code, .gate-ref code {
|
||
background: #fff; padding: 1px 5px; border-radius: 3px;
|
||
font-family: "JetBrains Mono", monospace; font-size: 12px; color: #b91c1c;
|
||
}
|
||
.approve-btn {
|
||
margin-top: 8px; padding: 5px 14px; border-radius: 6px; border: none;
|
||
background: #16a34a; color: #fff; font-size: 12.5px; font-weight: 600; cursor: pointer;
|
||
transition: background .15s;
|
||
}
|
||
.approve-btn:hover { background: #15803d; }
|
||
.approve-cmd { margin-top: 10px; padding: 10px 12px; background: #1e293b; border-radius: 8px; }
|
||
.approve-cmd-label { font-size: 12px; color: #94a3b8; margin-bottom: 6px; }
|
||
.approve-cmd-code { font-family: "JetBrains Mono", monospace; font-size: 13px; color: #86efac; display: block; word-break: break-all; }
|
||
.approve-copy-btn { margin-top: 8px; padding: 4px 12px; border-radius: 6px; background: #334155; color: #e2e8f0; border: none; font-size: 12px; cursor: pointer; }
|
||
.approve-copy-btn:hover { background: #475569; }
|
||
.stat-pill.gate-stat { background: #f97316; }
|
||
|
||
/* ── 子任务区 ── */
|
||
.subtask-block {
|
||
margin-top: 12px; padding: 10px 14px;
|
||
background: #f8fafc; border: 1px solid #e4e9ef; border-radius: 8px;
|
||
}
|
||
.subtask-header {
|
||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap;
|
||
}
|
||
.subtask-label {
|
||
font-size: 11px; font-weight: 700; color: #7b8794;
|
||
text-transform: uppercase; letter-spacing: .6px;
|
||
}
|
||
.subtask-progress-text { font-size: 12px; color: #52606d; }
|
||
.subtask-progress-bar {
|
||
flex: 1; height: 5px; background: #e4e9ef; border-radius: 3px; min-width: 60px;
|
||
}
|
||
.subtask-progress-fill { height: 100%; background: #22c55e; border-radius: 3px; transition: width .4s; }
|
||
.subtask-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
|
||
.subtask-item {
|
||
display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
|
||
padding: 6px 10px; border-radius: 6px; font-size: 13px;
|
||
background: #fff; border: 1px solid #e8edf2;
|
||
}
|
||
.subtask-item.s-done { background: #f0fdf4; border-color: #bbf7d0; }
|
||
.subtask-item.s-doing { background: #fff7ed; border-color: #fed7aa; }
|
||
.subtask-item.s-accepted { background: #f0fdf4; border-color: #bbf7d0; opacity: .75; }
|
||
.sub-icon {
|
||
width: 16px; text-align: center; font-size: 13px; font-weight: 700; flex-shrink: 0;
|
||
}
|
||
.sub-icon.s-done { color: #16a34a; }
|
||
.sub-icon.s-doing { color: #ea580c; }
|
||
.sub-icon.s-open { color: #93aec8; }
|
||
.sub-icon.s-accepted { color: #16a34a; }
|
||
.sub-sid {
|
||
font-family: "JetBrains Mono", monospace; font-size: 11.5px;
|
||
color: #52606d; font-weight: 700; min-width: 32px; flex-shrink: 0;
|
||
}
|
||
.sub-title { flex: 1; font-size: 13px; min-width: 120px; }
|
||
.subtask-item.s-done .sub-title,
|
||
.subtask-item.s-accepted .sub-title { text-decoration: line-through; color: #52606d; }
|
||
.sub-badges { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
|
||
.dep-section { display: flex; align-items: center; gap: 3px; }
|
||
.dep-label { font-size: 11px; color: #8aa3c4; margin-right: 2px; }
|
||
.dep-badge {
|
||
padding: 1px 6px; border-radius: 8px; font-size: 11px;
|
||
font-weight: 700; font-family: monospace; cursor: default;
|
||
}
|
||
.dep-done { background: #dcfce7; color: #166534; }
|
||
.dep-pending { background: #fee2e2; color: #b91c1c; }
|
||
|
||
/* ── 拒绝 Modal ── */
|
||
.modal-overlay {
|
||
position: fixed; inset: 0; background: rgba(0,0,0,.45);
|
||
display: flex; align-items: center; justify-content: center; z-index: 999;
|
||
}
|
||
.modal-box {
|
||
background: #fff; border-radius: 12px; padding: 28px 32px;
|
||
width: min(480px, 94vw); box-shadow: 0 20px 60px rgba(0,0,0,.2);
|
||
}
|
||
.modal-box h3 { margin: 0 0 4px; font-size: 17px; color: #1f2933; }
|
||
.modal-subtitle { font-size: 13px; color: #52606d; margin: 0 0 18px; }
|
||
.modal-label { font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 6px; display: block; }
|
||
.modal-label .req { color: #dc2626; margin-left: 2px; }
|
||
.modal-textarea {
|
||
width: 100%; border: 1.5px solid #d1d9e6; border-radius: 8px;
|
||
padding: 10px 12px; font-size: 14px; font-family: inherit; resize: vertical;
|
||
min-height: 88px; outline: none; transition: border-color .15s;
|
||
}
|
||
.modal-textarea:focus { border-color: #dc2626; }
|
||
.modal-textarea.error { border-color: #dc2626; background: #fff8f8; }
|
||
.modal-err { font-size: 12px; color: #dc2626; margin-top: 4px; display: none; }
|
||
.modal-err.show { display: block; }
|
||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
|
||
.modal-btn { padding: 8px 20px; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all .15s; }
|
||
.modal-btn-cancel { background: #f3f4f6; color: #374151; }
|
||
.modal-btn-cancel:hover { background: #e5e7eb; }
|
||
.modal-btn-reject { background: #dc2626; color: #fff; }
|
||
.modal-btn-reject:hover { background: #b91c1c; }
|
||
.modal-cmd-wrap { margin-top: 16px; padding: 12px 14px; background: #1e293b; border-radius: 8px; }
|
||
.modal-cmd-label { font-size: 12px; color: #94a3b8; margin-bottom: 8px; }
|
||
.modal-cmd-code { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 13px; color: #86efac; word-break: break-all; display: block; }
|
||
.modal-copy-btn { margin-top: 10px; padding: 5px 14px; border-radius: 6px; background: #334155; color: #e2e8f0; border: none; font-size: 12px; cursor: pointer; }
|
||
.modal-copy-btn:hover { background: #475569; }
|
||
/* ════ 家族深色主题覆盖层(2026-07-06 对齐 brain/docs 家族风格;后置覆盖上方浅色基础样式) ════ */
|
||
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--green:#3fb950;--amber:#d29922;--red:#f85149}
|
||
body{color:var(--fg);background:radial-gradient(1200px 600px at 80% -10%,rgba(88,166,255,.08),transparent 60%),var(--bg)}
|
||
header{background:linear-gradient(135deg,#101a2c 0%,#15294a 100%);border-bottom:1px solid var(--border)}
|
||
header .header-meta{color:var(--fg-soft)}
|
||
.stat-pill{background:rgba(88,166,255,.12);border:1px solid var(--border)}
|
||
.filter-bar{background:rgba(13,17,23,.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--border)}
|
||
.filter-label{color:var(--muted)}
|
||
.filter-chip,.filter-btn{background:var(--card);border-color:var(--border);color:var(--fg-soft)}
|
||
.filter-chip:hover,.filter-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||
.filter-chip.active,.filter-btn.active{background:var(--accent);border-color:var(--accent);color:#0d1117}
|
||
.filter-sep{background:var(--border)}
|
||
.section-title .s-count{background:rgba(255,255,255,.08)}
|
||
.st-open {background:rgba(88,166,255,.10);color:#79b8ff;border-left-color:var(--accent)}
|
||
.st-doing {background:rgba(210,153,34,.10);color:#e3b341;border-left-color:var(--amber)}
|
||
.st-done {background:rgba(63,185,80,.08);color:#7ee787;border-left-color:var(--green)}
|
||
.st-accepted{background:rgba(63,185,80,.12);color:#7ee787;border-left-color:var(--green)}
|
||
.todo-card{background:var(--card);border-color:var(--border)}
|
||
.todo-card.s-doing{border-left-color:var(--amber)}
|
||
.todo-card.s-done{border-left-color:var(--green)}
|
||
.todo-card.s-accepted{background:rgba(63,185,80,.06);border-left-color:var(--green)}
|
||
.item-id{color:var(--accent);background:#0b1020;border-color:var(--border)}
|
||
.item-title{color:var(--fg)}
|
||
.todo-card.s-accepted .item-title{color:var(--muted)}
|
||
.item-desc{color:var(--fg-soft)}
|
||
.item-desc code{background:#0b1020;border:1px solid var(--border);color:#c9d6e6}
|
||
.item-meta{color:var(--muted)}
|
||
.t-block{background:rgba(248,81,73,.15);color:#ff9492}
|
||
.t-high{background:rgba(210,153,34,.15);color:#e3b341}
|
||
.t-low{background:rgba(88,166,255,.12);color:#79b8ff}
|
||
.t-tag{background:var(--card-2);color:var(--fg-soft)}
|
||
.t-tag:hover{background:rgba(88,166,255,.15);color:var(--accent)}
|
||
.ver-badge{color:var(--green)}
|
||
.sub-progress-badge{background:rgba(88,166,255,.12);color:#79b8ff}
|
||
.s-open{background:rgba(88,166,255,.15);color:#79b8ff}
|
||
.s-doing{background:rgba(210,153,34,.18);color:#e3b341}
|
||
.s-done{background:rgba(63,185,80,.15);color:#7ee787}
|
||
.s-accepted{background:rgba(63,185,80,.18);color:#7ee787}
|
||
.empty-tip{color:var(--muted)}
|
||
.tier-1{background:rgba(163,113,247,.15);color:#d2a8ff}
|
||
.tier-2{background:rgba(57,197,187,.12);color:#76e3db}
|
||
.tier-3{background:var(--card-2);color:var(--fg-soft)}
|
||
.reject-btn{background:transparent;border-color:var(--red);color:#ff9492}
|
||
.reject-btn:hover{background:var(--red);color:#0d1117}
|
||
.reject-note{background:rgba(248,81,73,.08);border-color:rgba(248,81,73,.4);color:#ff9492}
|
||
.reject-date{color:rgba(248,81,73,.6)}
|
||
.gate-pending{background:rgba(210,153,34,.08);border-color:rgba(210,153,34,.4)}
|
||
.gate-granted{background:rgba(63,185,80,.08);border-color:rgba(63,185,80,.4)}
|
||
.gate-info{background:var(--card-2);border-color:var(--border)}
|
||
.gate-kind,.gate-note,.gate-ref{color:var(--fg-soft)}
|
||
.gate-date{color:var(--muted)}
|
||
.gate-note code,.gate-ref code{background:#0b1020;border:1px solid var(--border);color:#c9d6e6}
|
||
.subtask-block{background:var(--card-2);border-color:var(--border)}
|
||
.subtask-label{color:var(--muted)}
|
||
.subtask-progress-text{color:var(--fg-soft)}
|
||
.subtask-progress-bar{background:var(--border)}
|
||
.subtask-item{background:var(--card);border-color:var(--border)}
|
||
.subtask-item.s-done,.subtask-item.s-accepted{background:rgba(63,185,80,.06);border-color:rgba(63,185,80,.3)}
|
||
.subtask-item.s-doing{background:rgba(210,153,34,.06);border-color:rgba(210,153,34,.3)}
|
||
.sub-icon.s-open{color:var(--muted)}
|
||
.sub-sid{color:var(--muted)}
|
||
.subtask-item.s-done .sub-title,.subtask-item.s-accepted .sub-title{color:var(--muted)}
|
||
.dep-label{color:var(--muted)}
|
||
.dep-done{background:rgba(63,185,80,.15);color:#7ee787}
|
||
.dep-pending{background:rgba(248,81,73,.15);color:#ff9492}
|
||
.modal-overlay{background:rgba(0,0,0,.6)}
|
||
.modal-box{background:var(--card);border:1px solid var(--border)}
|
||
.modal-box h3{color:var(--fg)}
|
||
.modal-subtitle{color:var(--fg-soft)}
|
||
.modal-label{color:var(--fg-soft)}
|
||
.modal-textarea{background:#0b1020;border-color:var(--border);color:var(--fg)}
|
||
.modal-btn-cancel{background:var(--card-2);color:var(--fg-soft)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="wrap">
|
||
<h1>feature+windows — 项目 TODO</h1>
|
||
<div class="header-meta">生成于 2026-07-08 · 真相源 todo/todo.json</div>
|
||
<div class="stats">
|
||
<div class="stat-pill"><strong>33</strong>全部</div>
|
||
<div class="stat-pill"><strong>12</strong>待开始</div>
|
||
<div class="stat-pill"><strong>0</strong>开发中</div>
|
||
<div class="stat-pill"><strong>6</strong>待验收</div>
|
||
<div class="stat-pill"><strong>15</strong>已验收</div>
|
||
<div class="stat-pill gate-stat"><strong>1</strong>待确认</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="filter-bar" id="filterBar">
|
||
<div class="filter-group">
|
||
<span class="filter-label">重要度</span>
|
||
<button class="filter-btn active" data-filter-level="all">全部</button>
|
||
<button class="filter-btn" data-filter-level="high">高优</button>
|
||
<button class="filter-btn" data-filter-level="mid">重要</button>
|
||
<button class="filter-btn" data-filter-level="low">一般</button>
|
||
</div>
|
||
<div class="filter-sep"></div>
|
||
<div class="filter-group">
|
||
<span class="filter-label">状态</span>
|
||
<button class="filter-btn active" data-filter-status="all">全部</button>
|
||
<button class="filter-btn" data-filter-status="open">待开始</button>
|
||
<button class="filter-btn" data-filter-status="doing">开发中</button>
|
||
<button class="filter-btn" data-filter-status="done">待验收</button>
|
||
<button class="filter-btn" data-filter-status="accepted">已验收</button>
|
||
</div>
|
||
<div class="filter-sep"></div>
|
||
<div class="filter-group">
|
||
<span class="filter-label">改动等级</span>
|
||
<button class="filter-btn active" data-filter-tier="all">全部</button>
|
||
<button class="filter-btn" data-filter-tier="1">一级</button>
|
||
<button class="filter-btn" data-filter-tier="2">二级</button>
|
||
<button class="filter-btn" data-filter-tier="3">三级</button>
|
||
</div>
|
||
<div class="filter-sep"></div>
|
||
<div class="filter-group">
|
||
<span class="filter-label">平台 / 标签</span>
|
||
<button class="filter-chip" data-filter-tag="Android">Android</button><button class="filter-chip" data-filter-tag="CI/CD">CI/CD</button><button class="filter-chip" data-filter-tag="Web">Web</button><button class="filter-chip" data-filter-tag="Windows">Windows</button><button class="filter-chip" data-filter-tag="gRPC">gRPC</button><button class="filter-chip" data-filter-tag="iOS">iOS</button><button class="filter-chip" data-filter-tag="mac">mac</button><button class="filter-chip" data-filter-tag="network">network</button><button class="filter-chip" data-filter-tag="pangolin1">pangolin1</button><button class="filter-chip" data-filter-tag="reddit">reddit</button><button class="filter-chip" data-filter-tag="warp">warp</button><button class="filter-chip" data-filter-tag="前端">前端</button><button class="filter-chip" data-filter-tag="后端">后端</button><button class="filter-chip" data-filter-tag="支付">支付</button><button class="filter-chip" data-filter-tag="数据库">数据库</button><button class="filter-chip" data-filter-tag="文档">文档</button><button class="filter-chip" data-filter-tag="运维">运维</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
|
||
|
||
<div class="section-block" id="section-open">
|
||
<div class="section-title st-open" data-toggle="open">
|
||
📋 待开始 <span class="s-count">12</span>
|
||
<span class="s-arrow">▴ 收起</span>
|
||
</div>
|
||
<div class="section-list-wrap " id="list-wrap-open">
|
||
<ul class="todo-list" id="list-open">
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="1"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="2"
|
||
data-tags="mac">
|
||
<div class="card-header">
|
||
<span class="item-id">#1</span>
|
||
<span class="item-title">macOS 原生 KillSwitch 补齐 L0→L3</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">现状 L0(stub):VpnChannel.swift:69 setKillSwitch 直接 result(nil)、includeAllNetworks()→false。天花板 L3。改 NetworkExtension:includeAllNetworks=true + NEOnDemandRule 常开 + enforceRoutes,由 neagent 系统级强制、扛 app 崩溃。性价比最高的跨端缺口。依据见 docs/killswitch-design.md §4-6。</div>
|
||
<div class="gate-block gate-granted">
|
||
<div class="gate-head"><span class="gate-badge granted">✓ 已确认</span>
|
||
<span class="gate-kind">二级改动说明</span>
|
||
<span class="gate-date">2026-06-30</span></div>
|
||
<div class="gate-note">macOS L0→L3,改 2 个原生文件(不动 Dart/服务端)。【VpnChannel.swift】①加缓存 killSwitchEnabled(默认true);②setKillSwitch 落地:存标志+manager已存在则重新applyKillSwitchConfig+saveToPreferences即时生效;③loadOrCreateManager 按标志设 proto.includeAllNetworks/enforceRoutes=true/excludeLocalNetworks=true(放行LAN)+mgr.onDemandRules=[NEOnDemandRuleConnect()]+isOnDemandEnabled;④stop() gotcha:先关 isOnDemandEnabled+save 再 stopVPNTunnel,否则on-demand把断开拉回;⑤start() options 带 killSwitch 传扩展。【PacketTunnelProvider.swift】startTunnel 读 killSwitch option 缓存,includeAllNetworks() 回调返回该值与NE层对齐。无需新增entitlement。验证靠真机:连上后杀扩展/拔网验证流量被阻断(过L1→L2质变线)+on-demand开机/掉线自动拉起+手动断开不反弹。</div><div class="gate-ref">📄 详见 <code>docs/killswitch-design.html</code></div>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="mac">mac</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-22</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="2"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="2"
|
||
data-tags="Android">
|
||
<div class="card-header">
|
||
<span class="item-id">#2</span>
|
||
<span class="item-title">Android KillSwitch L1 + 引导系统 Always-on</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">现状 L0(stub):MainActivity.kt setKillSwitch 只打日志(TODO 11G)。目标:app 内做到 L1(config TUN strict_route,复刻 Windows 的 applyKillSwitchToConfig 语义) + 深链引导用户开系统设置「始终开启 VPN + 无 VPN 时阻止连接」拿 L3,UI 诚实标注「彻底防泄漏需在系统设置开启」。属 Android 客户端 11G。依据见 docs/killswitch-design.md §4-6。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="Android">Android</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-22</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="4"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="3"
|
||
data-tags="后端,运维">
|
||
<div class="card-header">
|
||
<span class="item-id">#4</span>
|
||
<span class="item-title">线上启用 TOTP 两步验证并验 2FA</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">pangolin1 的 /etc/pangolin/server.env 配 USER_TOTP_ENC_KEY(32字节或64位hex)并重启 pangolin-server,启用后验 A.1 客户端 2FA:绑定 Authenticator→重登 6 位码校验→备份码可用。当前该键未设置,/auth/login/totp 与 /me/totp/* 未挂载,2FA 无法测。属 A.1 手动验证清单第 4 项的前置。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="运维">运维</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="8"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="2"
|
||
data-tags="后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#8</span>
|
||
<span class="item-title">agent 掉线告警 + 静默吞错全量审计</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">TODO #7 的剩余两块:① Hub 检测到 agent gRPC 流断开时触发告警(alert 包),避免再像这次停 6 天没人知道;② 全仓审计 _ = err / 忽略错误的静默吞错点(今天已遇邮件/连接500/connect-push 三处),逐一改为记日志或上抛。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="12"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="1"
|
||
data-tags="前端,后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#12</span>
|
||
<span class="item-title">协议选择(REALITY/Hysteria2/自动)端到端</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">让用户选数据面隧道协议:auto(服务端自决)/reality(TCP443隐蔽)/hysteria2(UDP443弱网更快)。客户端 settings 加 ProtocolChoice 枚举+持久化+三段选择器、connect 时下发。关键:protocol 必须走 query(?protocol=)不走 body(与 split_cn 一致,nodes.go:183),旧 WIP 误移 body 会破坏分流。服务端 nodes.go connect 读 query + clientconfig BuildClientConfig 按 proto 渲染。(从本地 main todo 合并而来)</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="14"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="1"
|
||
data-tags="Android,iOS,后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#14</span>
|
||
<span class="item-title">移动端真·常连:Android Always-on + iOS/iPad On-Demand(无UI自连)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">系统在 app 未开时拉起隧道:Android Always-on VPN(系统设置开启+VpnService 支持 onStartCommand 无UI拉起+配置缓存)、iOS/iPad On-Demand(NEOnDemandRule + PacketTunnelProvider 用缓存 lastConfigJson 自连)。难点:paid 凭证 TTL 仅24h,长期常连需原生侧刷新配置/凭证。桌面自启+全平台自动连接已在另一轮做(plan crystalline-exploring-lerdorf)。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-29</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="31"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="2"
|
||
data-tags="Web,文档">
|
||
<div class="card-header">
|
||
<span class="item-id">#31</span>
|
||
<span class="item-title">官网文档页:撰写 快速开始/FAQ/协议与安全/隐私政策 4 页,接回 Docs 卡片链接</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">web/website Docs 区当前 4 张卡为占位信息卡(不可点)。需撰写真实文档页(可用 Astro content/MDX 或 docs 站),写好后把 Docs.astro 卡片改回 <a href> 并恢复「阅读」CTA、页脚 文档/FAQ/隐私政策 指向对应页。注意:隐私政策属法律文本,由创始人审定,Claude 仅出初稿;对「无日志」VPN,隐私政策是软性上线门槛。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="文档">文档</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-05</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="32"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="2"
|
||
data-tags="前端,后端,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#32</span>
|
||
<span class="item-title">控制面 fallback:CF 域名被 SNI 封时客户端退回直连节点 IP 的 https 控制口</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">#25 采用 CF Tunnel 前置控制面(api.yanmeiai.com)。已知弱点:该域名可被 GFW 单独 SNI 封锁,届时登录/拉配置不可用(已连隧道不受影响,数据面 REALITY:443 直连也不受影响)。加固:客户端 api 层保留一个直连某健康节点 IP 的 https 控制面 fallback——域名连不上/握手被 RST 时自动退回直连,保证仍能登录/刷新配置/换节点。服务端需在节点上也暴露一个可直连的 https 控制口(或复用现有端口+证书)。属 #25 的后续加固,不在 #25 本轮范围。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-06</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="34"
|
||
data-level="mid"
|
||
data-status="open"
|
||
data-tier="1"
|
||
data-tags="后端,支付,运维">
|
||
<div class="card-header">
|
||
<span class="item-id">#34</span>
|
||
<span class="item-title">独角数卡 + USDT 收款闭环</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">自建发卡网(独角数卡,独立小 VPS 隔离部署,不放 pangolin1)+ USDT(TRC20)收款,打通『下单→付款→自动发激活码→客户端/用户中心兑换→订阅生效』闭环。复用已就绪后端:POST /webhook/store/codes(HMAC+时间戳+nonce)、POST /v1/redeem(JWT)、批次生成/导出(server/internal/codes)。收款终点=单一自托管 USDT 钱包,变现经交易所出金到 US LLC(记账走 accounting)。真相源三文档:docs/payment-a-selfhosted-store-plan.html · payment-channels-overview.html · payment-reseller-fulfillment-design.html。tier-1:含新增 /internal/codes/mint(模型B 单源产码)+ 跨 server/发卡网/客户端/运维。待定:钱包(自托管热钱包 vs 交易所地址)、USDT 路子(自建 TronGrid watcher vs 加密网关)、起步用模型A(预充卡密,零胶水当天可卖)。开工前按 tier-1 走 plan→propose→approve。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="支付">支付</span> <span class="tag t-tag" data-tag="运维">运维</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-08</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="3"
|
||
data-level="low"
|
||
data-status="open"
|
||
data-tier="1"
|
||
data-tags="Windows">
|
||
<div class="card-header">
|
||
<span class="item-id">#3</span>
|
||
<span class="item-title">Windows KillSwitch L1→L2 (WFP 防火墙强制)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">现状 L1:desktop_vpn_bridge.dart strict_route + 子进程重载,进程被硬杀仍泄漏。目标 L2:app 装 WFP(Windows Filtering Platform) 过滤器阻断非隧道流量,做成系统服务持久化才能扛崩溃。工作量较大,backlog。依据见 docs/killswitch-design.md §4-6。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="Windows">Windows</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-22</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="19"
|
||
data-level="low"
|
||
data-status="open"
|
||
data-tier="3"
|
||
data-tags="前端,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#19</span>
|
||
<span class="item-title">连接页 resume 时整页闪 spinner(锁屏久重开)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">长锁屏重开 app 连接页整页 blank 成全屏转圈再回来。根因:MeNotifier.build 里 watch(authProvider),token 过期后 resume 首个请求 401 刷 token,authProvider 变,meProvider 重建进 AsyncLoading,meLoadingProvider(=me.isLoading)true,connect_page:38 整页转圈。功能无碍(VPN 没断),仅体验跳。改:meLoadingProvider 只在真首次(还没成功拿过账户数据)整页转圈;已有数据后的刷新保持连接页不动、后台静默更新,同时保留原意(别先闪免费再跳会员)。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-01</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-open"
|
||
data-id="24"
|
||
data-level="low"
|
||
data-status="open"
|
||
data-tier="1"
|
||
data-tags="前端,后端,数据库,mac,Windows,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#24</span>
|
||
<span class="item-title">联系我们渠道二级页:Telegram/LINE 多频道/群组,DB 配置</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-open">待开始</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">点渠道进二级页,按 kind 分组(频道/群组/Bot)列多条链接,内容 DB 配置。新表 contact_link + GET /v1/contact + 各端 UI。设计:docs/contact-telegram-channels-design.html。待用户统一交互后开发。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="数据库">数据库</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="section-block" id="section-doing">
|
||
<div class="section-title st-doing" data-toggle="doing">
|
||
🔨 开发中 <span class="s-count">0</span>
|
||
<span class="s-arrow">▴ 收起</span>
|
||
</div>
|
||
<div class="section-list-wrap " id="list-wrap-doing">
|
||
<ul class="todo-list" id="list-doing">
|
||
<p class="empty-tip">暂无条目</p>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="section-block" id="section-done">
|
||
<div class="section-title st-done" data-toggle="done">
|
||
🔍 待验收 <span class="s-count">6</span>
|
||
<span class="s-arrow">▴ 收起</span>
|
||
</div>
|
||
<div class="section-list-wrap " id="list-wrap-done">
|
||
<ul class="todo-list" id="list-done">
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="25"
|
||
data-level="high"
|
||
data-status="done"
|
||
data-tier="2"
|
||
data-tags="后端,前端,CI/CD">
|
||
<div class="card-header">
|
||
<span class="item-id">#25</span>
|
||
<span class="item-title">控制面上 TLS:域名 + 反代证书,客户端默认 https(F1)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
<button class="reject-btn" data-id="25" data-title="控制面上 TLS:域名 + 反代证书,客户端默认 https(F1)">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">审查 F1(P0 安全):当前客户端默认 http://103.119.13.48:8080 明文,密码/JWT 裸奔公网。需先有域名/HTTP 服务:域名解析到 pangolin1 → Caddy/nginx 自动 Let's Encrypt 反代 :8080 → 客户端 api_config.dart 默认改 https → Android 移除 usesCleartextTraffic → 8080 收回 loopback。见 docs/code-review-2026-07.html</div>
|
||
<div class="gate-block gate-granted">
|
||
<div class="gate-head"><span class="gate-badge granted">✓ 已确认</span>
|
||
<span class="gate-kind">二级改动说明</span>
|
||
<span class="gate-date">2026-07-06</span></div>
|
||
<div class="gate-note">控制面上 TLS 走 CF Tunnel:cloudflared 出站隧道前置 api.yanmeiai.com→127.0.0.1:8080(不占 :443,sing-box REALITY 不动)。6 个 Task:①建隧道+装 cloudflared(加法上线,与旧口并存)②客户端默认切 https+去 Android 明文③CI 守护禁明文④PANGOLIN_PUBLIC_URL 切 https(规则集下载基址)⑤待客户端迁移后收 8080 loopback+关防火墙+修健康检查⑥文档。fallback 拆到 #32。</div><div class="gate-ref">📄 详见 <code>docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md</code></div>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="CI/CD">CI/CD</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="26"
|
||
data-level="high"
|
||
data-status="done"
|
||
data-tier="3"
|
||
data-tags="后端,CI/CD,数据库">
|
||
<div class="card-header">
|
||
<span class="item-id">#26</span>
|
||
<span class="item-title">SQLite 每日备份 + 异地容灾(F2)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
<button class="reject-btn" data-id="26" data-title="SQLite 每日备份 + 异地容灾(F2)">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">审查 F2(P0 运维):pangolin1 生产 SQLite 无任何备份,单盘单机。最简:cron 每日 sqlite3 .backup + rclone 推 R2/S3 异地保留 30 天;更优 Litestream 持续复制。恢复流程写 runbook 并演练。见 docs/code-review-2026-07.html</div>
|
||
<div class="gate-block gate-info">
|
||
<div class="gate-head"><span class="gate-badge info">改动说明</span><span class="gate-kind">三级 · 可直接执行</span></div>
|
||
<div class="gate-note">定时 Gitea Actions 备份工作流(镜像 jiu backup.yml):scripts/ci/backup-db.sh(ssh pangolin1 → sqlite3 online .backup 取 WAL 一致快照 + 远端 integrity_check → gzip 流回 → 存 NAS /volume1/docker/backups/pangolin,保留30天)+ .gitea/workflows/backup.yml(每日02:00北京 cron + 手动,ubuntu-latest,复用 DEPLOY_SSH_KEY,Telegram 通知)。异地=家里NAS≠VPS机房。不动 pangolin1。</div>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="CI/CD">CI/CD</span> <span class="tag t-tag" data-tag="数据库">数据库</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="29"
|
||
data-level="high"
|
||
data-status="done"
|
||
data-tier="1"
|
||
data-tags="warp,network,reddit,pangolin1">
|
||
<div class="card-header">
|
||
<span class="item-id">#29</span>
|
||
<span class="item-title">给 pangolin1 挂 Cloudflare WARP 出站分流:信誉敏感站走 WARP 干净 IP</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
<button class="reject-btn" data-id="29" data-title="给 pangolin1 挂 Cloudflare WARP 出站分流:信誉敏感站走 WARP 干净 IP">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">背景与目标:
|
||
pangolin1 出口是机房脏 IP,访问 Reddit 会触发 403 / 验证码。用 Cloudflare WARP 作为 pangolin1 的一条出站(egress),按目的地分流——把信誉敏感流量(Reddit 等)从 WARP 出,出口 IP 变成 Cloudflare 干净 IP,验证码/403 基本消失;其余流量仍直出保满速。WARP 免费(住宅代理按 GB 收费)。参考本地知识库 /Users/wangjia/code/reddit-karma/docs/sni-throttle-warp.html。
|
||
|
||
实现步骤(pangolin1,sing-box 数据面):
|
||
1) 在 pangolin1 用 wgcf(或 warp-go)注册并生成 WARP 的 WireGuard 配置 profile。
|
||
2) sing-box 增加一个 wireguard 类型 outbound,tag=warp。
|
||
3) 增加路由规则:下方「走 WARP 的域名」匹配到 outbound=warp;其余走 direct。
|
||
4) 重启 sing-box 并验证。
|
||
|
||
走 WARP 的域名(用 domain_suffix,自动覆盖子域):
|
||
- reddit.com(含 www / old / new / sh / oauth.reddit.com API)
|
||
- redd.it(短链)
|
||
- i.redd.it、v.redd.it(图片/视频媒体)
|
||
- redditstatic.com(静态资源)
|
||
- redditmedia.com(媒体)
|
||
先只放 Reddit 全家桶;后续若遇其它易弹验证码的社交站,再按同样方式往这条规则里追加,规则集可扩展。
|
||
|
||
保持直出(不走 WARP):
|
||
- 其余所有流量,尤其大流量/大下载——避免 WARP 免费版吞吐瓶颈,保满速。
|
||
|
||
约束:
|
||
- 改机器前遵循 notify-before-system-changes:先只读勘察 pangolin1 现有 sing-box 配置与是否已装 wgcf → 给出改动清单请用户确认 → 再改 + 重启验证。
|
||
- WARP 配置/密钥不入 git;wgcf 生成的 profile 落 pangolin1 本地(/etc 下),敏感值走 Bitwarden 记录,不提交明文。
|
||
- WARP 本质仍是数据中心 IP,对最严反欺诈可能仍被标记,但对 Reddit 足够;WARP 不作翻墙用途。
|
||
|
||
验收标准:
|
||
- 经 pangolin 访问 reddit.com 不再出现 403 / 验证码;
|
||
- curl 确认 reddit 路由的出口 IP 属 Cloudflare 段,而其它流量出口仍为 pangolin1;
|
||
- 直出流量速度不受影响。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="warp">warp</span> <span class="tag t-tag" data-tag="network">network</span> <span class="tag t-tag" data-tag="reddit">reddit</span> <span class="tag t-tag" data-tag="pangolin1">pangolin1</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="30"
|
||
data-level="high"
|
||
data-status="done"
|
||
data-tier="1"
|
||
data-tags="CI/CD,后端,前端,mac,Windows,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#30</span>
|
||
<span class="item-title">CI/CD 全流程:tag 触发编译/发版/部署到 pangolin1(server+agent+client 产物+官网)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
<span class="tag sub-progress-badge">2/2 子任务</span>
|
||
<button class="reject-btn" data-id="30" data-title="CI/CD 全流程:tag 触发编译/发版/部署到 pangolin1(server+agent+client 产物+官网)">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">参考 jiu 的 .gitea/workflows(checks/deploy-server/deploy-site/deploy-client/build-windows/backup),套路:tag 触发(server-v*/site-v*/client-v*)→ checkout → provision(幂等)→ compile(scripts/ci/*.sh)→ go test/flutter test → 建 Gitea/Forgejo release 传产物 → ssh 部署目标机;国内镜像 goproxy.cn/flutter-io.cn。pangolin 差异:①部署机=pangolin1(103.119.13.48,非 jiu 的 ali/EC2),走 ssh 别名;②runner label=nas;③server 端要跑 migrate(SQLite,先备份再迁);④client 产出 apk(split-per-abi arm64)+ macOS 公证 dmg + Windows exe + 未来 iOS,这些产物正是官网下载链接的来源(接 web/website Download.astro 的 href)。现有仅 .gitea/workflows/ci.yml(校验,无部署)。含:官网 Astro 构建+部署、server/agent/migrate 编译部署、客户端多端出包与 release 挂载。</div>
|
||
<div class="gate-block gate-granted">
|
||
<div class="gate-head"><span class="gate-badge granted">✓ 已确认</span>
|
||
<span class="gate-kind">一级方案规划</span>
|
||
<span class="gate-date">2026-07-05</span></div>
|
||
<div class="gate-note">三期 11 任务(基座+官网+服务端 / Android / macOS+Windows);镜像 jiu scripts/ci+workflows,runner nas/mac/windows,tag 触发;服务端部署固化 F3/F4 备份/迁移/回滚;下载链接接 release;密钥已建对齐 jiu。设计 docs/cicd-design.html,计划 docs/cicd-plan.html。</div><div class="gate-ref">📄 详见 <code>docs/superpowers/plans/2026-07-05-cicd.md</code></div>
|
||
</div>
|
||
<div class="subtask-block">
|
||
<div class="subtask-header">
|
||
<span class="subtask-label">子任务</span>
|
||
<span class="subtask-progress-text">2 / 2 完成</span>
|
||
<div class="subtask-progress-bar"><div class="subtask-progress-fill" style="width:100%"></div></div>
|
||
</div>
|
||
<ul class="subtask-list"><li class="subtask-item s-done" data-sid="30A">
|
||
<span class="sub-icon s-done">✓</span>
|
||
<span class="sub-sid">30A</span>
|
||
<span class="sub-title">官网入口接真实 URL:部署后恢复 Header 登录(→usercenter)+ 下载按钮 href(→客户端产物)</span>
|
||
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span></div>
|
||
</li>
|
||
<li class="subtask-item s-done" data-sid="30B">
|
||
<span class="sub-icon s-done">✓</span>
|
||
<span class="sub-sid">30B</span>
|
||
<span class="sub-title">官网部署:静态站发布到 pangolin.yanmeiai.com + CF DNS 记录指向服务器</span>
|
||
<div class="sub-badges"><span class="tag tier-2">二级</span><span class="tag status-badge s-done">待验收</span></div>
|
||
</li></ul>
|
||
</div>
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="CI/CD">CI/CD</span> <span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-05</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="33"
|
||
data-level="mid"
|
||
data-status="done"
|
||
data-tier="1"
|
||
data-tags="前端,Web,mac,Windows,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#33</span>
|
||
<span class="item-title">三端多语言(6语)+ 默认英文</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
<button class="reject-btn" data-id="33" data-title="三端多语言(6语)+ 默认英文">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">客户端/官网/用户中心新增 日/韩/俄/西(共6语 zh/en/ja/ko/ru/es),默认语种改英文。官网根路由/=英文、中文挪/zh/、新增/ja//ko//ru//es/;用户中心补入 site-v* CI 部署。B1:硬编码UI文案6语化+修auth错误消息永远显中文bug;B2:语言选择持久化(shared_preferences)。红线扫描0违规。已发 client-v1.0.60 + site-v0.0.6;B1/B2 待 client-v1.0.61。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-07</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-done"
|
||
data-id="18"
|
||
data-level="low"
|
||
data-status="done"
|
||
data-tier="3"
|
||
data-tags="前端,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#18</span>
|
||
<span class="item-title">节点判死无备用时自动重连当前节点(而非断开报错)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-done">待验收</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
<button class="reject-btn" data-id="18" data-title="节点判死无备用时自动重连当前节点(而非断开报错)">拒绝验收</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">长挂机后数据面睡死→看门狗 path A 正确判上游死,但智能模式仅1节点/找不到备用时只会 _disconnect+报节点异常,逼用户手动重连(实测挂机2h回来即遇)。改 _onNodeUnhealthy:无备用节点时自动重连当前节点重建通路,加有限次数保护避免节点真挂时死循环。connection_provider.dart 一处。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-01</span>
|
||
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="section-block" id="section-accepted">
|
||
<div class="section-title st-accepted" data-toggle="accepted">
|
||
✅ 已验收 <span class="s-count">15</span>
|
||
<span class="s-arrow">▾ 展开</span>
|
||
</div>
|
||
<div class="section-list-wrap collapsed" id="list-wrap-accepted">
|
||
<ul class="todo-list" id="list-accepted">
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="28"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="3"
|
||
data-tags="后端,前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#28</span>
|
||
<span class="item-title">disconnect 撤销每设备凭证而非账户级(F4)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">审查 F4:ConnectNode 发每设备 devDp,DisconnectNode 却吊销账户级 ent.DpUUID 且无 device_id 入参——断开从未真正吊销,凭证活到 TTL。修:disconnect 收 device_id,吊销该设备 dp_uuid(账户级作遗留兜底);客户端带上 device_id。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
<span class="meta-date">✅ 验收 2026-07-05</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="27"
|
||
data-level="high"
|
||
data-status="accepted"
|
||
data-tier="1"
|
||
data-tags="后端,数据库">
|
||
<div class="card-header">
|
||
<span class="item-id">#27</span>
|
||
<span class="item-title">同机换账号设备注册 403 死结:devices 唯一键改 (user_id,uuid)(F3)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">审查 F3:device_id 跨账号复用 + devices.uuid 全局 UNIQUE → 第二账号注册永远 403 → 连接被 DEVICE_NOT_REGISTERED 卡死。落地 migration16 注释里推迟的 UNIQUE(user_id,uuid) 表重建,注册查找改按 (user,uuid)。见 docs/code-review-2026-07.html</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="数据库">数据库</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
<span class="meta-date">✅ 验收 2026-07-05</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="22"
|
||
data-level="high"
|
||
data-status="accepted"
|
||
data-tier="2"
|
||
data-tags="后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#22</span>
|
||
<span class="item-title">用量分钟数超计(多设备并发按 dp_uuid 累加)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">agent 每 dp_uuid 每窗口计 SessionMinutes=1,控制面 ReportUsage 逐条累加进 usage_daily.minutes_used → N 台并发=N×墙上时钟(chenxin 2903/天)。免费卡控下会错误判耗尽。改:账户维度按 user 分组去重(minutes=max,bytes=sum),每设备维度不变。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
<span class="meta-date">✅ 验收 2026-07-02 · <span class="ver-badge">v1.0.47</span></span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="21"
|
||
data-level="high"
|
||
data-status="accepted"
|
||
data-tier="1"
|
||
data-tags="后端,前端,数据库,mac,Windows,Android,iOS">
|
||
<div class="card-header">
|
||
<span class="item-id">#21</span>
|
||
<span class="item-title">免费版10分钟卡控+累加式看广告加时</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">连接期倒计时+到点切断;耗尽按钮灰化;点击弹广告(占位流程)看完+10分钟(累加,每日封顶120);桌面硬10分钟不可延+升级提示。额度全账户共享。服务端ConnectNode按remaining卡控+TTL硬切;ad_bonus_minutes累加模型;DevVerifier占位。</div>
|
||
<div class="gate-block gate-granted">
|
||
<div class="gate-head"><span class="gate-badge granted">✓ 已确认</span>
|
||
<span class="gate-kind">一级方案规划</span>
|
||
<span class="gate-date">2026-07-01</span></div>
|
||
<div class="gate-note">已进 plan 模式出方案并经用户 ExitPlanMode 批准</div><div class="gate-ref">📄 详见 <code>/Users/wangjia/.claude/plans/crystalline-exploring-lerdorf.md</code></div>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="数据库">数据库</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-01</span>
|
||
<span class="meta-date">✅ 验收 2026-07-02 · <span class="ver-badge">v1.0.44</span></span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="23"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="3"
|
||
data-tags="mac,前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#23</span>
|
||
<span class="item-title">macOS 最小化托盘后点 Dock 图标无法唤回窗口</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">windowManager.hide() 走 orderOut: 后窗口不可见,点 Dock 图标只发 applicationShouldHandleReopen(hasVisibleWindows=false),AppDelegate 未接管 → 点了没反应。修:AppDelegate 实现 applicationShouldHandleReopen 唤回 mainFlutterWindow。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-02</span>
|
||
<span class="meta-date">✅ 验收 2026-07-02 · <span class="ver-badge">v1.0.48</span></span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="20"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="3"
|
||
data-tags="Android,iOS,前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#20</span>
|
||
<span class="item-title">看门狗后台唤醒/切后台误判节点死→假断连(已修)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">后台/挂起期 urltest 被限流停更但 speed 在流→path A 误判上游死断连。修:statsLive 闸 + 切后台(paused/hidden)停看门狗、resumed 重启并重置基准。另修连接时长墙上时钟(切后台不漏计)。已合 main 07861ea/37c1724/58211d2,含回归测试。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-07-01</span>
|
||
<span class="meta-date">✅ 验收 2026-07-01</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="16"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="1"
|
||
data-tags="后端,前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#16</span>
|
||
<span class="item-title">启用设备数量限制 + 超限 UX(踢旧/选择移除,非硬拒登)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">现状:plans.max_devices(free1/pro5/team10)+ devices/service.go:216 强制逻辑 + DEVICE_LIMIT_EXCEEDED 错误码 + 账户API DevicesMax 都在,但 main.go:534 登录注册传 MaxDevices:0(不启用)。原因:free 用户重装会换 device UUID,登录硬卡会把人锁外面。需:① 登录/注册从套餐读 max_devices 传入 RegisterIfAbsent;② 超限不直接拒登,改为'踢最旧设备/让用户选哪台下线'的 UX(客户端弹窗 + 设备管理页操作);③ 处理 free 重装 churn(同机复用而非每次新增)。tier-1:涉及登录接口 + 跨后端/前端 + 策略设计。</div>
|
||
<div class="gate-block gate-pending">
|
||
<div class="gate-head"><span class="gate-badge pending">⏸ 待确认</span>
|
||
<span class="gate-kind">一级方案规划 · 确认后方可继续开发</span></div>
|
||
<div class="gate-note">登录卡超限(非硬拒登:token照发+返回device_limit信号,客户端挡在移除设备页)+连接backstop兜底;选择移除+一键踢最旧;服务端按last_seen自动清理久不活跃(staleWindow=30d,活跃count排除+登录best-effort删僵尸)。复用现成 DeleteDevice/ResolvePlan/CheckDeviceQuota/ConnectNode。后端:auth service/handler(recordLogin回传+LoginOutcome.DeviceLimit)、devices(DeviceLimitStatus+活跃count+prune)、main wiring、nodes ConnectNode backstop。前端:auth_api解析、auth_provider路由、DeviceLimitScreen、l10n。无DB schema变更。</div><div class="gate-ref">📄 详见 <code>~/.claude/plans/crystalline-exploring-lerdorf.md</code></div>
|
||
<button class="approve-btn" data-id="16">确认执行 ✓</button>
|
||
<div class="approve-cmd" id="approve-cmd-16" style="display:none">
|
||
<div class="approve-cmd-label">在终端运行:</div>
|
||
<code class="approve-cmd-code">/todo approve 16</code>
|
||
<button class="approve-copy-btn" data-cmd="/todo approve 16">复制命令</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-30</span>
|
||
<span class="meta-date">✅ 验收 2026-07-01</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="11"
|
||
data-level="low"
|
||
data-status="accepted"
|
||
data-tier="3"
|
||
data-tags="前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#11</span>
|
||
<span class="item-title">清理旧命名 tablet golden 孤儿文件</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">client/test/golden/goldens/ 下未被引用的 tablet_connect.png/tablet_connect_dark.png/tablet_servers.png/tablet_stats.png/tablet_account.png(旧命名,已被 _light_/_dark_zh 矩阵取代),git rm 清掉。(从本地 main todo 合并而来)</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="7"
|
||
data-level="high"
|
||
data-status="accepted"
|
||
data-tier="1"
|
||
data-tags="后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#7</span>
|
||
<span class="item-title">节点健康绑定 agent 在线 + connect 校验凭证下发 + 掉线告警</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">本次 google 打不开暴露的架构漏洞:① 节点 status 是静态 DB 列(种子 up),只有 SCHED 健康循环会改、而单节点没开 → Hub 已知 agent 离线却不反映到节点状态/列表,客户端永远显示健康。应把 Hub 的 agent 在线状态接进 ListNodes/节点 status。② ConnectNode 把 hub.Push 错误吞了(_ = pushErr)且不校验 agent 在线 → 凭证没下发也返回 200,客户端误以为连上。应在 agent 离线/push 失败时降级或明确报错,并把推送结果纳入响应。③ agent 掉线无告警(本次停 6 天没发现),应在 Hub 检测到 agent stream 断开时告警。④ 通病:错误被 _ = 静默吞掉(今天邮件/连接500/此处共三处),需排查同类静默吞错。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="10"
|
||
data-level="low"
|
||
data-status="accepted"
|
||
data-tier="2"
|
||
data-tags="前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#10</span>
|
||
<span class="item-title">统计页设备下拉接真实设备数据(依赖#上条)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-low">一般 / 优化</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">统计重设计的设备下拉,先只挂「全部」上线;每设备归因打通后,把下拉填上「全部 + 近30天用过的设备名」,选设备走 /v1/usage/devices 过滤。依赖每设备归因 TODO。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="15"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="2"
|
||
data-tags="iOS,前端">
|
||
<div class="card-header">
|
||
<span class="item-id">#15</span>
|
||
<span class="item-title">iOS/iPad 实时统计+urltest 延迟与 macOS 对齐</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">移植 macOS 已验证的统计架构:扩展注入 clash_api + StatsCollector(clash 取 urltest),app StatsClient 改 sendProviderMessage 轮询;前置生成 iOS Libbox.xcframework。Dart 侧已对齐(共用 VpnNativeBridge)。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-30</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="9"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="2"
|
||
data-tags="前端,后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#9</span>
|
||
<span class="item-title">打通每设备归因(设备注册 + per-device dp_uuid)</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">现状:多设备共用账户级 dp_uuid,统计无法区分设备;usage_device_daily 永远空,统计页设备下拉只有「全部」。基础设施已具备(devices.dp_uuid / EnsureDeviceDpUUID / usage_device_daily / UserDeviceByDpUUID)但未激活——客户端没 POST /v1/me/devices 注册本机,连接时 EnsureDeviceDpUUID 查不到设备行→回退账户 uuid。需:① 客户端启动/登录时注册本机设备(稳定 uuid + name + platform);② 连接时带该 device_id;③ 或让 EnsureDeviceDpUUID 缺行时自动建设备行。打通后每设备统计 + 统计页设备下拉才有真数据。是统计重设计「设备下拉」的前置依赖。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="13"
|
||
data-level="high"
|
||
data-status="accepted"
|
||
data-tier="1"
|
||
data-tags="后端,前端,gRPC">
|
||
<div class="card-header">
|
||
<span class="item-id">#13</span>
|
||
<span class="item-title">数据面故障韧性:节点健康上报+客户端连通看门狗</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-block">高优 · 紧急</span>
|
||
<span class="tag tier-1">一级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">agent 心跳带 sing-box 健康(clash_api /version)→控制面 effectiveNodeStatus 标 down(列表置灰+connect拦截);客户端连上后 HTTP 204 看门狗,连续失败:智能选择自动切节点/手动只提示断开。plan: crystalline-exploring-lerdorf.md</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="gRPC">gRPC</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-29</span>
|
||
<span class="meta-date">✅ 验收 2026-06-30</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="6"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="2"
|
||
data-tags="前端,后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#6</span>
|
||
<span class="item-title">节点列表:正式名(地区·国家) + 国旗图标</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-2">二级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">客户端节点列表项展示改造:① 节点名改正式格式「地区 · 国家」(如 香港 · 中国),去掉测试名(当前 prod 节点 name_zh=单节点·测试);② 节点图标用国旗(按 region/国家码渲染 emoji 或 SVG 旗)。涉及:服务端 nodes 行的 name/region/国家码数据(可能需补国家码字段),客户端节点卡片渲染国旗+名格式。HK 节点国旗/国家标签需产品确认(🇭🇰香港 vs 🇨🇳中国)。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-28</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
|
||
<li class="todo-card s-accepted"
|
||
data-id="5"
|
||
data-level="mid"
|
||
data-status="accepted"
|
||
data-tier="3"
|
||
data-tags="后端">
|
||
<div class="card-header">
|
||
<span class="item-id">#5</span>
|
||
<span class="item-title">SendCode 发信失败记日志 + 测试</span>
|
||
<div class="card-badges">
|
||
<span class="tag status-badge s-accepted">已验收</span>
|
||
<span class="tag t-high">重要</span>
|
||
<span class="tag tier-3">三级</span>
|
||
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="item-desc">auth/service.go 异步发信把错误静默吞了(:157 SendCode 的 _=s.mailer.SendCode(...),:133 SendAlreadyRegistered 同样),导致 SMTP 故障日志无痕、本次 587 被封难排查。改为发信失败时记日志(slog.Error,脱敏:只记 send failed:<err>,绝不记验证码,邮箱可打码,守 no-secret-in-logs)。并加测试:注入会失败的 mock Mailer,断言发送失败时确实记了一条错误日志(可用 LogMailer 思路/捕获 slog handler 验证)。</div>
|
||
|
||
|
||
<div class="card-footer">
|
||
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
|
||
<div class="item-meta">
|
||
<span class="tag owner-agent">🤖 agent</span>
|
||
<span class="meta-date">🕐 2026-06-28</span>
|
||
<span class="meta-date">✅ 验收 2026-06-28</span>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- 拒绝验收 Modal -->
|
||
<div id="reject-modal" class="modal-overlay" style="display:none">
|
||
<div class="modal-box">
|
||
<h3>拒绝验收</h3>
|
||
<p class="modal-subtitle" id="reject-modal-subtitle"></p>
|
||
<label class="modal-label">拒绝原因<span class="req">*</span></label>
|
||
<textarea class="modal-textarea" id="reject-reason-input" rows="3" placeholder="请说明具体问题,Claude 将以此为依据修复…"></textarea>
|
||
<div class="modal-err" id="reject-reason-err">请填写拒绝原因</div>
|
||
<div id="reject-cmd-wrap" class="modal-cmd-wrap" style="display:none">
|
||
<div class="modal-cmd-label">在终端运行以下命令:</div>
|
||
<code class="modal-cmd-code" id="reject-cmd-text"></code>
|
||
<button class="modal-copy-btn" id="reject-copy-btn">复制命令</button>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="modal-btn modal-btn-cancel" id="reject-cancel-btn">取消</button>
|
||
<button class="modal-btn modal-btn-reject" id="reject-confirm-btn">确认拒绝</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
let curLevel = 'all';
|
||
let curStatus = 'all';
|
||
let curTier = 'all';
|
||
let curTags = new Set();
|
||
|
||
// 通过本地服务(http)打开时,按钮直接写库;用 file:// 直接打开则回退为「复制命令」
|
||
const SERVED = location.protocol === 'http:' || location.protocol === 'https:';
|
||
function postAction(url, payload) {
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: payload ? { 'Content-Type': 'application/json' } : undefined,
|
||
body: payload ? JSON.stringify(payload) : undefined,
|
||
}).then(r => r.json());
|
||
}
|
||
|
||
function applyFilter() {
|
||
document.querySelectorAll('.todo-card').forEach(card => {
|
||
const lvl = card.dataset.level;
|
||
const status = card.dataset.status;
|
||
const tier = card.dataset.tier;
|
||
const tags = card.dataset.tags ? card.dataset.tags.split(',') : [];
|
||
|
||
let show = true;
|
||
if (curLevel !== 'all' && lvl !== curLevel) show = false;
|
||
if (curStatus !== 'all' && status !== curStatus) show = false;
|
||
if (curTier !== 'all' && String(tier) !== curTier) show = false;
|
||
if (curTags.size > 0 && ![...curTags].some(t => tags.includes(t))) show = false;
|
||
|
||
card.classList.toggle('hidden', !show);
|
||
});
|
||
|
||
if (curStatus !== 'all') {
|
||
const wrap = document.getElementById('list-wrap-' + curStatus);
|
||
if (wrap) {
|
||
wrap.classList.remove('collapsed');
|
||
const title = document.querySelector('[data-toggle="' + curStatus + '"] .s-arrow');
|
||
if (title) title.textContent = '▴ 收起';
|
||
}
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('[data-filter-level]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
curLevel = btn.dataset.filterLevel;
|
||
document.querySelectorAll('[data-filter-level]').forEach(b => b.classList.toggle('active', b === btn));
|
||
applyFilter();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-filter-tier]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
curTier = btn.dataset.filterTier;
|
||
document.querySelectorAll('[data-filter-tier]').forEach(b => b.classList.toggle('active', b === btn));
|
||
applyFilter();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-filter-status]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
curStatus = btn.dataset.filterStatus;
|
||
document.querySelectorAll('[data-filter-status]').forEach(b => b.classList.toggle('active', b === btn));
|
||
applyFilter();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-filter-tag]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const t = btn.dataset.filterTag;
|
||
if (curTags.has(t)) { curTags.delete(t); btn.classList.remove('active'); }
|
||
else { curTags.add(t); btn.classList.add('active'); }
|
||
applyFilter();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.t-tag').forEach(tag => {
|
||
tag.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const t = tag.dataset.tag;
|
||
const chip = document.querySelector('[data-filter-tag="'+t+'"]');
|
||
if (chip) chip.click();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.section-title[data-toggle]').forEach(title => {
|
||
title.addEventListener('click', () => {
|
||
const st = title.dataset.toggle;
|
||
const wrap = document.getElementById('list-wrap-' + st);
|
||
const arrow = title.querySelector('.s-arrow');
|
||
if (!wrap) return;
|
||
const collapsed = wrap.classList.toggle('collapsed');
|
||
if (arrow) arrow.textContent = collapsed ? '▾ 展开' : '▴ 收起';
|
||
});
|
||
});
|
||
|
||
// ── 拒绝验收 Modal ──
|
||
const modal = document.getElementById('reject-modal');
|
||
const reasonInput = document.getElementById('reject-reason-input');
|
||
const reasonErr = document.getElementById('reject-reason-err');
|
||
const cmdWrap = document.getElementById('reject-cmd-wrap');
|
||
const cmdText = document.getElementById('reject-cmd-text');
|
||
const copyBtn = document.getElementById('reject-copy-btn');
|
||
let rejectId = null;
|
||
|
||
document.querySelectorAll('.reject-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
rejectId = btn.dataset.id;
|
||
document.getElementById('reject-modal-subtitle').textContent = '#' + rejectId + ' ' + btn.dataset.title;
|
||
reasonInput.value = '';
|
||
reasonInput.classList.remove('error');
|
||
reasonErr.classList.remove('show');
|
||
cmdWrap.style.display = 'none';
|
||
document.getElementById('reject-confirm-btn').style.display = '';
|
||
copyBtn.textContent = '复制命令';
|
||
modal.style.display = 'flex';
|
||
setTimeout(() => reasonInput.focus(), 80);
|
||
});
|
||
});
|
||
|
||
document.getElementById('reject-cancel-btn').addEventListener('click', () => {
|
||
modal.style.display = 'none';
|
||
});
|
||
|
||
modal.addEventListener('click', e => {
|
||
if (e.target === modal) modal.style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('reject-confirm-btn').addEventListener('click', () => {
|
||
const reason = reasonInput.value.trim();
|
||
if (!reason) {
|
||
reasonInput.classList.add('error');
|
||
reasonErr.classList.add('show');
|
||
reasonInput.focus();
|
||
return;
|
||
}
|
||
if (SERVED) {
|
||
postAction('/api/reject/' + rejectId, { reason })
|
||
.then(d => { if (d.ok) { location.reload(); } else { alert('拒绝失败:' + (d.error || '')); } })
|
||
.catch(() => alert('请求失败,确认本地服务是否在运行'));
|
||
return;
|
||
}
|
||
const cmd = '/todo reject ' + rejectId + ' ' + JSON.stringify(reason);
|
||
cmdText.textContent = cmd;
|
||
cmdWrap.style.display = 'block';
|
||
document.getElementById('reject-confirm-btn').style.display = 'none';
|
||
});
|
||
|
||
reasonInput.addEventListener('input', () => {
|
||
if (reasonInput.value.trim()) {
|
||
reasonInput.classList.remove('error');
|
||
reasonErr.classList.remove('show');
|
||
}
|
||
});
|
||
|
||
copyBtn.addEventListener('click', () => {
|
||
navigator.clipboard.writeText(cmdText.textContent).then(() => {
|
||
copyBtn.textContent = '已复制 ✓';
|
||
});
|
||
});
|
||
|
||
// ── 确认闸:服务模式直接写库;file:// 模式回退为展开命令 ──
|
||
document.querySelectorAll('.approve-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const id = btn.dataset.id;
|
||
if (SERVED) {
|
||
btn.disabled = true; btn.textContent = '确认中…';
|
||
postAction('/api/approve/' + id)
|
||
.then(d => {
|
||
if (d.ok) { location.reload(); }
|
||
else { btn.disabled = false; btn.textContent = '确认执行 ✓'; alert('确认失败:' + (d.error || '')); }
|
||
})
|
||
.catch(() => { btn.disabled = false; btn.textContent = '确认执行 ✓'; alert('请求失败,确认本地服务是否在运行'); });
|
||
} else {
|
||
const box = document.getElementById('approve-cmd-' + id);
|
||
if (box) box.style.display = box.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll('.approve-copy-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
navigator.clipboard.writeText(btn.dataset.cmd).then(() => { btn.textContent = '已复制 ✓'; });
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |