Files
trade-message-center/prd.html
T

2794 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 连接 TradeBrightTradeBright 写入 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">AC0608</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">AC1111c</span
><span
>Mind 不能写 Bright DB;历史直查 Brighttoken 不入
URL;最小授权视图生效。</span
>
</li>
<li>
<span class="ac-id">AC1213</span
><span
>OneTalk 不写 Mind 通用 <code>messages</code>WhatsApp /
邮件原链路不受影响。</span
>
</li>
<li>
<span class="ac-id">AC1414a</span
><span>缺 request ID 或四字段之一时只记结构化异常,同批与后续继续处理。</span>
</li>
<li>
<span class="ac-id">AC15</span
><span>类型检查、单元测试、真实 PostgreSQL 迁移、幂等与断线测试通过。</span>
</li>
<li>
<span class="ac-id">AC1617</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">AC1819</span
><span>首次初始化枚举当前账号可见单聊、群聊,并为每会话独立记录结果。</span>
</li>
<li>
<span class="ac-id">AC2022</span
><span>每批上传前先落 IndexedDB;中断可续传;账号结果不伪装为 ready。</span>
</li>
<li>
<span class="ac-id">AC2325</span
><span>异常表耐久、可去重、可处理;扫描继续,结果准确显示异常。</span>
</li>
<li>
<span class="ac-id">AC2627</span
><span>明确历史结束且全部确认后才创建/推进锚点;增量从最新向旧方向扫描。</span>
</li>
<li>
<span class="ac-id">AC2829</span
><span>找到旧锚点前候选不上传;找到后按旧到新逐条确认,再通知 Mind。</span>
</li>
<li>
<span class="ac-id">AC30</span
><span>成功增量推进到最新有效 messageId;最新缺 ID 时不推进。</span>
</li>
<li>
<span class="ac-id">AC3132</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">AC3334</span
><span>新设备获得共享锚点;旧 binding 原子失效,不产生消息或锚点副本。</span>
</li>
<li>
<span class="ac-id">AC3536</span
><span>Bright 只读最小认证视图;同用户不能同时激活第二设备 / 账号。</span>
</li>
<li>
<span class="ac-id">AC3738</span
><span>接管期间旧设备迟到回报被拒绝;无账号迁移或隐式归属变更。</span>
</li>
<li>
<span class="ac-id">AC3940</span
><span>首条 Bright 消息写入后旧 OneTalk 路径永久禁用;旧插件明确升级错误。</span>
</li>
<li>
<span class="ac-id">AC4142</span
><span>Bright 技术进度不含业务字段;Mind 不能创建 Bright 未发现的会话。</span>
</li>
<li>
<span class="ac-id">AC43</span
><span>找不到联系人时显示原始 senderId / 未关联占位,不自动补资料。</span>
</li>
<li>
<span class="ac-id">AC4445</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>