- 桌面应用
- 数据分析
- 数据可视化
【免费下载链接】cs-demo-manager
Companion application for your Counter-Strike demos.
cs-demo-manager 是一个典型的“重逻辑集中在单点”的多进程应用:Electron 渲染进程、主进程、独立 CLI 与游戏内插件全部作为客户端,挂接到一个独立的 WebSocket 服务器守护进程上。本文完整讲解官方技能文档 skills/process-communication/SKILL.md 中定义的七类跨进程通信模式(Renderer/Server 请求响应、服务器推送、主进程双向消息、Electron IPC、CLI 通信、与 Counter-Strike 插件的双向通道),并结合 src/server/server.ts 等源码剖析消息分发、uuid 应答关联、守护进程发现与空闲退出等底层机制,帮助你在新增任何一条消息、handler 或 IPC 通道时有章可循。
一、总体架构:一个 WebSocket 守护进程,多个客户端
应用实际运行三个(及以上)操作系统级进程,所有重量级逻辑都位于WebSocket server进程中。它是一个分离式守护进程(detached daemon),被 GUI 和 CLI 共享:
- attach-or-spawn:任何需要连接的一方先通过应用目录下的
daemon.json发现文件探测是否已有健康守护进程,有则直接附着,无则拉起一个新的; - idle-exit:当没有任何客户端连接且没有后台工作时,守护进程自行退出,释放内存与端口;
- 客户端构成:Renderer(React UI)、Electron 主进程、CLI 以 WebSocket 客户端身份接入;Counter-Strike 游戏内的 C++ 插件在游戏运行时也作为一个客户端接入。
文档给出的进程拓扑图:
Electron main process ←IPC→ Renderer process (UI) Counter-Strike CLI ↕ ↕ ↕ ↕ └──────────→ WebSocket server process (daemon) ←───┴───────────┘各进程职责
| 进程 | 入口 | 职责 |
|---|---|---|
electron-main | src/electron-main/main.ts | 窗口管理、托盘、自动更新、IPC 注册 |
server | src/server/start-server.ts | WebSocket hub 守护进程;将消息分发给类型化 handler;运行后台任务(demo 分析、下载、视频队列) |
renderer | src/ui/renderer.tsx | React UI;仅通过 WebSocket 客户端通信(src/ui/web-socket-client.ts) |
preload | src/preload/preload.ts | 通过contextBridge把 Node.js API 桥接给渲染进程(文件 I/O、设置、OS 级对话框的 IPC) |
cli | src/cli/cli.ts | 独立 CLI;附着到运行中的守护进程或拉起一个新的(src/node/daemon/attach-or-spawn-daemon.ts) |
消息封套与分发机制
每一条 WebSocket 消息都是一个 JSON 对象:{ name, payload?, uuid? }。服务器把收到的消息按name分发给对应的类型化 handler 函数,并以SharedServerMessageName.Reply或SharedServerMessageName.ReplyError应答。所有消息名枚举与共享消息类型集中在src/server/messages/目录。
从 src/server/server.ts 的源码可以看到这套机制的完整实现:
- 客户端识别:
onConnection通过连接 URL 的查询参数?process=区分客户端类型——main、renderer、cli、probe,其余一律视为游戏进程(见 src/server/server.ts#L214-L270)。CLI 客户端额外分配一个随机clientId,用于在连接断开时回收它入队的资源(videoQueue.removeVideosAddedByClient(clientId)、analysesListener.removeDemosAddedByClient(clientId)),因为 CLI 进程可能直接崩溃而没有机会自行清理。 - 消息分发:
dispatchMessageToHandlers以消息名从对应的 handlers mapping 对象中取 handler 执行;未知消息名只记 warning 不回复。成功时回{ name: 'reply', payload: result, uuid },失败时把错误归一为ErrorCode数字或字符串后回{ name: 'reply-error', payload: errorPayload, uuid }(见 src/server/server.ts#L290-L333)。 - uuid 关联:客户端发送请求时生成
uuid并注册replyHandler,服务器原样带回该uuid,客户端据此 resolve/reject 对应的 Promise。三端客户端(src/ui/web-socket-client.ts、src/electron-main/web-socket/web-socket-client.ts、src/cli/web-socket/cli-web-socket-client.ts)都实现了同一套Reply / ReplyError分支。
uuid与name/payload的组合类型定义在 src/server/messages/identifiable-client-message.ts,请求/应答的共享消息名定义在 src/server/messages/shared-server-message-name.ts。
端口与守护进程生命周期
- 端口:默认端口为
4574,环境变量CSDM_WS_PORT可覆盖(src/server/port.ts)。注意其中注释要求该默认值必须与 cs2-server-plugin/cs2-server-plugin/main.cpp#L449-L462 中的DEFAULT_WEB_SOCKET_SERVER_PORT以及csgo-server-plugin中对应常量保持同步——这是 Node 侧与 C++ 侧的一份跨语言“契约”。 - 守护进程启动:src/server/start-server.ts 依次执行
server.listen()绑定端口、writeDaemonInfoFile({ port, pid, version })写发现文件、startIdleMonitor()启动空闲监视,并注册SIGINT/SIGTERM的优雅退出。 - 发现文件:
daemon.json位于应用文件夹(src/node/daemon/daemon-info-file.ts),包含port、pid、version三个字段;写入采用“先写临时文件再 rename”的原子策略,保证轮询该文件的并发读者永远不会看到半截写入。 - attach-or-spawn:src/node/daemon/attach-or-spawn-daemon.ts 的
attachOrSpawnDaemon先读发现文件,校验进程存活(isProcessAlive)与探针健康(probeDaemon),再处理版本不一致:若旧版本守护进程既无客户端也无进行中工作,会通过 probe 通道请求其退出并轮询最多 40 秒等待其消亡;若有客户端附着则降级为附着并在日志中警告版本差异。否则以detached: true拉起新的server.jsbundle,然后每 250ms 轮询发现文件 + 探针直到就绪(默认 10 秒超时)。 - probe 客户端:探针是短生命周期连接(src/node/daemon/probe-daemon.ts),2 秒超时,只被允许查询
GetDaemonStatus与ShutdownDaemon(见 src/server/handlers/cli-handlers-mapping.ts#L55-L60 中的probeHandlers),且不计入客户端计数,避免“探测”本身阻止空闲退出。 - 空闲退出:src/server/idle-monitor/idle-monitor.ts 中
hasWorkInProgress()聚合了 demo 分析进行中、视频队列忙、下载队列非空、数据库连接挂起四类“后台工作”;客户端计数与工作判定均为空时才走exitDaemon()。 - 端口冲突:
server.ts的handlePortAlreadyInUse处理EADDRINUSE——若占用者是一个同版本(dev/production 区分)的 CSDM 守护进程(典型场景:两个进程几乎同时拉起 daemon),则本进程直接退出,让先启动者持有端口;否则回退到0端口让操作系统分配随机端口,实际端口通过CSDM_WS_PORT环境变量与发现文件暴露给其他进程(src/server/server.ts#L457-L486)。
这套“文件发现 + 探针校验 + 空闲退出”的机制,是后续所有通信模式能成立的前提:任何客户端都能用统一的方式找到并连上唯一的服务器进程。
二、模式一:Renderer → Server(请求/响应)
这是最常用的模式:UI 请求服务器做一件事并等待结果。
需要改动的文件
| 文件 | 加什么 |
|---|---|
src/server/messages/renderer-client-message-name.ts | 新增出站消息名枚举条目 |
src/server/messages/server-push-message-name.ts | 若服务器还需异步回推,则补充 payload/响应类型 |
src/server/handlers/renderer-process/<feature>/<name>-handler.ts | Handler 函数 |
src/server/handlers/renderer-handlers-mapping.ts | 注册 handler |
1. 添加消息名
// src/server/messages/renderer-client-message-name.ts export const RendererClientMessageName = { // …existing entries… MyNewAction: 'my-new-action', } as const;2. 编写 handler
// src/server/handlers/renderer-process/<feature>/my-new-action-handler.ts import { handleError } from '../../handle-error'; export type MyNewActionPayload = { id: number }; export async function myNewActionHandler(payload: MyNewActionPayload) { try { const result = await doSomething(payload.id); return result; // 返回值会被服务器作为 Reply 的 payload 发回 } catch (error) { handleError(error, 'Error in myNewActionHandler'); } }handleError的源码细节:src/server/handlers/handle-error.ts 全文只有 12 行——它先通过getErrorCodeFromError把未知异常映射为ErrorCode.UnknownError(仅此时打印日志,已知错误码不重复打印),然后throw errorCode。注意两点:
- 它的返回类型是
never,调用后不要再写return;也不需要在它前面加logger.error,因为未知错误它已经记录过了; - 抛出的应该是 src/common/error-code.ts 中的
ErrorCode数字或普通字符串——服务器端在dispatchMessageToHandlers中把这类值自动包装进ReplyError消息,其他未知异常则归一为ErrorCode.UnknownError并打日志。
长任务型 handler(无返回值、全程靠 push 事件对外通信)应跳过handleError,改为手动处理错误以便发送错误 push 事件:
export async function longTaskHandler(payload: LongTaskPayload) { try { for (const [i, item] of payload.items.entries()) { server.sendPushMessage({ name: ServerPushMessageName.LongTaskProgress, payload: { count: i + 1, totalCount: payload.items.length }, }); await processItem(item); } server.sendPushMessage({ name: ServerPushMessageName.LongTaskSuccess }); } catch (error) { logger.error('Error during long task'); logger.error(error); server.sendPushMessage({ name: ServerPushMessageName.LongTaskError }); } }3. 注册 handler
// src/server/handlers/renderer-handlers-mapping.ts import { myNewActionHandler } from './renderer-process/<feature>/my-new-action-handler'; export const rendererHandlers: RendererMessageHandlers = { // …existing entries… [RendererClientMessageName.MyNewAction]: myNewActionHandler, };实际文件 src/server/handlers/renderer-handlers-mapping.ts 中,RendererMessageHandlers是一个以消息名为计算属性键的接口,逐条声明Handler<Payload, ReturnType>,映射对象必须完整实现该接口——漏注册任何一条都会直接编译报错。这是这套架构类型安全的关键一环(第四节详述)。
4. 在 UI 中调用
// anywhere inside src/ui/ import { useWebSocketClient } from 'csdm/ui/web-socket/use-web-socket-client'; import { RendererClientMessageName } from 'csdm/server/messages/renderer-client-message-name'; const client = useWebSocketClient(); const result = await client.send({ name: RendererClientMessageName.MyNewAction, payload: { id: 42 }, });result的类型并非unknown:WebSocketClient.send的返回类型是ReturnType<RendererMessageHandlers[MessageName]>(src/ui/web-socket-client.ts#L74-L87),也就是说你在 mapping 里声明的 handler 返回类型会自动成为 UI 侧await的结果类型,消息名、payload、响应三处类型全部对齐。
从 src/ui/web-socket-client.ts 的实现还能看到两个工程细节:
- 连接 URL 为
ws://localhost:${window.csdm.WEB_SOCKET_SERVER_PORT}?process=renderer,端口由 preload 从环境变量注入; - 断线时客户端自动重连;断线期间发出的
send不会丢失,而是进入messageQueue,重连成功后按序补发(onConnect中遍历队列)。
三、模式二:Server → Renderer/CLI(服务器推送 / 事件)
当服务器需要在没有前置请求的情况下推送更新(进度事件、后台任务完成等)时使用。push 消息会同时扇出到 renderer 与所有已连接的 CLI 客户端——这正是“CLI 发起的工作能实时显示在 GUI 上、反之亦然”的实现基础。
服务端sendPushMessage的扇出逻辑见 src/server/server.ts#L151-L164:对cliSockets逐个发送,再发给 renderer socket;若两者都不存在(典型场景:只有 CLI 在用时 UI 未打开),会打一条 warning 说明该 push 被丢弃,而不是抛异常。
需要改动的文件
| 文件 | 加什么 |
|---|---|
src/server/messages/server-push-message-name.ts | 新增枚举条目 + payload 类型 |
| handler 或后台任务 | 调用server.sendPushMessage(…) |
| UI 组件 / hook | 用client.on(name, listener)订阅 |
1. 声明 push 消息
// src/server/messages/server-push-message-name.ts export const ServerPushMessageName = { // …existing entries… MyProgressUpdate: 'my-progress-update', } as const; export interface ServerPushMessagePayload extends SharedServerMessagePayload { // …existing entries… [ServerPushMessageName.MyProgressUpdate]: { percent: number }; }对照 src/server/messages/server-push-message-name.ts 的实际内容,可以看到这一模式在真实业务中已大量落地:DownloadDemoProgress、AnalysisUpdated、VideoUpdated、ExportToXlsxProgress、SettingsUpdated等数十条推送消息都按此结构定义。
2. 从服务器推送
import { server } from 'csdm/server/server'; import { ServerPushMessageName } from 'csdm/server/messages/server-push-message-name'; server.sendPushMessage({ name: ServerPushMessageName.MyProgressUpdate, payload: { percent: 50 }, });3. 在 UI 中监听
import { useWebSocketClient } from 'csdm/ui/web-socket/use-web-socket-client'; import { ServerPushMessageName } from 'csdm/server/messages/server-push-message-name'; import { useEffect } from 'react'; function MyComponent() { const client = useWebSocketClient(); useEffect(() => { const onProgress = ({ percent }: { percent: number }) => { console.log(percent); }; client.on(ServerPushMessageName.MyProgressUpdate, onProgress); return () => { client.off(ServerPushMessageName.MyProgressUpdate, onProgress); }; }, [client]); }CLI 通过自己的客户端以完全相同的方式订阅(src/cli/web-socket/cli-web-socket-client.ts中的on/off与 renderer 客户端同构,见 src/cli/web-socket/cli-web-socket-client.ts#L43-L68)。
四、模式三:Main 进程 → WS Server(请求/响应)
Electron 主进程使用与 Renderer 相同的 WebSocket 模式,但走的是独立的客户端类与独立的 handler 映射:
| 文件 | 加什么 |
|---|---|
src/server/messages/main-client-message-name.ts | 新增枚举条目 |
src/server/messages/main-server-message-name.ts | 按需补充 payload/响应类型 |
src/server/handlers/main-process/<name>-handler.ts | Handler 函数 |
src/server/handlers/main-handlers-mapping.ts | 注册 handler |
模式与模式一完全镜像。主进程侧的客户端在src/electron-main/web-socket/create-web-socket-client.ts中创建(src/electron-main/web-socket/create-web-socket-client.ts 的createWebSocketClient同时完成了若干 push 监听器的注册,如DownloadValveDemoStarted等下载通知):
// src/electron-main/some-file.ts import { MainClientMessageName } from 'csdm/server/messages/main-client-message-name'; // fire-and-forget await client.send({ name: MainClientMessageName.MyAction }); // or wait for a typed reply const result: boolean = await client.send({ name: MainClientMessageName.MyActionWithReply });以 src/server/handlers/main-handlers-mapping.ts 为例,目前主进程只有两条消息:StartMinimizedMode与StartCounterStrike(后者返回CounterStrikeErrorPayload | undefined),映射同样以接口 + 对象的形式提供完整类型约束。
五、模式四:WS Server → Main 进程(服务器推送)
| 文件 | 加什么 |
|---|---|
src/server/messages/main-server-message-name.ts | 新增枚举条目 + payload 类型 |
| handler 或后台任务 | 调用server.sendMessageToMainProcess(…) |
src/electron-main/web-socket/… | 用client.on(name, listener)监听 |
1. 声明推送消息(与模式二第 1 步相同,只是写在main-server-message-name.ts)。对照 src/server/messages/main-server-message-name.ts:MainServerMessagePayload定义请求 payload,另有一个MainServerMessageResponse接口描述主进程回传的响应类型——这是本模式独有的。
2. 从服务器推送
import { server } from 'csdm/server/server'; import { MainServerMessageName } from 'csdm/server/messages/main-server-message-name'; server.sendMessageToMainProcess({ name: MainServerMessageName.MyEvent, payload: { data: 'value' }, });3. 在主进程中监听
client.on(MainServerMessageName.MyEvent, ({ data }) => { // handle event });这一模式值得从源码多看一层:main通道的消息流与其他客户端不同。服务器端 src/server/server.ts#L344-L387 的onMainProcessSocketMessage会把收到的Reply/ReplyError消息路由到按uuid索引的mainReplyHandlers,其余消息才走mainHandlers分发;而主进程客户端(src/electron-main/web-socket/web-socket-client.ts#L151-L186)在监听器执行后,会把监听器的返回值(或异常)作为Reply/ReplyError带原uuid发回服务器。配合服务器侧的sendToMainAndWaitForReply(src/server/server.ts#L176-L192:生成 uuid、登记 resolve/reject、发送消息并等待),就构成了服务器主动向主进程发起请求并等待其应答的完整链路——典型用例是GetScreenSize:服务器请求主进程测量屏幕尺寸,主进程 listener 返回Size后,服务器的 Promise 以MainServerMessageResponse.GetScreenSize声明的Size类型 resolve。
六、模式五:Main ↔ Renderer(Electron IPC)
Electron IPC 只用于不需要服务器的 OS 级交互:原生对话框、窗口状态、托盘、系统自启。其余一切优先走 WebSocket 通道。
| 文件 | 加什么 |
|---|---|
src/common/ipc-channel.ts | 新的 channel 字符串常量 |
src/electron-main/register-main-process-listeners.ts | ipcMain.handle(IPCChannel.Foo, …) |
types/window-preload.d.ts | PreloadApi中的方法签名 |
src/preload/preload.ts | api对象中的实现 |
src/ui/ | 通过window.csdm.myMethod()调用 |
1. 添加 channel 常量
// src/common/ipc-channel.ts export const IPCChannel = { // …existing entries… MyAction: 'my-action', };现有的 src/common/ipc-channel.ts 列出了全部通道:BrowseToFolder、ShowSaveDialog、窗口最大化/最小化、剪贴板、自启行为等,清一色 OS/窗口级能力。
2. 在主进程注册 handler
// src/electron-main/register-main-process-listeners.ts ipcMain.handle(IPCChannel.MyAction, async (event, arg: string) => { return doSomething(arg); });3. 在 preload API 中声明类型
// types/window-preload.d.ts (inside the PreloadApi interface) myAction: (arg: string) => Promise<string>;4. 通过 preload 暴露
// src/preload/preload.ts (inside the api object) myAction: (arg: string): Promise<string> => ipcRenderer.invoke(IPCChannel.MyAction, arg),5. 在 UI 中调用
const result = await window.csdm.myAction('hello');ipc-channel.ts顶部注释解释了为什么必须走 preload:渲染进程禁用了 Node 集成,所有与主进程交互的方法都必须经由 src/preload/preload.ts 注入window对象。renderer 侧用到的window.csdm.WEB_SOCKET_SERVER_PORT也是同一注入机制的产物。
七、模式六:CLI → Server(请求/响应)
CLI 拥有自己的一套小型 handler 映射——不要把 renderer 的 handler 暴露给 CLI。这是刻意的能力隔离:对照 src/server/handlers/cli-handlers-mapping.ts,CLI 只能访问GetDaemonStatus、ShutdownDaemon、数据库连接设置、分析入队、视频队列、工具安装等约 12 条消息,而 renderer 侧的映射有 100 余条。
| 文件 | 加什么 |
|---|---|
src/server/messages/cli-client-message-name.ts | 新增枚举条目 |
src/server/handlers/cli-process/<name>-handler.ts | Handler 函数 |
src/server/handlers/cli-handlers-mapping.ts | 注册 handler |
模式与模式一镜像。在命令(src/cli/commands/command.ts的子类)中通过this.connectToDaemon()获取已连接的客户端——它会附着到运行中的守护进程或拉起一个新的:
// src/cli/commands/my-command.ts import { CliClientMessageName } from 'csdm/server/messages/cli-client-message-name'; const client = await this.connectToDaemon(); const result = await client.send({ name: CliClientMessageName.MyAction, payload: { id: 42 } }); // client.send() 带超时(默认 3s,可用第二个参数 { timeoutMs } 按调用覆盖) client.on(ServerPushMessageName.MyProgressUpdate, onProgress); // push 事件同样会到达 CLI(见模式二) client.close(); // 当没有其他客户端且没有进行中的工作时,守护进程会空闲退出源码级补充:
connectToDaemon的实现在 src/cli/commands/command.ts#L97-L105,内部调用 src/cli/create-daemon-connection.ts:server.jsbundle 与正在执行的cli.js同目录,attachOrSpawnDaemon成功后连接ws://localhost:<port>?process=cli;若连接瞬间守护进程恰好空闲退出,还会自动重试一次。- 超时语义:src/cli/web-socket/cli-web-socket-client.ts#L70-L97 中
send默认timeoutMs = 3_000;传{ timeoutMs: null }表示无限等待——src/cli/commands/command.ts 的ensureDaemonDatabaseConnection正是如此使用,因为嵌入式数据库首次initdb或大库迁移可能耗时数分钟,而GetDatabaseConnectionSettings等后续请求仍保持短超时。 - 命令如何直连数据库:
Command.initDatabaseConnection()展示了 CLI 与 daemon 的协作范式——连接设置必须来自 daemon(只有它知道如何到达嵌入式服务器并执行迁移),且 daemon 客户端刻意保持打开直到进程退出,以维持守护进程存活;同时用assertDatabaseSchemaVersionMatches()拒绝在 schema 版本不匹配的“旧版守护进程”上执行查询。 - 服务器侧对 CLI 断开做资源回收(第二节所述的
clientId清理),保证 CLI 崩溃不会遗留挂起的视频或分析任务。
八、模式七:WS Server ↔ Counter-Strike(游戏插件双向通道)
游戏启动后,通过 C++ 插件(cs2-server-plugin/与csgo-server-plugin/)连接 WS 服务器,通信是双向的:插件向服务器发送事件,服务器也能向游戏下发命令。
插件侧的连接实现见 cs2-server-plugin/cs2-server-plugin/main.cpp#L449-L484:优先读取CSDM_WS_PORT环境变量,否则回退到4574(注释明确要求与 src/server/port.ts 中的默认值保持同步),随后连接ws://localhost:<port>?process=game并进入poll + dispatch循环。
监听 Counter-Strike 事件(服务器侧)
新的事件名加入src/server/messages/game-client-message-name.ts(现有条目:Status、CapturePlayerViewResult)。服务器把事件分发到按消息名组织的监听器表gameListeners,同一事件可注册多个监听器,且游戏断开时监听器表会被整体清空(src/server/server.ts#L407-L451):
import { server } from 'csdm/server/server'; import { GameClientMessageName } from 'csdm/server/messages/game-client-message-name'; server.addGameMessageListener(GameClientMessageName.SomeEvent, (payload) => { // handle event }); // Remove all listeners for a given event name: server.removeGameEventListeners(GameClientMessageName.SomeEvent);向 Counter-Strike 发送消息并等待响应
新命令名加入src/server/messages/game-server-message-name.ts(现有条目:PlayDemo、CapturePlayerView)。推荐使用 src/server/counter-strike.ts 中的sendMessageToGame辅助函数:它依次检查游戏是否已连接(否则抛CounterStrikeNotConnected)、注册响应监听器、发送消息、等待 2 秒、清理监听器;若期间未收到响应则抛CounterStrikeNoResponse:
import { sendMessageToGame } from 'csdm/server/counter-strike'; import { GameServerMessageName } from 'csdm/server/messages/game-server-message-name'; import { GameClientMessageName } from 'csdm/server/messages/game-client-message-name'; await sendMessageToGame({ message: { name: GameServerMessageName.SomeCommand, payload: { value: 1 } }, responseMessageName: GameClientMessageName.SomeResponse, onResponse: (payload) => { // handle response }, });对应的错误载荷(CounterStrikeErrorPayload/WatchDemoErrorPayload)通过handleCounterStrikeError等函数以ServerPushMessageName.CounterStrikeError推送给 UI,形成“命令—响应—错误通知”的完整闭环(src/server/counter-strike.ts#L46-L67)。
九、类型系统如何保证“消息契约”不漂移
贯穿上述七种模式的,是src/server/messages/下的一组类型工具,值得单独拆解:
Handler(src/server/messages/handler.ts):(payload, context?) => Promise<ReturnType>。Payload为void时退化为无参签名;HandlerContext.clientId目前只有 CLI 连接会提供,用于按连接追踪资源归属(见第二节的断开清理)。SendableMessage(src/server/messages/message.ts):从各端专属的 handlers mapping推导客户端可发送的消息集合——payload 类型取对应 handler 的第一个参数类型。也就是说:CLI 客户端在类型层面就发不出任何 renderer 专属消息,跨端消息串线在编译期即被禁止。Message:{ name } & (payload 非 void 时附加 payload 字段),保证voidpayload 的消息不携带payload属性。client.send的返回类型:三端客户端均以ReturnType<Handlers[MessageName]>约束返回值,handler 映射即“接口契约”,UI/CLI/主进程三方共享同一份 src/server/handlers 目录下的类型定义。
从这套结构看,新增一条消息的完整闭环是:消息名枚举 → payload/response 类型 → handler 实现 → mapping 注册(接口强制全覆盖)→ 客户端按类型自动获得 send/订阅能力,漏掉任何一环 TypeScript 都会报错。
十、小结:新增一条消息该走哪条通道
结合 skills/process-communication/SKILL.md 的分流原则,可以归纳为一张决策表:
| 场景 | 通道 | 关键文件 |
|---|---|---|
| UI 请求服务器做事并要结果 | 模式一 | renderer-client-message-name.ts+renderer-handlers-mapping.ts |
| 服务器主动通知 UI/CLI(进度、完成) | 模式二 | server-push-message-name.ts+server.sendPushMessage |
| 主进程(托盘、菜单等)请求服务器 | 模式三 | main-client-message-name.ts+main-handlers-mapping.ts |
| 服务器请求主进程能力(如取屏幕尺寸) | 模式四 | main-server-message-name.ts+sendMessageToMainProcess/sendToMainAndWaitForReply |
| OS 级交互(对话框、窗口、托盘、自启) | 模式五 | ipc-channel.ts+register-main-process-listeners.ts+preload.ts |
| CLI 命令 | 模式六 | cli-client-message-name.ts+cli-handlers-mapping.ts+connectToDaemon() |
| 游戏运行时命令/事件 | 模式七 | game-server-message-name.ts/game-client-message-name.ts+sendMessageToGame |
掌握以上七类模式后,你既能沿“消息名 → 类型 → handler → mapping → 客户端调用”的标准路径为 cs-demo-manager 扩展任意跨进程功能,也能理解daemon.json发现、CSDM_WS_PORT端口约定、probe 探活与空闲退出等守护进程机制如何支撑这套通信体系长期稳定运行。
- 桌面应用
- 数据分析
- 数据可视化
【免费下载链接】cs-demo-manager
Companion application for your Counter-Strike demos.
相关推荐
Electron + Vue 响应式 IPC 实战:用 useIpcRenderer 打通主进程与渲染进程
Electron + Vue 响应式 IPC 实战:用 useIpcRenderer 打通主进程与渲染进程 本篇技术指南围绕 VueUse 的 useIpcRe
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染Egg 多进程模型与进程间通讯(IPC)实战指南:Master / Agent / Worker 架构与 Messenger 消息机制
Egg 多进程模型与进程间通讯(IPC)实战指南:Master / Agent / Worker 架构与 Messenger 消息机制 Egg(本项目仓库 gh
后端Web框架Electron 进程间通信(IPC)全指南:ipcMain 与 ipcRenderer 的核心模式与实战示例
Electron 进程间通信(IPC)全指南:ipcMain 与 ipcRenderer 的核心模式与实战示例 进程间通信(IPC)是构建功能丰富的 Elect
桌面应用跨平台前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考