news 2026/10/9 1:17:52

Repowise Codebase Chat 技术指南:基于 MCP 工具集的 SSE 流式 Agent 对话架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Repowise Codebase Chat 技术指南:基于 MCP 工具集的 SSE 流式 Agent 对话架构

【免费下载链接】repowise

Codebase intelligence for AI and humans: code health scores, auto-generated docs, git analytics, dead code detection, and architectural decisions via MCP.

项目地址:https://gitcode.com/gh_mirrors/re/repowise
点击查看免费下载

本文是 Repowise「代码库聊天(Codebase Chat)」功能的技术参考与实战解析。该功能让用户以自然语言直接与代码库交互:Agent 使用用户配置的任意 LLM Provider,从 MCP 工具面(MCP surface)精选7 个工具(而非完整的 11 个默认 MCP 工具),通过 SSE 把流式响应实时推送到浏览器,边生成边展示工具调用过程,并在 Artifact 面板中渲染工具结果。读完本文,你将掌握其端到端调用链(数据库 → ChatProvider 协议 → Agentic Loop → SSE → 前端状态机)、Provider 配置解析规则、SSE 事件协议以及各 Provider(Anthropic / OpenAI / Gemini / Ollama / LiteLLM)的差异化实现。文中所有实现细节均可在当前仓库源码中找到对应依据。

1. 架构总览:一次提问的完整旅程

用户在聊天框输入问题后,请求按如下链路流转(流程图引自 docs/architecture/chat.md,并对照 聊天路由实现 做了细化):

User types question | v POST /api/repos/{repo_id}/chat/messages | v +------ Chat Router (SSE stream) ------+ | | | 1. Create/load conversation | | 2. Save user message to DB | | 3. Build LLM message history | | 4. Call provider.stream_chat() <----+---- tool_executor callback | | | | v | | 5. Stream text_delta events -------> SSE to browser | 6. On tool_start: | | - Execute tool (or provider | | executes internally) | | - Emit tool_result event -------> SSE to browser | 7. If tool calls found: | | - Append to history, loop to 4 | | 8. If no tool calls: | | - Save assistant message to DB | | - Emit done event | +---------------------------------------+

一个关键设计决策是:Agentic Loop 默认跑在 Chat Router 中(OpenAI、Anthropic、Ollama、LiteLLM 皆是如此),只有 Gemini 例外——它的循环在stream_chat()内部执行。原因在于 Gemini 的 API 要求在回放对话历史中的函数调用时携带thought_signature签名,而通过 Router 做 OpenAI 格式的往返转换会丢失该签名,因此 Router 向 Gemini 传入一个tool_executor回调,由 Gemini 在内部循环中直接调用。这一点在第 10 节会详细展开。

2. 数据库 Schema:会话与消息的持久化

两张表由迁移0005_chat_conversations.py引入,迁移文件位于 packages/core/alembic/versions/0005_chat_conversations.py,其中upgrade()与文档描述完全对应。

conversations表

ColumnTypeNotes
idString(32)PKUUID hex
repository_idString(32)FKrepositories.id,ondelete="CASCADE"
titleText自动生成;新会话默认"New conversation",首轮回答后由前 6 个词提炼
created_atDateTime(tz)server_default=sa.func.now()
updated_atDateTime(tz)收到新消息时自动更新

索引:ix_conversations_repo_updated,建立在(repository_id, updated_at)上——按仓库列出会话并按更新时间排序是主要查询路径。

chat_messages表

ColumnTypeNotes
idString(32)PKUUID hex
conversation_idString(32)FKconversations.id,ondelete="CASCADE"
roleString(32)user或assistant
content_jsonTextJSON 负载(见下)
created_atDateTime(tz)

索引:ix_chat_messages_conv_created,建立在(conversation_id, created_at)上。

消息内容格式

用户消息:

{"text": "What does the auth module do?"}

助手消息:除了正文文本,还会把本轮所有工具调用及其结果作为持久化负载一起存储(tool_calls数组),这样历史会话回放时能完整还原当时的工具证据:

{ "text": "The auth module handles...", "tool_calls": [ { "id": "call_abc123", "name": "get_context", "arguments": {"targets": ["src/auth"]}, "result": { ... } } ] }

从源码看,存储的tool_calls条目实际结构为{id, name, arguments, summary, artifact},即还包含工具结果摘要与 Artifact 信封(见 routers/chat.py 中_stored_tool_call),Artifact 数据随消息持久化,历史会话无需重新调用工具即可回放结果面板。

3. ChatProvider 协议:可选接入的流式聊天能力

协议定义在 packages/core/src/repowise/core/providers/llm/base.py。设计要点:既有的BaseProvider.generate()完全不动,新增一个基于typing.Protocol+@runtime_checkable的ChatProvider协议类,把「流式聊天 + 工具调用」作为 Provider 的可选能力——实现stream_chat()即视为支持。

@runtime_checkable class ChatProvider(Protocol): def stream_chat( self, messages: list[dict], # OpenAI-format message list tools: list[dict], # OpenAI-format tool definitions system_prompt: str, max_tokens: int = 8192, temperature: float = 0.7, request_id: str | None = None, tool_executor: Any | None = None, # async callable(name, args) -> dict ) -> AsyncIterator[ChatStreamEvent]: ...

配套数据类:

  • ChatToolCall(id, name, arguments)— LLM 想执行的工具调用;
  • ChatStreamEvent(type, text?, tool_call?, tool_result_data?, stop_reason?, input_tokens, output_tokens)— 流中的一个事件。

事件类型:

type填充字段含义
text_deltatext增量文本 token
tool_starttool_callLLM 请求调用某个工具
tool_resulttool_call,tool_result_data工具已执行(由 Provider 内部执行时)
usageinput_tokens,output_tokenstoken 用量更新
stopstop_reason生成结束(end_turn/tool_use/max_tokens)

消息统一使用OpenAI 格式的 dict 列表作为协议输入,各 Provider 在stream_chat()内部自行转换为自家原生格式。stop_reason采用 Provider 无关的中性取值,base.py中的normalize_stop_reason()会把各厂商枚举(stop、length、tool_calls、function_call等)归一化为end_turn、max_tokens、tool_use,未知值原样保留以便诊断。

现有实现:Anthropic、OpenAI、Gemini、Ollama、LiteLLM。它们都接受tool_executor参数,但只有 Gemini 真正使用它(用于thought_signature处理,见第 10 节)。

4. Tool Registry:聊天工具的唯一事实来源

定义在 packages/server/src/repowise/server/chat_tools.py。它是聊天工具 schema 与执行的单一来源:从 MCP 注册表投影出「请求作用域」的工具面,向 LLM 暴露 OpenAI 格式的 function 定义。

核心函数:

函数用途
get_tool_catalog(repo_path)返回该仓库配置的 MCP 工具面(含生成的 schema)
get_tool_schemas_for_llm()转成 OpenAI 格式的 tool definitions 交给 LLM
execute_tool(name, args)仅当工具属于该仓库配置面时执行,并保证输出可 JSON 序列化
get_artifact_type(name)/get_artifact_presentation(name)/get_artifact_evidence_basis(name)把工具名映射为前端 Artifact 类型 / 呈现方式 / 证据依据
init_tool_state(...)把 FastAPI app state 桥接到 MCP 模块全局(session 工厂、FTS、向量库、决策存储、repo 路径)

实现细节值得注意:

  • 工具面由仓库配置决定:get_tool_catalog调用selected_tool_entries(repo_path),即工具集来自仓库的 MCP 配置,而非写死的列表;
  • 双重安全约束:execute_entry遵循ToolEntry.safety合约——mutating工具在未显式确认时直接返回confirmation_required错误;执行失败不会抛异常中断流,而是返回{"error": ..., "error_code": "tool_failed"};
  • 工作区别名兜底:_scope_repo_arg会拦截模型擅自填写的repo参数,用当前工作区别名覆盖;
  • 确定性序列化:_make_json_serializable递归把 dict/list/dataclass 转成纯 JSON 类型,避免 SSE 序列化失败。

7 个聊天工具与 Artifact 类型映射

聊天 Agent 只暴露精选子集,不包含完整的 MCP 默认面(没有get_answer、get_symbol、get_health、list_repos):

ToolArtifact Type
get_overviewoverview
get_contextwiki_page
get_riskrisk_report
get_change_riskrisk_report
get_whydecisions
search_codebasesearch_results
get_dead_codedead_code

前端 ArtifactPanel 依据artifact.type决定渲染方式(Markdown、Mermaid 图、搜索结果、原始 JSON)。

5. Provider 配置:API Key 与活动 Provider 的解析链

实现在 packages/server/src/repowise/server/provider_config.py。API Key 与活动 Provider/模型选择存储在服务端provider_config.json中(路径为$REPOWISE_CONFIG_DIR/provider_config.json,未设置时位于~/.repowise/provider_config.json),环境变量优先于存储的 Key。文件采用「先写临时文件再os.replace原子替换」的方式落盘,并尽力设置0600权限保护 Key 材料,日志输出会用_redact_key对sk-前缀的 Key 打码。

API Key 解析顺序(源码为三层,比文档更细)

  1. 进程环境变量(如GEMINI_API_KEY、ANTHROPIC_API_KEY、OPENAI_API_KEY);
  2. 目标仓库的.repowise/.env——以 dict 形式读取,不会注入os.environ,因此工作区模式下某个仓库的 Key 不会泄漏到另一个仓库;
  3. 服务端存储的 Key(provider_config.json中keys段,通过set_api_key写入)。

另外,UI 里新增的 Key 会被_mirror_key_to_repo_env同步镜像到该仓库的.repowise/.env(以 Provider 目录中的第一个规范环境变量名为准),这样之后在该仓库跑 CLI 也能读到同一把 Key——CLI 只读.env不读服务端存储。

活动 Provider 解析顺序(源码为五层,覆盖文档的两步)

  1. 每仓库持久化的 UI 选择(provider_config.json的repos[repo_id])——这是用户对该仓库的显式覆盖,按仓库隔离,避免「为一个仓库选模型却影响其他仓库」的历史 bug;
  2. 仓库自身config.yaml(provider+model,由repowise init写入)——无缝默认;
  3. 服务端全局active_provider(PATCH /api/providers/active写入的旧式单仓库状态);
  4. REPOWISE_PROVIDER/REPOWISE_MODEL环境变量;
  5. 自动探测——遍历目录,取第一个有可用 Key 或无需 Key 的 Provider。

Provider 目录

文档第 5 节列出的核心目录为 Gemini、Anthropic、OpenAI、Ollama(本地、无需 Key)、LiteLLM;对照当前源码中的PROVIDER_CATALOG,实际还包含 OpenRouter、DeepSeek、Kimi、Eden AI、Claude Code(本地 CLI)、Codex(本地 CLI)、OpenCode(本地 CLI)等。每个条目声明default_model、可选的models列表、env_keys与requires_key。示例(源码原文):

{ "id": "gemini", "name": "Google Gemini", "default_model": "gemini-3.5-flash-lite", "models": ["gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview"], "env_keys": ["GEMINI_API_KEY", "GOOGLE_API_KEY"], "requires_key": True, },

base_url同样有三层解析:进程环境变量(名目来自PROVIDER_BASE_URL_ENVS,与 CLI/MCP 解析共用同一张表,保证不漂移)→ 仓库.env→ 仓库config.yaml中按 Provider 分段的openai: {base_url: http://localhost:4000/v1}这类写法。这使得本地 LiteLLM / OpenAI 兼容端点可以在init时配置并被聊天复用。

6. SSE 流式协议:浏览器实时看到生成过程

聊天端点返回Content-Type: text/event-stream。每个事件格式:

event: data data: {"type": "...", ...}

事件形状(逐条解析):

// LLM 的增量文本 {"type": "text_delta", "text": "The auth module..."} // LLM 要调用工具 {"type": "tool_start", "tool_id": "call_123", "tool_name": "get_context", "input": {"targets": ["src/auth"]}} // 工具执行完成 {"type": "tool_result", "tool_id": "call_123", "tool_name": "get_context", "summary": "Context for 1 target(s)", "artifact": {"type": "wiki_page", "data": {...}}} // 流结束 {"type": "done", "conversation_id": "abc123", "message_id": "def456"} // 错误 {"type": "error", "message": "Provider error: ..."}

Headers:Cache-Control: no-cache, no-transform、X-Accel-Buffering: no、Connection: keep-alive。其中no-transform是为了防止 Next.js rewrite 代理的压缩中间件对流做 gzip 缓冲(与 jobs 流同一处理策略)。

Retry:流开始时发送retry: 3000。

终端事件纪律:每条流必须以done或error在data通道上收尾。前端useChat只按type字段分派事件,因此发送到其他通道、或缺少type的事件会被丢弃——客户端只会看到「回答到一半流停了」。客户端在 reader 结束时若未收到终端事件,会自行结算本地状态(把进行中的工具标为错误态),但服务端仍然欠它一个终端事件。源码中该格式由_sse_event(event, data)统一生成(见 routers/chat.py)。

此外,源码还会在特定场景发出文档未列出的补充事件:

  • {"type": "suggestions", "suggestions": [...]}—— 回答完成、且本轮有可跟进问题时,附带建议追问;
  • {"type": "truncated", "loops": 10}—— 10 轮循环全部以工具调用结束、始终未产出最终答案时发出。

7. Agentic Loop:最多 10 轮的模型-工具交替循环

循环每请求最多运行10 次迭代(源码常量_MAX_AGENTIC_LOOPS = 10,见 routers/chat.py):

for each iteration: 1. Call provider.stream_chat(messages, tools, system_prompt, tool_executor) 2. Collect text_delta events -> stream to client 3. Collect tool_start events -> stream to client 4. Collect tool_result events (from internal execution) -> stream to client 5. If there are pending tool calls (not internally executed): a. Execute each tool b. Emit tool_result to client c. Append assistant + tool results to message history d. Continue loop 6. If no tool calls: break

循环结束后,助手消息(文本 + 全部工具调用及其结果)保存到数据库,并发出done事件。

对照源码_AgentTurn的实现,有几个值得说明的细节:

  • 两阶段工具执行模型:_model_turn收集tool_start事件进入pending列表;若 Provider 已内部执行(tool_result事件),则从pending中移除对应项——这正是 Gemini 内部循环与 Router 循环的协作点;
  • 客户端断连即中止:每轮都检查request.is_disconnected(),断连置aborted并停止,不保存任何内容(源码注释明确:客户端断连或 Provider 失败时不落库,避免半截回答污染历史);
  • grounding 预读:在模型第一轮之前,Router 会基于页面上下文(ChatPageContext)用plan_grounding+run_grounding做一次「该页面已获授权的读取」(例如打开 Wiki 页先拉一次get_context),并把结果注入消息历史、以origin="grounding"标记存储,重复的调用会被历史去重跳过;
  • ProviderError 即 error 事件:模型调用抛出ProviderError时置aborted并立即yield一个errorSSE 事件;
  • 截断标记:10 轮全是工具调用时置truncated=True,回复内容中带truncated标记并发出truncated事件。

历史构建方面,_db_messages_to_llm_format把数据库消息转成 LLM 格式,_with_navigation_context再注入页面导航上下文;工具结果以tool_result_message回填历史,助手侧用assistant_tool_call_message记录本轮文本与工具调用。

8. REST API 端点

Chat 端点

MethodPathDescription
POST/api/repos/{repo_id}/chat/messagesSSE 流——发送消息并获取流式响应
GET/api/repos/{repo_id}/chat/conversations列出仓库的会话
GET/api/repos/{repo_id}/chat/conversations/{id}获取会话及其全部消息
DELETE/api/repos/{repo_id}/chat/conversations/{id}删除会话

POST body:

{ "message": "What does the auth module do?", "conversation_id": null, "provider": null, "model": null }
  • conversation_id— 省略或null表示开启新会话;
  • provider/model— 可选的单请求覆盖(即 UI 的模型选择器),仅对本次请求生效;不带覆盖时按第 5 节的仓库级解析链确定 Provider 与模型。

从源码看,POST还接受一个context字段(kind/label/target/target_kind),用于把页面上下文传给 grounding 与系统提示词;且所有聊天端点都受verify_api_key依赖保护(Router 级依赖)。

补充端点(源码中的完整面,超出文档表格)

源码中的聊天路由还提供了会话生命周期管理的更多端点,均带verify_api_key依赖:

  • GET /api/repos/{repo_id}/chat/suggestions?kind=...&target=...— 基于页面已获授权读取生成建议问题,不产生模型调用,与主聊天共用同一套 grounding 函数,保证两处对页面类型的映射永不漂移;
  • POST /api/repos/{repo_id}/chat/conversations/{id}/restore— 恢复(软删除的)会话;
  • PATCH /api/repos/{repo_id}/chat/conversations/{id}— 更新标题或固定(pinned)状态,标题不可为空;
  • POST /api/repos/{repo_id}/chat/conversations/{id}/fork— 从某个消息节点派生新会话(through_message_id与before_message_id二选一);
  • GET /api/repos/{repo_id}/chat/conversations/{id}/artifacts/{artifact_id}与PATCH .../artifacts/{artifact_id}— 读取历史消息中持久化的 Artifact、切换其固定状态。

Providers 端点

MethodPathDescription
GET/api/providers列出全部 Provider 及其状态与活动选择
PATCH/api/providers/active设置活动 Provider 与模型
POST/api/providers/{id}/key存储 API Key
DELETE/api/providers/{id}/key移除 API Key

9. 前端架构

API 层

  • packages/api-client/src/chat.ts—listConversations、getConversation、deleteConversation、getChatSuggestions、restoreConversation、updateConversation、forkConversation、getConversationArtifact、setConversationArtifactPinned;核心的postChatMessage返回原始Response(而非解析后的 JSON),供调用方读取response.body作为ReadableStream逐帧消费 SSE。它还会透传AbortSignal:不仅中止读循环,连 POST 请求本身一起中止,否则服务端 Agentic Loop 会继续空转、DB 会话直到 socket 坍塌才被回收;
  • Provider 管理封装(getProviders/setActiveProvider/addProviderKey/removeProviderKey)同样在 api-client 层。

Hooks

  • useChat(repoId)(实现见 packages/web/src/lib/hooks/use-chat.ts)—— 完整的聊天状态机。使用fetch+ReadableStream读取(而非EventSource,后者仅支持 GET)。管理消息列表、流式状态、会话 ID、错误处理与中止控制。实现上的细节:用AbortController在中止/切换仓库/组件卸载时打断流;文本增量通过requestAnimationFrame批处理(queueText/flushText),避免高频 text_delta 触发过量 React 重渲染;流异常终止时用stopRunningTools把进行中的工具调用标记为错误态并附上说明。对外暴露sendMessage、loadConversation、reset;
  • useProviders()—— SWR 包装的 Provider 管理,暴露providers、activeProvider、activeModel、activate、saveKey、removeKey。

组件(packages/ui/src/chat/与packages/web/src/components/chat/)

组件用途
ChatInterface主容器——空态(问候语 + 建议问题 + 模型选择器)与活跃态(消息列表 + 输入框)
ChatMessage渲染用户气泡或助手消息(工具块 + Markdown)
ChatMarkdown客户端 Markdown 渲染,react-markdown+remark-gfm,使用设计 token 样式
ToolCallBlock/ToolCallGroup内联工具调用可视化——运行中(spinner)、已完成折叠(勾选 + 摘要)、已完成展开(输入/输出 JSON)
ArtifactPanel右侧滑入面板,多 Artifact 分 Tab;按类型渲染:Markdown、Mermaid 图、搜索结果、原始 JSON
ModelSelector紧凑 popover,切换 Provider/模型并内联添加 API Key
ConversationHistory下拉列出历史会话,支持删除、新建、恢复、fork、固定等操作

页面结构

仓库落地页/repos/[id]就是聊天界面:紧凑头部(仓库名 + commit 徽标 + 分支徽标)、ChatInterface占满剩余视口高度、侧边栏导航项由 "Overview" 改为 "Chat"。仓库的其他子页(graph、wiki、coverage 等)保持不变。

10. Provider 专属说明

Anthropic

使用client.messages.stream(),原生 Anthropic 消息格式。OpenAI 格式消息需转换:工具结果转为user角色的tool_resultcontent block,工具调用转为tool_usecontent block。Agentic Loop 跑在 Chat Router。

OpenAI

使用client.chat.completions.create(stream=True)。原生 OpenAI 格式,几乎无需转换。工具调用片段在流 chunk 中累积,完整后一次性以tool_start事件发出。Agentic Loop 跑在 Chat Router。

Gemini

使用client.models.generate_content()(非流式,在线程池中执行,见 packages/core/src/repowise/core/providers/llm/gemini.py)。Agentic Loop 在stream_chat()内部运行:Gemini 的 API 在回放对话历史中的 function call 时必须携带thought_signature,若经 Router 做 OpenAI 格式往返会丢失该签名,因此内部循环全程使用原生Content对象,通过tool_executor回调执行工具并产出tool_start/tool_result事件。tool_executor参数对 Gemini 是必需的:缺失时它会yield一个stop让调用方接管循环,但注释明确这会在下一次往返时因thought_signature缺失而失败。

Ollama

使用 OpenAI 兼容端点(localhost:11434/v1)经AsyncOpenAI调用,流式模式与 OpenAI 一致,无需 API Key。Agentic Loop 跑在 Chat Router。

LiteLLM

使用litellm.acompletion(stream=True),OpenAI 兼容流式输出。Agentic Loop 跑在 Chat Router。

附注:推理类模型的温度参数兼容

值得补充的是,流式聊天与生成共用 Provider 层的温度兼容策略(见 base.py):推理时代的部分模型(如gpt-5、o1、o3、o4系列,Anthropic Opus/Sonnet 5 系列等)会拒绝显式temperature并返回 400。temperature_kwargs()对已知前缀直接跳过该参数,is_temperature_rejection()/remember_temperature_rejection()则在运行时从第一次拒绝中「学习」新模型并加入进程内缓存,避免每次多付一次失败的调用。这是跨生成与聊天两条路径的共享底层逻辑。

结语与进一步阅读

Repowise Codebase Chat 的核心价值在于「把 MCP 工具面安全地投影给一个带流式的 Agent 循环」:仓库级工具配置决定能力边界,ChatProvider协议让不同厂商的流式/工具语义归一化,SSE 协议与前端useChat状态机保证「生成过程可见、结果可回放」。若想深入源码,建议按以下顺序阅读:

  • 聊天路由与 Agentic Loop:packages/server/src/repowise/server/routers/chat.py
  • 工具注册表与执行合约:packages/server/src/repowise/server/chat_tools.py
  • Provider 协议与事件类型:packages/core/src/repowise/core/providers/llm/base.py
  • Provider 配置解析链:packages/server/src/repowise/server/provider_config.py
  • 数据库迁移:packages/core/alembic/versions/0005_chat_conversations.py
  • 前端 API 层:packages/api-client/src/chat.ts、packages/web/src/lib/hooks/use-chat.ts
  • 架构级说明:docs/architecture/chat.md、docs/architecture/ARCHITECTURE.md

【免费下载链接】repowise

Codebase intelligence for AI and humans: code health scores, auto-generated docs, git analytics, dead code detection, and architectural decisions via MCP.

项目地址:https://gitcode.com/gh_mirrors/re/repowise
点击查看免费下载

相关推荐

上一篇:【免费下载】 探索Windows驱动存储库的利器:Driver Store Explorer(RAPR)
下一篇:终极指南:如何使用Go-libp2p构建去中心化网络应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 1:16:44

题解:洛谷 AT_abc439_a [ABC439A] 2^n - 2*n

本文分享的必刷题目是从蓝桥云课、洛谷、AcWing等知名刷题平台精心挑选而来,并结合各平台提供的算法标签和难度等级进行了系统分类。题目涵盖了从基础到进阶的多种算法和数据结构,旨在为不同阶段的编程学习者提供一条清晰、平稳的学习提升路径。 欢迎大家订阅我的专栏:算法…

作者头像 李华
网站建设 2026/10/9 1:15:10

MPP中control命令不是开关,而是编码器实时调控神经中枢

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 1:13:35

P2P局域网即时通信系统实现:无服务器架构的节点发现与消息收发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 1:13:30

用好王协瑞版计算机网络PPT:课件重组、实验激活与教学避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/9 1:12:44

硬件级时间同步:VIO系统稳定性的物理基石

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华