阅读视图

发现新文章,点击刷新页面。

独立开发者主流技术栈(2026最新)

一般情况独立开发者的技术栈核心追求:全栈统一、开发高效、部署简单、成本极低、生态完善,以下按Web、移动端、桌面端、数据库、运维/工具、AI辅助六大维度,整理当前最主流、最实用的技术选型(含热门组合与单类选项)。

一、Web全栈(最主流,SaaS/工具/网站首选)

1. 前端(React系,2026绝对主流)

  • 核心框架Next.js 15(全栈React,App Router+Server Components,一人搞定前后端)
  • 备选框架:Nuxt 3(Vue全栈,上手快)、Remix、SvelteKit
  • 样式方案Tailwind CSS + shadcn/ui(无样式组件+自由定制,开发最快)
  • 备选UI:Ant Design、Material Design、DaisyUI、Chakra UI
  • 状态管理:Zustand、Jotai、Redux Toolkit、Pinia(Vue)
  • 数据请求:TanStack Query(React Query)、SWR、Axios
  • 表单/验证:React Hook Form + Zod、Formik
  • 语言TypeScript(必选,类型安全,减少bug)

2. 后端(全栈JS/Python为主,轻量优先)

  • Node.js生态(最主流)
    • 框架:Express、NestJS(企业级)、Hono(轻量Edge)
    • 全栈:Next.js API Routes/Edge Functions(无需单独后端)
  • Python生态(AI/数据/快速原型)
    • 框架:FastAPI(高性能API)、Flask(极简)、Django(全功能)
  • 备选:Go(Gin,高性能)、Rust(Axum,安全高效)
  • ORMPrisma(全数据库支持,生态最好)、Drizzle(轻量Serverless)

二、移动端(跨平台优先,减少学习成本)

  • 跨平台首选Flutter(Dart,性能接近原生,一套代码双端)
  • Web开发者首选React Native(React语法,复用Web技能)
  • 轻量/小程序转App:UniApp(Vue语法,支持多端+小程序)、Taro
  • 原生(性能极致):Android(Kotlin+Jetpack Compose)、iOS(Swift+SwiftUI)

三、桌面端(跨平台,Web技术复用)

  • 主流Electron(React/Vue+Node,成熟稳定,如VS Code)
  • 新锐轻量Tauri(Rust后端,体积小、性能优)
  • 备选:Qt(C++,跨平台原生)、WPF(Windows原生)

四、数据库(免费+托管优先,减少运维)

1. 关系型(主流)

  • 托管首选Supabase(PostgreSQL,免费500MB,自带认证/存储/实时)
  • 备选托管:Neon、PlanetScale(Serverless MySQL)、Turso(SQLite)
  • 自建:PostgreSQL、MySQL(经典稳定)

2. 非关系型

  • 文档型:MongoDB(托管MongoDB Atlas)
  • 缓存/实时:Redis(托管Upstash)
  • 向量数据库(AI):Milvus、Pinecone、Chroma

五、运维/部署/工具(零成本+自动化)

  • 部署(免费额度足)
    • Web:Vercel(Next.js最佳搭档,一键部署)、Cloudflare Pages
    • Serverless:Cloudflare Workers(免费10万次/天)、Vercel Edge Functions
  • 认证:Supabase Auth、NextAuth.js、Better Auth、Clerk
  • 支付(SaaS必备):Stripe(全球)、PayPal、微信/支付宝(国内)
  • 邮件:Resend(免费3000封/月)、Nodemailer
  • 存储:Cloudflare R2、AWS S3、Supabase Storage
  • 监控/分析:Sentry(错误)、Posthog、Umami、Plausible(用户分析)
  • CI/CD:GitHub Actions(免费)
  • 开发工具:VS Code、Git、Figma(设计)、Postman(API测试)

六、AI辅助(2026必备,效率翻倍)

  • 代码生成:GitHub Copilot、Cursor、Claude Code、Vercel v0(前端UI)
  • AI工具链:LangChain、LlamaIndex(大模型应用)、OpenAI/Anthropic API
  • 设计/素材:Midjourney、DALL·E 3(图片)、Runway(视频)

七、2026独立开发者「黄金技术栈组合」(直接抄作业)

  1. SaaS/Web应用(最强) Next.js 15 + TypeScript + Tailwind + shadcn/ui + Zustand + Supabase + Vercel + Stripe
  2. Vue生态(易上手) Nuxt 3 + Tailwind + Supabase + Prisma + Pinia + Vercel
  3. AI应用 Next.js + FastAPI(Python) + Supabase + Pinecone(向量) + OpenAI API
  4. 移动App Flutter + Supabase + Riverpod(状态)

八、选型核心原则(独立开发必看)

  1. 全栈统一:优先JS/TS(前后端同语言),减少切换成本
  2. 托管优先:不用自建服务器,用Supabase/Vercel等BaaS,零运维
  3. 免费起步:所有工具选有 generous 免费额度的,验证PMF再付费
  4. 生态成熟:选文档全、社区大、坑少的技术,独立开发没时间踩坑
  5. AI赋能:全程用AI工具,代码/设计/文案全流程提效

iOS应用上架全流程:从证书申请到发布避坑指南

iOS上架全流程避坑指南速存!

作为一名独立开发者,今天来和大家分享一下将「楼里」这款应用从iOS打包到上架的全流程。iOS打包到上架,对个人开发者来说就像“九九八十一难”,但只要一步步来,也能顺利完成。下面,我会毫无保留地分享每一个关键步骤。

证书申请篇

  1. 准备一台Mac电脑:这是前提条件,没有Mac的同学可能需要借力或者购买云服务。

  2. 申请苹果开发者账号:费用为688元/年,这是开启iOS开发大门的钥匙。

  3. 证书生成:苹果官方提供了Certificates、Identifiers、Profiles的申请流程,建议自己本地生成.p12私钥证书,这样更安全也更方便后续操作。

此外,使用工具如AppUploader可以在Windows、Linux或Mac系统中直接申请iOS证书,无需依赖Mac电脑,简化证书管理流程。

ICP备案篇

  1. 准备服务器:有免费和付费的选择,根据自己的需求来。

  2. 申请域名:域名价格差异大,好的域名更贵,建议提前规划。

  3. 备案流程:选择App备案,分为初审(平台审)和终审(管局审),正常情况下7天内可以通过。全国互联网安全管理服务平台是备案的重要一环,特别是产品功能基本开发完毕后,审核人员会仔细查看产品。如果App/小程序,还需要线下面签;如果是网站,则可以线上完成。

App打包篇

  1. 使用UniApp开发,打包工具为HBuilderX,这可以大大提高开发效率。

  2. App图标处理:去掉Alpha通道,确保图标显示正常。

  3. 启动界面:创建自定义的storyboard作为启动界面,提升用户体验。

  4. 广告标识:去掉使用广告标识(IDFA)的勾选,保护用户隐私。

  5. 云打包:使用申请的证书文件进行云打包,用回复的链接下载iOS安装包。

发布上架篇

  1. 下载Transporter工具:这是苹果官方提供的安装包交付工具,确保安装包能够顺利提交。

或者使用AppUploader工具上传IPA文件到App Store,支持在Windows、Mac或Linux系统上操作,无需Mac电脑,比Transporter更高效,且能批量上传应用截图和管理描述信息。

  1. 资料准备:准备产品的10张截图、推广文本、描述、关键词等资料,这些都是审核的重要依据。

  2. 图标与截图:App图标大小为1024x1024,直角边,确保在各设备上显示效果最佳。

  3. 隐私政策与技术支持:提供隐私政策网址 (URL) 和技术支持网址 (URL),可以用github或Notion搭建静态网站,方便用户查看。

  4. App供应情况:按情况填需要上架的地区,确保应用能够在目标市场上线。

整个流程下来,虽然复杂,但只要一步步来,每一个细节都处理到位,就能够成功将应用上架到iOS平台。希望今天的分享能够对大家有所帮助。

作为前端,如果使用 Langgraph 实现第一个 Agent

大家好 👋,我是 Moment,目前正在使用 Next.js、NestJS、LangChain 开发 DocFlow。这是一个面向 AI 场景的协同文档平台,集成了基于 Tiptap 的富文本编辑、NestJS 后端服务、实时协作与智能化工作流等核心模块。

在这个项目的持续打磨过程中,我积累了不少实战经验,不只是 Tiptap 的深度定制、编辑器性能优化和协同方案设计,也包括前端工程化建设、React 源码理解以及复杂项目架构实践。

如果你对 AI 全栈开发、Agent、长期记忆、文档编辑器、前端工程化或者 React 源码相关内容感兴趣,欢迎添加我的微信 yunmz777 一起交流。觉得项目还不错的话,也欢迎给 DocFlow 点个 star ⭐

从这一篇开始,用一个简化版计算器 Agent 走一遍 LangGraph 的核心要素。目标很具体:只用节点、边、状态这三个概念,从零定义一张最小的图、让它真正跑起来,在代码里看清楚状态如何在节点之间流转。持久化、本地服务、子图等进阶内容都留到后面,这一章先让你对图式编排有可运行的手感。

用计算器 Agent 认识图

例子的场景是:用户用自然语言描述算式,比如"请帮我把三加四再乘二",模型理解后决定是否调工具,工具负责加减乘除等具体运算,结果回到模型整理成一句友好的回复。

这个场景不复杂,但很好地覆盖了图的三个关键能力:节点之间如何传递状态、条件边如何根据状态决定下一跳、工具节点执行完后如何回到模型节点继续推理。用图来表示整体执行流程,如下图所示。

20260317080940

模型节点与工具节点之间的回环,就是 LangGraphLangChain 线性链最本质的区别。下面按这个结构一步步把代码写出来。

准备模型与工具

图要跑起来,先得有一个支持工具调用的聊天模型,再配几个简单的计算工具。这部分仍然由 LangChain 提供,LangGraph 暂时不登场。

模型初始化时加了 temperature: 0,是为了让模型在判断"该不该调工具、该调哪个工具"这类结构化决策时输出更稳定,减少随机性带来的不必要波动。三个计算工具 addmultiplydividetool 函数定义,schemazod 写,这样模型拿到工具描述后能清楚知道每个参数的类型。最后调 model.bindTools(tools) 把工具列表注入模型,之后每次调用这个模型时,它就知道手边有哪些工具可用。

import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import * as z from "zod";

const model = new ChatOpenAI({
  model: "deepseek-chat",
  apiKey: "sk-60816d9be57f4189b658f1eaee52382e",
  configuration: { baseURL: "https://api.deepseek.com" },
  temperature: 0,
});

const add = tool(({ a, b }) => a + b, {
  name: "add",
  description: "Add two numbers",
  schema: z.object({
    a: z.number().describe("First number"),
    b: z.number().describe("Second number"),
  }),
});

const multiply = tool(({ a, b }) => a * b, {
  name: "multiply",
  description: "Multiply two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const divide = tool(({ a, b }) => a / b, {
  name: "divide",
  description: "Divide two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const toolsByName = {
  [add.name]: add,
  [multiply.name]: multiply,
  [divide.name]: divide,
};

const tools = Object.values(toolsByName);
const modelWithTools = model.bindTools(tools);

到这里模型和工具都准备好了,接下来才是 LangGraph 登场的地方。

定义图的状态

任何一张 LangGraph 图都需要一个状态模式,用来描述在节点之间流转的是哪些数据。状态不是普通对象,每个节点不是整体替换状态,而是只返回需要更新的字段,LangGraph 按字段的 reducer 把更新合并进去。

对于对话类应用,最常用的状态定义是 MessagesAnnotation,它内置了消息列表的 reducer 逻辑。节点每次返回 { messages: [newMessage] },状态系统就自动把这条消息追加到已有列表里,不需要手动维护整个消息数组。

import {
  StateGraph,
  MessagesAnnotation,
  START,
  END,
} from "@langchain/langgraph";

后面定义节点和组装图时都会用到 MessagesAnnotation,它既是状态模式的定义,也给 TypeScript 提供了节点函数参数的类型推断,写 state: typeof MessagesAnnotation.State 就能拿到完整的类型提示。

两个核心节点

这张图里只有两个真正干活的节点。llmCall 负责调用模型,根据当前 messages 生成一条新消息,并判断要不要请求工具。toolNode 根据上一轮模型的工具调用请求执行工具,把结果封装成 ToolMessage 返回。

节点函数可以只接收 state。如果需要流式或回调,可以声明第二个参数 config?: RunnableConfig,图运行时会自动传入。这样 invokestreamEvents 的回调就能一路传到模型和工具里,流式输出才能正常触发。

先写模型节点。它把系统提示和已有消息一起发给模型,只返回本次新生成的那条消息,状态系统负责追加。

import { SystemMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";

const llmCall = async (
  state: typeof MessagesAnnotation.State,
  config?: RunnableConfig
) => {
  const response = await modelWithTools.invoke(
    [
      new SystemMessage(
        "你是一个负责做算术的助手,根据用户描述执行加减乘除等运算,需要时调用工具得到结果后再用自然语言回复。"
      ),
      ...state.messages,
    ],
    config
  );
  return { messages: [response] };
};

再写工具节点。逻辑分三步:拿到最后一条 AIMessage,根据里面的 tool_calls 逐个执行对应工具,把每个工具的返回值包成 ToolMessage 追加到状态里。tool_call_id 是关键,模型后续要靠它把工具结果和当初的请求对应起来。

import { AIMessage, ToolMessage } from "@langchain/core/messages";

const toolNode = async (
  state: typeof MessagesAnnotation.State,
  config?: RunnableConfig
) => {
  const lastMessage = state.messages.at(-1);
  if (!lastMessage || !AIMessage.isInstance(lastMessage)) {
    return { messages: [] };
  }

  const results: ToolMessage[] = [];
  for (const toolCall of lastMessage.tool_calls ?? []) {
    const t = toolsByName[toolCall.name];
    if (!t) continue;
    const value = await t.invoke(toolCall.args ?? {}, config);
    results.push(
      new ToolMessage({
        content: String(value),
        tool_call_id: toolCall.id ?? "",
      })
    );
  }
  return { messages: results };
};

如果最后一条不是 AIMessage,或者 AIMessage 里没有工具调用,直接返回空列表,图会照常往下走,不会卡住。

条件边与路由

节点准备好之后,还要告诉图跑完某个节点之后下一步去哪。这里的逻辑很清楚:模型回来的消息里如果带着 tool_calls,说明它想用工具,就走到 toolNode;如果没有 tool_calls,说明模型已经可以直接给用户回复了,图结束。

const shouldContinue = (state: typeof MessagesAnnotation.State) => {
  const lastMessage = state.messages.at(-1);
  if (!lastMessage || !AIMessage.isInstance(lastMessage)) return END;
  if (lastMessage.tool_calls?.length) return "toolNode";
  return END;
};

这个函数返回的是字符串(节点名)或 ENDLangGraph 拿到返回值后就知道下一步跳到哪个节点。条件边是 LangGraph 表达"分支逻辑"的核心机制,比把 if/else 藏在节点函数里要清晰得多,图的结构一眼就能看懂。

组装并运行整张图

把状态、节点和边用 StateGraph 链式调用串在一起,最后调 compile() 得到可执行的图。addConditionalEdges 的第三个参数是允许到达的节点列表,LangGraph 会在编译时验证条件边函数的返回值不会跳到意外的节点,起到一定的安全检查作用。

import { HumanMessage } from "@langchain/core/messages";

const agent = new StateGraph(MessagesAnnotation)
  .addNode("llmCall", llmCall)
  .addNode("toolNode", toolNode)
  .addEdge(START, "llmCall")
  .addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
  .addEdge("toolNode", "llmCall")
  .compile();

const result = await agent.invoke({
  messages: [new HumanMessage("请帮我算一下 3 加 4 等于多少。")],
});

for (const message of result.messages) {
  const content = typeof message.content === "string" ? message.content : "";
  console.log(message.getType(), content);
}

以"请帮我算一下 3 加 4 等于多少"为例,图的完整执行路径如下:

  1. START 进入 llmCall,模型判断需要调 add 工具,返回带 tool_callsAIMessage
  2. shouldContinue 检测到有工具调用,走到 toolNode
  3. toolNode 执行 add(3, 4) 得到 7,包成 ToolMessage 追加到状态,沿固定边回到 llmCall
  4. 模型拿到工具结果,生成"3 加 4 等于 7"这样的自然语言回复,这次没有工具调用,shouldContinue 返回 END,图结束

走完这四步,messages 列表里依次记录了用户消息、模型的工具请求、工具的执行结果、模型的最终回复,完整还原了整条推理过程。

流式输出

invoke 是一次性拿到全部结果,适合脚本和批处理。如果要做"边生成边显示"的体验,用 agent.streamEvents 按事件消费。

import { ChatOpenAI } from "@langchain/openai";
import { tool, type StructuredTool } from "@langchain/core/tools";
import { SystemMessage, AIMessage, ToolMessage, HumanMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";
import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
import * as z from "zod";

// 模型
const model = new ChatOpenAI({
  model: "deepseek-chat",
  apiKey: "sk-60816d9be57f4189b658f1eaee52382e",
  configuration: { baseURL: "https://api.deepseek.com" },
  temperature: 0,
});

// 工具
const add = tool(({ a, b }) => String(a + b), {
  name: "add",
  description: "Add two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const multiply = tool(({ a, b }) => String(a * b), {
  name: "multiply",
  description: "Multiply two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const divide = tool(({ a, b }) => String(a / b), {
  name: "divide",
  description: "Divide two numbers",
  schema: z.object({ a: z.number(), b: z.number() }),
});

const toolsByName: Record<string, StructuredTool> = {
  add,
  multiply,
  divide,
};

const modelWithTools = model.bindTools(Object.values(toolsByName));

// 节点
const llmCall = async (
  state: typeof MessagesAnnotation.State,
  config?: RunnableConfig
) => {
  const response = await modelWithTools.invoke(
    [
      new SystemMessage("你是一个负责做算术的助手,根据用户描述执行加减乘除等运算,需要时调用工具得到结果后再用自然语言回复。"),
      ...state.messages,
    ],
    config
  );
  return { messages: [response] };
};

const toolNode = async (
  state: typeof MessagesAnnotation.State,
  config?: RunnableConfig
) => {
  const lastMessage = state.messages.at(-1);
  if (!lastMessage || !AIMessage.isInstance(lastMessage)) return { messages: [] };

  const results: ToolMessage[] = [];
  for (const toolCall of lastMessage.tool_calls ?? []) {
    const t = toolsByName[toolCall.name];
    if (!t) continue;
    const value = await t.invoke(toolCall.args ?? {}, config);
    results.push(new ToolMessage({ content: String(value), tool_call_id: toolCall.id ?? "" }));
  }
  return { messages: results };
};

// 条件路由
const shouldContinue = (state: typeof MessagesAnnotation.State) => {
  const lastMessage = state.messages.at(-1);
  if (!lastMessage || !AIMessage.isInstance(lastMessage)) return END;
  return lastMessage.tool_calls?.length ? "toolNode" : END;
};

// 组装图
const agent = new StateGraph(MessagesAnnotation)
  .addNode("llmCall", llmCall)
  .addNode("toolNode", toolNode)
  .addEdge(START, "llmCall")
  .addConditionalEdges("llmCall", shouldContinue, ["toolNode", END])
  .addEdge("toolNode", "llmCall")
  .compile();

// 流式运行
async function main() {
  const stream = agent.streamEvents(
    { messages: [new HumanMessage("请帮我算一下 3 加 4 等于多少。")] },
    { version: "v2" }
  );

  for await (const event of stream) {
    if (event.event === "on_chat_model_stream") {
      const chunk = event.data?.chunk?.content;
      if (typeof chunk === "string" && chunk) process.stdout.write(chunk);
    }
    if (event.event === "on_tool_start") {
      console.log(`\n[工具调用] ${event.name}`, JSON.stringify(event.data?.input));
    }
    if (event.event === "on_tool_end") {
      console.log(`[工具结果] ${event.name}: ${event.data?.output}`);
    }
  }

  console.log("\n");
}

main().catch(console.error);

on_chat_model_stream 是模型逐 token 输出,从 event.data.chunk.content 取片段写到终端就能得到打字机效果。on_tool_starton_tool_end 分别在工具开始和结束时触发,可以用来显示"正在计算……"这样的进度提示。这些事件能正常触发的前提是节点里把 config 传给了 modelWithTools.invoket.invoke,回调通路才算打通。如果节点没有传 config,这些事件就不会冒出来。

如果只想按节点观察每一步的状态增量,不关心逐字,可以换成 agent.streamstreamMode: "updates",每个 chunk 就是"节点名 -> 该节点本次返回的状态更新",调试时很有用。

和 LangChain 传统写法的对比

LangChain 写过类似计算器 Agent 的话,会发现两者的逻辑其实差不多,都是模型判断是否需要工具、调用工具、再根据工具结果生成回复。但有几个点上 LangGraph 的优势很明显。

流程可见方面,用 LangChainAgentExecutor,整条执行链路藏在对象内部,从外部很难直接看出走了哪些步骤。用 LangGraph 写,节点、边、条件路由全都显式定义,图的结构就是代码本身,不需要额外文档解释流程。

状态可追方面,LangGraph 图执行过程中,每个节点的输入输出都是状态的一次快照。后面加上 checkpointer 之后,这些快照可以持久化,支持暂停恢复、时间旅行和回放,AgentExecutor 做不到这一点。

扩展性方面,这一章的图很小,只有两个节点。后面加入持久化、人机协同、子图、多 Agent 协作时,只需要在图里加节点和边,不需要重写整个逻辑,扩展起来很自然。

小结

这一章用计算器 Agent 完整走了一遍 LangGraph 的核心三要素,几个值得记住的点:

  • MessagesAnnotation 定义消息列表状态,每个节点只返回增量,框架负责合并,不用手动维护整个数组
  • llmCall 节点负责调用模型,toolNode 节点负责执行工具,两者通过条件边构成一个可以反复循环的 Agent 推理回路
  • shouldContinue 是整张图的路由核心,tool_calls 有值走工具、为空走 END,分支逻辑和节点实现彻底分开
  • streamEvents 打通了逐 token 流式输出和工具事件,前提是节点里把 config 一路传下去,回调通路才能正常工作
  • addConditionalEdges 的第三个参数声明了合法的目标节点,图在编译时会做边界检查,防止条件函数返回意外节点名

下一章会在这张图上引入 checkpointer,给每次执行打快照,为持久化、暂停恢复和时间旅行做准备。

前端重生之 - 前端视角下的 Python

以前我认为 JavaScript 就是编程世界的全部。从 jQuery 时代的 DOM 操作,到 React/Vue 的组件化革命,再到 TypeScript 的类型安全,见证了前端技术的每一次跃迁。然而,AI 时代来临,人人都在喊转 “全栈“,所以我也开始真正深入 Python 的生态系统,才发现这不仅是两门语言的对话,更是两种编程哲学、两种技术文化的碰撞与融合。这篇文章,是我从前端视角重新审视 Python 的记录,也是我对技术本质的一次探索,接下来我还将从前端视角看 Java、Go、C# 等不同的后端的语言,可能会有错误的地方,欢迎指正,也欢迎关注我,后期还将有分析其他语言的文章,奥利给!


从 JS 的异步到 Python 的同步

1.1 事件循环的底层机制

前端对事件循环(Event Loop)的理解,往往始于浏览器中的 setTimeoutPromise。JavaScript 的单线程异步模型,是为了应对浏览器环境中用户交互、网络请求等 I/O 密集型场景而设计的。我们习惯了回调地狱的煎熬,也享受过 async/await 带来的语法糖甜蜜。但很少有人深入思考:为什么 JavaScript 必须是单线程的?这个设计选择背后的权衡是什么?

JavaScript 诞生于浏览器环境,而浏览器的核心职责是渲染页面和响应用户交互。如果 JavaScript 是多线程的,一个线程正在修改 DOM,另一个线程同时也在修改同一个 DOM 节点,就会产生竞争条件(Race Condition),导致不可预测的行为。为了避免这种复杂性,JavaScript 的设计者选择了单线程模型,并通过事件循环来实现异步非阻塞 I/O。

浏览器的事件循环可以简化为以下伪代码:

while (true) {
    // 1. 执行宏任务队列中的一个任务
    const macroTask = macroTaskQueue.shift();
    if (macroTask) execute(macroTask);
    
    // 2. 执行所有微任务
    while (microTaskQueue.length > 0) {
        const microTask = microTaskQueue.shift();
        execute(microTask);
    }
    
    // 3. 渲染(如果需要)
    if (shouldRender) render();
}

这个模型保证了 JavaScript 的执行顺序是可预测的:宏任务 → 微任务 → 渲染。Promise 的回调之所以比 setTimeout 先执行,就是因为它们被放入了微任务队列。

然而,当我第一次接触 Python 的 asyncio 时,一种奇妙的熟悉感与陌生感同时涌现。Python 的协程机制与 JavaScript 的 Promise 有着惊人的相似性,但底层哲学却截然不同。

1.2 Python asyncio 的设计哲学

Python 的 asyncio 是在 Python 3.4 中引入的,在 3.5 中通过 async/await 语法得到大幅改进。与 JavaScript 不同,Python 并不是天生单线程的——它有多线程(threading 模块)和多进程(multiprocessing 模块)的完整支持。asyncio 是 Python 对协程(Coroutine)这一并发模型的选择,而不是被迫的设计。

让我们深入对比两者的实现:

# Python asyncio 示例
import asyncio

async def fetch_data(url):
    print(f"开始请求: {url}")
    await asyncio.sleep(1)  # 模拟网络请求
    print(f"请求完成: {url}")
    return f"数据来自 {url}"

async def main():
    # 并发执行多个任务
    tasks = [
        fetch_data("https://api.example.com/1"),
        fetch_data("https://api.example.com/2"),
        fetch_data("https://api.example.com/3")
    ]
    results = await asyncio.gather(*tasks)
    print(results)

asyncio.run(main())
// JavaScript 对比实现
async function fetchData(url) {
    console.log(`开始请求: ${url}`);
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`请求完成: ${url}`);
    return `数据来自 ${url}`;
}

async function main() {
    const tasks = [
        fetchData("https://api.example.com/1"),
        fetchData("https://api.example.com/2"),
        fetchData("https://api.example.com/3")
    ];
    const results = await Promise.all(tasks);
    console.log(results);
}

main();

表面看两者几乎相同,但底层实现有本质区别:

特性 JavaScript Python
事件循环 浏览器/Node 内置,不可替换 asyncio 库实现,可自定义
协程实现 基于 Promise 和微任务队列 基于生成器(Generator)和事件循环
线程模型 单线程 + 事件循环 多线程/多进程 + 可选的协程
GIL 影响 无(天生单线程) 有(多线程受 GIL 限制)
并发性能 适合 I/O 密集型 适合 I/O 密集型,CPU 密集型需用多进程

1.3 GIL:Python 的"阿喀琉斯之踵"

谈到 Python 的并发,就不能不提 GIL(Global Interpreter Lock,全局解释器锁)。GIL 是 CPython 实现中的一个机制,它确保任何时候只有一个线程在执行 Python 字节码。这意味着,即使在多核 CPU 上,Python 的多线程也无法实现真正的并行计算。

import threading
import time

def cpu_bound_task(n):
    """CPU 密集型任务"""
    count = 0
    for i in range(n):
        count += i * i
    return count

# 多线程版本(受 GIL 限制)
def multi_threaded():
    threads = []
    for _ in range(4):
        t = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

# 多进程版本(绕过 GIL)
from multiprocessing import Process

def multi_process():
    processes = []
    for _ in range(4):
        p = Process(target=cpu_bound_task, args=(10_000_000,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()

# 性能对比
start = time.time()
multi_threaded()
print(f"多线程耗时: {time.time() - start:.2f}秒")

start = time.time()
multi_process()
print(f"多进程耗时: {time.time() - start:.2f}秒")

在我的测试环境中(4 核 CPU),多线程版本耗时约 12 秒,而多进程版本仅需 3 秒。这就是 GIL 的影响——多线程在 CPU 密集型任务上无法发挥多核优势。

JavaScript 没有 GIL 的问题,因为它天生就是单线程的。但这也意味着 JavaScript 无法利用多核 CPU 进行并行计算——除非使用 Worker Threads(Node.js)或 Web Workers(浏览器),但这些机制与主线程是隔离的,通信成本较高。

1.4 编程范式的思维转换

JavaScript 是一门多范式语言,但前端开发中函数式编程的影子无处不在:mapfilterreduce 成为日常,Immutable.js 和 Ramda 这样的库广受欢迎。我们追求纯函数、避免副作用、崇尚不可变性。这种趋势在 React 的函数组件和 Hooks 中达到顶峰。

// React 函数组件 + Hooks(函数式风格)
import React, { useState, useEffect } from 'react';

function UserList({ users }) {
    const [filteredUsers, setFilteredUsers] = useState([]);
    
    useEffect(() => {
        const activeUsers = users
            .filter(u => u.isActive)
            .map(u => ({ ...u, name: u.name.toUpperCase() }));
        setFilteredUsers(activeUsers);
    }, [users]);
    
    return (
        <ul>
            {filteredUsers.map(u => <li key={u.id}>{u.name}</li>)}
        </ul>
    );
}

Python 则是一门 "batteries included" 的语言,它拥抱多种范式却从不偏执。在 Python 中,你可以写出优雅的函数式代码:

# Python 函数式风格
users = [
    {"id": 1, "name": "Alice", "is_active": True},
    {"id": 2, "name": "Bob", "is_active": False},
    {"id": 3, "name": "Charlie", "is_active": True}
]

# 函数式写法
filtered_users = list(
    map(
        lambda u: {**u, "name": u["name"].upper()},
        filter(lambda u: u["is_active"], users)
    )
)

# 但更 Pythonic 的方式是列表推导式
filtered_users = [
    {**u, "name": u["name"].upper()} 
    for u in users 
    if u["is_active"]
]

这种"列表推导式"的语法,是 Python 对函数式编程的本土化改造。它既保留了函数式的表达能力,又符合 Python 简洁优雅的设计哲学。

Python 还支持面向对象和命令式编程:

# Python 面向对象风格
class User:
    def __init__(self, id, name, is_active):
        self.id = id
        self.name = name
        self.is_active = is_active
    
    def activate(self):
        self.is_active = True
    
    def __repr__(self):
        return f"User({self.name})"

# 使用类
users = [User(1, "Alice", True), User(2, "Bob", False)]
for user in users:
    if not user.is_active:
        user.activate()

这让我反思:前端开发中是否过度追求函数式的"纯粹",而忽略了实用主义的平衡?React 的类组件被函数组件取代,但类组件在某些场景下(如复杂的生命周期管理)仍然有其优势。Python 的多范式支持提醒我们:没有最好的范式,只有最适合场景的范式。


类型系统——从动态到静态的考虑

2.1 TypeScript 的革命

2012 年,TypeScript 的诞生改变了前端开发的格局。作为 JavaScript 的超集,TypeScript 为动态语言带来了静态类型的严谨。今天,几乎所有大型前端项目都采用 TypeScript,类型安全已成为行业标准。

TypeScript 的成功不是偶然的。它解决了 JavaScript 开发中的几个核心痛点:

  1. 运行时错误前置:在编译阶段发现类型错误,而不是在生产环境崩溃
  2. IDE 支持:智能提示、自动补全、重构支持
  3. 文档即代码:类型定义就是最好的 API 文档
  4. 团队协作:类型约束作为团队间的契约
// TypeScript 示例
interface User {
    id: number;
    name: string;
    email?: string;  // 可选属性
}

function greet(user: User): string {
    return `Hello, ${user.name}`;
}

// 编译错误:类型不匹配
const result = greet({ id: "1", name: "Alice" });  // Error: id 应该是 number

2.2 Python 类型注解的演进

有趣的是,Python 的类型注解(Type Hints)几乎是与 TypeScript 同期发展的。PEP 484 在 2014 年引入类型注解,PEP 526 在 2016 年完善变量注解。两条平行线,却走向了相似的终点。

from typing import Optional, List, Dict

class User:
    def __init__(self, id: int, name: str, email: Optional[str] = None):
        self.id = id
        self.name = name
        self.email = email

def greet(user: User) -> str:
    return f"Hello, {user.name}"

# 类型检查工具(如 mypy)会在静态分析时报告错误
user = User(id="1", name="Alice")  # mypy: Argument "id" has incompatible type "str"; expected "int"

然而,TypeScript 和 Python 类型系统的底层哲学存在本质差异:

2.2.1 编译时 vs 运行时

TypeScript 的类型在编译时完全擦除,编译后的 JavaScript 不包含任何类型信息:

// TypeScript 源码
function add(a: number, b: number): number {
    return a + b;
}

// 编译后的 JavaScript
function add(a, b) {
    return a + b;
}

Python 的类型注解在运行时保留,但解释器不做强制检查:

# Python 源码
def add(a: int, b: int) -> int:
    return a + b

# 运行时可以通过 __annotations__ 访问类型信息
print(add.__annotations__)  # {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}

# 但解释器不会检查类型
result = add("hello", "world")  # 正常运行,返回 "helloworld"

2.2.2 结构类型 vs 名义类型

TypeScript 采用结构类型系统(Structural Typing),也称为"鸭子类型"(Duck Typing):

interface Point {
    x: number;
    y: number;
}

function printPoint(p: Point) {
    console.log(`${p.x}, ${p.y}`);
}

// 只要结构匹配,就可以传递
printPoint({ x: 1, y: 2 });  // OK
printPoint({ x: 1, y: 2, z: 3 });  // OK(多余属性允许)

Python 的类型检查器(如 mypy)同样支持结构类型,通过 Protocol(PEP 544):

from typing import Protocol

class Point(Protocol):
    x: int
    y: int

def print_point(p: Point) -> None:
    print(f"{p.x}, {p.y}")

class MyPoint:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

# MyPoint 没有显式继承 Point,但结构匹配即可
print_point(MyPoint(1, 2))  # OK

2.2.3 类型推断

TypeScript 的类型推断更为激进:

// TypeScript 能推断出 arr 是 number[]
const arr = [1, 2, 3];

// 能推断出 result 是 number
const result = arr.map(x => x * 2).filter(x => x > 2);

Python 的类型推断相对保守,需要显式注解:

from typing import List

# Python 需要显式类型注解
arr: List[int] = [1, 2, 3]

# 或者让 mypy 推断(有限支持)
result = [x * 2 for x in arr if x > 2]  # mypy 能推断为 List[int]

2.3 渐进式类型的价值

这种对比从侧面来说:类型系统的价值不在于"正确性"本身,而在于它如何帮助团队协作和代码演进。Python 的渐进式类型(Gradual Typing)策略——允许在需要时添加类型,在灵活时保持动态——或许比 TypeScript 的"全有或全无"更加务实。

# Python 渐进式类型示例
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models import User  # 仅在类型检查时导入

def process_user(user):  # 动态类型,灵活
    return user.name.upper()

def process_user_typed(user: "User") -> str:  # 静态类型,安全
    return user.name.upper()

在实际的开发中,我通常采用以下策略:

  • 公共 API 和核心模块使用完整的类型注解
  • 脚本和原型代码保持动态类型,快速迭代
  • 使用 mypy 在 CI 中检查关键模块的类型安全

生态系统

3.1 npm 与 PyPI:包管理的两种哲学

前端对于 npm 生态的复杂情感,可以用一句话概括:"node_modules 是世界上最重的东西"。JavaScript 的微包文化(left-pad 事件)和依赖,是每个前端的心头痛。

让我们看看一个典型的 React 项目的依赖树:

$ npm list | wc -l
# 输出可能超过 1000 行

$ du -sh node_modules
# 输出可能超过 500MB

这种依赖膨胀的原因是多方面的:

  1. 微包文化:JavaScript 生态倾向于将功能拆分为极小的包,一个左填充函数(left-pad)也能成为一个包
  2. 重复依赖:不同版本的同一个库可能同时存在
  3. 开发依赖混杂:构建工具、测试框架、类型定义都混在一起

2016 年的微包事件是一个标志性案例。一个只有 11 行代码的包被作者从 npm 下架,导致全球数千个项目无法构建。这暴露了微包文化的脆弱性。

Python 的包管理生态则呈现出不同的面貌。pip、conda、poetry、pipenv …… 工具超级多,但核心理念一致:显式优于隐式

# Python 的 requirements.txt
requests==2.28.1
numpy>=1.21.0
pandas~=1.5.0

这个文件明确告诉我们:

  • requests 必须严格等于 2.28.1 版本
  • numpy 可以是 1.21.0 或更高版本
  • pandas 可以是 1.5.x 系列(补丁版本可以变)

这种显式依赖管理的文化,让 Python 项目的可重现性远超 JavaScript。当你克隆一个 Python 项目,你知道需要安装什么;而当你克隆一个 Node.js 项目,node_modules 的深渊往往让人望而却步。

3.2 虚拟环境:Python 的隔离艺术

Python 的虚拟环境(virtualenv/venv)是包管理的另一大特色。每个项目可以有独立的 Python 环境和依赖,互不干扰。

# 创建虚拟环境
python -m venv myproject-env

# 激活虚拟环境
source myproject-env/bin/activate  # Linux/Mac
myproject-env\Scripts\activate  # Windows

# 安装依赖
pip install -r requirements.txt

# 退出虚拟环境
deactivate

这与 Node.js 的 node_modules 本地安装有相似之处,但更加彻底——虚拟环境甚至隔离了 Python 解释器本身。

现在 Python 项目更倾向于使用 pyproject.toml(PEP 518)来管理依赖:

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "my-project"
version = "0.1.0"
description = "A sample project"

[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.28"
pydantic = "^1.10"

[tool.poetry.dev-dependencies]
pytest = "^7.0"
black = "^22.0"
mypy = "^0.991"

Poetry 不仅管理依赖,还管理虚拟环境、打包、发布,是一个完整的项目管理工具。这与 JavaScript 生态中 npm/yarn/pnpm 的竞争格局形成鲜明对比。

3.3 标准库的力量

Python 的 "batteries included" 哲学,在标准库中体现得淋漓尽致。从文件处理到网络编程,从正则表达式到 JSON 解析,从单元测试到并发编程——Python 标准库几乎覆盖了一个开发者 80% 的日常需求。

# Python 标准库示例
import json
import re
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
from unittest import TestCase, main

# JSON 处理
data = {"name": "Alice", "age": 30}
json_str = json.dumps(data, indent=2)

# 正则表达式
pattern = r"\b\w+@\w+\.\w+\b"
emails = re.findall(pattern, "Contact: alice@example.com, bob@test.org")

# 文件路径操作
config_path = Path.home() / ".config" / "myapp" / "settings.json"

# 日期时间
now = datetime.now()
future = now + timedelta(days=7)

# HTTP 请求
with urllib.request.urlopen("https://api.example.com/data") as response:
    data = json.loads(response.read())

相比之下,JavaScript 的标准库堪称贫瘠。直到 ES6 引入 Promise、fetch、模块化,JavaScript 才勉强跟上时代。但即便如此,lodash、axios、moment 依然是大多数项目的标配。

这种差异的根源在于语言的设计目标:

  • JavaScript 诞生于浏览器,被设计为轻量级脚本语言,依赖浏览器提供的 DOM API
  • Python 诞生于通用编程,被设计为"可执行的伪代码",需要在各种环境中独立运行

标准库的丰富程度,反映的是语言设计者对"开箱即用"的不同理解。

3.4 生态系统的成熟度对比

维度 JavaScript/npm Python/PyPI
包数量 200万+ 40万+
包平均大小 小(微包文化) 大(功能完整)
依赖管理 嵌套依赖(node_modules) 扁平依赖 + 虚拟环境
标准库 贫瘠 丰富("batteries included")
类型定义 @types/* 包 内置类型注解
安全审计 npm audit safety, pip-audit
私有仓库 Verdaccio, Nexus PyPI Enterprise, Devpi

数据科学的疆域的追赶

4.1 Python 的数据霸权

如果说前端是 JavaScript 的天下,那么数据科学就是 Python 的帝国。NumPy、Pandas、Matplotlib、Scikit-learn、TensorFlow、PyTorch——这些库构成了数据科学的完整工具链,而 Python 是它们的通用语言。

这种霸权不是偶然的。Python 的简洁语法、丰富的科学计算库、与 C/C++/Fortran 的良好互操作性,使其成为数据科学家的首选语言。

4.1.1 NumPy:向量化计算的威力

NumPy 是 Python 科学计算的基础。它提供了高效的多维数组对象和数学函数库,底层使用 C 实现,性能远超纯 Python。

import numpy as np

# 创建数组
arr = np.array([1, 2, 3, 4, 5])

# 向量化运算——比 Python 循环快 100 倍
result = arr * 2 + 1  # [3, 5, 7, 9, 11]

# 多维数组
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# 矩阵运算
transposed = matrix.T
dot_product = matrix @ matrix.T

# 统计函数
mean = np.mean(arr)
std = np.std(arr)
max_val = np.max(matrix, axis=0)  # 每列的最大值

让我们做一个性能对比:

import time

# Python 原生列表
python_list = list(range(1_000_000))

start = time.time()
result = [x * 2 for x in python_list]
print(f"Python 列表推导式: {time.time() - start:.4f}秒")

# NumPy 数组
numpy_array = np.array(python_list)

start = time.time()
result = numpy_array * 2
print(f"NumPy 向量化运算: {time.time() - start:.4f}秒")

在我的电脑上,Python 列表推导式耗时约 0.08 秒,而 NumPy 仅需 0.001 秒——80 倍的性能差距!这是因为 NumPy 的运算是在 C 层面执行的,避免了 Python 的解释器开销。

4.1.2 Pandas:数据处理的艺术

如果说 NumPy 是数组计算的利器,Pandas 就是数据处理的瑞士军刀。它提供了 DataFrame 和 Series 两种数据结构,让数据清洗、转换、分析变得异常简单。

import pandas as pd

# 读取数据
df = pd.read_csv('sales_data.csv')

# 数据清洗
df = df.dropna()  # 删除缺失值
df = df[df['price'] > 0]  # 过滤异常值
df['date'] = pd.to_datetime(df['date'])  # 类型转换

# 数据转换
df['revenue'] = df['price'] * df['quantity']
df['month'] = df['date'].dt.month

# 分组聚合
monthly_sales = df.groupby('month').agg({
    'revenue': 'sum',
    'quantity': 'mean'
}).reset_index()

# 透视表
pivot = df.pivot_table(
    values='revenue',
    index='category',
    columns='month',
    aggfunc='sum'
)

# 合并数据
merged = pd.merge(df, customer_df, on='customer_id', how='left')

这段代码如果用 JavaScript 实现,需要多少行?lodash 可以处理数组,但没有原生的 DataFrame 概念。D3.js 可以做数据转换,但学习曲线陡峭。Pandas 的链式操作让复杂的数据处理变得可读、可维护。

4.1.3 数据可视化

Matplotlib 和 Seaborn 是 Python 数据可视化的主力军:

import matplotlib.pyplot as plt
import seaborn as sns

# 设置样式
sns.set_style("whitegrid")

# 创建图表
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 折线图
df.groupby('date')['revenue'].sum().plot(ax=axes[0, 0], title='Daily Revenue')

# 柱状图
df['category'].value_counts().plot(kind='bar', ax=axes[0, 1], title='Category Distribution')

# 散点图
axes[1, 0].scatter(df['price'], df['quantity'], alpha=0.5)
axes[1, 0].set_title('Price vs Quantity')

# 热力图
corr = df[['price', 'quantity', 'revenue']].corr()
sns.heatmap(corr, annot=True, ax=axes[1, 1], title='Correlation Matrix')

plt.tight_layout()
plt.savefig('analysis.png', dpi=300)

前端可能会说:"这些用 D3.js 也能做,而且交互性更强。"没错,D3.js 的交互能力是 Matplotlib 无法比拟的。但 Matplotlib 的优势在于快速探索和静态报告——数据分析不需要为每个图表写 200 行 D3 代码。

4.2 前端的数据觉醒

幸运的是,前端世界正在觉醒。TensorFlow.js、ONNX.js、Apache Arrow JS——这些项目正在把数据科学的能力带入浏览器。

4.2.1 TensorFlow.js

TensorFlow.js 让机器学习模型可以在浏览器中运行:

import * as tf from '@tensorflow/tfjs';

// 创建一个简单的神经网络
const model = tf.sequential({
    layers: [
        tf.layers.dense({ inputShape: [784], units: 32, activation: 'relu' }),
        tf.layers.dense({ units: 10, activation: 'softmax' })
    ]
});

model.compile({
    optimizer: 'adam',
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy']
});

// 训练模型(在浏览器中)
await model.fit(xs, ys, {
    epochs: 10,
    batchSize: 32,
    callbacks: {
        onEpochEnd: (epoch, logs) => {
            console.log(`Epoch ${epoch}: loss = ${logs.loss}`);
        }
    }
});

// 预测
const prediction = model.predict(newImage);

这意味着前端可以在用户设备上运行机器学习模型,无需服务器参与。隐私保护、低延迟、离线可用——这些是服务端推理无法比拟的优势。

4.2.2 Apache Arrow JS

Apache Arrow 是一种跨语言的列式内存格式,Arrow JS 让 JavaScript 可以高效地处理大规模数据:

import { Table, FloatVector } from 'apache-arrow';

// 创建 Arrow 表
const table = Table.new(
    [
        FloatVector.from([1, 2, 3, 4, 5]),
        FloatVector.from([10, 20, 30, 40, 50])
    ],
    ['x', 'y']
);

// 高效查询
const sum = table.getColumn('y').toArray().reduce((a, b) => a + b, 0);

Arrow 的列式存储格式让数据在 Python、JavaScript、R、Julia 之间零拷贝传输成为可能。这对于前后端数据交互是一个革命性的改进。

4.3 前端的数据科学学习路径

但更深层的思考是:前端是否应该掌握数据科学的能力?

我的答案是肯定的。现代前端不再是简单的页面展示,而是数据驱动的交互应用。理解数据处理、理解机器学习的基本原理,将成为高级前端的必备技能。

Web 开发的殊途同归

5.1 Django vs Express:两种架构哲学

Django 是 Python Web 开发的旗舰框架,它的哲学是"约定优于配置"。ORM、表单处理、认证系统、管理后台——Django 提供了一站式解决方案。这种"全功能框架"的思路,让开发者可以快速搭建复杂的 Web 应用。

# Django 模型定义
from django.db import models
from django.contrib.auth.models import User

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']

# Django 视图
from django.shortcuts import render, get_object_or_404
from rest_framework import viewsets
from rest_framework.decorators import action

class ArticleViewSet(viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    
    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.publish()
        return Response({'status': 'published'})

Django 的优势在于:

  • 快速开发:内置的 admin 界面让 CRUD 操作无需额外代码
  • 安全性:内置 CSRF 保护、SQL 注入防护、XSS 过滤
  • 可扩展性:丰富的第三方应用生态

但 Django 也有其局限性:

  • 灵活性不足:Django 的"全功能"意味着你必须按照它的方式做事
  • 学习曲线陡峭:需要理解 ORM、视图、模板、中间件等多个概念
  • 性能开销:大而全的框架必然带来性能损耗

Express.js 则是 Node.js 世界的微框架代表:

const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// 模型定义(使用 Mongoose)
const articleSchema = new mongoose.Schema({
    title: String,
    content: String,
    author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
    createdAt: { type: Date, default: Date.now }
});

const Article = mongoose.model('Article', articleSchema);

// 路由
app.get('/api/articles', async (req, res) => {
    const articles = await Article.find().sort({ createdAt: -1 });
    res.json(articles);
});

app.post('/api/articles', async (req, res) => {
    const article = new Article(req.body);
    await article.save();
    res.status(201).json(article);
});

app.listen(3000);

Express 的优势在于:

  • 灵活性:只提供基础功能,其他由你选择
  • 学习曲线平缓:理解中间件概念后即可上手
  • 性能:轻量级框架,开销小

但 Express 的灵活性也带来了问题:

  • 选择困难症:ORM 用 Sequelize、TypeORM 还是 Prisma?验证用 Joi、Yup 还是 class-validator?
  • 项目结构不一致:每个 Express 项目的结构都可能不同
  • 重复造轮子:很多功能需要自己实现或选择第三方库
维度 Django Express.js
架构风格 全功能框架(" batteries included ") 微框架
ORM 内置 Django ORM 需额外选择(Sequelize/TypeORM/Prisma)
认证授权 内置 需额外实现(Passport.js 等)
管理后台 内置 admin
学习曲线 陡峭 平缓
灵活性 较低
适用场景 大型项目、快速原型 中小型项目、API 服务
性能 中等

5.2 FastAPI:Python 的现代答案

如果说 Django 是 Python 的 Spring,那么 FastAPI 就是 Python 的 NestJS。FastAPI 采用声明式编程、依赖注入、类型注解,它的设计哲学与 TypeScript 生态高度契合。

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from typing import List, Optional
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session

# 数据库设置
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# 数据库模型
class UserDB(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    email = Column(String, unique=True, index=True)

# Pydantic 模型(用于 API 验证)
class UserBase(BaseModel):
    name: str
    email: str

class UserCreate(UserBase):
    pass

class User(UserBase):
    id: int
    
    class Config:
        orm_mode = True

# FastAPI 应用
app = FastAPI(title="User API", version="1.0.0")

# 依赖注入:数据库会话
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# 路由
@app.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = UserDB(**user.dict())
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

@app.get("/users/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = db.query(UserDB).offset(skip).limit(limit).all()
    return users

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(UserDB).filter(UserDB.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user

这段代码的优雅让我惊叹:

  1. 类型安全:Pydantic 模型自动验证请求和响应
  2. 自动文档:访问 /docs 即可获得 Swagger UI 文档
  3. 异步支持:原生支持 async/await
  4. 依赖注入Depends 让代码解耦、可测试

对比 TypeScript 的 NestJS:

// NestJS 对比
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

class CreateUserDto {
    name: string;
    email: string;
}

@Controller('users')
export class UserController {
    constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>
    ) {}
    
    @Post()
    async create(@Body() createUserDto: CreateUserDto) {
        const user = this.userRepository.create(createUserDto);
        return this.userRepository.save(user);
    }
    
    @Get()
    async findAll() {
        return this.userRepository.find();
    }
}

FastAPI 和 NestJS 的设计如此相似——装饰器路由、依赖注入、DTO 验证、ORM 集成。这或许预示着 Web 开发的未来:语言的边界正在模糊,好的设计理念会被跨语言借鉴。

5.3 性能对比:Node.js vs Python

让我们做一个简单的性能测试,对比 Node.js 和 Python 处理 HTTP 请求的能力:

Node.js (Express)

const express = require('express');
const app = express();

app.get('/api/data', (req, res) => {
    // 模拟数据库查询
    const data = Array.from({ length: 1000 }, (_, i) => ({
        id: i,
        value: Math.random()
    }));
    res.json(data);
});

app.listen(3000);

Python (FastAPI)

from fastapi import FastAPI
import random

app = FastAPI()

@app.get("/api/data")
async def get_data():
    data = [
        {"id": i, "value": random.random()}
        for i in range(1000)
    ]
    return data

# 使用 uvicorn 运行:uvicorn main:app --workers 4

使用 wrk 进行压力测试:

# Node.js
wrk -t12 -c400 -d30s http://localhost:3000/api/data
# Requests/sec:  15000

# Python (单 worker)
wrk -t12 -c400 -d30s http://localhost:8000/api/data
# Requests/sec:   8000

# Python (4 workers)
wrk -t12 -c400 -d30s http://localhost:8000/api/data
# Requests/sec:  25000

结果挺让人惊讶的:在单 worker 模式下,Node.js 的性能是 Python 的 2 倍。但当 Python 使用多 worker(利用多核 CPU)时,性能反超 Node.js。这说明:

  1. 单线程性能:Node.js 的 V8 引擎优于 Python 的解释器
  2. 多核利用:Python 的多进程模型可以充分利用多核 CPU
  3. 场景选择:I/O 密集型任务两者差距不大,CPU 密集型任务需要多进程

融会贯通

6.1 语言只是工具,思维才是核心

深入 Python 之后,我越来越确信一个观点:编程语言的差异,远不如编程思维的差异重要。无论是 JavaScript 还是 Python,优秀的代码都遵循相同的原则:

6.1.1 SOLID 原则

单一职责原则(Single Responsibility Principle)

# 不好的设计:一个类做太多事
class UserManager:
    def create_user(self, data): ...
    def send_email(self, user): ...
    def generate_report(self): ...

# 好的设计:职责分离
class UserService:
    def create_user(self, data): ...

class EmailService:
    def send_email(self, user): ...

class ReportService:
    def generate_report(self): ...

开闭原则(Open/Closed Principle)

from abc import ABC, abstractmethod

# 抽象基类
class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount: float) -> bool:
        pass

# 具体实现
class AlipayProcessor(PaymentProcessor):
    def process(self, amount: float) -> bool:
        # 支付宝支付逻辑
        return True

class WechatProcessor(PaymentProcessor):
    def process(self, amount: float) -> bool:
        # 微信支付逻辑
        return True

# 使用
class PaymentService:
    def __init__(self, processor: PaymentProcessor):
        self.processor = processor
    
    def pay(self, amount: float) -> bool:
        return self.processor.process(amount)

# 新增支付方式无需修改现有代码
class StripeProcessor(PaymentProcessor):
    def process(self, amount: float) -> bool:
        return True

6.1.2 设计模式

设计模式是跨语言的。无论是 JavaScript 还是 Python,观察者模式、工厂模式、策略模式等都有相似的实现:

# Python 观察者模式
from typing import List, Callable

class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, List[Callable]] = {}
    
    def on(self, event: str, callback: Callable):
        if event not in self._listeners:
            self._listeners[event] = []
        self._listeners[event].append(callback)
    
    def emit(self, event: str, *args, **kwargs):
        for callback in self._listeners.get(event, []):
            callback(*args, **kwargs)

# 使用
emitter = EventEmitter()
emitter.on('user_created', lambda user: print(f"User created: {user}"))
emitter.emit('user_created', {'name': 'Alice'})
// JavaScript 观察者模式(几乎相同)
class EventEmitter {
    constructor() {
        this.listeners = {};
    }
    
    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }
    
    emit(event, ...args) {
        (this.listeners[event] || []).forEach(cb => cb(...args));
    }
}

6.1.3 编程思维的培养

掌握多门语言的价值,不在于"技多不压身",而在于从不同视角理解这些原则,形成更全面的技术判断力。

  • JavaScript 教会我:异步编程、函数式思维、事件驱动
  • Python 教会我:简洁优雅、实用主义、科学计算
  • TypeScript 教会我:类型安全、接口设计、静态分析

6.2 未来的融合趋势

技术发展的趋势是融合而非对立。我们看到 Python 和 JavaScript 都在做的事情:

6.2.1 WebAssembly 的崛起

WebAssembly(Wasm)让 Python 可以在浏览器中运行:

# 使用 Pyodide 在浏览器中运行 Python
import micropip
await micropip.install('numpy')

import numpy as np
arr = np.array([1, 2, 3, 4, 5])
result = arr * 2

这意味着前端可以在浏览器中使用 Python 的数据处理能力,而无需服务器参与。

6.2.2 PyScript 的革命

PyScript 让 Python 可以直接嵌入 HTML:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
    <div id="output"></div>
    
    <py-script>
        from js import document
        import numpy as np
        
        arr = np.array([1, 2, 3, 4, 5])
        result = arr * 2
        
        output = document.getElementById('output')
        output.innerHTML = f"Result: {result.tolist()}"
    </py-script>
</body>
</html>

6.2.3 跨语言借鉴

  • Node.js 的 worker_threads 借鉴了 Python 的多进程模型
  • TypeScript 的类型系统影响了 Python 的类型注解设计
  • Rust 的所有权系统正在影响 JavaScript 和 Python 的内存管理思路

6.2.4 全栈工程师的新定义

未来的工程师,可能不再被"前端"或"后端"的标签所限制。他们会根据场景选择最合适的工具:

  • Python 处理数据、训练模型、编写自动化脚本
  • JavaScript/TypeScript 构建用户界面、实现交互逻辑
  • Rust 编写高性能模块、系统级工具
  • Go 构建微服务、高并发后端

这不是"全栈"的泛化,而是技术能力的深化。真正的技术专家,不是掌握最多语言的人,而是知道何时使用哪门语言的人。


结语

写完这篇文章,我想起了一个古老的比喻:

"如果你手里只有一把锤子,那么所有问题看起来都像钉子。"

JavaScript 是我手中的第一把锤子,它帮助我构建了无数精彩的 Web 应用。从简单的页面交互到复杂的单页应用,从 jQuery 到 React,从回调地狱到 async/await——JavaScript 陪伴我走过了前端技术的每一个阶段。

但 Python 让我看到了另一片天空:

  • 数据科学的深邃:NumPy、Pandas、Scikit-learn 让数据处理变得优雅而高效
  • 自动化的便捷:几行 Python 脚本可以替代 hours of manual work
  • 科学计算的严谨:从物理模拟到金融建模,Python 是科学家的首选语言
  • Web 开发的简洁:FastAPI 的设计哲学让我重新审视"好的代码"的定义

这两门语言不是竞争对手,而是互补的伙伴。

技术的深度,来自于对一门语言的精通;技术的广度,来自于对多门语言的理解。而技术的智慧,来自于知道何时使用哪一门语言,加油,奥利给!

手撕 Claude Code-4: TodoWrite 与任务系统

第 4 章:TodoWrite 与任务系统

源码位置:src/tools/TodoWriteTool/src/tasks/src/utils/tasks.ts


4.1 两个容易混淆的概念

Claude Code 中有两个相关但不同的"任务"概念:

概念 说明 存储位置
Todo(任务清单) Claude 在当前会话中规划的工作步骤 AppState.todos
Task(系统任务) 运行时管理的实体(子代理、Shell 命令、后台会话等) AppState.tasks

本章先讲 TodoWrite(任务清单),再讲 Task System(系统任务)。


4.2 TodoWrite:Claude 的工作计划本

4.2.1 数据模型

// src/utils/todo/types.ts
type TodoItem = {
  content: string                                    // 任务描述
  status: 'pending' | 'in_progress' | 'completed'
  activeForm: string                                 // 任务的主动形式(如 "Reading src/foo.ts")
}

type TodoList = TodoItem[]

4.2.2 工具实现

源码位置:src/tools/TodoWriteTool/TodoWriteTool.ts:65

async call({ todos }, context) {
  const appState = context.getAppState()
  
  // 每个代理有自己的 todo 列表(用 agentId 或 sessionId 区分)
  const todoKey = context.agentId ?? getSessionId()
  const oldTodos = appState.todos[todoKey] ?? []
  
  // 关键逻辑:如果所有任务都已完成,AppState 中清空为 []
  const allDone = todos.every(_ => _.status === 'completed')
  const newTodos = allDone ? [] : todos

  // 验证提醒:仅当 allDone && 3+ 项 && 无验证步骤时才触发
  let verificationNudgeNeeded = false
  if (
    feature('VERIFICATION_AGENT') &&
    getFeatureValue_CACHED_MAY_BE_STALE('tengu_hive_evidence', false) &&
    !context.agentId &&   // 只在主线程触发,不在子代理
    allDone &&            // 必须是关闭列表的那一刻
    todos.length >= 3 &&
    !todos.some(t => /verif/i.test(t.content))
  ) {
    verificationNudgeNeeded = true
  }

  // 注意:API 是 setAppState(接收 prev → newState 函数),不是 updateAppState
  context.setAppState(prev => ({
    ...prev,
    todos: { ...prev.todos, [todoKey]: newTodos },
  }))

  // 返回值中 newTodos 是原始输入 todos(非清空后的 [])
  return {
    data: { oldTodos, newTodos: todos, verificationNudgeNeeded },
  }
}

4.2.3 关键设计点

1. 所有完成即清空

当所有 todo 都标记为 completed,AppState 中的列表被清空为 [] 而不是保留所有已完成项。这避免了 token 浪费(每次 API 调用都要序列化已完成的任务)。注意:工具返回值中的 newTodos 字段仍为原始 todos 输入,清空仅影响 AppState 存储层。

2. agentId 隔离

每个子代理有独立的 todo 列表(通过 agentId 作为 key)。主线程用 sessionId。这样并行运行的子代理不会互相干扰。

3. shouldDefer: true

TodoWrite 被标记为延迟披露,不在每次 API 请求中直接包含其 Schema。Claude 通过 ToolSearch 找到它。

4. 与 TodoV2(Task API)互斥

源码位置:src/tools/TodoWriteTool/TodoWriteTool.ts:53 中:

isEnabled() {
  return !isTodoV2Enabled()
}

源码位置:src/utils/tasks.ts:133 isTodoV2Enabled() 在以下情况返回 true(即 TodoWrite 被禁用):

  • 环境变量 CLAUDE_CODE_ENABLE_TASKS=1
  • 非交互式会话(SDK 模式)

这意味着 SDK 用户默认使用 Task API 而非 TodoWrite。

5. verificationNudgeNeeded

这是一个"行为引导"机制。触发条件(需同时满足):

  • Feature flag VERIFICATION_AGENT 已开启(编译时 gate)
  • GrowthBook flag tengu_hive_evidence 为 true
  • 当前在主线程(!context.agentId
  • 当前调用正在关闭整个列表(allDone === true
  • 任务数 ≥ 3
  • 没有任何任务内容匹配 /verif/i

满足时,工具结果会追加提示,要求 Claude 在写最终摘要前先 spawn 验证代理。


4.3 Task System:运行时实体管理

Task System 管理着 Claude Code 中所有的"运行中的实体"。

4.3.0 TaskStateBase:所有任务的公共基础

源码位置:src/Task.ts:45

type TaskStateBase = {
  id: string           // 任务 ID(带类型前缀,见下表)
  type: TaskType       // 'local_bash' | 'local_agent' | 'remote_agent' | 'in_process_teammate' | 'local_workflow' | 'monitor_mcp' | 'dream'
  status: TaskStatus   // 'pending' | 'running' | 'completed' | 'failed' | 'killed'
  description: string
  toolUseId?: string   // 触发此任务的 tool_use block ID(用于通知回调)
  startTime: number
  endTime?: number
  totalPausedMs?: number
  outputFile: string   // 磁盘输出文件路径(快照,/clear 后不变)
  outputOffset: number // 已读取字节偏移量(用于增量 delta 读取)
  notified: boolean    // 是否已发送完成通知(防止重复通知)
}

Task ID 前缀表(src/Task.ts:79):

类型 前缀 示例
local_bash b b3f9a2c1
local_agent a a7d8e4f2
remote_agent r r2a1b3c4
in_process_teammate t t5e6f7a8
local_workflow w w9b0c1d2
monitor_mcp m m3e4f5a6
dream d d7b8c9d0
主会话(特殊) s s1a2b3c4

local_agent 和主会话后台化共用 a 前缀(LocalAgentTaskState),但主会话后台化通过 LocalMainSessionTask.ts 中独立的 generateMainSessionTaskId() 使用 s 前缀,与普通子代理区分。

4.3.1 TaskState 联合类型

源码位置:src/tasks/types.ts

// 注意:类型名是 TaskState,不是 Task
type TaskState =
  | LocalShellTaskState        // 长运行 Shell 命令
  | LocalAgentTaskState        // 本地子代理(含后台化主会话)
  | RemoteAgentTaskState       // 远程代理
  | InProcessTeammateTaskState // in-process 队友(团队协作)
  | LocalWorkflowTaskState     // 工作流
  | MonitorMcpTaskState        // MCP 监控
  | DreamTaskState             // 记忆整合子代理(auto-dream)

注意LocalMainSessionTask 不是独立的联合类型成员。它是 LocalAgentTaskState 的子类型:

// src/tasks/LocalMainSessionTask.ts
type LocalMainSessionTaskState = LocalAgentTaskState & { agentType: 'main-session' }

这意味着后台化主会话与普通子代理共用同一个 type: 'local_agent' 标识符,通过 agentType 字段区分。

4.3.2 主会话后台化(LocalMainSessionTask)

这是一个特殊功能:用户按 Ctrl+B 两次可以将当前对话"后台化",然后开始新的对话。后台化后,原查询继续在后台独立运行。

源码位置:src/tasks/LocalMainSessionTask.ts

// LocalMainSessionTaskState 不是独立类型,而是 LocalAgentTaskState 的子类型
type LocalMainSessionTaskState = LocalAgentTaskState & {
  agentType: 'main-session'  // 唯一区分标识
}
// type: 'local_agent'(继承)
// taskId: 's' 前缀(区分普通代理的 'a' 前缀)
// messages?: Message[](继承,存储后台查询的消息历史)
// isBackgrounded: boolean(继承,true = 后台,false = 前台展示中)
// 无独立 transcript 字段

关键函数:

// 注册后台会话任务,返回 { taskId, abortSignal }
registerMainSessionTask(description, setAppState, agentDefinition?, abortController?)

// 启动真正的后台查询(wrap runWithAgentContext + query())
startBackgroundSession({ messages, queryParams, description, setAppState })

// 将后台任务切回前台展示
foregroundMainSessionTask(taskId, setAppState): Message[]

为什么需要隔离的转录路径? 后台任务使用 getAgentTranscriptPath(agentId) 而不是主会话的路径。若用同一路径,/clear 会意外覆盖后台会话数据。后台任务通过 initTaskOutputAsSymlink() 创建软链接,/clear 重链接时不影响后台任务的历史记录。

4.3.3 本地代理任务(LocalAgentTask)

每个子代理对应一个 LocalAgentTaskState

源码位置:src/tasks/LocalAgentTask/LocalAgentTask.tsx:116

type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: string
  prompt: string
  selectedAgent?: AgentDefinition
  agentType: string          // 区分子类型,'main-session' = 后台化主会话
  model?: string
  abortController?: AbortController
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress   // 进度信息(包含 toolUseCount、tokenCount)
  retrieved: boolean         // 结果是否已被取回
  messages?: Message[]       // 代理的消息历史(UI 展示用)
  lastReportedToolCount: number
  lastReportedTokenCount: number
  isBackgrounded: boolean    // false = 前台展示中,true = 后台运行
  pendingMessages: string[]  // SendMessage 排队的消息,在工具轮边界处理
  retain: boolean            // UI 持有此任务(阻止驱逐)
  diskLoaded: boolean        // 是否已从磁盘加载 sidechain JSONL
  evictAfter?: number        // 驱逐时间戳(任务完成后设置)
}

注意:文档中常见误写 toolUseCount 为 LocalAgentTaskState 的直接字段,实际它在 progress.toolUseCount 中。status 字段继承自 TaskStateBase'running' | 'completed' | 'failed' | 'stopped')。

进度追踪通过专门的辅助函数:

// src/tasks/LocalAgentTask/LocalAgentTask.tsx
createProgressTracker()           // 初始化 ProgressTracker(分别追踪 input/output tokens)
updateProgressFromMessage()       // 从 assistant message 累积 token 和工具调用数
getProgressUpdate()               // 生成 AgentProgress 快照(供 UI 消费)
createActivityDescriptionResolver() // 通过 tool.getActivityDescription() 生成人类可读描述

Token 追踪的精妙设计ProgressTracker 分开存储 latestInputTokens(Claude API 累积值,取最新)和 cumulativeOutputTokens(逐轮累加),避免重复计数。

4.3.4 in-process 队友任务(InProcessTeammateTask)

这是 Agent Teams 的核心数据结构:

src/tasks/InProcessTeammateTask/types.ts

type TeammateIdentity = {
  agentId: string        // e.g., "researcher@my-team"
  agentName: string      // e.g., "researcher"
  teamName: string
  color?: string
  planModeRequired: boolean
  parentSessionId: string  // Leader 的 sessionId
}

type InProcessTeammateTaskState = TaskStateBase & {
  type: 'in_process_teammate'
  identity: TeammateIdentity        // 队友身份(存储在 AppState 中的 plain data)
  prompt: string
  model?: string
  selectedAgent?: AgentDefinition
  abortController?: AbortController         // 终止整个队友
  currentWorkAbortController?: AbortController  // 终止当前轮次
  awaitingPlanApproval: boolean             // 是否等待 plan 审批
  permissionMode: PermissionMode            // 独立权限模式(Shift+Tab 切换)
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress
  messages?: Message[]              // UI 展示用(上限 50 条,TEAMMATE_MESSAGES_UI_CAP)
  pendingUserMessages: string[]     // 查看该队友时用户输入的队列消息
  isIdle: boolean                   // 是否处于空闲(等待 leader 指令)
  shutdownRequested: boolean        // 是否已请求关闭
  lastReportedToolCount: number
  lastReportedTokenCount: number
}

常见误解mailbox 字段不存在于 InProcessTeammateTaskState。队友间的通信邮箱存储在运行时上下文 teamContext.inProcessMailboxes(AsyncLocalStorage 中),不在 AppState 里。pendingUserMessages 是用户从 UI 发给该队友的消息队列,与邮箱是两回事。

内存上限设计messages 字段上限 50 条(TEAMMATE_MESSAGES_UI_CAP),超出后从头部截断。原因是生产环境出现过单个 whale session 启动 292 个 agent、内存达 36.8GB 的情况,根本原因正是此字段持有第二份完整消息副本。

4.3.5 记忆整合任务(DreamTask)

源码位置:src/tasks/DreamTask/DreamTask.ts

type DreamTaskState = TaskStateBase & {
  type: 'dream'
  phase: 'starting' | 'updating'    // starting → updating(首个 Edit/Write 后翻转)
  sessionsReviewing: number          // 正在整理的会话数量
  filesTouched: string[]             // 被 Edit/Write 触碰的文件(不完整,仅 pattern-match 到的)
  turns: DreamTurn[]                 // assistant 轮次(工具调用折叠为计数)
  abortController?: AbortController
  priorMtime: number                 // 用于 kill 时回滚 consolidationLock 时间戳
}

DreamTask 是"auto-dream"记忆整合的 UI 表面层。它不改变子代理运行逻辑,只是让原本不可见的 fork agent 在 footer pill 和 Shift+Down 对话框中可见。子代理按 4 阶段 prompt 运行(orient → gather → consolidate → prune),但 DreamTask 不解析阶段,只通过工具调用类型推断 phase。


4.4 任务注册与生命周期框架

源码位置:src/utils/task/framework.ts

4.4.1 registerTask:注册与恢复

registerTask(task: TaskState, setAppState): void

注册一个新任务时,有两条路径:

新建existing === undefined):

  1. 将 task 写入 AppState.tasks[task.id]
  2. 向 SDK 事件队列发出 task_started 事件

恢复/替换existing !== undefined,如 resumeAgentBackground):

  1. 合并保留以下 UI 状态(避免用户正在查看的面板闪烁):
    • retain:UI 持有标记
    • startTime:面板排序稳定性
    • messages:用户刚发送的消息还未落盘
    • diskLoaded:避免重复加载 sidechain JSONL
    • pendingMessages:待处理消息队列
  2. 发出 task_started(防止 SDK 重复计数)

4.4.2 任务完成通知(XML 格式)

子代理/后台任务完成时,通过 enqueuePendingNotification 将 XML 推入消息队列:

<task_notification>
  <task_id>a7d8e4f2</task_id>
  <tool_use_id>toolu_01xxx</tool_use_id>  <!-- 可选 -->
  <output_file>/tmp/.../tasks/a7d8e4f2.output</output_file>
  <status>completed</status>
  <summary>Task "修复登录 bug" completed successfully</summary>
</task_notification>

这段 XML 在下一轮 API 调用前作为 user 消息被注入 messages,使 LLM 感知到后台任务完成。

4.4.3 evictTerminalTask:两级驱逐

任务完成后并不立即从 AppState.tasks 中删除,而是两级驱逐:

任务完成 → status='completed' + notified=true
  │
  ├─ 如果 retain=true 或 evictAfter > Date.now()
  │    → 保留(UI 正在展示,等待 30s grace period 后驱逐)
  │
  └─ 否则 → 立即从 AppState.tasks 中删除(eagerly evict)
               作为保底,generateTaskAttachments() 也会在下次 poll 时驱逐

PANEL_GRACE_MS = 30_000(30秒)是 coordinator panel 中 agent 任务的展示宽限期,确保用户能看到结果后再消失。


4.5 后台 API 任务工具

用户(通过 Claude)可以创建和管理后台任务:

// TaskCreateTool / TaskUpdateTool / TaskStopTool / TaskGetTool / TaskListTool

这些工具允许 Claude 自己创建和监控后台任务,实现真正的异步多任务处理。注意:这套 Task API 工具仅在 TodoV2 模式下启用(即 isTodoV2Enabled() === true),与 TodoWrite 互斥。


4.6 任务输出的磁盘管理

源码位置:src/utils/task/diskOutput.ts

4.6.1 输出文件路径

// 注意:路径不是 .claude/tasks/,而是项目临时目录下的会话子目录
getTaskOutputPath(taskId) → `{projectTempDir}/{sessionId}/tasks/{taskId}.output`

为什么包含 sessionId? 防止同一项目的并发 Claude Code 会话互相踩踏输出文件。路径在首次调用时被 memoize(let _taskOutputDir),/clear 触发 regenerateSessionId() 时不会重新计算,确保跨 /clear 存活的后台任务仍能找到自己的文件。

4.6.2 DiskTaskOutput 写队列

任务输出通过 DiskTaskOutput 类异步写入磁盘:

class DiskTaskOutput {
  append(content: string): void  // 入队,自动触发 drain
  flush(): Promise<void>         // 等待队列清空
  cancel(): void                 // 丢弃队列(任务被 kill 时)
}

核心设计要点

  1. 写队列#queue: string[] 平铺数组,单个 drain 循环消费,chunk 写入后立即可被 GC,避免 .then() 链持有引用导致内存膨胀

  2. 5GB 上限:超限后追加截断标记并停止写入:

    [output truncated: exceeded 5GB disk cap]
    
  3. O_NOFOLLOW 安全:Unix 上用 O_NOFOLLOW flag 打开文件,防止沙箱中的攻击者通过创建软链接将 Claude Code 写入任意宿主文件

  4. 事务追踪:所有 fire-and-forget 异步操作(initTaskOutputevictTaskOutput 等)注册到 _pendingOps: Set<Promise>,测试可通过 allSettled 等待全部完成,防止跨测试的 ENOENT 竞争

4.6.3 增量输出读取(OutputOffset)

TaskStateBase.outputOffset 记录已消费的字节偏移,实现增量读取:

// 仅读取 fromOffset 之后的新内容(最多 8MB)
getTaskOutputDelta(taskId, fromOffset): Promise<{ content: string; newOffset: number }>

framework.ts 中的 generateTaskAttachments() 在每次 poll 时调用 getTaskOutputDelta,将新增内容附加到 task_status attachment 中推送给 LLM,避免重复加载完整输出文件。

4.6.4 关键函数对比

函数 是否删磁盘文件 是否清内存 适用场景
evictTaskOutput(taskId) 是(flush 后清 Map) 任务完成,结果已消费
cleanupTaskOutput(taskId) 彻底清理(测试、取消)
flushTaskOutput(taskId) 读取前确保写入完成

4.7 Cron 定时任务

通过 Cron 工具,可以创建定期自动运行的任务:

// CronCreate / CronDelete / CronList 工具
// 底层通过 src/utils/cron.ts 管理

// 使用示例(Claude 会这样调用):
// CronCreate({ schedule: '0 9 * * 1', command: '/review-pr' })
// → 每周一早上 9 点自动运行 /review-pr

4.8 流程图:任务的完整生命周期

用户输入复杂任务
      │
      ▼
Claude 调用 TodoWrite         ← 规划工作步骤
  todos: [
    { content: '读取代码', status: 'pending' },
    { content: '修改功能', status: 'pending' },
    { content: '运行测试', status: 'pending' },
  ]
      │
      ▼
Claude 开始执行第一步
TodoWrite: { status: 'in_progress' }  ← 标记进行中
      │
      ├─ 如果需要并行工作:
      │    Agent({ subagent_type: 'general-purpose', ... })
      │         │
      │         ▼
      │    创建 LocalAgentTaskState(id: 'a-xxxxxxxx',agentId 同值)
      │    子代理在独立上下文中运行
      │         │
      │         ▼
      │    子代理有自己的 Todo 列表(隔离)
      │
      ▼
每步完成后:
TodoWrite: { status: 'completed' }    ← 标记完成
      │
      ▼
所有步骤完成:
TodoWrite: todos.every(done) → 清空列表 []
      │
      ▼
Agent Loop 检测到无工具调用 → 停止

小结

组件 职责 源码位置
TodoWriteTool 会话内工作计划追踪(交互模式) src/tools/TodoWriteTool/TodoWriteTool.ts
Task API 工具 后台任务创建与管理(SDK/非交互模式) TaskCreateTool / TaskStopTool
TaskStateBase 所有任务的公共字段(含 id、status、outputOffset) src/Task.ts
LocalMainSessionTask 主会话后台化(LocalAgentTaskState 子类型) src/tasks/LocalMainSessionTask.ts
LocalAgentTaskState 子代理生命周期管理 src/tasks/LocalAgentTask/LocalAgentTask.tsx
InProcessTeammateTaskState 团队队友状态(含内存上限 50 条) src/tasks/InProcessTeammateTask/types.ts
DreamTaskState 记忆整合子代理的 UI 表面层 src/tasks/DreamTask/DreamTask.ts
任务框架 registerTask / evictTerminalTask / 通知 XML src/utils/task/framework.ts
磁盘输出管理 DiskTaskOutput 写队列 / 增量读取 / 5GB 上限 src/utils/task/diskOutput.ts
Cron 定时任务 自动化周期任务 CronCreate/Delete/List 工具

关键设计约定总结

约定 说明
LocalMainSessionTask 不是独立类型 它是 LocalAgentTaskState & { agentType: 'main-session' }
TaskState(不是 Task) 联合类型的正确名称
setAppState(不是 updateAppState AppState 更新的实际 API
toolUseCountprogress 不是 LocalAgentTaskState 的直接字段
TodoWrite 与 Task API 互斥 通过 isTodoV2Enabled()isEnabled() 中切换

零成本打造专业域名邮箱:Cloudflare + Gmail 终极配置保姆级全攻略

大家好,我是 Immerse

专注分享 AI 玩法独立开发AI 出海的 AGI 实践者,更多干货欢迎关注公众号 #沉浸式AI 或访问 yaolifeng.com


如果你手上有自己的域名,只是想把 hi@你的域名.com 用起来收发邮件,又不想每年给 Google Workspace 或 Microsoft 365 交钱,这套 Cloudflare Email Routing + Gmail 现在依然够用。

Cloudflare Email Routing 负责把别人发到你域名邮箱的邮件转进 Gmail。Gmail 负责从这个地址回信。

它不是完整企业邮箱,但拿来放在博客、作品集、简历、联系页、独立开发者官网上,已经很够用了。

主要边界

  • 能做到:别人发到你的域名邮箱,你在 Gmail 里收到;你也能在 Gmail 里用这个域名地址发信和回信
  • 做不到:Cloudflare 不给你真正的邮箱空间,也不提供发信服务器;多人协作、共享日历、管理员后台,这套都没有
  • 要注意:Cloudflare 开启 Email Routing 时会接管你这个域名的邮件 MX 记录。如果你这个域名已经在用腾讯企业邮、阿里企业邮、Google Workspace,不要直接点开通

如果你要一个看起来专业、能正常往来邮件的联系邮箱,这套很合适。你要的是团队邮箱系统,那就别省这点钱,直接上正式服务。

准备资料

  1. 一个已经接入 Cloudflare DNS 的域名
  2. 一个你能正常登录的 Gmail 账号
  3. 能开启 Google 两步验证
  4. 一个备用邮箱用来做测试,QQOutlook、另一个 Gmail 都行

部分参考文档:

先把收件打通

先做最小可用版:让别人发到 hi@你的域名.com 的邮件,能够进你的 Gmail。

  1. 登录 Cloudflare,进入你的域名,打开 Email -> Email Routing
  2. 第一次开通时,Cloudflare 会让你添加或替换邮件相关的 MXTXT 记录。这里要认真看一眼,如果面板提示要删除旧的 MX,就代表旧邮箱服务会一起停掉
  3. 开通后,去 Routing rules 里创建一个自定义地址。比如把 hi@你的域名.com 转发到 yourname@gmail.com
  4. Cloudflare 会往你的 Gmail 发一封验证邮件。点掉验证之前,这条转发规则不算真的生效
  5. 验证完成后,用另一个邮箱发一封测试邮件到你的域名邮箱,不要直接用同一个 Gmail 自己给自己发,很多时候会把你绕晕

你看到的正确结果应该是这样的:

  • Gmail 收到了这封信
  • 收件人还是你的域名邮箱,不是裸露的 Gmail
  • Cloudflare 面板里的规则状态是已启用

小建议:有建明确地址,不要一上来开 Catch-allCatch-all 很方便,但也很容易把拼错地址、垃圾邮件、爬虫乱发的邮件一起吞进来。大多数人先用 hicontacthello 这种固定地址就够了。

再把发件打通

收件只是前半段。那怎么用对应的这个域名发出去。

注意,Cloudflare 不负责发信。它只是把邮件转进来。真正往外发信的,还是 Gmail,或者你后面换掉的别家 SMTP 服务。

开 Google 两步验证,生成应用专用密码

Gmail 不会让你直接拿账号密码去配 SMTP。你要用的是应用专用密码。

  1. 打开 Google 账号的安全页,先把两步验证开起来
  2. 开完以后,再进入 应用专用密码
  3. 新建一个密码,名字随便写,比如“域名邮箱”
  4. Google 会给你一串 16 位密码,保存下来

如果找不到“应用密码”入口,可能的两个原因:

  • 你的账号还没开两步验证
  • 你开了 Advanced Protection,Google 官方说明里明确写了,这种模式下应用专用密码不可用

碰到第二种情况,就别死磕了。要么退出 Advanced Protection,要么直接跳到后面的“换第三方 SMTP”方案。

在 Gmail 里添加对应的发件域名

  1. 打开 Gmail,点右上角设置,进入“查看所有设置”
  2. 打开 账号和导入
  3. 在“用这个地址发送邮件”里点“添加其他电子邮件地址”
  4. 名字填你想展示给别人的名字,邮箱填你的域名地址,比如 hi@你的域名.com
  5. Treat as an alias 这个选项,默认保留勾选就行。你本来就是在给同一个人加另一个发件地址
  6. SMTP 配成下面这样
SMTP 服务器:smtp.gmail.com
端口:465
用户名:你的 Gmail 完整地址
密码:刚刚生成的 16 位应用专用密码
加密方式:SSL

如果 465 + SSL 连不上,再试一次 587 + TLS。这是 Google 官方文档里也支持的组合。

  1. 提交后,Gmail 会往你的域名邮箱发一封确认邮件
  2. 这封确认邮件会先到 Cloudflare,再转发进你的 Gmail
  3. 点掉确认链接,或者把验证码填回去,这个发件地址就能用了

配置自动选择回复邮件

注意,如果你想要别人给你发了邮件,你想用对应的邮件回复,这一步设置是必须的。

设置下面这一项

测试整个流程

用其他邮箱,比如 QQ,163 邮箱测试一遍

  1. 用另一个邮箱发信到 hi@你的域名.com
  2. 去 Gmail 收件箱里打开这封信,直接点回复
  3. 发出去之前,看一眼 From,确认显示的是你的域名邮箱
  4. 回到对方邮箱,确认能收到回复

到这一步,主流程就已经跑通了。

对方为什么还会看到 via gmail.com 或者 on behalf of

这不是你哪里配错了,很多时候是 Gmail 这条发信链路本来就有这个边界。

你现在这套方案里:

  • Cloudflare 负责收件转发
  • Gmail 负责真正把邮件发出去

所以在部分收件客户端里,对方可能会看到你的 Gmail 痕迹,比如 via gmail.com,或者 yourname@gmail.com on behalf of hi@你的域名.com

这在个人沟通、博客联系邮箱、独立开发者业务往来里通常问题不大

最容易踩的几个坑

  • Cloudflare 规则建好了但一直收不到邮件。先看目标 Gmail 有没有点验证邮件,没验证就不会真正转发
  • 一开通 Email Routing,原来的企业邮箱突然废了。因为 MX 已经被 Cloudflare 接管了
  • Gmail 一直提示用户名或密码错误。这里填的不是你的 Google 登录密码,是 16 位应用专用密码
  • Google 账号里根本没有应用专用密码。先查两步验证,再查是不是开了 Advanced Protection
  • 你给自己发测试邮件,觉得没收到。先换一个别的邮箱测,自己给自己发最容易被 Gmail 的会话和去重逻辑干扰判断
  • 对方看到 via gmail.com。这通常不是配置错,是 Gmail SMTP 的边界

真要说这套配置最难的地方,也就两个:一个是别把 MX 记录改糊涂,另一个是 Gmail 一定要用应用专用密码。

Claude半个月崩7次!算力不够自己造,强制实名制封

大家好,我是凌览。

如果本文能给你提供启发或帮助,欢迎动动小手指,一键三连(点赞评论转发),给我一些支持和鼓励谢谢。


Claude 最近真的不太稳。

4月15号,Anthropic 状态页一片红,Claude、Claude Code、API 全线飘绿——哦不,全线报错。 堆了 6000 多条故障报告,三小时后恢复正常。

11.png

这已经是4月以来第7次了。

翻一下记录:1号 Opus 4.6 超时、3号 Claude Code 挂了一小时、6号7号连崩、10号集体出错、13号又挂15分钟。半个月七次,谁顶得住。

服务器扛不住

Anthropic 每次都说是"重磅发布后需求暴涨",说白了就是服务器不够用。

Claude Code 和 Claude Cowork 这类产品,跑起来就是 GPU 黑洞——连续工作几小时不停,每次响应都在烧卡。需求涨太快,算力储备没跟上,怎么办?

Anthropic 的答案:自己造芯片。

22.jpeg

越赚钱,账越难算

反直觉的是,Anthropic 其实赚得不少。年化营收突破 300 亿美元,比去年底翻了三倍多,企业市场 73% 选 Claude。

但 Agent 产品太吃算力了。收入涨,成本也在飙。

怎么算账?三招:

第一招:改了企业版定价。 以前纯订阅,现在 20 美元月费加按量计费。用的多的多付,本质是把重度用户单独拎出来收钱。

第二招:Claude Code 订阅加闸。 用 OpenClaw 这类第三方 Agent,得额外交钱。"算力要优先保障自家产品"。

第三招:强制实名验证。 这刀对国内用户特别狠。KYC 需要政府证件加自拍,靠中转、套壳在用的账号,基本没通过空间。账号一封,记录全清。

33.png

巨头都在绕开英伟达

自研芯片,Anthropic 不是第一个。Meta、OpenAI 都在和博合合作造芯。

为什么找博通?定制 ASIC 的 TCO 比通用 GPU 低 30% 到 50%,每瓦性能高一个数量级。

但 ASIC 绑定特定架构,模型一变效率就下来。没有 CUDA 这种成熟生态,实验场景还是得靠英伟达。

44.png

所以各家都是"多云多芯"——谁都不完全绑在某一家。

总结

Claude 频繁宕机,表面是服务器问题,背后是算力瓶颈。营收涨得再快,Agent 产品一跑起来就是在烧钱。

Anthropic 想自研芯片,把命门攥回自己手里。但在芯片造出来之前,只能靠涨价、附加费、实名制强行算账。

说白了,故事讲得再大,芯片还得看别人脸色。

邪修!让显示器支持AI、远程、手势三种控制方式

大家好,我是石小石~


解锁明基RD270Q的新玩法

前不久,明基发布了最新款式的编程系列显示器 RD270Q,很荣幸我获得了优先体验资格。刚开箱,我就被它出众的颜值所吸引。

这款显示器保留了RD系列最核心也是我最喜欢的「编程模式」,而且它还升级到144Hz 高刷 并增加了彩纸模式。这使得在长时间编码下,它能极大缓解眼部疲劳,体验感非常舒适。

接下来,我会分享借助RD270Q配套的DisplayPilot2软件,结合AI与编码,如何玩转显示器的特色功能:

  • 用 Claude code 切换显示器编程模式

  • 用手机远程操控显示器锁屏

  • 用手势实现显示屏亮度调节 (动图帧率问题,图片效果不是很明显)

同时,我会结合长时间的编码体验,验证它是否能成为程序员必备的专业显示器。

显示器控制的核心——Display Pilot 2

无论是通过 AI、手机远程还是手势来控制显示器,核心本质都是依靠电脑上运行的 “脚本” 去操控显示器硬件。借助一些键鼠模拟脚本(如 Node 的robotjs、nut-js,或Python的keyboard),我们可以通过模拟鼠标事件来间接操控软件实现功能,如通过 Node.js 脚本实现自动移动鼠标,并双击启动软件的自动化操作:

对应核心代码如下:

const { mouse,straightTo,Point,Button} = require("@nut-tree-fork/nut-js");
(async () => {
  // 移动鼠标到指定位置
  await mouse.move(straightTo(new Point(10, 10)));
  console.log("鼠标移动完成!");
  // 点击鼠标
  await mouse.doubleClick(Button.LEFT);
  console.log("执行完成!");
})();

可以看出,一些复杂的软件操作,通过模拟鼠标实现还是非常麻烦的,最重要的是脚本几乎无法控制硬件。

幸运的是,明基 RD270Q 自带了配套软件 Display Pilot 2,它可以直接通过软件快速调用显示器的硬件级操作能力,以满足我们编程中的个性化控制需求。参考软件截图,它拥有非常多的显示屏操作功能,且基本都支持通过快捷键操作。

思路到这里就很清晰了:我们完全可以编写脚本,模拟键盘事件触发 Display Pilot 2 的快捷操作,从而间接实现对显示器的控制。

使用Clade code+skills控制显示屏

编程模式切换效果演示

编程模式是明基 RD 系列显示器的特色功能,在深色模式下,显示器会通过硬件级算法强化语法高亮效果,以提升长期编程的舒适度;RD270Q新增的彩纸模式,则能让界面产生类纸感的细腻色彩,满足深度护眼需求。如下图,在黑暗模式下,明基对代码的显示优化非常明显,代码对比更加鲜明,不刺眼。

它还搭载了莱茵认证的抗反射抗面板,即便在强光环境下使用,屏幕也不会刺眼、不产生明显眩光,长时间观看依旧舒适。

在配套软件的基础上,我们能否借助 AI 实现这些显示模式的一键自动切换呢?答案是完全可以。 比如,直接通过 AI 对话下达指令,让显示器自动切换至电子书模式

或是通过指令让 AI 精准调节屏幕亮度、音频大小等参数

原理分析——RD270Q-Opera-skills

Claude Code 为例,我们来实现这一效果。需要明确的是:AI 本身并不能直接操控显示器硬件,即便它能生成脚本,也不知道如何与显示器交互。因此,我们可以通过自定义技能(Skills) —— 比如创建一个 RD270Q-operation-skills,来为 AI 扩展控制显示器的能力。

如果你不了解 Skills,请自行百度。

该技能的项目结构如下:

RD270Q-operation-skills/
├── SKILL.md              # 元数据与指令定义
├── index.js              # 主入口:命令解析与分发
├── package.json          # 项目依赖配置
├── test.js               # 功能测试脚本
├── scripts/              # 底层操作模块
│   ├── keyboard.js       # 键盘快捷键封装
│   └── mouse.js          # 鼠标操作封装
└── references/           # 参考文档
    └── 快捷键表.md        # Display Pilot 2 完整快捷键

整个技能的核心逻辑非常简单: 将 Display Pilot 2 的快捷键功能在代码中做映射,让 AI 可以通过函数调用触发。

示例核心代码(scripts/keyboard.js):

// 键盘快捷键模块 - 封装 Display Pilot 2 所有控制功能
const { keyboard, Key } = require("@computer-use/nut-js");

// 执行快捷键组合
async function executeShortcut(...keys) {
  await keyboard.pressKey(...keys);
  await new Promise(resolve => setTimeout(resolve, 100));
  await keyboard.releaseKey(...keys);
}

// ==================== 色彩模式 ====================
// 循环切换色彩模式 Ctrl+Alt+C
async function cycleColorModes() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.C);
}
// 编程亮模式 Ctrl+Alt+1
async function setCodingLight() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num1);
}
// 编程暗模式 Ctrl+Alt+2
async function setCodingDark() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num2);
}
// 编程纸张模式 Ctrl+Alt+0
async function setCodingPaper() {
  await executeShortcut(Key.LeftControl, Key.LeftAlt, Key.Num0);
}
// ..... 其他快捷操作


// 导出所有方法
module.exports = {
  executeShortcut,
  cycleColorModes,
  setCodingLight,
  setCodingDark,
  setMBook,
  // ...
};

我们只需要在 SKILL.md 中规范好 AI 的调用方式与指令规则,完成整套技能开发后,Claude Code 就拥有了直接操控显示器模式的能力,使用体验直接拉满。

除了编程模式的切换,凡是 Display Pilot 2 能通过快捷键实现的显示器操控功能,这个skills都能完美胜任,甚至像Display Pilot 2屏幕分区这样的高级功能,也能通过控制鼠标来模拟实现。

使用手机远程控制显示屏

很多时候,我们可能临时有事需要离开工位,如果我们突然想锁屏或者想远程控制一下鼠标执行某个简单操作就必须立刻回到工位才行。基于这中场景,实现手机远程控制显示器就非常有意义。

如下图,就是根据明基RD270Q支持的快捷键开发的一个移动端操作界面,并增加了鼠标触摸移动控制功能。

远程锁屏、鼠标控制演示

如果外出忘记锁屏,通过手机实现这个功能非常方便实用。

此外,通过移动端界面的触控区域,我们还能远程操控鼠标移动、直接打开 VSCode 等软件。是不是有点Todesk青春版的感觉?

除此之外,其他快捷操作,如编程模式、亮度调节、夜间保护调节等功能都是支持的,这里也就不一一展示了。

原理分析——websoket+node控制快捷键

远程控制的方案其实非常简单:核心就是跑在本地的一个 Node 脚本,用来模拟键盘、鼠标操作,间接通过 Display Pilot 2 控制显示器。同时启动一个 Web 服务提供移动端操作界面,借助 WebSocket 实现手机与 Node 服务实时通信,最终完成远程控制。简单涞水,就是Web 端通过WebSocket 控制本地端Node服务模拟系统快捷键操作

前端就是一个普通的 Vue 项目 , 页面上放几个控制按钮,点击时通过 WebSocket 向 Node 服务发送对应指令:

function createWebSocketServer(server) {
  const wss = new WebSocket.Server({ server, path: "/ws" });
  wss.on("connection", (ws) => {
    console.log("移动端已连接");
    ws.on("message", async (msg) => {
        const { type, action, params } = JSON.parse(msg);
        // 鼠标操作
        if (type === "mouse") {
          if (action === "move") {
            // 鼠标移动
            await mouse.move(params.x, params.y);
          } else if (action === "click") {
            // 鼠标点击
            await mouse.click(params.button);
          }
        }
        // 键盘操作
        if (type === "keyboard"){
          
        }
    });
  });
}

Node 端主要搭建 WebSocket 服务,接收移动端指令并执行系统操作。

const app = express();
const server = http.createServer(app);

// 初始化 WebSocket 服务
createWebSocketServer(server);

server.listen(PORT, () => {
  console.log(`WS 服务已启动:ws://localhost:${PORT}/ws`);
});;

具体的鼠标移动、键盘快捷键等逻辑,统一封装在 mouse.jskeyboard.js 中,底层依赖node第三方库nut-js实现鼠标和快捷键控制。

使用手势控制显示屏

RD270Q 还有个我觉得特别实用的功能 ——Visual Optimizer 视觉优化。它通过内置光传感器,能根据环境光智能同步调节屏幕亮度与色温,降低屏幕与环境的明暗反差,配合编码深色模式,长时间看代码也更柔和护眼。

不仅如此,我们还可以通过Display Pilot 2进一步调整屏幕亮度,实现个性化需求。基于Display Pilot 2,我们还能实现通过手势控制实现显示器的隔空操作,作为技术创意尝鲜、趣味交互玩具,还是得研究和尝试的。

桌面版的手势识别存在一定技术难度,恰好之前我有写过类似的技术文章:油猴+手势识别:我实现了任意网页隔空控制!索性偷个懒,在网页上实现手势识别用来控制显示器。先看看Demo效果:

  • 左手张开 + 右手滑动,即可调低屏幕亮度(左手握拳 + 右手滑动,即可调高屏幕亮度)

  • 右手握拳,可以实现一键锁屏功能

它的核心实现是基于MediaPipe,这是一个是谷歌开源的跨平台、实时轻量级多媒体机器学习框架,支持 Python、JS 等多种编程语言,借助它能轻松实现桌面级的手势识别功能。

如果你对相关技术感兴趣,可以看看这个实现

Demo:油猴+手势识别:我实现了任意网页隔空控制!

代码:《有趣的手势识别、人脸识别脚本》

Flow 智能工作流

本来我还在琢磨,能不能通过 AI 指令或远程控制,自己搭一套编码时的专属显示方案,比如打开 VS Code 就自动切换到我习惯的亮度、护眼参数等。结果发现 RD270Q 早已自带了 Flow 智能工作流,在 Display Pilot 2 里提前预设好编程、文档、设计等场景后,打开对应软件就能自动切换显示参数,省去反复调节的麻烦,真正实现了 “打开即用” 的智能个性化体验。

结语

从借助 AI 指令、移动端远程控制显示器,到创意十足的手势隔空控制,这篇文章我通过三种个性化玩法,把RD270Q显示器的自定义操控能力发挥到了极致。这些功能实现的核心,离不开Display Pilot 2对显示器本身的 稳定操控能力。

当然,即便不借助这款软件,文中的思路也可以延伸到电脑本身的快捷操作、系统级功能调用上,大家不妨顺着这个方向自行尝试拓展。

写完这篇文章已是凌晨,144Hz 高刷屏搭配显示器的深色编码模式,长时间使用眼部依然舒适,没有出现干涩、疲劳感。实际体验下来,RD270Q 的护眼技术确实做得不错,整体感受很好。

总而言之,新款 RD270Q 不仅保留了核心优势,价格也很有诚意,三千出头,上市期间会更优惠!兄弟们,不用犹豫,这次可以放心冲了。当然,要是追求极致编程体验 RD280URD280UGRD320U也也都是非常不错的选择。

最后, 附上一张深夜codding的图,希望这篇分享能为大家带来一些实用参考。

iOS逆向工程:详细解析ptrace反调试机制的破解方法与实战步骤

Ptrace 提供了一种父进程可以控制子进程运行的机制,并可以检查和改变它的核心image。

  • 它主要用于实现断点调试。

1、一个被跟踪的进程运行中,直到发生一个信号,则进程被中止,并且通知其父进程。 2、在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。

  • 本文采用tweak 的方式进行MSHookFunction

在iOS应用安全中,除了反调试机制,代码混淆也是一种重要的保护手段。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持多种开发平台,有效增加反编译难度。它提供代码混淆、资源文件混淆、调试信息清理等功能,可以帮助开发者保护应用免受逆向工程攻击。

软件环境:Xcode 硬件环境:iPhone5越狱手机、Mac 开发工具:Cycript、LLDB、logos Tweak、hopper、MonkeyDev、AFLEXLoader、dumpdecrypted、debugserver、ssh、class_dump、hook

  • 破解方案

1、运行时期,断点ptrace,直接返回 2、分析如何调用的ptrace,hook ptrace 3、通过tweak,替换disable_gdb函数 4、修改 PT_DENY_ATTACH

I、运行时期,断点ptrace,直接返回

初始化应用程序,而不是运行中附着

iPhone:~ root#  debugserver -x posix *:12345  /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet

初始化程序,目的是从程序入口就开始进行附着,这样我们就可以在一些安全防护代码执行之前,进行破解。

跳过ptrace:过命令thread return直接返回,以跳过函数的逻辑。

 (lldb) br set -n ptrace
 Breakpoint 2: where = libsystem_kernel.dylib`__ptrace, address = 0x00000001966af2d4
 (lldb) br command add 2
 Enter your debugger command(s).  Type 'DONE' to end.
 > thread return
 > c
 > DONE

II、分析如何调用的ptrace,hook ptrace

去掉ptrace的思路

  • 当程序运行后,使用 debugserver *:1234 -a BinaryName 附加进程出现 segmentfault 11 时,一般说明程序内部调用了ptrace 。
  • 为验证是否调用了ptrace 可以 debugserver -x backboard *:1234 /BinaryPath(这里是完整路径),然后下符号断点 b ptrace,c 之后看ptrace第一行代码的位置,然后 p $lr 找到函数返回地址,再根据 image list -o -f 的ASLR偏移,计算出原始地址。最后在 IDA 中找到调用ptrace的代码,分析如何调用的ptrace。
  • 开始hook ptrace。

2.0 准备工作:砸壳

  • 砸壳

blog.csdn.net/z929118967/…

  • 查找app路径
iPhone:~ root# ps -e |grep AlipayWallet
  714 ??         0:26.44 /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
  736 ttys000    0:00.01 grep AlipayWallet

  • 获取NSDocumentDirectory
iPhone:~ root# cycript -p AlipayWallet
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]
#"file:///var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/"

  • scp 拷贝文件
devzkndeMacBook-Pro:decrypted devzkn$ scp /Users/devzkn/Downloads/kevin-software/ios-Reverse_Engineering/dumpdecrypted-master/dumpdecrypted.dylib iphone150:/var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/

devzkndeMacBook-Pro:decrypted devzkn$ scp iphone150:/var/mobile/Containers/Data/Application/89313E1C-76C2-41E3-8ECD-F4BDC1A78524/Documents/AlipayWallet.decrypted /Users/devzkn/decrypted/AlipayWallet

  • class-dump
devzkndeMacBook-Pro:bin devzkn$ class-dump --arch armv7 /Users/devzkn/decrypted/AlipayWallet10.1.8/AlipayWallet.decrypted -H -o /Users/devzkn/decrypted/AlipayWallet10.1.8/head

-查看 bundleIdentifier

iPhone:~ root# cycript -p AlipayWallet
cy# [[NSBundle mainBundle] bundleIdentifier]
@"com.alipay.iphoneclient"

2.1 编写 tweak 分析

%hook DFClientDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    %log();

    // 打印某个类的所有方法的,查看所有方法的执行顺序

     [KNHook hookClass:@"H5WebViewController"];//aluLoginViewController
     [KNHook hookClass:@"TBSDKServer"];//getUaPageName    aluMTopService _tokenLoginInvoker

     [KNHook hookClass:@"TBSDKMTOPServer"];//getUaPageName    aluMTopService _tokenLoginInvoker

    return %orig;
}
%end

2.2 具体步骤

2.2.1 debugserver

iPhone:~ root#  debugserver *:12345 -a AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Attaching to process AlipayWallet...
Segmentation fault: 11

当程序运行后,使用 debugserver *:1234 -a BinaryName 附加进程出现 segmentfault 11 时,一般说明程序内部调用了ptrace 。

iPhone:~ root#  debugserver *:12345 -x backboard /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Segmentation fault: 11

2.2.2 分析如何调用的ptrace

  • debugserver -x
iPhone:~ root# debugserver -x *:12345 /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
error: invalid TYPE for the --launch=TYPE (-x TYPE) option: '*:12345'
Valid values TYPE are:
  auto       Auto-detect the best launch method to use.
  posix      Launch the executable using posix_spawn.
  fork       Launch the executable using fork and exec.
  backboard  Launch the executable through BackBoard Services.

总共有四种类型

debugserver -x backboard *:1234 /var/mobile/...... 把这个backboard改成posix试试

iPhone:~ root#  debugserver -x posix *:12345  /var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-320.2.89
 for armv7.
Listening to port 12345 for a connection from *...

  • 在ptrace上下断点,找到调用ptrace的地方
(lldb)  b ptrace
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

(lldb) c
Process 1657 resuming

  • 关闭Target,重新启动Target
1 location added to breakpoint 1
Process 1657 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x37a13e64 libsystem_kernel.dylib`__ptrace
libsystem_kernel.dylib`__ptrace:
->  0x37a13e64 <+0>:  ldr    r12, [pc, #0x4]           ; <+12>
    0x37a13e68 <+4>:  ldr    r12, [pc, r12]
    0x37a13e6c <+8>:  b      0x37a13e74                ; <+16>
    0x37a13e70 <+12>: rsbeq  r9, r11, #192, #2
Target 0: (AlipayWallet) stopped.
(lldb)  p/x $lr
(unsigned int) $0 = 0x0000bfbb

由此可见ptrace函数在libsystem_kernel.dylib这个动态库中,使用时才进行加载,不是静态放在本地的,所以我们不能简单地去tweak ptrace函数。

(lldb) image list -o -f |grep AlipayWallet
[  0] 0x00000000 /private/var/mobile/Containers/Bundle/Application/A612F542-81EF-456A-A6A0-B23046EF57BA/AlipayWallet.app/AlipayWallet(0x0000000000004000)

所以ptrace的调用者位于0x0000bfbb - 0x00000000 = 0x0000bfbb处,如图所示:

  • 在hopper使用go to add ,快捷键G

Flutter应用代码混淆完整指南:Android与iOS平台配置详解

Flutter中的代码混淆

代码混淆可以隐藏你的Dart代码中的函数和类名,让 反编译 App变得困难。对于更全面的混淆需求,特别是针对iOS IPA文件,可以使用专业工具如IpaGuard,它支持无需源码的代码和资源混淆,兼容Flutter等多种开发平台,有效增加反编译难度。

注:Dart的混淆还没有经过完全的测试,如果发现问题请到GitHub上提 issue 。关于混淆的问题,还可以参考 Stack Overflow 上的这个问题。

Flutter中的混淆配置其实是在Android和iOS端分别配置的。

Android

<ProjectRoot>/android/gradle.properties 文件中添加如下代码:

extra-gen-snapshot-options=--obfuscate

默认情况下,Flutter不会混淆或者缩减Android host,如果你使用了第三方的Java或者Android库,那么你可能需要减小APK体积,或者防止你的App被反编译。

  • Step 1:配置Proguard文件

新建 /android/app/proguard-rules.pro 文件,然后添加如下配置:


#Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.**  { *; }
-keep class io.flutter.util.**  { *; }
-keep class io.flutter.view.**  { *; }
-keep class io.flutter.**  { *; }
-keep class io.flutter.plugins.**  { *; }

上面的配置只保护Flutter库,其他额外的库(比如Firebase)需要你自己添加配置。

  • Step 2:

打开 /android/app/build.gradle 文件,定位到 buildTypes 处,在 release 配置中将 minifiyEnableduseProguard 标志设为true,同时还需要指向Step1中创建的ProGuard文件:


android {
    ...
    buildTypes {
        release {
            signingConfig signingConfigs.debug
            minifyEnabled true
            useProguard true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

注意混淆和缩减无用代码会加长App的编译时间。

iOS

  • Step 1:修改 "build aot"

<ProjectRoot>/packages/flutter_tools/bin/xcode_backend.sh 文件中添加 build aot flag:

${extra_gen_snapshot_options_or_none}

然后定义这个flag:


local extra_gen_snapshot_options_or_none=""
if [[ -n "$EXTRA_GEN_SNAPSHOT_OPTIONS" ]]; then
  extra_gen_snapshot_options_or_none="--extra-gen-snapshot-options=$EXTRA_GEN_SNAPSHOT_OPTIONS"
fi
  • Step 2:应用你的修改

在你的App的根目录下运行以下两条命令:


git commit -am "Enable obfuscation on iOS"
flutter
  • Step 3:更改release配置

<ProjectRoot>/ios/Flutter/Release.xcconfig 中添加下面这行:

EXTRA_GEN_SNAPSHOT_OPTIONS=--obfuscate

对于iOS平台,如果需要更强大的混淆保护,可以考虑使用IpaGuard这样的工具,它可以直接对IPA文件进行混淆加密,支持代码和资源文件的全面混淆,无需源码即可操作,并兼容Flutter应用,提供即时测试功能。

Flutter iOS应用混淆与安全配置详细文档指南

Flutter iOS应用混淆与安全配置文档

概述

本文档详细描述了iOS应用的混淆与安全配置过程。这些配置旨在保护应用代码、API密钥和敏感数据,防止逆向工程和恶意攻击。配置包括 Dart 代码混淆、原生代码混淆、运行时安全检查和数据安全措施。

混淆与安全措施

Dart代码混淆

Flutter提供了内置的代码混淆功能,通过以下参数启用:

--obfuscate --split-debug-info=./symbols
1

这将:

  • 重命名代码中的标识符,使反编译后的代码难以理解
  • 将调试信息分离到单独的文件中,减少发布版本中的可读信息
  • 保留符号信息用于崩溃分析,但不包含在发布版本中

此外,使用像IpaGuard这样的专业混淆工具可以进一步增强应用安全性。IpaGuard是一款强大的iOS IPA文件混淆工具,无需源码即可对代码和资源进行混淆加密,支持Flutter等多种开发平台,有效增加反编译难度。

原生代码混淆与安全

在iOS上,我们通过以下配置增强安全性:

  1. BuildSettings.xcconfig 配置:
// 启用代码混淆和优化
GCC_OPTIMIZATION_LEVEL = s
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
DEAD_CODE_STRIPPING = YES

// 安全设置
ENABLE_STRICT_OBJC_MSGSEND = YES
CLANG_WARN_SUSPICIOUS_MOVE = YES
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES
GCC_NO_COMMON_BLOCKS = YES
STRIP_STYLE = all
STRIP_INSTALLED_PRODUCT = YES
COPY_PHASE_STRIP = YES
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym

// 启用应用传输安全
PRODUCT_SETTINGS_URL_SCHEMES = "$(inherit)"
PRODUCT_SETTINGS_APP_TRANSPORT_SECURITY_ALLOWS_ARBITRARY_LOADS = NO

// 添加其他安全属性
OTHER_LDFLAGS = $(inherited) -Wl,-no_pie
12345678910111213141516171819202122
  1. Xcode构建参数
xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release clean build \
  ENABLE_BITCODE=YES STRIP_INSTALLED_PRODUCT=YES DEPLOYMENT_POSTPROCESSING=YES \
  -sdk iphoneos -allowProvisioningUpdates
123

这些参数确保:

  • 启用Bitcode,允许App Store进一步优化代码
  • 移除不必要的符号和调试信息
  • 进行部署后处理操作,应用额外的优化

运行时安全检查

通过以下Swift代码实现运行时安全检查:

// 检查设备是否已越狱
func isJailbroken() -> Bool {


    #if targetEnvironment(simulator)
    return false
    #else
    // 检查常见的越狱文件路径
    let jailbreakPaths = [
        "/Applications/Cydia.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt",
        "/private/var/lib/apt/",
        "/usr/bin/ssh"
    ]

    for path in jailbreakPaths {


        if FileManager.default.fileExists(atPath: path) {


            return true
        }
    }

    // 检查是否可以写入私有目录
    let stringToWrite = "Jailbreak Test"
    do {


        try stringToWrite.write(toFile: "/private/jailbreak.txt", atomically: true, encoding: .utf8)
        try FileManager.default.removeItem(atPath: "/private/jailbreak.txt")
        return true
    } catch {


        // 无法写入,说明没有越狱
    }

    return false
    #endif
}

// 检查是否连接调试器
func isDebuggerAttached() -> Bool {


    #if DEBUG
    return false
    #else
    var info = kinfo_proc()
    var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
    var size = MemoryLayout<kinfo_proc>.stride
    let status = sysctl(&mib, UInt32(mib.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556

苹果iOS应用开发上架与推广完整教程

苹果应用上架与推广详细指南

作为苹果个人开发者,确保用户能够顺畅地下载并安装自己精心开发的应用,是不可或缺的一环。接下来,我们将深入探讨实现这一目标的一系列核心步骤。

01应用上架准备

> 注册开发者账号

注册开发者账号是发布应用的第一步,必须在苹果开发者官网完成信息填写和费用支付。你需要前往苹果开发者官网,填写包括姓名、联系方式在内的个人真实信息,并支付相应费用。完成注册与身份验证后,你将获得发布应用的权限。

> 确保应用符合准则

在用户能够顺畅地下载并安装应用之前,开发者需要完成一系列的准备工作。这些准备工作的目标是确保应用的下载和安装过程尽可能顺畅,从而提升用户体验。

应用需符合苹果的质量与安全标准,不得包含恶意代码,需具备良好的UI设计和正常功能。苹果对应用的质量、功能及安全性都设有严格标准。应用不得包含恶意代码,必须具备良好的用户界面设计,且功能正常、无显著漏洞。此外,应用还需遵守法律法规,如不得侵犯他人知识产权,不得诱导用户进行不合理付费等。只有满足这些准则的应用,才有可能通过审核并上架供用户下载。

> 选用开发工具与测试

接下来,我们就将详细介绍这些准备工作。在着手构建和测试应用之前,有几个关键步骤需要完成。这些步骤旨在为应用的顺畅下载和安装铺平道路,进而优化用户体验。

使用Xcode进行开发,选择合适的编程语言并在多设备和系统版本上进行全面测试,确保功能完备、兼容性和性能。通常,Xcode被视为开发首选,它提供了从代码编写到编译、调试的一站式服务。依据应用特性,选择如Swift或Objective-C等编程语言。以一个简易笔记应用为例,我们可以运用Swift来构建用户界面,并实现笔记的增删改查功能。务必在不同型号的iOS设备和多种系统版本上进行详尽测试。这包括验证功能的完备性,例如确认笔记应用能否顺利存储和读取数据;确保兼容性,以保证应用在iPhone、iPad等设备上的一致性;以及评估性能,如测试应用的启动速度和响应时间。例如,在iPhone 13与iPhone 8上分别运行笔记应用,观察是否存在布局混乱或功能失效的问题。此外,开发者也可以使用AppUploader等工具来简化iOS证书的申请和管理,支持在Windows、Linux或Mac系统中操作,无需依赖Mac电脑。

02向App Store提交与推广

> 准备元数据与分类选择

接下来,就可以将你的应用提交到App Store了。 提交应用前需准备应用名称、描述、截图及视频,并选择合适的分类以提高用户搜索的精准性

为应用起一个简洁且能体现核心功能的名称,如“速记笔记”,同时撰写详细且吸引人的描述,突出应用特点、功能和优势。此外,还需提供展示关键界面和操作流程的截图及视频预览,使用户能直观了解应用。依此选择合适的分类,如笔记应用可归于“效率”类别。这样能帮助用户在App Store中更精准地搜索到应用。

> 提交审核与推广策略

通过App Store Connect提交审核,积极进行应用推广,包括社交媒体、博主合作及应用内推广,提高用户下载量。

使用Xcode完成应用打包后,通过App Store Connect提交应用进行审核。或者,使用AppUploader工具上传IPA文件到App Store,它支持多平台,比Application Loader更高效,且不携带设备信息。审核过程通常需数日,期间苹果团队将检查应用是否符合各项标准。若审核过程中发现任何问题,将收到反馈通知,需根据反馈进行相应修改后重新提交。同时,积极推广应用也是提高下载量的关键。

  1. 社交媒体推广:借助Twitter、Facebook、Instagram等社交媒体平台,广泛宣传您的应用。您可以分享应用截图、详细的功能介绍以及使用教程等,以此吸引更多潜在用户的关注。例如,在Twitter上发布一系列关于您的笔记应用使用技巧的推文,并附上应用的下载链接。

  2. 与博主和媒体合作:积极联系相关领域的博主、自媒体或科技媒体,向他们介绍您的应用,并努力争取他们的推荐或报道。例如,与专注于效率类应用评测的博主取得联系,邀请他们体验您的应用并分享他们的使用感受。

  3. 应用内推广:如果您还有其他已发布的应用,可以在这些应用内部推广您的新应用,从而引导现有用户下载并体验。

通过上述推广策略,苹果个人开发者可以成功地推动应用的下载安装,并通过广泛的推广活动提高应用的下载量和用户活跃度,从而让您的开发成果得到更多用户的认可和喜爱。

HTTPS超文本传输安全协议全面解析与工作原理

计算机网络---https(超文本传输安全协议)

1. HTTPS的定义与核心定位

HTTPS(HyperText Transfer Protocol Secure,超文本传输安全协议)并非独立于HTTP的“新协议”,而是 HTTP协议与TLS/SSL加密层的结合体——它在HTTP的应用层与TCP传输层(或HTTP/3的QUIC协议层)之间,增加了一套标准化的加密、认证与数据校验机制,核心目标是解决HTTP协议的安全缺陷,保障客户端与服务器之间的通信安全。

从技术本质来看,HTTPS的定位可概括为:

  • 基础不变:仍基于HTTP的请求/响应模型(方法、状态码、头字段完全兼容HTTP),底层传输依赖TCP(HTTP/1.x/2)或UDP(HTTP/3+QUIC);
  • 安全增强:通过TLS/SSL协议实现“加密传输+身份认证+数据防篡改”,弥补HTTP明文传输、无身份校验、数据易被劫持的短板;
  • 行业标配:当前所有涉及隐私(如登录、支付)、敏感数据(如政务、医疗)的场景均强制要求使用HTTPS,主流浏览器(Chrome、Safari)已对HTTP明文网站标记“不安全”警示。

2. HTTPS的核心价值:解决HTTP的三大安全缺陷

要理解HTTPS的必要性,需先明确HTTP的安全漏洞——这些漏洞在公网(如WiFi、运营商网络)中极易被利用,引发隐私泄露、财产损失等风险。HTTPS通过三大核心能力,针对性解决这些问题:

2.1 机密性(Confidentiality):防止数据被窃听

HTTP的致命缺陷是 明文传输:请求/响应内容(如密码、银行卡号、搜索记录)在网络中以“可读文本”形式传输,任何处于传输链路中的设备(如路由器、黑客的抓包工具)都能直接窃取数据。

例如,用户在HTTP网站输入“用户名:test,密码:123456”,抓包工具可直接捕获如下明文:

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=test&password=123456

HTTPS通过 对称加密算法(如AES-256-GCM)解决此问题:客户端与服务器协商出一个“会话密钥”,所有数据传输前用该密钥加密,传输后用同一密钥解密——即使数据被截获,攻击者因无会话密钥,也无法还原为可读文本。

在开发或测试过程中,可以使用抓包工具如 Sniffmaster 来监控和分析 HTTPS 流量,以验证加密效果和排查问题,支持全平台操作且无需复杂代理设置。

2.2 完整性(Integrity):防止数据被篡改

HTTP不校验数据完整性:攻击者可通过“中间人攻击”(MITM)拦截HTTP数据包,修改内容后再转发给目标方,且双方均无法察觉。

典型场景:用户在HTTP电商网站下单,支付金额为100元,攻击者拦截请求后将金额改为10000元,服务器接收并处理修改后的请求,导致用户多支付。

HTTPS通过 哈希算法+数字签名 保证完整性:

  1. 发送方(如服务器)对数据计算哈希值(如SHA-256),生成“数据指纹”;
  2. 用私钥对哈希值签名,生成“数字签名”,与数据一同发送;
  3. 接收方(如客户端)用公钥验证签名,若验证通过,再对接收数据重新计算哈希值;
  4. 对比两次哈希值:一致则数据未被篡改,不一致则数据已被修改,直接丢弃。

2.3 身份认证(Authentication):防止身份被伪造

HTTP无身份认证机制:攻击者可伪造服务器(如搭建虚假WiFi热点,伪装成“商场免费WiFi”),诱骗用户访问虚假网站,窃取用户信息(即“钓鱼攻击”)。

例如,用户想访问 http://www.bank.com,但攻击者伪造了一个域名相似的 http://www.bankk.com,页面样式与真实银行完全一致,用户输入账号密码后,数据直接发送给攻击者。

HTTPS通过 CA证书体系 实现身份认证:服务器需向权威的“证书颁发机构(CA)”申请证书,证书中包含服务器的域名、公钥、CA签名等信息。客户端(如浏览器)接收证书后,会通过内置的“根CA证书”验证服务器证书的合法性——只有验证通过,才确认服务器是真实的,而非伪造。

3. HTTPS的关键组件:TLS/SSL协议与CA证书体系

HTTPS的安全能力依赖两大核心组件:TLS/SSL协议(负责加密与握手)和CA证书(负责身份认证),二者协同工作,构成HTTPS的安全基石。

3.1 TLS/SSL协议:HTTPS的“加密引擎”

TLS(Transport Layer Security,传输层安全协议)是SSL(Secure Sockets Layer)的升级版,当前主流版本为 TLS 1.2TLS 1.3(SSLv3因安全漏洞已被禁用)。TLS协议栈分为三层,自上而下分别为:

协议层 核心功能 关键技术/字段
警报层(Alert) 传递TLS会话中的错误信息(如证书过期、加密套件不支持),触发连接关闭或重试 警报级别(Warning/Fatal)、错误代码(如42表示证书过期)
握手层(Handshake) 协商TLS会话参数(加密套件、会话密钥)、交换证书、验证身份 客户端Hello、服务器Hello、证书、密钥交换消息
记录层(Record) 对应用层数据(HTTP请求/响应)进行分片、压缩、加密、添加认证标签 对称加密算法(AES)、哈希算法(SHA-256)、记录长度
TLS版本演进与核心优化

TLS协议的迭代始终围绕“更安全、更高效”展开,各版本的关键差异如下:

版本 发布时间 核心特点 安全性/性能评价
SSLv3 1996年 首个广泛应用的版本,但存在POODLE漏洞(可被破解加密) 已废弃,完全不安全
TLS 1.0 1999年 修复SSLv3漏洞,引入RSA密钥交换、AES加密 安全性不足(如BEAST漏洞),部分浏览器已禁用
TLS 1.1 2006年 修复BEAST漏洞,改进IV(初始化向量)生成方式 安全性一般,逐步被淘汰
TLS 1.2 2008年 支持SHA-2哈希算法、AEAD加密模式(如AES-GCM),增强完整性与机密性 当前主流版本,安全性可靠
TLS 1.3 2018年 1. 简化握手流程(从4次交互减至3次,支持0-RTT);
2. 移除不安全加密套件;
3. 合并密钥交换与服务器Hello
性能最优、安全性最高,逐步普及

关键优化:TLS 1.3的“0-RTT(Round-Trip Time)”握手可实现“首次重连时无需等待握手完成即可发送数据”,大幅降低延迟——例如,用户第二次访问某网站时,可直接用缓存的会话参数发送请求,无需重新协商密钥。

3.2 CA证书体系:HTTPS的“身份身份证”

CA(Certificate Authority,证书颁发机构)是公认的“网络信任第三方”,负责验证服务器身份并颁发证书。CA证书体系采用“层级信任模型”,确保证书的合法性可追溯,核心构成如下:

证书的核心结构(X.509标准)

一份合法的TLS证书包含以下关键信息(可通过浏览器“查看证书”功能查看):

  • 版本:如X.509 v3;
  • 序列号:CA分配的唯一标识,用于吊销证书;
  • 主体(Subject):证书持有者信息(服务器域名、公司名称等);
  • 公钥信息:服务器的公钥(用于加密会话密钥)及算法(如RSA、ECC);
  • 签发者(Issuer):颁发证书的CA名称(如Let’s Encrypt、Symantec);
  • 有效期:证书生效与过期时间(通常为1-2年,需提前续期);
  • 数字签名:CA用自身私钥对证书内容的哈希值签名,用于客户端验证。
证书的信任链验证

客户端(如浏览器)验证服务器证书时,需通过“信任链”逐层校验,确保证书未被伪造,流程如下:

  1. 客户端接收服务器证书,先验证“服务器证书的签名”——用CA的公钥解密签名,得到哈希值,再对证书内容重新计算哈希值,对比一致则证书内容未被篡改;
  2. 若签发服务器证书的是“中间CA”(而非根CA),则需验证“中间CA证书”的签名,直到追溯至“根CA证书”;
  3. 根CA证书是浏览器/操作系统内置的“信任根”(如微软根CA、苹果根CA),无需额外验证——若根CA未被信任(如自制证书),浏览器会弹出“证书风险”警示,阻止用户访问。
证书的类型与应用场景

根据验证严格程度,CA证书分为三类,适用于不同场景:

  • 域名验证型证书(DV证书):仅验证域名所有权(如通过DNS解析、文件上传验证),无公司信息,仅显示“安全锁”图标,适合个人博客、小型网站,免费(如Let’s Encrypt);
  • 组织验证型证书(OV证书):验证域名所有权+公司主体信息(如营业执照),证书中包含公司名称,适合企业官网、普通电商,需付费(年费数百至数千元);
  • 扩展验证型证书(EV证书):最高级别验证,需提交公司资质、法律文件、实地核验,浏览器地址栏会显示“绿色锁+公司名称”,适合金融、支付、政务网站,费用较高(年费数千元至数万元)。

4. HTTPS的核心工作流程:TLS握手与加密通信

HTTPS的通信过程分为两大阶段: TLS握手阶段(协商会话参数、交换证书、生成会话密钥)和 加密通信阶段(用会话密钥传输HTTP数据)。以下以主流的 TLS 1.2 和优化后的 TLS 1.3 为例,详细拆解流程。

4.1 TLS 1.2握手流程(4次交互)

TLS 1.2握手需客户端与服务器进行4次TCP交互(2个RTT),流程如下:

  1. 客户端Hello(Client Hello)
    • 客户端向服务器发送:支持的TLS版本(如TLS 1.2)、支持的加密套件(如TLS_RSA_WITH_AES_256_GCM_SHA384)、客户端随机数(Client Random,用于后续生成会话密钥)、会话ID(若为重连,携带历史会话ID)。
  2. 服务器Hello + 证书 + 服务器Hello Done
    • 服务器响应:确认TLS版本和加密套件(从客户端支持的列表中选择)、服务器随机数(Server Random,与Client Random共同用于生成密钥)、服务器证书(包含公钥)、“服务器Hello Done”消息(表示服务器已完成初始响应)。
  3. 客户端证书验证 + 密钥交换 + 客户端完成
    • 客户端验证服务器证书:通过信任链校验证书合法性,若验证失败,终止连接并提示风险;
    • 生成预主密钥(Pre-Master Secret):客户端生成一个随机数,用服务器证书中的公钥加密,发送给服务器(即“密钥交换消息”);
    • 生成会话密钥:客户端用Client Random + Server Random + Pre-Master Secret,通过PRF(伪随机函数)生成“会话密钥”(用于后续对称加密);
    • 发送“客户端完成”消息:包含对前序所有握手消息的哈希值+数字签名,证明握手过程未被篡改,同时告知服务器后续将用会话密钥加密数据。
  4. 服务器密钥交换 + 服务器完成
    • 服务器用自身私钥解密“预主密钥”,得到Pre-Master Secret;
    • 用与客户端相同的算法(Client Random + Server Random + Pre-Master Secret)生成会话密钥;
    • 发送“服务器完成”消息:包含对前序握手消息的哈希值+数字签名,告知客户端后续将用会话密钥加密数据。

至此,TLS握手完成,客户端与服务器持有相同的会话密钥,进入加密通信阶段。

4.2 TLS 1.3握手流程(3次交互,优化版)

TLS 1.3通过合并消息、减少交互,将握手压缩至3次TCP交互(1个RTT),流程如下:

  1. 客户端Hello + 密钥交换
    • 客户端发送:支持的TLS 1.3版本、加密套件(仅保留安全套件,如TLS_AES_256_GCM_SHA384)、客户端随机数、“密钥共享”消息(提前生成密钥交换所需的公钥参数,替代TLS 1.2的Pre-Master Secret)。
  2. 服务器Hello + 证书 + 密钥交换 + 服务器完成
    • 服务器响应:确认TLS 1.3版本和加密套件、服务器随机数、服务器证书、“密钥共享”消息(用客户端的公钥参数计算出会话密钥)、“服务器完成”消息(包含握手消息的哈希签名)。
  3. 客户端完成
    • 客户端验证证书后,用服务器的密钥共享参数生成会话密钥;
    • 发送“客户端完成”消息(包含握手消息的哈希签名),进入加密通信阶段。

核心优化:TLS 1.3删除了“服务器Hello Done”消息,将密钥交换与服务器响应合并,减少1次交互;同时支持“0-RTT”模式——若客户端缓存了历史会话参数(如会话票证),首次重连时可直接发送加密的HTTP请求,无需等待握手完成,进一步降低延迟。

4.3 加密通信阶段:HTTP数据的安全传输

TLS握手完成后,客户端与服务器开始用“会话密钥”传输HTTP数据,流程如下:

  1. 应用层(HTTP)生成请求/响应数据(如 GET /index.html HTTP/1.1);
  2. TLS记录层对数据进行处理:
    • 分片:将数据分割为最大16KB的记录;
    • 压缩(可选):用指定算法(如DEFLATE)压缩数据;
    • 加密:用会话密钥通过对称加密算法(如AES-GCM)加密数据;
    • 认证:添加“消息认证码(MAC)”或“认证标签(Tag)”,用于验证数据完整性;
  3. 加密后的记录通过TCP(或QUIC)传输给对方;
  4. 接收方的TLS记录层解密数据、验证完整性、解压缩、重组,再传递给应用层(HTTP)处理。

5. HTTPS的性能优化:打破“HTTPS更慢”的误区

早期HTTPS因TLS握手延迟、加密计算开销,确实比HTTP慢,但随着协议优化(如TLS 1.3)、硬件升级(CPU支持AES指令集)、缓存机制改进,HTTPS的性能已接近HTTP,甚至通过优化可超越HTTP。以下是核心优化方向:

5.1 减少TLS握手延迟

  • 升级TLS 1.3:从2个RTT减至1个RTT,重连时支持0-RTT,延迟降低50%以上;
  • 会话复用
    • 会话ID复用:服务器存储会话参数(如会话密钥),客户端重连时携带会话ID,无需重新协商密钥;
    • 会话票证(TLS Ticket)复用:服务器用密钥加密会话参数,生成“会话票证”发送给客户端,客户端重连时携带票证,服务器解密后直接复用会话,无需存储会话状态(适合分布式服务器);
  • OCSP Stapling(证书状态 stapling):客户端验证证书时,无需向CA服务器查询证书状态(如是否吊销),而是由服务器提前获取CA的OCSP响应,“装订”在证书中一同发送,减少1次CA查询的RTT。

5.2 降低加密计算开销

  • 选择高效加密套件:优先使用AEAD模式的加密套件(如AES-GCM、ChaCha20-Poly1305),兼顾安全与性能——ChaCha20在不支持AES指令集的设备(如低端手机)上性能比AES快3倍;
  • 采用ECC椭圆曲线加密:ECC(Elliptic Curve Cryptography)比RSA更高效,相同安全级别下,ECC的密钥长度更短(如256位ECC≈3072位RSA),加密/解密速度更快,适合移动设备和高并发场景。

5.3 优化证书传输

  • 证书链合并:将服务器证书、中间CA证书合并为一个文件,减少证书传输的TCP连接次数;
  • 证书压缩:使用Brotli或GZIP压缩证书(尤其是EV证书,内容较大),减少传输带宽。

5.4 结合HTTP/3与QUIC

HTTP/3基于QUIC协议(UDP传输),QUIC内置TLS 1.3加密,无需单独的TLS握手——QUIC的“0-RTT”握手可同时完成QUIC连接建立与TLS加密协商,进一步降低延迟;同时QUIC支持“连接迁移”(如手机从WiFi切换到4G,无需重新握手),提升移动场景下的HTTPS体验。

6. HTTPS的常见误区与澄清

误区1:“HTTPS绝对安全,不会被攻击”

HTTPS并非“绝对安全”,仍存在潜在风险,但需满足特定条件:

  • 证书私钥泄露:若服务器私钥被窃取,攻击者可伪造证书,拦截加密数据;
  • 弱加密套件:使用TLS 1.0/1.1或不安全套件(如TLS_RSA_WITH_3DES_EDE_CBC_SHA),可能被破解;
  • 中间人攻击(MITM):若用户信任了伪造的根CA证书(如恶意软件植入伪造根CA),攻击者可拦截并解密HTTPS数据。

应对措施:定期轮换私钥、禁用弱TLS版本和套件、通过安全工具(如Qualys SSL Labs)检测证书配置。此外,工具如 Sniffmaster 提供 HTTPS 抓包功能,支持无需代理的设置,便于开发者进行安全审计和调试。

误区2:“免费CA证书不安全,不如付费证书”

免费证书(如Let’s Encrypt)与付费证书的核心安全机制完全一致,均符合TLS标准,差异仅在验证严格程度和品牌信任度:

  • 安全层面:免费DV证书与付费OV/EV证书均采用相同的加密算法(如AES-256),均可实现机密性、完整性、身份认证;
  • 差异层面:付费证书验证公司信息,适合需要展示企业可信度的场景(如金融),免费证书适合个人或无需展示企业信息的场景(如博客)。

误区3:“HTTPS会增加服务器负载,不适合高并发”

早期HTTPS的加密计算确实会增加服务器CPU负载(约10%-20%),但通过优化可大幅缓解:

  • 硬件优化:使用支持AES-NI指令集的CPU(如Intel Xeon、AMD EPYC),加密计算速度提升10倍以上;
  • 软件优化:Nginx、Apache等服务器已优化TLS处理,支持多进程/多线程并发;
  • 缓存与会话复用:通过会话票证、OCSP Stapling减少重复计算,降低负载。

当前主流互联网公司(如阿里、腾讯)的高并发业务(秒杀、直播)均使用HTTPS,证明其可支撑高并发场景。


HTTPS不仅是“安全协议”,更是当前Web生态的“基础设施”——它通过TLS加密解决了HTTP的安全漏洞,保障了用户隐私与数据安全;同时,HTTPS也是SEO(搜索引擎优化)、PWA(渐进式Web应用)、WebSocket等技术的前提条件,无HTTPS则无法使用这些功能。

未来,HTTPS的发展方向将聚焦于:

  1. TLS 1.3的全面普及:浏览器与服务器逐步淘汰TLS 1.2及以下版本,强制使用更安全、更高效的TLS 1.3;
  2. 量子抗性加密(Post-Quantum Cryptography):研发可抵御量子计算攻击的加密算法(如格密码、哈希签名),避免未来量子计算机破解RSA/ECC加密;
  3. 简化证书管理:通过自动化工具(如Certbot)实现证书的自动申请、续期、部署,降低中小企业使用HTTPS的门槛。

日志诊断 Skill:用 AI + MCP 一键解决BUG|得物技术

一、概述

做后端开发,调 BUG 有一个让人头疼的固定流程:打开日志平台,输入 traceId 或关键词,搜日志;从几十上百条日志里,找到关键的那几条;把日志里的类名、方法名复制出来,去 IDE 里找对应代码;结合代码逻辑,判断哪里出了问题;如果一次找不准,回去再搜日志,再翻代码……

这个过程相对固定,但非常耗时间。每次 BUG 定位,光在日志平台和 IDE 之间来回切换,就能消耗掉大半的时间。

最开始在去年 Q3 想到这个问题的时候,脑子里浮现的第一个方案是:用 Cursor + MCP,把日志平台接进来,再挂一个代码知识库,让 AI 帮我查日志。但这个方案有缺陷 —— 日志查询是「动态的」,它依赖环境、应用、时间范围,没办法静态预置。此外,这样处理没有办法做到比较丝滑地读代码、改代码。

后来开始用 Claude Code,接触到了 Skill 的概念:可以在项目里定义一套自定义命令,描述 AI 应该怎么执行这个命令的每个步骤,于是整个思路变得清晰了。

日志平台有 MCP,Claude Code 有 Skill,两者结合,就能让 AI 自动完成「查日志 → 找关键信息 → 扫描代码 → 定位问题」这整个闭环。然后在 PM 的帮助下,才有了 /log-diagnosis 这个 Skill。

二、日志平台 MCP 是什么

MCP 原理

日志平台推出了基于 MCP(Model Context Protocol)协议的日志查询服务,让 Claude 可以直接调用日志平台的能力,无需人工在日志平台上手动查询。

MCP 本质上是一种标准化的「工具调用协议」,Claude Code 通过 SSE(Server-Sent Events)长连接与 MCP Server 通信,实时获取日志数据。

MCP 环境对照

核心 MCP 工具

鉴权流程

secretKey(日志平台后管申请)
    ↓ acquireTokenTool
accessToken(1小时有效,最多同时存在5个)
    ↓ 携带 accessToken
logsQuery / logSqlQuery / countLogTool ...

secretKey 申请地址:进入日志管理后台 → 日志权限 → 我的应用 → 生成密钥。

三、/log-diagnosis Skill 是什么

Skill 工作原理

log-diagnosis 是一个运行在 Claude Code 里的自定义诊断命令。Claude Code 支持通过 .claude/skills/ 目录定义自定义技能(Skill),以 Markdown 文件描述行为规范,Claude 在收到对应命令时会自动加载并执行。你只需要把 traceId 或告警信息告诉它,剩下的全部交给 AI。完整执行链路如下:

用户输入 /log-diagnosis {环境} {代码分支} {诉求}
    ↓
Claude 加载 .claude/skills/log-diagnosis/SKILL.md
    ↓
读取 .diagnosis/config.json 获取当前环境配置
    ↓
检查 accessToken 是否过期,过期则自动刷新
    ↓
从 traceId 计算日志时间范围(取第9-16位16进制时间戳)
    ↓
调用日志平台 MCP 分页拉取全量日志(最多20页,不遗漏)
    ↓
切换到指定代码分支,结合日志关键词检索代码
    ↓
综合分析:上游日志 + 当前服务日志 + 代码逻辑 → 根因
    ↓
生成诊断报告(飞书文档 or 本地 Markdown)
    ↓
恢复原始代码分支

两种诊断入口

核心能力

  • Token 自动管理:accessToken 过期自动刷新,无需手动维护;
  • 分页全量拉取:自动分页拉完所有日志,禁止只查第一页就下结论(最多 20 页);
  • 跨服务分析:自动识别上下游服务,拉取关联服务日志交叉验证;
  • 代码联动:日志里出现的类名/方法名,直接在代码里精确定位。

queryString 语法规则

# 格式
{field} {操作符} "{值}" {连接符} {field} {操作符} "{值}"
# 操作符
=  : 精确匹配
≈  : 模糊匹配(like)
# 连接符
AND / OR / NOT
# 示例
trace_id"a1b2c3d4e5f6789012345678abcdef01"
trace_id"xxx" AND log_level = "ERROR"
endpoint ≈ "/api/your-endpoint" AND log_level"ERROR"
message ≈ "timeout"

注意:时间范围只通过 start/end 参数控制,不要写在 queryString 中。

四、安装与配置

安装日志平台 MCP

Claude Code

在 Claude Code 命令行中执行,按需安装对应环境:

# 测试环境
claude mcp add --transport sse dw-log-mcp-t1 https://{your-t1-aigw-domain}/api/v1/mcp/log-mcp/sse
# 预发环境
claude mcp add --transport sse dw-log-mcp-pre https://{your-pre-aigw-domain}/api/v1/mcp/log-mcp/sse
# 生产环境
claude mcp add --transport sse dw-log-mcp-prd https://{your-prd-aigw-domain}/api/v1/mcp/log-mcp/sse

安装后重启 Claude Code,执行 /mcp 确认连接状态正常。

Cursor

  1. 打开 Cursor Setting;

  2. 点击 Tools & MCP,添加 MCP Server;

  3. 添加 URL,MCP Server 名称任意。

建议按需安装 MCP Server,避免额外消耗 token,示例配置:

{
  "mcpServers": {
    "dw-log-mcp-t1": {
      "url": "https://{your-t1-aigw-domain}/api/v1/mcp/log-mcp/sse"
    },
    "dw-log-mcp-pre": {
      "url": "https://{your-pre-aigw-domain}/api/v1/mcp/log-mcp/sse"
    },
    "dw-log-mcp-prd": {
      "url": "https://{your-prd-aigw-domain}/api/v1/mcp/log-mcp/sse"
    },
    "dw-log-mcp-oversea-prd": {
      "url": "https://{your-oversea-aigw-domain}/api/v1/mcp/log-mcp/sse"
    }
  }
}

4. 返回设置,就可以看到已经连接上。

安装 /log-diagnosis Skill

将 log-diagnosis 目录放到项目的对应目录下:

Claude Code

your-project/
└── .claude/
    └── skills/
        └── log-diagnosis/
            ├── SKILL.md        # 技能行为规范(核心)
            ├── README.md       # 使用说明
            └── reference.md   # 附录:时间脚本、queryString 示例等

Cursor

your-project/
└── .cursor/
    └── skills/
        └── log-diagnosis/
            ├── SKILL.md        # 技能行为规范(核心)
            ├── README.md       # 使用说明
            └── reference.md   # 附录:时间脚本、queryString 示例等

配置 .diagnosis/config.json

首次运行会自动引导创建 (直接调用 /log-diagnosis,Skill 会一步步指示你给出 secret key),也可手动在项目根目录创建 .diagnosis/config.json:

your-project/
└── .cursor/
    └── skills/
        └── log-diagnosis/
            ├── SKILL.md        # 技能行为规范(核心)
            ├── README.md       # 使用说明
            └── reference.md   # 附录:时间脚本、queryString 示例等

字段说明:

secretKey:唯一需要人工填写的字段,在日志平台后管申请;

accessToken:首次使用时由 AI 自动调用 acquireTokenTool 获取,过期自动刷新;

accessTokenExpireAt:从 acquireTokenTool 返回值自动填充;

fields:调用 logFields 工具自动获取。

五、使用方式

命令格式:

/log-diagnosis {环境} {代码分支(可选)} {诉求描述}

参数说明:

  • {环境}:T1 / PRE / PRD(按实际环境标识填写);
  • {代码分支}:可选,留空则使用当前分支;
  • {诉求描述}:包含 traceId 或告警信息的问题描述,用自然语言书写即可。

示例:

# 用 traceId 定位接口异常
/log-diagnosis T1 feature/your-branch trace_id: "your-trace" 为什么最终没有返回数据
# 用告警信息分析错误原因
/log-diagnosis PRD master 告警详情:【接口:YourService/yourMethod】【业务码:10002000】【业务码消息:系统异常,请稍后重试】帮我分析问题可能性

一行命令,AI 全程接管,几分钟内给出根因分析。

六、实战案例:一个隐蔽的 SQL BUG

背景

某搜索接口在测试环境反馈没有返回数据。拿到 traceId,直接执行:

/log-diagnosis T1 feature/your-branch trace_id: "your-trace" 为什么最终没有返回数据

← 就这一句话,接下来全部交给 AI。

AI 自动拉取日志

Skill 触发后,AI 自动完成:

  • 从 traceId 推算出日志时间范围(2026-02-27 全天);
  • 检查 accessToken 已过期,自动刷新;
  • 调用日志平台 MCP,分 2 页拉取完整日志,共 73 条。

请求入参(从日志自动提取):

{
  "assembleByOrg": true,
  "channelType": "MANUAL",
  "orderNo": "your-order-no",
  "status": 1,
  "ticketNo": "your-ticket-no"
}

AI 还原完整调用链路

AI 自动识别出关键节点:resultList is empty,SQL 查询返回了空结果。问题在 DB 层,而不在业务逻辑层。

AI 提取组装后的查询 DTO

从日志中提取到 toSearchDTO 组装结果:

{
  "channelType": "MANUAL",
  "customerTag": 1,
  "deliveryMode": "某配送方式",
  "orderStatus": "8010",
  "orderType": "0",
  "productCategoryIds": [29],
  "status": 1,
  "ticketSource": 67,
  "ticketTypeId": 5802
}

AI 从日志中提取实际执行的 SQL 发现根因

ORM 框架在日志中打印了实际执行的 SQL,AI 直接读取并分析:

SELECT a.id, a.pid, a.name, a.mode, a.status, a.org_id, a.org_ids,
       a.ticket_group_id, a.tenant_id, a.is_del, a.channel_types
FROM your_type_table a
LEFT JOIN your_relation_table b
    ON b.tenant_id = 1 AND a.id = b.type_id AND b.type = 3 AND b.is_del = 0
WHERE a.tenant_id = 1 AND a.mode = 2 AND a.is_del = 0
  AND a.status = 1
  AND (a.channel_types IS NULL OR a.channel_types = '' OR FIND_IN_SET('MANUAL', a.channel_types) > 0)
  AND (b.root_id is null or b.root_id in (29))
  AND (a.order_types IS NULL OR a.order_types = '' OR FIND_IN_SET('0', a.order_types) > 0)
  AND (a.order_statuses IS NULL OR a.order_statuses = '' OR FIND_IN_SET('8010', a.order_statuses) > 0)
  AND (a.delivery_modes IS NULL OR a.delivery_modes = '' OR FIND_IN_SET('某配送方式', a.delivery_modes) > 0)
  AND (a.ticket_sources IS NULL OR a.ticket_sources = '' OR FIND_IN_SET(67, a.ticket_sources) > 0)
  AND (a.customer_tag IS NULL OR a.customer_tag = 1)   ← BUG 在此

AI 发现:其他字段都处理了 IS NULL 和 = ''(空字符串代表 “不限制”)两种情况,唯独 customer_tag 只判断了 IS NULL,遗漏了空字符串 '' 的情况。

SQL 语义对比:

-- 其他字段(正确):IS NULL 和 '' 都处理了
AND (a.order_types IS NULL OR a.order_types'' OR FIND_IN_SET('0', a.order_types) > 0)
AND (a.delivery_modes IS NULL OR a.delivery_modes'' OR FIND_IN_SET('某配送方式', a.delivery_modes) > 0)
AND (a.ticket_sources IS NULL OR a.ticket_sources'' OR FIND_IN_SET(67, a.ticket_sources) > 0)
-- customer_tag(遗漏了 = '' 的判断)← BUG
AND (a.customer_tag IS NULL OR a.customer_tag1)

DB 中现有的数据,customer_tag 字段都存的是空字符串(未配置),按业务语义本应匹配所有请求,却因为这个遗漏被全部过滤掉了。

AI 定位代码,给出修复方案

AI 在代码中直接找到对应的 MyBatis Mapper XML:

<!-- 问题代码 -->
<if test="customerTag != null">
    and (a.customer_tag IS NULL OR a.customer_tag = #{customerTag})
</if>
<!-- 修复后 -->
<if test="customerTag != null">
    and (a.customer_tag IS NULL OR a.customer_tag = '' OR a.customer_tag = #{customerTag})
</if>

效率对比

这个 BUG 的隐蔽性在于:SQL 语法正确,逻辑上也「看起来」没问题——只有对比了其他字段的写法,才能发现 customer_tag 独自遗漏了空字符串的处理。这类细节差异,人工排查很容易忽略,AI 反而很擅长。

七、诊断效率关键点

  • 有 traceId 时优先用 traceId 拉日志,可精准获取单次请求的完整链路,比关键词搜索精确得多;
  • 关注关键日志节点:toSearchDTO finished / search begins / resultList is empty / search finished 等,快速判断数据在哪一层丢失;
  • SQL 打印日志(ORM 框架输出)是黄金线索,直接反映最终执行的查询条件,AI 能从中发现肉眼难以察觉的差异;
  • 分页必须拉完:日志平台一次只返回部分数据,AI 会严格执行分页直到取完,确保不遗漏关键日志。

八、总结

核心思路:用「协议 + 规范」让 AI 接管固定流程:

这篇文章的本质,是一次对重复性工程劳动的自动化尝试。调 BUG 的过程——查日志、提取关键信息、找代码、分析原因——逻辑固定,步骤繁琐,但并不需要太多创造性思维。这类工作恰好是 AI 最擅长接管的。

实现这个闭环,靠的是两个关键组合:

  • MCP:让 AI 能够调用外部系统(日志平台),突破了「AI 只能处理静态上下文」的限制,实现了对动态数据的实时获取。
  • Skill:给 AI 一份行为规范,告诉它每一步该怎么做、先做什么后做什么、遇到什么情况怎么处理,把「一次性对话」变成「可复用的工程化能力」。

两者缺一不可。只有 MCP,AI 能查日志但不知道怎么系统地分析;只有 Skill,AI 有流程但没有数据来源。组合起来,才形成了真正可落地的闭环。

值得借鉴的地方:

识别「固定流程」是自动化的起点:不是所有工作都适合 AI 接管,但凡是「步骤固定、信息来源明确、输出格式可预期」的工作,都值得尝试用 Skill + MCP 的方式来自动化。排查 BUG 是一个典型,类似的还有:代码审查、性能分析报告生成、告警巡检等。

Skill 的本质是「给 AI 写操作手册」:Skill 文件不是在「训练模型」,而是在给 AI 一份清晰的 SOP。写得越细、约束越明确(比如「禁止只查第一页就下结论」「必须分页拉完所有数据」),AI 的执行质量越稳定。这和写给人看的文档本质上是一回事。

AI 擅长发现「横向对比」类的 BUG:本文的案例揭示了一个有意思的规律:AI 在处理「同类字段逻辑不一致」这类问题时,表现往往比人工更好。原因在于 AI 没有「先入为主」的经验偏见,不会因为「这段代码看起来没问题」就跳过,它会对所有字段做同等的审查。

最后说一句:AI 时代,工程师的核心竞争力不只是「能写代码」,更是「能把自己的经验和流程转化成可复用的 AI 能力」。/log-diagnosis 是一次小小的尝试,但背后的思路,值得在更多场景里延伸。

往期回顾

1.Redis 自动化运维最佳实践|得物技术

2.Claude在得物App数仓的深度集成与效能演进

3.Claude Code + OpenSpec 正在加速 AICoding 落地:从模型博弈到工程化的范式转移|得物技术

4.大禹平台:流批一体离线Dump平台的设计与应用|得物技术

5.基于 Cursor Agent 的流水线 AI CR 实践|得物技术

文 /阿程

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

iOS 多技术栈混淆实现,跨平台 App 混淆拆解与组合

当项目从单一 iOS 原生扩展到 Flutter、React Native 或 Unity 时,混淆这件事会变得复杂。原因不在于工具少,而是每一层代码完全不同

  • Swift / Objective-C → Mach-O 符号
  • Flutter → Dart AOT + assets
  • React Native → JS bundle
  • Unity → DLL + 资源

如果只用一种 iOS 混淆工具,通常只能覆盖其中一部分。


不同技术栈暴露的信息完全不一样

拿一个混合项目举例(Flutter + 原生 + H5),解包 IPA 后可以看到:

AppBinary          // 原生代码
flutter_assets/    // Dart + 资源
main.jsbundle      // JS 逻辑
assets/            // 图片与配置

每一层的“暴露方式”不同:

技术 可被读取的内容
Swift / OC 类名、方法名、参数
Flutter Dart 符号(部分)、资源路径
React Native JS 逻辑
Unity DLL + AssetBundle

这意味着混淆必须分层处理。


原生层:符号混淆(iOS 混淆工具核心能力)

先看最传统的一层:Swift / Objective-C。

检查方式:

strings AppBinary | grep Controller

如果看到:

HomeViewController
PaymentManager

说明符号未处理。


处理方式

使用 Ipa Guard 这类 IPA 级别的 iOS 混淆工具:

  • 导入 IPA
  • 进入代码模块
  • 勾选类 / 方法 / 参数

执行后:

PaymentManager → a82kd3

这一步直接改变 Mach-O 符号,是跨平台项目中最“统一”的一层处理。


Flutter 层:Dart 混淆 + IPA 补充

Flutter 提供内置混淆:

flutter build ios --obfuscate --split-debug-info=./symbols

执行后:

  • Dart 符号被替换
  • 生成符号映射

但 IPA 解包后仍然可以看到:

assets/images/banner.png
config/app.json

补充处理

使用 Ipa Guard 的资源模块:

banner.png → x92kd.png
app.json → a83ks.json

这样 Dart 层 + 资源层同时处理。


React Native:JS 混淆 + 文件重命名

React Native 的关键在 JS bundle:

main.jsbundle

直接打开可以读。


处理步骤

1)压缩 JS:

terser main.js -o main.min.js

2)替换 bundle

3)用 Ipa Guard 修改文件名称:

main.jsbundle → k39sd.bundle

这样:

  • 内容不可读
  • 路径无语义

Unity:资源与 DLL 的组合处理

Unity 项目解包后:

Data/Managed/Assembly-CSharp.dll
Data/Resources/

DLL 可以被反编译,资源路径也能推断逻辑。


处理方式

  • 使用 Unity 构建参数减少符号
  • 在 IPA 层用 Ipa Guard 处理资源名称
  • 修改资源 MD5

例如:

level1.assetbundle → a82kd.bundle

统一处理:资源指纹与结构差异

跨平台项目中,资源重复是一个常见问题。

例如多个 App 使用同一套 UI:

banner.png
icon.png

即使改名,内容仍然一致。

处理方式

在 Ipa Guard 中开启 MD5 修改:

md5 banner.png

处理前后不同。

这一步可以打散资源特征。


七、调试信息清理

检查:

strings AppBinary | grep NSLog

或:

strings AppBinary | grep Flutter

如果存在调试信息,可以统一清理。Ipa Guard 支持删除调试符号和部分日志字符串。


签名工具

无论哪个技术栈,只要修改 IPA,就必须重新签名。

可以使用:

kxsign sign app.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

本地执行 IPA 混淆 无需上传致云端且不修改工程的方案

在很多团队里,混淆这一步常常被外包给在线加固服务:上传 IPA,等结果,下载再签名。流程确实顺手,但当项目涉及商业逻辑或私有算法时,这种方式总让人有点不踏实——完整的二进制、资源、接口结构都离开了本地环境。

后来我们把这一步彻底改成本地执行,不上传任何文件不改工程源码只操作已编译好的 IPA


一、先确认 IPA 当前长什么样

把构建好的 IPA 复制一份并解压:

unzip app.ipa

进入目录:

Payload/App.app

检查三个位置:

1)二进制可读信息

strings AppBinary | head

如果能看到:

UserManager
PaymentService
VipController

说明符号没有做处理。


2)资源目录结构

assets/images/vip_banner.png
config/payment.json

路径本身已经带有业务语义。


3)前端资源

main.jsbundle
index.html

这些文件如果未压缩,直接可读。


二、本地链路的核心思路

整个流程不依赖任何远程服务,结构如下:

IPA 文件
→ 本地解析
→ 本地混淆
→ 本地资源处理
→ 本地签名
→ 本地测试

关键在于:所有操作都发生在开发机器上。


先处理 JS / H5(如果存在)

如果项目中包含 WebView 或 React Native 模块,可以在 IPA 处理前压缩脚本。

例如:

terser main.js -o main.min.js

或者:

uglifyjs page.js -o page.min.js

压缩后再替换回 IPA 资源目录。

这样可以先降低 JS 层的可读性。


在本地执行 IPA 符号混淆

这一步是核心。

使用 Ipa Guard 这类本地运行的 IPA 混淆工具,可以直接处理 Mach-O 文件,而不需要源码。

操作过程:

  • 打开工具
  • 导入 IPA
  • 进入「代码模块」

可以看到:

OC 类
Swift 类
OC 方法
Swift 方法

在列表中选择需要处理的符号,例如:

UserManager
PaymentHandler
VipService

执行后:

UserManager → k39sd2

整个过程在本地完成,不会上传任何数据。


资源文件本地重写

继续在 Ipa Guard 的资源模块中操作。

勾选:

  • 图片
  • JSON
  • HTML
  • JS

执行后:

vip_banner.png → a82kd.png
payment.json → x92ks.json

工具会自动更新引用路径。

这一层的作用是让资源结构失去语义。


改变资源指纹(避免“同源识别”)

如果多个应用使用相同资源,文件内容会成为识别依据。

在 Ipa Guard 中开启 MD5 修改:

md5 banner.png

处理前后不同。

文件视觉效果不变,但指纹已经改变。


清理调试信息

检查:

strings AppBinary | grep NSLog

如果存在日志或调试字符串,可以在混淆阶段删除。

Ipa Guard 提供调试信息清理选项。


补充一个“简单校验机制”

为了避免 IPA 被二次篡改,可以在原生层加入简单校验:

  • 计算关键文件 hash
  • 启动时验证

例如:

if hash != expected { exit(0) }

这一步不依赖混淆工具,但可以作为补充。


本地完成签名与安装

混淆后 IPA 已失去原签名,需要重新签名。

可以使用:

kxsign sign app.ipa \
-c cert.p12 \
-p password \
-m dev.mobileprovision \
-z test.ipa \
-i

或者直接在 Ipa Guard 中配置证书。

连接设备后可以直接安装。


验证结果(这一步不能跳)

安装后重点检查:

  • 页面是否正常
  • 资源是否加载
  • 动态调用是否正常
  • WebView 内容是否可用

如果出现异常,通常是:

  • 某些符号被误混淆
  • 某些资源路径未正确更新

把 IPA 混淆完全放在本地执行,并不只是“更安全”的选择,它还带来一个实际好处:每一步都可控、可调试、可回滚。相比上传到云端处理,本地流程更适合需要长期维护的项目。

搭建一个云端Skills系统,随时随地记录TikTok爆款

最近 Claude Skills 很火。

但我观察了一圈,发现大家都在陷入一种“开发者的自嗨”。

绝大多数 Skills 的应用场景都被死死锁在 IDE 里,锁在开发者的电脑前。

这叫开发提效,不叫业务提效。

真正的业务发生在移动端,发生在你通勤、吃饭、甚至躺在床上刷 TikTok 的时候。

如果你的 AI 能力必须打开电脑、输入命令行才能调用,那它的时空效率就是零。

于是我抛弃本地的 Claude Code,基于 OpenHands 做了一套云端 Skills 系统。

效果极其简单粗暴:

我在刷 TikTok,看到一个爆款视频,点击复制链接,敲击 iPhone 背面三下。

wxv_4355007050494509070

20 秒后,我的飞书多维表格里自动新增了一行数据。

Image

这行数据包含了:这个视频的无水印文件、Gemini 拆解的镜头语言分析、爆款原因推导,以及一套可直接复用的 AI 视频生成提示词。

全过程我不需要打开电脑,不需要切换 APP,不需要等待。

这就是我今天要聊的:如何用 OpenHands + Skills + iOS 快捷指令,构建一套真正落地的业务自动化系统。

01 为什么 Claude Code 在业务侧是伪需求

先厘清两个概念:OpenHands 和 Claude Code。

Claude Code 是 Anthropic 官方推出的命令行工具,它是一个嵌入在你本地终端里的结对程序员。它的 Skills 本质是上下文记忆和本地工具接口。

它的优势是懂你的代码规范,能直接改你电脑里的文件。

但它有一个对于业务场景的致命弱点:它必须依附于你的会话,你不在,它就不动。

它是一个副驾驶(Copilot)。

而 OpenHands(前身 OpenDevin)是一个开源的、自主的 AI 软件工程师。它运行在 Docker 容器里,是一个独立的服务端 Agent。

Image

openhands.dev/

它是一个可以被封装成 API 服务的数字员工。

我看重 OpenHands 的核心理由只有一个:它可以 24 小时在线,并且可以通过 API 远程唤醒。

我做的这个 TikTok 分析系统,本质就是把 OpenHands 部署在服务器上,通过 FastAPI 暴露接口。

Claude Code 是给你用的工具;OpenHands 是你雇佣的、随时待命的员工。

🐵

小提示:FastAPI 的服务地址后加/docs就是文档了

02 业务视角:从 刷视频 到「数据入库」的闭环

对于做出海营销和短视频矩阵的朋友,拆解爆款是每天的必修课。

传统的流程极其反人类:

  1. 1. 手机刷到视频,点收藏。
  2. 2. 晚上回家打开电脑,把链接导出来。
  3. 3. 找第三方工具去水印下载。
  4. 4. 把视频传给 Gemini 分析。
  5. 5. 人工把分析结果复制粘贴到 Excel 或飞书。

这个链路太长,断点太多。任何需要延迟满足的流程,最终都会变成不了了之。

我的远程 Skills 方案,把这个流程压缩到了极致。

整个逻辑是这样的:

Image

用户端(前端)

利用 iOS 自带的快捷指令 + 背部轻点功能。

  • 动作:获取剪贴板内容(TikTok 链接)。
  • 触发:发送 HTTP POST 请求给我的服务器。
  • 反馈:手机震动一下,表示任务已接收。

Image

Image

服务端(后端)

OpenHands 接收到请求后,自主执行以下 Skills:

  1. Playwright Skill:

启动无头浏览器。这里有一个技术难点,TikTok 的反爬虫机制非常严格。如果用普通的 request 请求,成功率几乎为零。OpenHands 调用 Playwright 模拟真实浏览器行为,绕过 blob 协议,抓取真实的 MP4 视频流。这种方式的下载成功率稳定在 70%-80%

  1. Gemini Skill:

视频下载后,调用Gemini 2.5 Flash,快且便宜。它不只是看,它是理解。它可以识别拍摄角度(俯拍/特写)、运镜方式(推拉摇移)、BGM 节奏点、色彩心理学。

  1. Feishu Skill:

将清洗好的结构化数据(JSON),通过 API 写入飞书多维表格。

结果:

当你刷完半小时视频,打开飞书,几十个爆款视频的深度分析报告已经整整齐齐躺在那里了。

这才是 AI 赋能业务的本质:隐形化。

Image

Openhands 的 Skills 文档:

docs.openhands.dev/sdk/guides/…

03 举一反三:跨境电商的远程 Skills 玩法

这套架构的核心逻辑是:移动端触发 -> 服务端 API -> OpenHands 执行复杂 Skills -> 结果回传。

这个逻辑在出海业务里有无限的延展性。

我给几个具体的场景,你们可以拿去直接落地。

场景一:竞品独立站监控

  • 动作:在手机浏览器看到竞品的 Shopify 店铺,复制链接,触发 Shortcut。
  • Skills:OpenHands 调起爬虫 Skill 扫描该站点的新品上架情况、价格策略,并调用 SEO Skill 分析其关键词布局。
  • 产出:一份竞品分析简报直接推送到你的 Slack 或 钉钉。

场景二:亚马逊差评自动预警与回复草稿

  • 动作:系统监控到差评(自动触发,无需人工)。
  • Skills:OpenHands 读取差评内容,结合历史客服知识库 Skill,分析用户情绪,并模仿金牌客服的语气撰写 3 个版本的回复邮件。
  • 产出:草稿进入审核流,你只需要在手机上点批准。

场景三:广告素材批量生产

  • 动作:上传一张产品图到指定文件夹。
  • Skills:OpenHands 识别产品特征,调用 Midjourney 或 Runway 的 API,结合当下的流行趋势 Skill,自动生成 10 种不同风格的广告背景图。
  • 产出:素材自动同步到 Google Drive 供投放团队筛选。

04 为什么非要用 Agent Skills?写个 Python 脚本不行吗?

这是很多技术出身的朋友最容易陷入的误区。

你这个功能,我写个 Python 脚本 + 定时任务也能跑,为什么要搞这么复杂的 OpenHands Skills?

因为业务逻辑是流动的,而脚本是僵死的。

如果你写死了一个 Python 脚本:

  • 当 TikTok 的前端代码更新了 class 名,脚本报错,你得去修。
  • 当飞书的 API 接口变动,脚本报错,你得去修。
  • 当 Gemini 的模型参数调整,脚本报错,你得去修。

但在 OpenHands Skills 的架构下,我们定义的不是步骤,而是目标。

在我的 Skill 定义里,我告诉 OpenHands:你的任务是下载这个页面上的视频,如果常规方法失败,尝试模拟用户滚动;如果还失败,检查是否有验证码并尝试通过。

OpenHands 作为一个 Agent,它具备自主决策和自我修复的能力。

  • 它发现 TikTok 改了页面结构?它会尝试用视觉识别去定位播放按钮。
  • 它发现 API 报错?它会自主查阅文档或尝试备用节点。

在跨境出海这种平台规则朝令夕改的环境下,维护脚本的成本极高。

我们需要的是一个能够理解意图并自主寻找路径的智能体。

05 思路打开,Agentic Skills 的高级玩法

文章到这里,这套远程 Skills 系统的雏形已经搭建完毕。

但如果你觉得这就结束了,那你就小看了 Agentic Skills 的天花板。

我们现在的架构是“一个请求触发一个 Skill”,但这只是冰山一角。真正的威力在于 Multi-Skill Orchestration(多技能编排)。

  1. 1. Skill Chain(技能链)与递归调用

OpenHands 的 Skill 本质是可执行的逻辑单元。我们可以像写代码一样,让 Skill A 去调用 Skill B。

  • 比如定义一个 Base-Skill:只负责做基础的数据清洗。
  • 再定义一个 Pro-Skill:先调用 Base-Skill 处理数据,再把结果传给 Analysis-Skill,最后调用 Report-Skill 生成报告。

你可以构建一个自我迭代的 Agent。让它先写一段代码(Coding Skill),然后自己运行测试(Testing Skill),如果报错,递归调用 Coding Skill 进行修复,直到测试通过。

  1. 混合云架构(Hybrid Agent Architecture)

OpenHands 运行在 Docker 里,这意味着它可以部署在任何地方。

  • 私有化部署:对于涉及公司财务、用户隐私的数据,你可以把 OpenHands 部署在公司内网服务器上。
  • 公有云调用:对于需要访问外网(如 TikTok 下载、竞品分析)的任务,部署在 AWS 或 Vercel 上。

这样,通过 API 网关,你可以指挥内网的 Agent 去调用外网的 Agent,实现数据在安全域和互联网域之间的智能流转。

  1. “人机回环”的异步交互

谁说 API 只有“请求-响应”这一种模式? 在我的系统中,有些复杂任务(如竞品深度调研)可能需要运行 30 分钟。

  • 流程设计:OpenHands 接收任务 -> 立即返回 TaskID -> 后台异步执行。
  • 关键点:当 Agent 遇到无法决策的卡点(例如:这个验证码我解不开,或者这个竞品网站有两套价格体系,取哪套?),它可以主动通过飞书/Slack 给你发消息请求确认。

你点击确认后,Agent 继续执行。这才是真正的人机协作:AI 处理海量冗余信息,人类只在关键节点做决策。

在这个体系下,Skills 不再是静态的脚本,而是可生长、可组合的原子能力。

未来,你的个人服务器里可能运行着上百个这样的 Skills。它们是一群田螺姑娘,在你睡觉的时候,帮你监控市场、回复邮件、整理知识、优化代码。

而你,只需要握着手机,轻轻敲两下背部,就像魔法师挥动了魔杖。

这,才是 Agent 时代的真正玩法。

Win11 抓包工具怎么选?网页请求与设备流量抓取

在 Windows 11 上抓包时,问题一般是当前任务应该用哪一类工具

同样是抓包,不同目标差异很大:

  • 想看浏览器接口
  • 想调试 App 请求
  • 想抓手机流量
  • 想分析 TCP 连接

如果一开始选错工具,就会在配置上面搞半天


一、只想看网页接口就先不用装工具

如果目标是查看网页请求和分析接口返回,直接用浏览器即可。


操作步骤(Chrome / Edge)

  1. 打开网页
  2. 按 F12
  3. 切换到 Network 面板
  4. 刷新页面

可以看到什么

  • 请求 URL
  • 请求方法
  • Header
  • Response

验证

点击某个按钮(例如登录),Network 面板会新增请求。

可以直接点进去查看参数。


二、需要修改请求或重放接口

浏览器工具只能查看,不能灵活修改。

这时需要代理抓包工具,例如:

  • Fiddler
  • Charles(Windows 版)
  • Sniffmaster

在 Win11 上配置 Fiddler

操作步骤:

  1. 启动 Fiddler
  2. 打开 Tools → Options
  3. 开启 HTTPS 解密
  4. 安装证书

让浏览器走代理

Fiddler 会自动设置系统代理。

打开网页后,可以在 Fiddler 中看到请求。


修改请求

  1. 开启断点(Breakpoints)
  2. 发送请求
  3. 修改参数
  4. 再发送

观察变化

修改参数后,服务器返回的数据会发生变化。


三、抓 Windows 本机程序流量

如果目标不是浏览器,而是:

  • 桌面软件
  • 本地客户端

仍然可以用 Fiddler、Charles 或 Sniffmaster。


验证方法

  1. 启动抓包工具
  2. 打开目标程序
  3. 触发网络请求

判断结果

  • 如果出现请求 → 程序走系统代理
  • 如果没有 → 程序未使用代理

四、抓 iPhone 流量(Win11 场景)

如果需要在 Windows 上抓 iPhone 的请求,可以先尝试代理方式。


配置步骤

  1. Win11 上启动 工具
  2. 查看端口(例如 8888)
  3. iPhone 连接同一 Wi-Fi
  4. 在 iPhone 设置代理
  5. 安装证书

端口


测试

用 Safari 打开网页:

  • 如果 Fiddler 有请求 → 配置成功

问题分支

如果 Safari 有请求,但 App 没有,说明 App 没走代理


五、使用数据线直接对设备进行抓包

在 Win11 上,如果代理抓不到移动端流量,可以使用 SniffMaster(抓包大师)


操作步骤

  1. 用 USB 连接 iPhone
  2. 解锁设备
  3. 点击“信任此电脑”
  4. 启动 SniffMaster
  5. 选择设备
  6. 安装驱动(Win11 会提示)
  7. 安装描述文件
  8. 进入 HTTPS 暴力抓包 / 数据流抓包模式
  9. 点击开始

------暴力

观察结果

在界面中可以看到:

  • iPhone 发起的请求
  • 包括未经过代理的流量

https


六、在 Win11 上减少抓包噪音

对设备抓包数据会很多,可以通过筛选降低复杂度。


按 App 筛选

在 SniffMaster 中:

  1. 点击 选择 App
  2. 勾选目标应用
  3. 再触发请求

app选择


再做一次控制变量

  1. 清空记录
  2. 点击开始
  3. 只触发一次操作

这样请求数量会明显减少。


七、分析 TCP / 网络问题

如果问题不是接口,而是:

  • 请求超时
  • 网络波动

可以结合 Wireshark。


操作方式

  1. 在 Win11 上启动 Wireshark
  2. 选择网卡
  3. 开始抓包
  4. 触发请求

可以看到

  • TCP 三次握手
  • 重传
  • 连接断开

在 Win11 上抓包,可以按任务选择工具:

  • 看网页 → 浏览器
  • 改接口 → Fiddler/Sniffmaster
  • 抓 App → 代理工具
  • 抓不到 → SniffMaster
  • 查连接 → Wireshark

Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑

很多开发者在把 Tauri 2 应用上架到 iOS(真机或模拟器)时,都会在文件保存这一步踩坑:明明代码代码在其他平台没问题,在 iOS 路径就返回 null,或者在「文件」App 里根本看不到自己的 App 文件夹。

下面我把最常见的几个坑总结成一份避坑科普文,帮你一次性避开这些“iOS 特色”问题。

坑 1:@tauri-apps/plugin-dialogsave() 在 iOS 上经常返回 null 或路径不可用

现象
调用 const path = await save({...}) 后,一个 0KB 的文件写入成功,但是 path 返回是 null

避坑方法

  • 不要过度依赖 dialog.save() 来实现“用户任意选择保存位置”。
  • 优先使用 直接写入 App 的 Documents 目录(见坑 3)。
  • capabilities 中确保开启 dialog:save 权限。

坑 2:文件明明写入了,但「文件」App 里完全看不到 “Mind Elixir” 文件夹

现象: 用了 BaseDirectory.Document 保存文件后,在「文件」App → 浏览 → On My iPhone 里找不到你的 App 文件夹。

原因: iOS 沙盒机制严格控制 App 的 Documents 目录是否对「文件」App 可见。Tauri 默认生成的 iOS 项目不会自动添加暴露文件夹的配置,就算你写再多文件,文件夹也不会出现。

避坑方法(最关键的一步): 在 Info.plist 中添加以下两个 key(必须同时添加):

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

位置:通常在 src-tauri/gen/apple/ios/App/App/Info.plist(或你的项目对应路径),加在 <dict> 标签内,</dict> 之前。

添加后必须重新构建并安装 Appcargo tauri ios build 或用 Xcode 编译),然后:

  • 先执行一次写入操作(创建文件)。
  • 完全退出「文件」App(上滑关闭),重新打开并下拉刷新「On My iPhone」。

此时你应该能看到和 App 同名的文件夹(显示名称来自 productName 或 Xcode Display Name)。

注意:这两个 key 只控制可见性,不影响代码读写。

坑 3:iOS 上最好的保存方式其实不是 dialog,而是直接用 Documents 目录

推荐做法

import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'

await writeTextFile('my-note.md', '你的内容...', {
  dir: BaseDirectory.Document
})

优点:

  • 最稳定,几乎不会出现 0 字节文件的问题。
  • 用户可以在「文件」App 里直接看到和管理文件(添加上面两个 plist key 后)。
  • 无需处理复杂的 URI 和潜在的 fs bug。

如果你想让用户输入文件名,可以结合 prompt 或自定义输入框实现。

总结建议

在 Tauri 2 + iOS 开发中:

  1. 优先使用 BaseDirectory.Document 直接保存(最稳)。
  2. 必须在 Info.plist 添加 UIFileSharingEnabledLSSupportsOpeningDocumentsInPlace
  3. 谨慎使用 dialog.save() + writeFile,因为移动端兼容性还有待完善(官方 issue 仍在跟进)。
  4. 开发时多用控制台日志 + Safari/XCode 调试,遇到路径问题先检查 plist 和权限。

避开这几个坑后,你的 Mind Elixir(或其他 App)在 iOS 上的文件保存功能就会顺畅很多。iOS 的沙盒和文件系统规则和桌面差异很大,提前了解这些“Apple 特色”能省下大量调试时间。

(本文基于 Tauri 2 常见 issue 和实际开发经验总结,iOS 规则可能随系统版本微调,建议以 Apple 官方文档为准。)

❌