mirror of
https://github.com/sinanyuntu/trade-message-center.git
synced 2026-09-17 13:22:11 +08:00
2794 lines
92 KiB
HTML
2794 lines
92 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<meta name="theme-color" content="#09121b" />
|
||
<meta name="description" content="OneTalk 消息收发同步机制重构 — 架构 PRD 展示页" />
|
||
<title>OneTalk · 消息事实链路</title>
|
||
<style>
|
||
:root {
|
||
color-scheme: dark;
|
||
--bg: #08111a;
|
||
--bg-soft: #0d1924;
|
||
--panel: rgba(17, 32, 45, 0.82);
|
||
--panel-strong: #122535;
|
||
--panel-light: #172d3e;
|
||
--ink: #eef5f0;
|
||
--muted: #91a7b7;
|
||
--dim: #638092;
|
||
--line: rgba(158, 202, 216, 0.16);
|
||
--line-strong: rgba(158, 202, 216, 0.3);
|
||
--cyan: #6be4d4;
|
||
--cyan-soft: rgba(107, 228, 212, 0.12);
|
||
--lime: #c5f36f;
|
||
--orange: #ffad65;
|
||
--orange-soft: rgba(255, 173, 101, 0.12);
|
||
--blue: #9ab9ff;
|
||
--red: #ff7e7e;
|
||
--shadow: 0 28px 80px rgba(0, 0, 0, 0.28);
|
||
--radius: 22px;
|
||
--radius-sm: 14px;
|
||
--mono: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||
--sans: "Avenir Next", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
--serif: "Iowan Old Style", "Noto Serif SC", "Songti SC", serif;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
html {
|
||
scroll-behavior: smooth;
|
||
}
|
||
body {
|
||
margin: 0;
|
||
min-width: 320px;
|
||
color: var(--ink);
|
||
background:
|
||
radial-gradient(circle at 82% 8%, rgba(74, 188, 191, 0.16), transparent 25rem),
|
||
radial-gradient(circle at 10% 28%, rgba(39, 91, 141, 0.18), transparent 28rem),
|
||
linear-gradient(135deg, #08111a 0%, #0a151f 52%, #09121b 100%);
|
||
font-family: var(--sans);
|
||
line-height: 1.6;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
body::before {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: -1;
|
||
pointer-events: none;
|
||
opacity: 0.28;
|
||
background-image:
|
||
linear-gradient(rgba(165, 212, 220, 0.045) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(165, 212, 220, 0.045) 1px, transparent 1px);
|
||
background-size: 48px 48px;
|
||
mask-image: linear-gradient(to bottom, black, transparent 86%);
|
||
}
|
||
|
||
body::after {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 20;
|
||
pointer-events: none;
|
||
opacity: 0.035;
|
||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.7'/%3E%3C/svg%3E");
|
||
}
|
||
|
||
a {
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
button {
|
||
font: inherit;
|
||
}
|
||
::selection {
|
||
color: #071016;
|
||
background: var(--cyan);
|
||
}
|
||
|
||
.icon {
|
||
width: 18px;
|
||
height: 18px;
|
||
display: inline-block;
|
||
flex: 0 0 auto;
|
||
fill: none;
|
||
stroke: currentColor;
|
||
stroke-width: 1.8;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
vertical-align: -4px;
|
||
}
|
||
|
||
.icon--sm {
|
||
width: 15px;
|
||
height: 15px;
|
||
}
|
||
.icon--lg {
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
.icon-sprite {
|
||
position: absolute;
|
||
width: 0;
|
||
height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.topbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 15;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
min-height: 68px;
|
||
padding: 0 34px;
|
||
border-bottom: 1px solid rgba(164, 205, 216, 0.14);
|
||
background: rgba(8, 17, 26, 0.74);
|
||
backdrop-filter: blur(20px);
|
||
}
|
||
|
||
.brand {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.brand-mark {
|
||
display: grid;
|
||
width: 32px;
|
||
height: 32px;
|
||
place-items: center;
|
||
border: 1px solid rgba(107, 228, 212, 0.55);
|
||
border-radius: 10px;
|
||
color: var(--cyan);
|
||
background: linear-gradient(145deg, rgba(107, 228, 212, 0.18), rgba(107, 228, 212, 0.03));
|
||
box-shadow: inset 0 0 18px rgba(107, 228, 212, 0.08);
|
||
}
|
||
|
||
.brand-text {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
}
|
||
.topbar-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.topbar-meta .mono {
|
||
color: var(--dim);
|
||
}
|
||
|
||
.status-dot {
|
||
display: inline-block;
|
||
width: 7px;
|
||
height: 7px;
|
||
margin-right: 7px;
|
||
border-radius: 50%;
|
||
background: var(--lime);
|
||
box-shadow:
|
||
0 0 0 4px rgba(197, 243, 111, 0.09),
|
||
0 0 12px rgba(197, 243, 111, 0.55);
|
||
}
|
||
|
||
.layout {
|
||
display: grid;
|
||
grid-template-columns: 220px minmax(0, 1fr);
|
||
gap: 42px;
|
||
width: min(1420px, calc(100% - 68px));
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.sidebar {
|
||
position: sticky;
|
||
top: 104px;
|
||
align-self: start;
|
||
padding: 36px 0 28px;
|
||
}
|
||
|
||
.side-kicker {
|
||
margin: 0 0 18px;
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.side-nav {
|
||
display: grid;
|
||
gap: 3px;
|
||
}
|
||
.side-nav a {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 9px 12px;
|
||
border-radius: 10px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
transition:
|
||
color 0.2s ease,
|
||
background 0.2s ease,
|
||
transform 0.2s ease;
|
||
}
|
||
.side-nav a::before {
|
||
content: "";
|
||
width: 3px;
|
||
height: 0;
|
||
border-radius: 3px;
|
||
background: var(--cyan);
|
||
transition: height 0.2s ease;
|
||
}
|
||
.side-nav a:hover {
|
||
color: var(--ink);
|
||
background: rgba(107, 228, 212, 0.06);
|
||
transform: translateX(2px);
|
||
}
|
||
.side-nav a.is-active {
|
||
color: var(--cyan);
|
||
background: rgba(107, 228, 212, 0.1);
|
||
}
|
||
.side-nav a.is-active::before {
|
||
height: 16px;
|
||
}
|
||
|
||
.side-note {
|
||
margin-top: 42px;
|
||
padding: 16px;
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius-sm);
|
||
background: rgba(18, 37, 53, 0.46);
|
||
}
|
||
.side-note strong {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: var(--lime);
|
||
font-size: 12px;
|
||
}
|
||
.side-note p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
main {
|
||
min-width: 0;
|
||
padding: 70px 0 100px;
|
||
}
|
||
|
||
.hero {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.13fr) minmax(300px, 0.87fr);
|
||
gap: 28px;
|
||
min-height: 510px;
|
||
align-items: center;
|
||
padding: 30px 0 48px;
|
||
}
|
||
|
||
.hero-copy {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.eyebrow {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
letter-spacing: 0.12em;
|
||
text-transform: uppercase;
|
||
}
|
||
.eyebrow::before {
|
||
content: "";
|
||
width: 24px;
|
||
height: 1px;
|
||
background: var(--cyan);
|
||
}
|
||
h1,
|
||
h2,
|
||
h3,
|
||
p {
|
||
margin-top: 0;
|
||
}
|
||
h1 {
|
||
max-width: 790px;
|
||
margin-bottom: 24px;
|
||
font-family: var(--serif);
|
||
font-size: clamp(44px, 6.2vw, 86px);
|
||
font-weight: 600;
|
||
letter-spacing: -0.055em;
|
||
line-height: 1.07;
|
||
text-wrap: balance;
|
||
}
|
||
h1 em {
|
||
color: var(--cyan);
|
||
font-style: normal;
|
||
}
|
||
.hero-lede {
|
||
max-width: 650px;
|
||
margin-bottom: 28px;
|
||
color: #b0c1ca;
|
||
font-size: 17px;
|
||
line-height: 1.9;
|
||
}
|
||
|
||
.hero-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 42px;
|
||
padding: 0 15px;
|
||
border: 1px solid var(--line-strong);
|
||
border-radius: 10px;
|
||
color: var(--ink);
|
||
background: rgba(20, 42, 57, 0.65);
|
||
cursor: pointer;
|
||
transition:
|
||
border-color 0.2s ease,
|
||
background 0.2s ease,
|
||
transform 0.2s ease;
|
||
}
|
||
.button:hover {
|
||
border-color: rgba(107, 228, 212, 0.62);
|
||
background: rgba(107, 228, 212, 0.11);
|
||
transform: translateY(-2px);
|
||
}
|
||
.button--primary {
|
||
border-color: rgba(107, 228, 212, 0.7);
|
||
color: #071115;
|
||
background: var(--cyan);
|
||
}
|
||
.button--primary:hover {
|
||
color: #071115;
|
||
background: #a2f2e6;
|
||
}
|
||
.button--quiet {
|
||
border-color: transparent;
|
||
background: transparent;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.hero-orbit {
|
||
position: relative;
|
||
min-height: 420px;
|
||
overflow: hidden;
|
||
border: 1px solid rgba(123, 204, 205, 0.22);
|
||
border-radius: 30px;
|
||
background:
|
||
radial-gradient(circle at 50% 48%, rgba(107, 228, 212, 0.16), transparent 8rem),
|
||
linear-gradient(150deg, rgba(21, 48, 64, 0.86), rgba(9, 23, 35, 0.65));
|
||
box-shadow:
|
||
var(--shadow),
|
||
inset 0 0 60px rgba(107, 228, 212, 0.04);
|
||
}
|
||
.hero-orbit::before,
|
||
.hero-orbit::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 12%;
|
||
border: 1px solid rgba(107, 228, 212, 0.14);
|
||
border-radius: 50%;
|
||
transform: rotate(-22deg) scaleX(1.12);
|
||
}
|
||
.hero-orbit::after {
|
||
inset: 22%;
|
||
border-color: rgba(197, 243, 111, 0.18);
|
||
transform: rotate(46deg) scaleX(1.25);
|
||
}
|
||
.orbit-scan {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(
|
||
130deg,
|
||
transparent 48%,
|
||
rgba(107, 228, 212, 0.06) 50%,
|
||
transparent 52%
|
||
);
|
||
animation: scan 8s linear infinite;
|
||
}
|
||
.orbit-core {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
display: grid;
|
||
width: 118px;
|
||
height: 118px;
|
||
place-items: center;
|
||
border: 1px solid rgba(107, 228, 212, 0.64);
|
||
border-radius: 50%;
|
||
color: var(--cyan);
|
||
background: rgba(8, 17, 26, 0.72);
|
||
box-shadow:
|
||
0 0 0 14px rgba(107, 228, 212, 0.05),
|
||
0 0 60px rgba(107, 228, 212, 0.18);
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
.orbit-core span {
|
||
display: block;
|
||
margin-top: 5px;
|
||
color: var(--ink);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.12em;
|
||
}
|
||
.orbit-tag {
|
||
position: absolute;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
padding: 8px 10px;
|
||
border: 1px solid var(--line-strong);
|
||
border-radius: 10px;
|
||
color: #b8cbd3;
|
||
background: rgba(10, 25, 37, 0.8);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.02em;
|
||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16);
|
||
}
|
||
.orbit-tag svg {
|
||
color: var(--cyan);
|
||
}
|
||
.orbit-tag--one {
|
||
top: 15%;
|
||
left: 8%;
|
||
}
|
||
.orbit-tag--two {
|
||
top: 20%;
|
||
right: 8%;
|
||
color: var(--lime);
|
||
}
|
||
.orbit-tag--two svg {
|
||
color: var(--lime);
|
||
}
|
||
.orbit-tag--three {
|
||
bottom: 17%;
|
||
left: 12%;
|
||
color: var(--orange);
|
||
}
|
||
.orbit-tag--three svg {
|
||
color: var(--orange);
|
||
}
|
||
.orbit-tag--four {
|
||
right: 9%;
|
||
bottom: 16%;
|
||
}
|
||
.hero-stamp {
|
||
position: absolute;
|
||
right: 24px;
|
||
bottom: 22px;
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.signal-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
margin: 0 0 70px;
|
||
border-top: 1px solid var(--line);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.signal {
|
||
min-height: 112px;
|
||
padding: 20px 22px;
|
||
border-right: 1px solid var(--line);
|
||
}
|
||
.signal:last-child {
|
||
border-right: 0;
|
||
}
|
||
.signal-num {
|
||
display: block;
|
||
margin-bottom: 3px;
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 28px;
|
||
letter-spacing: -0.06em;
|
||
}
|
||
.signal:nth-child(2) .signal-num {
|
||
color: var(--lime);
|
||
}
|
||
.signal:nth-child(3) .signal-num {
|
||
color: var(--orange);
|
||
}
|
||
.signal:nth-child(4) .signal-num {
|
||
color: var(--blue);
|
||
}
|
||
.signal-label {
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.section {
|
||
scroll-margin-top: 94px;
|
||
margin-bottom: 100px;
|
||
}
|
||
.section-head {
|
||
display: flex;
|
||
align-items: end;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
margin-bottom: 24px;
|
||
}
|
||
.section-index {
|
||
margin-bottom: 9px;
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
letter-spacing: 0.12em;
|
||
}
|
||
h2 {
|
||
margin-bottom: 8px;
|
||
font-family: var(--serif);
|
||
font-size: clamp(30px, 4vw, 48px);
|
||
font-weight: 600;
|
||
letter-spacing: -0.045em;
|
||
line-height: 1.12;
|
||
}
|
||
.section-intro {
|
||
max-width: 690px;
|
||
margin-bottom: 0;
|
||
color: var(--muted);
|
||
font-size: 14px;
|
||
}
|
||
.section-rule {
|
||
flex: 1;
|
||
min-width: 50px;
|
||
height: 1px;
|
||
margin-bottom: 11px;
|
||
background: linear-gradient(90deg, var(--line-strong), transparent);
|
||
}
|
||
.mono {
|
||
font-family: var(--mono);
|
||
}
|
||
.muted {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.panel {
|
||
border: 1px solid var(--line);
|
||
border-radius: var(--radius);
|
||
background: var(--panel);
|
||
box-shadow: 0 16px 44px rgba(0, 0, 0, 0.1);
|
||
}
|
||
.panel-pad {
|
||
padding: 25px;
|
||
}
|
||
.label {
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.13em;
|
||
text-transform: uppercase;
|
||
}
|
||
.tiny {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.architecture-panel {
|
||
overflow: hidden;
|
||
}
|
||
.architecture-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
padding: 17px 22px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: rgba(9, 20, 30, 0.35);
|
||
}
|
||
.toolbar-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 12px;
|
||
}
|
||
.toolbar-title svg {
|
||
color: var(--cyan);
|
||
}
|
||
.toolbar-legend {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.legend-item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.legend-swatch {
|
||
width: 16px;
|
||
height: 2px;
|
||
border-radius: 3px;
|
||
background: var(--cyan);
|
||
}
|
||
.legend-swatch--read {
|
||
background: var(--blue);
|
||
}
|
||
.legend-swatch--legacy {
|
||
background: var(--dim);
|
||
border-top: 1px dashed var(--dim);
|
||
}
|
||
.architecture-svg {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
min-height: 510px;
|
||
padding: 18px 18px 26px;
|
||
}
|
||
.svg-label {
|
||
fill: #eef5f0;
|
||
font-family: var(--sans);
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
.svg-label-small {
|
||
fill: #91a7b7;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.svg-label-mono {
|
||
fill: #6be4d4;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.svg-box {
|
||
fill: #122535;
|
||
stroke: rgba(158, 202, 216, 0.35);
|
||
stroke-width: 1.2;
|
||
}
|
||
.svg-box-bright {
|
||
fill: rgba(107, 228, 212, 0.09);
|
||
stroke: rgba(107, 228, 212, 0.68);
|
||
}
|
||
.svg-box-mind {
|
||
fill: rgba(154, 185, 255, 0.08);
|
||
stroke: rgba(154, 185, 255, 0.55);
|
||
}
|
||
.svg-box-source {
|
||
fill: rgba(197, 243, 111, 0.08);
|
||
stroke: rgba(197, 243, 111, 0.63);
|
||
}
|
||
.svg-db {
|
||
fill: #0b1925;
|
||
stroke: rgba(158, 202, 216, 0.45);
|
||
stroke-width: 1.2;
|
||
}
|
||
.svg-db-bright {
|
||
fill: rgba(107, 228, 212, 0.1);
|
||
stroke: rgba(107, 228, 212, 0.65);
|
||
}
|
||
.svg-db-mind {
|
||
fill: rgba(154, 185, 255, 0.09);
|
||
stroke: rgba(154, 185, 255, 0.54);
|
||
}
|
||
.svg-line {
|
||
fill: none;
|
||
stroke: #6be4d4;
|
||
stroke-width: 2;
|
||
stroke-linecap: round;
|
||
marker-end: url(#arrowCyan);
|
||
}
|
||
.svg-line-read {
|
||
fill: none;
|
||
stroke: #9ab9ff;
|
||
stroke-width: 1.6;
|
||
stroke-linecap: round;
|
||
marker-end: url(#arrowBlue);
|
||
}
|
||
.svg-line-dashed {
|
||
fill: none;
|
||
stroke: #638092;
|
||
stroke-width: 1.4;
|
||
stroke-dasharray: 6 7;
|
||
stroke-linecap: round;
|
||
marker-end: url(#arrowDim);
|
||
}
|
||
.svg-pill {
|
||
fill: #0b1925;
|
||
stroke: rgba(158, 202, 216, 0.24);
|
||
}
|
||
.svg-pill-text {
|
||
fill: #91a7b7;
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
}
|
||
|
||
.ownership-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 14px;
|
||
}
|
||
.owner-card {
|
||
position: relative;
|
||
min-height: 220px;
|
||
padding: 22px;
|
||
overflow: hidden;
|
||
}
|
||
.owner-card::after {
|
||
content: "";
|
||
position: absolute;
|
||
right: -34px;
|
||
bottom: -52px;
|
||
width: 130px;
|
||
height: 130px;
|
||
border: 1px solid rgba(107, 228, 212, 0.18);
|
||
border-radius: 50%;
|
||
}
|
||
.owner-card:nth-child(2)::after {
|
||
border-color: rgba(154, 185, 255, 0.18);
|
||
}
|
||
.owner-card:nth-child(3)::after {
|
||
border-color: rgba(197, 243, 111, 0.16);
|
||
}
|
||
.owner-icon {
|
||
display: grid;
|
||
width: 38px;
|
||
height: 38px;
|
||
margin-bottom: 24px;
|
||
place-items: center;
|
||
border: 1px solid var(--line-strong);
|
||
border-radius: 12px;
|
||
color: var(--cyan);
|
||
background: var(--cyan-soft);
|
||
}
|
||
.owner-card:nth-child(2) .owner-icon {
|
||
color: var(--blue);
|
||
background: rgba(154, 185, 255, 0.1);
|
||
}
|
||
.owner-card:nth-child(3) .owner-icon {
|
||
color: var(--lime);
|
||
background: rgba(197, 243, 111, 0.1);
|
||
}
|
||
.owner-card h3 {
|
||
margin-bottom: 9px;
|
||
font-size: 18px;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
.owner-card p {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-bottom: 18px;
|
||
color: var(--muted);
|
||
font-size: 13px;
|
||
}
|
||
.tag-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 7px;
|
||
}
|
||
.tag {
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 6px;
|
||
color: #a7bcc7;
|
||
background: rgba(255, 255, 255, 0.025);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
|
||
.fact-bar {
|
||
display: grid;
|
||
grid-template-columns: 1.1fr 0.9fr;
|
||
gap: 14px;
|
||
margin-top: 14px;
|
||
}
|
||
.fact-callout {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 18px 20px;
|
||
border: 1px solid rgba(197, 243, 111, 0.28);
|
||
border-radius: var(--radius-sm);
|
||
background: rgba(197, 243, 111, 0.06);
|
||
}
|
||
.fact-callout svg {
|
||
color: var(--lime);
|
||
}
|
||
.fact-callout strong {
|
||
display: block;
|
||
margin-bottom: 2px;
|
||
font-size: 13px;
|
||
}
|
||
.fact-callout span {
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.key-box {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 18px 20px;
|
||
border: 1px solid rgba(107, 228, 212, 0.3);
|
||
border-radius: var(--radius-sm);
|
||
background: rgba(107, 228, 212, 0.06);
|
||
}
|
||
.key-box code {
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
word-break: break-word;
|
||
}
|
||
.copy-button {
|
||
display: inline-grid;
|
||
width: 30px;
|
||
height: 30px;
|
||
flex: 0 0 auto;
|
||
place-items: center;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
color: var(--muted);
|
||
background: transparent;
|
||
cursor: pointer;
|
||
}
|
||
.copy-button:hover {
|
||
color: var(--cyan);
|
||
border-color: rgba(107, 228, 212, 0.5);
|
||
}
|
||
|
||
.sequence-wrap {
|
||
overflow: hidden;
|
||
}
|
||
.sequence-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 17px 22px;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.sequence-top strong {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
}
|
||
.sequence-top strong svg {
|
||
color: var(--orange);
|
||
}
|
||
.sequence-svg {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 520px;
|
||
height: auto;
|
||
padding: 12px 12px 24px;
|
||
}
|
||
.seq-lane {
|
||
stroke: rgba(158, 202, 216, 0.2);
|
||
stroke-width: 1;
|
||
stroke-dasharray: 4 7;
|
||
}
|
||
.seq-header {
|
||
fill: #172d3e;
|
||
stroke: rgba(158, 202, 216, 0.34);
|
||
stroke-width: 1;
|
||
}
|
||
.seq-header-accent {
|
||
fill: rgba(107, 228, 212, 0.1);
|
||
stroke: rgba(107, 228, 212, 0.58);
|
||
}
|
||
.seq-header-orange {
|
||
fill: rgba(255, 173, 101, 0.1);
|
||
stroke: rgba(255, 173, 101, 0.55);
|
||
}
|
||
.seq-text {
|
||
fill: #eef5f0;
|
||
font-family: var(--sans);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
text-anchor: middle;
|
||
}
|
||
.seq-text-small {
|
||
fill: #91a7b7;
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
text-anchor: middle;
|
||
}
|
||
.seq-action {
|
||
fill: none;
|
||
stroke: #6be4d4;
|
||
stroke-width: 1.7;
|
||
marker-end: url(#seqArrowCyan);
|
||
}
|
||
.seq-action-orange {
|
||
fill: none;
|
||
stroke: #ffad65;
|
||
stroke-width: 1.7;
|
||
marker-end: url(#seqArrowOrange);
|
||
}
|
||
.seq-action-blue {
|
||
fill: none;
|
||
stroke: #9ab9ff;
|
||
stroke-width: 1.7;
|
||
marker-end: url(#seqArrowBlue);
|
||
}
|
||
.seq-action-dashed {
|
||
fill: none;
|
||
stroke: #638092;
|
||
stroke-width: 1.4;
|
||
stroke-dasharray: 5 6;
|
||
marker-end: url(#seqArrowDim);
|
||
}
|
||
.seq-number {
|
||
fill: #071115;
|
||
font-family: var(--mono);
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
text-anchor: middle;
|
||
}
|
||
.seq-number-circle {
|
||
fill: #6be4d4;
|
||
}
|
||
.seq-number-circle-orange {
|
||
fill: #ffad65;
|
||
}
|
||
.seq-number-circle-blue {
|
||
fill: #9ab9ff;
|
||
}
|
||
|
||
.result-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 14px;
|
||
margin-top: 14px;
|
||
}
|
||
.result-card {
|
||
padding: 20px;
|
||
border-top: 2px solid var(--cyan);
|
||
}
|
||
.result-card:nth-child(2) {
|
||
border-top-color: var(--orange);
|
||
}
|
||
.result-card:nth-child(3) {
|
||
border-top-color: var(--red);
|
||
}
|
||
.result-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
margin-bottom: 15px;
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
}
|
||
.result-card:nth-child(2) .result-status {
|
||
color: var(--orange);
|
||
}
|
||
.result-card:nth-child(3) .result-status {
|
||
color: var(--red);
|
||
}
|
||
.result-status::before {
|
||
content: "";
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: currentColor;
|
||
box-shadow: 0 0 12px currentColor;
|
||
}
|
||
.result-card h3 {
|
||
margin-bottom: 7px;
|
||
font-size: 16px;
|
||
}
|
||
.result-card p {
|
||
margin-bottom: 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.result-card .tag {
|
||
display: inline-block;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.sync-grid {
|
||
display: grid;
|
||
grid-template-columns: 1.05fr 0.95fr;
|
||
gap: 14px;
|
||
}
|
||
.sync-panel {
|
||
padding: 25px;
|
||
}
|
||
.sync-panel h3 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
margin-bottom: 7px;
|
||
font-size: 17px;
|
||
}
|
||
.sync-panel h3 svg {
|
||
color: var(--cyan);
|
||
}
|
||
.sync-panel > p {
|
||
margin-bottom: 23px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.sync-steps {
|
||
display: grid;
|
||
gap: 0;
|
||
}
|
||
.sync-step {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: 32px 1fr;
|
||
gap: 11px;
|
||
min-height: 64px;
|
||
}
|
||
.sync-step:not(:last-child)::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 28px;
|
||
left: 15px;
|
||
width: 1px;
|
||
height: calc(100% - 3px);
|
||
background: linear-gradient(var(--cyan), rgba(107, 228, 212, 0.08));
|
||
}
|
||
.step-num {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: grid;
|
||
width: 30px;
|
||
height: 30px;
|
||
place-items: center;
|
||
border: 1px solid rgba(107, 228, 212, 0.46);
|
||
border-radius: 50%;
|
||
color: var(--cyan);
|
||
background: #102334;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.sync-step h4 {
|
||
margin: 3px 0 3px;
|
||
font-size: 13px;
|
||
}
|
||
.sync-step p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.sync-state {
|
||
margin-top: 20px;
|
||
padding: 13px 15px;
|
||
border: 1px solid rgba(255, 173, 101, 0.28);
|
||
border-radius: 10px;
|
||
color: var(--orange);
|
||
background: var(--orange-soft);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
|
||
.state-map {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 26px;
|
||
}
|
||
.state-row {
|
||
display: grid;
|
||
grid-template-columns: 106px 1fr;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.state-name {
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
text-align: right;
|
||
}
|
||
.state-bar {
|
||
position: relative;
|
||
height: 30px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
}
|
||
.state-bar span {
|
||
display: flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
padding: 0 10px;
|
||
color: #c1d0d5;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.state-bar--cyan span {
|
||
width: 72%;
|
||
background: rgba(107, 228, 212, 0.16);
|
||
}
|
||
.state-bar--orange span {
|
||
width: 48%;
|
||
background: rgba(255, 173, 101, 0.16);
|
||
}
|
||
.state-bar--red span {
|
||
width: 30%;
|
||
background: rgba(255, 126, 126, 0.14);
|
||
}
|
||
.state-bar--lime span {
|
||
width: 88%;
|
||
background: rgba(197, 243, 111, 0.13);
|
||
}
|
||
.sync-rule {
|
||
display: grid;
|
||
gap: 10px;
|
||
margin-top: 24px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid var(--line);
|
||
}
|
||
.rule-line {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 9px;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.rule-line svg {
|
||
margin-top: 3px;
|
||
color: var(--cyan);
|
||
}
|
||
.rule-line strong {
|
||
color: var(--ink);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.guardrail-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
}
|
||
.guardrail {
|
||
min-height: 174px;
|
||
padding: 19px;
|
||
}
|
||
.guardrail-icon {
|
||
display: inline-grid;
|
||
width: 32px;
|
||
height: 32px;
|
||
margin-bottom: 18px;
|
||
place-items: center;
|
||
border: 1px solid var(--line-strong);
|
||
border-radius: 10px;
|
||
color: var(--cyan);
|
||
background: var(--cyan-soft);
|
||
}
|
||
.guardrail:nth-child(2) .guardrail-icon {
|
||
color: var(--orange);
|
||
background: var(--orange-soft);
|
||
}
|
||
.guardrail:nth-child(3) .guardrail-icon {
|
||
color: var(--blue);
|
||
background: rgba(154, 185, 255, 0.1);
|
||
}
|
||
.guardrail:nth-child(4) .guardrail-icon {
|
||
color: var(--lime);
|
||
background: rgba(197, 243, 111, 0.1);
|
||
}
|
||
.guardrail h3 {
|
||
margin-bottom: 6px;
|
||
font-size: 14px;
|
||
}
|
||
.guardrail p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.acceptance-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
padding: 17px 20px;
|
||
border: 1px solid rgba(107, 228, 212, 0.23);
|
||
border-radius: var(--radius-sm);
|
||
background: rgba(107, 228, 212, 0.055);
|
||
}
|
||
.acceptance-meter {
|
||
flex: 1;
|
||
height: 7px;
|
||
overflow: hidden;
|
||
border-radius: 20px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.acceptance-meter span {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--cyan), var(--lime));
|
||
opacity: 0.72;
|
||
}
|
||
.acceptance-head strong {
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
.acceptance-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px;
|
||
}
|
||
.acceptance-group {
|
||
overflow: hidden;
|
||
}
|
||
.acceptance-group summary {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 17px 19px;
|
||
border-bottom: 1px solid var(--line);
|
||
color: var(--ink);
|
||
cursor: pointer;
|
||
list-style: none;
|
||
}
|
||
.acceptance-group summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
.acceptance-group summary::after {
|
||
content: "+";
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 18px;
|
||
font-weight: 300;
|
||
}
|
||
.acceptance-group[open] summary::after {
|
||
content: "–";
|
||
}
|
||
.acceptance-group summary strong {
|
||
display: block;
|
||
font-size: 13px;
|
||
}
|
||
.acceptance-group summary span {
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.acceptance-list {
|
||
display: grid;
|
||
gap: 0;
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
.acceptance-list li {
|
||
display: grid;
|
||
grid-template-columns: 43px 1fr;
|
||
gap: 8px;
|
||
padding: 11px 19px;
|
||
border-bottom: 1px solid rgba(158, 202, 216, 0.08);
|
||
color: var(--muted);
|
||
font-size: 11px;
|
||
}
|
||
.acceptance-list li:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
.ac-id {
|
||
color: var(--cyan);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
|
||
.scope-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
}
|
||
.scope-card {
|
||
padding: 23px;
|
||
}
|
||
.scope-card h3 {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
font-size: 16px;
|
||
}
|
||
.scope-card--out h3 {
|
||
color: var(--orange);
|
||
}
|
||
.scope-card--risk h3 {
|
||
color: var(--lime);
|
||
}
|
||
.scope-list {
|
||
display: grid;
|
||
gap: 11px;
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
}
|
||
.scope-list li {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 9px;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
.scope-list svg {
|
||
margin-top: 3px;
|
||
color: var(--orange);
|
||
}
|
||
.scope-card--risk .scope-list svg {
|
||
color: var(--lime);
|
||
}
|
||
.open-question {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 13px;
|
||
padding: 22px 24px;
|
||
border: 1px solid rgba(154, 185, 255, 0.3);
|
||
border-radius: var(--radius);
|
||
background: rgba(154, 185, 255, 0.06);
|
||
}
|
||
.open-question > svg {
|
||
flex: 0 0 auto;
|
||
margin-top: 2px;
|
||
color: var(--blue);
|
||
}
|
||
.open-question strong {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: var(--blue);
|
||
font-size: 13px;
|
||
}
|
||
.open-question p {
|
||
margin: 0;
|
||
color: var(--muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
padding-top: 26px;
|
||
border-top: 1px solid var(--line);
|
||
color: var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
}
|
||
.footer span:last-child {
|
||
text-align: right;
|
||
}
|
||
|
||
.reveal {
|
||
opacity: 0;
|
||
transform: translateY(16px);
|
||
animation: reveal 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
|
||
animation-delay: var(--delay, 0ms);
|
||
}
|
||
.reveal-on-scroll {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition:
|
||
opacity 0.7s ease,
|
||
transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
|
||
}
|
||
.reveal-on-scroll.is-visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
@keyframes reveal {
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
@keyframes scan {
|
||
from {
|
||
transform: translateX(-45%) rotate(0deg);
|
||
}
|
||
to {
|
||
transform: translateX(45%) rotate(0deg);
|
||
}
|
||
}
|
||
|
||
.mobile-nav-toggle {
|
||
display: none;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
color: var(--muted);
|
||
background: transparent;
|
||
cursor: pointer;
|
||
}
|
||
|
||
@media (max-width: 1080px) {
|
||
.layout {
|
||
grid-template-columns: 180px minmax(0, 1fr);
|
||
gap: 27px;
|
||
width: min(100% - 42px, 1420px);
|
||
}
|
||
.hero {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.hero-orbit {
|
||
min-height: 360px;
|
||
}
|
||
.guardrail-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 760px) {
|
||
.topbar {
|
||
min-height: 60px;
|
||
padding: 0 18px;
|
||
}
|
||
.topbar-meta {
|
||
gap: 10px;
|
||
}
|
||
.topbar-meta > span:first-child {
|
||
white-space: nowrap;
|
||
}
|
||
.topbar-meta .mono {
|
||
display: none;
|
||
}
|
||
.layout {
|
||
display: block;
|
||
width: min(100% - 32px, 1420px);
|
||
}
|
||
.sidebar {
|
||
position: fixed;
|
||
inset: 60px 0 auto;
|
||
z-index: 14;
|
||
display: none;
|
||
padding: 12px 16px 16px;
|
||
border-bottom: 1px solid var(--line);
|
||
background: rgba(8, 17, 26, 0.95);
|
||
backdrop-filter: blur(20px);
|
||
}
|
||
.sidebar.is-open {
|
||
display: block;
|
||
}
|
||
.side-kicker,
|
||
.side-note {
|
||
display: none;
|
||
}
|
||
.side-nav {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.mobile-nav-toggle {
|
||
display: inline-grid;
|
||
width: 34px;
|
||
height: 34px;
|
||
place-items: center;
|
||
}
|
||
main {
|
||
padding-top: 42px;
|
||
}
|
||
.hero {
|
||
min-height: auto;
|
||
padding-top: 15px;
|
||
}
|
||
h1 {
|
||
font-size: clamp(42px, 13vw, 70px);
|
||
}
|
||
.hero-lede {
|
||
font-size: 15px;
|
||
}
|
||
.hero-orbit {
|
||
min-height: 310px;
|
||
border-radius: 22px;
|
||
}
|
||
.orbit-tag {
|
||
font-size: 8px;
|
||
}
|
||
.orbit-core {
|
||
width: 92px;
|
||
height: 92px;
|
||
}
|
||
.signal-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.signal:nth-child(2) {
|
||
border-right: 0;
|
||
}
|
||
.signal:nth-child(-n + 2) {
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.section {
|
||
margin-bottom: 76px;
|
||
}
|
||
.section-head {
|
||
display: block;
|
||
}
|
||
.section-rule {
|
||
display: none;
|
||
}
|
||
.architecture-svg,
|
||
.sequence-svg {
|
||
min-height: 0;
|
||
overflow: visible;
|
||
}
|
||
.ownership-grid,
|
||
.fact-bar,
|
||
.result-grid,
|
||
.sync-grid,
|
||
.acceptance-grid,
|
||
.scope-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.guardrail-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
.architecture-toolbar,
|
||
.sequence-top {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
.footer {
|
||
align-items: flex-start;
|
||
flex-direction: column;
|
||
}
|
||
.footer span:last-child {
|
||
text-align: left;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 460px) {
|
||
.guardrail-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.hero-actions .button {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
.state-row {
|
||
grid-template-columns: 86px 1fr;
|
||
}
|
||
.state-name {
|
||
font-size: 9px;
|
||
}
|
||
}
|
||
|
||
@media print {
|
||
:root {
|
||
color-scheme: light;
|
||
--bg: #fff;
|
||
--ink: #17212b;
|
||
--muted: #4d5d69;
|
||
--dim: #6f7f89;
|
||
--line: #d8e0e4;
|
||
--panel: #fff;
|
||
--panel-strong: #f3f7f8;
|
||
}
|
||
body {
|
||
background: #fff;
|
||
}
|
||
body::before,
|
||
body::after,
|
||
.topbar,
|
||
.sidebar,
|
||
.hero-orbit,
|
||
.hero-actions {
|
||
display: none !important;
|
||
}
|
||
.layout {
|
||
display: block;
|
||
width: auto;
|
||
margin: 0 24px;
|
||
}
|
||
main {
|
||
padding-top: 20px;
|
||
}
|
||
.hero {
|
||
min-height: auto;
|
||
}
|
||
.panel {
|
||
box-shadow: none;
|
||
}
|
||
.reveal,
|
||
.reveal-on-scroll {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
.section {
|
||
break-inside: avoid;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<svg class="icon-sprite" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||
<symbol id="i-route" viewBox="0 0 24 24">
|
||
<circle cx="6" cy="6" r="3" />
|
||
<circle cx="18" cy="18" r="3" />
|
||
<path d="M8.5 7.5c1.4 1.3 1.9 3.2 3.4 4.5 1.7 1.5 3.7.2 4.6 2.2" />
|
||
</symbol>
|
||
<symbol id="i-database" viewBox="0 0 24 24">
|
||
<ellipse cx="12" cy="5" rx="8" ry="3" />
|
||
<path d="M4 5v7c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12v7c0 1.7 3.6 3 8 3s8-1.3 8-3v-7" />
|
||
</symbol>
|
||
<symbol id="i-plug" viewBox="0 0 24 24">
|
||
<path d="M8 12h8M9 3v6M15 3v6M7 9h10v3a5 5 0 0 1-5 5v4M12 21v-4" />
|
||
<path d="M5 9H3M21 9h-2" />
|
||
</symbol>
|
||
<symbol id="i-layers" viewBox="0 0 24 24">
|
||
<path d="m12 3 9 5-9 5-9-5 9-5Z" />
|
||
<path d="m3 12 9 5 9-5M3 16l9 5 9-5" />
|
||
</symbol>
|
||
<symbol id="i-shield" viewBox="0 0 24 24">
|
||
<path d="M12 3 20 6v5c0 5.2-3.4 8.5-8 10-4.6-1.5-8-4.8-8-10V6l8-3Z" />
|
||
<path d="m8.5 12 2.2 2.2 4.8-5" />
|
||
</symbol>
|
||
<symbol id="i-send" viewBox="0 0 24 24">
|
||
<path d="m21 3-7.2 18-3.6-7.2L3 10.2 21 3Z" />
|
||
<path d="M10.2 13.8 15 9" />
|
||
</symbol>
|
||
<symbol id="i-inbox" viewBox="0 0 24 24">
|
||
<path d="M4 4h16l2 11v4H2v-4L4 4Z" />
|
||
<path d="M2 15h5l2 3h6l2-3h5M8 8h8" />
|
||
</symbol>
|
||
<symbol id="i-refresh" viewBox="0 0 24 24">
|
||
<path
|
||
d="M20 11a8.1 8.1 0 0 0-14.8-3L3 11M4 4v7h7M4 13a8.1 8.1 0 0 0 14.8 3L21 13m0 7v-7h-7"
|
||
/>
|
||
</symbol>
|
||
<symbol id="i-alert" viewBox="0 0 24 24">
|
||
<path d="M12 3 2.5 20h19L12 3Z" />
|
||
<path d="M12 9v5M12 17.5v.1" />
|
||
</symbol>
|
||
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6" /></symbol>
|
||
<symbol id="i-lock" viewBox="0 0 24 24">
|
||
<rect x="4" y="10" width="16" height="11" rx="2" />
|
||
<path d="M8 10V7a4 4 0 0 1 8 0v3M12 14v3" />
|
||
</symbol>
|
||
<symbol id="i-fingerprint" viewBox="0 0 24 24">
|
||
<path
|
||
d="M12 11a2 2 0 0 1 2 2v3M8 13a4 4 0 0 1 8 0v4M5 13a7 7 0 0 1 14 0v2M12 5a8 8 0 0 1 8 8M12 5a8 8 0 0 0-8 8M10 20c.6-1.2 1-2.7 1-4v-3a1 1 0 0 1 2 0v3c0 1.8-.4 3.5-1.2 5"
|
||
/>
|
||
</symbol>
|
||
<symbol id="i-copy" viewBox="0 0 24 24">
|
||
<rect x="8" y="8" width="11" height="11" rx="2" />
|
||
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
|
||
</symbol>
|
||
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12h13M13 6l6 6-6 6" /></symbol>
|
||
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16" /></symbol>
|
||
<symbol id="i-print" viewBox="0 0 24 24">
|
||
<path
|
||
d="M6 9V3h12v6M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"
|
||
/>
|
||
<path d="M6 14h12v7H6z" />
|
||
<path d="M17 12h2" />
|
||
</symbol>
|
||
<symbol id="i-question" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="9" />
|
||
<path d="M9.6 9a2.6 2.6 0 1 1 4.6 1.7c-1.2 1.2-2.2 1.5-2.2 3M12 17.5v.1" />
|
||
</symbol>
|
||
</svg>
|
||
|
||
<header class="topbar">
|
||
<a class="brand" href="#overview" aria-label="回到页面顶部">
|
||
<span class="brand-mark"
|
||
><svg class="icon"><use href="#i-route" /></svg
|
||
></span>
|
||
<span class="brand-text">TradeBridge / OneTalk</span>
|
||
</a>
|
||
<div class="topbar-meta">
|
||
<span><i class="status-dot"></i>架构已确认</span>
|
||
<span class="mono">PRD · 2026.08</span>
|
||
<button class="mobile-nav-toggle" type="button" aria-label="打开导航" aria-expanded="false">
|
||
<svg class="icon"><use href="#i-menu" /></svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="layout">
|
||
<aside class="sidebar" id="sidebar">
|
||
<p class="side-kicker">Map / 00—07</p>
|
||
<nav class="side-nav" aria-label="页面章节">
|
||
<a href="#overview" class="is-active"
|
||
><svg class="icon icon--sm"><use href="#i-layers" /></svg>总览</a
|
||
>
|
||
<a href="#ownership"
|
||
><svg class="icon icon--sm"><use href="#i-shield" /></svg>事实归属</a
|
||
>
|
||
<a href="#architecture"
|
||
><svg class="icon icon--sm"><use href="#i-route" /></svg>系统架构</a
|
||
>
|
||
<a href="#send"
|
||
><svg class="icon icon--sm"><use href="#i-send" /></svg>发送确认</a
|
||
>
|
||
<a href="#sync"
|
||
><svg class="icon icon--sm"><use href="#i-refresh" /></svg>同步状态机</a
|
||
>
|
||
<a href="#guardrails"
|
||
><svg class="icon icon--sm"><use href="#i-lock" /></svg>边界守则</a
|
||
>
|
||
<a href="#acceptance"
|
||
><svg class="icon icon--sm"><use href="#i-check" /></svg>验收清单</a
|
||
>
|
||
<a href="#scope"
|
||
><svg class="icon icon--sm"><use href="#i-question" /></svg>范围与风险</a
|
||
>
|
||
</nav>
|
||
<div class="side-note">
|
||
<strong>唯一事实源</strong>
|
||
<p>OneTalk 页面观察到的真实消息,经过精确身份闭环后才进入 Bright DB。</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<main>
|
||
<section class="hero section" id="overview">
|
||
<div class="hero-copy">
|
||
<div class="eyebrow reveal" style="--delay: 80ms">Architecture brief / 01</div>
|
||
<h1 class="reveal" style="--delay: 150ms">让消息回到<br /><em>事实发生的地方</em></h1>
|
||
<p class="hero-lede reveal" style="--delay: 230ms">
|
||
OneTalk 页面成为唯一事实源。Chrome 插件负责精确观察与耐久缓冲,TradeBright
|
||
负责消息事实与授权边界,TradeMind 负责业务关联与用户体验。
|
||
</p>
|
||
<div class="hero-actions reveal" style="--delay: 310ms">
|
||
<a class="button button--primary" href="#architecture"
|
||
>查看系统架构 <svg class="icon icon--sm"><use href="#i-arrow" /></svg
|
||
></a>
|
||
<button class="button button--quiet" type="button" id="printPage">
|
||
<svg class="icon icon--sm"><use href="#i-print" /></svg>打印 / 导出 PDF
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="hero-orbit reveal"
|
||
style="--delay: 260ms"
|
||
aria-label="OneTalk 消息事实链路示意"
|
||
>
|
||
<div class="orbit-scan"></div>
|
||
<div class="orbit-core">
|
||
<div>
|
||
<svg class="icon icon--lg"><use href="#i-fingerprint" /></svg
|
||
><span>FACT SOURCE</span>
|
||
</div>
|
||
</div>
|
||
<div class="orbit-tag orbit-tag--one">
|
||
<svg class="icon icon--sm"><use href="#i-inbox" /></svg>OneTalk page
|
||
</div>
|
||
<div class="orbit-tag orbit-tag--two">
|
||
<svg class="icon icon--sm"><use href="#i-database" /></svg>Bright DB
|
||
</div>
|
||
<div class="orbit-tag orbit-tag--three">
|
||
<svg class="icon icon--sm"><use href="#i-plug" /></svg>IndexedDB
|
||
</div>
|
||
<div class="orbit-tag orbit-tag--four">
|
||
<svg class="icon icon--sm"><use href="#i-shield" /></svg>Mind auth view
|
||
</div>
|
||
<div class="hero-stamp">One source · two stores · zero legacy fallback</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="signal-strip reveal-on-scroll" aria-label="架构关键数字">
|
||
<div class="signal">
|
||
<span class="signal-num">01</span><span class="signal-label">OneTalk 事实源</span>
|
||
</div>
|
||
<div class="signal">
|
||
<span class="signal-num">02</span
|
||
><span class="signal-label">独立数据库:Bright / Mind</span>
|
||
</div>
|
||
<div class="signal">
|
||
<span class="signal-num">03</span
|
||
><span class="signal-label">发送终态:确认 / 拒绝 / 未知</span>
|
||
</div>
|
||
<div class="signal">
|
||
<span class="signal-num">0×</span
|
||
><span class="signal-label">OneTalk 不写 outbox / dispatch</span>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="section" id="ownership">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">02 / OWNERSHIP</div>
|
||
<h2>谁拥有哪一种事实?</h2>
|
||
<p class="section-intro">
|
||
边界先于接口。每个系统只保管自己真正负责的事实,跨系统通过原始
|
||
ID、版本化认证视图和受权实时通道协作。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="ownership-grid">
|
||
<article class="panel owner-card reveal-on-scroll">
|
||
<div class="owner-icon">
|
||
<svg class="icon"><use href="#i-database" /></svg>
|
||
</div>
|
||
<h3>TradeBright</h3>
|
||
<p>OneTalk 消息中心。唯一写入并读取 <code>onetalk_message</code> 的服务端。</p>
|
||
<div class="tag-list">
|
||
<span class="tag">消息正文</span><span class="tag">附件 / 引用</span
|
||
><span class="tag">原始 ID</span><span class="tag">同步锚点</span>
|
||
</div>
|
||
</article>
|
||
<article class="panel owner-card reveal-on-scroll">
|
||
<div class="owner-icon">
|
||
<svg class="icon"><use href="#i-shield" /></svg>
|
||
</div>
|
||
<h3>TradeMind</h3>
|
||
<p>CRM 业务事实。消息页面读取 Bright,继续持有权限、客户关系和业务标注。</p>
|
||
<div class="tag-list">
|
||
<span class="tag">workspace 权限</span><span class="tag">客户关联</span
|
||
><span class="tag">负责人</span><span class="tag">摘要 / 未读</span>
|
||
</div>
|
||
</article>
|
||
<article class="panel owner-card reveal-on-scroll">
|
||
<div class="owner-icon">
|
||
<svg class="icon"><use href="#i-plug" /></svg>
|
||
</div>
|
||
<h3>Chrome 插件</h3>
|
||
<p>页面事实采集器。精确匹配 OneTalk 页面,先落 IndexedDB,再逐条上传确认。</p>
|
||
<div class="tag-list">
|
||
<span class="tag">身份范围</span><span class="tag">全量 / 增量</span
|
||
><span class="tag">发送闭环</span><span class="tag">异常采集</span>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<div class="fact-bar reveal-on-scroll">
|
||
<div class="fact-callout">
|
||
<svg class="icon icon--lg"><use href="#i-fingerprint" /></svg>
|
||
<div>
|
||
<strong>OneTalk 页面 = 消息存在性的唯一来源</strong
|
||
><span>Mind 不得主动创建尚未被插件发现的 OneTalk 会话。</span>
|
||
</div>
|
||
</div>
|
||
<div class="key-box">
|
||
<code id="idempotencyKey">channelAccountId + conversationId + messageId</code
|
||
><button
|
||
class="copy-button"
|
||
type="button"
|
||
data-copy="channelAccountId + conversationId + messageId"
|
||
aria-label="复制幂等键"
|
||
>
|
||
<svg class="icon icon--sm"><use href="#i-copy" /></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="architecture">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">03 / SYSTEM MAP</div>
|
||
<h2>三套系统,一条事实链路</h2>
|
||
<p class="section-intro">
|
||
实时消息走 WebSocket,历史消息走 Bright HTTP 查询;Mind 服务端不代理消息,也不拥有
|
||
Bright DB 凭据。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="panel architecture-panel reveal-on-scroll">
|
||
<div class="architecture-toolbar">
|
||
<div class="toolbar-title">
|
||
<svg class="icon icon--sm"><use href="#i-route" /></svg
|
||
><strong>Target topology / v1</strong>
|
||
</div>
|
||
<div class="toolbar-legend">
|
||
<span class="legend-item"><i class="legend-swatch"></i>双向实时</span
|
||
><span class="legend-item"
|
||
><i class="legend-swatch legend-swatch--read"></i>受权只读</span
|
||
><span class="legend-item"
|
||
><i class="legend-swatch legend-swatch--legacy"></i>非 OneTalk legacy</span
|
||
>
|
||
</div>
|
||
</div>
|
||
<svg
|
||
class="architecture-svg"
|
||
viewBox="0 0 1000 570"
|
||
role="img"
|
||
aria-labelledby="architectureTitle architectureDesc"
|
||
>
|
||
<title id="architectureTitle">OneTalk 消息中心目标架构</title>
|
||
<desc id="architectureDesc">
|
||
OneTalk 页面通过 Chrome 插件和 WebSocket 连接 TradeBright,TradeBright 写入 Bright
|
||
DB。TradeMind 页面通过 HTTP 和 WebSocket 访问 TradeBright,并从 Mind DB
|
||
获取业务信息。
|
||
</desc>
|
||
<defs>
|
||
<marker
|
||
id="arrowCyan"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto-start-reverse"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#6be4d4" />
|
||
</marker>
|
||
<marker
|
||
id="arrowBlue"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#9ab9ff" />
|
||
</marker>
|
||
<marker
|
||
id="arrowDim"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#638092" />
|
||
</marker>
|
||
</defs>
|
||
<text x="30" y="35" class="svg-label-small">SOURCE LAYER</text>
|
||
<text x="340" y="35" class="svg-label-small">FACT SERVICE</text>
|
||
<text x="720" y="35" class="svg-label-small">BUSINESS EXPERIENCE</text>
|
||
|
||
<rect x="26" y="68" width="194" height="122" rx="18" class="svg-box svg-box-source" />
|
||
<circle cx="57" cy="104" r="17" fill="rgba(197,243,111,.14)" stroke="#c5f36f" />
|
||
<path
|
||
d="M49 104h16M57 96v16"
|
||
stroke="#c5f36f"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
/>
|
||
<text x="87" y="105" class="svg-label">OneTalk 页面</text>
|
||
<text x="45" y="139" class="svg-label-small">真实消息事实发生处</text>
|
||
<text x="45" y="158" class="svg-label-mono" fill="#c5f36f">
|
||
conversation / message
|
||
</text>
|
||
|
||
<rect x="26" y="230" width="194" height="174" rx="18" class="svg-box" />
|
||
<circle cx="57" cy="266" r="17" fill="rgba(107,228,212,.12)" stroke="#6be4d4" />
|
||
<path
|
||
d="M49 266h16M53 258v16M61 258v16"
|
||
stroke="#6be4d4"
|
||
stroke-width="1.5"
|
||
stroke-linecap="round"
|
||
/>
|
||
<text x="87" y="267" class="svg-label">Chrome 插件</text>
|
||
<text x="45" y="304" class="svg-label-small">精确页面适配 + 本地耐久账本</text>
|
||
<rect x="45" y="326" width="156" height="48" rx="10" class="svg-db" />
|
||
<path
|
||
d="M57 341c0-4 8-7 17-7s17 3 17 7-8 7-17 7-17-3-17-7ZM57 341v13c0 4 8 7 17 7s17-3 17-7v-13"
|
||
fill="none"
|
||
stroke="#6be4d4"
|
||
stroke-width="1.2"
|
||
/>
|
||
<text x="107" y="352" class="svg-label-small" text-anchor="middle">IndexedDB</text>
|
||
<text x="107" y="365" class="svg-label-small" text-anchor="middle">
|
||
awaiting_anchor
|
||
</text>
|
||
|
||
<rect
|
||
x="315"
|
||
y="85"
|
||
width="340"
|
||
height="222"
|
||
rx="20"
|
||
class="svg-box svg-box-bright"
|
||
/>
|
||
<rect x="338" y="108" width="170" height="64" rx="13" class="svg-box-bright" />
|
||
<path
|
||
d="M360 136h26M373 123v26"
|
||
stroke="#6be4d4"
|
||
stroke-width="1.8"
|
||
stroke-linecap="round"
|
||
/>
|
||
<text x="420" y="139" class="svg-label">TradeBright</text>
|
||
<text x="420" y="157" class="svg-label-small">消息事实 / WS / HTTP</text>
|
||
<rect x="530" y="108" width="100" height="64" rx="13" class="svg-box" />
|
||
<path
|
||
d="M555 128c0-3 5-5 12-5s12 2 12 5-5 5-12 5-12-2-12-5ZM555 128v13c0 3 5 5 12 5s12-2 12-5v-13"
|
||
fill="none"
|
||
stroke="#6be4d4"
|
||
stroke-width="1.2"
|
||
/>
|
||
<text x="592" y="157" class="svg-label-small" text-anchor="middle">Bright DB</text>
|
||
<text x="338" y="211" class="svg-label-small">唯一服务端消息表</text>
|
||
<text x="338" y="234" class="svg-label-mono">onetalk_message</text>
|
||
<text x="338" y="257" class="svg-label-mono">onetalk_message_anomaly</text>
|
||
<text x="338" y="280" class="svg-label-small">anchor / binding / diagnostics</text>
|
||
|
||
<rect x="736" y="85" width="237" height="150" rx="20" class="svg-box svg-box-mind" />
|
||
<circle cx="776" cy="126" r="17" fill="rgba(154,185,255,.13)" stroke="#9ab9ff" />
|
||
<path
|
||
d="M768 126h16M776 118v16"
|
||
stroke="#9ab9ff"
|
||
stroke-width="1.7"
|
||
stroke-linecap="round"
|
||
/>
|
||
<text x="806" y="128" class="svg-label">TradeMind 页面</text>
|
||
<text x="760" y="162" class="svg-label-small">历史查询 + 实时收发</text>
|
||
<text x="760" y="183" class="svg-label-mono" fill="#9ab9ff">
|
||
read / send / realtime
|
||
</text>
|
||
<text x="760" y="208" class="svg-label-small">浏览器不直连数据库</text>
|
||
|
||
<rect x="736" y="276" width="237" height="128" rx="20" class="svg-box svg-box-mind" />
|
||
<path
|
||
d="M778 309c0-4 9-7 19-7s19 3 19 7-9 7-19 7-19-3-19-7ZM778 309v25c0 4 9 7 19 7s19-3 19-7v-25"
|
||
fill="none"
|
||
stroke="#9ab9ff"
|
||
stroke-width="1.3"
|
||
/>
|
||
<text x="845" y="319" class="svg-label">Mind DB</text>
|
||
<text x="760" y="358" class="svg-label-small">权限 / binding / 客户业务事实</text>
|
||
<text x="760" y="379" class="svg-label-small">不保存 OneTalk 正文副本</text>
|
||
|
||
<path d="M123 190V230" class="svg-line" />
|
||
<path d="M220 293C270 293 270 157 315 157" class="svg-line" />
|
||
<path d="M315 196C270 196 270 363 220 363" class="svg-line" />
|
||
<path d="M655 157C697 157 696 157 736 157" class="svg-line" />
|
||
<path d="M736 186C691 186 691 214 655 214" class="svg-line" />
|
||
<path d="M854 235V276" class="svg-line-read" />
|
||
<path d="M736 333C695 333 694 255 655 255" class="svg-line-read" />
|
||
<path d="M736 390C680 435 306 444 220 404" class="svg-line-dashed" />
|
||
|
||
<rect x="235" y="266" width="62" height="22" rx="11" class="svg-pill" />
|
||
<text x="266" y="280" class="svg-pill-text" text-anchor="middle">WS ↔</text>
|
||
<rect x="662" y="132" width="65" height="22" rx="11" class="svg-pill" />
|
||
<text x="694" y="146" class="svg-pill-text" text-anchor="middle">HTTP / WS</text>
|
||
<rect x="675" y="365" width="55" height="22" rx="11" class="svg-pill" />
|
||
<text x="702" y="379" class="svg-pill-text" text-anchor="middle">read</text>
|
||
<rect x="365" y="432" width="235" height="25" rx="12" class="svg-pill" />
|
||
<text x="482" y="448" class="svg-pill-text" text-anchor="middle">
|
||
legacy outbox / dispatch · non-OneTalk only
|
||
</text>
|
||
|
||
<text x="27" y="475" class="svg-label-small">AUTHORIZATION BOUNDARY</text>
|
||
<path d="M315 487H973" stroke="rgba(154,185,255,.32)" stroke-dasharray="3 7" />
|
||
<text x="315" y="512" class="svg-label-small">
|
||
Bright 只读 Mind 的最小版本化认证视图;授权失败 / 不可用时全部 fail closed
|
||
</text>
|
||
</svg>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="send">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">04 / SEND CONFIRMATION</div>
|
||
<h2>发送不是事实,确认才是</h2>
|
||
<p class="section-intro">
|
||
发送请求只存在于本次交互内存中。只有 OneTalk 页面完成确定性事实闭环,Bright
|
||
才会写入消息表并推送最终事实。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="panel sequence-wrap reveal-on-scroll">
|
||
<div class="sequence-top">
|
||
<strong
|
||
><svg class="icon icon--sm"><use href="#i-send" /></svg>Outbound confirmation /
|
||
deterministic evidence loop</strong
|
||
><span class="label">same sendRequestId · no durable task</span>
|
||
</div>
|
||
<svg
|
||
class="sequence-svg"
|
||
viewBox="0 0 1000 555"
|
||
role="img"
|
||
aria-labelledby="sequenceTitle sequenceDesc"
|
||
>
|
||
<title id="sequenceTitle">发送确认时序图</title>
|
||
<desc id="sequenceDesc">
|
||
TradeMind 页面发起发送,TradeBright 转发给 Chrome 插件,插件驱动精确 OneTalk
|
||
页面并检查事实,确认后反向回报并最终写入 Bright DB。
|
||
</desc>
|
||
<defs>
|
||
<marker
|
||
id="seqArrowCyan"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#6be4d4" />
|
||
</marker>
|
||
<marker
|
||
id="seqArrowOrange"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#ffad65" />
|
||
</marker>
|
||
<marker
|
||
id="seqArrowBlue"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#9ab9ff" />
|
||
</marker>
|
||
<marker
|
||
id="seqArrowDim"
|
||
viewBox="0 0 10 10"
|
||
refX="9"
|
||
refY="5"
|
||
markerWidth="5"
|
||
markerHeight="5"
|
||
orient="auto"
|
||
>
|
||
<path d="M0 0 10 5 0 10Z" fill="#638092" />
|
||
</marker>
|
||
</defs>
|
||
<rect
|
||
x="52"
|
||
y="20"
|
||
width="170"
|
||
height="42"
|
||
rx="11"
|
||
class="seq-header seq-header-accent"
|
||
/>
|
||
<text x="137" y="46" class="seq-text">TradeMind 页面</text>
|
||
<rect
|
||
x="300"
|
||
y="20"
|
||
width="170"
|
||
height="42"
|
||
rx="11"
|
||
class="seq-header seq-header-accent"
|
||
/>
|
||
<text x="385" y="46" class="seq-text">TradeBright</text>
|
||
<rect x="548" y="20" width="170" height="42" rx="11" class="seq-header" />
|
||
<text x="633" y="46" class="seq-text">Chrome 插件</text>
|
||
<rect
|
||
x="796"
|
||
y="20"
|
||
width="150"
|
||
height="42"
|
||
rx="11"
|
||
class="seq-header seq-header-orange"
|
||
/>
|
||
<text x="871" y="46" class="seq-text">OneTalk 页面</text>
|
||
<path d="M137 62V520M385 62V520M633 62V520M871 62V520" class="seq-lane" />
|
||
|
||
<circle cx="137" cy="98" r="12" class="seq-number-circle" />
|
||
<text x="137" y="101" class="seq-number">01</text>
|
||
<path d="M149 98H373" class="seq-action" />
|
||
<text x="261" y="91" class="seq-text-small">send(text, conversationId)</text>
|
||
<text x="385" y="120" class="seq-text-small">授权 read + send</text>
|
||
|
||
<circle cx="385" cy="143" r="12" class="seq-number-circle" />
|
||
<text x="385" y="146" class="seq-number">02</text>
|
||
<path d="M397 143H621" class="seq-action" />
|
||
<text x="509" y="136" class="seq-text-small">relay(sendRequestId)</text>
|
||
<text x="633" y="165" class="seq-text-small">绑定精确 channelAccountId</text>
|
||
|
||
<circle cx="633" cy="188" r="12" class="seq-number-circle-orange" />
|
||
<text x="633" y="191" class="seq-number">03</text>
|
||
<path d="M645 188H859" class="seq-action-orange" />
|
||
<text x="752" y="181" class="seq-text-small">drive exact page</text>
|
||
<text x="871" y="210" class="seq-text-small">冻结发送前快照</text>
|
||
|
||
<circle cx="871" cy="233" r="12" class="seq-number-circle-orange" />
|
||
<text x="871" y="236" class="seq-number">04</text>
|
||
<path d="M859 233H645" class="seq-action-orange" />
|
||
<text x="752" y="226" class="seq-text-small">page fact observation</text>
|
||
<text x="633" y="255" class="seq-text-small">快照后出现新 messageId</text>
|
||
|
||
<circle cx="633" cy="278" r="12" class="seq-number-circle-orange" />
|
||
<text x="633" y="281" class="seq-number">05</text>
|
||
<path d="M621 278H397" class="seq-action-orange" />
|
||
<text x="509" y="271" class="seq-text-small">confirmed / rejected / unknown</text>
|
||
<text x="385" y="300" class="seq-text-small">保持原请求结果,不追溯改写</text>
|
||
|
||
<circle cx="385" cy="323" r="12" class="seq-number-circle" />
|
||
<text x="385" y="326" class="seq-number">06</text>
|
||
<path d="M373 323H149" class="seq-action" />
|
||
<text x="261" y="316" class="seq-text-small">terminal receipt</text>
|
||
<text x="137" y="345" class="seq-text-small">仅确认成功显示最终事实</text>
|
||
|
||
<circle cx="385" cy="381" r="12" class="seq-number-circle-blue" />
|
||
<text x="385" y="384" class="seq-number">07</text>
|
||
<path d="M385 393V445" class="seq-action-blue" />
|
||
<rect
|
||
x="309"
|
||
y="453"
|
||
width="152"
|
||
height="41"
|
||
rx="10"
|
||
fill="rgba(154,185,255,.08)"
|
||
stroke="rgba(154,185,255,.4)"
|
||
/>
|
||
<text x="385" y="478" class="seq-text-small">DB commit → WS push</text>
|
||
<path d="M300 507H222" class="seq-action-blue" />
|
||
<text x="261" y="500" class="seq-text-small">message fact</text>
|
||
|
||
<path d="M633 377C723 365 792 366 859 381" class="seq-action-dashed" />
|
||
<text x="755" y="360" class="seq-text-small">timeout / disconnect</text>
|
||
<rect
|
||
x="696"
|
||
y="405"
|
||
width="236"
|
||
height="40"
|
||
rx="10"
|
||
fill="rgba(255,173,101,.08)"
|
||
stroke="rgba(255,173,101,.35)"
|
||
/>
|
||
<text x="814" y="430" class="seq-text-small" fill="#ffad65">
|
||
delivery_unknown · no retry / no row
|
||
</text>
|
||
</svg>
|
||
</div>
|
||
<div class="result-grid">
|
||
<article class="panel result-card reveal-on-scroll">
|
||
<span class="result-status">confirmed_sent</span>
|
||
<h3>页面事实闭环</h3>
|
||
<p>
|
||
四字段齐全、精确会话匹配、新 messageId、内容与时间一致且无歧义。写入
|
||
<code>onetalk_message</code>。
|
||
</p>
|
||
<span class="tag">唯一可落库</span>
|
||
</article>
|
||
<article class="panel result-card reveal-on-scroll">
|
||
<span class="result-status">rejected_before_send</span>
|
||
<h3>发送前明确拒绝</h3>
|
||
<p>插件离线、精确页面不存在或身份不匹配。没有发送副作用,不排队、不切页。</p>
|
||
<span class="tag">数据库零写入</span>
|
||
</article>
|
||
<article class="panel result-card reveal-on-scroll">
|
||
<span class="result-status">delivery_unknown</span>
|
||
<h3>事实无法确认</h3>
|
||
<p>
|
||
人工干扰、并发、延迟、超时或断线。宁可未知,不误报成功;不自动恢复、不自动重试。
|
||
</p>
|
||
<span class="tag">临时气泡可消失</span>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="sync">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">05 / DURABLE SYNC</div>
|
||
<h2>先落地,再跨边界</h2>
|
||
<p class="section-intro">
|
||
IndexedDB 是插件的耐久事实缓冲。全量与增量都按会话独立推进,浏览器或 service worker
|
||
中断后从检查点继续。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="sync-grid">
|
||
<article class="panel sync-panel reveal-on-scroll">
|
||
<h3>
|
||
<svg class="icon"><use href="#i-refresh" /></svg>全量 / 增量会话状态机
|
||
</h3>
|
||
<p>
|
||
只有 OneTalk 明确返回历史结束、有效消息逐条确认、IndexedDB
|
||
无待确认项,才允许提交最新锚点。
|
||
</p>
|
||
<div class="sync-steps">
|
||
<div class="sync-step">
|
||
<span class="step-num">01</span>
|
||
<div>
|
||
<h4>枚举当前账号的全部会话</h4>
|
||
<p>单聊与群聊均纳入;OneTalk 是会话存在性的唯一来源。</p>
|
||
</div>
|
||
</div>
|
||
<div class="sync-step">
|
||
<span class="step-num">02</span>
|
||
<div>
|
||
<h4>读取共享服务端锚点</h4>
|
||
<p>存在锚点走增量追赶;无锚点或找不到锚点时转会话级全量。</p>
|
||
</div>
|
||
</div>
|
||
<div class="sync-step">
|
||
<span class="step-num">03</span>
|
||
<div>
|
||
<h4>每批消息先写入 IndexedDB</h4>
|
||
<p>增量候选在找到旧锚点前保持 <code>awaiting_anchor</code>,不上传、不通知。</p>
|
||
</div>
|
||
</div>
|
||
<div class="sync-step">
|
||
<span class="step-num">04</span>
|
||
<div>
|
||
<h4>按旧到新逐条上传并确认</h4>
|
||
<p>Bright 以精确三元组幂等;每条 ACK 后才能标记本地项已确认。</p>
|
||
</div>
|
||
</div>
|
||
<div class="sync-step">
|
||
<span class="step-num">05</span>
|
||
<div>
|
||
<h4>提交锚点,再通知 Mind</h4>
|
||
<p>全部有效候选确认后推进 <code>latestMessageId</code>,随后推送实时事实。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sync-state">
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg> 锚点未找到 →
|
||
incremental_anchor_not_found → 复用候选与检查点进入当前会话全量
|
||
</div>
|
||
</article>
|
||
<article class="panel sync-panel reveal-on-scroll">
|
||
<div class="label">Session progress / Bright</div>
|
||
<div class="state-map">
|
||
<div class="state-row">
|
||
<span class="state-name">discovered</span>
|
||
<div class="state-bar state-bar--cyan"><span>会话已发现</span></div>
|
||
</div>
|
||
<div class="state-row">
|
||
<span class="state-name">awaiting_anchor</span>
|
||
<div class="state-bar state-bar--orange"><span>本地候选等待边界</span></div>
|
||
</div>
|
||
<div class="state-row">
|
||
<span class="state-name">succeeded</span>
|
||
<div class="state-bar state-bar--lime"><span>明确结束 + 全部确认</span></div>
|
||
</div>
|
||
<div class="state-row">
|
||
<span class="state-name">failed</span>
|
||
<div class="state-bar state-bar--red"><span>游标 / 身份 / 页面失败</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="sync-rule">
|
||
<div class="rule-line">
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span
|
||
><strong>锚点归属:</strong
|
||
><code>channelAccountId + conversationId</code>,不归设备。</span
|
||
>
|
||
</div>
|
||
<div class="rule-line">
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span><strong>单会话隔离:</strong>一个会话失败,不阻塞同账号其它会话。</span>
|
||
</div>
|
||
<div class="rule-line">
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span
|
||
><strong>异常可见:</strong>成功但有异常时标记
|
||
<code>succeeded_with_anomalies</code>。</span
|
||
>
|
||
</div>
|
||
<div class="rule-line">
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span
|
||
><strong>重启可续传:</strong>分页位置、候选和结果都在 IndexedDB
|
||
耐久保存。</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="guardrails">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">06 / GUARDRAILS</div>
|
||
<h2>四条不会被绕过的守则</h2>
|
||
<p class="section-intro">
|
||
它们是跨系统契约的硬边界,接口设计、数据迁移和故障处理都必须服从这些不变量。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="guardrail-grid">
|
||
<article class="panel guardrail reveal-on-scroll">
|
||
<div class="guardrail-icon">
|
||
<svg class="icon"><use href="#i-fingerprint" /></svg>
|
||
</div>
|
||
<h3>不造 ID</h3>
|
||
<p>
|
||
<code>latest-*</code>、<code>hist_*</code>、hash 合成值不得进入
|
||
<code>messageId</code>、去重键或锚点。
|
||
</p>
|
||
</article>
|
||
<article class="panel guardrail reveal-on-scroll">
|
||
<div class="guardrail-icon">
|
||
<svg class="icon"><use href="#i-lock" /></svg>
|
||
</div>
|
||
<h3>不越权</h3>
|
||
<p>
|
||
binding、授权版本与设备接管在连接、操作、heartbeat 中复核;任何不可用都 fail
|
||
closed。
|
||
</p>
|
||
</article>
|
||
<article class="panel guardrail reveal-on-scroll">
|
||
<div class="guardrail-icon">
|
||
<svg class="icon"><use href="#i-database" /></svg>
|
||
</div>
|
||
<h3>不双写</h3>
|
||
<p>
|
||
OneTalk 不进入 Mind 通用 <code>messages</code>、outbox 或
|
||
dispatch;其它渠道保持原路径。
|
||
</p>
|
||
</article>
|
||
<article class="panel guardrail reveal-on-scroll">
|
||
<div class="guardrail-icon">
|
||
<svg class="icon"><use href="#i-alert" /></svg>
|
||
</div>
|
||
<h3>异常可诊断</h3>
|
||
<p>缺字段进入独立 anomaly 表,脱敏保留上下文;异常不能阻塞后续有效消息。</p>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="acceptance">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">07 / ACCEPTANCE</div>
|
||
<h2>从事实到切换,45 条验收线</h2>
|
||
<p class="section-intro">
|
||
下面按交付风险重新分组,保留 PRD 的 AC 编号,方便后续拆分为插件、Bright、Mind
|
||
与集成测试。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="acceptance-head reveal-on-scroll">
|
||
<svg class="icon icon--lg"><use href="#i-check" /></svg>
|
||
<div class="acceptance-meter" aria-label="验收项总量"><span></span></div>
|
||
<strong>45 criteria / 0 skipped</strong>
|
||
</div>
|
||
<div class="acceptance-grid">
|
||
<details class="panel acceptance-group reveal-on-scroll" open>
|
||
<summary>
|
||
<div><strong>发送与消息事实</strong><span>AC01—08 · outcome certainty</span></div>
|
||
</summary>
|
||
<ul class="acceptance-list">
|
||
<li>
|
||
<span class="ac-id">AC01</span
|
||
><span
|
||
>入站与确认发件共用
|
||
<code>onetalk_message</code>,精确范围唯一约束保证幂等。</span
|
||
>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC02</span
|
||
><span>页面事实闭环前不产生消息行,也不显示最终成功。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC03</span
|
||
><span>插件只返回三种终态,只有 <code>confirmed_sent</code> 落库。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC04</span
|
||
><span>并发、人工干扰、延迟或接口假成功均为 <code>delivery_unknown</code>。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC05</span
|
||
><span>未知结果不恢复、不重试,临时气泡允许消失。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC05a</span
|
||
><span
|
||
><code>sendRequestId</code> 仅内存关联,终端响应、超时或断线后不持久化。</span
|
||
>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC05b</span
|
||
><span>迟到的真实发件仍正常入库,但不改写原请求终态。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC06–08</span
|
||
><span>离线、身份不符、页面不存在时零写入;路由、幂等和授权边界精确生效。</span>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
<details class="panel acceptance-group reveal-on-scroll" open>
|
||
<summary>
|
||
<div>
|
||
<strong>数据边界与安全</strong><span>AC09—17 · ownership & security</span>
|
||
</div>
|
||
</summary>
|
||
<ul class="acceptance-list">
|
||
<li>
|
||
<span class="ac-id">AC09</span
|
||
><span>数据库提交后,Mind 页面才通过 WebSocket 看到消息事实。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC10</span
|
||
><span>现有 outbox 和非 OneTalk 机制保持可用,OneTalk 不触碰。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC11–11c</span
|
||
><span
|
||
>Mind 不能写 Bright DB;历史直查 Bright;token 不入
|
||
URL;最小授权视图生效。</span
|
||
>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC12–13</span
|
||
><span
|
||
>OneTalk 不写 Mind 通用 <code>messages</code>,WhatsApp /
|
||
邮件原链路不受影响。</span
|
||
>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC14–14a</span
|
||
><span>缺 request ID 或四字段之一时只记结构化异常,同批与后续继续处理。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC15</span
|
||
><span>类型检查、单元测试、真实 PostgreSQL 迁移、幂等与断线测试通过。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC16–17</span
|
||
><span>历史事实均可追溯到页面重观察;合成 ID 不进入 messageId 或锚点。</span>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
<details class="panel acceptance-group reveal-on-scroll" open>
|
||
<summary>
|
||
<div><strong>同步与 IndexedDB</strong><span>AC18—32 · durable rebuild</span></div>
|
||
</summary>
|
||
<ul class="acceptance-list">
|
||
<li>
|
||
<span class="ac-id">AC18–19</span
|
||
><span>首次初始化枚举当前账号可见单聊、群聊,并为每会话独立记录结果。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC20–22</span
|
||
><span>每批上传前先落 IndexedDB;中断可续传;账号结果不伪装为 ready。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC23–25</span
|
||
><span>异常表耐久、可去重、可处理;扫描继续,结果准确显示异常。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC26–27</span
|
||
><span>明确历史结束且全部确认后才创建/推进锚点;增量从最新向旧方向扫描。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC28–29</span
|
||
><span>找到旧锚点前候选不上传;找到后按旧到新逐条确认,再通知 Mind。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC30</span
|
||
><span>成功增量推进到最新有效 messageId;最新缺 ID 时不推进。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC31–32</span
|
||
><span>锚点找不到转会话级全量,复用候选/检查点,不重复抓取、不丢失。</span>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
<details class="panel acceptance-group reveal-on-scroll" open>
|
||
<summary>
|
||
<div>
|
||
<strong>设备接管与切换</strong><span>AC33—45 · cutover & degradation</span>
|
||
</div>
|
||
</summary>
|
||
<ul class="acceptance-list">
|
||
<li>
|
||
<span class="ac-id">AC33–34</span
|
||
><span>新设备获得共享锚点;旧 binding 原子失效,不产生消息或锚点副本。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC35–36</span
|
||
><span>Bright 只读最小认证视图;同用户不能同时激活第二设备 / 账号。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC37–38</span
|
||
><span>接管期间旧设备迟到回报被拒绝;无账号迁移或隐式归属变更。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC39–40</span
|
||
><span>首条 Bright 消息写入后旧 OneTalk 路径永久禁用;旧插件明确升级错误。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC41–42</span
|
||
><span>Bright 技术进度不含业务字段;Mind 不能创建 Bright 未发现的会话。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC43</span
|
||
><span>找不到联系人时显示原始 senderId / 未关联占位,不自动补资料。</span>
|
||
</li>
|
||
<li>
|
||
<span class="ac-id">AC44–45</span
|
||
><span>插件离线仍可读历史但禁收发;Bright / Mind 接口按故障矩阵独立降级。</span>
|
||
</li>
|
||
</ul>
|
||
</details>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section" id="scope">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="section-index">08 / DECISIONS</div>
|
||
<h2>明确边界,保留真实风险</h2>
|
||
<p class="section-intro">
|
||
本阶段确认架构方向和失败边界;字段、游标、超时、索引、表结构与部署域名留给后续子项目设计。
|
||
</p>
|
||
</div>
|
||
<div class="section-rule"></div>
|
||
</div>
|
||
<div class="scope-grid">
|
||
<article class="panel scope-card scope-card--out reveal-on-scroll">
|
||
<h3>
|
||
<svg class="icon icon--sm"><use href="#i-arrow" /></svg>本阶段不做
|
||
</h3>
|
||
<ul class="scope-list">
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg
|
||
><span>不删除、迁移或改造现有 outbox 历史数据。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg
|
||
><span>不重构 WhatsApp、邮件等非 OneTalk 渠道。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg
|
||
><span>不为未确认发送增加任务表、占位消息、自动恢复或重试。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg
|
||
><span>不把 Mind legacy 消息直接转换成 Bright 新事实。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-alert" /></svg
|
||
><span>不实现 Mind 后台消费者、未读 / 摘要派生与两库消费 cursor。</span>
|
||
</li>
|
||
</ul>
|
||
</article>
|
||
<article class="panel scope-card scope-card--risk reveal-on-scroll">
|
||
<h3>
|
||
<svg class="icon icon--sm"><use href="#i-shield" /></svg>接受的取舍
|
||
</h3>
|
||
<ul class="scope-list">
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span>无法保证恰好一次;未经核对的手动重试可能重复。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span>发送后断线只能标记未知,不自动恢复检查。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span>未确认发送不持久化,刷新后临时气泡可消失。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span>遇到歧义宁可未知,不误报成功。</span>
|
||
</li>
|
||
<li>
|
||
<svg class="icon icon--sm"><use href="#i-check" /></svg
|
||
><span>Mind 原有 OneTalk 关联、摘要、未读、引用需要后续改造。</span>
|
||
</li>
|
||
</ul>
|
||
</article>
|
||
</div>
|
||
<div class="open-question reveal-on-scroll" style="margin-top: 14px">
|
||
<svg class="icon icon--lg"><use href="#i-question" /></svg>
|
||
<div>
|
||
<strong>Q1 · 低层设计留白</strong>
|
||
<p>
|
||
WebSocket 断线补读、字段类型 /
|
||
空值、游标、超时、索引、表结构、部署域名和凭证传输方式,推迟到插件、Bright、Mind
|
||
与集成子项目,不阻塞父级架构确认。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="footer">
|
||
<span>Source: <strong>prd.md</strong> · OneTalk message fact architecture</span>
|
||
<span>Confirmed boundaries / implementation details deferred</span>
|
||
</footer>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
(() => {
|
||
const sidebar = document.getElementById("sidebar");
|
||
const toggle = document.querySelector(".mobile-nav-toggle");
|
||
const navLinks = [...document.querySelectorAll(".side-nav a")];
|
||
const sections = navLinks
|
||
.map((link) => document.querySelector(link.getAttribute("href")))
|
||
.filter(Boolean);
|
||
|
||
toggle?.addEventListener("click", () => {
|
||
const isOpen = sidebar.classList.toggle("is-open");
|
||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||
});
|
||
|
||
navLinks.forEach((link) =>
|
||
link.addEventListener("click", () => {
|
||
sidebar.classList.remove("is-open");
|
||
toggle?.setAttribute("aria-expanded", "false");
|
||
}),
|
||
);
|
||
|
||
const updateActiveLink = () => {
|
||
const marker = window.scrollY + 150;
|
||
let current = sections[0]?.id;
|
||
sections.forEach((section) => {
|
||
if (section.offsetTop <= marker) current = section.id;
|
||
});
|
||
navLinks.forEach((link) =>
|
||
link.classList.toggle("is-active", link.getAttribute("href") === `#${current}`),
|
||
);
|
||
};
|
||
window.addEventListener("scroll", updateActiveLink, { passive: true });
|
||
updateActiveLink();
|
||
|
||
const observer = new IntersectionObserver(
|
||
(entries) => {
|
||
entries.forEach((entry) => {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add("is-visible");
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
},
|
||
{ threshold: 0.12 },
|
||
);
|
||
document
|
||
.querySelectorAll(".reveal-on-scroll")
|
||
.forEach((element) => observer.observe(element));
|
||
|
||
document.querySelectorAll("[data-copy]").forEach((button) => {
|
||
button.addEventListener("click", async () => {
|
||
const value = button.dataset.copy;
|
||
let copied = false;
|
||
try {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(value);
|
||
copied = true;
|
||
}
|
||
} catch (error) {
|
||
copied = false;
|
||
}
|
||
if (!copied) {
|
||
const helper = document.createElement("textarea");
|
||
helper.value = value;
|
||
helper.setAttribute("readonly", "");
|
||
helper.style.position = "fixed";
|
||
helper.style.opacity = "0";
|
||
document.body.appendChild(helper);
|
||
helper.select();
|
||
copied = document.execCommand("copy");
|
||
helper.remove();
|
||
}
|
||
if (copied) {
|
||
const original = button.innerHTML;
|
||
button.innerHTML = '<svg class="icon icon--sm"><use href="#i-check"/></svg>';
|
||
button.setAttribute("aria-label", "已复制");
|
||
setTimeout(() => {
|
||
button.innerHTML = original;
|
||
button.setAttribute("aria-label", "复制幂等键");
|
||
}, 1400);
|
||
} else {
|
||
button.setAttribute("title", value);
|
||
}
|
||
});
|
||
});
|
||
|
||
document.getElementById("printPage")?.addEventListener("click", () => window.print());
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|