在 Mac 上跑一台能被 AI 操作的虚拟 iPhone:vphone-cli 是怎么做到的?

在这里插入图片描述

平时做 iOS 开发,我们最熟悉的是:

Xcode
↓
iOS Simulator
↓
启动 App
↓
调试

但最近看到一个很有意思的开源项目:

vphone-cli

项目地址:

https://github.com/Lakr233/vphone-cli

它做的事情和普通 iOS Simulator 不太一样。

项目的定位是:

使用 Apple 的 Virtualization.framework 和 PCC Research VM 基础设施,在 Apple Silicon Mac 上启动一台虚拟 iPhone。

更有意思的是,这台虚拟 iPhone 不是只能拿鼠标点。

项目还暴露了一套程序控制接口:

截图

触摸

滑动

Home / Power / 音量键

剪贴板

甚至每执行一步操作,都可以把当前屏幕截图直接返回给调用方。

于是整个系统就变成:

AI Agent
↓
看手机截图
↓
决定下一步
↓
点击 / 滑动 / 按键
↓
重新截图
↓
继续判断

这已经非常接近:

AI 驱动的 iPhone Computer Use

项目 README 也明确把这个能力定位到了 AI-driven E2E testing。


这不是普通的 iOS Simulator

这一点必须先说清楚。

普通 Xcode Simulator 更接近:

macOS
↓
Simulator Runtime
↓
模拟 iOS 用户空间环境

而 vphone-cli 的目标是:

Apple Silicon Mac
↓
Virtualization.framework
↓
虚拟硬件
↓
iOS 启动链
↓
iOS 系统

也就是说,它实际上围绕:

VZVirtualMachine

构建了一整套虚拟设备。

项目源码里直接使用:

import Virtualization

核心类:

class VPhoneVirtualMachine:
    NSObject,
    VZVirtualMachineDelegate

最终创建:

virtualMachine =
    VZVirtualMachine(
        configuration: config
    )

项目使用 Swift 6.0,最低 macOS 15,并链接了 Virtualization、AppKit、SwiftUI、CoreLocation、AVFoundation 等 Framework。


一、一台虚拟 iPhone,本质上要配置什么?

如果暂时忽略项目里的私有 API 和研究型实现,一台 VM 最核心的组成其实很好理解:

CPU

内存

磁盘

显示器

声卡

网络

键盘

触摸屏

串口

Host ↔ Guest 通信

可以先写一个简化版:

import Virtualization

func createConfiguration(
    diskURL: URL
) throws -> VZVirtualMachineConfiguration {

    let config =
        VZVirtualMachineConfiguration()

    config.cpuCount = 8

    config.memorySize =
        8 * 1024 * 1024 * 1024

    let attachment =
        try VZDiskImageStorageDeviceAttachment(
            url: diskURL,
            readOnly: false
        )

    let disk =
        VZVirtioBlockDeviceConfiguration(
            attachment: attachment
        )

    config.storageDevices = [
        disk
    ]

    try config.validate()

    return config
}

然后:

let config =
    try createConfiguration(
        diskURL: diskURL
    )

let vm =
    VZVirtualMachine(
        configuration: config
    )

这其实就是 VM 最基础的骨架。


二、vphone-cli 的配置明显复杂得多

真正的项目里,需要模拟的是一整台移动设备。

源码中的 Options 包含:

struct Options {

    var configURL: URL

    var romURL: URL?

    var nvramURL: URL

    var diskURL: URL

    var cpuCount: Int = 8

    var memorySize: UInt64 =
        8 * 1024 * 1024 * 1024

    var sepStorageURL: URL

    var screenWidth: Int = 1290

    var screenHeight: Int = 2796

    var screenPPI: Int = 460

    var screenScale: Double = 3.0

    var kernelDebugPort: Int?

    var variant: Variant

    var noVphoned: Bool
}

也就是说,一台 vPhone Bundle 里不仅有:

Disk Image

还有:

ROM

NVRAM

Machine Identifier

SEP Storage

Screen Configuration

Network Configuration

项目还会保存 machineIdentifier,让设备身份在多次启动之间保持稳定。

这点很重要。

如果每次启动:

设备身份都发生变化

那么系统里的:

ECID
UDID
设备注册
缓存状态
测试状态

都会非常混乱。


三、屏幕其实也是一个虚拟硬件

项目会配置:

VZMacGraphicsDeviceConfiguration

概念上可以理解成:

let graphics =
    VZMacGraphicsDeviceConfiguration()

let display =
    VZMacGraphicsDisplayConfiguration(
        widthInPixels: 1290,
        heightInPixels: 2796,
        pixelsPerInch: 460
    )

graphics.displays = [
    display
]

config.graphicsDevices = [
    graphics
]

于是虚拟机看到的是:

1290 × 2796

的一块显示设备。

这也是后面为什么:

AI 点击坐标

可以直接映射到:

iPhone 屏幕像素坐标。

四、声音也可以直接映射到宿主 Mac

项目同时配置了 Virtio Sound。

简化一下:

let sound =
    VZVirtioSoundDeviceConfiguration()

let input =
    VZVirtioSoundDeviceInputStreamConfiguration()

let output =
    VZVirtioSoundDeviceOutputStreamConfiguration()

input.source =
    VZHostAudioInputStreamSource()

output.sink =
    VZHostAudioOutputStreamSink()

sound.streams = [
    input,
    output
]

config.audioDevices = [
    sound
]

这意味着:

虚拟 iPhone 麦克风

←

Mac 音频输入

以及:

虚拟 iPhone 扬声器

→

Mac 音频输出

所以从架构上看,它已经越来越像:

一台真正运行在 Mac 内部的设备。


五、网络也不是简单写死的

vphone-cli 支持:

NAT

Bridged

None

不同网络模式。

最终都是转换成:

config.networkDevices

如果你想做自动化测试,这其实特别重要。

例如:

正常网络测试

弱网测试

完全断网测试

都可以围绕 VM 网络层设计。

甚至未来完全可以做:

Agent Test Case

↓

切换网络

↓

打开 App

↓

执行登录

↓

观察错误页面

这就比普通 UI 自动化更接近真实测试环境。


六、真正关键的一步:Host 和 Guest 怎么通信?

这才是整个项目最值得看的地方之一。

如果 Mac 上的程序想告诉虚拟 iPhone:

给我读取一个文件

设置剪贴板

发送按键

获取状态

怎么办?

最笨的方法:

TCP

但是 VM 内部 IP 可能变化。

而且:

Host
↔
Guest

其实不一定需要走真正的网络。

vphone-cli 使用的是:

Virtio Socket / vsock

VM 配置:

config.socketDevices = [
    VZVirtioSocketDeviceConfiguration()
]

也就是说:

macOS Host
      │
      │ vsock
      │
      ▼
iOS Guest

完全不需要:

192.168.x.x

这种 IP 通信。

项目里的 Guest Daemon 叫:

vphoned

运行在虚拟 iPhone 里面。

Host 侧对应:

VPhoneControl

架构变成:

vphone-cli
    │
    │ VZVirtioSocket
    ▼
vphoned
    │
    ▼
iOS

项目内部固定使用 vsock Port 1337,并通过一套带版本号的 JSON 协议通信。


七、他们没有直接裸传 JSON

这里还有一个很典型的协议设计。

如果在 Socket 里不断发送:

{"t":"hello"}
{"t":"clipboard"}

接收方怎么知道:

一条消息到底在哪里结束?

TCP / Stream Socket 本身没有“消息边界”。

可能收到:

{"t":"hello"}{"t":"clip

下一次才收到:

board"}

所以 vphone-cli 使用:

Length-Prefixed JSON

格式类似:

┌──────────────┬─────────────────────┐
│ uint32 长度   │ JSON Payload        │
│ Big Endian   │ UTF-8               │
└──────────────┴─────────────────────┘

比如:

{
  "v": 1,
  "t": "hello"
}

先编码:

let jsonData =
    try JSONSerialization.data(
        withJSONObject: message
    )

然后:

var length =
    UInt32(jsonData.count).bigEndian

写入:

4 Byte Length
+
JSON

八、我们自己实现一个最小协议编码器

可以写:

func encodeMessage(
    _ object: [String: Any]
) throws -> Data {

    let payload =
        try JSONSerialization.data(
            withJSONObject: object
        )

    var length =
        UInt32(payload.count).bigEndian

    var result =
        Data(
            bytes: &length,
            count: 4
        )

    result.append(payload)

    return result
}

发送:

let message =
    try encodeMessage([
        "v": 1,
        "t": "hello"
    ])

socket.write(
    message
)

Guest 先读取:

4 Byte

得到:

Payload Length

再按长度读取完整 JSON。

这是一个非常经典的:

自定义二进制 Frame Protocol。


九、为什么每条消息还有 Request ID?

如果同时发多个请求:

Request A

Request B

Request C

Guest 返回:

Response ?

Host 怎么知道是谁的?

所以协议里还有:

{
  "v": 1,
  "t": "request",
  "id": "1001"
}

返回:

{
  "v": 1,
  "t": "response",
  "id": "1001"
}

Host 保存:

pendingRequests[id]

项目当前实现就是维护:

private var pendingRequests:
    [String: PendingRequest] = [:]

然后响应回来以后:

ID
↓
找到 Callback
↓
完成 Request

这已经很像:

一个轻量 RPC 协议。


十、再往外还有一层:vphone.sock

vsock 解决的是:

Host
↔
iOS Guest

但是如果:

Codex

Claude Code

Python 脚本

测试框架

想控制这台手机呢?

直接让每个工具实现一遍 vsock 协议很麻烦。

所以项目又暴露了一层:

Unix Domain Socket

每台 VM Bundle 下会出现:

vphone.sock

调用链就变成:

Python / Codex / MCP
        │
        │ Unix Socket
        ▼
    vphone.sock
        │
        ▼
   vphone-cli
        │
        │ vsock
        ▼
     vphoned
        │
        ▼
       iOS

这层设计非常漂亮。

因为外部程序根本不用知道:

Virtualization.framework

VZVirtioSocket

Guest Daemon

它只需要:

给 Unix Socket 发 JSON。


十一、Host Control 协议有多简单?

例如截图:

{
  "t": "screenshot"
}

点击:

{
  "t": "tap",
  "x": 645,
  "y": 1398
}

滑动:

{
  "t": "swipe",
  "x1": 645,
  "y1": 2600,
  "x2": 645,
  "y2": 1400,
  "ms": 300
}

按 Home:

{
  "t": "key",
  "name": "home"
}

设置剪贴板:

{
  "t": "type",
  "text": "Hello"
}

项目当前 Host Control 就支持 screenshot、tap、swipe、home/power/音量键以及剪贴板控制。


十二、甚至可以直接用 nc 控制

假设 VM Bundle:

~/.vphone/VMs/myphone

那么 Socket 可以理解成:

SOCK="$HOME/.vphone/VMs/myphone/vphone.sock"

截图:

printf '%s\n' \
'{"t":"screenshot"}' \
| nc -U "$SOCK"

点击:

printf '%s\n' \
'{"t":"tap","x":645,"y":1398}' \
| nc -U "$SOCK"

Home:

printf '%s\n' \
'{"t":"key","name":"home"}' \
| nc -U "$SOCK"

滑动:

printf '%s\n' \
'{
  "t":"swipe",
  "x1":645,
  "y1":2200,
  "x2":645,
  "y2":800,
  "ms":300
}' \
| nc -U "$SOCK"

这意味着:

任何会操作 Unix Socket 的语言,都可以控制 iPhone。


十三、Python 控制就更简单了

自己写一个最小 Client。

import json
import socket


class VPhoneClient:

    def __init__(self, socket_path):
        self.socket_path = socket_path

    def send(self, command):
        client = socket.socket(
            socket.AF_UNIX,
            socket.SOCK_STREAM,
        )

        client.connect(
            self.socket_path
        )

        payload = (
            json.dumps(command)
            + "\n"
        )

        client.sendall(
            payload.encode("utf-8")
        )

        chunks = []

        while True:
            data = client.recv(4096)

            if not data:
                break

            chunks.append(data)

            if b"\n" in data:
                break

        client.close()

        raw = b"".join(chunks)

        return json.loads(
            raw.decode("utf-8")
        )

然后:

phone = VPhoneClient(
    "/Users/me/.vphone/VMs/myphone/vphone.sock"
)

截图:

result = phone.send({
    "t": "screenshot"
})

print(result)

点击:

phone.send({
    "t": "tap",
    "x": 645,
    "y": 1398,
})

滑动:

phone.send({
    "t": "swipe",
    "x1": 645,
    "y1": 2300,
    "x2": 645,
    "y2": 900,
    "ms": 300,
})

这时候你已经有一个最小的:

iPhone Automation SDK


十四、最关键的设计:每执行一步,都返回截图

普通自动化:

执行点击
↓
不知道发生了什么

Agent 自动化最大的问题就是:

操作以后怎么知道结果?

所以 vphone-cli 做了一件很关键的事情:

除了截图命令以外,tap、swipe、key、type 等动作执行完后,默认也会重新抓取屏幕。

返回:

{
  "ok": true,
  "image": "/9j/4AAQSkZJRg..."
}

这个 image:

是:

Base64

编码后的压缩截图。

项目为了减少传输量,还会:

缩小到约 1/3

转灰度

降低 JPEG Quality

最终让每一步操作都形成:

Action
+
Observation

这就是 AI Agent 最喜欢的接口设计。


十五、为什么截图要压缩?

假设原始屏幕:

1290 × 2796

RGBA 原始数据大约:

1290
×
2796
×
4
≈
13.7 MB

如果 Agent 每一步都传:

10 MB+

连续执行 100 步:

1GB

非常夸张。

所以项目把截图缩到大约:

430 × 932

再:

灰度 JPEG
+
低质量压缩

通常已经足够:

给视觉模型判断下一步点哪里。

这其实是一个很现实的 Agent 工程优化。


十六、AI 操作手机,本质是一个闭环

现在我们可以把逻辑写出来:

while True:

    screen = phone.send({
        "t": "screenshot"
    })

    action = agent.decide(
        screen["image"]
    )

    if action["type"] == "done":
        break

    phone.send(action)

逻辑其实就是:

Observe

↓

Reason

↓

Act

↓

Observe

这就是典型的:

Agent Loop


十七、比如让 AI 自动测试登录

任务:

打开测试 App

输入账号密码

点击登录

判断是否进入首页

Agent 第一步:

{
  "t": "screenshot"
}

看到登录页。

然后判断输入框位置。

点击:

{
  "t": "tap",
  "x": 300,
  "y": 900
}

设置剪贴板:

{
  "t": "type",
  "text": "test@example.com"
}

再操作密码框。

点击:

{
  "t": "tap",
  "x": 645,
  "y": 1700
}

执行完成:

系统重新返回:

当前截图

Agent 判断:

有没有进入首页?

如果没有:

继续处理。

十八、这和传统 Appium 最大区别是什么?

传统 Appium 更喜欢:

Selector

Accessibility ID

XPath

Element Tree

比如:

driver.find_element(
    AppiumBy.ACCESSIBILITY_ID,
    "Login"
).click()

这是:

结构化 UI 自动化

而 vphone-cli 这种方式更适合:

Screenshot
+
Coordinate Action
+
Vision Agent

也就是:

视觉型 Computer Use

两者各有优缺点。

传统 Appium:

快

稳定

适合确定性测试

视觉 Agent:

更灵活

不用提前写大量 Selector

更适合未知 UI

更适合探索式测试

真正合理的未来可能不是二选一。

而是:

Selector 优先

↓

Selector 不可用

↓

Vision Agent 兜底

十九、项目甚至已经留好了 MCP 方向

README 直接提到了:

vphone-mcp

用 MCP 包装:

vphone.sock

之后架构就会变成:

Codex
Claude Code
Agent
    │
    │ MCP
    ▼
vphone-mcp
    │
    │ Unix Socket
    ▼
vphone-cli
    │
    │ vsock
    ▼
Virtual iPhone

这件事情真正有意思的地方是:

以前 Codex 的能力范围大概是:

代码

Shell

文件

Git

现在:

Codex
↓
MCP
↓
虚拟 iPhone

它开始拥有:

手机操作能力


二十、我们甚至可以自己写一个 MCP Tool

下面只是一个简化示意。

Python:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("vphone")

phone = VPhoneClient(
    "/Users/me/.vphone/VMs/myphone/vphone.sock"
)


@mcp.tool()
def screenshot():
    """
    Get current iPhone screen.
    """

    return phone.send({
        "t": "screenshot"
    })


@mcp.tool()
def tap(x: int, y: int):
    """
    Tap the iPhone screen.
    """

    return phone.send({
        "t": "tap",
        "x": x,
        "y": y,
    })


@mcp.tool()
def swipe(
    x1: int,
    y1: int,
    x2: int,
    y2: int,
    ms: int = 300,
):
    """
    Swipe on the iPhone screen.
    """

    return phone.send({
        "t": "swipe",
        "x1": x1,
        "y1": y1,
        "x2": x2,
        "y2": y2,
        "ms": ms,
    })


@mcp.tool()
def press_home():
    return phone.send({
        "t": "key",
        "name": "home",
    })


if __name__ == "__main__":
    mcp.run()

这时候 Codex 可以拿到:

screenshot

tap

swipe

press_home

几个 Tool。

然后你告诉它:

打开设置。

进入电池。

检查当前电量页面能否正常打开。

Agent 就可以:

截图
↓
看 UI
↓
点击设置
↓
截图
↓
找到电池
↓
点击
↓
截图
↓
判断结果

二十一、这对 AI Coding 意味着什么?

这里其实有一个比“虚拟 iPhone”更大的故事。

现在 Coding Agent 最明显的问题是:

代码写完了

↓

它怎么知道真的能用?

Web 领域已经有:

Playwright

BrowserSkill

Computer Use

于是 Agent 可以:

写网页
↓
打开浏览器
↓
自己测试

移动端以前比较麻烦。

如果有:

Virtual iPhone
+
Control Socket
+
Screenshot
+
MCP

Agent 就可以形成:

修改 iOS App
↓
编译
↓
安装
↓
启动
↓
点击
↓
截图
↓
发现 Bug
↓
回去改代码
↓
重新测试

真正闭环。


二十二、比如未来可以做一个 Mobile Coding Loop

伪代码:

def coding_loop(task):

    while True:

        result = coding_agent.modify_code(
            task
        )

        build_result = build_app()

        if not build_result.ok:
            coding_agent.fix(
                build_result.logs
            )
            continue

        install_app(
            build_result.ipa
        )

        test_result =
            mobile_agent.run_test(
                task
            )

        if test_result.ok:
            break

        coding_agent.fix(
            test_result
        )

最终:

开发
+
编译
+
安装
+
测试
+
修复

都可以在一个 Agent Workflow 里。

这可能才是 vphone-cli 最值得关注的方向。


二十三、虚拟机管理能力也非常完整

项目不是:

只能启动一个 VM。

它已经提供:

vphone-cli vm list

查看:

所有虚拟 iPhone

创建:

vphone-cli vm new myphone

配置:

vphone-cli vm config myphone \
  --cpu 8 \
  --memory 8192

克隆:

vphone-cli vm clone \
  myphone \
  myphone-2

导出:

vphone-cli vm export \
  myphone \
  --out myphone.tzst

导入:

vphone-cli vm import \
  myphone.tzst \
  --name restored

这些能力都已经在官方 CLI 里。


二十四、Clone 特别适合测试

假设你有:

base-ios

已经:

安装 App

登录测试账号

初始化数据

然后:

vphone-cli vm clone \
  base-ios \
  test-001

再:

vphone-cli vm clone \
  base-ios \
  test-002

再:

vphone-cli vm clone \
  base-ios \
  test-003

就可以形成:

测试环境 1

测试环境 2

测试环境 3

README 说明这里使用的是快速 APFS Clone,并为新 VM 创建新的设备身份。

这特别适合:

并行 E2E 测试。

二十五、甚至可以做多 Agent 手机测试

比如:

Agent A
↓
test-001
↓
登录流程
Agent B
↓
test-002
↓
订单流程
Agent C
↓
test-003
↓
支付失败页面

最后三个 Agent:

并行测试。

如果再配合:

Git Worktree

甚至可以做到:

Agent A
代码分支 A
+
虚拟 iPhone A

Agent B
代码分支 B
+
虚拟 iPhone B

Agent C
代码分支 C
+
虚拟 iPhone C

这就开始有:

AI Mobile QA Farm

的感觉了。


二十六、虚拟 iPhone 还有哪些使用场景?

我觉得至少有 5 个。

1. iOS E2E 自动化

编译
↓
安装
↓
点击
↓
截图
↓
验证

2. Coding Agent 自动验证

Codex 修改 App 后:

自己打开手机测试。

3. UI 回归测试

比如:

版本 A 截图

vs

版本 B 截图

做:

Visual Diff

4. Agent Benchmark

同一个任务:

打开设置
找到 Wi-Fi
修改某项配置

分别交给:

GPT
Claude
Gemini

测试:

成功率

步骤数

Token

执行时间

5. 移动端 Computer Use 研究

这可能是最有意思的一个。

以后:

Browser Agent

之外,很可能还有:

Phone Agent。

二十七、当然,这个项目目前门槛并不低

这一点一定要讲清楚。

项目官方要求:

Apple Silicon

macOS 15+

Xcode

iOS SDK

而且因为用到了:

私有 PV=3 能力

私有 Entitlement

宿主机还需要对 SIP / AMFI 做研究用途的放宽。

所以它目前并不是:

brew install
↓
下一步
↓
下一步
↓
完成

这种普通用户软件。

更适合:

安全研究人员

iOS 底层开发者

虚拟化研究者

Agent / E2E 自动化开发者

而且我不建议在日常主力生产机上,为了体验一个项目就随意降低系统安全保护。

最好:

专用测试 Mac

或

明确隔离的研究设备。

二十八、另外它大量使用 Private API

项目的 AGENTS.md 直接写明:

Swift 6.0

Virtualization.framework

Private API

Dynamic Runtime Dispatch

而源码中除了公开的:

VZVirtualMachine

之外,还能看到诸如:

_VZPL011SerialPortConfiguration

_VZUSBTouchScreenConfiguration

_VZMacSyntheticBatterySource

_VZSEPCoprocessorConfiguration

等私有能力。

这意味着:

系统版本升级以后,兼容性风险会明显高于纯公开 API 项目。

所以它更像:

Research Tool

而不是:

Stable Production SDK


二十九、我觉得这个项目最值得看的,其实是三层架构

如果把复杂的 Firmware 和底层研究全部拿掉。

vphone-cli 最漂亮的一部分架构其实特别清楚。

第一层:

Virtualization.framework

负责:

虚拟硬件

启动 VM

显示

音频

网络

存储

第二层:

vsock
+
vphoned

负责:

Host
↔
Guest

第三层:

Unix Socket

负责:

外部工具
↔
vphone-cli

最终:

Codex / Claude / Python
          │
          │
      Unix Socket
          │
          ▼
      vphone-cli
          │
          │
         vsock
          │
          ▼
       vphoned
          │
          ▼
         iOS

每一层职责非常明确。


三十、为什么我认为它最值得关注的是 Agent,而不是虚拟化?

因为能把 iPhone 启动起来本身当然已经很难。

但从开发者生态的角度来看:

真正有扩展性的能力是:

这台 iPhone 可以被程序控制。

尤其是:

执行动作

↓

自动返回截图

这个设计天然就是给:

Vision Agent

准备的。

Agent 不需要:

理解 Virtualization.framework。

也不需要:

理解 iOS 内部控制协议。

只需要几个工具:

screenshot()

tap()

swipe()

key()

clipboard()

就够了。

这其实也是 Agent 工具设计一个很好的案例:

底层可以极其复杂,但暴露给 Agent 的 Tool 一定要足够简单。


最后

我觉得 vphone-cli 真正有意思的地方,不只是:

终于可以在 Mac 上跑一台虚拟 iPhone。

更值得关注的是,它开始把移动设备变成:

可编程基础设施

以前一台 iPhone 是:

人
↓
手指
↓
屏幕

现在:

程序
↓
Unix Socket
↓
vphone-cli
↓
Virtual iPhone

再往前一步:

AI Agent
↓
MCP
↓
截图
↓
理解 UI
↓
点击 / 滑动
↓
截图
↓
继续执行

它就变成了一套完整的:

Mobile Computer Use 基础设施

过去 AI Coding 的重点一直是:

让 AI 写更多代码。

但我觉得接下来更重要的一件事会变成:

让 AI 能够验证自己写出来的东西到底能不能用。

Web 已经开始通过浏览器 Agent 补上这一环。

桌面软件开始通过 Computer Use 补上这一环。

而像 vphone-cli 这样的项目,则正在把:

真实移动系统操作能力

也接进 Agent Workflow。

当一个 Coding Agent 可以:

写代码
↓
编译 App
↓
安装到虚拟 iPhone
↓
自己打开
↓
自己点击
↓
自己截图
↓
发现问题
↓
回去修改

AI Coding 才真正从:

“代码生成器”

开始变成:

能够完成开发—验证闭环的软件工程 Agent。

我觉得这才是 vphone-cli 最值得看的地方。

posted @ 2026-09-17 15:42  JavaPub  阅读(27)  评论(0)    收藏  举报