[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fSSK0_85-7K7g0UdaniQIjwb_ygbyb-0g9WqVCoAIk0g":3,"$fJU-4tot_gC5fDkujNeoE-cGsdMy5V_KcdUXLuAnTFgw":16,"$fXKbxn6YNPQpbc2tqB7kosZxJvjPSv7iQ5dMnOjoJsM8":423},{"slug":4,"title":5,"description":6,"content":7,"content_html":8,"pub_date":9,"tags":10,"draft":15},"electron-ipc-types","Electron IPC 类型安全：从 any 到完全类型化","用 TypeScript 泛型封装 Electron IPC，彻底消灭 any，preload 契约集中管理","## 问题\n\nElectron IPC 的默认写法是这样的：\n\n```typescript\n\u002F\u002F main.ts — 地狱\nipcMain.handle('get-config', async () => { \u002F* ... *\u002F })\n\n\u002F\u002F renderer — 更地狱\nconst result = await ipcRenderer.invoke('get-config') \u002F\u002F any\n```\n\n字符串 channel 名散落全项目，参数类型全是 `any`，重构时改了 main 忘改 renderer，\n运行时才发现。这不是工程，这是赌博。\n\n## 解决思路\n\n定义一个**中心契约**，所有 IPC channel 的入参和返回值都在这里声明。\n\n```typescript\n\u002F\u002F src\u002Fshared\u002Fipc-contract.ts\n\nexport interface IpcContract {\n  \u002F\u002F invoke 风格：请求-响应\n  'config:get': {\n    request: void\n    response: AppConfig\n  }\n  'config:set': {\n    request: Partial\u003CAppConfig>\n    response: { ok: boolean; error?: string }\n  }\n  'device:list': {\n    request: void\n    response: DeviceInfo[]\n  }\n  'device:connect': {\n    request: { vid: number; pid: number }\n    response: { ok: boolean }\n  }\n}\n\n\u002F\u002F 主进程推送给渲染进程的事件（单向）\nexport interface IpcEvents {\n  'device:status-changed': { connected: boolean; deviceId?: string }\n  'config:updated': AppConfig\n  'log:line': { level: 'info' | 'warn' | 'error'; message: string }\n}\n```\n\n两个接口分开：`IpcContract` 用于 `invoke`（双向），`IpcEvents` 用于主进程 `→` 渲染进程的单向推送。\n\n## 类型化 invoke 封装\n\n### 渲染侧（preload 里）\n\n```typescript\n\u002F\u002F src\u002Fpreload\u002Fbridge.ts\nimport { contextBridge, ipcRenderer } from 'electron'\nimport type { IpcContract, IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req\u003CC extends InvokeChannel> = IpcContract[C]['request']\ntype Res\u003CC extends InvokeChannel> = IpcContract[C]['response']\n\n\u002F\u002F invoke 封装：request 为 void 时不需要传参\nconst invoke = \u003CC extends InvokeChannel>(\n  channel: C,\n  ...args: Req\u003CC> extends void ? [] : [Req\u003CC>]\n): Promise\u003CRes\u003CC>> => ipcRenderer.invoke(channel, ...args)\n```\n\n### 主进程侧（双端类型安全）\n\n主进程的 `ipcMain.handle` 也可以用同样的泛型约束，让两端都受类型保护：\n\n```typescript\n\u002F\u002F src\u002Fmain\u002Fipc-handler.ts\nimport { ipcMain } from 'electron'\nimport type { IpcContract } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req\u003CC extends InvokeChannel> = IpcContract[C]['request']\ntype Res\u003CC extends InvokeChannel> = IpcContract[C]['response']\n\n\u002F\u002F 类型化的 handle 封装\nfunction handle\u003CC extends InvokeChannel>(\n  channel: C,\n  handler: (req: Req\u003CC>) => Promise\u003CRes\u003CC>> | Res\u003CC>\n) {\n  ipcMain.handle(channel, (_event, req) => handler(req))\n}\n\n\u002F\u002F 使用：channel 名和参数类型全部受约束\nhandle('config:get', async () => {\n  return loadConfig() \u002F\u002F 返回类型必须匹配 AppConfig ✅\n})\n\nhandle('config:set', async (req) => {\n  \u002F\u002F req 类型是 Partial\u003CAppConfig>，不是 any ✅\n  await saveConfig(req)\n  return { ok: true }\n})\n\nhandle('device:connect', async ({ vid, pid }) => {\n  \u002F\u002F 解构直接有类型提示 ✅\n  const ok = await connectDevice(vid, pid)\n  return { ok }\n})\n```\n\n## 事件推送（on\u002Fsend）的类型化\n\n`invoke` 是请求-响应模型。但有些场景是**主进程主动推送**给渲染进程：设备状态变化、后台任务进度、日志流……这需要用 `ipcMain.emit` + `webContents.send` + `ipcRenderer.on`。\n\n### 主进程：发送事件\n\n```typescript\n\u002F\u002F src\u002Fmain\u002Fevents.ts\nimport { BrowserWindow } from 'electron'\nimport type { IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype EventChannel = keyof IpcEvents\n\n\u002F\u002F 类型化的 send 封装\nfunction sendToRenderer\u003CC extends EventChannel>(\n  win: BrowserWindow,\n  channel: C,\n  payload: IpcEvents[C]\n) {\n  win.webContents.send(channel, payload)\n}\n\n\u002F\u002F 使用示例\nsendToRenderer(mainWindow, 'device:status-changed', { connected: true, deviceId: 'abc' })\n\u002F\u002F payload 类型受 IpcEvents 约束，传错会报错 ✅\n```\n\n### 渲染侧：监听事件（也在 preload 里封装）\n\n```typescript\n\u002F\u002F preload 里加上 on 封装\nimport type { IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype EventChannel = keyof IpcEvents\n\nconst on = \u003CC extends EventChannel>(\n  channel: C,\n  listener: (payload: IpcEvents[C]) => void\n): (() => void) => {\n  \u002F\u002F 包一层让返回值是\"取消订阅\"函数，方便组件 onUnmounted 清理\n  const wrapped = (_event: Electron.IpcRendererEvent, payload: IpcEvents[C]) =>\n    listener(payload)\n  ipcRenderer.on(channel, wrapped)\n  return () => ipcRenderer.off(channel, wrapped)\n}\n```\n\n## 完整的 preload.ts\n\n把 `invoke` 和 `on` 都通过 `contextBridge` 暴露出去：\n\n```typescript\n\u002F\u002F src\u002Fpreload\u002Findex.ts\nimport { contextBridge, ipcRenderer } from 'electron'\nimport type { IpcContract, IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req\u003CC extends InvokeChannel> = IpcContract[C]['request']\ntype Res\u003CC extends InvokeChannel> = IpcContract[C]['response']\n\ntype EventChannel = keyof IpcEvents\n\nconst bridge = {\n  \u002F\u002F 请求-响应\n  invoke\u003CC extends InvokeChannel>(\n    channel: C,\n    ...args: Req\u003CC> extends void ? [] : [Req\u003CC>]\n  ): Promise\u003CRes\u003CC>> {\n    return ipcRenderer.invoke(channel, ...args)\n  },\n\n  \u002F\u002F 主进程推送事件订阅，返回取消函数\n  on\u003CC extends EventChannel>(\n    channel: C,\n    listener: (payload: IpcEvents[C]) => void\n  ): () => void {\n    const wrapped = (_: Electron.IpcRendererEvent, payload: IpcEvents[C]) =>\n      listener(payload)\n    ipcRenderer.on(channel, wrapped)\n    return () => ipcRenderer.off(channel, wrapped)\n  },\n}\n\ncontextBridge.exposeInMainWorld('bridge', bridge)\n\n\u002F\u002F 全局类型声明（配合 tsconfig paths 或放在 global.d.ts）\ndeclare global {\n  interface Window {\n    bridge: typeof bridge\n  }\n}\n```\n\n在 Vue 组件里使用：\n\n```typescript\n\u002F\u002F DeviceStatus.vue\nimport { ref, onMounted, onUnmounted } from 'vue'\n\nconst connected = ref(false)\nlet unsubscribe: (() => void) | null = null\n\nonMounted(() => {\n  unsubscribe = window.bridge.on('device:status-changed', ({ connected: c }) => {\n    connected.value = c\n    \u002F\u002F payload 类型完整推断，无需断言 ✅\n  })\n})\n\nonUnmounted(() => {\n  unsubscribe?.() \u002F\u002F 清理，避免内存泄漏\n})\n```\n\n## 运行时验证：为什么仅靠 TypeScript 不够\n\nTypeScript 的类型检查只在**编译期**有效。IPC 跨越进程边界，数据以序列化形式传输——渲染进程发来的数据在主进程眼里就是一个 JSON 反序列化结果，编译器管不到。\n\n攻击场景：恶意扩展或 XSS 注入代码直接调用 `ipcRenderer.invoke('config:set', { evil: '...' })`，绕过 TypeScript，主进程收到的是任意 payload。\n\n解决方案：用 **[Zod](https:\u002F\u002Fgithub.com\u002Fcolinhacks\u002Fzod)** 在主进程侧做运行时 schema 验证。\n\n```typescript\n\u002F\u002F src\u002Fshared\u002Fipc-schemas.ts\nimport { z } from 'zod'\n\nexport const IpcSchemas = {\n  'config:set': z.object({\n    theme: z.enum(['light', 'dark']).optional(),\n    language: z.string().optional(),\n    autoUpdate: z.boolean().optional(),\n  }),\n  'device:connect': z.object({\n    vid: z.number().int().min(0).max(0xffff),\n    pid: z.number().int().min(0).max(0xffff),\n  }),\n} satisfies Partial\u003CRecord\u003Ckeyof IpcContract, z.ZodTypeAny>>\n```\n\n在 `handle` 封装里加入验证层：\n\n```typescript\n\u002F\u002F src\u002Fmain\u002Fipc-handler.ts（加了验证的版本）\nimport { IpcSchemas } from '..\u002Fshared\u002Fipc-schemas'\n\nfunction handle\u003CC extends InvokeChannel>(\n  channel: C,\n  handler: (req: Req\u003CC>) => Promise\u003CRes\u003CC>> | Res\u003CC>\n) {\n  ipcMain.handle(channel, async (_event, req) => {\n    \u002F\u002F 如果有 schema，先验证\n    const schema = IpcSchemas[channel as keyof typeof IpcSchemas]\n    if (schema) {\n      const result = schema.safeParse(req)\n      if (!result.success) {\n        \u002F\u002F 验证失败返回错误，而不是崩溃\n        console.error(`[IPC] Invalid payload for ${channel}:`, result.error.flatten())\n        throw new Error(`Invalid IPC payload: ${channel}`)\n      }\n      req = result.data \u002F\u002F 用 zod 解析后的值（已过滤多余字段）\n    }\n    return handler(req)\n  })\n}\n```\n\n这样就形成了**双重保险**：TypeScript 在编译期保护开发体验，Zod 在运行时保护主进程安全。\n\n## 使用效果\n\n```typescript\n\u002F\u002F renderer — 完全类型化\nconst config = await window.bridge.invoke('config:get')\n\u002F\u002F    ^? AppConfig  ✅\n\nawait window.bridge.invoke('device:connect', { vid: 0x1234, pid: 0x5678 })\n\u002F\u002F                                            ^? { vid: number; pid: number } ✅\n\nawait window.bridge.invoke('config:get', { wrong: true })\n\u002F\u002F                                        ^? TS Error: 应为 void ✅\n\nawait window.bridge.invoke('nonexistent-channel')\n\u002F\u002F                          ^? TS Error: 不在 IpcContract 里 ✅\n```\n\n## 小结\n\n| 层次 | 工具 | 作用 |\n|------|------|------|\n| 契约定义 | `IpcContract` + `IpcEvents` | 单一事实来源，所有 channel 在这里 |\n| 编译期检查 | TypeScript 泛型 | channel 名、参数、返回值全部类型安全 |\n| 运行时验证 | Zod schema | 防止恶意或异常 payload 进入主进程 |\n| 清理机制 | `on()` 返回取消函数 | 配合 Vue `onUnmounted` 防内存泄漏 |\n\n10 分钟的前期投入，换来整个项目 IPC 层零 `any`，重构时 TS 报错直接定位，主进程也不再裸奔。","\u003Ch2 id=\"问题\">问题\u003C\u002Fh2>\n\u003Cp>Electron IPC 的默认写法是这样的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F main.ts — 地狱\nipcMain.handle('get-config', async () =&gt; { \u002F* ... *\u002F })\n\n\u002F\u002F renderer — 更地狱\nconst result = await ipcRenderer.invoke('get-config') \u002F\u002F any\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>字符串 channel 名散落全项目，参数类型全是 \u003Ccode>any\u003C\u002Fcode>，重构时改了 main 忘改 renderer，\n运行时才发现。这不是工程，这是赌博。\u003C\u002Fp>\n\u003Ch2 id=\"解决思路\">解决思路\u003C\u002Fh2>\n\u003Cp>定义一个\u003Cstrong>中心契约\u003C\u002Fstrong>，所有 IPC channel 的入参和返回值都在这里声明。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fshared\u002Fipc-contract.ts\n\nexport interface IpcContract {\n  \u002F\u002F invoke 风格：请求-响应\n  'config:get': {\n    request: void\n    response: AppConfig\n  }\n  'config:set': {\n    request: Partial&lt;AppConfig&gt;\n    response: { ok: boolean; error?: string }\n  }\n  'device:list': {\n    request: void\n    response: DeviceInfo[]\n  }\n  'device:connect': {\n    request: { vid: number; pid: number }\n    response: { ok: boolean }\n  }\n}\n\n\u002F\u002F 主进程推送给渲染进程的事件（单向）\nexport interface IpcEvents {\n  'device:status-changed': { connected: boolean; deviceId?: string }\n  'config:updated': AppConfig\n  'log:line': { level: 'info' | 'warn' | 'error'; message: string }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>两个接口分开：\u003Ccode>IpcContract\u003C\u002Fcode> 用于 \u003Ccode>invoke\u003C\u002Fcode>（双向），\u003Ccode>IpcEvents\u003C\u002Fcode> 用于主进程 \u003Ccode>→\u003C\u002Fcode> 渲染进程的单向推送。\u003C\u002Fp>\n\u003Ch2 id=\"类型化-invoke-封装\">类型化 invoke 封装\u003C\u002Fh2>\n\u003Ch3 id=\"渲染侧-preload-里\">渲染侧（preload 里）\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fpreload\u002Fbridge.ts\nimport { contextBridge, ipcRenderer } from 'electron'\nimport type { IpcContract, IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req&lt;C extends InvokeChannel&gt; = IpcContract[C]['request']\ntype Res&lt;C extends InvokeChannel&gt; = IpcContract[C]['response']\n\n\u002F\u002F invoke 封装：request 为 void 时不需要传参\nconst invoke = &lt;C extends InvokeChannel&gt;(\n  channel: C,\n  ...args: Req&lt;C&gt; extends void ? [] : [Req&lt;C&gt;]\n): Promise&lt;Res&lt;C&gt;&gt; =&gt; ipcRenderer.invoke(channel, ...args)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"主进程侧-双端类型安全\">主进程侧（双端类型安全）\u003C\u002Fh3>\n\u003Cp>主进程的 \u003Ccode>ipcMain.handle\u003C\u002Fcode> 也可以用同样的泛型约束，让两端都受类型保护：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fmain\u002Fipc-handler.ts\nimport { ipcMain } from 'electron'\nimport type { IpcContract } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req&lt;C extends InvokeChannel&gt; = IpcContract[C]['request']\ntype Res&lt;C extends InvokeChannel&gt; = IpcContract[C]['response']\n\n\u002F\u002F 类型化的 handle 封装\nfunction handle&lt;C extends InvokeChannel&gt;(\n  channel: C,\n  handler: (req: Req&lt;C&gt;) =&gt; Promise&lt;Res&lt;C&gt;&gt; | Res&lt;C&gt;\n) {\n  ipcMain.handle(channel, (_event, req) =&gt; handler(req))\n}\n\n\u002F\u002F 使用：channel 名和参数类型全部受约束\nhandle('config:get', async () =&gt; {\n  return loadConfig() \u002F\u002F 返回类型必须匹配 AppConfig ✅\n})\n\nhandle('config:set', async (req) =&gt; {\n  \u002F\u002F req 类型是 Partial&lt;AppConfig&gt;，不是 any ✅\n  await saveConfig(req)\n  return { ok: true }\n})\n\nhandle('device:connect', async ({ vid, pid }) =&gt; {\n  \u002F\u002F 解构直接有类型提示 ✅\n  const ok = await connectDevice(vid, pid)\n  return { ok }\n})\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"事件推送-on-send-的类型化\">事件推送（on\u002Fsend）的类型化\u003C\u002Fh2>\n\u003Cp>\u003Ccode>invoke\u003C\u002Fcode> 是请求-响应模型。但有些场景是\u003Cstrong>主进程主动推送\u003C\u002Fstrong>给渲染进程：设备状态变化、后台任务进度、日志流……这需要用 \u003Ccode>ipcMain.emit\u003C\u002Fcode> + \u003Ccode>webContents.send\u003C\u002Fcode> + \u003Ccode>ipcRenderer.on\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Ch3 id=\"主进程-发送事件\">主进程：发送事件\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fmain\u002Fevents.ts\nimport { BrowserWindow } from 'electron'\nimport type { IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype EventChannel = keyof IpcEvents\n\n\u002F\u002F 类型化的 send 封装\nfunction sendToRenderer&lt;C extends EventChannel&gt;(\n  win: BrowserWindow,\n  channel: C,\n  payload: IpcEvents[C]\n) {\n  win.webContents.send(channel, payload)\n}\n\n\u002F\u002F 使用示例\nsendToRenderer(mainWindow, 'device:status-changed', { connected: true, deviceId: 'abc' })\n\u002F\u002F payload 类型受 IpcEvents 约束，传错会报错 ✅\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"渲染侧-监听事件-也在-preload-里封装\">渲染侧：监听事件（也在 preload 里封装）\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F preload 里加上 on 封装\nimport type { IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype EventChannel = keyof IpcEvents\n\nconst on = &lt;C extends EventChannel&gt;(\n  channel: C,\n  listener: (payload: IpcEvents[C]) =&gt; void\n): (() =&gt; void) =&gt; {\n  \u002F\u002F 包一层让返回值是&quot;取消订阅&quot;函数，方便组件 onUnmounted 清理\n  const wrapped = (_event: Electron.IpcRendererEvent, payload: IpcEvents[C]) =&gt;\n    listener(payload)\n  ipcRenderer.on(channel, wrapped)\n  return () =&gt; ipcRenderer.off(channel, wrapped)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"完整的-preload-ts\">完整的 preload.ts\u003C\u002Fh2>\n\u003Cp>把 \u003Ccode>invoke\u003C\u002Fcode> 和 \u003Ccode>on\u003C\u002Fcode> 都通过 \u003Ccode>contextBridge\u003C\u002Fcode> 暴露出去：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fpreload\u002Findex.ts\nimport { contextBridge, ipcRenderer } from 'electron'\nimport type { IpcContract, IpcEvents } from '..\u002Fshared\u002Fipc-contract'\n\ntype InvokeChannel = keyof IpcContract\ntype Req&lt;C extends InvokeChannel&gt; = IpcContract[C]['request']\ntype Res&lt;C extends InvokeChannel&gt; = IpcContract[C]['response']\n\ntype EventChannel = keyof IpcEvents\n\nconst bridge = {\n  \u002F\u002F 请求-响应\n  invoke&lt;C extends InvokeChannel&gt;(\n    channel: C,\n    ...args: Req&lt;C&gt; extends void ? [] : [Req&lt;C&gt;]\n  ): Promise&lt;Res&lt;C&gt;&gt; {\n    return ipcRenderer.invoke(channel, ...args)\n  },\n\n  \u002F\u002F 主进程推送事件订阅，返回取消函数\n  on&lt;C extends EventChannel&gt;(\n    channel: C,\n    listener: (payload: IpcEvents[C]) =&gt; void\n  ): () =&gt; void {\n    const wrapped = (_: Electron.IpcRendererEvent, payload: IpcEvents[C]) =&gt;\n      listener(payload)\n    ipcRenderer.on(channel, wrapped)\n    return () =&gt; ipcRenderer.off(channel, wrapped)\n  },\n}\n\ncontextBridge.exposeInMainWorld('bridge', bridge)\n\n\u002F\u002F 全局类型声明（配合 tsconfig paths 或放在 global.d.ts）\ndeclare global {\n  interface Window {\n    bridge: typeof bridge\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在 Vue 组件里使用：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F DeviceStatus.vue\nimport { ref, onMounted, onUnmounted } from 'vue'\n\nconst connected = ref(false)\nlet unsubscribe: (() =&gt; void) | null = null\n\nonMounted(() =&gt; {\n  unsubscribe = window.bridge.on('device:status-changed', ({ connected: c }) =&gt; {\n    connected.value = c\n    \u002F\u002F payload 类型完整推断，无需断言 ✅\n  })\n})\n\nonUnmounted(() =&gt; {\n  unsubscribe?.() \u002F\u002F 清理，避免内存泄漏\n})\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"运行时验证-为什么仅靠-typescript-不够\">运行时验证：为什么仅靠 TypeScript 不够\u003C\u002Fh2>\n\u003Cp>TypeScript 的类型检查只在\u003Cstrong>编译期\u003C\u002Fstrong>有效。IPC 跨越进程边界，数据以序列化形式传输——渲染进程发来的数据在主进程眼里就是一个 JSON 反序列化结果，编译器管不到。\u003C\u002Fp>\n\u003Cp>攻击场景：恶意扩展或 XSS 注入代码直接调用 \u003Ccode>ipcRenderer.invoke('config:set', { evil: '...' })\u003C\u002Fcode>，绕过 TypeScript，主进程收到的是任意 payload。\u003C\u002Fp>\n\u003Cp>解决方案：用 \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fcolinhacks\u002Fzod\">Zod\u003C\u002Fa>\u003C\u002Fstrong> 在主进程侧做运行时 schema 验证。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fshared\u002Fipc-schemas.ts\nimport { z } from 'zod'\n\nexport const IpcSchemas = {\n  'config:set': z.object({\n    theme: z.enum(['light', 'dark']).optional(),\n    language: z.string().optional(),\n    autoUpdate: z.boolean().optional(),\n  }),\n  'device:connect': z.object({\n    vid: z.number().int().min(0).max(0xffff),\n    pid: z.number().int().min(0).max(0xffff),\n  }),\n} satisfies Partial&lt;Record&lt;keyof IpcContract, z.ZodTypeAny&gt;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在 \u003Ccode>handle\u003C\u002Fcode> 封装里加入验证层：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F src\u002Fmain\u002Fipc-handler.ts（加了验证的版本）\nimport { IpcSchemas } from '..\u002Fshared\u002Fipc-schemas'\n\nfunction handle&lt;C extends InvokeChannel&gt;(\n  channel: C,\n  handler: (req: Req&lt;C&gt;) =&gt; Promise&lt;Res&lt;C&gt;&gt; | Res&lt;C&gt;\n) {\n  ipcMain.handle(channel, async (_event, req) =&gt; {\n    \u002F\u002F 如果有 schema，先验证\n    const schema = IpcSchemas[channel as keyof typeof IpcSchemas]\n    if (schema) {\n      const result = schema.safeParse(req)\n      if (!result.success) {\n        \u002F\u002F 验证失败返回错误，而不是崩溃\n        console.error(`[IPC] Invalid payload for ${channel}:`, result.error.flatten())\n        throw new Error(`Invalid IPC payload: ${channel}`)\n      }\n      req = result.data \u002F\u002F 用 zod 解析后的值（已过滤多余字段）\n    }\n    return handler(req)\n  })\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这样就形成了\u003Cstrong>双重保险\u003C\u002Fstrong>：TypeScript 在编译期保护开发体验，Zod 在运行时保护主进程安全。\u003C\u002Fp>\n\u003Ch2 id=\"使用效果\">使用效果\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-typescript\">\u002F\u002F renderer — 完全类型化\nconst config = await window.bridge.invoke('config:get')\n\u002F\u002F    ^? AppConfig  ✅\n\nawait window.bridge.invoke('device:connect', { vid: 0x1234, pid: 0x5678 })\n\u002F\u002F                                            ^? { vid: number; pid: number } ✅\n\nawait window.bridge.invoke('config:get', { wrong: true })\n\u002F\u002F                                        ^? TS Error: 应为 void ✅\n\nawait window.bridge.invoke('nonexistent-channel')\n\u002F\u002F                          ^? TS Error: 不在 IpcContract 里 ✅\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"小结\">小结\u003C\u002Fh2>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>层次\u003C\u002Fth>\n\u003Cth>工具\u003C\u002Fth>\n\u003Cth>作用\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>契约定义\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>IpcContract\u003C\u002Fcode> + \u003Ccode>IpcEvents\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>单一事实来源，所有 channel 在这里\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>编译期检查\u003C\u002Ftd>\n\u003Ctd>TypeScript 泛型\u003C\u002Ftd>\n\u003Ctd>channel 名、参数、返回值全部类型安全\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>运行时验证\u003C\u002Ftd>\n\u003Ctd>Zod schema\u003C\u002Ftd>\n\u003Ctd>防止恶意或异常 payload 进入主进程\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>清理机制\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>on()\u003C\u002Fcode> 返回取消函数\u003C\u002Ftd>\n\u003Ctd>配合 Vue \u003Ccode>onUnmounted\u003C\u002Fcode> 防内存泄漏\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>10 分钟的前期投入，换来整个项目 IPC 层零 \u003Ccode>any\u003C\u002Fcode>，重构时 TS 报错直接定位，主进程也不再裸奔。\u003C\u002Fp>\n","2026-04-25",[11,12,13,14],"electron","typescript","ipc","vue",false,[17,30,41,53,63,70,77,84,91,98,107,116,126,135,143,151,160,169,178,188,195,205,211,218,224,233,240,247,255,264,267,276,286,296,306,314,324,335,345,354,362,368,376,384,392,400,408,415],{"slug":18,"title":19,"description":20,"pub_date":21,"tags":22,"draft":15,"word_count":29},"ide-skills-guide","Agent Skills 完全指南：21 款第三方 Skill 深度评测与使用心得","全面评测 21 款第三方 Agent Skills，涵盖 Vue 生态、前端设计、构建工具、实用工具四大分类。从安装配置到实际使用场景，带你了解每个 Skill 的功能特点、最佳实践与使用心得。","2026-06-15",[23,24,25,26,27,28],"agent","skills","AI","效率工具","前端","Vue",4169,{"slug":31,"title":32,"description":33,"pub_date":34,"tags":35,"draft":15,"word_count":40},"linux-kernel-skeleton-struct-funcptr-container_of","Linux 内核骨架：struct、函数指针与 container_of","读懂 Linux 内核源码的三件套：巨大的 struct 组合代替继承、函数指针表实现虚派发、container_of 宏从嵌入成员找回完整对象。","2026-05-09",[36,37,38,39],"linux","kernel","C","container_of",1369,{"slug":42,"title":43,"description":44,"pub_date":45,"tags":46,"draft":15,"word_count":52},"astro-complete-guide-2025","Astro 5 深度剖析：Islands 架构原理、构建优化与 Cloudflare Workers 边缘部署","从编译器视角解析 Astro 5 的 Islands 架构实现原理，Content Layer API 的 Vite 插件机制，Server Islands 的流式渲染，以及如何在 Cloudflare Workers + D1 边缘环境下榨干性能。","2026-05-08",[47,48,49,50,51],"astro","frontend","cloudflare","performance","architecture",3663,{"slug":54,"title":55,"description":56,"pub_date":57,"tags":58,"draft":15,"word_count":62},"llm-prompt-engineering","Prompt Engineering 实战：让 LLM 真正听话的技巧","System prompt 怎么写、Few-shot 怎么设计、Chain-of-Thought 原理，以及常见失败模式和调试方法。","2026-05-03",[59,60,61],"ai","llm","工程实践",1723,{"slug":64,"title":65,"description":66,"pub_date":57,"tags":67,"draft":15,"word_count":69},"rag-system-design","RAG 系统设计：从 naive 到 production-ready","Retrieval-Augmented Generation 不只是「向量数据库 + LLM」，分块策略、召回质量、重排序、缓存才是工程核心。",[59,68,60,61],"rag",1613,{"slug":71,"title":72,"description":73,"pub_date":57,"tags":74,"draft":15,"word_count":76},"git-advanced-workflow","Git 进阶工作流：rebase、cherry-pick、bisect 的正确使用","merge 会了，但 rebase 总搞错？bisect 找 bug 提交？interactive rebase 整理历史？这篇一次说清楚。",[75,61],"git",1396,{"slug":78,"title":79,"description":80,"pub_date":57,"tags":81,"draft":15,"word_count":83},"docker-practical-guide","Docker 实战：从会用到用好","会 docker run 不够，Dockerfile 最佳实践、多阶段构建、Compose 编排、镜像瘦身才是日常真正需要的。",[82,36,61],"docker",1268,{"slug":85,"title":86,"description":87,"pub_date":57,"tags":88,"draft":15,"word_count":90},"anthropics-skills-guide","anthropics\u002Fskills：Anthropic 官方 Agent Skills 仓库解析","Anthropic 官方开源的 Agent Skills 标准仓库，127k stars，解析 SKILL.md 规范、17 个示例 skill 的设计模式，以及如何在 Claude Code \u002F Claude.ai \u002F API 中使用",[59,89,23,24],"Claude",2090,{"slug":92,"title":93,"description":94,"pub_date":57,"tags":95,"draft":15,"word_count":97},"karpathy-claude-code-guidelines","Karpathy 的 LLM 编码批评与 CLAUDE.md 最佳实践","基于 Andrej Karpathy 对 LLM 编程助手的观察，forrestchang 提炼出一个 CLAUDE.md 文件，4 条原则解决 AI 编码的典型失控问题：乱猜假设、过度设计、乱改代码、目标不清",[59,89,96,61],"Claude Code",2699,{"slug":99,"title":100,"description":101,"pub_date":57,"tags":102,"draft":15,"word_count":106},"typescript-advanced-patterns","TypeScript 高级模式：让类型系统为你工作","基础 TS 会了但类型总是 any？条件类型、映射类型、模板字面量类型、infer 关键字才是 TS 的真正威力。",[12,103,104,105],"类型系统","前端工程","高级模式",1419,{"slug":108,"title":109,"description":110,"pub_date":57,"tags":111,"draft":15,"word_count":115},"linux-performance-tuning","Linux 性能调优实战：从 top 到 perf 的完整工具链","遇到性能问题不知道从哪下手？这篇建立系统化的排查思路，从 CPU\u002F内存\u002FIO\u002F网络逐层分析。",[36,112,113,114],"性能","运维","系统编程",1524,{"slug":117,"title":118,"description":119,"pub_date":57,"tags":120,"draft":15,"word_count":125},"python-functional-programming","Python 函数式编程：map\u002Ffilter\u002Freduce 之外","Python 不是纯函数式语言，但 functools、itertools、偏函数、闭包这些工具用好了能让代码简洁一个量级。",[121,122,123,124],"python","函数式","闭包","装饰器",1867,{"slug":127,"title":128,"description":129,"pub_date":57,"tags":130,"draft":15,"word_count":134},"python-oop-guide","Python 面向对象：__init__ 之外你需要知道的","Python OOP 不只是 class + __init__，魔术方法、描述符、元类才是真正的武器。",[121,131,132,133],"OOP","面向对象","魔术方法",1792,{"slug":136,"title":137,"description":138,"pub_date":57,"tags":139,"draft":15,"word_count":142},"python-data-structures","Python 内置数据结构深度解析","list、dict、set、tuple 不只是数据容器，搞懂它们的底层实现和时间复杂度，才能写出高性能 Python。",[121,140,112,141],"数据结构","算法",1517,{"slug":144,"title":145,"description":146,"pub_date":57,"tags":147,"draft":15,"word_count":150},"python-basics-quick-start","Python 快速上手：写给有编程基础的人","已经会其他语言，想快速掌握 Python 的语法特性和思维方式，这篇是捷径。",[121,148,149],"入门","基础",1607,{"slug":152,"title":153,"description":154,"pub_date":57,"tags":155,"draft":15,"word_count":159},"python-dataclass-pydantic","Python dataclass vs Pydantic：数据类选型指南","dataclass 是标准库的轻量选择，Pydantic v2 是带验证的重武器，什么时候用哪个，这篇说清楚。",[121,156,157,158],"dataclass","pydantic","数据验证",1323,{"slug":161,"title":162,"description":163,"pub_date":57,"tags":164,"draft":15,"word_count":168},"python-asyncio-practical","Python asyncio 实战：从回调地狱到协程优雅","asyncio 是 Python 异步编程的核心，搞懂 event loop、Task、gather 这些概念才能写出真正高效的异步代码。",[121,165,166,167],"asyncio","并发","网络编程",1258,{"slug":170,"title":171,"description":172,"pub_date":57,"tags":173,"draft":15,"word_count":177},"python-type-hints-guide","Python 类型注解完全指南：从入门到实践","Python 3.5+ 引入类型注解，配合 mypy\u002Fpyright 让 Python 也能享受静态类型检查的好处。",[121,174,175,176],"typescript-style","type-hints","工具链",1102,{"slug":179,"title":180,"description":181,"pub_date":182,"tags":183,"draft":15,"word_count":187},"pwa-install-update-button","PWA 踩坑：为什么安装按钮从来不出现","从 beforeinstallprompt 到 Service Worker waiting，把 PWA 的安装与更新提示真正做对","2026-05-02",[184,185,186],"pwa","javascript","web",1683,{"slug":189,"title":190,"description":191,"pub_date":192,"tags":193,"draft":15,"word_count":194},"openclaw-vs-hermes-agent","OpenClaw vs Hermes Agent：两个本地优先 Agent 的设计差异","OpenClaw（Novita AI）和 Hermes Agent（Nous Research）都是本地运行的个人 AI Agent，但在记忆系统、技能学习、运行环境和模型生态上走了不同的路。深入对比两种架构的核心差异。","2026-05-01",[59,23,60],1679,{"slug":196,"title":197,"description":198,"pub_date":192,"tags":199,"draft":15,"word_count":204},"cpp-random-design-patterns","C++ 设计模式实战：RAII、观察者、工厂","用现代 C++（C++17\u002F20）实现三种高频设计模式：RAII 资源管理、观察者模式事件系统、工厂模式插件架构。每种模式给出问题场景、实现代码和真实工程案例。",[200,201,202,203],"cpp","设计模式","c++17","工程",2613,{"slug":206,"title":207,"description":208,"pub_date":192,"tags":209,"draft":15,"word_count":210},"data-structures-fundamentals","数据结构基础：从数组到红黑树","系统梳理常用数据结构的核心原理、时间复杂度和适用场景。数组、链表、栈、队列、哈希表、二叉树、堆、图，每种结构附实现要点和 C++ 代码片段。",[140,141,200,149],3004,{"slug":212,"title":213,"description":214,"pub_date":215,"tags":216,"draft":15,"word_count":217},"ai-agent-what-is","什么是 AI Agent？从 LLM 到自主执行","LLM 本身是无状态问答机，Agent 是什么让它’动’起来的？本文深入解析 Agent 的四个核心能力、ReAct 框架、工具调用原理，以及主流框架横向对比。","2026-04-30",[59,23,60],2116,{"slug":219,"title":220,"description":221,"pub_date":215,"tags":222,"draft":15,"word_count":223},"ai-agent-memory","AI Agent 的记忆系统：从上下文窗口到长期记忆","深入拆解 AI Agent 的四种记忆类型、上下文窗口压缩策略、RAG 向量检索原理，以及三种典型失败模式和工程选型建议。",[59,23,68],2052,{"slug":225,"title":226,"description":227,"pub_date":215,"tags":228,"draft":15,"word_count":232},"network-proxy-vpn-guide","代理与翻墙技术原理：从 HTTP 代理到现代协议","深入解析代理与 VPN 的本质区别，梳理从 SOCKS5 到 Shadowsocks、V2Ray\u002FXray、Hysteria2 的协议演进，以及机场订阅的技术本质。",[229,230,231],"网络","代理","协议",2148,{"slug":234,"title":235,"description":236,"pub_date":215,"tags":237,"draft":15,"word_count":150},"algorithm-binary-search","二分查找：永远写不对？记住这个模板","彻底搞清楚二分查找的边界问题：闭区间和左闭右开两套模板、三道经典 LeetCode 题目完整 C++ 实现，以及二分答案的进阶思路。",[141,238,239,200],"二分查找","leetcode",{"slug":241,"title":242,"description":243,"pub_date":215,"tags":244,"draft":15,"word_count":246},"algorithm-sliding-window","滑动窗口算法：从暴力到 O(n) 的思维跃迁","系统讲解滑动窗口算法的核心模板、适用题型，配合三道经典 LeetCode 题目的完整 C++ 实现，彻底理解双指针收缩思路。",[141,245,239,200],"滑动窗口",1943,{"slug":248,"title":249,"description":250,"pub_date":215,"tags":251,"draft":15,"word_count":254},"network-clash-config","Clash \u002F Mihomo 配置详解：规则、策略组与分流","深入解析 Clash\u002FMihomo 的核心配置结构，包括代理节点、策略组类型、规则优先级、DNS fake-ip 模式，以及一份实用的完整配置模板。",[229,252,230,253],"clash","配置",1292,{"slug":256,"title":257,"description":258,"pub_date":259,"tags":260,"draft":15,"word_count":263},"hid-hotplug","HID 设备热插拔检测：从 udev 到 node-hid","在 Linux 上用 node-hid + usb 库实现可靠的 USB HID 设备热插拔检测，踩坑记录","2026-04-28",[200,261,36,262,11],"hid","nodejs",2039,{"slug":4,"title":5,"description":6,"pub_date":9,"tags":265,"draft":15,"word_count":266},[11,12,13,14],1446,{"slug":268,"title":269,"description":270,"pub_date":271,"tags":272,"draft":15,"word_count":275},"element-plus-popover-hide","手动关闭多个 el-popover（不用 v-model:visible）","通过 ref + Reflect.get 调用 hide() 方法手动关闭 Element Plus Popover，解释 Vue3 Proxy 导致无法直接调用实例方法的原因。","2024-10-25",[14,273,274],"element-plus","vue3",1321,{"slug":277,"title":278,"description":279,"pub_date":280,"tags":281,"draft":15,"word_count":285},"vite-vue3-ts-elementplus-pinia","用 Vite+（vp）从零搭建 Vue3 + TypeScript + Element Plus + Pinia + Vue Router","使用 Vite+ 统一工具链（vp）一条命令搭建 Vue3 全家桶，涵盖按需导入、Pinia store、路由配置，以及常见坑的解决方案。","2024-08-27",[14,282,12,273,283,284],"vite","pinia","vite-plus",1960,{"slug":287,"title":288,"description":289,"pub_date":290,"tags":291,"draft":15,"word_count":295},"cef-lnk2038-iterator-debug-level","CEF LNK2038：解决 _ITERATOR_DEBUG_LEVEL 不匹配错误","分析 CEF（Chromium Embedded Framework）集成时出现的 LNK2038 _ITERATOR_DEBUG_LEVEL 链接错误，从根本原因到解决方案的完整指南。","2024-05-07",[200,292,293,294],"CEF","Visual Studio","链接错误",1509,{"slug":297,"title":298,"description":299,"pub_date":300,"tags":301,"draft":15,"word_count":305},"npm-electron-install-fix","彻底解决 npm 安装 Electron 失败的问题","分析 npm install electron 失败的根本原因（下载二进制超时\u002F被墙），通过国内镜像（npmmirror）彻底解决，并介绍多种备选方案和常见错误排查。","2024-03-01",[11,302,303,304],"npm","前端工具链","国内镜像",1494,{"slug":307,"title":308,"description":309,"pub_date":310,"tags":311,"draft":15,"word_count":313},"git-out-of-memory","解决 git 报错：Fatal: Out of memory, malloc failed","分析 git 大仓库操作时出现 Out of memory malloc failed 的根本原因，通过调整 pack.windowMemory、http.postBuffer 和 git repack 彻底解决。","2024-01-31",[75,36,312],"工具",2244,{"slug":315,"title":316,"description":317,"pub_date":318,"tags":319,"draft":15,"word_count":323},"vmware-tools-install","在 VMware 虚拟机中安装 open-vm-tools 完整指南","详解 VMware Tools 的作用、open-vm-tools 与官方 VMware Tools 的区别，以及在 Ubuntu 虚拟机中安装并生效的完整步骤和常见问题排查。","2023-11-21",[320,36,321,322],"VMware","Ubuntu","虚拟机",2523,{"slug":325,"title":326,"description":327,"pub_date":328,"tags":329,"draft":15,"word_count":334},"load-balancing-algorithms","负载均衡算法完全指南：从轮询到一致性哈希","系统梳理静态与动态负载均衡算法，涵盖轮询、随机、权重、IP Hash、一致性 Hash、最少连接、最快响应等，并对比 Nginx、Dubbo、Spring Cloud LoadBalancer 的实现差异。","2023-11-15",[330,331,332,333],"分布式","负载均衡","Nginx","微服务",1764,{"slug":336,"title":337,"description":338,"pub_date":339,"tags":340,"draft":15,"word_count":344},"win-cw2a-ca2w","ATL 字符串转换：CW2A 与 CA2W 完全指南","详解 ATL 宏 CW2A\u002FCA2W 在 Unicode 与 ANSI 之间的字符串转换用法、头文件依赖、USES_CONVERSION 宏的作用与常见陷阱。","2023-06-09",[200,341,342,343],"windows","ATL","字符串",1665,{"slug":346,"title":347,"description":348,"pub_date":339,"tags":349,"draft":15,"word_count":353},"csharp-sendmessage-cpp","C# 通过 SendMessage 向 C++ 窗口发送消息与字符串","使用 P\u002FInvoke 调用 user32.dll 的 SendMessage，从 C# 发送自定义 WM_USER 消息及字符串指针给 C++ 原生窗口，并在 C++ 侧正确接收和转换。",[350,200,341,351,352],"C#","互操作","PInvoke",1554,{"slug":355,"title":356,"description":357,"pub_date":358,"tags":359,"draft":15,"word_count":361},"win-postmessage-vector","Windows PostMessage 跨线程传递 std::vector 指针","通过 PostMessage 在 Windows 消息队列中传递 std::vector 指针，使用 reinterpret_cast 将指针装入 LPARAM，并在接收方正确释放内存。","2023-05-26",[200,341,360],"WinAPI",1823,{"slug":363,"title":364,"description":365,"pub_date":358,"tags":366,"draft":15,"word_count":367},"exe-dll-single-package","将 EXE 和 DLL 打包成单一可执行文件","介绍两种将 exe 和依赖 dll 打包成单文件的方案：Enigma Virtual Box 和 WinRAR 自解压，适合发布 Windows 桌面程序时简化分发流程。",[341,200,312],1619,{"slug":369,"title":370,"description":371,"pub_date":358,"tags":372,"draft":15,"word_count":375},"cpp-random-mt19937","C++ 现代随机数生成：用 mt19937 彻底告别 rand()","深入讲解为什么 rand() 不够用，以及如何用 C++11 的 \u003Crandom> 库正确生成高质量随机数，涵盖 mt19937、各种分布和线程安全。",[200,373,374],"c++11","random",1549,{"slug":377,"title":378,"description":379,"pub_date":380,"tags":381,"draft":15,"word_count":383},"win-startup-registry","C++ 实现程序开机自启动：注册表方式详解","通过操作 Windows 注册表 Run 键实现程序开机自启动，包括 HKCU 与 HKLM 区别、完整封装代码、工作目录问题和 UAC 权限处理。","2022-12-26",[341,200,382],"registry",1201,{"slug":385,"title":386,"description":387,"pub_date":388,"tags":389,"draft":15,"word_count":391},"mfc-cstring-wparam","MFC 中 CString 与 WPARAM 之间的转换","详解 MFC 消息传递中 CString 无法直接强转为 WPARAM 的原因，以及两种正确的转换方案，并介绍结构体指针传递的正确姿势。","2022-11-25",[390,200,341],"mfc",1546,{"slug":393,"title":394,"description":395,"pub_date":396,"tags":397,"draft":15,"word_count":399},"duilib-static-build","正确编译 Duilib 静态库：避免 ATL 依赖和链接错误","详解如何用 DuiLib_Static.vcxproj 编译 Duilib 静态库，解决 VARIANT 未定义、Unicode 配置不匹配和 ATL 依赖等常见问题。","2022-08-24",[200,398,341,390],"duilib",2639,{"slug":401,"title":402,"description":403,"pub_date":404,"tags":405,"draft":15,"word_count":407},"mfc-dpi-adaptive","MFC 界面自适应不同分辨率","MFC 对话框程序实现控件和字体随分辨率自动缩放的完整方案，附 DPI Awareness 配置说明","2022-08-17",[390,200,341,406],"dpi",1414,{"slug":409,"title":410,"description":411,"pub_date":412,"tags":413,"draft":15,"word_count":414},"mfc-drag-window","MFC 无标题栏窗口客户区拖动：三种方法对比","MFC 对话框去掉标题栏后如何实现拖动移动窗口，三种方案完整实现与适用场景分析","2022-08-16",[390,200,341],1633,{"slug":416,"title":417,"description":418,"pub_date":419,"tags":420,"draft":15,"word_count":422},"algorithm-number-complement","整数的补数：位运算掩码解法","LeetCode 476 题，用掩码 XOR 实现整数补数，附 C++\u002FPython\u002FJava 三种实现及补数与补码的区别","2021-03-08",[141,421,239],"位运算",1374,[]]