把 Claude Code 装进口袋:一个手机远程操控 Claude Code 的全栈实现
阅读 74 · 评论 0
缘起:为什么想在手机上跑 Claude Code
Claude Code 很好用,但它长在终端里——人不在电脑前,就和它失联了。我想要的体验很简单:人在外面,掏出手机就能继续指挥那台电脑上的 Claude Code 干活——发消息、看它流式回复、批准它执行命令、回答它的提问、让它把生成的文件或截图发到手机上。
它不是把 Claude API 重新包一遍,而是把真正的 Claude Code(带着你的项目、技能、MCP、权限体系)原样搬到手机上。于是有了 claude-remote:
server/—— 跑在你电脑上的 Node + TypeScript 服务,用@anthropic-ai/claude-agent-sdk驱动 Claude Code,对外暴露 REST + WebSocket。app/—— Expo(React Native)写的原生 Android 应用,连上服务端,提供完整的聊天/审批/文件交互界面。
本文详细讲它是怎么运行的:整体架构、服务端原理、客户端原理、一条消息的端到端旅程,以及一路踩过的几个有意思的坑。
〇、为什么不直接用 Claude Code 自带的 Remote Control?
很多人第一反应是:Claude Code 官方不是已经有 Remote Control(远程操控)了吗,手机装个官方 Claude App 就能连,为什么还要自己造轮子?
我一开始也是冲着官方功能去的,用下来发现它解决的其实是另一个问题。官方 Remote Control 的定位是「接管并盯住一个已经在电脑上跑着的会话」——而我想要的是「人不在电脑前,掏出手机从零开一个新活儿」。这中间差了一道致命的坎:
Remote Control 没法在手机端新建 session。 你必须先回到电脑前,
cd到目标目录、把会话起起来、再开启 Remote Control,手机才能连上去;而且工作目录在电脑端启动那一刻就定死了,手机端改不了。
对我的核心场景——「躺在沙发上突然想让 Claude 去某个项目里干点事」——这条路直接走不通:我根本没在电脑前,谈何先去电脑上把会话起好。这是我决定自己写一个 App 的首要原因。
把项目功能逐条对下来,差异不止这一点:
| 维度 | 官方 Remote Control | 本项目 claude-remote |
|---|---|---|
| 手机端新建会话 | ❌ 不行,必须电脑端先起会话再接管 | ✅ 手机端直接新建,带服务端目录选择器任选工作目录 |
| 选/换工作目录 | ❌ 电脑端启动时定死 | ✅ 建会话时在手机上浏览服务器目录树选定 |
| 传输架构 | 经 Anthropic 云中继(机器只发出站请求;文件不出机器,但聊天内容/工具结果过 Anthropic 服务器) | 纯局域网点对点,数据完全不经任何第三方 |
| 客户端 | 官方 Claude App / claude.ai 网页,不可定制 | 自己的开源 App,UI 与功能完全可改 |
| 自定义能力 | 受限于官方实现 | 可加任意能力——比如本项目的文件下发、图片内联展示就是官方没有的 |
| 输入补全 | ❌ 斜杠命令、技能名、文件路径全程手敲,没有边打边匹配 | ✅ / 实时匹配命令/技能/插件命令;@ 浏览服务器项目目录树自动补全文件路径 |
| 鉴权门槛 | 需要 claude.ai 订阅 + OAuth 登录,不支持 API key | 用你自己的 Bearer token;底层就是跑你本地的 claude CLI,用什么登录都行 |
| 多会话 | 交互模式单会话(server 模式可多会话) | 原生多会话列表 + 持久化 + 断线恢复 |
「输入补全」这点在手机上体感尤其明显,单独展开说一下——用过官方 App 在手机上敲斜杠命令的都知道有多难受:命令名、技能名、文件路径全靠记忆手敲,打错一个字符就白费。claude-remote 把桌面端那套"边打边匹配"原样搬到了手机:
/斜杠命令面板:输入/就弹出命令列表,按前缀实时过滤。数据来自 SDK 的supportedCommands()——不只是内置命令,连你装的技能(skill)和插件命令都在里面,选一下就发,不用记全名。@文件提及面板:在输入框任意位置打@,就开始浏览服务器上这个项目的真实目录树,按前缀补全文件名;目录可以一层层下钻,选中文件自动补成相对路径。等于在手机上也能"@文件"精确引用代码,而不用手敲一长串路径。
当然,公平地说,官方 Remote Control 也有它明显更强的地方,这些正是 claude-remote 的取舍:
- 零搭建、官方维护:装个官方 App 就能用,不用自己跑服务器、不用自己打包维护。
- 天生跨网络:它走云中继、机器只开出站连接,所以不限局域网、不用开入站端口、对 NAT/防火墙友好——人在外网也能连。而 claude-remote 默认只在同一局域网内可用,要远程得自己套 VPN / 内网穿透。
- 平台覆盖:官方 App 有 iOS + Android,claude-remote 目前只有 Android。
所以这不是"谁取代谁",而是定位不同:
- 官方 Remote Control = 把一个已经在跑的会话,安全地投射到手机/网页上去盯进度、给指令;强在零成本和跨网络。
- claude-remote = 把手机当成一个完整的客户端,能从零新建会话、选目录、全程操控,全程跑在自己的局域网里、想加什么功能就加什么。
正因为"手机端新建会话"这个刚需官方满足不了,加上想要一个完全本地、可随意扩展(文件/图片下发就是这么加上去的)的客户端,才有了下面这套自己实现的全栈方案。
官方功能文档:https://code.claude.com/docs/en/remote-control(其能力与限制随版本演进,以官方为准)
一、整体架构
flowchart LR
subgraph PHONE["📱 Android App · Expo / RN"]
direction TB
P1["zustand store"]
P2["expo-router"]
P3["WireEvent 事件流 reducer"]
end
subgraph HOST["💻 常开主机 · Node Server"]
direction TB
H1["Express REST + ws WebSocket"]
H2["SessionManager"]
H3["ClaudeSession × N"]
H4["@anthropic-ai/claude-agent-sdk"]
H5["claude CLI(子进程)"]
H1 --> H2 --> H3 --> H4 -->|spawn| H5
end
PHONE -->|"REST · HTTP + Bearer token(同一局域网)"| HOST
HOST <-->|"WebSocket · /ws?token= · 双向事件流 + 控制"| PHONE核心思路:
- 服务端把 SDK 的"消息流"归一化成一套自定义的
WireEvent线协议,客户端只认这套协议,不直接接触 SDK 的复杂类型。 - REST 管"请求-响应"语义(列会话、建会话、读历史、下载文件),WebSocket 管"实时流"语义(流式 token、工具卡片、权限请求、状态变化)。
- 一份协议定义放在
server/src/protocol.ts,镜像一份到app/src/api/protocol.ts,两边手动保持同步——这是单一事实来源。
技术选型:
| 层 | 选型 |
|---|---|
| 服务端 | Node + TypeScript、Express(REST)、ws(WebSocket)、@anthropic-ai/claude-agent-sdk |
| 客户端 | Expo SDK 56、expo-router(文件路由)、React Native 0.85、zustand(状态)、expo-image、react-native-blob-util |
| 鉴权 | 启动时随机生成的 Bearer token,存 ~/.claude-remote/config.json |
| 打包 | EAS 本地构建(eas build --local),本地签名密钥库 |
二、服务端:把 Claude Code 包成一个会话服务
2.1 用 Agent SDK 的"流式输入"模式
整个服务端最核心的一行,是用 SDK 的 query() 开一个长期存活的会话。关键在于 prompt 传的不是字符串,而是一个异步生成器——这让会话变成多轮、可中断、随时能塞新消息:
typescriptthis.q = query({ prompt: this.inputGenerator(), options });
private async *inputGenerator(): AsyncGenerator<SDKUserMessage> {
while (!this.ended) {
if (this.inputQueue.length) yield this.inputQueue.shift()!;
else await new Promise<void>((r) => (this.inputResolver = r)); // 没消息就挂起等待
}
}
用户发消息时,把消息塞进 inputQueue 并唤醒生成器;会话因此能一直开着,支持 interrupt()、setModel()、多轮对话。
options 里几个关键配置:
typescriptconst options: Options = {
cwd, // Claude Code 运行的工作目录
pathToClaudeCodeExecutable: claudePath,
settingSources: ['user', 'project', 'local'], // 加载你的全局/项目级技能、MCP、命令
permissionMode: 'default', // 永远保持 default,权限由我们自己的 hook 把关
includePartialMessages: true, // 要流式 token
model,
mcpServers: { ask: askServer, files: filesServer }, // 自建的两个进程内 MCP
allowedTools: [ASK_TOOL_NAME, SEND_FILE_TOOL_NAME, SEND_IMAGE_TOOL_NAME],
hooks: { PreToolUse: [{ hooks: [(input) => this.onPreToolUse(input)] }] },
};
2.2 会话状态机与 init 的坑
ClaudeSession 维护一个状态机:starting → idle → running → awaiting_permission / awaiting_question → ...。SDK 吐出的每条 SDKMessage 在 handle() 里被分发:assistant/user/stream_event 交给转换器变成 WireEvent,result 结束一轮,system/init 用来确认会话就绪。
这里有个极其反直觉的坑,值得单独说(详见后文"踩坑实录"):
- SDK 的
system/init消息不是开局发一次,而是每一轮都会重发,且第一条用户消息流动之前根本不发。 - 因此「会话建好后一直卡在
starting」和「第二条消息后历史被清空」这两个 bug,根因都在init。
最终的处理是:会话一 query() 建好就立即置为 idle(它已经能排队接收输入了,不必等 init);而判断"上下文是否被 /clear 清空",不能靠"收到第二个 init",而要看 session_id 是否真的变了:
typescriptif (m.subtype === 'init') {
const newSid = m.session_id ?? null;
if (wasInitialized && newSid && prevSid && newSid !== prevSid) {
this.resetTranscript(); // 只有 session_id 真的换了(/clear 开了新会话)才清空
}
// 普通轮次的 init 携带相同 session_id —— 什么都不做
}
2.3 权限审批:为什么不用 canUseTool,而用 PreToolUse hook
让 Claude 执行 Bash、写文件这类有副作用的操作前,要把决定权交给手机上的用户。SDK 本来有个 canUseTool 回调——但在我们用的这个版本里它是坏的:一旦提供 canUseTool,SDK 会给 CLI 传 --permission-prompt-tool,任何需要授权的工具都会在 CLI 侧崩溃(Cannot read properties of undefined (reading 'length')),回调根本来不及跑。
所以改用 PreToolUse hook:每个工具调用前,hook 在 SDK 侧同步触发,可以 await 一个外部决定再返回 allow/deny:
typescriptprivate async onPreToolUse(input: PreToolUseHookInput) {
const policy = decidePolicy(input.tool_name, this.mode, this.remembered);
if (policy === 'allow') return { hookSpecificOutput: { permissionDecision: 'allow', ... } };
if (policy === 'deny') return { hookSpecificOutput: { permissionDecision: 'deny', ... } };
// policy === 'ask' → 把请求推给 App,挂起等用户在手机上点"允许/拒绝"
const decision = await new Promise((resolve) => {
this.pendingPermissions.set(requestId, { resolve, timer });
this.emit('permission_request', request); // → WS → 手机弹出权限卡片
});
return decision.decision === 'allow' ? allow(...) : deny(...);
}
decidePolicy 根据权限模式(default / acceptEdits / plan / bypassPermissions)和"本会话记住的允许项"决定 allow/deny/ask。只读工具(Read/Glob/Grep…)永远放行。
2.4 自建的两个进程内 MCP 工具
有些交互 Claude 没有现成工具表达,于是用 createSdkMcpServer 在进程内自建 MCP 工具——它们被 allowedTools 预先放行,所以不会触发权限弹窗:
ask_user(澄清卡片):Claude 需要用户做选择时调用它,参数是结构化的"问题 + 选项",App 渲染成可点的选项卡片,用户选完作为 tool_result 返回。这避免了用自由文本来回猜测。send_file(文件下发)/send_image(图片内联):Claude 想把工作目录里的文件发给用户时调用。服务端校验路径、登记一个fileId,推一个file或image事件给 App(只发元数据和 fileId,绝不发服务器真实路径),字节由 App 之后通过GET /api/sessions/:id/files/:fileId按需拉取。send_file在 App 里是"下载卡片",send_image则内联渲染成图片。
typescript// filesTool.ts —— 和 ask_user 同一个模子
tool('send_image',
'把图片内联显示在用户的聊天里(不是当文件下载)。用于截图、图表、示意图、生成图…',
{ path: z.string(), caption: z.string().optional() },
async (args) => {
const { name, size } = sendImage(args.path, args.caption); // 校验是图片 + 登记 fileId + 推 image 事件
return { content: [{ type: 'text', text: `已把 "${name}" 内联显示在用户聊天里。` }] };
},
)
2.5 线协议:把 SDK 消息归一化成 WireEvent
transform.ts 里的 LiveTransformer 把 SDK 五花八门的消息(stream_event 里的 text_delta/thinking_delta/input_json_delta、assistant 的内容块、作为 user 消息回来的 tool_result…)归一化成一套扁平、自洽的事件:
typescripttype WireEvent =
| { kind: 'user'; text; ... }
| { kind: 'block_start'; blockId; blockType: 'text' | 'thinking'; ... } // 流式块开始
| { kind: 'block_delta'; blockId; text; ... } // 增量 token
| { kind: 'block_end'; blockId; ... }
| { kind: 'tool_use'; toolUseId; name; category; title; input; fileChange?; ... }
| { kind: 'tool_result'; toolUseId; isError; text; ... }
| { kind: 'result'; subtype; isError; costUsd?; ... } // 一轮结束
| { kind: 'notice' | 'task' | 'file' | 'image'; ... };
客户端只需要按 kind 把事件"叠加"到一个消息列表上即可,完全不碰 SDK 的类型。
2.6 REST + WebSocket 接口
REST(rest.ts,Express)负责请求-响应类操作,全部走 Bearer token 鉴权(也兼容 ?token= query 参数,方便下载场景):
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/health |
健康检查(免鉴权) |
| GET/POST | /api/sessions |
列出 / 新建会话 |
| GET/DELETE | /api/sessions/:id |
查 / 删会话 |
| GET | /api/sessions/:id/messages |
拉历史 transcript |
| GET | /api/sessions/:id/files/:fileId |
流式下载 send_file/send_image 登记的文件 |
| GET | /api/fs/roots、/api/fs/list、POST /api/fs/mkdir |
服务端目录选择器(建会话时挑工作目录) |
| GET | /api/capabilities |
斜杠命令 / 模型 / 子代理列表 |
WebSocket(gateway.ts)在 HTTP upgrade 时校验 ?token=,之后双向收发:客户端发 attach/user_message/permission_response/interrupt/set_model…,服务端广播 event/session_state/permission_request/transcript_reset…。一个会话可被多个 socket 订阅,事件向所有订阅者广播。
文件下载端点有个安全要点:它只服务被 send_file/send_image 显式登记过的 fileId,而不是"按路径下载任意文件"——服务器路径永远不出进程。
2.7 会话持久化与恢复
SessionManager(manager.ts)管所有会话:
- 持久化:会话元数据写
~/.claude-remote/sessions.json,记录sdkSessionId,服务重启后能恢复。 - 懒恢复:客户端 attach 一个不在内存里的会话时,用 SDK 的
resume重新拉起,并通过getSessionMessages()把磁盘上的历史回灌成 backlog,手机一连上就能看到完整历史。 - LRU 驱逐:内存里活跃会话数有上限,空闲的按 LRU 关掉(正在跑/等授权的不动)。
三、客户端:Expo 原生 App
3.1 架构
- expo-router 文件式路由:
index(会话列表)、settings(服务器地址+token)、new-session(服务端目录选择器建会话)、session/[id](聊天主界面)。 - zustand 单一 store:持有
config、wsStatus、sessions、每个会话的views(消息列表 + 待处理的权限/问题 + 元数据 + capabilities)。 - WebSocket 连接和 ApiClient 是模块级单例,不进响应式状态。
3.2 事件流 → 消息列表的 reducer
客户端的"渲染"本质是一个纯函数 reducer:把服务端推来的 WireEvent 一条条叠加到不可变的 TranscriptItem[] 上。
typescript// transcript.ts —— applyEvent(items, ev) 返回新数组
case 'block_start': // 新建一个流式 text/thinking 块
case 'block_delta': return items.map(i => i.id === ev.blockId ? { ...i, text: i.text + ev.text } : i);
case 'tool_use': // 追加一张工具卡片(pending)
case 'tool_result': // 把对应工具卡片标记成 done/error,填入结果
case 'file': // 追加下载卡片
case 'image': // 追加内联图片
store._onMessage 把 WS 消息分派进去:backlog 用 reduceEvents 整体重放历史,event 增量 applyEvent,transcript_reset 清空(/clear 时),permission_request/question_request 进各自队列。
3.3 聊天界面与各种卡片
session/[id].tsx 用一个 FlatList 渲染所有 TranscriptItem,按 type 分发到不同组件:
- 流式文本/思考块(带打字光标)、Markdown 渲染
ToolCard(工具调用,含 Bash 命令、文件路径)、Diff(Write/Edit 的 diff 卡片)PermissionSheet(权限"允许/拒绝/记住")、QuestionCards(澄清选项卡)FileCard(下载)、ImageCard(内联图片)
输入框上方有两套自动补全面板:输入 / 弹斜杠命令面板,按前缀实时过滤,数据来自 SDK 的 supportedCommands()(内置命令 + 你的技能 + 插件命令),/model 则是客户端合成的命令,点了弹模型选择器;输入 @ 弹文件提及面板,对着服务器端这个会话的工作目录树拉 fs/list、按前缀补全文件路径,目录可下钻、选中文件自动补成相对路径。两套补全把桌面端的输入体验搬到了手机,不用手敲长命令和路径。
3.4 图片内联:复用下载端点 + expo-image
send_image 来的图片,App 用 expo-image 直接拉同一个下载端点渲染(带 Authorization 头),按图片原始宽高比内联显示,点击放大到全屏:
tsxconst source = { uri: `${baseUrl}/api/sessions/${sessionId}/files/${item.fileId}`,
headers: { Authorization: `Bearer ${token}` } };
<Image source={source} contentFit="cover"
style={{ width: '100%', aspectRatio: Math.max(aspect, 0.7) }}
onLoad={(e) => setAspect(e.source.width / e.source.height)} />
3.5 文件下载:交给 Android 系统的 DownloadManager
send_file 的下载,最终用 react-native-blob-util 调 Android 系统的 DownloadManager,流式落到公共「下载」目录、带系统通知、不占应用内存:
typescriptReactNativeBlobUtil.config({
addAndroidDownloads: {
useDownloadManager: true,
storeInDownloads: true, // Android 10+ 经 MediaStore 落到公共 Downloads
notification: true, title: item.name, mime: item.mime, mediaScannable: true,
},
}).fetch('GET', `${baseUrl}/api/sessions/${sessionId}/files/${item.fileId}?token=${token}`);
(鉴权走 ?token= query,这样 DownloadManager 不需要转发自定义 header——和 WebSocket URL 的鉴权方式一致。)
四、端到端:一条消息的完整旅程
以"在手机上发一句话,让 Claude 跑个命令"为例:
- 手机 输入框发送 → zustand
sendMessage→ WS 发{ t: 'user_message', sessionId, text }。 - 服务端 gateway 收到 →
manager.sendUserMessage→ClaudeSession.sendUserMessage:先推一个user事件回广播(手机立刻显示自己的气泡),再把消息塞进inputQueue并唤醒生成器,状态置running。 - SDK 处理这一轮,吐出
stream_event(增量 token)、assistant(内容块)、需要跑Bash时触发 PreToolUse hook。 - hook 判定为
ask→ 推permission_request→ 广播给手机 → 手机弹出权限卡片,整轮挂起等待。 - 用户 点"允许" → WS 发
permission_response→ 服务端 resolve 那个挂起的 Promise → hook 返回allow→ 命令执行 →tool_result事件回流 → 手机工具卡片变成"完成"。 - 一轮结束 SDK 发
result(含本轮花费)→ 状态回idle→ 手机输入框恢复可发送。
整个过程中,文本是逐 token 流式到手机的;权限、提问是带外的双向往返;所有事件都进了服务端 backlog,于是中途断线重连也能完整回放。
五、踩坑实录
这个项目最有价值的部分,其实是几个"看起来不可能、查下去很合理"的坑。
坑 1:每发一条消息,历史就被清空
现象:第一轮正常,第二条消息一发,之前所有对话全没了,之后每次只剩 Claude 当轮回复。
排查:客户端唯一能清空历史的路径是 transcript_reset,它只来自服务端的 resetTranscript(),而后者原本的触发条件是"收到第二个 init"——当初是把"第二个 init"当成 /clear 的信号。
真相:这个 SDK 在每一轮开始都会重发 system/init。于是每条普通消息都被误判成 /clear。我翻了磁盘上真实会话的 transcript 文件确认:一段多轮对话里 session_id 始终是同一个,只有 /clear 开新会话才换 ID。
修复:reset 的判据从"第二个 init"改成"session_id 真的变了"。
坑 2:新建会话后一直卡在 "starting",要点停止才能开始
同一个 init 时机问题的另一面:init 在第一条消息流动之前根本不发,而旧代码非要等 init 才把状态从 starting 切到 idle。结果会话一直 starting,UI 把它当"忙碌",显示的是停止按钮而非发送按钮——死锁。点一下停止(interrupt)反而把状态强制切到 idle,于是"歪打正着"能发了。
修复:会话 query() 一建好就立即置 idle——它此刻已经能排队接收输入,不必等那个迟到的 init。
坑 3:下载大文件 App 直接 OOM
文件下载最初想"下到用户可见的目录",走的是 Expo 的 SAF(Storage Access Framework)。但 SAF 的 content:// 只能当拷贝源、不能当目标,写入必须 writeAsStringAsync + base64——整个文件被读成 base64 字符串塞进内存。几十上百 MB 的 APK 一来直接 OOM。
修复:放弃 SAF,改用 Android 系统的 DownloadManager(react-native-blob-util)。它由系统服务流式下载、直接落到公共 Downloads、带通知、全程不进应用内存。教训:Android 上要把大文件下到用户可见位置,用 DownloadManager,别用 SAF + base64。
坑 4:独立 APK 连不上局域网服务器
Expo Go 里一切正常,打成独立 APK 后连不上 http:// 的局域网服务。原因是 Android 的网络安全策略默认禁止明文 HTTP(CLEARTEXT),只有 Expo Go 放行。解决:在 app.json 的 expo-build-properties 里开 usesCleartextTraffic: true,并确保手机和电脑在同一子网、防火墙放行。
坑 5(顺手优化):打包又慢又大
buildArchs: ["arm64-v8a"],只编一个 ABI(默认会编 4 个),打包明显加速、APK 也更小——现代手机基本都是 arm64。
六、构建与部署
服务端:npm run build 编译,npm run start 启动(默认端口 8787)。install/install.sh 可把它装成开机自启(macOS launchd / Linux systemd --user)。首次启动会在 ~/.claude-remote/config.json 生成 Bearer token,手机端填这个 token 即可。
客户端:用 EAS 本地构建 APK:
basheas build -p android --profile preview --local
eas.json 的 preview profile = apk + credentialsSource: local。本地构建仍需要登录 Expo 账号;签名密钥库放在 app/credentials/(已 gitignore,永不上传)。装好后在 App 的 Settings 里填 http://<电脑局域网IP>:8787 + token,手机与电脑同一 Wi-Fi 即可连通。
七、安全考量
- 鉴权:随机 Bearer token,REST 与 WebSocket 都校验;token 只在本地生成、本地存储。
- 文件下载端点按
fileId作用域:只服务被显式登记的文件,绝不暴露任意路径,服务器路径不出进程。 - 权限模型:默认所有有副作用的工具都要手机端逐次批准;
send_file/send_image这类"展示/给文件"被视为安全(真正的同意是用户点下载/查看那一下)。 - 明文传输:仅限局域网内(
usesCleartextTraffic)。这是个自用、局域网内的工具,不建议把端口暴露到公网;要远程访问请套一层 VPN/内网穿透 + TLS。 - 签名密钥库与 EAS 凭证全部 gitignore,开源仓库里不含任何密钥。
八、结语
claude-remote 不大,但它把一条完整的链路跑通了:手机 → 局域网 → Node 服务 → Claude Agent SDK → 真正的 Claude Code 子进程,再把流式输出、工具权限、澄清交互、文件与图片原路返回手机。
它有意思的地方不在某个炫技点,而在于把"一个本来长在终端里的 Agent"拆成清晰的几层——SDK 封装、归一化线协议、双通道传输、纯函数化的客户端渲染——每一层都能独立理解和替换。沿着这套结构,把客户端换成 iOS、桌面端,或者把服务端的"Claude Code"换成别的 Agent,都不难。
如果你也想随身指挥自己电脑上的 AI,欢迎去仓库折腾:https://github.com/eater-altria/claude-remote。
本文基于项目截至 2026-06 的实现;细节以仓库代码为准。
评论
登录后可以发表评论,游客仍然可以完整阅读正文。
- 还没有评论,第一条留言会很显眼。