内容
# MCphone DeepSeek
把 DeepSeek 装进 [MCphone](https://github.com/november521/mcphone) 的手机里。
一个照着网页版做的对话 App:流式逐字输出、可折叠的深度思考、Markdown 排版、
多段对话,全部画在那块 120×176 的屏幕里。
这是 MCphone 的**第一个附属模组**。它同时是那套 SPI 的第一份实测——App 系统
到底能不能让外面的模组用起来,这个仓库就是答案。
- Minecraft 1.21.1 / NeoForge 21.1.248(与本体同版)
- 前置:MCphone **1.9.3+**。1.0.4 起抬到这一版:本体 1.9.3 让玩家能在「设置 → 界面大小」
里把整个手机放大,而裁剪必须走它新加的 `PhoneCanvas.clipped`——原版的 `enableScissor`
收窗口坐标、不看 PoseStack,放大之后消息和列表会被切掉一块。1.0.3 及更早在放大时就是
这个毛病。(更早的下限是 1.8.19,理由仍然成立:那一版的 SPI 扫描用了单参的
`ServiceLoader.load`,拿的是线程 context classloader,而扫描跑在并行模组派发的
worker 上,赶上一个看不见 `META-INF/services` 的就一个 App 都扫不到、还不报错。
丢的是全部 App,但玩家只会认为是刚装的这个附属坏了。本体 v1.8.19 已修)
- 只有客户端。服务端一个字节都不参与
---
## 长什么样
```
┌────────────────────────┐
│ 09:41 ▮▮▯ │ MCphone 的状态栏
├────────────────────────┤
│ ● DeepSeek + ≡ │ 新对话 / 对话列表
│────────────────────────│
│ ┌───────┐ │
│ │怎么做?│ │ 玩家:右对齐的蓝气泡
│ └───────┘ │
│ ▾ 已深度思考(用时 6 秒)│ 点一下收起或展开
│ │ 嗯,这个问题要分两步 │
│ │ 看。先确认…… │
│ │
│ **结论**:先做 A, │ DeepSeek:铺满整行的纯文字
│ 再做 B。 │
│ ┌────────────────────┐ │
│ │ int a = 1; │ │ 围栏代码块
│ └────────────────────┘ │
│ • 第一点 │
│ • 第二点 │
│────────────────────────│
│ (深度思考) v4-flash│ 开关 / 当前模型(点进设置)
│ ┌──────────────────┐ ● │ 输入框 + 发送键(生成时变停止)
│ └──────────────────┘ │
├────────────────────────┤
│ ◁ ○ □ │ MCphone 的导航栏
└────────────────────────┘
```
照着网页版的:用户有气泡、助手没有;思考过程收在一行里点开;输入框上方的
胶囊开关;圆形发送键;空页面上那句「我是 DeepSeek,很高兴见到你!」。
没照抄的三样,各有原因:
| 网页版有 | 这里没有 | 为什么 |
| --- | --- | --- |
| 联网搜索开关 | 无 | API 没有这个参数,做一个点不动的开关比没有更糟 |
| 上传附件 | 无 | 手机里没有文件系统可以挑文件 |
| 左边栏 | 单独一页 | 120 像素宽塞不下边栏,改成 ≡ 进去的一页 |
---
## 开始用
1. 去 <https://platform.deepseek.com> 拿一个 API Key
2. 游戏里打开手机 → DeepSeek → 空页面上的「去设置」(或 ≡ → 设置)
3. 点「API Key」那一行,粘贴(Ctrl+V),回车
Key 填好之后模型列表会自动从 `GET /models` 拉一次,设置页里点一下就换。
**Key 和对话都只在你自己这台电脑上。** 请求由你的客户端直连 DeepSeek,
不经过 Minecraft 服务器,服主和同服玩家都看不到任何内容。多人游戏里每个人
用自己的 Key、花自己的钱。
### 指向别的服务
设置里的「服务地址」可以填任何 OpenAI 兼容的接口(硅基流动、Ollama、
自建网关等)。注意多数服务要求把 `/v1` 带上,例如
`https://api.siliconflow.cn/v1`。填错时报错会明说是 404 而不是含糊的
"网络错误"。
### 花钱这件事
- 「单次回复上限」默认 2048 token。模型一口气能吐 38 万,而这块屏幕只有
120 像素宽——真吐那么多,滚不到底,账单却是真的。设成 0 才是不限。
- 「带上几条上下文」默认 20 条。带得越多越懂前文,每次也越贵。设成 0 就是
每次只发当前这一句。
- 「深度思考」关掉能省一截,简单问题不必让它想。
---
## 东西存在哪
两个文件,都在客户端的 `config/` 下:
| 文件 | 装什么 |
| --- | --- |
| `config/mcphone_deepseek/config.json` | API Key、服务地址、模型、几个开关 |
| `config/mcphone_deepseek/conversations.json` | 所有对话记录 |
刻意分成两个:对话记录可以随手拷给别人看、可以整个删掉重来,Key 不行;
而且对话文件写得勤得多,它写坏了不该把 Key 一起带走。
Key 是明文存的。这一点不含糊:模组没有系统钥匙串可用,自己"加密"再把密钥
放在同一台机器上,只是让人误以为它安全。实际做的是另外三件事——界面上只
显示末四位、日志里一个字都不写、除了你填的那个服务地址之外不发去任何地方。
对话不按存档分。它们是你和 DeepSeek 之间的事,和世界、和服务器都没关系,
换个服务器还该在。
---
## 图标
游戏里用的是 `src/main/resources/assets/mcphone_deepseek/textures/app/deepseek.png`,
**20×20 的 PNG-32**——这个尺寸不能改,MCphone 主屏的格子就是 20×20。
原图放在 `docs/icon-source.png`(多大都行),换图标是两步:
```bash
cp 你的新图.png docs/icon-source.png
python3 docs/make_icon.py && ./gradlew build
```
`make_icon.py` 只解决一件事:**宽高比**。
图标其实**不需要**正好 20×20——`renderIcon` 把整张贴图的 UV 0..1 铺满格子,多大
的图都显示得出来。但那个格子是**正方**的(接口签名里只有一个 `size`),源图不是
正方时横竖缩放倍数不一样,图会被压扁:这只鲸鱼 380×322,直接丢进去会被压掉 15%
的宽度。脚本按原比例缩成 20×17,上下补透明边居中。
缩放用的是最近邻,不是面积平均。App 图标是纯色块拼出来的 logo,形状全靠边界
撑着,而面积平均恰好把边界抹成中间色——试过,白肚子和眼睛都化开了,整只鲸鱼
成了一个蓝团。源图要是带渐变或照片质感,这个结论会反过来。
这张图会出现在两个地方:主屏那一格,以及对话页一条消息都没有时画在正中间
的那张。后者是刻意的,从主屏点进来有一条看得见的线。
---
## 从源码构建
```bash
./gradlew build # 产物在 build/libs/
./gradlew runClient # 开发环境(会自动带上 libs/ 里的 MCphone)
```
编译对着 `libs/mcphone-1.9.3.jar`。换本体版本时**两处都要改**:把新 jar 放进
`libs/`,同时改 `gradle.properties` 里的 `mcphone_version`——那个数同时拼出
文件名和 `mods.toml` 里的 versionRange 下限,对不上就会出现"编译对着新版、
运行时却宣称支持旧版"这种最难查的错配。
### 测试
```bash
./docs/run-tests.sh # 先跑过一次 ./gradlew build
```
三组断言,145 条:
- **对齐**(`docs/LayoutTest.java`)。不测好不好看,只测三件算得出来的事:同一行
上的东西中线差不超过 1 像素、排进框里的东西不许超出那个框、点击区和画出来的
位置用的是同一个算式。这一组是补出来的——界面出过一整批"差一像素"的毛病,
成因都是各处手写偏移,而那种错误在代码里完全看不出来。
- **Markdown 解析**(`docs/MdTest.java`)。一半用例是"不该发生什么"——
`3 * 4 * 5` 不该变斜体、`__init__` 不该被下划线语法吃掉、未闭合的围栏在流式
输出中途也得当代码块画。模型的输出里代码含量很高,这些才是天天撞上的。
- **网络层**(`docs/DeepSeekClientTest.java` + `docs/fake_deepseek.py`)。起一个
假的 DeepSeek 服务端,把流式那条路径整个跑一遍:思考与正文的增量、流里夹
一个坏块该跳过、HTTP 402 要认成"余额不足"而不是笼统的网络错误、中途取消
要真的把连接断掉(服务端那边验得到)。
跑法不走 `./gradlew test`:这两个要 Minecraft 的类路径(`Turn` 的错误信息是
`Component`),而给这个工程接一套测试框架,要引的东西比被测的代码还多。本体
`docs/` 下那几个测试是同一个思路。
---
## 代码怎么分的
```
MCphoneDeepSeek @Mod 主类。几乎什么都不做,App 由 SPI 自己被发现
client/
DeepSeekApp implements IPhoneApp —— 对 MCphone 的全部接触面就这六个方法
net/
Turn 一次请求的实时状态。HTTP 线程往里写,渲染线程读,
两条线程只在这一个类上碰面
DeepSeekClient 发请求、解 SSE。不碰界面、不碰对话记录
ModelCatalog GET /models
store/
ChatMessage 一条消息
Conversation 一段对话
ConversationStore 落盘。只在客户端线程上读写
DeepSeekConfig Key 与几个开关
JsonFile 读写 json,先写 .tmp 再挪过去
ui/
DeepSeekPage implements IPhonePage —— 三页之间的路由
ChatView 对话页
HistoryView 对话列表
SettingsView 设置
Md Markdown 解析
Blocks 排好版、能画的块
MessageLayout 排好版的一条消息
Theme / Ui 配色快照与绘制原语
```
一条线贯穿始终:**网络线程只往 `Turn` 里灌字符串**。它不碰
`ConversationStore`、不碰任何界面对象、也不碰 `Minecraft` 实例。回复写完之后,
由渲染线程在下一帧收进对话记录。反过来做是这类功能最常见的崩法,而且症状
极难认——`ConcurrentModificationException` 出现在某个渲染方法里,堆栈上一个
字都不会提到网络。
---
## 给后来的附属作者
写完这一个之后,有几件事值得先知道:
**那套 SPI 是真开放的。** 从零到一个能用的 App,对本体的接触面只有
`IPhoneApp` 的六个方法加一行 `META-INF/services`。本体不需要认识你,也没有
任何一处需要它配合改动。
**`IPhonePage` 值得用。** 覆盖 `openPage()` 返回一页,你的界面就画在手机屏幕
【里】,共用状态栏、导航栏、壁纸和返回键。走 `onPress()` 自己 `setScreen` 会
让你的 App 变成"从手机里弹出来的一个窗口",那是两种完全不同的观感。
**绘制工具得自己写一份。** 本体的 `GuiUtil` 什么都有,但它在 `core.client` 下,
而 `MCphoneApi` 第五条写明只有 `api` 包对附属有效。所以 `Ui.java` 里那些圆角、
圆、贴图、截字是刻意的重复。有一处要特别当心:`GuiGraphics` 那条
`blit(ResourceLocation, ...)` 不开混合,半透明和抗锯齿的图会画坏——照抄
`Ui.texture()` 里 `enableBlend` 那三行。
**`capturesKeyboard()` 有输入框就必须返回 true。** 否则玩家打拼音按到 e 会命中
背包键,手机当场关掉、输入全丢。中文用户躲都躲不开。
**`PhoneStyle` 每帧现取,别缓存成常量。** 接口做成接口而不是一堆
`public static final int`,正是因为常量会被编译器内联进你的 class 文件——本体
换配色之后,你画的还是编译那天的颜色,而且没有任何迹象表明哪里不对。
---
## 许可
[MIT](LICENSE)。
与 DeepSeek 官方无关,是一个非官方客户端。
连接信息
猜你喜欢
暂无推荐服务器