普通视图

发现新文章,点击刷新页面。
昨天以前一个工匠

AI 发展临界点 - 快上车

作者 海驴
2026年2月8日 13:32
<p>最近做了 Coding Copilot 的一些分享,这里脱敏 + 剪裁后做下分享。核心内容:</p><ol><li>当前这一刻是进入 AI 的最后时间<ul><li>AI Transformers 基建能力已经被榨干,当前这一刻 AI 能力达到巅峰,且将持续很久。</li><li>之前进入的,与现在进入的,没有差距 (AI 内核基建快速迭代、AI ABI 不稳定、没有现象级产品)。</li><li>现在进入的,与往后进入的,存在代沟 (应用层推进、协议层被 AI 重塑、时代变化飞跃)。</li></ul></li><li>Vibe 辅助协作式 已经非常成熟, 完全托管方式也已经成型。</li><li>所有 AI CLI / App 的工作方式都是一致的 (普适性),通过 Prompts 做任务编排。</li><li>人类和 AI 的交互目前已经被限制在 Prompts 里,这有可能限制 AI 多年的发展。<ul><li>Thinking 可以提升质量,无法规避用户原始 prompts 的输入。</li><li>Prompt 提示词在往后相当长的时间里,都是 AI 与人交互的唯一方式。</li></ul></li></ol><h2 id="ToC"><a href="#ToC" class="headerlink" title="ToC"></a>ToC</h2><!-- prettier-ignore --><table><thead><tr><th>1. 为什么现在是拥抱 AI 的最好时刻</th><th>2. Anthropic 产品矩阵</th><th>3. Vibe Coding: Copilot &amp; Autopilot</th></tr></thead><tbody><tr><td>4. AI CLI 工作方式:任务编排 (Prompts)</td><td>5. Prompts e.g.:Legal Skill</td><td>6. Prompts e.g.:One Prompt, One App</td></tr></tbody></table><span id="more"></span><h2 id="缩略图预览"><a href="#缩略图预览" class="headerlink" title="缩略图预览"></a>缩略图预览</h2><!-- prettier-ignore --><table><thead><tr><th><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140357438.png" alt="拥抱 AI 最好的时刻"></th><th><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140500166.png" alt="Anthropic 产品矩阵"></th><th><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140618965.png" alt="Vibe Coding 的两种方式"></th></tr></thead><tbody><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140720340.png" alt="C-C 工作方式(普适性)"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140745216.png" alt="Prompts 与 AI 沟通:Legal Skill"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140819771.png" alt="Prompts 与 AI 沟通:One Prompt, One App"></td></tr></tbody></table><h2 id="大图预览"><a href="#大图预览" class="headerlink" title="大图预览"></a>大图预览</h2><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140357438.png" alt="拥抱 AI 最好的时刻"></p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140500166.png" alt="Anthropic 产品矩阵"></p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140618965.png" alt="Vibe Coding: Copilot &amp; Autopilot"></p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140720340.png" alt="AI CLI 工作方式:任务编排(Prompts)"></p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140745216.png" alt="Prompts e.g.:Legal Skill"></p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20260306140819771.png" alt="Prompts e.g.:One Prompt, One App"></p>

流式响应(Chat):HTTP 响应体字节流的读取与解析

作者 海驴
2025年12月29日 20:02
<blockquote><p>主要通过 web fetch api 的 <code>ReadableStream</code> 能力,解释 HTTP 通道中的流式响应。<br>流式关键在于 HTTP 协议层解析 Body 体,和 TCP 粘包和拆包的处理类似。各种编程语言都具有 body 二进制数据流的拦截和解析能力。</p></blockquote><h2 id="Fetch-API-速览"><a href="#Fetch-API-速览" class="headerlink" title="Fetch API 速览"></a>Fetch API 速览</h2><ul><li><code>await fetch(url)</code> → 返回 <code>Response</code> 对象:这时浏览器通常已经从底层连接(TCP / QUIC)里拿到并解析完 <strong>HTTP 响应行(status)+ 响应头(headers)</strong>;但 <strong>响应体(body)</strong> 还没被消费 / 解析(它会通过单独的流式接口暴露出来,供上层代码增量读取)。</li><li><code>await res.json()</code> / <code>await res.text()</code> → <strong>一次性读取并解析完整 Body</strong>,适用于非流式场景。</li><li><code>res.body</code> → <code>ReadableStream&lt;Uint8Array&gt;</code>,<strong>字节流接口</strong>,可增量消费。</li><li><code>res.body.getReader().read()</code> → 手动 Pull 模式,每次返回 <code>{ value: Uint8Array, done: boolean }</code>。</li></ul><p>可以这样理解:<code>fetch()</code> 先把 <strong>status/headers</strong> 拿到手并封装成 <code>Response</code>;至于 <strong>body 字节</strong> 怎么读、怎么解析,由调用方选择:</p><ul><li>通过 <code>ReadableStream</code> 增量读取(适合 AI token、NDJSON、SSE 等流式场景)。</li><li>或调用 <code>json()</code> / <code>text()</code> 这类封装好的方法,让它们内部把 body 全部读完后再一次性解析。</li></ul><p><strong>核心区别</strong>:<code>res.json()</code> 等 “便捷方法” 会等待整个响应体下载完毕后一次性解析;<code>reader.read()</code> 则支持<strong>边接收边处理</strong>,是流式读取的基础。</p><span id="more"></span><figure class="highlight ts"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 非流式:等 Body 全部接收完再解析</span></span><br><span class="line"><span class="keyword">const</span> full = <span class="title function_">await</span> (<span class="keyword">await</span> <span class="title function_">fetch</span>(url)).<span class="title function_">json</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 流式:边接收边处理(Chunk 边界不等于消息边界)</span></span><br><span class="line"><span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(url);</span><br><span class="line"><span class="keyword">const</span> reader = res.<span class="property">body</span>!.<span class="title function_">getReader</span>();</span><br><span class="line"><span class="keyword">const</span> decoder = <span class="keyword">new</span> <span class="title class_">TextDecoder</span>(<span class="string">"utf-8"</span>);</span><br><span class="line"><span class="keyword">for</span> (;;) {</span><br><span class="line"> <span class="keyword">const</span> { value, done } = <span class="keyword">await</span> reader.<span class="title function_">read</span>();</span><br><span class="line"> <span class="keyword">if</span> (done) <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">const</span> chunkText = decoder.<span class="title function_">decode</span>(value, { <span class="attr">stream</span>: <span class="literal">true</span> });</span><br><span class="line"> <span class="comment">// handle(chunkText)</span></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="SSE-与-Fetch"><a href="#SSE-与-Fetch" class="headerlink" title="SSE 与 Fetch"></a>SSE 与 Fetch</h2><p>很多 “Chat 流式输出” 看起来像 Server-Sent Events (SSE),但底层实现常见就两类:</p><ul><li><strong>SSE(协议 + API)</strong>:<code>text/event-stream</code> + <code>EventSource</code>(浏览器负责按协议分帧并提供重连等语义)。参考 <a href="https://www.yigegongjiang.com/2024/sse/">SSE 指南</a>。</li><li><strong>Fetch 响应体流式读取(传输能力 + 自定义分帧)</strong>:<code>fetch()</code> + <code>Response.body</code>(拿到的是 <code>ReadableStream&lt;Uint8Array&gt;</code>,需要自己定义消息边界与语义,如 NDJSON / 长度前缀等)。</li><li>顺带一提:很多实现也在从 SSE/<code>EventSource</code> 转向 <code>fetch</code> stream,因为 SSE 本身有不少限制(比如只能 GET、Header / 鉴权不够灵活等)。</li></ul><p>共同点:两者都依赖 “长连接 + 增量写入 + 及时 Flush”,最后在客户端看起来都是 <strong>HTTP 响应体字节流</strong>。</p><p>一句话区别:<strong>SSE 把 “怎么切消息 + 事件语义” 都标准化了;Fetch 流式读取只把字节暴露给应用层,剩下由应用协议来定。</strong></p><ul><li><strong>分帧</strong>:SSE 固定是 <code>data:</code> 行 + 空行;Fetch Stream 的边界完全自定义(NDJSON、长度前缀、分隔符等)。</li><li><strong>语义</strong>:SSE 浏览器内建重连、<code>Last-Event-ID</code>;Fetch Stream 想要重连/断点续传/错误语义,需要在应用层设计。</li><li><strong>适用场景</strong>:SSE 更像 “标准事件流”;而 Chat 经常需要 POST、鉴权 Header、以及自定义协议时,Fetch Stream 会更顺手。</li></ul><h2 id="链路传输(从服务端-write-到客户端拿到-chunk)"><a href="#链路传输(从服务端-write-到客户端拿到-chunk)" class="headerlink" title="链路传输(从服务端 write 到客户端拿到 chunk)"></a>链路传输(从服务端 write 到客户端拿到 chunk)</h2><p>想要真的 “边推边显示”,关键往往不是 HTTP 语法本身,而是:<strong>字节在链路的哪一段被缓冲住了</strong>。</p><ol><li><strong>服务端应用写出</strong>:应用调用 <code>write()</code>/<code>send()</code> 将字节写入 Socket。若仅写入用户态缓冲而未执行 <code>flush</code>(或被框架 / 中间件缓冲),客户端将无法接收增量数据。</li><li><strong>TCP Socket 缓冲区(发送 / 接收)</strong>:<code>send()</code> 通常只是把数据拷贝进 <strong>TCP 发送缓冲区</strong>,真正 “发出去” 要看 TCP 栈怎么分段、流控 / 拥塞控制允不允许。结果就是:<strong>应用 write 的粒度</strong>,基本不等于 <strong>对端 read 到的粒度</strong>(还会受 Nagle/延迟 ACK/cwnd/rwnd 等影响)。</li><li><strong>HTTP 承载方式</strong>:<ul><li><strong>HTTP/1.1</strong>:通常使用分块传输编码(Chunked Transfer Encoding)或在未知 <code>Content-Length</code> 时持续写入。</li><li><strong>HTTP/2 / HTTP/3</strong>:基于 DATA Frame 或 QUIC Stream 持续传输,受多路复用与流控机制影响。</li></ul></li><li><strong>浏览器网络栈 → ReadableStream</strong>:浏览器将 “已到达且可用” 的字节推入 <code>ReadableStream</code> 内部队列,JavaScript 通过 <code>reader.read()</code> 或 <code>pipeThrough()</code> 以 Pull 模式消费。</li></ol><ul><li><strong>Chunk 边界 ≠ 消息边界</strong>:一次 <code>read()</code> 获取的 <code>Uint8Array</code> 仅是当前可用的字节片段,可能截断在任意位置(如 UTF-8 字符中间、JSON 结构中间或自定义帧头中间)。</li><li><strong>全链路缓冲会 “假装不流式”</strong>:应用层 Flush、反向代理 Buffering、压缩器缓冲、CDN 策略、浏览器内部队列…… 任何一段在攒数据,都会让 Token 看起来变成 “凑一批才到”。</li></ul><h2 id="Fetch-流式读取的基本模型"><a href="#Fetch-流式读取的基本模型" class="headerlink" title="Fetch 流式读取的基本模型"></a>Fetch 流式读取的基本模型</h2><p><code>fetch()</code> 返回的 <code>Response</code> 对象包含 <code>body</code> 属性,其类型为 <code>ReadableStream&lt;Uint8Array&gt;</code>。消费方式主要有两种:</p><ol><li><strong>手动读取</strong>:获取 <code>reader</code> 并循环调用 <code>read()</code>。</li><li><strong>管道处理</strong>:使用 <code>pipeThrough()</code> 构建解码、分帧、解析的流水线(推荐)。</li></ol><p>整体可以当成 <strong>pull 模式</strong>:每次 <code>read()</code> 拿到的是 “目前已经到手的那点字节”。如果处理速度慢于网络进入速度,队列就会堆起来,进而触发 <strong>背压(Backpressure)</strong>(后续传输会被放慢)。</p><h2 id="示例:解析消息流"><a href="#示例:解析消息流" class="headerlink" title="示例:解析消息流"></a>示例:解析消息流</h2><h3 id="字节解码:处理增量-UTF-8"><a href="#字节解码:处理增量-UTF-8" class="headerlink" title="字节解码:处理增量 UTF-8"></a>字节解码:处理增量 UTF-8</h3><p>网络传输交付的是 <code>Uint8Array</code>,而 Chat 最终要的是文本 Token。注意 UTF-8 是变长编码,一个字符可能被拆到两个 Chunk 里;如果直接 <code>decoder.decode(chunk)</code>(默认非流式),边界处就可能乱码 / 丢字。这里要用增量解码:</p><ul><li>使用 <code>TextDecoder</code> 的 <code>{ stream: true }</code> 选项。</li><li>或使用 <code>TextDecoderStream</code> 管道:<code>response.body.pipeThrough(new TextDecoderStream())</code>,直接获得 <code>ReadableStream&lt;string&gt;</code>。</li></ul><h3 id="分帧策略:定义消息边界"><a href="#分帧策略:定义消息边界" class="headerlink" title="分帧策略:定义消息边界"></a>分帧策略:定义消息边界</h3><p>想做到 “边接收边渲染”,需要先定一个能增量解析的分帧(Framing)规则:到底每条消息怎么切出来?</p><ol><li><p><strong>NDJSON / JSON Lines(推荐)</strong></p><ul><li>格式:每条消息占一行,如 <code>{"type":"delta","text":"..."}\n</code>。</li><li>优点:解析简单,调试友好,兼容 <code>JSON.parse</code>。</li><li>注意:需确保 Payload 内无未转义的换行符(标准 JSON 字符串会将换行编码为 <code>\n</code>,通常安全)。</li></ul></li><li><p><strong>分隔符协议</strong></p><ul><li>格式:使用自定义分隔符(如 <code>\n\n</code> 或特定 Boundary)切分消息。</li><li>风险:若 Payload 包含分隔符,需进行转义或设计复杂的 Boundary 机制。</li></ul></li><li><p><strong>长度前缀(二进制 Framing)</strong></p><ul><li>格式:<code>[Length][Payload]...</code>。</li><li>优点:对任意二进制或文本内容安全,不受内容字符影响。</li><li>缺点:实现复杂度较高,需维护字节级状态机。</li></ul></li></ol><p>一般优先选 <strong>NDJSON</strong> 或 <strong>长度前缀</strong>;千万别指望 Chunk 边界刚好对齐业务消息边界。</p><h3 id="Fetch-NDJSON"><a href="#Fetch-NDJSON" class="headerlink" title="Fetch + NDJSON"></a>Fetch + NDJSON</h3><p><strong>字节读取 → 文本解码 → 行分帧 → JSON 解析 → UI 更新</strong> 的完整流程:</p><figure class="highlight typescript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">ChatChunk</span> = { <span class="attr">type</span>: <span class="string">"delta"</span>; <span class="attr">text</span>: <span class="built_in">string</span> } | { <span class="attr">type</span>: <span class="string">"done"</span> } | { <span class="attr">type</span>: <span class="string">"error"</span>; <span class="attr">message</span>: <span class="built_in">string</span> };</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">streamChat</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="attr">input</span>: { prompt: <span class="built_in">string</span> },</span></span><br><span class="line"><span class="params"> <span class="attr">onChunk</span>: (c: ChatChunk) =&gt; <span class="built_in">void</span>,</span></span><br><span class="line"><span class="params"> <span class="attr">signal</span>?: <span class="title class_">AbortSignal</span>,</span></span><br><span class="line"><span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">fetch</span>(<span class="string">"/api/chat"</span>, {</span><br><span class="line"> <span class="attr">method</span>: <span class="string">"POST"</span>,</span><br><span class="line"> <span class="attr">headers</span>: { <span class="string">"Content-Type"</span>: <span class="string">"application/json"</span> },</span><br><span class="line"> <span class="attr">body</span>: <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(input),</span><br><span class="line"> signal,</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!res.<span class="property">ok</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`HTTP <span class="subst">${res.status}</span>`</span>);</span><br><span class="line"> <span class="keyword">if</span> (!res.<span class="property">body</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">"ReadableStream not supported"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 1. 字节读取层:获取 ReadableStream reader</span></span><br><span class="line"> <span class="keyword">const</span> reader = res.<span class="property">body</span>.<span class="title function_">getReader</span>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2. 文本解码层:处理 UTF-8 边界</span></span><br><span class="line"> <span class="keyword">const</span> decoder = <span class="keyword">new</span> <span class="title class_">TextDecoder</span>(<span class="string">"utf-8"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3. 行分帧层:缓冲未闭合的行</span></span><br><span class="line"> <span class="keyword">let</span> lineBuffer = <span class="string">""</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="comment">// 读取下一批字节(Chunk 边界随机)</span></span><br><span class="line"> <span class="keyword">const</span> { value, done } = <span class="keyword">await</span> reader.<span class="title function_">read</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (done) {</span><br><span class="line"> <span class="comment">// 流结束,处理缓冲区剩余内容</span></span><br><span class="line"> <span class="keyword">if</span> (lineBuffer.<span class="title function_">trim</span>()) {</span><br><span class="line"> <span class="keyword">const</span> msg = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(lineBuffer.<span class="title function_">trim</span>()) <span class="keyword">as</span> <span class="title class_">ChatChunk</span>;</span><br><span class="line"> <span class="title function_">onChunk</span>(msg);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 增量解码:stream: true 保留不完整字节序列</span></span><br><span class="line"> <span class="keyword">const</span> text = decoder.<span class="title function_">decode</span>(value, { <span class="attr">stream</span>: <span class="literal">true</span> });</span><br><span class="line"> lineBuffer += text;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 按换行符切分</span></span><br><span class="line"> <span class="keyword">const</span> lines = lineBuffer.<span class="title function_">split</span>(<span class="string">"\n"</span>);</span><br><span class="line"> <span class="comment">// 最后一个元素可能是不完整行,留待下轮处理</span></span><br><span class="line"> lineBuffer = lines.<span class="title function_">pop</span>() ?? <span class="string">""</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4. JSON 解析层:逐行解析</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> line <span class="keyword">of</span> lines) {</span><br><span class="line"> <span class="keyword">const</span> trimmed = line.<span class="title function_">trim</span>();</span><br><span class="line"> <span class="keyword">if</span> (!trimmed) <span class="keyword">continue</span>; <span class="comment">// 跳过空行</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> msg = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(trimmed) <span class="keyword">as</span> <span class="title class_">ChatChunk</span>;</span><br><span class="line"> <span class="title function_">onChunk</span>(msg);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 5. 业务终止:显式结束信号</span></span><br><span class="line"> <span class="keyword">if</span> (msg.<span class="property">type</span> === <span class="string">"done"</span>) <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> <span class="keyword">throw</span> e;</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> reader.<span class="title function_">releaseLock</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><strong>服务端 send</strong>:</p><blockquote><p><strong>将增量 Token 封装为可切分的消息单元(行),确保客户端始终解析完整的 JSON 对象。</strong></p></blockquote><ul><li>增量 Token:<code>{"type":"delta","text":"..."}\n</code> + Flush</li><li>结束信号:<code>{"type":"done"}\n</code> + End Response</li></ul><hr>

iPhone 截屏高效翻译

作者 海驴
2025年12月14日 23:48
<p>工作生活中可能有以下多语言的困扰:</p><p>a. 在 iPhone 小屏幕上工作,临时切不到 Mac 等多效率平台。<br>b. 多国语言的文本需要阅读、处理,如 email、app 使用、社交媒体等。<br>c. 语言学习能力很渣。</p><p>AI 在文本领域已经足够强大,翻译领域更是和 code 领域一起,是第一批被攻下神坛的大山。<br>以下提供一些 iPhone 设备上能够『较快、较高质量』的完成文本翻译的方案。</p><h2 id="系统-Translate-翻译-app"><a href="#系统-Translate-翻译-app" class="headerlink" title="系统 Translate(翻译) app"></a>系统 Translate (翻译) app</h2><blockquote><p>Version: iOS 26+</p></blockquote><p>iPhone 系统自带的 Translate 功能,目前已经提供了较完备的翻译能力。<br>打开 app 后甚至可以和其他人面对面语音输入并翻译沟通。除了需要打开 app 外,iPhone 还提供了以下翻译的隐藏入口:</p><p>a. 网页内容,长按文本,弹出框中选择『翻译』<br>b. 截屏,文本 OCR 识别后,旁边显示『翻译』<br>c. 拍照,文本 OCR 识别后,旁边显示『翻译』(对准文本区块即可,不用点击拍摄)</p><span id="more"></span><p>以上三个隐藏入口,会唤起系统级别的翻译弹窗面板,可以快速的解决近乎所有需要翻译的场景(尤其 b- 截屏)。特别说明:</p><ol><li>如果是 Safari 浏览器阅读文本,通过配置广为流传的「沉浸式翻译」,搭配自定义 ai 模型后,效果会很好。</li><li>如果是 app 中内嵌的 web 浏览器,通过 a 方案,效率就已经很高了。</li><li>有些网页实在是烂,长按选择文本很吃力,就切到 b 方案。</li><li>对于 c 方案,如果有大片文本在不同区域,可能只识别一个区块。可以直接截屏切到 b 方案。</li></ol><p>截屏方案是非常好用的大杀器,对任何文本、app 都生效。还可以自定义很多快捷方式来启动截屏。<br>有一个显著的优化建议:有时候「截屏 - 文本识别 - 翻译」后,文本会很小,看不清。这个时候可以双指捏合来放大截屏内容,然后再翻译。</p><h2 id="Translate-app"><a href="#Translate-app" class="headerlink" title="Translate app"></a>Translate app</h2><p>iOS 26+ 系统对非官方 app 开放了系统级的翻译入口。有不少 ai app 都接入了这个入口。好处是:</p><p>a. 普通 app 可以直接通过上面系统 Translate 的入口(截屏、长按等)来唤起普通 app 的翻译面板。<br>b. 普通 app 可以使用 ai 模型进行翻译,提升质量。</p><p>走到这一步,还是 Apple 太邋遢。它掌握着翻译的入口、但翻译能力有时候真不太行。</p><p>操作:在系统设置中,将翻译的默认 app 设置成自己正在使用的 ai app 就可以了(如 OpenCat、FlowDown)。</p><h2 id="其他建议"><a href="#其他建议" class="headerlink" title="其他建议"></a>其他建议</h2><p>a. AI 模型推荐使用 Gemini-2.5-flash-lite。速度贼快、质量极好、价格低廉。在文本处理上,不可能三角被它打下来了。<br>b. 如果有条件,一定切到 AI 模型。系统 Translate 的质量有时候真的堪忧。尤其很多工作生活场景,文本会出现换行,它处理的就不好。<br>c. 若使用 iPhone 的同时也正在使用 Mac,通过「iPhone Mirror」可以投屏。Mac 上翻译的工具就太多了(如 Bob)。</p><hr>

Flash Open Terminal

作者 海驴
2025年11月13日 10:19
<p>问题 (TLDR):zsh 已经成为标配,但每次启动它都需要等待 1-2s,很烦。<br>问题 (Detail):<code>.zshrc</code> 配置中有很多耗时插件和功能,如 nvm、jenv 等等,打开终端很慢。所有 lazy load 等方案都会在各个旮旯里影响原有的启动逻辑,且维护成本高。</p><p>解决方案:ITerm2 &amp; Tmux。</p><span id="more"></span><h3 id="场景还原:"><a href="#场景还原:" class="headerlink" title="场景还原:"></a>场景还原:</h3><ol><li><code>.zshrc</code> 有很多功能需求配置,已经影响到了 terminal 启动速度。</li><li>工作期间需要开启 n 个 app 画面,非常乱。而对于 ternimal 总是习惯性使用完 kill。每次打开都会 waiting 很久。</li><li>终端使用 Warp 和 Iterm2,app launch 使用 Raycast。</li></ol><h3 id="解决方案:"><a href="#解决方案:" class="headerlink" title="解决方案:"></a>解决方案:</h3><ol><li><p>使用 iterm2 创建一个 profile,设置为 default (即 app 启动后默认打开当前 profile)。</p></li><li><p>General - Command 选择 <code>Command</code>,命令配置:</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/usr/bin/env zsh -c <span class="string">'/opt/homebrew/bin/tmux new -As dev'</span></span><br></pre></td></tr></tbody></table></figure></li><li><p>tmux 使用较简单,了解下就可以熟练操作。</p></li><li><p>后续工作中需要快速执行命令,通过 Raycast 打开 iterm2 即可,0ms 延迟启动。</p></li><li><p>使用完可直接 kill iterm2 窗口,依靠 tmux 做会话保持。</p></li><li><p>其他场景,按个人喜好使用 Warp 即可。</p></li></ol><h3 id="结果:"><a href="#结果:" class="headerlink" title="结果:"></a>结果:</h3><p>Iterm2 自身启动耗时 0ms,tmux attach sesstion 0ms。 配合一下,终端秒开,快速执行工作生活中的小命令,再也不用处理 <code>.zshrc</code> 多配置启动慢问题。</p><h3 id="扩展:"><a href="#扩展:" class="headerlink" title="扩展:"></a>扩展:</h3><h4 id="shell-启动参数"><a href="#shell-启动参数" class="headerlink" title="shell 启动参数"></a>shell 启动参数</h4><ul><li><code>-l</code>:以登录 shell 启动,读取登录相关配置(如 <code>~/.zprofile</code>),保证 PATH、环境变量齐全。</li><li><code>-i</code>:以交互式 shell 启动,启用提示符、job control,并读取交互配置(如 <code>~/.zshrc</code>)。</li><li><code>-c</code>:执行后面的一条命令字符串并退出,常与 <code>-l</code>、<code>-i</code> 组合,用来在「完整环境」里执行单条命令。</li></ul><p>配置了 <code>-lic</code>,就会走完整的模拟器登录交互环境,保证各种环境变量、插件都能正常加载。这也是文档开头说到的问题根源。即 <code>.zshrc</code> 等配置启动太耗时了。<br>配置 <code>-c</code>,就在裸环境执行 shell 命令,非常快。但这种场景下是裸环境,各种 shell 配置都没有。只应该在必要的场景使用。</p><h4 id="更多-Command-配置示例"><a href="#更多-Command-配置示例" class="headerlink" title="更多 Command 配置示例"></a>更多 Command 配置示例</h4><ul><li><p><code>-lic</code> 示例:</p><p><code>/usr/bin/env zsh -c "/opt/homebrew/bin/tmux has-session -t dev 2&gt;/dev/null &amp;&amp; /opt/homebrew/bin/tmux attach -t dev || zsh -lic '/opt/homebrew/bin/tmux new -s dev'"</code></p><p>完全等价于:</p><p><code>/usr/bin/env zsh -c '/opt/homebrew/bin/tmux new -As dev'</code></p><p>解析:tmux Check 和 Attach 的时候使用的 <code>-c</code>,new 的时候,tmux 会在 session 内部通过 <code>-lic</code> 启动 shell (tmux 默认行为)。</p></li><li><p>tmux 跟随待执行命令:</p><p><code>/usr/bin/env zsh -c '/opt/homebrew/bin/tmux new -As xxx "zsh -lic \"exec npx xxx@latest\""'</code></p><p>解析:tmux new session 的时候,如果跟随了待执行命令,就不在走上面提供的默认行为 (<code>-lic</code>),即 tmux 默认在裸环境下执行待执行命令。需要手动补齐登录交互环境,即 <code>zsh -lic "exec npx xxx@latest"</code>。</p></li></ul><hr>

iPhone 侧载安装

作者 海驴
2025年9月7日 22:35
<p>iPhone 上安装一个非 App Store 的 app 太难了,Apple 生态基于【公私钥】非对称检测,使得专业的行内人员也无法很好的在开发机上随意安装三方 app。<br>这里有一些对普通人来说也比较方便的实施方案。安全性上,除了被安装的 ipa 包本身可能有风险,其他链路很安全。</p><blockquote><ol><li>非 App Store 版本的 app,来源有 github、三方分享平台等。</li><li>app 安装需要 ipa 包。App Store 上下载的就是 ipa 包,这里需要通过 github、telegram 等渠道获取。</li></ol></blockquote><h1 id="普通、小白、非专业、非-IT-用户"><a href="#普通、小白、非专业、非-IT-用户" class="headerlink" title="普通、小白、非专业、非 IT 用户"></a>普通、小白、非专业、非 IT 用户</h1><p>通过 <span class="exturl" data-url="aHR0cHM6Ly9hbHRzdG9yZS5pby8=">AltStore</span>,就可以非常方便的安装 ipa 包了,具体流程需要查看下文档。<br>需要 Mac / Window 电脑 + 个人的 AppleId。安全性上,非常安全,技术方案是:</p><span id="more"></span><ol><li>Apple 开发人员需要 Apple Account 才能开发 / 发布 app 到 App Store。前些年 Apple 开放了门槛,使用个人 Apple Id(free account) 也能够进入开发流程,但是 app 不能上架 App Store。</li><li>free account 有限制:一个 iPhone 同一时间最多只能安装 3 个 app + 一周只能激活 10 个 app(每个 app 都有唯一 id,一周最多 10 个 id)</li><li>通过对三方渠道获取的 ipa 使用 free account 进行重签名(骗过 iPhone 公私钥检测),假装自己是 app 的开发人员,就可以安装到自己的 iPhone 上。</li></ol><p>缺点:</p><ol><li>每个 app 安装 7 天后就打不开了,需要通过 AltStore 将 iPhone 连接上 Mac / Window 后重新安装下(free account 限制)。</li><li>同一时间最多安装 2 个 app(free account 合计 3 个,有一个是 AltStore app)。</li></ol><h2 id="更进一步"><a href="#更进一步" class="headerlink" title="更进一步"></a>更进一步</h2><p>上面列出的 2 个缺点,一般人用用也够了。如果想方便一些,通过 <span class="exturl" data-url="aHR0cHM6Ly9zaWRlc3RvcmUuaW8v">SideStore</span> 可以更进一步的解决一个小痛点 (其他缺点均无法解决):<br>SideStore 的重签流程放到了 iPhone 上,可以脱离 AltStore Mac / Window 了。<br>这样,可以很方便的在 iPhone 上完成 7 天失效的【续重签名】,不用打开电脑了。</p><h1 id="Apple-开发人员"><a href="#Apple-开发人员" class="headerlink" title="Apple 开发人员"></a>Apple 开发人员</h1><p>Apple 开发者都有 Apple Account,AltSotre 除了支持个人 AppleId,也支持开发者账号。上面的两个缺点就都解决了。<br>开发者证书签名的 app 有 1 年有效期,足够用了。</p><h2 id="单点登录-Apple-Account"><a href="#单点登录-Apple-Account" class="headerlink" title="单点登录 Apple Account"></a>单点登录 Apple Account</h2><p>有些开发者账号是公司提供的,管理员关闭了账号密码直接登录,就无法在 AltStore 使用了。这个时候要进行深度探索了。</p><p>有一个 app / ipa,叫【全能签】。主动设置开发证书后,全能签通过证书来完成 ipa 重签名并安装到 iPhone。原理也是和 AltStore 一样,都是 ipa 重签名,骗过 iPhone 公私钥检测。</p><p>这个流程的具体操作是:</p><ol><li>通过 AltStore 安装 全能签(全能签 a,7 天有效期)</li><li>设置 全能签(开发证书等)</li><li>通过 全能签 安装 全能签(全能签 b,1 年有效期)</li></ol><p>第三步通过全能签 a 自举 安装的 全能签 b,有 1 年有效期。<br>这个时候,就可以把 AltStore、全能签 a 等全部卸载了,仅保留 全能签 b。<br>后续所有的 ipa 包,都可以通过 全能签 b 来安装,有 1 年有效期,够用了。</p><h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><p>之前越狱很有名,现在基本灭绝了。不过衍生出来一个新的分支,叫【巨魔】,又称【半越狱】。<br>巨魔 可以方便的安装 ipa 包,因为走的系统漏洞,app 不会过期,长久有效。<br>巨魔 技术上需要使用系统漏洞,而 Apple 会更新补丁,所以最新系统基本都无法使用 巨魔。<br>而巨魔之所以存在,因为非 App Store app 依旧有一批市场。</p><p>So,如果想了解上面介绍的【侧载】安装的应用场景,还是需要先进入这片字面意义上,不正规 / 灰色的领域。</p><hr>

Xcode Symbolic Debug

作者 海驴
2025年8月22日 18:57
<p>Xcode 的 Symbolic Breakpoint(符号断点)在排查问题的时候非常好用,尤其在三方闭源库联调的时候。</p><blockquote><p>在闭源三方库中,如果能根据公开的 Api 找到一些有用的信息,还是非常 nice 的。</p></blockquote><p>不知道什么时候开始,符号断点的内容格式非常严格,不然无法被断点。虽然 Xcode 给了如下提示,但那么多符号,很难短时间处理好:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Xcode won't pause at this breakpoint because it has not been resolved.</span><br><span class="line">Resolving it requires that:</span><br><span class="line">• The symbolic name is spelled correctly.</span><br><span class="line">• The symbol actually exists in its library.</span><br><span class="line">• The library for the breakpoint is loaded.</span><br></pre></td></tr></tbody></table></figure><span id="more"></span><p>简单来说,之前通过快捷键可以将当前指针所在的 Symbolic 快速录入到搜索框中进行搜索(xcode 支持符号检索),然后把输入框内容复制到 Breakpoint 就能进行符号断点了。现在死活断不到。<br>需要:绝对准确的符号签名,包括 static、参数、返回值 等等全量信息,少一点就断不成。</p><p>快速的方案,是在 Debug 中执行 <code>image lookup -rn xxx</code> 来查找整个工程中特定符号的内容,在其中找到准确的符号签名后,再设置到 Breakpoint 中。这里使用的 xcode lldb 命令,输出内容一不小心就闪瞎眼。</p><p>还有一个比较快捷的方案是使用 <span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL0RlcmVrU2VsYW5kZXIvTExEQg==">https://github.com/DerekSelander/LLDB</span>,通过其 <code>lookup xxx</code> 命令,就可以非常整洁的整理出来所需符号的完整签名信息。copy 一下就能使用了。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">// e.g.</span><br><span class="line"></span><br><span class="line">(lldb) lookup initApp</span><br><span class="line">****************************************************</span><br><span class="line">1 hits in: EleSDK</span><br><span class="line">****************************************************</span><br><span class="line">static EleSDK.Ele.initApp(key: Swift.String, apiURLString: Swift.Optional&lt;Swift.String&gt;) -&gt; ()</span><br><span class="line">****************************************************</span><br><span class="line">4 hits in: ManagedConfiguration</span><br><span class="line">****************************************************</span><br><span class="line">+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]</span><br><span class="line">__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke</span><br><span class="line">__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke_2</span><br><span class="line">objc_msgSend$initAppleIDSSOAuthentication</span><br><span class="line"></span><br></pre></td></tr></tbody></table></figure><p>最后,也比较一下 <code>image lookup -rn xxx</code> 的结果:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">(lldb) image lookup -rn initApp</span><br><span class="line">1 match found in /xxx/Ele-ios-demo-swift-gfeqjptpbrkqcrborvnyugpagjfl/Build/Products/Debug-iphoneos/Ele-ios-demo-swift.app/Frameworks/EleSDK.framework/EleSDK:</span><br><span class="line"> Address: EleSDK[0x000000041efc] (EleSDK.__TEXT.__text + 237308)</span><br><span class="line"> Summary: EleSDK`static EleSDK.Ele.initApp(key: Swift.String, apiURLString: Swift.Optional&lt;Swift.String&gt;) -&gt; ()</span><br><span class="line">4 matches found in /Users/hailv/Library/Developer/Xcode/iOS DeviceSupport/iPhone17,3 18.6.2 (22G100)/Symbols/System/Library/PrivateFrameworks/ManagedConfiguration.framework/ManagedConfiguration:</span><br><span class="line"> Address: ManagedConfiguration[0x00000001a1bc9a2c] (ManagedConfiguration.__TEXT.__text + 225740)</span><br><span class="line"> Summary: ManagedConfiguration`+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]</span><br><span class="line"> Address: ManagedConfiguration[0x00000001a1bc9ab4] (ManagedConfiguration.__TEXT.__text + 225876)</span><br><span class="line"> Summary: ManagedConfiguration`__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke</span><br><span class="line"> Address: ManagedConfiguration[0x00000001a1bc9b18] (ManagedConfiguration.__TEXT.__text + 225976)</span><br><span class="line"> Summary: ManagedConfiguration`__61+[MCLazyInitializationUtilities initAppleIDSSOAuthentication]_block_invoke_2</span><br><span class="line"> Address: ManagedConfiguration[0x00000001a1cc8a20] (ManagedConfiguration.__TEXT.__objc_stubs + 22656)</span><br><span class="line"> Summary: ManagedConfiguration`objc_msgSend$initAppleIDSSOAuthentication</span><br><span class="line"></span><br></pre></td></tr></tbody></table></figure><p>找到 func 签名,挺费事儿的。</p><hr>

投资风险收益评估

作者 海驴
2025年5月25日 13:58
<p>风险等级说明:</p><ul><li>R1: 极低风险 (年化回报率: 3.0-5.5%)</li><li>R2: 低风险 (年化回报率: 3.5-6.0%)</li><li>R3: 中低风险 (年化回报率: 4.5-7.5%)</li><li>R4: 中等风险 (年化回报率: 5.0-10.0%)</li><li>R5: 中高风险 (年化回报率: 7.0-13.0%)</li><li>R6: 高风险 (年化回报率: 5.0-20.0%, 损失风险: -50% 至 - 70%)</li><li>R7: 极高风险 (年化回报率: 15% 以上 (部分品种远超此范围), 损失风险: -50% 至 - 100%)</li></ul><p>金融投资产品(按风险从低到高排列):</p><ul><li><p>R1 年化回报率: 3.0-5.5%</p><ul><li>储蓄账户 (Savings Accounts) <em>年化回报: 0.5-4.5%</em></li><li>国库券 (Treasury Bills) <em>年化回报: 4-5.5%</em></li><li>存款证 (CDs - Certificates of Deposit) <em>年化回报: 3.5-5.0%</em></li><li>货币市场基金 (Money Market Funds) <em>年化回报: 4.0-5.5%</em> (通常属于共同基金的一种)</li></ul></li></ul><span id="more"></span><ul><li><p>R2 年化回报率: 3.5-6.0%</p><ul><li>政府债券 (Government Bonds) <em>年化回报: 4.0-5.0%</em></li><li>市政债券 (Municipal Bonds) <em>年化回报: 3.0-5.5%</em> (风险通常低于公司债券,但高于国债,税前回报)</li><li>年金 (Annuities) <em>年化回报: 4.0-7.0%</em> (固定年金部分参考: 4.5-5.8%,变额年金风险和回报波动大)</li></ul></li><li><p>R3 年化回报率: 4.5-7.5%</p><ul><li>债券 (Bonds) <em>年化回报: 4-7%</em> (泛指,风险高于政府债券)<ul><li>公司债券 (Corporate Bonds) <em>年化回报: 5.0-6.5%</em> (投资级公司债)</li></ul></li><li>债券基金 (Bond Funds) <em>年化回报: 3.5-6.0%</em> (投资于上述债券的基金,回报取决于具体配置)</li><li>优先股 (Preferred Stock) <em>年化回报: 5.5-8.0%</em> (风险通常介于债券和普通股之间)</li></ul></li><li><p>R4 年化回报率: 5.0-10.0%</p><ul><li>共同基金 (Mutual Funds) <em>年化回报: 6-12%</em> (这是一个大类,其下具体基金风险各异,回报取决于投资标的)</li><li>混合基金 (Balanced Funds) <em>年化回报: 6.0-9.0%</em> (同时投资于股票和债券)</li><li>指数基金 (Index Funds) <em>年化回报: 4.0-7.0%</em> (主要指债券指数基金及其他低风险指数基金;股票指数基金回报见 R5)</li><li>交易所交易基金 (ETFs - Exchange Traded Funds) <em>年化回报: 4.0-7.0%</em> (主要指债券 ETF 及其他低风险 ETF;股票 ETF 回报见 R5)</li><li>可转换证券 (Convertible Securities) <em>年化回报: 5.0-9.0%</em></li><li>外汇 (现汇交易/非杠杆) (Foreign Exchange - Spot/Non-leveraged) <em>年化回报: 1-8%</em> (高度不确定,含利率差和汇率波动)</li></ul></li><li><p>R5 年化回报率: 7.0-13.0% (损失风险: -20% 至 - 40%)</p><ul><li>股票 (Stocks) <em>年化回报: 8-12%</em> (蓝筹股或大型稳定公司股票)</li><li>股票基金 (Stock Funds) <em>年化回报: 9-13%</em> (投资于股票的基金,主动管理型)</li><li>股票指数基金 (Stock Index Funds) <em>年化回报: 8-12%</em> (追踪股票指数的基金)</li><li>股票类 ETFs (Stock ETFs) <em>年化回报: 8-12%</em> (追踪股票指数的交易所基金)</li><li>房地产投资信托 (REITs - Real Estate Investment Trusts) <em>年化回报: 7-11%</em> (总回报,含股息和资本增值)</li><li>房地产 (Real Estate) <em>年化回报: 6-10%</em> (直接投资,总回报,注意高持有成本和低流动性)<ul><li>住宅房地产 (Residential Real Estate) <em>年化回报: 5-8%</em></li><li>商业房地产 (Commercial Real Estate) <em>年化回报: 7-12%</em> (近年面临挑战)</li></ul></li><li>企业年金/退休计划 (Pension Plans/Retirement Plans) <em>年化回报: 6-10%</em> (风险和回报高度取决于计划内的具体投资组合)</li></ul></li><li><p>R6 年化回报率: 5.0-20.0% (损失风险: -50% 至 - 70%)</p><ul><li>P2P 借贷 (Peer-to-Peer Lending) <em>年化回报: 5-12%</em> (净回报预期,高违约风险)</li><li>高收益债券/垃圾债券 (High-yield Bonds/Junk Bonds) <em>年化回报: 6-10%</em> (总回报预期,或 Yield to Worst 7-9%)</li><li>小盘股 (Small-cap Stocks) <em>年化回报: 9-15%</em> (成长型小公司股票,高波动性)</li><li>新兴市场股票 (Emerging Market Stocks) <em>年化回报: 5-15%</em> (长期平均回报,高波动性)</li><li>大宗商品 (Commodities) <em>年化回报: 0-10%</em> (长期平均回报,波动极大,可能负回报)<ul><li>贵金属 (Precious Metals - e.g., Gold, Silver) <em>年化回报: 2-10%</em> (黄金波动相对较低,白银波动大)</li><li>能源 (Energy - e.g., Oil, Natural Gas) <em>年化回报: -5-15%</em> (波动极大,高风险)</li><li>农产品 (Agricultural Products - e.g., Corn, Wheat) <em>年化回报: 0-12%</em> (波动极大,高风险)</li></ul></li><li>众筹投资 (Crowdfunding Investments) <em>年化回报: -50% 至 + 50%</em> (通常是初创企业股权,风险极高,高失败率)</li><li>结构性产品 (Structured Products) <em>年化回报: 5-20%</em> (风险复杂且不透明,回报高度依赖具体结构)</li><li>收藏品 (Collectibles) <em>年化回报: 2-10%</em> (流动性极差,价值波动大,持有成本高)</li></ul></li><li><p>R7 年化回报率: 15% 以上 (部分品种远超此范围) (损失风险: -50% 至 - 100%)</p><ul><li>私募股权 (Private Equity) <em>年化回报: 12-25%</em> (净 IRR 预期,高风险、高门槛、长周期、低流动性)</li><li>风险投资基金 (Venture Capital) _年化回报: 15-30%+ _ (早期高成长性投资预期,整体回报差异极大,高失败率)</li><li>对冲基金 (Hedge Funds) <em>年化回报: 5-20%</em> (策略多样,大类资产平均预期,少数可能更高)</li><li>期权 (Options) <em>年化回报: -100% 至 + 数倍</em> (极高风险,专业性要求高)</li><li>期货 (Futures) <em>年化回报: -100% 至 + 数倍</em> (极高风险,专业性要求高)</li><li>杠杆 ETF (Leveraged ETFs) <em>年化回报: 高度波动 (-90% 至 + 数倍可能), 不宜长期持有</em></li><li>外汇 (Forex - Foreign Exchange) <em>年化回报: -100% 至 + 数倍</em> (杠杆交易风险极高,专业性要求高)</li><li>二元期权 (Binary Options) <em>年化回报: 高投机,回报范围极大,通常不被视为正规投资</em></li><li>加密货币 (Cryptocurrencies) <em>年化回报: 极高波动 (-90% 至 + 数千百分比可能), 投机性强,风险巨大</em></li><li>微盘股 (Penny Stocks) <em>年化回报: 极高风险 (-95% 至 + 数倍可能,大概率亏损)</em></li></ul></li></ul><hr>

Mac 快速修改系统快捷键

作者 海驴
2025年5月19日 14:14
<p>Macos 系统快速调整快捷键方案:</p><ol><li>在 <code>system settings</code> 中清理不需要的快捷键,把核心快捷键用来自定义</li><li><code>Raycast</code> - 最方便的快速创建自定义快捷键解决方案</li><li><code>Hammerspoon</code> - 复杂疑难病症的解决方案</li></ol><p>操作说明和快捷键建议指南如下。</p><span id="more"></span><h2 id="系统快捷键设置"><a href="#系统快捷键设置" class="headerlink" title="系统快捷键设置"></a>系统快捷键设置</h2><p>入口:<code>system settings</code> - <code>keyboard</code></p><ul><li><code>Press 地球 key to</code> 调整为 nothing<ul><li>候选项有<code>Change Input Source</code>、<code>Show Emoji &amp; Symbols</code>、<code>Start Dictation</code></li><li>Input Source 通过 <code>Input Source Pro</code> app 可以快速设置(支持快捷键)</li><li>Emoji &amp; Symbols 通过系统全局 <code>Control + CMD + Space</code> 呼出,不需要快捷键</li><li>Start Dictation 如果不需要这个功能,不用开启</li></ul></li><li><code>Dictation</code> - <code>Shortcut</code>: 改成不需要常用快捷键的方式</li><li><code>Keyboard Shortcuts</code> - 这里面是系统提供的很多没必要的快捷键,不需要的都可以取消掉</li></ul><p>上面三个步骤设置完成后,核心的快捷键都可以留给我们自己来分配了。</p><h2 id="通过-Raycast-修改"><a href="#通过-Raycast-修改" class="headerlink" title="通过 Raycast 修改"></a>通过 Raycast 修改</h2><p>Raycast 非常好用,通过 <code>Hotkey</code> 可以快速设置系统级快捷键,非常方便。</p><ol><li>打开 app:一般不需要,通过 Raycast 启动入口打开 app 已经很快了</li><li>Script Command:这个非常好用,很多系统级别的能力,可以通过 sh / applescript 的形式执行,分配一个快捷键后非常方便。<ul><li>e.g. 写一个【当前最前置 window】移动到下一个屏幕上(多屏幕场景),增加 [Double Click CMD] 快捷键。</li></ul></li></ol><h2 id="通过-Hammerspoon-修改"><a href="#通过-Hammerspoon-修改" class="headerlink" title="通过 Hammerspoon 修改"></a>通过 Hammerspoon 修改</h2><p>Hammerspoon 是非常强大的快捷键执行器, 解决 any 疑难杂症。</p><p>在多屏幕场景下,很难实现【移动鼠标到指定屏幕】,Hammerspoon 可以实现。</p><h2 id="快捷键建议"><a href="#快捷键建议" class="headerlink" title="快捷键建议"></a>快捷键建议</h2><ul><li>1 只手能操作的快捷键,绝不留给 2 只手</li><li><code>Ctrl</code>、<code>Option</code>、<code>Cmd</code> 和 <code>q/w/e/r/a/s/d/f/z/x/c/v</code> 组合<ul><li>尽量少用 <code>Shift</code></li></ul></li><li><code>Double Click Ctrl</code>、<code>Double Click Option</code>、<code>Double Click Cmd</code> 这三个最方便,要留给最常用的操作</li><li>很多时候快捷键操作的都是【文件 / 文件夹】,Mac Finder 支持太弱了,上 <code>QSpace Pro</code> 就非常方便(使用 Raycast Script Cammand 也很方便)。</li><li>快捷键的脚本怎么写,别自己写,让 AI 写。</li></ul><hr>

AI 指北

作者 海驴
2025年6月30日 22:44
<br><p>整理 &amp; 学习了 AI 相关的知识点。这里把 PPT 内容放一下,详细内容可查阅 via <span class="exturl" data-url="aHR0cHM6Ly95aWdlZ29uZ2ppYW5nLm5vdGlvbi5zaXRlL0FJLTJlNzJmYmRhZDViYzRlZTRiOGYyYzc5Y2ZhZjkyN2QyP3B2cz00">https://yigegongjiang.notion.site/AI</span></p><h2 id="主题"><a href="#主题" class="headerlink" title="主题"></a>主题</h2><table><colgroup><col width="30%"><col width="70%"></colgroup><tbody><tr> <td style="text-align: center; vertical-align: middle; font-size: 1.8em; font-weight: bold;">神经网络抽象了现实世界</td> <td style="text-align: center; vertical-align: middle;"> <img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010329.png" width="50%"> </td></tr></tbody></table><h2 id="ToC"><a href="#ToC" class="headerlink" title="ToC"></a>ToC</h2><table><tbody><tr><td>1. 数学对现实的抽象</td><td> 2. 二元感知机 - 1957</td><td>3. 感知机升维困境 - AI 寒冬</td></tr><tr><td>4. 正向传播 &amp; 反向传播 - 1974</td><td>5. 梯度下降 - 古老的算法</td><td> 6. 多层感知机 MLP - 1986</td></tr><tr><td>7. 卷积神经 CNN - 1998</td><td>8. 自注意力机制 - 2017</td><td>9. Transformer 流程解析</td></tr><tr><td>10. AI - 缸中大脑</td><td> 11. Prompts 是 AI 入场券</td><td> 12. 调参 - 进一步掌控 AI</td></tr><tr><td>13. AI 悬停点 - 2025.06</td><td></td><td></td></tr></tbody></table><span id="more"></span><h2 id="缩略图预览"><a href="#缩略图预览" class="headerlink" title="缩略图预览"></a>缩略图预览</h2><table><tbody><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010329.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010346.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010354.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010400.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010408.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010416.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010426.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010435.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010445.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010451.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010455.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010500.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010506.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010510.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010514.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010518.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010522.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010529.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010537.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010544.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010548.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010552.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010557.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010603.png" width="100%"></td></tr><tr><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010609.png" width="100%"></td><td><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010617.png" width="100%"></td><td></td></tr></tbody></table><h2 id="大图预览"><a href="#大图预览" class="headerlink" title="大图预览"></a>大图预览</h2><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010329.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010346.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010354.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010400.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010408.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010416.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010426.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010435.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010445.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010451.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010455.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010500.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010506.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010510.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010514.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010518.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010522.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010529.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010537.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010544.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010548.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010552.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010557.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010603.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010609.png" width="100%"><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/20250724010617.png" width="100%"><hr>

Git Record

作者 海驴
2025年5月13日 14:20
<blockquote><p>SSH / Personal Access Tokens / GPG Keys / Signing Key / ssh-agent<br>裸中央仓库 / worktree<br>…</p></blockquote><span id="more"></span><h1 id="Email"><a href="#Email" class="headerlink" title="Email"></a>Email</h1><p>通过 ssh key 等方式操作 github 等仓库平台时,只要 ssh 验权通过就可以进行仓库操作。不过对于 git commit 等提交操作,git 会强制要求配置 username 和 email。<br>不过,email 一定要配置好,和账号的 email 一致。github 虽然不对 email 进行操作验证,但是在显示 verified 等标记的时候,还是会校验 email 的。如果 email 不对,则标记 <code>Unverified</code>。</p><p>整体来说,虽然不强求,但尽量配置好。</p><h1 id="SSH-Key"><a href="#SSH-Key" class="headerlink" title="SSH Key"></a>SSH Key</h1><p>github 这些 git 平台,除了 https 之外,也支持 git 协议的 ssh 操作。<br>对于公私钥,本机主要使用私钥,公钥在 github 远程平台上设置。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">tree ~/.ssh/</span><br><span class="line">/Users/example/.ssh/</span><br><span class="line">├── config</span><br><span class="line">├── id_ed25519_personal</span><br><span class="line">├── id_ed25519_personal.pub</span><br><span class="line">├── id_ed25519_work</span><br><span class="line">├── id_ed25519_work.pub</span><br><span class="line">├── known_hosts</span><br><span class="line">├── company-key</span><br><span class="line">└── company-key.pub</span><br></pre></td></tr></tbody></table></figure><p>当需要在一台主机上管理多个 github 账号的时候,需要在 config 文件中进行如下配置:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">&gt; cat ~/.ssh/config</span><br><span class="line"></span><br><span class="line">Host github-personal</span><br><span class="line"> HostName github.com</span><br><span class="line"> User git</span><br><span class="line"> IdentityFile ~/.ssh/id_ed25519_personal</span><br><span class="line"></span><br><span class="line">Host github-work</span><br><span class="line"> HostName github.com</span><br><span class="line"> User git</span><br><span class="line"> IdentityFile ~/.ssh/id_ed25519_work</span><br><span class="line"></span><br><span class="line">Host company-github</span><br><span class="line"> HostName github.company.com</span><br><span class="line"> User git</span><br><span class="line"> IdentityFile ~/.ssh/company-key</span><br></pre></td></tr></tbody></table></figure><p>对应的 git clone 地址也需要做改变:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git clone git@github.com:octocat/test.git</span><br><span class="line"></span><br><span class="line">-&gt;</span><br><span class="line"></span><br><span class="line">git clone git@github-personal:octocat/test.git</span><br></pre></td></tr></tbody></table></figure><h2 id="SSH-与-Personal-Access-Tokens-的关系"><a href="#SSH-与-Personal-Access-Tokens-的关系" class="headerlink" title="SSH 与 Personal Access Tokens 的关系"></a>SSH 与 Personal Access Tokens 的关系</h2><blockquote><p>一般操作,使用 SSH Key 的情况下,不需要设置 PAT。</p></blockquote><h3 id="核心区别"><a href="#核心区别" class="headerlink" title="核心区别"></a>核心区别</h3><ul><li><p>SSH 认证</p><ul><li>使用 <code>git@github.com:user/repo.git</code> 格式地址</li><li>依赖本地 <code>~/.ssh/</code> 目录的密钥对</li><li>通过 <code>ssh -T git@github.com</code> 验证连接</li></ul></li><li><p>HTTPS 认证</p><ul><li>使用 <code>https://github.com/user/repo.git</code> 格式地址</li><li>需要配置 PAT 替代密码(GitHub 已禁用密码认证)</li></ul></li></ul><h3 id="协议检查方法"><a href="#协议检查方法" class="headerlink" title="协议检查方法"></a>协议检查方法</h3><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git remote -v</span><br><span class="line"><span class="comment"># 显示 git@github.com → 使用 SSH</span></span><br><span class="line"><span class="comment"># 显示 https://github.com → 需要 PAT</span></span><br></pre></td></tr></tbody></table></figure><h3 id="需要-PAT-的场景"><a href="#需要-PAT-的场景" class="headerlink" title="需要 PAT 的场景"></a>需要 PAT 的场景</h3><ol><li>调用 GitHub REST API</li><li>使用 GitHub CLI (<code>gh</code>) 操作敏感资源</li><li>访问 GitHub Packages 服务(npm / Docker 等)</li><li>操作其他 HTTPS 协议的仓库时</li><li>第三方 CI / CD 工具集成</li><li>使用 GitHub Actions 需要访问仓库时</li><li>使用双重认证 (2FA) 的账户通过 HTTPS 协议操作仓库</li></ol><h3 id="mac-钥匙串存储-PAT"><a href="#mac-钥匙串存储-PAT" class="headerlink" title="mac 钥匙串存储 PAT"></a>mac 钥匙串存储 PAT</h3><p>git 客户端(命令)会在用户输入 PAT 后,将 PAT 存储到 mac 的 keychain 中,并和 github domain 绑定。<br>这个时候,如果有多个 github 账号,就会发生冲突,导致其中一个 github 账号下面的 pat 验证失败。<br>所以,需要使用 <code>https://github.company.com/xxx/xx.git</code> 或 <code>https://alias.github.com/xxx/xx.git</code> 这样的格式,来对不同的 github 账号进行区分。</p><h3 id="URL-使用-PAT"><a href="#URL-使用-PAT" class="headerlink" title="URL 使用 PAT"></a>URL 使用 PAT</h3><p>URL 协议本身是支持增加 username 和 password 的,即 <code>https://username:password@github.com/xxx/xx.git</code>。</p><p>这个时候,可以把 username 换成 github 账号,password 换成 PAT,就可以在通过 github 授权的情况下操作 xx 仓库了。<br>原理:</p><ol><li>git 客户端会解析 URL,将 username 和 password 提取成 a:b 的格式,进行 base64 编码,放到 “Authorization: Basic ???” 头中。</li><li>服务器端接收到请求后,会进行 base64 解码,获取到 username 和 password,然后进行验证。</li></ol><blockquote><p>不推荐,简单实用一下还是可以的。</p></blockquote><h1 id="Signing-Key"><a href="#Signing-Key" class="headerlink" title="Signing Key"></a>Signing Key</h1><p>github 推出的 commit / tag 等操作的签名认证。被签名的 commit 会在历史记录中显示一个绿色的 <code>verified</code> 标记。<br>GPG Keys 是专门做这个事情的,其中,github 也推出了自己的 Signing Key,可以复用 SSH Keys 的设置(操作入口在同一个地方)。</p><p>具体配置如下,通过在 <code>~/.gitconfig</code> 中增加 url 分流配置,可以为不同的 host 组设置不同的 signing 规则。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">// ~/.gitconfig-github-personal</span><br><span class="line"></span><br><span class="line">[user]</span><br><span class="line"> signingkey = ~/.ssh/id_ed25519_personal</span><br><span class="line">[commit]</span><br><span class="line">gpgsign = true</span><br><span class="line">[gpg]</span><br><span class="line">format = ssh</span><br><span class="line">[tag]</span><br><span class="line">gpgsign = true</span><br></pre></td></tr></tbody></table></figure><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">// ~/.gitconfig-github-work</span><br><span class="line"></span><br><span class="line">[user]</span><br><span class="line"> signingkey = ~/.ssh/id_ed25519_work</span><br><span class="line">[commit]</span><br><span class="line">gpgsign = true</span><br><span class="line">[gpg]</span><br><span class="line">format = ssh</span><br><span class="line">[tag]</span><br><span class="line">gpgsign = true</span><br></pre></td></tr></tbody></table></figure><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">// ~/.gitconfig</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">[includeIf "hasconfig:remote.*.url:git@github-personal:*/**"]</span><br><span class="line"> path = ~/.gitconfig-github-personal</span><br><span class="line">[includeIf "hasconfig:remote.*.url:git@github-work:*/**"]</span><br><span class="line"> path = ~/.gitconfig-github-work</span><br><span class="line"></span><br><span class="line">...</span><br></pre></td></tr></tbody></table></figure><h1 id="ssh-agent"><a href="#ssh-agent" class="headerlink" title="ssh-agent"></a>ssh-agent</h1><p>ssh 和 ssh-agent 都是 openssh 的一部分。对于 ssh 而言,公私钥中,私钥是可以被加密的,加密后,私钥就无法被直接使用,需要使用 passphrase 进行解密后才能使用。<br>这个时候操作流程就比较复杂,每次使用 ssh 的时候,ssh 命令通过操作系统弹窗,让用户输入 passphrase,然后对私钥原始内容进行解密获取私钥再使用。</p><p>ssh-agent 就是一个内存服务,ssh 拿到解密后的私钥后,直接放到 ssh-agent 中,后续直接从 ssh-agent 中进行读取。</p><p>这个时候又会遇到一个问题,就是 macos 系统重启了,ssh-agent 内存数据消失了。所以 ssh 会把 passphrase 存储到 keychain 中,下次启动时,会自动从 keychain 中读取 passphrase。<br>具体流程是:</p><ol><li>macos 重启,ssh-agent 数据清空</li><li>用户操作了 ssh 命令</li><li>ssh 客户端,通过 secret api 读取 keychain 中的 passphrase</li><li>对密钥原始数据进行解密后,把密钥放到 ssh-agent 中</li><li>后续直接从 ssh-agent 中读取</li></ol><p>配置 ssh-agent 示例 (UseKeychain &amp; AddKeysToAgent):</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&gt; <span class="built_in">cat</span> ~/.ssh/config</span><br><span class="line"></span><br><span class="line">Host example.com</span><br><span class="line"> HostName example.com</span><br><span class="line"> User git</span><br><span class="line"> PreferredAuthentications publickey</span><br><span class="line"> IdentityFile ~/.ssh/id_ed25519_personal</span><br><span class="line"> UseKeychain <span class="built_in">yes</span></span><br><span class="line"> AddKeysToAgent <span class="built_in">yes</span></span><br></pre></td></tr></tbody></table></figure><h1 id="git-如何存储文件的修改"><a href="#git-如何存储文件的修改" class="headerlink" title="git 如何存储文件的修改"></a>git 如何存储文件的修改</h1><p>via <span class="exturl" data-url="aHR0cHM6Ly9zd2lmdHJvY2tzLmNvbS93aGF0LWhhcHBlbnMtd2hlbi15b3UtbW92ZS1hLWZpbGUtaW4tZ2l0">https://swiftrocks.com/what-happens-when-you-move-a-file-in-git</span></p><h1 id="裸仓库"><a href="#裸仓库" class="headerlink" title="裸仓库"></a>裸仓库</h1><p>【裸仓库】是一个不包含工作区(working tree)的 Git 仓库,只包含 Git 版本库数据(.git 目录中的内容)。在 github 等平台上,我们通过 push、pull 操作的远程仓库,都是【裸仓库】,因为它们只需要存储版本数据,不需要工作区。</p><p>裸仓库 一般使用 xx.git 命名文件夹,内部没有工程目录,完全是 git 操作目录,如下:</p><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">tree</span><br><span class="line">.</span><br><span class="line">├── HEAD</span><br><span class="line">├── config</span><br><span class="line">├── description</span><br><span class="line">├── hooks</span><br><span class="line">│ ├── applypatch-msg.sample</span><br><span class="line">│ ├── commit-msg.sample</span><br><span class="line">│ ├── fsmonitor-watchman.sample</span><br><span class="line">│ ├── post-update.sample</span><br><span class="line">│ ├── pre-applypatch.sample</span><br><span class="line">│ ├── pre-commit.sample</span><br><span class="line">│ ├── pre-merge-commit.sample</span><br><span class="line">│ ├── pre-push.sample</span><br><span class="line">│ ├── pre-rebase.sample</span><br><span class="line">│ ├── pre-receive.sample</span><br><span class="line">│ ├── prepare-commit-msg.sample</span><br><span class="line">│ ├── push-to-checkout.sample</span><br><span class="line">│ └── update.sample</span><br><span class="line">├── info</span><br><span class="line">│ └── exclude</span><br><span class="line">├── objects</span><br><span class="line">│ ├── info</span><br><span class="line">│ └── pack</span><br><span class="line">└── refs</span><br><span class="line"> ├── heads</span><br><span class="line"> └── tags</span><br></pre></td></tr></tbody></table></figure><ol><li>创建 裸仓库:<code>git init --bare example.git</code></li><li>clone:<code>git clone ./path/to/example.git example</code></li></ol><h1 id="worktree"><a href="#worktree" class="headerlink" title="worktree"></a>worktree</h1><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">NAME</span><br><span class="line"> git-worktree - 管理多个工作目录</span><br><span class="line"></span><br><span class="line">SYNOPSIS</span><br><span class="line"> git worktree add [-f] [--detach] [--checkout] [--lock [--reason &lt;string&gt;]]</span><br><span class="line"> [--orphan] [(-b | -B) &lt;new-branch&gt;] &lt;path&gt; [&lt;commit-ish&gt;]</span><br><span class="line"> git worktree list [-v | --porcelain [-z]]</span><br><span class="line"> git worktree lock [--reason &lt;string&gt;] &lt;worktree&gt;</span><br><span class="line"> git worktree move &lt;worktree&gt; &lt;new-path&gt;</span><br><span class="line"> git worktree prune [-n] [-v] [--expire &lt;expire&gt;]</span><br><span class="line"> git worktree remove [-f] &lt;worktree&gt;</span><br><span class="line"> git worktree repair [&lt;path&gt;...]</span><br><span class="line"> git worktree unlock &lt;worktree&gt;</span><br></pre></td></tr></tbody></table></figure><p><code>git worktree</code> - 管理多个工作目录</p><p>核心功能: 允许你从一个 Git 仓库创建多个独立的工作目录,方便同时处理不同分支或任务。</p><p>常用命令:</p><ol><li><code>git worktree add &lt;path&gt; [&lt;branch&gt;]</code> - 创建新的工作目录<ul><li>作用: 在 <code>&lt;path&gt;</code> 创建新的工作目录,并检出 <code>[&lt;branch&gt;]</code> 分支 (默认当前分支)。</li><li>常用选项:<ul><li><code>f</code>: 强制创建,即使目录已存在 (小心数据丢失)。</li><li><code>-detach</code>: 创建分离 HEAD 的工作目录 (不关联分支)。</li><li><code>b &lt;new-branch&gt;</code>: 创建并检出新分支。</li></ul></li><li>示例:<ul><li><code>git worktree add -b feature-branch ../feature-branch</code>: 创建新分支 <code>feature-branch</code> 并检出到新工作目录。</li><li><code>git worktree add ../working-dir feature-branch</code>: 检出已存在的 <code>feature-branch</code> 分支到新工作目录。</li><li><code>git worktree add ../hotfix origin/main</code>: 创建 <code>hotfix</code> 工作目录 (基于 <code>origin/main</code>)。</li></ul></li></ul></li><li><code>git worktree list</code> - 列出工作目录<ul><li>作用: 显示所有已创建的工作目录及其路径和分支信息。</li><li>常用选项:<ul><li><code>v</code>: 显示更详细的信息。</li></ul></li></ul></li><li><code>git worktree remove &lt;path&gt;</code> - 删除工作目录<ul><li>作用: 删除 <code>&lt;path&gt;</code> 指定的工作目录。</li><li>常用选项:<ul><li><code>f</code>: 强制删除,即使工作目录不干净 (小心数据丢失)。</li></ul></li><li>注意: 仅删除工作目录,不影响仓库本身。</li></ul></li><li><code>git worktree lock &lt;worktree&gt;</code> - 锁定工作目录<ul><li>作用: 锁定 <code>&lt;worktree&gt;</code>,防止被 <code>git worktree remove</code> 删除。</li><li>常用选项:<ul><li><code>-reason &lt;string&gt;</code>: 添加锁定原因。</li></ul></li></ul></li><li><code>git worktree unlock &lt;worktree&gt;</code> - 解锁工作目录<ul><li>作用: 解锁 <code>&lt;worktree&gt;</code>,使其可以被 <code>git worktree remove</code> 删除。</li></ul></li><li><code>git worktree move &lt;worktree&gt; &lt;new-path&gt;</code> - 移动工作目录<ul><li>作用: 将 <code>&lt;worktree&gt;</code> 移动到 <code>&lt;new-path&gt;</code>。</li></ul></li><li><code>git worktree prune</code> - 清理无效信息<ul><li>作用: 清理已手动删除的工作目录在 Git 仓库中残留的记录。</li><li>常用选项:<ul><li><code>n</code>: 模拟运行,不实际删除。</li></ul></li></ul></li><li><code>git worktree repair</code> - 修复工作目录<ul><li>作用: 修复工作目录的元数据 (通常自动维护,极少手动使用)。</li></ul></li></ol><h1 id="文件夹大小写"><a href="#文件夹大小写" class="headerlink" title="文件夹大小写"></a>文件夹大小写</h1><p>Git 对文件夹和文件的大小写,是不敏感的。有些 IDE 会默认做很多事情,让用户无感知。当不通过 IDE 操作 Git 的时候,开发过程中有可能遇到【修改文件夹 / 文件名】无效的场景。</p><h1 id="orphan-branch"><a href="#orphan-branch" class="headerlink" title="orphan branch"></a>orphan branch</h1><p>孤儿分支,是指没有父分支的分支。场景:</p><ul><li>希望从当前 git 工程独立一份完全没有 git 记录的 new-branch,又需要保留当前 git old-branch 所有的 git 状态</li><li>还希望 new-branch 在当前 git 仓库中被管理。</li></ul><figure class="highlight bash"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git switch --orphan new-branch</span><br><span class="line">git checkout old-branch -- .</span><br><span class="line"></span><br></pre></td></tr></tbody></table></figure><p>new-branch 将成为独立 commit 节点,没有任何父节点,并且 git 状态和 old-branch 一致。<br>后面可以新增 remote url,将 new-branch 推送到远程仓库。</p><hr>

How to pay

作者 海驴
2024年12月9日 01:18
<h1 id="All-pays"><a href="#All-pays" class="headerlink" title="All pays"></a>All pays</h1><p><span class="exturl" data-url="aHR0cHM6Ly9zdHJpcGUuY29tL3poLXNnL3BheW1lbnRzL3BheW1lbnQtbWV0aG9kcw==">https://stripe.com/zh-sg/payments/payment-methods</span></p><p>Stripe 支持了很多支付方式,可以窥见一些。Stripe 主要对接线上支付,对于很多线下支付的渠道如【nanaco】等电子货币,就看不到了。</p><h2 id="japan"><a href="#japan" class="headerlink" title="japan"></a>japan</h2><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090122548.png" width="60%"><span id="more"></span><h1 id="货币"><a href="#货币" class="headerlink" title="货币"></a>货币</h1><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090139321.png" width="60%"><h1 id="电子货币-支付链路"><a href="#电子货币-支付链路" class="headerlink" title="电子货币 - 支付链路"></a>电子货币 - 支付链路</h1><p>Suica、PASMO、nanaco、WAON、Edy(Rakuten Edy)、QUICPay、iD</p><p>如上文【货币】-【电子货币】中所描述,E-money(电子货币)是法定货币的 1:n 等值替换,即电子货币 = 法定货币。</p><p>电子货币无法凭空产生,需要基于预付费模式工作的,这意味着用户需要先充值然后才能消费。</p><p>用户将一定金额的资金存入电子钱包或智能卡中,然后在各种接受 E-money 支付的地点使用这些资金进行消费。</p><p>消费方式:实体卡刷卡、Apple / Google Pay、App 出示识别码等等。</p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090124537.png" width="60%"><h1 id="钱包-支付链路"><a href="#钱包-支付链路" class="headerlink" title="钱包 - 支付链路"></a>钱包 - 支付链路</h1><p>市场上有各种 X Pay,均为钱包。除了 Apple / Google Pay 这种专门为 信用卡、预付卡、借记卡、电子货币 提供聚合服务的产品外,其他钱包均有实际公司主体。</p><p>比如常见的 Paypay、wechat、alipay 等等,它们提供各式各样的终端消费能力。</p><p>但用户在钱包中的钱并不能凭空产生。</p><p>一种方式是依靠【信用卡】,信用卡 的链路最为复杂,下一章会单独讲诉。</p><p>一种方式是依靠【预充值】,用户将法定货币预先充值到平台侧。形式有:预付卡、借记卡、平台账户余额。</p><p>通过 预充值 的形式,钱包相当于中间平台,为用户的【纸币、硬币】这些法定货币,提供了中间放置平台,以进一步通过该平台向外部商家进行支付。</p><blockquote><p>当用户把钱充值到钱包后,钱包就需要一系列的管理功能。一来确保用户账户数额不能有差错,二来确保用户可以随意把钱花出去。 所以平台需要做一个大管家,提供各种安全保障能力。 甚至与,哪个商户若需要对平台用户进行收款,哪个商户就也要在该平台进行商家注册并绑定收款银行账号。 这样,商户的账户也被平台管控,平台就很容易进行交易清算。</p></blockquote><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090125351.png" width="60%"><h1 id="信用卡-预付卡-借记卡"><a href="#信用卡-预付卡-借记卡" class="headerlink" title="信用卡 - 预付卡 - 借记卡"></a>信用卡 - 预付卡 - 借记卡</h1><h2 id="概要"><a href="#概要" class="headerlink" title="概要"></a>概要</h2><p>信用卡关键的能力有两点:</p><ol><li>提供个人信用背书,个人没有钱,也能够通过信用卡消费。</li><li>有稳定的支付通道,为世界各地的信用卡提供清算能力。</li></ol><p>信用卡普及且应用广泛后,整个体系就非常成熟,商家已经对信用卡支付形成依赖。此时,一些不符合 1 条件的用户就没有信用卡,但自身也有钱 / 能力进行支付,就衍生出了预付卡、借记卡。</p><p>预付卡、借记卡 借用 2 支付通道,不提供信用背书,直接用现金支付,从而完成交易。</p><blockquote><p>预付卡、借记卡 也包括电子货币。只是在信用卡体系里,预付卡、借记卡 一定依赖 visa、mastercard、… 等支付网络,一定属于 visa、mastercard、… 卡。<br>若商户对 个人信用 十分在意,那么只能使用 信用卡 完成交易,预付卡、借记卡 无法支付。</p></blockquote><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090126692.png" width="60%"><h2 id="品牌贴牌"><a href="#品牌贴牌" class="headerlink" title="品牌贴牌"></a>品牌贴牌</h2><p>目前大范围使用的信用卡支付网络,只有 Visa、MasterCard、American Express (AmEx)、Discover、UnionPay、JCB。其中,大部分不发行卡,只提供【支付网络】。支付网络将是下一个章节重点说明的内容。</p><blockquote><p>American Express 和 Discover 提供发卡服务。即提供支付网络,也下场作为发卡行。&lt;这两位,拥有银行执照&gt;</p></blockquote><p>大众最常见的信用卡,是从银行处申请获取。发卡行一定是具有【银行执照】的金融机构,银行天然具有该优势。基本上,所有的信用卡,都是银行发行的。</p><p>但还有一种常见的信用卡,是从【钱包】公司处获取,如 Paypay、MerPay 等。这些公司并不具备银行执照,即没有发卡能力。</p><p>这是另一种常见的【品牌贴牌】信用卡,即 公司 对接发卡行。公司对用户进行信用评估等审核,并提供品牌福利,而发卡行承担用户的消费风险及盈利。</p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090127218.png" width="60%"><h2 id="信用卡支付网络"><a href="#信用卡支付网络" class="headerlink" title="信用卡支付网络"></a>信用卡支付网络</h2><p>【支付网络】是信用卡交易的核心,也是连接全球所有信用卡的枢纽。</p><p>所有的发卡行、收单行、有能力对接支付网络 的机构、单位、环节,其【资质、技术、安全】等都需要满足【支付网络】提供商的要求。</p><p>【支付网络】决定了整条交易链路的【规则、安全保障、清算、结算】等。</p><p>所有的信用卡,不分地区,通过【支付网络】都可以进行联通、交易。发卡行承担不同币种的汇率、消费额度等工作。</p><blockquote><p>发卡行会在交易前进行验卡,防止卡滥用。如有些卡不允许国际支付,会拒付。</p></blockquote><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090128277.png" width="60%"><h2 id="著名的国际支付服务提供商"><a href="#著名的国际支付服务提供商" class="headerlink" title="著名的国际支付服务提供商"></a>著名的国际支付服务提供商</h2><p>上文中,很多商业产品都会通过【支付服务提供商】来对接【支付网络】最终完成交易。以下是一些有名的提供商列表:</p><p>via <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXIuYXBwbGUuY29tL3RhcC10by1wYXkvcmVnaW9ucy8=">https://developer.apple.com/tap-to-pay/regions/</span></p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090128854.png" width="60%"><h2 id="Apple-Pay-如何支持-EMV-FaliCa"><a href="#Apple-Pay-如何支持-EMV-FaliCa" class="headerlink" title="Apple Pay 如何支持 EMV &amp; FaliCa"></a>Apple Pay 如何支持 EMV &amp; FaliCa</h2><p>EMV 是 Europay MasterCard Visa 的缩写,是信用卡的协议标准,不同支付网络的信用卡均遵守该协议。</p><p>所以,信用卡在世界各地支持 EMV 读卡器的机子上均能【插卡支付】并消费。</p><p>但是在 Touch (contactless) 方面,日本提前广泛使用了 FaliCa 标准,没能很好的支持 EMV,导致前期不管国内还是国外的信用卡,都不支持【非触控支付】。</p><p>在技术专区里,会重点说明 FaliCa 的这段历史,以及 Quicpay / iD 如何解决这个问题。</p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090129489.png" width="60%"><h2 id="信用卡的不同支付链路"><a href="#信用卡的不同支付链路" class="headerlink" title="信用卡的不同支付链路"></a>信用卡的不同支付链路</h2><p>信用卡最终需要【发卡行】通过【支付网络】转账到【收单行】。</p><p>发卡行 依靠哪些信息来判定一张信用卡的合法性,是十分关键的。</p><p>在不同的 信用卡 使用场景中,发卡行 从【支付网络】侧获取到的卡信息是不一样的。</p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090129298.png" width="60%"><h1 id="特别支付场景介绍"><a href="#特别支付场景介绍" class="headerlink" title="特别支付场景介绍"></a>特别支付场景介绍</h1><h2 id="Tap-to-pay"><a href="#Tap-to-pay" class="headerlink" title="Tap to pay"></a>Tap to pay</h2><blockquote><p>基于 NFC 技术,可以【技术专区】中详细了解实现过程。</p></blockquote><p>通过<strong>移动设备终端</strong> (iPhone / Android) 识别 visa 等实体卡或者 apple pay 等<strong>物理硬件</strong>,<strong>获取到卡片的【代理卡号 / 虚拟码】,并完成支付</strong>的方式。</p><ol><li>通过 NFC 协议,完成数据读取</li><li>移动设备会有一个 app,用来对接 nfc api,完成数据读取、服务器交互等工作</li><li>Android 对于 NFC 较开放,可以方便实施。</li><li>iPhone 使用了 Core NFC 中【卡读取】的能力。对个别国家和供应商有开放。</li></ol><h3 id="example"><a href="#example" class="headerlink" title="example"></a>example</h3><p>via <span class="exturl" data-url="aHR0cHM6Ly93d3cuYXBwbGUuY29tL2J1c2luZXNzL3RhcC10by1wYXktb24taXBob25lLw==">https://www.apple.com/business/tap-to-pay-on-iphone/</span></p><h3 id="apple-供应地区说明"><a href="#apple-供应地区说明" class="headerlink" title="apple 供应地区说明"></a><strong>apple 供应地区说明</strong></h3><p>via <span class="exturl" data-url="aHR0cHM6Ly9kZXZlbG9wZXIuYXBwbGUuY29tL3RhcC10by1wYXkv">https://developer.apple.com/tap-to-pay/</span></p><h3 id="Stripe-支持地区说明"><a href="#Stripe-支持地区说明" class="headerlink" title="Stripe 支持地区说明"></a><strong>Stripe 支持地区说明</strong></h3><p>via <span class="exturl" data-url="aHR0cHM6Ly9kb2NzLnN0cmlwZS5jb20vdGVybWluYWwvcGF5bWVudHMvc2V0dXAtcmVhZGVyL3RhcC10by1wYXk/cGxhdGZvcm09aW9z">https://docs.stripe.com/terminal/payments/setup-reader/tap-to-pay?platform=ios</span></p><h2 id="Alipay-碰一下"><a href="#Alipay-碰一下" class="headerlink" title="Alipay 碰一下"></a>Alipay 碰一下</h2><blockquote><p>基于 NFC 技术,可以【技术专区】中详细了解实现过程。</p></blockquote><ol><li>支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。</li><li>以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。</li><li>alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。</li></ol><h1 id="技术专区"><a href="#技术专区" class="headerlink" title="技术专区"></a>技术专区</h1><h2 id="Visa-支付网关和清算平台-跨国际交易"><a href="#Visa-支付网关和清算平台-跨国际交易" class="headerlink" title="Visa - 支付网关和清算平台 - 跨国际交易"></a>Visa - 支付网关和清算平台 - 跨国际交易</h2><ol><li>Visa 不发行卡,不对接个人用户。它只对接银行、大企业主。</li><li>Visa 负责两个银行之间的资金流动,它会使用一天的固定汇率 (高于实时汇率) 加上自身的手续费进行计价。</li><li>因为 Visa 自身做的比较大、有信任、打广告,所以只要牵涉到跨国之间的终端用户级别的资金流动,都会有 Visa 的影子。</li><li>如果仅仅是本地企业、同一货币,那么不需要使用 Visa,就可以省去手续费。如银联。</li><li>所有的 Visa 信用卡交易,都需要过 Visa 的网关进行清算。最终也不是实时扣款,visa 会在结算时间到来后,统一将所有银行的账单进行清算。</li></ol><blockquote><p>扩展:</p></blockquote><ol><li>Visa 和 SWIFT 是两条赛道。SWIFT 主要处理两个国家之间银行界资金的流动。是直接流动,金额更大、级别更高。SWFIT 处理的金钱要比 Visa 多的多。</li><li>SWIFT 一日处理 5W 亿美元的交易,Visa 一年处理 10W 亿美元的交易。交易量不在一个量级。</li></ol><p>Visa 等一众【支付网络】的具体运转,详见上文中的【信用卡支付网络】章节。</p><h3 id="非银行如何对接【支付网络】"><a href="#非银行如何对接【支付网络】" class="headerlink" title="非银行如何对接【支付网络】"></a>非银行如何对接【支付网络】</h3><p>Stripe 对 Visa 的一些解释:<span class="exturl" data-url="aHR0cHM6Ly9zdHJpcGUuY29tL3poLXNnL3Jlc291cmNlcy9tb3JlL3doYXQtaXMtdmlzYSNzaHVpLXphaS1zaGkteW9uZy12aXNh">https://stripe.com/zh-sg/resources/more/what-is-visa#shui-zai-shi-yong-visa</span></p><p>Stripe 在 2015 年开始直接对接 Visa,不在走【收单行】对接:<span class="exturl" data-url="aHR0cHM6Ly93d3cuYnVzaW5lc3N3aXJlLmNvbS9uZXdzL2hvbWUvMjAxNTA3MjkwMDU4NDEvemgtQ04v">https://www.businesswire.com/news/home/20150729005841/zh-CN/</span></p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090134169.png" width="60%"><h2 id="Apple-Pay"><a href="#Apple-Pay" class="headerlink" title="Apple Pay"></a>Apple Pay</h2><h3 id="原理概要"><a href="#原理概要" class="headerlink" title="原理概要"></a>原理概要</h3><p>Apple 提供的【apple pay】方案,是将用户的信用卡信息,使用 “令牌化” 方案,通过【代理卡号】的方式存储于 iPhone 设备,后续将 DAN 提供给读取者,读取者使用 DAN 完成后续的支付链路。</p><blockquote><p>代理卡号(Device Account Number, DAN) 不存储卡号、有效期、cvs,是银行侧生成的一个 code。银行会根据这个 code 在结算的时候进行卡账户校验。</p><p>每次进行 apple wallet 录入的时候,都会生成一个新的 DAN。</p></blockquote><p>使用 DAN 的业务方:</p><ol><li>Apple Wallet:iPhone 作为 NFC 卡模拟,供系统级别的 Wallet app 使用。<ol><li>wallet app<ol><li>将银行卡信息存储到 iPhone 中(录入 DAN)</li><li>有外部 NFC 读卡器的时候,wallet 被激活,完成用户身份认证,并读取 DAN 并提供给外部 NFC 读卡器。</li><li>外部读卡器获取到 DAN 后,对接 Stripe 或者卡服务商。最终在发卡行完成 DAN 的校验,完成扣款和资金转移。</li></ol></li></ol></li><li>Native app:通过 passkit sdk,swift / oc 对接 sdk 在 app 内部完成【支付信息】的读取<ol><li>native app 绑定 【Merchant ID】出口,未绑定的 id 不允许支付。</li><li>native app 弹出 apple pay 弹窗,获取 pay token(公钥加密)</li><li>将 token 给到 self server 或者 stripe 等平台,这些服务平台需要对 token 进行私钥解密。<ol><li>私钥的来源有一套比较复杂的流程</li></ol></li><li>server 将解密后的信息给到【发卡行】,发卡行完成校验及扣款事宜。</li></ol></li><li>Web app:<ol><li>整体和 native app 一致</li><li>因为不在 app 内部,缺少必要的 Merchant ID 信息。而是在 safari 中唤醒 apple pay 支付,所有多了一个【Identity】认证<ol><li>通过公私钥证书来完成</li><li>该认证,主要用于对服务器进行确认,进而获取商户信息。后续流程和 native app 一致。</li></ol></li></ol></li></ol><h3 id="技术流程"><a href="#技术流程" class="headerlink" title="技术流程"></a>技术流程</h3><h3 id="native-self-server"><a href="#native-self-server" class="headerlink" title="native - self server"></a>native - self server</h3><ol><li>Apple Developer 申请 Merchant ID(id)</li><li>dev 申请 CRS 文件(本机绑定私钥 private key),在 developer 后台绑定 id 生成 cer 证书。<ol><li>dev 将 cer 证书安装到本机后,导出 p12 文件(包含公私钥 public + private key)</li><li>将 p12 给到 self server</li></ol></li><li>native app 在 xcode 中绑定 id<ol><li>该行为使得 app 可以确定允许的商户,可绑定多个 id</li><li>web app 没有这一步,所以需要 【identity】认证</li></ol></li><li>native app 完成 apple pay 弹窗并获取 payment token</li><li>token 给到 server,server 通过 p12 拿到 private key,对 token 进行解密后,提交发卡行进行验证扣款。</li></ol><h3 id="native-stripe"><a href="#native-stripe" class="headerlink" title="native - stripe"></a>native - stripe</h3><ol><li>Apple Developer 申请 Merchant ID(id)</li><li>在 stripe 后台申请 CRS 文件后,在 developer 绑定 id 生成 cer 证书,再将 cer 证书上传到 stripe 后台。<ol><li>stripe 这里就拥有了 p12 (private key + public key)</li></ol></li><li>native app 调用 stripe api,完成 apple pay 弹窗 等所有操作,直接获取支付结果<ol><li>stripe sdk 将 payment token 给到 stripe server 完成 token 解密</li><li>stripe server 进行发卡行提交</li><li>stripe 处理支付结果并返回给业务</li></ol></li></ol><h3 id="web-self-server"><a href="#web-self-server" class="headerlink" title="web - self server"></a>web - self server</h3><p>和 native 基本流程一致,在 id 绑定证书环节,除了 【Apple Pay Payment Processing Certificate】cer 证书外,还需要以下:</p><ol><li><p>Apple Pay Merchant Identity Certificate</p><ol><li>商户需要通过该证书向 apple 获取 Merchant ID 的详细信息</li></ol><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"> // example</span><br><span class="line"></span><br><span class="line">const https = require('https');</span><br><span class="line">const fs = require('fs');</span><br><span class="line"></span><br><span class="line">function getApplePaySession(validationURL, callback) {</span><br><span class="line"> const options = {</span><br><span class="line"> url: validationURL,</span><br><span class="line"> method: 'POST',</span><br><span class="line"> cert: fs.readFileSync('path/to/merchant_identity_certificate.pem'),</span><br><span class="line"> key: fs.readFileSync('path/to/merchant_identity_private_key.pem'),</span><br><span class="line"> headers: {</span><br><span class="line"> 'Content-Type': 'application/json'</span><br><span class="line"> },</span><br><span class="line"> body: JSON.stringify({</span><br><span class="line"> merchantIdentifier: 'merchant.com.yourdomain.yourmerchantname',</span><br><span class="line"> domainName: 'yourdomain.com',</span><br><span class="line"> displayName: '您的商户名称'</span><br><span class="line"> })</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> const req = https.request(options, (res) =&gt; {</span><br><span class="line"> let data = '';</span><br><span class="line"> res.on('data', (chunk) =&gt; { data += chunk; });</span><br><span class="line"> res.on('end', () =&gt; { callback(null, JSON.parse(data)); });</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> req.on('error', (e) =&gt; { callback(e); });</span><br><span class="line"> req.end();</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure></li><li><p>Merchant Domains</p><ol><li>需要将必要的 apple 文件放置于 domain 服务器上,供 apple 对 domain 进行验证。未验证通过的 domain 无法进行 apple pay 支付。</li></ol></li></ol><h2 id="NFC"><a href="#NFC" class="headerlink" title="NFC"></a>NFC</h2><p>NFC(近场通信)的技术原理基于无线电频率识别(RFID)技术,使用磁场感应来实现在设备间的通信。NFC 设备在 13.56 MHz 频率上操作,通常用于非接触式数据传输,距离范围非常短,通常在几厘米内,传输速率慢,在 400kbps (50kb / s) 左右。</p><h3 id="技术原理"><a href="#技术原理" class="headerlink" title="技术原理"></a>技术原理</h3><p>识卡器发出信号(电磁感应),激活了终端(手机自动点亮),然后进行数据交互,并可能需要机主进行身份验证,最后完成信息的交互(支付、上公交车、开门等)。</p><p>NFC 有三种工作模式:点对点通信模式、读卡器模式、卡模拟模式。又分为【主动模式】和【被动模式】,其中一个设备提供射频场,另一个设备利用这个射频场进行通信。</p><p>使用 NFC,需要两个终端,一个做控制器,用于发射磁场来识别信息。一个做无电源的数据芯片,通过接收到的磁场来感应并传输信息。</p><p>对于 NFC 设备来说:</p><ol><li>点对点通信:两个 NFC 设备相互交换信息。</li><li>读卡器模式:一个 NFC 作为控制器,读取其他 NFC 芯片中的信息。</li><li>卡模拟模式:一个 NFC 作为数据芯片,其他读卡器可以读取其中的信息。</li></ol><p>现代电子产品中,Android 和 iPhone 都支持 NFC 技术,手机作为 NFC 设备,使用读卡器模式和卡模拟模式,已经可以完成很多事情。</p><ol><li>当作为 NFC 控制器的时候,手机可以主动的读取外部 NFC 芯片中的信息,也可以将必要的信息写入到外部 NFC 芯片。(物流中,可以通过手机对商品挂载的 NFC 芯片进行记录)</li><li>当作为 NFC 芯片的时候,手机可以模拟一个 NFC 芯片,通过软件将信息提前写入手机中,其他读卡器就可以直接读取手机中的信息。(可以实现门禁卡等)</li></ol><p>Android 对 NFC 的 API 开放较多,app 可通过 api 来控制 NFC 进行 读取、写入、模拟 的操作,来实现快捷的智能家居、门禁卡等场景。</p><p>iPhone 上则比较保守,在【卡模拟】、【卡读取】方面,都有不少限制。</p><h3 id="门禁卡"><a href="#门禁卡" class="headerlink" title="门禁卡"></a>门禁卡</h3><p>普通门禁卡:</p><ol><li>门禁卡中有 【微芯片】(存储卡片的识别信息和其他数据)和 【天线】(用于接收和发送无线信号)。</li><li>门禁卡靠近门禁系统的读卡器 → 读卡器会发出一个射频信号 → 信号通过天线供电给门禁卡上的微芯片 → 微芯片被激活, 通过天线将存储在芯片上的识别信息发送回读卡器 → 读卡器接收到信息后,将数据传输给后端的门禁控制系统。</li></ol><p>NFC 门禁卡:</p><p>原理基本和普通门禁卡一样,不过,NFC 提供了更高的安全性。它支持双向通信,卡和识卡器之间可以通信。它们之间会进行密钥交换,通过对称、非对称加密来完成数据的安全传输。相比普通门禁卡,NFC 门禁卡会更加的安全。</p><blockquote><p>NFC 是一种普适性的技术方案,手机也可以作为 NFC 终端。这里就可以把 NFC 门禁卡的信息保存在 手机中,使得手机可以充当 NFC 门禁卡的功能。</p></blockquote><p>蓝牙门锁:</p><p>有些 app 会通过 蓝牙的方式,和门锁连接。这在智能门锁中非常常见。因为距离很远,就可以连接上。而 NFC 需要非常短的距离 (4cm) 才能通信。</p><h3 id="移动支付"><a href="#移动支付" class="headerlink" title="移动支付"></a>移动支付</h3><p>通过 NFC 进行移动支付,主要有三种方案:</p><ol><li>卡模拟方案。mobile 录入支付卡信息,被外部读取器识别<ol><li>系统级别的支持,如 apple pay。开发人员没有掌控能力。用户只能通过 apple wallet 录入银行卡信息,然后通过 apple pay 进行支付。</li><li>应用级别的支持。Android 支持的较好,iPhone 限制很多。<ol><li>iPhone 在 iOS 18.1 放开了该限制,app 可以将支付卡信息写入 app 中,支付的时候调用 app 完成支付。</li><li>不对普通开发者开放,需要和 apple 签订商务协议,支付费用,一般都是支付中间商如 Stripe。并且只对个别国家开放。</li></ol></li></ol></li><li>读卡器方案。mobile 作为读取器识别外部实体卡(信用卡等)。Android 支持的叫好,iPhone 限制很多。<ol><li>Apple Tap to pay。商家可以在自己的手机中,打开 m app,然后用户把信用卡、iWatch 靠近手机,即可完成支付。<ol><li>普通开发人员没有太多的控制能力。也需要签订商务协议,一般都是支付中间商如 Stripe。它们提供 SDK 并和 Apple Api 交互完成支付。</li><li>Apple 和 Stripe 中间商会对地区等有限制。只在少有的地区开放了 Tap To Pay 能力。</li></ol></li><li>alipay 碰一碰。非常聪明的通过 NFC 实现支付的方案。<ol><li>支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。</li><li>以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。</li><li>alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。</li></ol></li></ol></li></ol><h3 id="Apple-iPhone-NFC"><a href="#Apple-iPhone-NFC" class="headerlink" title="Apple iPhone NFC"></a>Apple iPhone NFC</h3><p>简单介绍一下 iPhone 对 NFC 支持的历史:</p><ul><li>WWDC 2017:引入 Core NFC,并具备 NDEF 标签【<strong>读取</strong>】 功能。</li><li>WWDC 2018:在较新设备上对 NDEF 消息进行后台标签读取。</li><li>WWDC 2019:重大扩展,允许 NDEF【<strong>写入</strong>】,支持 ISO 7816、ISO 15693 和 MIFARE 标签,并支持自定义命令。</li><li>WWDC 2020:多标签检测,VAS 协议支持和 ISO 15693 标签的后台读取。</li><li>WWDC 2021-2023:专注于稳定性、性能提升和小幅增强,没有重大 API 更改。</li><li>WWDC 2024:支持【<strong>卡模拟</strong>】。</li></ul><h3 id="EMV-vs-FeliCa"><a href="#EMV-vs-FeliCa" class="headerlink" title="EMV vs FeliCa"></a>EMV vs FeliCa</h3><p>EMV:基于 NFC Type A / B 设计的通信标准。NFC 的【读卡器】和【卡芯片】均使用 EMV 协议。</p><p>FeliCa:Sony 开发,基于 NFC Type F 设计的通信标准,速度快,成本高,仅在日本大范围使用。NFC 的【读卡器】和【卡芯片】均使用 FeliCa 协议。</p><p>这两者,是使用 NFC 实现的两套不兼容的通信协议标准。详细技术解释可参考【Quicpay】章节。</p><h2 id="Quicpay-iD-为什么称为【电子货币】"><a href="#Quicpay-iD-为什么称为【电子货币】" class="headerlink" title="Quicpay - iD 为什么称为【电子货币】"></a>Quicpay - iD 为什么称为【电子货币】</h2><p>Quicpay 官网: <a href="https://www.quicpay.jp/"><span class="exturl" data-url="aHR0cHM6Ly93d3cucXVpY3BheS5qcC8=">https://www.quicpay.jp</span></a></p><p>日本很早期,地铁等交通就很繁华,对于人流量大的场景,过关卡排队就需要很长时间。</p><p>One day,Sony 基于 NFC Type F 研发了 FeliCa 协议的高频无线通信技术。</p><p>FeliCa 主要用于 Touch / Contactless【非触控】场景,Pasmo、Suica 都是基于 FeliCa 实现的刷卡,特点是:速度快。</p><p>很快,FeliCa 成为日本在【非触控】刷卡领域的事实标准,所有能刷卡的地方,都是基于 FeliCa 实现。</p><p>前面介绍 NFC 的时候,说到 NFC 需要两个终端(读卡器、卡模拟)才能工作,日本所有的读卡器也都是基于 FeliCa 实现的。</p><p>而卡模拟一侧,包括 Visa 信用卡、Pasmo 公交卡、电子货币实体卡 等,都是支持 FeliCa 的。</p><p>前面介绍 Apple Wallet 对接 Quicpay 的时候,流程图中有提到 Quicpay DAN。在 Apple Pay 还没有进入日本之前,Quicpay、iD 就已经发行实体卡进行消费,同时 Visa 等信用卡也都是支持 Quicpay / iD 支付。</p><p>交易的链路和现在相比没有改变,依旧是读卡器通过 FeliCa 读取卡信息后提交到 Quicpay 后台,后台通过 收单行 对接 Visa 支付网络,完成交易。</p><p>只是这个时候没有 DAN,DAN 安全码是 Apple Wallet 特有的产物。</p><p>但是 FeliCa 的硬件成本比普通的 NFC Type A/B 协议高,国际上普遍使用的都是 EMV 协议。EMV 是基于 NFC Type A/B 实现的通信协议。</p><p>在刷卡过程中,数据是需要加密的,而这套加密规则,也是和 FeliCa / EMV 的设计绑定的。</p><p>此时,日本基于 FeliCa 的读卡器,在【非触控】刷卡的时候,就无法读取基于 EMV 协议设计的国际信用卡。</p><blockquote><p>但是对于插卡消费是没有影响的。 Visa、masterCard 等官方平台,制定的通信规则就是【信用卡基于 EMV 协议】。所以日本的信用卡本身绝对是符合 EMV 协议的。 所以日本的信用卡在插卡消费的时候,也同样使用 EMV 协议。因为 FeliCa 协议只在【非触控】场景下使用。 So,这个时期,外国游客在日消费,可以使用信用卡插卡消费,但无法使用 Touch / Contactless【非触控】刷卡消费。</p></blockquote><blockquote><p>对于在日本申请的信用卡实体卡,本身就是支持 EMV 和 FeliCa 两种通信协议。在日本的时候,读卡器可以插卡识别 EMV,也可以【非触控】识别 FeliCa。 当日本人出国在境外刷卡,外国的读卡器都是支持 EMV 插卡 &amp;【非触控】识别,所以日本信用卡在国外使用完全不受影响。</p></blockquote><p>对于国外信用卡在日本不能很好使用【非触控】的体验问题,并没有好的解决办法。FeliCa 历史已久,在速度方面很优秀,Pasmo 等众多卡都使用这个协议,根子是不能替换的。</p><p>所以日本开始升级读卡器,截至目前,很多读卡器也都同时支持 EMV 和 FeliCa 两种协议的 NFC 识别了。</p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090137924.png" width="60%"><p>同时,随着 Apple/Google Pay 的发展,现在 Apple Wallet 也支持 Quicpay/iD 支付了,在日本称为【电子货币】。</p><p>在 Apple Wallet 中见到 Quicpay / iD 的场景,都是信用卡场景。即 wallet 中,一张卡的右下角,同时具有 Quicpay + Visa 或者 iD + masterCard Logo。</p><p>有些钱包公司发行的卡,仅仅支持 Quicpay 或者 iD,就没有 Visa 或者 masterCard Logo 了,即这个卡就不能在国外使用啦。</p><blockquote><p>但从 Apple Wallet 中 Visa、masterCard 支持 Quicpay/iD,被称为【电子货币】或许有些奇怪。 若从 Quicpay/iD 自身的功能场景出发,它们本身就是提供预充值的实体卡而后刷卡消费,的确属于【电子货币】。 只是 信用卡 虽然走了 Quicpay/iD 的支付通道,又的确和【电子货币】没关联,其实依旧属于【信用卡支付】。 so,Quicpay/iD 被称为【电子支付】,完全是根据其自身的原始功能,下的定义。</p></blockquote><p>最终在说明一点,在日本银行发行的信用卡,基本上都是和 Quicpay 或者 iD 合作的。有些钱包公司借壳发卡行发行的卡,有可能没有 Quicpay / iD。</p><p>这里有一份列表,via <span class="exturl" data-url="aHR0cHM6Ly9hdGFkaXN0YW5jZS5uZXQvYXBwbGUtcGF5LWphcGFuLWNyZWRpdC1jYXJkcy8=">https://atadistance.net/apple-pay-japan-credit-cards/</span></p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202412090138969.png" width="60%"><hr>

apple frameworks

作者 海驴
2024年11月14日 13:16
<p>动态库 &amp; 静态库,若单独输出并接入主工程,逻辑倒很清晰。如果牵涉到多个 framework 不同类型并且相互依赖,会增加不少复杂度。<br>以下说明中,动态库使用 DFramework 表示,静态库使用 SFramework 表示。</p><h1 id="1-framework"><a href="#1-framework" class="headerlink" title="1 framework"></a>1 framework</h1><p>仅需要提供 1 个 framework,直接提供 DFramework 即可。DFramework 优先,必要的时候再提供 SFramework</p><h1 id="n-framework"><a href="#n-framework" class="headerlink" title="n framework"></a>n framework</h1><p>因为代码结构分组,可能需要提供多个 framework:</p><ol><li>DFramework 优先,能不提供 SFramework 就不要提供<ul><li>有 n 个 DFramework 就提供 n 个。相互之间做好依赖文档,App 需要根据依赖文档 <strong>flat</strong> 平铺接入所有需要的 DFramework。</li><li>如果 b DFramework 仅仅被 a DFramework 依赖,可以将 b 打入 a DFramework 的 <code>frameworks</code>文件夹中,通过 <code>embed</code> 实现。<ul><li>这样就可以不再文档中说明 b DFramework 的存在,对外界透明。</li><li>这种方案,最终的 app 中 framework 不是 flat 平铺的,而是具有层级结构,如 :App - a DFramework - b DFramework</li></ul></li><li>如果 b DFramework 同时被 m DFramework 和 n DFramework 依赖,不能将 b 隐藏(打入 m 和 n 中)。<ul><li>否则,App 集成 m 和 n 的时候,b 会出现多份。如果 b 的版本不一样,将按照打包顺序只使用其中一个。</li></ul></li></ul></li></ol><span id="more"></span><ol start="2"><li>如果提供 SFramework,则将所有的 framework 做成依赖,输出一份 SFramework 即可。<ul><li>如果有按需接入的场景,按按照场景输出 n 个 SFramework。</li></ul></li></ol><h1 id="依赖开源库"><a href="#依赖开源库" class="headerlink" title="依赖开源库"></a>依赖开源库</h1><p>一般这种场景是因为开发 framework 的同时,又使用了开源的库。这个时候如何处理开源库是个问题(开源库一般都是 源码 提供的,不讨论 闭源包 提供方式,这个和上面的场景一致)。<br>由于 apple 的 Swift Package Manager 三方库管理的介入,一个 module 在最终的 app 中到底是 dymanic 还是 static,已经不可控了。SPM 会自行管理,不像 cocoapods 的时候我们可以自行控制。</p><blockquote><p>如果是源码提供者,可以增加 dymanic / static 约束。但很多源码都不会提供这个选项,而是让 SPM 自行管理采用哪一个。</p></blockquote><p>在这种场景下,开发的 framework 如果对开源库有依赖,就必须考虑是否【去开源】事宜了。<br>另外,除了自己使用开源库,App 主工程开发者,可能也需要使用同一个开源库,这时候还会出现【版本不一致问题】。<br>以下场景,假设有 T 开源代码需要使用:</p><h2 id="不封装,让外界接入"><a href="#不封装,让外界接入" class="headerlink" title="不封装,让外界接入"></a>不封装,让外界接入</h2><p>framework 需要依赖 T</p><ul><li>如果不是强依赖,在 xcode 中使用 <code>-weak_framework</code>进行标记,这样业务也可以不接入,相当于默认不使用这个功能。</li><li>如果是强依赖,并且在 文档 中说明业务需要接入 x 的具体版本范围。不接入无法编译或者 app 启动闪退。</li></ul><h2 id="封装,外界无感"><a href="#封装,外界无感" class="headerlink" title="封装,外界无感"></a>封装,外界无感</h2><p>第一步就是需要对 T 进行【模块隔离】,否则外界也接入 T 的时候,App 主工程和我们提供的 framework 同时有 T 代码,会有问题。<br>虽然 xcode 在编译连接的时候,会处理好这个问题,使得同一份代码尽量在 app 中仅保留一份。<br>但如果开发人员使用不恰当或者配置错误,很可能导致出现两份 T 源码在项目中。<br>这个时候如果版本还不兼容,就会有非常大的调试和异常隐患。<br>模块隔离方案:<strong>修改 Product Name,将 T 变成 TT 模块</strong><br>然后,还需要考虑将 TT 变成静态库还是动态库:</p><h3 id="静态库:"><a href="#静态库:" class="headerlink" title="静态库:"></a>静态库:</h3><p>这是完全去开源的方案。变成静态库后,TT 源码将打入 framework 二进制中一起提供。因为模块隔离,外部也无法调用和感知。</p><ul><li>通过对 framework 二进制字符表进行分析,还是能够看到 T 的踪迹。</li><li>如果不做模块隔离,也能成功。主 app 也使用 T 模块的时候,链接的时候将自动保留 1 份静态代码。有版本差异的话,出现异常问题将非常难以排查。</li></ul><h3 id="动态库:"><a href="#动态库:" class="headerlink" title="动态库:"></a>动态库:</h3><p>使用动态库的话,如前面【需要提供多个 framework】所描述的,有两种方案:</p><ul><li>TT 可以提供给业务,让业务自行接入。是 flat 平铺 结构。</li><li>也可以内嵌到 m framework 的 frameworks 文件夹中专门供 m framework 使用。是 内嵌 结构。<ul><li>如果不做模块隔离,也可以内嵌。但主 app 可能也使用了 T 模块,使得同一个工程包含 2 个版本的 T 模块,运行的时候按照 xcode 编译顺序,只有一份被使用。有版本差异的话,出现异常问题将非常难以排查。</li></ul></li></ul><hr>

Mac - Command Line Tools

作者 海驴
2024年11月11日 11:38
<p>Mac 系统上,默认没有提供 git、xcodebuild 这些开发者命令。所以当一些终端命令 (brew 等) 被触发后,macos 系统会弹窗提醒用户进行【开发者工具】的安装,即【Command Lines Tools】(下面简称 tools)。<br>如果安装了不同版本的 xcode,则每个版本都会携带各自 xcode 版本的【tools】。<br>上面提到的系统弹窗,是不需要安装 xcode 也可以安装【tools】。但这个 tools 版本会比较低,内部的命令数量也会少于 xcode 携带的。即:最新版本的 xcode,携带的 tools 也是最新的。<br>这里就可以发现有多个 tools 目录了,如下:</p><ol><li>非 xcode 携带:<code>/Library/Developer/CommandLineTools</code></li><li>xcode:<code>/Applications/Xcode.app/Contents/Developer</code></li><li>xcode beta: <code>/Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer</code></li></ol><p>在终端执行 <code>git</code> 、<code>xcodebuild</code> 的时候,一定会使用某一个 tools 中的命令,可具体使用哪一个呢?</p><span id="more"></span><h1 id="xcode-select"><a href="#xcode-select" class="headerlink" title="xcode-select"></a>xcode-select</h1><p>通过 <code>xcode-select</code> 可以方便的切换 main tools,默认也只有 main tools 会生效。可以通过 <code>man xcode-select</code> 查看简介。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">xcode-select --install // 安装非 xcode 版本的 tools</span><br><span class="line">xcode-select --switch &lt;path&gt; // 切换不同的 tools 作为 main tools</span><br><span class="line">xcode-select -p // 打印当前 main tools path</span><br></pre></td></tr></tbody></table></figure><p>当 tools 被指定为 <code>xcode-xx-beta.app/xx/Develop</code> 目录的时候:<br>在终端中执行 git 命令,最终就会使用:<code>/Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer/usr/bin/git</code><br>xcode-select 默认只有 1 个 tools 被激活,是操作系统级别的全局有效。<br>如果在 a tools 激活的同时,希望使用 b tools 该怎么办呢?在多版本 xcode 共存的时候,可能需要使用不同的 tools 命令进行开发。<br>这个时候在当前的命令行环境中设置<code>DEVELOPER_DIR</code> 就可以临时切换 tools 目录:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">// 1</span><br><span class="line">&gt; xcrun --find git</span><br><span class="line">/Applications/Xcode-16.2.0-Beta.2.app/Contents/Developer/usr/bin/git</span><br><span class="line">// 2</span><br><span class="line">export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"</span><br><span class="line">// 3</span><br><span class="line">&gt; xcrun --find git</span><br><span class="line">/Applications/Xcode.app/Contents/Developer/usr/bin/git</span><br></pre></td></tr></tbody></table></figure><h1 id="xcrun"><a href="#xcrun" class="headerlink" title="xcrun"></a>xcrun</h1><p>当在终端中执行 <code>git</code> 命令的时候,使用的是 tools 中的 git,这里需要探究一下。</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">// 1</span><br><span class="line">&gt; where git</span><br><span class="line">/usr/bin/git</span><br><span class="line">// 2</span><br><span class="line">&gt; xcrun --find git</span><br><span class="line">/Applications/Xcode.app/Contents/Developer/usr/bin/git</span><br></pre></td></tr></tbody></table></figure><p>通过 where 发现,<code>/usr/bin</code> 目录下是存在 git 二进制可执行文件的。为什么说执行的是 tools 中的 git 可执行文件呢?<br>这就要说到 xcode-select 维护多个 tools 的意义了。<br><code>/usr/bin</code> 是 path 路径,可以直接找到命令。但是像 git 这些命令依托不同的 tools 环境提供,总不能在切换 main tools 的时候,把这些命令 copy 到 <code>/usr/bin</code> 中。所以对于 <code>/usr/bin</code> 中的 git、xcodebuild 等命令,在具体执行的时候都执行的是 main tools 中的可执行文件。<br>实现这个能力的,就是 【xcrun】。</p><p>xcrun 的目标只有一个,就是根据 xcode-select 设定的 mail tools 目录,在其下面寻找命令。可以通过 <code>man xcrun</code> 查看详细:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">Options:</span><br><span class="line"> -h, --help show this help message and exit</span><br><span class="line"> --version show the xcrun version</span><br><span class="line"> -v, --verbose show verbose logging output</span><br><span class="line"> --sdk &lt;sdk name&gt; find the tool for the given SDK name</span><br><span class="line"> --toolchain &lt;name&gt; find the tool for the given toolchain</span><br><span class="line"> -l, --log show commands to be executed (with --run)</span><br><span class="line"> -f, --find only find and print the tool path</span><br><span class="line"> -r, --run find and execute the tool (the default behavior)</span><br><span class="line"> -n, --no-cache do not use the lookup cache</span><br><span class="line"> -k, --kill-cache invalidate all existing cache entries</span><br><span class="line"> --show-sdk-path show selected SDK install path</span><br><span class="line"> --show-sdk-version show selected SDK version</span><br><span class="line"> --show-sdk-build-version show selected SDK build version</span><br><span class="line"> --show-sdk-platform-path show selected SDK platform path</span><br><span class="line"> --show-sdk-platform-version show selected SDK platform version</span><br></pre></td></tr></tbody></table></figure><p>上面示例中的 <code>xcrun --find git</code> 就是用来找 git 二进制可执行文件路径的。<br>当 git 在 tools 中的路径找到后,原先执行的 <code>/usr/bin/git</code> 命令,也就通过这个新的执行文件来实现了。</p><hr>

DNS 的 CNAME 是如何工作的

作者 海驴
2024年11月10日 22:54
<p>今天配置 cloudflare 中站点的 dns,遇到一些关于 cname、tls、cloudflare 代理相关的问题,做了下梳理。重点有下:</p><ol><li>cname 负责提供目标 ip,在机制上类似【权威域名服务器】</li><li>目标 ip 所在的服务如果非自行掌控(比如强行设置一个目标域名),很可能无法工作。</li><li>cloudflare 设置 cname 的时候有一个【代理】功能。这个设计非常糟糕,完全脱离了 cname 的本意。</li></ol><p>以下介绍中,使用 cloudflare 配置 <code>test.yigegongjiang.com</code> 的 cname 为 <code>httpbin.org</code>,默认不配置【代理】。</p><span id="more"></span><h1 id="CNAME-是做什么用的"><a href="#CNAME-是做什么用的" class="headerlink" title="CNAME 是做什么用的"></a>CNAME 是做什么用的</h1><p>DNS 解析中,通过 <code>dig A test.yigegongjiang.com</code>,<code>dig A httpbin.org</code> 会有如下返回(参数 A 表示返回目标域名的 ipv4):</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"> // dig A test.yigegongjiang.com</span><br><span class="line"> </span><br><span class="line"> ;; ANSWER SECTION:</span><br><span class="line">-&gt; test.yigegongjiang.com. 300 IN CNAME httpbin.org.</span><br><span class="line"> httpbin.org. 60 IN A 54.243.34.18</span><br><span class="line"> httpbin.org. 60 IN A 34.206.181.91</span><br><span class="line"> httpbin.org. 60 IN A 3.222.34.231</span><br><span class="line"> httpbin.org. 60 IN A 35.172.59.156</span><br><span class="line"> httpbin.org. 60 IN A 54.237.204.19</span><br><span class="line"> httpbin.org. 60 IN A 184.73.239.81</span><br><span class="line"> </span><br><span class="line"> // dig A httpbin.org</span><br><span class="line"> ;; ANSWER SECTION:</span><br><span class="line"> httpbin.org. 60 IN A 3.222.34.231</span><br><span class="line"> httpbin.org. 60 IN A 34.206.181.91</span><br><span class="line"> httpbin.org. 60 IN A 54.243.34.18</span><br><span class="line"> httpbin.org. 60 IN A 184.73.239.81</span><br><span class="line"> httpbin.org. 60 IN A 35.172.59.156</span><br><span class="line"> httpbin.org. 60 IN A 54.237.204.19</span><br></pre></td></tr></tbody></table></figure><p>可以发现,如果需要找到 <code>test.yigegongjiang.com</code> 的 A 记录,一定需要先找到 CNAME 记录,再通过 CNAME 指向的域名继续寻找目标 ip。</p><p>所以这里提出 <strong>CNAME</strong> 的第一个作用,就是【设定 IP】。使用 github pages 搭建博客的时候:</p><ol><li>购买了域名 m ,希望将 m 域名映射到 n.github.io Blog 域名上。</li><li>后续直接访问 m,ip 被重定向到 n.github.io,从而完成 Blog 的访问。</li></ol><p>其次,<strong>CNAME</strong> 还有一个作用是【负载均衡】。如使用 xx 云平台部署服务:</p><ol><li>在 xx 云平台上的多个边缘节点部署了服务,并统一使用域名 m 向用户提供服务。</li><li>无需自行搭建【权威域名服务器】,通过 xx 云平台提供的 n.xx.com 做域名 m 的 cname 指向。</li><li>用户请求域名 m 的时候,DNS 解析会进入 n.xx.com,xx 云平台负责根据用户的地理位置通过 n.xx.com 提供动态的 ip,实现【边缘访问】和【负载均衡】。</li></ol><p>Anyway,不管 CNAME 通过中继域名实现【设定 IP】,还是做【负载均衡】,CNAME 本质上都是为初始域名提供 ip。<br>不像 A / AAAA 记录,强制设定了一个固定 ip。CNAME 提供了一个【缓冲层】,可以做更多的事情。</p><p>这里也会出现一些问题,通过缓冲层获取到的 ip 所在的服务器,可能无法很好的处理我们的 m 域名请求。</p><h1 id="CNAME-目标服务器注意事项"><a href="#CNAME-目标服务器注意事项" class="headerlink" title="CNAME 目标服务器注意事项"></a>CNAME 目标服务器注意事项</h1><p>假设我们回到了没有 tls / ssl 安全验证的 http 场景,将 <code>test.yigegongjiang.com</code> 的 cname 指向 <code>www.google.com</code> 或者 <code>www.facebook.com</code>,可以访问 Google 和 facebook 吗?<br>理论上可以,实际并不行。如果把 cname 指向 <code>httpbin.org</code>,却又可以正常访问。<br>进行 http 请求的时候,主机域名 <code>test.yigegongjiang.com</code> 会通过 【host】字段携带到目标服务器,此时目标服务器完全可以主动拒绝服务,因为不是它们自己的域名。<br>而<code>httpbin.org</code>能正常访问,是它没有限制请求中的 host。</p><p>回到 https 场景,有了 <a href="https://www.yigegongjiang.com/2023/signature/#%E6%95%B0%E5%AD%97%E7%AD%BE%E5%90%8D%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%EF%BC%9ASSL">tls / ssl 安全证书</a>,又变得不一样了。<br>同样进行上面的设定,不管 cname 指向哪个域名,浏览器都会弹窗告诉我们不安全。因为 <code>www.google.com</code> 返回的 tls 证书,验证的域名是 <code>*.google.com</code>,而用户访问的域名是 <code>test.yigeogngjiang.com</code>,不匹配!证书验证不通过!<br>甚至于有些目标域名,连安全弹窗都不会弹起。</p><blockquote><p>服务器如何返回证书,是通过 tls 认证中的【SNI】标记识别具体服务的(一个服务器可以开 n 个服务,有 n 个证书)。<br>如何服务器都不认识 <code>test.yigegongjiang.com</code>这个 SNI 标记,那么它可能连 tls 证书都不会返回。</p></blockquote><p>上面提到的 google、facebook、httpbin 服务,虽然不认识 <code>test.yigegongjiang.com</code> 这个域名,但还是返回了他们各自默认的 tls 证书。此时,如果用户选择强制信任证书:</p><ol><li>google、facebook 回到了 http 时候一样,因为 host 不匹配,主动拒绝服务了。</li><li>httpbin 也回到了 http 时候一样,可以正常访问。因为 httpbin 默认没有限制 host。</li></ol><p>对于一台 Nginx 配置的目标服务器(假设默认域名是 a.com),如果需要完美兼容 <code>test.yigegongjiang.com</code> 这个域名的 cname 映射,需要做两件事:</p><ol><li>识别到 SNI 是 <code>test.yigegongjiang.com</code> 后,需要返回 <code>yigegongjinag.com</code> 的 tls 证书,供浏览器进行安全链验证。<br> a. 或者返回一个 tls 证书,该证书同时包含 <code>a.com</code> 和 <code>yigegongjiang.com</code> 的认证。</li><li>将 <code>a.com</code> 和 <code>test.yigegongjiang.com</code> 这两个 host 同时指向同一个内部的 port 服务。<br> a. 或者不识别 host,所有 host 都指向同一个内部的 port 服务。</li></ol><p>上面的两个方案,如果是特殊的服务场景倒还能实现,如 github page 提供了 static blog 服务,可以根据我们配置的域名来进行设置,从而完成访问。<br>对于 <code>fly.io</code>、<code>koyeb.com</code> 这些云平台,自定义域名就是它们的收费项,那肯定不会放开这个口子。</p><h2 id="通过-代理-实现-koyeb-com-云平台自定义域名?"><a href="#通过-代理-实现-koyeb-com-云平台自定义域名?" class="headerlink" title="通过 代理 实现 koyeb.com 云平台自定义域名?"></a>通过 代理 实现 koyeb.com 云平台自定义域名?</h2><p>如果有一个免费的中间代理,我们访问 <code>test.yigegongjiang.com</code> 的所有请求,这个中间代理帮我们将所有流量正向代理到 koyeb.com 平台上,不就可以既不买 koyeb 的服务,又能实现自定义域名了吗?<br>cloudflare 提供了这个能力,也是它的安全、缓存等一系列功能的起点。<br>cloudflare 配置 dns cname 的时候,有一个【代理】选项。选中后,当前配置虽然看起来是 cname,但完全脱离了 cname 本意。</p><ol><li>此时不是 cname 了,通过 dig 工具会发现此时返回的是 cloudflare 的 ip</li><li>访问域名的流量,会被 cloudflare 转发到 cname 指向的域名上,这里的 cname 充当【代理】的角色。</li></ol><p>实际上,依旧访问不通。显然 koyeb 这些云平台不会留下这么大的空子给别人钻。<br>因为 cloudflare、aws 这些正规的代理服务,在请求目标服务的时候,都会提供完善的信息已告知目标服务:我是代理。<br>它不仅把原始的域名等信息提供给了 koyeb,还会把自己的信息都提供给 koyeb。所以,koyeb 直接拒绝服务就行了。</p><p>当然,我们可以自己搭建一个【猥琐】的【三级代理】,将 cloudflare 那边的代理流量打过来,然后再去请求 koyeb,此时【猥琐代理】完全装作正常用户的浏览器,那 koyeb 的确是察觉不出来的。<br>此时,cloudflare 又能够捕获到完整的数据,实现缓存、边缘节点、cdn 等等能力。</p><hr><pre><code></code></pre>

SSE 指南

作者 海驴
2024年11月1日 12:16
<p>Server-Sent Events(SSE)是一种允许服务器通过 HTTP 连接主动向客户端发送数据的技术。它主要被用于创建实时应用,如消息推送和实时通知。SSE 使用简单的文本格式发送消息,这种格式使得其易于在浏览器中实现和使用。</p><p>SSE 注意事项:</p><ol><li>通过 HTTP 协议通道 建立单向长连接,即 Client 连接 Server 后,Server 不断开连接,并持续的通过 socket 套接字发送 data 给 Client。</li><li>网关等设备会主动关闭 tcp 通道,需要 SSE Server 端增加心跳。很多种场景都会导致 tcp 连接中断,和 IM 心跳一致。这里需要 SSE Server 增加应用层心跳,非 TCP 层心跳。</li></ol><span id="more"></span><h1 id="数据格式"><a href="#数据格式" class="headerlink" title="数据格式"></a>数据格式</h1><p>通过具有明显分割线的消息体,来分割数据字段:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">// 以下文本消息体,最终通过编译成二进制的形式被传递和解析,通过 \n 等标记符号进行【行】分割。</span><br><span class="line"></span><br><span class="line">event: message/userupdate/custom...\n</span><br><span class="line">data: {xxx}\n</span><br><span class="line">id: 98769879675\n</span><br><span class="line">retry: 10000\n</span><br><span class="line">\n</span><br><span class="line"></span><br><span class="line">// 一个完整的消息体如下,通过 \n 分割行,末尾通过 \n\n 分割单个消息体</span><br><span class="line">id: event-id-1\ndata:event-data-first\n\n</span><br></pre></td></tr></tbody></table></figure><p>以上 event/data/id/retry 四个字段中,data 是必须字段,其他三个是可选字段。每个消息体,必须以 \n\n 作为末尾标记。</p><p>在 ts 中,可行的生成消息体的 code 如下:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">private encodeMessage(message: ChatMessage) {</span><br><span class="line"> const data = {</span><br><span class="line"> type: message.type,</span><br><span class="line"> payload: message.data,</span><br><span class="line"> };</span><br><span class="line"> const content = [</span><br><span class="line"> `id: ${message.id}`,</span><br><span class="line"> `data: ${JSON.stringify(data)}`,</span><br><span class="line"> '',</span><br><span class="line"> '',</span><br><span class="line"> ].join('\n'); // 这里,通过空行分割,在消息体的末尾增加 `\n\n` 标记</span><br><span class="line"> return this.encoder.encode(content);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>对消息体的解析,也同样遵循固定的规律,即对二进制中的 \n 和 \n\n 进行解析。</p><p>解析流程:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 通过 \n\n 对消息体进行分割,获得一个消息体的二进制内容并开始解析</span><br><span class="line">2. 通过 \n 对行进行解析,获得 xxx:xxx 这样的一行内容</span><br><span class="line">3. 通过 : 符号,对行进行解析,获得 key:xxx 和 value:xxx 两个内容</span><br><span class="line">4. 整合获得的所有内容,聚合成 {id:xxx,event:xxx,data:xxx,retry:xxx} 这样的消息体</span><br></pre></td></tr></tbody></table></figure><h1 id="消息类型"><a href="#消息类型" class="headerlink" title="消息类型"></a>消息类型</h1><p>SSE 通过 event 字段,可以自定义各种消息体。约定通用的消息体是 message。有如下两种消息体:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">id: xxx\ndata:xxx\nevent:message\n\n</span><br><span class="line">id:xxx\ndata:xxx\nevent:{custom-name}\n\n</span><br></pre></td></tr></tbody></table></figure><p>如果 event 为空,默认当作且应该当作 message 消息来解析和处理。</p><h1 id="重连"><a href="#重连" class="headerlink" title="重连"></a>重连</h1><p>http 通道可能发生中断,每条消息题都可以携带一个 retry 字段,用来告知 client 在断开后多久应该重连。</p><p>而重连的逻辑就是重新发起 http 连接。这里为了保持和之前的通道一致,应该在重连的时候在 header 中携带最后一次收到的消息的 id,可以让 server 侧知道从哪里中断的,以保持连续的服务。</p><h1 id="心跳"><a href="#心跳" class="headerlink" title="心跳"></a>心跳</h1><p>SSE 应该使用应用层心跳,即发送一条空消息体,以保持 http tcp 套接字不被网关、nat 等场景强制关闭。格式如下:</p><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">:\n\n</span><br><span class="line">:heartbeat\n\n</span><br></pre></td></tr></tbody></table></figure><p>这里,心跳可以不遵循消息体的约定。</p><p>之前提到消息体一定需要有 data 字段,因为每一条消息都是需要传递信息,如果没有 data 字段,那么这条消息就无法被解析,也就没有传递的必要。</p><p>但是这套约定,并不是说没有 data 字段,消息的发送、接收、解析 整套流程就会失败,因为对于 TCP/UDP/HTTP 这套协议来说,它并不关心消息体的内容是什么。</p><p>而 Client 端,对消息体的解析应该是包容的,即消息体如果不符合约定,那么就应该抛弃。</p><p>这里的应用层心跳,就会走到这套逻辑里面。IM 是双向通信,为了保障心跳的到达,Client 端需要解析完整的心跳并回执。在 SSE 单向通道里面,只需要保障有一条消息从 Server 发往 Client 即可(没有成功率保障,因为是单向通信)。</p><hr>

设备发现

作者 海驴
2024年10月23日 14:08
<h1 id="Bonjour"><a href="#Bonjour" class="headerlink" title="Bonjour"></a>Bonjour</h1><p>local net 服务发现。核心是两个 api:NSNetService &amp; NSNetServiceBrowser。</p><p>Bonjour 的目的,是希望发现局域网中的设备,包括这些设备的 ip、port 等信息,从而进行下一步业务操作。因为有了 ip、port,就可以精确定位一台设备了,就可以做很多事情了,比如打通 socket 等。</p><p>NSNetService 用来发布服务,表明 m 提供了哪些能力,如打印机、http 服务等等。</p><span id="more"></span><figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 通过指定服务名称、类型和端口号来创建 NSNetService 实例</span></span><br><span class="line">netService = <span class="title class_">NSNetService</span>(<span class="attr">domain</span>: <span class="string">"local."</span>, <span class="attr">type</span>: <span class="string">"_http._tcp."</span>, <span class="attr">name</span>: <span class="string">"MyService"</span>, <span class="attr">port</span>: <span class="number">8080</span>)</span><br></pre></td></tr></tbody></table></figure><p>NSNetServiceBrowser 用来检索服务,查找当前 local net 中存在哪些与查询内容相匹配的服务,进而可以获取到 ip 和 port。</p><figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">netServiceBrowser = <span class="title class_">NSNetServiceBrowser</span>()</span><br><span class="line"><span class="comment">// 查找局域网中所有 _http._tcp. 类型的服务</span></span><br><span class="line">netServiceBrowser?.<span class="title function_">searchForServices</span>(<span class="attr">ofType</span>: <span class="string">"_http._tcp."</span>, <span class="attr">inDomain</span>: <span class="string">"local."</span>)</span><br></pre></td></tr></tbody></table></figure><h3 id="参数约定-from-ai"><a href="#参数约定-from-ai" class="headerlink" title="参数约定 - from ai"></a>参数约定 - from ai</h3><figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">`NSNetService`</span> 的参数 <span class="string">`domain`</span> 和 <span class="string">`type`</span> 并不是可以随意设置的,它们有明确的约定和用途,尤其是为了确保服务发现的兼容性和正确性。</span><br><span class="line"></span><br><span class="line">### <span class="number">1.</span> <span class="string">`domain`</span> 参数:</span><br><span class="line">- **约定**:</span><br><span class="line"> - 通常,局域网内使用的域名是 <span class="string">`"local."`</span>,这是 <span class="title class_">Bonjour</span> 标准中定义的默认域,表示当前局域网范围内的服务。</span><br><span class="line"> - <span class="title class_">Bonjour</span> 中一般使用的是 <span class="string">`"local."`</span>,它会自动将服务广播到本地网络。</span><br><span class="line"> - 如果你要在其他特定域中发布服务,也可以指定其他域名,但在局域网服务发现中几乎总是使用 <span class="string">`"local."`</span>。</span><br><span class="line"></span><br><span class="line">- **是否可以随意更改?**:</span><br><span class="line"> - 对于局域网内的服务发现,推荐使用 <span class="string">`"local."`</span>,它是 <span class="title class_">Bonjour</span> 默认的局域网广播域。</span><br><span class="line"> - 如果你有自己的 <span class="variable constant_">DNS</span> 域名或网络环境,你可以指定自定义域名,但这通常涉及更复杂的网络设置,且在局域网内使用可能不兼容。</span><br><span class="line"></span><br><span class="line">### <span class="number">2.</span> <span class="string">`type`</span> 参数:</span><br><span class="line">- **约定**:</span><br><span class="line"> - <span class="string">`type`</span> 定义了服务的协议和传输层信息。它遵循 <span class="variable constant_">IANA</span>(<span class="title class_">Internet</span> <span class="title class_">Assigned</span> <span class="title class_">Numbers</span> <span class="title class_">Authority</span>)发布的服务类型命名标准,并且通常采用如下格式:</span><br><span class="line"> - <span class="string">`_&lt;protocol&gt;._&lt;transport&gt;`</span></span><br><span class="line"> - 例如:</span><br><span class="line"> - <span class="string">`"_http._tcp."`</span>:表示 <span class="variable constant_">HTTP</span> 服务使用 <span class="variable constant_">TCP</span> 传输协议。</span><br><span class="line"> - <span class="string">`"_ftp._tcp."`</span>:表示 <span class="variable constant_">FTP</span> 服务使用 <span class="variable constant_">TCP</span> 传输协议。</span><br><span class="line"> - <span class="string">`"_ipp._tcp."`</span>:表示 <span class="title function_">IPP</span> (<span class="title class_">Internet</span> <span class="title class_">Printing</span> <span class="title class_">Protocol</span>) 使用 <span class="variable constant_">TCP</span> 传输协议。</span><br><span class="line"></span><br><span class="line"> - **标准服务类型**:有许多常见的服务类型,诸如:</span><br><span class="line"> - <span class="string">`"_http._tcp."`</span>:<span class="variable constant_">HTTP</span> 服务</span><br><span class="line"> - <span class="string">`"_ftp._tcp."`</span>:<span class="variable constant_">FTP</span> 服务</span><br><span class="line"> - <span class="string">`"_airplay._tcp."`</span>:<span class="title class_">AirPlay</span> 服务</span><br><span class="line"> - <span class="string">`"_ipp._tcp."`</span>:网络打印协议</span><br><span class="line"> - <span class="string">`"_ssh._tcp."`</span>:<span class="variable constant_">SSH</span> 服务</span><br><span class="line"></span><br><span class="line">- **是否可以随意更改?**:</span><br><span class="line"> - 如果你想发布一个标准协议的服务,必须遵循该协议的命名约定(例如,<span class="string">`"_http._tcp."`</span> 代表 <span class="variable constant_">HTTP</span> 服务)。</span><br><span class="line"> - 如果是自定义服务(非标准服务),你可以创建自定义的类型名称。例如,你可以创建 <span class="string">`"_mycustomservice._tcp."`</span> 这样自定义的服务类型。但是,自定义类型必须以下划线 <span class="string">`_`</span> 开头,以确保与标准服务类型区分。</span><br><span class="line"></span><br><span class="line">### 总结:</span><br><span class="line">- **<span class="string">`domain`</span>**:一般使用 <span class="string">`"local."`</span>,这是 <span class="title class_">Bonjour</span> 的局域网发现默认域。如果你在局域网中进行服务发现,最好不要随意修改这个参数。</span><br><span class="line">- **<span class="string">`type`</span>**:对于标准协议服务,如 <span class="variable constant_">HTTP</span>、<span class="variable constant_">FTP</span> 等,请使用标准的 <span class="variable constant_">IANA</span> 定义的服务类型。如果是自定义服务类型,可以按格式自定义命名,但仍需遵循命名规范(下划线开头)。</span><br><span class="line"></span><br><span class="line">确保遵循这些约定可以提高服务的兼容性,并确保局域网中的其他设备能够正确发现和解析服务。</span><br></pre></td></tr></tbody></table></figure><h1 id="MultipeerConnectivity"><a href="#MultipeerConnectivity" class="headerlink" title="MultipeerConnectivity"></a>MultipeerConnectivity</h1><p>是 Apple 提供的近场通信,基于蓝牙和局域网实现。</p><p>原理也是服务的发布和发现,和 Bonjour 的不同点是,除了局域网之外,还可以通过 蓝牙 来做服务发现。</p><p>和 Bonjour 的差异:</p><ol><li>Bonjour 是为了发布、发现目标设备的 ip、port。</li><li>MultipeerConnectivity 是为了发布、发现目标设备。注意,这是的设备是一个 peer、一个实体,通过这个 peer,可以直接进行 data 的发送等,即这是一个对等的实体对象,可以直接进行通信。发送图片、文字、视频等。</li></ol><h1 id="Network-framework"><a href="#Network-framework" class="headerlink" title="Network.framework"></a>Network.framework</h1><p>这是 Apple 推出的兼容高层级 API 和 低层级 API 的库,它有高级代码封装部分可供开发人员直接使用,也有底层代码部分可供开发人员拼装各种能力。</p><p>在使用上,Network.framewok 完全可以代替 Bonjour 了,因为它集成了 Bonjour 的能力。</p><p>但还不能代替 MultipeerConnectivity,因为 MultipeerConnectivity 的蓝牙通信能力,Network.framework 并没有。</p><p>MultipeerConnectivity 对于 p2p 场景是高度封装的,使用方便、使用场景范围大,但不支持细粒度的调整优化。比如发送数据,就是调用 api 进行数据 send。具体如何 send 并不能控制。</p><p>但是 Network.framework 提供了细粒度的 api 操作,可以选择 tcp、udp 等不同方案以及细节参数。</p><p>Network.framework 更多用于具有网络能力的设备间通信。当然,如果只有蓝牙场景,那只能使用 MultipeerConnectivity 了。</p><h1 id="WI-FI-Direct"><a href="#WI-FI-Direct" class="headerlink" title="WI-FI Direct"></a>WI-FI Direct</h1><p>上面三种局域网近景通信,都不具有普适性。蓝牙是很有普适性的产物,但是蓝牙发展这么多年,有很大的局限性,如速度、距离等。</p><p>目前,WI - FI Direct 已经基本上进入了所有的电子设备中,通过 WI - FI direct,可以非常方便的在两台设备之间打通 p2p 通信通道,进而实现高速数据传输。</p><p>对于苹果全家桶,基本上所有的跨设备互通能力,都是通过 Wi-Fi Direct 能力来实现的,比如 Handoff、通用剪贴板(Universal Clipboard)等。</p><h2 id="Wi-Fi-Direct-技术原理-from-ai"><a href="#Wi-Fi-Direct-技术原理-from-ai" class="headerlink" title="Wi-Fi Direct 技术原理 - from ai"></a>Wi-Fi Direct 技术原理 - from ai</h2><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line">1. **基本概念**</span><br><span class="line">- Wi-Fi Direct(也称 Wi-Fi P2P)是一种无线通信标准</span><br><span class="line">- 允许设备间直接通信,无需传统的无线接入点(如路由器)</span><br><span class="line">- 工作在与普通 Wi-Fi 相同的 2.4GHz 或 5GHz 频段</span><br><span class="line">1. **网络架构**</span><br><span class="line">- **组网模式**: 采用对等(P2P)网络架构</span><br><span class="line">- **角色分配**:</span><br><span class="line"> - 组主(Group Owner, GO): 类似微型接入点,负责管理网络</span><br><span class="line"> - 客户端: 连接到组主形成网络</span><br><span class="line">- **组主职责**:</span><br><span class="line"> - 信道选择与管理</span><br><span class="line"> - 网络参数配置(SSID、安全设置等)</span><br><span class="line"> - 资源分配与网络维护</span><br><span class="line">1. **连接建立流程**</span><br><span class="line">- 设备发现阶段:</span><br><span class="line"> 1. 设备相互扫描并发现对方</span><br><span class="line"> 2. 通过设备发现协议交换基本信息</span><br><span class="line">- 组建立阶段:</span><br><span class="line"> 1. 协商确定组主身份</span><br><span class="line"> 2. 组主配置并广播网络</span><br><span class="line">- 连接认证阶段:</span><br><span class="line"> 1. 客户端扫描并识别组主网络</span><br><span class="line"> 2. 通过 WPS 进行安全认证</span><br><span class="line"> 3. 建立加密连接</span><br><span class="line">1. **安全机制**</span><br><span class="line">- 采用 Wi-Fi Protected Setup(WPS)快速配置</span><br><span class="line">- 支持多种连接方式:</span><br><span class="line"> - 按钮配对</span><br><span class="line"> - PIN 码认证</span><br><span class="line"> - NFC 近场通信(如支持)</span><br><span class="line">- 使用 WPA2 等标准 Wi-Fi 安全协议加密通信</span><br><span class="line">1. **服务层功能**</span><br><span class="line">- 服务发现协议:</span><br><span class="line"> - 设备可广播自身提供的服务</span><br><span class="line"> - 其他设备可在连接前发现可用服务</span><br><span class="line">- 支持的服务类型:</span><br><span class="line"> - 文件传输</span><br><span class="line"> - 打印服务</span><br><span class="line"> - 多媒体流传输</span><br><span class="line"> - 屏幕镜像等</span><br><span class="line">1. **数据传输特点**</span><br><span class="line">- 直接设备间传输,无需经过中间节点</span><br><span class="line">- 支持高速数据传输</span><br><span class="line">- 较低的通信延迟</span><br><span class="line">- 不依赖互联网连接</span><br></pre></td></tr></tbody></table></figure><h1 id="AWDL"><a href="#AWDL" class="headerlink" title="AWDL"></a>AWDL</h1><p>AWDL(Apple Wireless Direct Link)与 Wi-Fi Direct 和蓝牙都是无线通信技术,用于设备之间的直接连接。主要用于 Apple 设备间的通信,支持如 AirDrop、AirPlay 等服务。AWDL 能在 Wi-Fi 频带上动态频道跳跃,优化连接质量和减少干扰,主要优化了点对点的数据传输速度和效率。</p><p>Apple 的跨设备数据同步功能,基本上都是使用的 AWDL。</p><p>相比蓝牙来说,Wifi-Direct 和 AWDL 的缺点就是建立连接慢(服务发现慢)。所以蓝牙更多的时候用来当作 AWDL 的前置条件,即通过 蓝牙 来发现设备建立连接,然后通过 AWDL 的进行数据传输。</p><p>Apple 提供了非常多的跨端通信功能如 Handoff、AirDrop、Universal Clipboard、Continuity Camera 等。苹果建议打开蓝牙,是为了更快的做服务发现和建立通道。但是蓝牙不打开,很多功能也都是可以正常使用的。因为还有很多备选的发现方案,如上面的 Bonjour 可以在局域网 wifi 下工作。甚至没有 wifi 的时候,也能使用 AWDL 自己的服务发现(速度慢)。</p><p>但是蓝牙,依旧是服务发现的第一优先级。</p><h2 id="apple-服务列表-from-ai"><a href="#apple-服务列表-from-ai" class="headerlink" title="apple 服务列表 - from ai"></a>apple 服务列表 - from ai</h2><figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">### <span class="number">1.</span> 基于<span class="title class_">Wi</span>-<span class="title class_">Fi</span>的直接通信技术:</span><br><span class="line"></span><br><span class="line">- **<span class="title class_">AirDrop</span>**:</span><br><span class="line"> - 使用<span class="title class_">Wi</span>-<span class="title class_">Fi</span>和蓝牙结合的方式,进行设备发现和建立安全的点对点<span class="title class_">Wi</span>-<span class="title class_">Fi</span>网络以传输文件。</span><br><span class="line"> </span><br><span class="line">- **<span class="title class_">AirPlay</span>**:</span><br><span class="line"> - 通过<span class="title class_">Wi</span>-<span class="title class_">Fi</span>网络将音频、视频和图片流式传输到支持<span class="title class_">AirPlay</span>的接收设备上。</span><br><span class="line"></span><br><span class="line">### <span class="number">2.</span> 基于云服务的同步技术:</span><br><span class="line"></span><br><span class="line">- **iCloud <span class="title class_">Drive</span>**:</span><br><span class="line"> - 利用云存储来同步和共享文件和文档。</span><br><span class="line"> </span><br><span class="line">- **<span class="title class_">Messages</span> <span class="keyword">in</span> iCloud**:</span><br><span class="line"> - 通过iCloud同步所有设备上的iMessage,保证消息的一致性。</span><br><span class="line"> </span><br><span class="line">- **iCloud <span class="title class_">Photos</span>**:</span><br><span class="line"> - 照片和视频通过iCloud自动同步到所有设备。</span><br><span class="line"></span><br><span class="line">### <span class="number">3.</span> 基于<span class="title class_">Continuity</span>的互操作功能:</span><br><span class="line"></span><br><span class="line">- **<span class="title class_">Handoff</span>**:</span><br><span class="line"> - 利用蓝牙和<span class="title class_">Wi</span>-<span class="title class_">Fi</span>来实现在设备之间无缝切换正在进行的活动(如邮件撰写、网页浏览)。</span><br><span class="line"></span><br><span class="line">- **<span class="title class_">Universal</span> <span class="title class_">Clipboard</span>**:</span><br><span class="line"> - 使用蓝牙和<span class="title class_">Wi</span>-<span class="title class_">Fi</span>通过<span class="title class_">Continuity</span>功能实现剪贴板内容在设备间的共享。</span><br><span class="line"></span><br><span class="line">- **<span class="title class_">Continuity</span> <span class="title class_">Camera</span>**、**<span class="title class_">Continuity</span> <span class="title class_">Markup</span>** 和 **<span class="title class_">Continuity</span> <span class="title class_">Sketch</span>**:</span><br><span class="line"> - 这些服务通过<span class="title class_">Wi</span>-<span class="title class_">Fi</span>和蓝牙连接,允许用户使用一个设备上的功能来直接影响另一个设备上的内容。</span><br><span class="line"></span><br><span class="line">### <span class="number">4.</span> 扩展显示和图形共享技术:</span><br><span class="line"></span><br><span class="line">- **<span class="title class_">Sidecar</span>**:</span><br><span class="line"> - 将iPad作为外部显示器使用,通过<span class="title class_">Wi</span>-<span class="title class_">Fi</span>或有线连接实现与<span class="title class_">Mac</span>的连接。</span><br></pre></td></tr></tbody></table></figure><h2 id="Handoff"><a href="#Handoff" class="headerlink" title="Handoff"></a>Handoff</h2><p>Apple 全家桶之间,可以通过 Handoff 进行操作转移。</p><ol><li>通过蓝牙做设备发现,蓝牙不可用的时候切换到其他设备发现渠道</li><li>通过 AWDL 做数据传输</li><li>app 通过 Activity api 实现 handoff 功能</li></ol><h2 id="Universal-Clipboard"><a href="#Universal-Clipboard" class="headerlink" title="Universal Clipboard"></a>Universal Clipboard</h2><p>和 Handoff 基本一致,通过 <strong>UIPasteboard</strong> api 实现跨设备 copy-paste 功能</p><h1 id="NFC"><a href="#NFC" class="headerlink" title="NFC"></a>NFC</h1><p>NFC(近场通信),和 以上 通信方式都不同。</p><p>NFC 的技术原理基于无线电频率识别(RFID)技术,使用磁场感应来实现在设备间的通信。NFC 设备在 13.56 MHz 频率上操作,通常用于非接触式数据传输,距离范围非常短,通常在几厘米内,传输速率慢,在 400kbps (50kb / s) 左右。<br>技术原理就是:识卡器发出信号(电磁感应),激活了终端(手机自动点亮),然后进行数据交互,并可能需要机主进行身份验证,最后完成信息的交互(支付、上公交车、开门等)。</p><p>NFC 有三种工作模式:点对点通信模式、读卡器模式、卡模拟模式。又分为【主动模式】和【被动模式】,其中一个设备提供射频场,另一个设备利用这个射频场进行通信。<br>使用 NFC,需要两个终端,一个做控制器,用于发射磁场来识别信息。一个做无电源的数据芯片,通过接收到的磁场来感应并传输信息。<br>对于 NFC 设备来说:</p><ol><li>点对点通信:两个 NFC 设备相互交换信息。</li><li>读卡器模式:一个 NFC 作为控制器,读取其他 NFC 芯片中的信息。</li><li>卡模拟模式:一个 NFC 作为数据芯片,其他读卡器可以读取其中的信息。</li></ol><p>现代电子产品中,Android 和 iPhone 都支持 NFC 技术,手机作为 NFC 设备,使用读卡器模式和卡模拟模式,已经可以完成很多事情。</p><ol><li>当作为 NFC 控制器的时候,手机可以主动的读取外部 NFC 芯片中的信息,也可以将必要的信息写入到外部 NFC 芯片。(物流中,可以通过手机对商品挂载的 NFC 芯片进行记录)</li><li>当作为 NFC 芯片的时候,手机可以模拟一个 NFC 芯片,通过软件将信息提前写入手机中,其他读卡器就可以直接读取手机中的信息。(可以实现门禁卡等)</li></ol><p>Android 对 NFC 的 API 开放较多,app 可通过 api 来控制 NFC 进行 读取、写入、模拟 的操作,来实现快捷的智能家居、门禁卡等场景。<br>iPhone 上则比较保守,在【卡模拟】、【卡读取】方面,都有不少限制。</p><h2 id="门禁卡"><a href="#门禁卡" class="headerlink" title="门禁卡"></a>门禁卡</h2><p>普通门禁卡:</p><ol><li>门禁卡中有 【微芯片】(存储卡片的识别信息和其他数据)和 【天线】(用于接收和发送无线信号)。</li><li>门禁卡靠近门禁系统的读卡器 → 读卡器会发出一个射频信号 → 信号通过天线供电给门禁卡上的微芯片 → 微芯片被激活, 通过天线将存储在芯片上的识别信息发送回读卡器 → 读卡器接收到信息后,将数据传输给后端的门禁控制系统。</li></ol><p>NFC 门禁卡:<br>原理基本和普通门禁卡一样,不过,NFC 提供了更高的安全性。它支持双向通信,卡和识卡器之间可以通信。它们之间会进行密钥交换,通过对称、非对称加密来完成数据的安全传输。相比普通门禁卡,NFC 门禁卡会更加的安全。</p><blockquote><p>NFC 是一种普适性的技术方案,手机也可以作为 NFC 终端。这里就可以把 NFC 门禁卡的信息保存在 手机中,使得手机可以充当 NFC 门禁卡的功能。</p></blockquote><p>蓝牙:<br>有些 app 会通过 蓝牙的方式,和门锁连接。这在智能门锁中非常常见。因为距离很远,就可以连接上。而 NFC 需要非常短的距离 (4cm) 才能通信。</p><h2 id="移动支付"><a href="#移动支付" class="headerlink" title="移动支付"></a>移动支付</h2><p>通过 NFC 进行移动支付,主要有三种方案:</p><ol><li><p>卡模拟方案。mobile 录入支付卡信息,被外部读取器识别<br> a. 系统级别的支持,如 apple pay。开发人员没有掌控能力。用户只能通过 apple wallet 录入银行卡信息,然后通过 apple pay 进行支付。<br> b. 应用级别的支持。Android 支持的较好,iPhone 限制很多。</p><ul><li>iPhone 在 iOS 18.1 放开了该限制,app 可以将支付卡信息写入 app 中,支付的时候调用 app 完成支付。</li><li>不对普通开发者开放,需要和 apple 签订商务协议,支付费用,一般都是支付中间商如 Stripe。并且只对个别国家开放。</li></ul></li><li><p>读卡器方案。mobile 作为读取器识别外部实体卡(信用卡等)。Android 支持的叫好,iPhone 限制很多。<br> a. Apple Tap to pay。商家可以在自己的手机中,打开 m app,然后用户把信用卡、iWatch 靠近手机,即可完成支付。</p><ul><li>普通开发人员没有太多的控制能力。也需要签订商务协议,一般都是支付中间商如 Stripe。它们提供 SDK 并和 Apple Api 交互完成支付。</li><li>Apple 和 Stripe 中间商会对地区等有限制。只在少有的地区开放了 Tap To Pay 能力。</li></ul><p> b. alipay 碰一碰。非常聪明的通过 NFC 实现支付的方案。</p><ul><li>支付宝给商家提供 NFC 卡模拟芯片。用户侧的支付宝 app 充当读卡器,读取商家的 NFC 芯片信息。app 获取商家信息后,进行网络处理,完成支付。</li><li>以往的支付,读卡器处于商家侧,如 Tap to pay、POS 机等。只要商家具有稳定的网络,就可以完成支付。</li><li>alipay 碰一碰方案,读卡器处于用户侧 app 中。这就需要用户侧具有稳定的网络,以完成支付。</li></ul></li></ol><h2 id="Apple-iPhone-NFC"><a href="#Apple-iPhone-NFC" class="headerlink" title="Apple iPhone NFC"></a>Apple iPhone NFC</h2><p>简单介绍一下 iPhone 对 NFC 支持的历史:</p><ul><li>WWDC 2017:引入 Core NFC,并具备 NDEF 标签 <strong>读取</strong> 功能。</li><li>WWDC 2018:在较新设备上对 NDEF 消息进行后台标签读取。</li><li>WWDC 2019:重大扩展,允许 NDEF <strong>写入</strong>,支持 ISO 7816、ISO 15693 和 MIFARE 标签,并支持自定义命令。</li><li>WWDC 2020:多标签检测,VAS 协议支持和 ISO 15693 标签的后台读取。</li><li>WWDC 2021-2023:专注于稳定性、性能提升和小幅增强,没有重大 API 更改。</li><li>WWDC 2024:支持 <strong>卡模拟</strong>。</li></ul><hr>

Mac 字体

作者 海驴
2024年10月22日 10:51
<blockquote><p>很久之前,写过一篇关于 【<a href="https://www.yigegongjiang.com/2023/unicode/"><strong>计算机字符编码与内存编码 - Unicode</strong></a>】 的快照,根据 码位、码表 对字符进行了介绍。这里特别说明一下 Mac 系统上的字体库。</p></blockquote><p>系统自带软件:<code>Font book</code><br>字体文件夹: <code>/System/Library/Fonts</code> 、<code>/Library/Fonts</code>、<code>~/Library/Fonts</code><br>苹果提供的字体:<code>Applexxx</code> 、<code>Apple xxx</code> 、<code>SFxxx</code> 、<code>PingFangxxx</code></p><h1 id="如何使用字体"><a href="#如何使用字体" class="headerlink" title="如何使用字体"></a>如何使用字体</h1><ol><li>操作系统根据语言的不同,会使用默认字体。比如中文系统,会使用 <code>PingFang</code> 字体。英文系统,会使用 <code>SF</code> 字体。</li><li>app、dmg 等软件,可以直接使用 <code>defaultfont:xxx</code> 的形式直接使用系统字体,或者使用 <code>fontname:xxx</code> 的形式自行选择字体。自行选择的时候,可以使用系统提供的字体,也可以将 xx 字体打入 app 中来使用。</li><li>app 提供修改字体的功能,用户可以自行选择需要的字体。</li></ol><span id="more"></span><h1 id="图标"><a href="#图标" class="headerlink" title="图标"></a>图标</h1><p>Apple 平台提供了两个图标字体,分别是:<code>Apple Color Emoji</code> 和<code>Apple Symbols</code> 。</p><p>字体库能够包含图标、颜色,在之前阐述 Unicode 的时候已经说明过,因为它们都依靠<code>码位</code> 进行检索。所以在不同的平台上,都会有自己的图标库的系统级别实现,显示效果会不一样。</p><h1 id="字体回退"><a href="#字体回退" class="headerlink" title="字体回退"></a>字体回退</h1><p>每个字体可以适配多种语言,但没有一个字体是全能的。即【一定需要<strong>字体回退</strong>】来对当前字体无法渲染的文字进行兜底。<br>在 mac font app 中,打开一个 font,会列出其 support 的文字集合。</p><ol><li>部分 app 支持设置 1 个字体:回退到系统字体。</li><li>部分 app 支持设置 n 个字体,按照优先级进行回退,最后回退到系统字体。如 browser 等</li><li>css:通过设置 <code>font-family</code> 属性来实现字体回退。例如:<code>font-family: "MyFont", "FallbackFont", sans-serif;</code></li></ol><h1 id="Nerd-Font"><a href="#Nerd-Font" class="headerlink" title="Nerd Font"></a>Nerd Font</h1><p>nerd font 是一个项目集合,它将非常多的字体进行扩展,增加了许多 icon,从而让已经非常优秀的字体扩展了更多的功能。</p><p>可以通过 brew 安装 nerd 字体。</p><figure class="highlight jsx"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">&gt; brew search nerd-font</span><br><span class="line"></span><br><span class="line">font-0xproto-nerd-font font-iosevka-nerd-font</span><br><span class="line">font-<span class="number">3270</span>-nerd-font font-iosevka-term-nerd-font</span><br><span class="line">font-agave-nerd-font font-iosevka-term-slab-nerd-font</span><br><span class="line">font-anonymice-nerd-font font-jetbrains-mono-nerd-font ✔</span><br><span class="line">font-arimo-nerd-font font-lekton-nerd-font</span><br><span class="line">font-aurulent-sans-mono-nerd-font font-liberation-nerd-font</span><br><span class="line">font-bigblue-terminal-nerd-font font-lilex-nerd-font</span><br><span class="line">font-bitstream-vera-sans-mono-nerd-font font-m+-nerd-font</span><br><span class="line">font-blex-mono-nerd-font font-martian-mono-nerd-font</span><br><span class="line">font-caskaydia-cove-nerd-font font-meslo-lg-nerd-font</span><br><span class="line">font-caskaydia-mono-nerd-font font-monaspace-nerd-font</span><br><span class="line">font-code-<span class="keyword">new</span>-roman-nerd-font font-monocraft-nerd-font</span><br><span class="line">font-comic-shanns-mono-nerd-font font-monofur-nerd-font</span><br><span class="line">font-commit-mono-nerd-font font-monoid-nerd-font</span><br><span class="line">font-cousine-nerd-font font-mononoki-nerd-font</span><br><span class="line">font-d2coding-nerd-font font-noto-nerd-font</span><br><span class="line">font-daddy-time-mono-nerd-font font-open-dyslexic-nerd-font</span><br><span class="line">font-dejavu-sans-mono-nerd-font font-overpass-nerd-font</span><br><span class="line">font-droid-sans-mono-nerd-font ✔ font-profont-nerd-font</span><br><span class="line">font-envy-code-r-nerd-font font-proggy-clean-tt-nerd-font</span><br><span class="line">font-fantasque-sans-mono-nerd-font font-recursive-mono-nerd-font</span><br><span class="line">font-fira-code-nerd-font ✔ font-roboto-mono-nerd-font</span><br><span class="line">font-fira-mono-nerd-font font-sauce-code-pro-nerd-font</span><br><span class="line">font-geist-mono-nerd-font font-shure-tech-mono-nerd-font</span><br><span class="line">font-go-mono-nerd-font font-space-mono-nerd-font</span><br><span class="line">font-gohufont-nerd-font font-symbols-only-nerd-font</span><br><span class="line">font-hack-nerd-font ✔ font-terminess-ttf-nerd-font</span><br><span class="line">font-hasklug-nerd-font font-tinos-nerd-font</span><br><span class="line">font-heavy-data-nerd-font font-ubuntu-mono-nerd-font</span><br><span class="line">font-hurmit-nerd-font font-ubuntu-nerd-font</span><br><span class="line">font-im-writing-nerd-font font-ubuntu-sans-nerd-font</span><br><span class="line">font-inconsolata-go-nerd-font font-victor-mono-nerd-font</span><br><span class="line">font-inconsolata-lgc-nerd-font font-zed-mono-nerd-font</span><br><span class="line">font-inconsolata-nerd-font netron ✔</span><br><span class="line">font-intone-mono-nerd-font</span><br></pre></td></tr></tbody></table></figure><p>通过 <code>brew install --cask font-hack-nerd-font</code> 安装的字体,会被安装到 <code>~/Library/Fonts</code> 文件夹中。</p><h2 id="等宽字体"><a href="#等宽字体" class="headerlink" title="等宽字体"></a>等宽字体</h2><p>有 <code>Fira Code</code>、<code>hack</code> 等,在英文场景非常舒服,IDE 场景经常使用。</p><h1 id="zsh"><a href="#zsh" class="headerlink" title="zsh"></a>zsh</h1><p>zsh 有 <code>powerlevel10k</code> 主题。该主题在字体方面比较丰富,主要是采用了很多字体 icon。有些字体如果提供不了 所需 的 icon,显示就会异常。</p><p>但是 <code>powerlevel10k</code> 本身仅仅是配置文件,它不提供字体的安装。所以用户需要自行安装所需要的字体。它和 nerd font 配合比较友好,只要是 nerd font 项目中的字体,都可以被 <code>powerlevel10k</code> 很好的使用。</p><p>操作流程:</p><ol><li>通过 brew 安装 nerd 项目中的字体,如 hack。</li><li>ITerm、Warp 中选择 nerd 字体。</li></ol><h1 id="浏览器切换字体"><a href="#浏览器切换字体" class="headerlink" title="浏览器切换字体"></a>浏览器切换字体</h1><p>安装 hack 等字体后,很多天天见面的 IDE 或者 app,就可以切换喜欢的字体了。<br>其中,浏览器可以设置全局的字体切换,这对爱好某一个字体的同学来说,将非常友好。<br>全局 css 内容推荐如下,后面配置的时候会用到:</p><figure class="highlight css"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">span</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.material-symbols-outlined</span>, <span class="selector-class">.material-icons</span>, <span class="selector-class">.google-material-icons</span>, <span class="selector-class">.fa</span>, <span class="selector-class">.fas</span>, <span class="selector-class">.far</span>, <span class="selector-class">.fal</span>, <span class="selector-class">.fab</span>, <span class="selector-class">.fad</span>, <span class="selector-attr">[class*=<span class="string">"icon"</span>]</span>, <span class="selector-tag">svg</span>, <span class="selector-attr">[class*=<span class="string">"DP"</span>]</span>, <span class="selector-attr">[class*=<span class="string">"hd"</span>]</span>),</span><br><span class="line"><span class="selector-tag">i</span><span class="selector-pseudo">:not</span>(<span class="selector-class">.material-symbols-outlined</span>, <span class="selector-class">.material-icons</span>, <span class="selector-class">.google-material-icons</span>, <span class="selector-class">.fa</span>, <span class="selector-class">.fas</span>, <span class="selector-class">.far</span>, <span class="selector-class">.fal</span>, <span class="selector-class">.fab</span>, <span class="selector-class">.fad</span>, <span class="selector-attr">[class*=<span class="string">"icon"</span>]</span>, <span class="selector-tag">svg</span>, <span class="selector-attr">[class*=<span class="string">"DP"</span>]</span>, <span class="selector-attr">[class*=<span class="string">"hd"</span>]</span>),</span><br><span class="line"><span class="selector-tag">body</span>, <span class="selector-tag">li</span>, <span class="selector-tag">p</span>, <span class="selector-tag">div</span>, <span class="selector-tag">h1</span>, <span class="selector-tag">h2</span>, <span class="selector-tag">h3</span>, <span class="selector-tag">h4</span>, <span class="selector-tag">h5</span>, <span class="selector-tag">h6</span>, <span class="selector-tag">a</span>, <span class="selector-tag">ul</span>, <span class="selector-tag">ol</span>, <span class="selector-tag">dl</span>, <span class="selector-tag">dt</span>, <span class="selector-tag">dd</span>, <span class="selector-tag">button</span>, <span class="selector-tag">input</span>, <span class="selector-tag">textarea</span>, <span class="selector-tag">select</span>, <span class="selector-tag">option</span>, <span class="selector-tag">optgroup</span>, <span class="selector-tag">label</span>, pre, <span class="selector-tag">code</span>, <span class="selector-tag">kbd</span>, <span class="selector-tag">samp</span>, <span class="selector-tag">var</span>, <span class="selector-tag">table</span>, <span class="selector-tag">th</span>, <span class="selector-tag">td</span>, <span class="selector-tag">tr</span>, <span class="selector-tag">thead</span>, <span class="selector-tag">tbody</span>, <span class="selector-tag">tfoot</span>, <span class="selector-tag">caption</span>, <span class="selector-tag">blockquote</span>, <span class="selector-tag">cite</span>, <span class="selector-tag">q</span>, <span class="selector-tag">strong</span>, <span class="selector-tag">em</span>, <span class="selector-tag">b</span>, small, sub, <span class="selector-tag">sup</span>, <span class="selector-tag">mark</span>, <span class="selector-tag">del</span>, <span class="selector-tag">ins</span>, <span class="selector-tag">abbr</span>, acronym, <span class="selector-tag">address</span>, <span class="selector-tag">time</span>, <span class="selector-tag">form</span>, <span class="selector-tag">fieldset</span>, <span class="selector-tag">legend</span>, <span class="selector-tag">nav</span>, <span class="selector-tag">header</span>, <span class="selector-tag">footer</span>, <span class="selector-tag">section</span>, <span class="selector-tag">article</span>, <span class="selector-tag">aside</span>, <span class="selector-tag">main</span>, <span class="selector-tag">details</span>,</span><br><span class="line"><span class="selector-tag">summary</span> {</span><br><span class="line"> <span class="attribute">font-family</span>: <span class="string">"FiraCode Nerd Font Mono"</span>, sans-serif <span class="meta">!important</span>;</span><br><span class="line"> -webkit-<span class="attribute">font-smoothing</span>: antialiased <span class="meta">!important</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><blockquote><p>Tips:最好不要使用 <code>body, * {}</code> 以及 <code>!important</code>,这样会影响到一些网站的显示效果。因为有些字体是图标字体,会紊乱变形。</p></blockquote><h2 id="safari"><a href="#safari" class="headerlink" title="safari"></a>safari</h2><p>safari 浏览器提供了全局样式表:【setting - advanced - style sheet】,这里可以在选择本地一个 <strong>xx.css</strong> 文件,来设置全局的样式。<br>文件内容就是上面提供的示例 css。</p><h2 id="chrome-家族"><a href="#chrome-家族" class="headerlink" title="chrome 家族"></a>chrome 家族</h2><p>chrome、arc 等,都可以通过插件 <code>Stylus</code> 来设置全局样式表,这个插件非常棒,可以定制很多网站的样式,有很多网友制作的样式,可以直接使用。<br>这里,我们可以通过该插件设定一个全局样式表,内容就是上面提供的示例 css。</p><hr>

ESP32

作者 海驴
2024年10月22日 10:48
<h1 id="嵌入式"><a href="#嵌入式" class="headerlink" title="嵌入式"></a>嵌入式</h1><ol><li>特定功能:嵌入式是为了完成特定的物理功能,如温度捕捉、机器人。</li><li>特定组合:通过小巧的不可修改的硬件、软件,协同后直接发布。后期,不太好修改硬件或者软件(可以修改,但一般不会主动修改)。</li><li>一次成型:很难进行二次改造。因为组合比较特定,硬件和软件之间的 api 都是私有的,无法迁移。</li></ol><span id="more"></span><h1 id="ESP32"><a href="#ESP32" class="headerlink" title="ESP32"></a>ESP32</h1><p>它本身是芯片,台积电加工,40 纳米制程 (中国应该也能造了)。</p><p>官方提供了主板,即芯片 + 板 + 辅助硬件,产品名如 ESP32-devkit-6 等。</p><p>开发人员可以在主板上进行开发工作。</p><h3 id="开发流程:"><a href="#开发流程:" class="headerlink" title="开发流程:"></a>开发流程:</h3><ol><li>部署环境。官方已经支持 mac、linux、window 下的 IDE 开发。</li><li>SDK。官方提供完善的 SDK 和 Api。Api 可以控制 LED、摄像头等大量硬件。</li><li>开发 &amp; 测试。通过 IDE 编写完代码,可将生成的 二进制 bin 文件上传到 ESP32 中,并完成运行。<ol><li>硬件输出:如果有 LED 等,代码中可以控制针脚,完成高低电平的设定,可以看到效果。</li><li>软件输出:可以在 IDE 中看到 ESP32 print 的 值。</li></ol></li></ol><h3 id="原理:"><a href="#原理:" class="headerlink" title="原理:"></a>原理:</h3><ol><li>ESP32 是完全的芯片,无 OS,所以只能无脑的执行指令。因配置较弱,只能执行编译好的非常小巧的二进制 bin 文件。</li><li>在启动后,主动执行 boot 引导的 bin 文件。</li><li>没有很好的内存管理,必要的使用,需要自己进行内存控制。</li></ol><h3 id="周边:"><a href="#周边:" class="headerlink" title="周边:"></a>周边:</h3><ol><li>通过 ESPHome,可以不用写代码,仅仅使用别人写好的软件,自己增加一些 yaml 配置,即可编译一个完善的 bin 出来。可以控制家里的 小米扫地机 等。</li></ol><h3 id="局限性:"><a href="#局限性:" class="headerlink" title="局限性:"></a>局限性:</h3><p>一个 ESP32 或者 主板,只能支持一个 bin 文件的执行。启动后,默认加载指定位置的 bin 文件,且只有 1 个。(相当于 pc,启动后 boot 引导操作系统。这里引导烧录的 bin 文件)</p><h1 id="树莓派"><a href="#树莓派" class="headerlink" title="树莓派"></a>树莓派</h1><p>性能强劲的计算机,但尺寸小、耗电低,适用于复杂一些的嵌入式场景。</p><hr>

trump - 川普

作者 海驴
2024年7月14日 14:17
<p>川普 于 美东时间 2024 年 7 月 13 日 18 时 11 分 在 宾夕法尼亚州 竞选集会 上 遭遇枪击。<br>真没想到,美国的政治已经到了这一步。上一次总统暗杀还是肯尼迪,那已经是 60 年前的事情了。</p><p>从视频上看,川普 被击中和不被击中,概率上没有大的偏差,即 50%。结局是:贯穿了耳朵,没击中大脑。</p><p>上一次川普参加竞选,是和希拉里竞争。那时候希拉里明显有票数优势,阿桑奇 泄漏了 邮件门,让希拉里很多丑闻曝光,使得川普反败为胜。</p><span id="more"></span><p>这一次,在很多原因如疫情、战争的作用下,世界范围都很不稳定,乱世换人本来有优势。<br>但这次的刺客,一弹定江山。没杀死川普,那就得保送了。</p><p>川普上一次在任期间,对华政策很严厉。尤其增加的进口税,拜登在任期间也没有去除。<br>那一次,川普是作为一个商人的影子上台,权利分散、命不下达。甚至于内斗不断,能干事的时间并不多,落地的也少。<br>一转眼 4 年已经过去了,这四年世界范围内日子都不好过,但看起来国内更不好过。而川普的势力,不能同日而语了。</p><p>后面几年中国咋办哦,一届可就是 4 年啊。现在是拔出了刀互看局势,但真要针锋对麦芒的话,还是那句话:在绝对的实力面前,一切都是虚的。</p><p>同一个场景大概率不会有两次好运。川普这次不弄出来大动静,是不可能的。</p><p><img data-src="https://raw.githubusercontent.com/yigegongjiang/image_space/main/blog_img/202407141438096.jpeg"></p><hr>
❌
❌