> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kombify.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 设置你的密码管理器应用

> 创建你的保险库账号，保存第一个密码，并把手机、浏览器和电脑上的 Bitwarden 应用连接到你自己的保险库。

export const ClientSetupGuide = ({id, labels, locale = 'en', direction = 'ltr', meta = [], requirements = [], requirementsHelp, appsTitle = labels.appsTitle, vendor, apps = [], appsNote, stepsTitle = labels.stepsTitle, steps = [], help = [], helpTitle = labels.helpTitle, next = [], nextTitle = labels.nextTitle, attribution, sources}) => {
  const Anchor = 'a', Article = 'article', Details = 'details', Dialog = 'dialog', Figcaption = 'figcaption', Figure = 'figure', Footer = 'footer', Img = 'img', Summary = 'summary';
  const number = new Intl.NumberFormat(locale);
  const stepNumber = new Intl.NumberFormat(locale, {
    minimumIntegerDigits: 2
  });
  const message = (key, values = {}) => labels[key].replace(/\{(\w+)\}/g, (_, name) => String(values[name]));
  const count = steps.length;
  const last = count - 1;
  const storageKey = `kombify-client-guide:${id}:v1`;
  const rootRef = useRef(null);
  const zoomRef = useRef(null);
  const toastTimer = useRef(null);
  const focusTab = useRef(false);
  const [current, setCurrent] = useState(0);
  const [completed, setCompleted] = useState(() => steps.map(() => false));
  const [verified, setVerified] = useState(() => steps.map(step => (step.verify || []).map(() => false)));
  const [failed, setFailed] = useState({});
  const [zoom, setZoom] = useState(null);
  const [toast, setToast] = useState('');
  const isExternal = href => (/^https?:\/\//).test(href || '');
  const icon = name => {
    const paths = {
      apple: <path d="M16.7 1.8c0 2.1-1.7 4.4-3.7 4.2-.4-2.1 1.8-4.3 3.7-4.2ZM20.4 17.3c-.5 1.2-.8 1.7-1.5 2.7-1 1.5-2.4 3.3-4.2 3.3-1.6 0-2-1-4.1-.9-2.1 0-2.5 1-4.2.9-1.8-.1-3.1-1.7-4.1-3.2C-.5 15.9-.7 10.4 1 7.6c1.2-2 3.1-3.1 4.9-3.1 1.9 0 3.1 1 4.7 1 1.5 0 2.5-1 4.7-1 1.6 0 3.3.9 4.5 2.3-4 2.2-3.3 8 0 10.5Z" fill="currentColor" stroke="none" transform="translate(2 0) scale(.87)" />,
      android: <><path d="M5 12a7 7 0 0 1 14 0v6H5Zm2-6L5 3m12 3 2-3M2 12v5m20-5v5M8 18v4m8-4v4" /><path d="M8.5 9h.01M15.5 9h.01" /></>,
      browser: <><rect height="16" rx="2" width="20" x="2" y="4" /><path d="M2 9h20M6 6.5h.01M9 6.5h.01" /></>,
      desktop: <><rect height="14" rx="2" width="20" x="2" y="3" /><path d="M8 22h8M12 17v5" /></>,
      web: <><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" /></>,
      server: <><rect height="7" rx="2" width="18" x="3" y="3" /><rect height="7" rx="2" width="18" x="3" y="14" /><path d="M7 6.5h.01M7 17.5h.01M16 6.5h2M16 17.5h2" /></>,
      phone: <><rect height="20" rx="3" width="12" x="6" y="2" /><path d="M10 5h4M11 19h2" /></>,
      steps: <><rect height="7" rx="1.5" width="7" x="3" y="3" /><rect height="7" rx="1.5" width="7" x="14" y="3" /><rect height="7" rx="1.5" width="7" x="3" y="14" /><rect height="7" rx="1.5" width="7" x="14" y="14" /></>,
      book: <path d="M12 5v15M3 4c4-1 6 0 9 1 3-1 5-2 9-1v15c-4-1-6 0-9 1-3-1-5-2-9-1Z" />,
      clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
      print: <><path d="M6 8V3h12v5M6 17H3V8h18v9h-3M6 14h12v7H6Z" /><path d="M17 11h.01" /></>,
      check: <path d="m5 12 4 4L19 6" />,
      arrow: <path d="M5 12h14M13 6l6 6-6 6" />,
      back: <path d="m15 5-7 7 7 7" />,
      external: <><path d="M14 4h6v6m0-6L10 14" /><path d="M10 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-5" /></>,
      info: <><circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 7h.01" /></>,
      ok: <><circle cx="12" cy="12" r="9" /><path d="m8 12 3 3 5-6" /></>,
      camera: <><path d="M3 7h4l2-3h6l2 3h4v13H3Z" /><circle cx="12" cy="13" r="4" /></>,
      zoom: <><circle cx="10.5" cy="10.5" r="6.5" /><path d="m16 16 5 5M7 10.5h7M10.5 7v7" /></>,
      shield: <><path d="M12 3 3 7v6c0 5 9 9 9 9s9-4 9-9V7Z" /><path d="m8 12 3 3 5-6" /></>,
      close: <path d="m6 6 12 12M6 18 18 6" />
    };
    return <svg aria-hidden="true" className="ico" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" viewBox="0 0 24 24">{paths[name] || paths.info}</svg>;
  };
  const safeHref = href => (/^(https:\/\/|\/|#)/).test(href || '') ? href : '#';
  const link = (href, children, className, key) => isExternal(href) ? <Anchor className={className || ''} href={safeHref(href)} key={key} rel="noopener noreferrer" target="_blank">{children} {icon('external')}<span className="sr-only">{labels.opensInNewTab}</span></Anchor> : <Anchor className={className || ''} href={safeHref(href)} key={key} onClick={(href || '').startsWith('#') ? () => {
    zoomRef.current?.close();
    reveal(href.slice(1));
  } : undefined}>{children}</Anchor>;
  const rich = text => {
    if (!text || !text.split) return text;
    return text.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\[[^\]]+\]\([^)\s]+\))/g).map((part, i) => {
      if ((/^\[\[[^\]]+\]\]$/).test(part)) return <span className="key" key={i}>{part.slice(2, -2)}</span>;
      if ((/^\*\*[^*]+\*\*$/).test(part)) return <strong key={i}>{part.slice(2, -2)}</strong>;
      const match = (/^\[([^\]]+)\]\(([^)\s]+)\)$/).exec(part);
      if (match) return link(match[2], match[1], undefined, i);
      return part;
    });
  };
  useEffect(() => {
    try {
      const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
      if (Array.isArray(saved?.completed) && saved.completed.length === count) setCompleted(saved.completed.map(Boolean));
      if (Array.isArray(saved?.verified) && saved.verified.length === count) setVerified(saved.verified.map((list, i) => (steps[i].verify || []).map((_, j) => Boolean(list?.[j]))));
    } catch (_) {}
    const root = rootRef.current;
    const timer = setTimeout(() => {
      if (!root) return;
      const broken = {};
      root.querySelectorAll('.screen-image > img').forEach(image => {
        if (image.complete && !image.naturalWidth) broken[image.getAttribute('src')] = true;
      });
      if (Object.keys(broken).length) setFailed(old => ({
        ...old,
        ...broken
      }));
    }, 7000);
    const openAll = () => root?.querySelectorAll('details').forEach(node => {
      node.open = true;
    });
    window.addEventListener('beforeprint', openAll);
    return () => {
      clearTimeout(timer);
      clearTimeout(toastTimer.current);
      window.removeEventListener('beforeprint', openAll);
    };
  }, []);
  useEffect(() => {
    if (!focusTab.current) return;
    focusTab.current = false;
    rootRef.current?.querySelector(`[id="${id}-tab-${current}"]`)?.focus();
  }, [current]);
  const persist = (nextCompleted, nextVerified) => {
    try {
      localStorage.setItem(storageKey, JSON.stringify({
        completed: nextCompleted,
        verified: nextVerified
      }));
    } catch (_) {}
  };
  const say = text => {
    setToast('');
    setTimeout(() => setToast(text), 60);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(''), 3800);
  };
  const select = (index, focus) => {
    const target = Math.max(0, Math.min(last, index));
    setCurrent(target);
    focusTab.current = Boolean(focus);
  };
  const onNext = () => {
    const checks = verified[current] || [];
    if (checks.length && !checks.every(Boolean)) {
      say(labels.verifyRequired);
      rootRef.current?.querySelector(`[id="${id}-verify-${current}-${checks.indexOf(false)}"]`)?.focus();
      return;
    }
    const nextCompleted = completed.map((done, i) => i === current ? true : done);
    setCompleted(nextCompleted);
    persist(nextCompleted, verified);
    if (current < last) select(current + 1, true); else {
      const done = nextCompleted.filter(Boolean).length;
      say(done === count ? labels.allDone : message('remainingSteps', {
        done: number.format(done),
        count: number.format(count)
      }));
      rootRef.current?.querySelector(`[id="${id}-more"], [id="${id}-help"]`)?.scrollIntoView({
        block: 'start'
      });
    }
  };
  const onVerify = (stepIndex, checkIndex, value) => {
    const nextVerified = verified.map((list, i) => i === stepIndex ? list.map((v, j) => j === checkIndex ? value : v) : list);
    const nextCompleted = completed.map((done, i) => i === stepIndex && !value ? false : done);
    setVerified(nextVerified);
    setCompleted(nextCompleted);
    persist(nextCompleted, nextVerified);
  };
  const onReset = () => {
    const nextCompleted = steps.map(() => false);
    const nextVerified = steps.map(step => (step.verify || []).map(() => false));
    setCompleted(nextCompleted);
    setVerified(nextVerified);
    persist(nextCompleted, nextVerified);
    select(0);
    say(labels.progressReset);
  };
  const onTabKey = event => {
    const moves = {
      ArrowRight: c => (c + 1) % count,
      ArrowLeft: c => (c - 1 + count) % count,
      Home: () => 0,
      End: () => last
    };
    const move = moves[event.key];
    if (!move) return;
    event.preventDefault();
    focusTab.current = true;
    setCurrent(move);
  };
  const openZoom = shot => {
    setZoom(shot);
    const dialog = zoomRef.current;
    if (dialog && !dialog.open) dialog.showModal();
  };
  const closeOnBackdrop = event => {
    const dialog = event.currentTarget;
    if (event.target !== dialog) return;
    const rect = dialog.getBoundingClientRect();
    if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
  };
  const reveal = targetId => {
    const node = rootRef.current?.querySelector(`[id="${targetId}"]`);
    const details = node && (node.tagName === 'DETAILS' ? node : node.querySelector('details'));
    if (details) details.open = true;
  };
  const renderShot = (shot, stepIndex) => {
    const unavailable = failed[shot.src];
    const label = shot.vendor ? labels.vendorScreenshotTag : labels.kombifyScreenshotTag;
    return <>
        <span className="screenshot-tag">{icon('camera')} {label}</span>
        <Figure className={`screen-figure${shot.frame === 'phone' ? ' phone' : ''}${unavailable ? ' image-unavailable' : ''}`}>
          <div className="screen-image">
            <Img alt={shot.alt} className={shot.srcDark ? 'only-light has-dark' : undefined} decoding="async" height={shot.height} loading={stepIndex === 0 ? 'eager' : 'lazy'} onError={() => setFailed(old => ({
      ...old,
      [shot.src]: true
    }))} referrerPolicy="no-referrer" src={shot.src} width={shot.width} />
            {shot.srcDark ? <Img alt={shot.alt} className="only-dark" decoding="async" height={shot.height} loading="lazy" onError={() => setFailed(old => ({
      ...old,
      [shot.src]: true
    }))} src={shot.srcDark} width={shot.width} /> : null}
            {(shot.vendor ? [] : shot.annotations || []).map((mark, i) => <span aria-hidden="true" className="annotation" data-n={mark.n ?? undefined} key={i} style={{
      top: `${mark.top}%`,
      left: `${mark.left}%`,
      width: `${mark.width}%`,
      height: `${mark.height}%`
    }} />)}
            <Anchor className="image-fallback" href={shot.vendor ? shot.creditHref || shot.src : shot.src} rel="noopener noreferrer" target="_blank">{icon('external')} {shot.vendor ? labels.openVendorPage : labels.openScreenshot}<span>{shot.vendor ? labels.vendorImageUnavailable : labels.imageUnavailable}</span></Anchor>
            <button aria-label={message('enlargeScreenshot', {
      description: shot.alt
    })} className="zoom-button" onClick={() => openZoom(shot)} type="button">{icon('zoom')}</button>
          </div>
          <Figcaption>
            {shot.caption}{shot.credit ? <> · {shot.creditHref ? link(shot.creditHref, shot.credit) : shot.credit}</> : null}
            {shot.version || shot.captured ? <span className="meta">{[shot.version, shot.captured ? message('captured', {
      date: shot.captured
    }) : null].filter(Boolean).join(' · ')}</span> : null}
          </Figcaption>
        </Figure>
      </>;
  };
  const renderLinkVisual = visual => <>
      <span className="screenshot-tag">{icon('external')} {visual.tag || labels.vendorGuideTag}</span>
      <div className="link-visual">
        {visual.title ? <strong>{visual.title}</strong> : null}
        {visual.text ? <p>{rich(visual.text)}</p> : null}
        {(visual.links || []).map((item, i) => <Anchor className="vendor-link" href={item.href} key={i} rel="noopener noreferrer" target="_blank">
            {icon(item.icon || 'book')}<span>{item.label}{item.detail ? <small>{item.detail}</small> : null}</span>{icon('external')}<span className="sr-only">{labels.opensInNewTab}</span>
          </Anchor>)}
      </div>
    </>;
  return <div className="kombify-client-guide not-prose" id={id} dir={direction} lang={locale} ref={rootRef} style={{
    '--kcg-steps': count
  }}>
      <div className="guide-content">
        <div className="page-top">
          <div className="intro-meta">{meta.map((item, i) => <span key={i}>{icon(item.icon)} {item.text}</span>)}</div>
          <div className="page-tools"><button aria-label={labels.printGuide} className="tool-button" onClick={() => window.print()} type="button">{icon('print')}</button></div>
        </div>
        {requirements.length ? <div className="ready-strip">
            <strong>{labels.requirementsTitle}</strong>
            {requirements.map((item, i) => <span key={i}>{icon('check')} {rich(item)}</span>)}
            {requirementsHelp ? <Anchor href={`#${requirementsHelp.target}`} onClick={() => reveal(requirementsHelp.target)}>{requirementsHelp.label} {icon('arrow')}</Anchor> : null}
          </div> : null}

        {apps.length ? <section aria-labelledby={`${id}-apps-title`} className="downloads" id={`${id}-apps`}>
            <div className="section-top">
              <h2 id={`${id}-apps-title`}>{appsTitle}</h2>
              {vendor ? link(vendor.href, vendor.label, 'small-link') : null}
            </div>
            <div className="downloads-grid">
              {apps.map((app, i) => <Anchor className="download-card" href={app.href} key={i} rel="noopener noreferrer" target="_blank">
                  <span className="platform-icon">{icon(app.platform)}</span>
                  <span className="download-text">
                    <strong>{app.label}</strong>
                    <small>{app.detail}</small>
                    <span className="store-name">{app.store} {icon('external')}</span>
                    <span className={`badge ${app.official ? 'official' : 'unofficial'}`}>{app.official ? message('officialApp', {
    vendor: app.vendor
  }) : message('thirdPartyApp', {
    vendor: app.vendor
  })}</span>
                  </span>
                  <span className="sr-only">{labels.opensInNewTab}</span>
                </Anchor>)}
            </div>
            {appsNote ? <p className="download-hint">{rich(appsNote)}</p> : null}
          </section> : null}

        <section aria-labelledby={`${id}-guide-title`} className="guide-section" id={`${id}-guide`}>
          <div className="guide-heading"><h2 id={`${id}-guide-title`}>{stepsTitle}</h2></div>
          <div className="wizard">
            <div aria-label={labels.setupSteps} className="step-tabs" onKeyDown={onTabKey} role="tablist">
              {steps.map((step, i) => <button aria-controls={`${id}-panel-${i}`} aria-selected={i === current} className={`step-tab${completed[i] ? ' done' : ''}`} id={`${id}-tab-${i}`} key={i} onClick={() => select(i)} role="tab" tabIndex={i === current ? 0 : -1} type="button">
                  <span className="step-dot">{completed[i] ? '✓' : number.format(i + 1)}</span><span>{step.tab}</span>
                </button>)}
            </div>
            {steps.map((step, i) => <Article aria-labelledby={`${id}-tab-${i}`} className={`guide-panel${i === current ? ' active' : ''}`} id={`${id}-panel-${i}`} key={i} role="tabpanel">
                <div className="panel-copy">
                  <div className="step-kicker">{message('stepPosition', {
    current: stepNumber.format(i + 1),
    count: stepNumber.format(count)
  })}</div>
                  <h3>{step.title}</h3>
                  {step.intro ? <p className="step-intro">{rich(step.intro)}</p> : null}
                  {step.clicks?.length ? <ol className="click-list">{step.clicks.map((click, j) => <li key={j}><span className="click-num">{number.format(j + 1)}</span><span>{rich(click)}</span></li>)}</ol> : null}
                  {step.verify?.length ? <div className="verify-list">
                      {step.verify.map((item, j) => <label key={j}><input checked={Boolean(verified[i]?.[j])} id={`${id}-verify-${i}-${j}`} onChange={event => onVerify(i, j, event.target.checked)} type="checkbox" /><span>{rich(item)}</span></label>)}
                    </div> : null}
                  {step.note ? <div className="micro-note">{icon('info')}<span>{rich(step.note)}</span></div> : null}
                  {(step.faq ? [].concat(step.faq) : []).map((faq, j) => <Details key={j}><Summary>{faq.q}</Summary><p>{rich(faq.a)}</p></Details>)}
                  {step.check ? <div className="step-check">{icon('ok')} {rich(step.check)}</div> : null}
                </div>
                <div className="panel-visual">
                  {step.screenshot ? renderShot(step.screenshot, i) : step.visual ? renderLinkVisual(step.visual) : null}
                </div>
              </Article>)}
            <div className="wizard-bottom">
              <div className="progress-note">
                <span>{message('progress', {
    done: number.format(completed.filter(Boolean).length),
    count: number.format(count)
  })}</span>
                <div aria-hidden="true" className="progress-meter">{steps.map((step, i) => <span className={completed[i] ? 'done' : undefined} key={i} />)}</div>
              </div>
              <button className="prev" disabled={current === 0} onClick={() => select(current - 1, true)} type="button">{icon('back')} {labels.back}</button>
              <button className="next" onClick={onNext} type="button"><span>{current === last ? labels.finish : labels.next}</span>{icon('arrow')}</button>
            </div>
          </div>
          {sources ? <p className="provenance-inline">{sources.summary} <Anchor href={`#${id}-sources`} onClick={() => reveal(`${id}-sources-detail`)}>{labels.imageSources}</Anchor></p> : null}
        </section>

        {next.length ? <section aria-labelledby={`${id}-more-title`} className="more-section" id={`${id}-more`}>
            <div className="section-top"><h2 id={`${id}-more-title`}>{nextTitle}</h2></div>
            <div className="more-grid">
              {next.map((item, i) => link(item.href, <>
                  <span className="more-icon">{icon(item.icon || 'arrow')}</span>
                  <span><strong>{item.title}</strong><small>{item.text}</small></span>
                </>, 'more-card', i))}
            </div>
          </section> : null}

        {help.length ? <section aria-labelledby={`${id}-help-title`} className="faq-section" id={`${id}-help`}>
            <h2 id={`${id}-help-title`}>{helpTitle}</h2>
            {help.map((item, i) => <Details className="faq" id={item.id} key={i}><Summary>{item.q}</Summary><div className="answer">{rich(item.a)}</div></Details>)}
          </section> : null}

        <Footer className="footline">
          <div className="attribution">{attribution ? rich(attribution) : null}</div>
          <button className="reset" onClick={onReset} type="button">{labels.resetProgress}</button>
        </Footer>

        {sources ? <section className="source-section" id={`${id}-sources`}>
            <Details id={`${id}-sources-detail`}>
              <Summary>{labels.sourcesTitle}</Summary>
              <div className="source-content">
                {(sources.paragraphs || []).map((text, i) => <p key={i}>{rich(text)}</p>)}
                <div className="source-links">{(sources.links || []).map((item, i) => link(item.href, item.label, undefined, i))}</div>
              </div>
            </Details>
            <div className="dim">{labels.ownershipNotice}</div>
          </section> : null}
      </div>

      <Dialog aria-labelledby={`${id}-zoom-title`} className="zoom-dialog" onClick={closeOnBackdrop} ref={zoomRef}>
        <div className="dialog-header">
          <h2 id={`${id}-zoom-title`}>{zoom?.vendor ? labels.vendorScreenshot : labels.screenshot}</h2>
          <button aria-label={labels.closeImage} className="dialog-close" onClick={() => zoomRef.current?.close()} type="button">{icon('close')}</button>
        </div>
        <div className="zoom-content">
          {zoom ? <Img alt={zoom.alt} src={zoom.src} /> : null}
          {zoom?.creditHref ? link(zoom.creditHref, zoom.vendor ? labels.openOriginal : labels.aboutScreenshot) : null}
        </div>
      </Dialog>
      <div aria-live="polite" className="sr-only" role="status">{toast}</div>
      <div aria-hidden="true" className={`toast${toast ? ' show' : ''}`}>{toast}</div>
    </div>;
};

export const clientSetupLabelsZhHans = {
  appsTitle: '先下载应用。',
  stepsTitle: '按步骤连接。',
  helpTitle: '卡住了？',
  nextTitle: '接下来做什么。',
  opensInNewTab: '（在新标签页中打开）',
  verifyRequired: '请在你亲自检查后，勾选每一项。',
  allDone: '所有步骤在此浏览器中都已标记为完成。',
  remainingSteps: '{count} 个步骤中有 {done} 个已标记为完成。请打开其他标签页完成剩余步骤。',
  progressReset: '你在此浏览器中的进度已重置。',
  vendorScreenshotTag: '厂商截图',
  kombifyScreenshotTag: 'KOMBIFY 截图',
  openVendorPage: '打开厂商页面',
  openScreenshot: '打开截图',
  vendorImageUnavailable: '加载需要网络连接。',
  imageUnavailable: '图片未能加载。',
  enlargeScreenshot: '放大截图：{description}',
  captured: '截取于 {date}',
  vendorGuideTag: '厂商指南',
  printGuide: '打印指南',
  requirementsTitle: '你需要准备什么',
  officialApp: '官方 · {vendor}',
  thirdPartyApp: '第三方 · {vendor}',
  setupSteps: '设置步骤',
  stepPosition: '第 {current} 步 / 共 {count} 步',
  progress: '{count} 个步骤中已完成 {done} 个',
  back: '返回',
  finish: '完成',
  next: '已完成 · 下一步',
  imageSources: '图片来源与日期',
  resetProgress: '重置进度',
  sourcesTitle: '图片来源、已核对的链接和日期',
  ownershipNotice: '由 kombify 提供的指南 · 产品名称、厂商应用截图和商标归其各自所有者所有。',
  vendorScreenshot: '厂商截图',
  screenshot: '截图',
  closeImage: '关闭图片',
  openOriginal: '在厂商处打开原图',
  aboutScreenshot: '关于此截图'
};

<ClientSetupGuide
  labels={clientSetupLabelsZhHans}
  locale="zh-Hans"
  id="kcg-passwords"
  meta={[
{ icon: "phone", text: "手机、浏览器和电脑" },
{ icon: "steps", text: "6 个简短步骤" },
{ icon: "book", text: "无需任何基础" },
]}
  requirements={[
"你的保险库地址，例如 https://vault.home.test",
"发送到你邮箱地址的邀请",
"每台设备都已由设置 kombify 的人为你的家庭保险库准备好",
"一个你能记住的主密码",
]}
  requirementsHelp={{ label: "还没有邀请？", target: "kcg-passwords-no-invite" }}
  appsTitle="先下载 Bitwarden 应用。"
  vendor={{ label: "Bitwarden 的全部下载", href: "https://bitwarden.com/download/" }}
  apps={[
{ platform: "apple", label: "iPhone 和 iPad", detail: "Bitwarden Password Manager", store: "在 App Store 中打开", href: "https://apps.apple.com/app/bitwarden-password-manager/id1137397744", vendor: "Bitwarden", official: true },
{ platform: "android", label: "Android", detail: "Bitwarden Password Manager", store: "在 Google Play 中打开", href: "https://play.google.com/store/apps/details?id=com.x8bit.bitwarden", vendor: "Bitwarden", official: true },
{ platform: "browser", label: "浏览器扩展", detail: "Chrome、Firefox、Edge、Safari 等", store: "打开 Bitwarden 下载页面", href: "https://bitwarden.com/download/", vendor: "Bitwarden", official: true },
{ platform: "desktop", label: "电脑", detail: "Windows、macOS 和 Linux", store: "打开 Bitwarden 下载页面", href: "https://bitwarden.com/download/", vendor: "Bitwarden", official: true },
]}
  appsNote="浏览器商店：[Chrome](https://chromewebstore.google.com/detail/bitwarden-password-manager/nngceckbapebfimnlniiiahkandclblb) · [Firefox](https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/) · [Edge](https://microsoftedge.microsoft.com/addons/detail/bitwarden-password-manager/jbkfoedolllekgbhcbcoahefnbanhhlh) · [Safari（Mac App Store）](https://apps.apple.com/app/bitwarden/id1352778147)。你的保险库运行在 Vaultwarden 上，它可以配合这些官方 Bitwarden 应用使用。你不需要单独的 Vaultwarden 应用。"
  stepsTitle="按步骤设置你的保险库。"
  steps={[
{
  tab: "打开保险库",
  title: "在浏览器中打开你的保险库。",
  intro: "先从电脑开始。你的保险库有自己的网址，就像一个只有你们家庭使用的网站。",
  clicks: [
    "在浏览器的地址栏中输入你的保险库地址，例如 **https://vault.home.test**，然后按 Enter。",
    "在登录框下方点击 [[Create account]]。",
  ],
  note: "本指南中的地址只是示例。请使用设置 kombify 的人提供给你的地址。",
  faq: { q: "什么是保险库地址？", a: "它是你们家庭密码保险库的网址。通常以 **https://vault.** 开头，以你家庭网络的名称结尾。设置 kombify 的人可以告诉你确切的地址。" },
  check: "你看到 [[Create account]] 表单。",
  screenshot: { src: "/images/guides/passwords/vaultwarden/web-01-open-vault.webp", width: 1088, height: 830, alt: "Vaultwarden 登录页面，标出了 Create account 链接", caption: "保险库登录页面", credit: "kombify 截图", creditHref: "#kcg-passwords-sources", version: "Vaultwarden 1.37.2 · web vault 2026.7.0", captured: "2026-09-24", annotations: [{ n: 2, top: 84.3, left: 38.3, width: 23.5, height: 7.2 }] },
},
{
  tab: "创建账号",
  title: "创建你的账号。",
  intro: "你的保险库只接受受邀的邮箱地址。请使用收到邀请的同一个地址。",
  clicks: [
    "在 [[Email address]] 中输入你受邀的邮箱地址。",
    "在 [[Name]] 中输入你的名字。运营保险库的人以及你与之共享密码的人都可以看到它。",
    "点击 [[Continue]]。",
  ],
  faq: { q: "这是我的 kombify 账号吗？", a: "不是。这个账号只属于你的密码保险库。它有自己的邮箱地址和自己的主密码。" },
  check: "页面现在要求你 [[Set a strong password]]。",
  screenshot: { src: "/images/guides/passwords/vaultwarden/web-02-create-account.webp", width: 1088, height: 718, alt: "创建账号表单，邮箱地址为 alex@example.com，名字为 Alex Example", caption: "创建账号", credit: "kombify 截图", creditHref: "#kcg-passwords-sources", version: "Vaultwarden 1.37.2 · web vault 2026.7.0", captured: "2026-09-24", annotations: [{ n: 1, top: 37.6, left: 16.4, width: 65.1, height: 13.4 }, { n: 2, top: 61, left: 16.4, width: 65.1, height: 13.4 }, { n: 3, top: 76.3, left: 14, width: 72.1, height: 13.9 }] },
},
{
  tab: "主密码",
  title: "选择你的主密码。",
  intro: "主密码是你仍然需要记住的唯一一个密码。它可以解锁所有其他密码。",
  clicks: [
    "在 [[Master password]] 中输入一个较长的密码。由四个或更多词组成的一句短话效果很好。",
    "在 [[Confirm master password]] 中再次输入相同的密码。",
    "点击 [[Create account]]。如果页面建议安装浏览器扩展，请点击 [[Add it later]]，然后点击 [[Skip to web app]]。你会在第 5 步连接扩展。",
  ],
  note: "**忘记的主密码无人能够找回**，运营你保险库的人也不行。请把它写下来，并把纸条保存在保险库之外的安全地方。",
  faq: { q: "强度条是什么意思？", a: "它估计你的密码有多难被猜到。请争取达到 **Strong**。长一些比复杂更好。" },
  check: "你的空保险库会打开，标题为 [[All vaults]]。",
  screenshot: { src: "/images/guides/passwords/vaultwarden/web-03-master-password.webp", width: 1088, height: 1310, alt: "Set a strong password 页面，主密码输入了两次，强度评级为 Strong", caption: "设置强密码", credit: "kombify 截图", creditHref: "#kcg-passwords-sources", version: "Vaultwarden 1.37.2 · web vault 2026.7.0", captured: "2026-09-24", annotations: [{ n: 1, top: 25.5, left: 16.4, width: 60.7, height: 7.3 }, { n: 2, top: 52.2, left: 16.4, width: 60.7, height: 7.3 }, { n: 3, top: 87, left: 14, width: 72.1, height: 7.6 }] },
},
{
  tab: "第一个密码",
  title: "保存你的第一个密码。",
  intro: "保存一条登录信息作为测试。之后它可以证明你的应用已连接到你的保险库。",
  clicks: [
    "在右上角点击 [[New]]，然后选择 [[Login]]。如果先出现欢迎窗口，请点击 [[Skip]]。",
    "在 [[Item name]] 中输入一个你认得的名称，例如某个商店的名称。",
    "输入你的 [[Username]]。在 [[Password]] 旁点击圆形箭头生成一个密码，然后点击 [[Use this password]]。",
    "点击 [[Save]]。",
  ],
  note: "在 [[Website (URI)]] 下添加该商店的网址。之后浏览器扩展就会在该网站上建议使用这条登录信息。",
  check: "出现消息 [[Item added]]，并且你的登录信息显示在列表中。",
  screenshot: { src: "/images/guides/passwords/vaultwarden/web-04-new-login.webp", width: 1472, height: 1664, alt: "New Login 窗口，条目名称为 Example Shop，含用户名和生成的密码", caption: "New Login", credit: "kombify 截图", creditHref: "#kcg-passwords-sources", version: "Vaultwarden 1.37.2 · web vault 2026.7.0", captured: "2026-09-24", annotations: [{ n: 2, top: 12.1, left: 7.9, width: 82.6, height: 5.8 }, { n: 3, top: 41.8, left: 7.9, width: 79.3, height: 5.8 }, { n: 3, top: 52.3, left: 87.5, width: 5.7, height: 5 }, { n: 4, top: 92.1, left: 2.7, width: 10.4, height: 6 }] },
},
{
  tab: "连接应用",
  title: "连接 Bitwarden 应用。",
  intro: "每个 Bitwarden 应用起初都会连接到 Bitwarden 自己的服务器。请告诉它一次，改用你的保险库。",
  clicks: [
    "从上面的列表安装 Bitwarden 应用并打开它。",
    "在登录界面中，打开 [[Logging in on]] 菜单并选择 [[Self-hosted]]。在电脑应用中，该菜单叫作 [[Accessing]]。",
    "在 [[Server URL]] 中输入你的保险库地址，开头带上 **https://**，然后点按 [[Save]]。",
    "使用你的邮箱地址和主密码登录。",
  ],
  note: "请在你使用的每部手机、每个浏览器和每台电脑上重复这些步骤。每一个都需要输入一次你的保险库地址。",
  faq: { q: "为什么这里没有应用的图片？", a: "Bitwarden 的条款没有授权复用其图片。它自己的指南展示了每一个界面，上面的标签与之一致。" },
  check: "应用显示你在第 4 步保存的登录信息。",
  visual: {
    tag: "BITWARDEN 指南",
    title: "Bitwarden 展示了每个界面",
    text: "在 Bitwarden 的指南中选择你的设备。请查找关于**自托管**服务器的部分。",
    links: [
      { label: "将应用连接到你自己的服务器", detail: "bitwarden.com · 所有设备", href: "https://bitwarden.com/help/change-client-environment/", icon: "server" },
      { label: "浏览器扩展入门", detail: "bitwarden.com", href: "https://bitwarden.com/help/getting-started-browserext/", icon: "browser" },
      { label: "移动应用入门", detail: "bitwarden.com", href: "https://bitwarden.com/help/getting-started-mobile/", icon: "phone" },
    ],
  },
},
{
  tab: "检查",
  title: "一切都连接好了吗？",
  intro: "在网页版保险库和一个应用中检查。只勾选你亲自检查过的项目。",
  verify: [
    "我可以打开我的保险库地址并登录。",
    "我保存的登录信息显示在我手机、浏览器或电脑上的 Bitwarden 应用中。",
    "我的主密码已写下来，并保存在保险库之外。",
  ],
  note: "本指南不会读取你的保险库。你的勾选只保存在此浏览器中。",
  check: "现在你可以把其他密码迁移到保险库中了。",
  screenshot: { src: "/images/guides/passwords/vaultwarden/web-05-first-item.webp", width: 1430, height: 300, alt: "All vaults 列表，显示已保存的 Example Shop 登录信息", caption: "保险库中的第一条登录信息", credit: "kombify 截图", creditHref: "#kcg-passwords-sources", version: "Vaultwarden 1.37.2 · web vault 2026.7.0", captured: "2026-09-24", annotations: [{ n: null, top: 37.2, left: 1, width: 98, height: 58.1 }] },
},
]}
  nextTitle="接下来做什么。"
  next={[
{ icon: "shield", title: "开启两步验证登录", text: "Bitwarden 介绍了如何添加第二道登录步骤。", href: "https://bitwarden.com/help/setup-two-step-login/" },
{ icon: "server", title: "备份保险库", text: "面向运营 kombify 的人：保护保险库数据。", href: "/zh-Hans/guides/stackkits/back-up-and-restore" },
]}
  help={[
{ id: "kcg-passwords-no-invite", q: "我还没有保险库地址或邀请。", a: "请询问你们家庭中设置 kombify 的人。他们会添加 Passwords 服务，并邀请你的邮箱地址。[Passwords 如何与 kombify 配合](/zh-Hans/guides/stackkits/use-cases/passwords) · [所有者设置](/zh-Hans/stackkits/apps/vaultwarden)。" },
{ q: "创建账号时提示“Registration not allowed or user already exists”。", a: "请检查你使用的是否恰好是受邀的邮箱地址。如果你已经创建过账号，请返回并直接登录。否则，请索取新的邀请。" },
{ q: "地址打不开，或者浏览器对连接发出警告。", a: "请先在你家庭网络中的设备上尝试该地址。如果你的保险库只在家里运行，请连接你的家庭 Wi-Fi，或你拿到的家庭连接应用。每台设备还必须设置为信任你的家庭服务器；这由设置 kombify 的人完成。如果浏览器提示连接不是私密连接，请到此为止并询问他们。不要点击跳过警告。[关于家庭地址](/zh-Hans/guides/stackkits/use-cases/network)。" },
{ q: "我忘记了主密码。", a: "没有人能够找回它。你的保险库是用它加密的。运营你保险库的人可以删除你的账号，让你重新开始，但保存在其中的密码会丢失。[Bitwarden 关于主密码](https://bitwarden.com/help/master-password/)。" },
{ q: "我的 Android 手机没有 Google Play。", a: "Bitwarden 运营着自己的 F-Droid 仓库。地址列在 [Bitwarden 下载页面](https://bitwarden.com/download/)上。" },
{ q: "为什么既提到 Vaultwarden 又提到 Bitwarden？", a: "Vaultwarden 是运行你的保险库的程序。它是一个非官方的服务器，可与官方 Bitwarden 应用配合使用。Vaultwarden 与 Bitwarden, Inc. 没有关联。图片展示的是英文版 Vaultwarden 1.37.2；较新的版本和其他语言可能看起来不同。" },
]}
  attribution="**Vaultwarden 运行你的保险库，Bitwarden 应用打开它。kombify 为你安装它。** kombify 并不制作这两个应用。[Vaultwarden](https://github.com/dani-garcia/vaultwarden)（AGPL-3.0）· 网页版保险库：[为 Vaultwarden 构建的 Bitwarden 网页客户端](https://github.com/dani-garcia/bw_web_builds)（GPL-3.0）· [Bitwarden](https://bitwarden.com/)"
  sources={{
summary: "kombify 从本地 Vaultwarden 安装中截取的截图 · 虚构账号 · 英文菜单。",
paragraphs: [
  "**截图 · 2026 年 9 月 24 日。** kombify 从本地参考安装中截取了网页版保险库的图片：Vaultwarden 1.37.2，即 StackKits 目录为 Passwords 提供的镜像，附带其内置的 web vault 2026.7.0（Bitwarden 网页客户端，GPL-3.0）。该安装使用示例地址 https://vault.home.test 和虚构账号 alex@example.com。它是参考安装，而不是 StackKits 部署测试。本指南添加了紫色标记。",
  "此处未展示 Bitwarden 应用的界面。[Bitwarden 的条款](https://bitwarden.com/terms/)没有授权复用其内容，因此第 5 步链接到 Bitwarden 自己的指南。",
  "商店链接和帮助页面于 2026 年 9 月 24 日核对。你的进度只保存在此浏览器中。本指南不会连接任何账号，也不会索要任何密码。",
],
links: [
  { label: "Bitwarden：连接各个应用", href: "https://bitwarden.com/help/change-client-environment/" },
  { label: "Bitwarden 下载", href: "https://bitwarden.com/download/" },
  { label: "Vaultwarden 项目", href: "https://github.com/dani-garcia/vaultwarden" },
],
}}
/>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.