Releases: Cmochance/mochan-linux
v1.0.2
What's Changed
- docs: fetch tags before VPS version build by @Cmochance in #8
- Complete app backend persistence queue by @Cmochance in #9
Full Changelog: v1.0.1...v1.0.2
v1.0.1
mochan-linux v1.0.1
v1.0.1 is a documentation and release-readiness patch for the current
server-backed desktop state.
Added
- README preview screenshots generated from the local app preview at
http://127.0.0.1:3001/against the local Go backend. - Screenshot coverage for the desktop, app launcher, File Manager, Terminal,
Settings, and Browser start page underdocs/img/. - README backend completion summary that states which applications are already
backed by real server APIs or server-side persistence. - Clear README separation between applications still being migrated to backend
support and local-only tools or games that do not yet need dedicated backend
APIs.
Backend Status Snapshot
Implemented server-backed applications through P15:
- System and desktop: login gate, File Manager, Terminal, Trash, System
Monitor, Task Manager, Audit Log, Settings, and Browser. - File and network tools: Text Editor, Markdown Editor, Image Viewer, Download
Manager, API Tester, RSS Reader, Git Client, SSH Client, FTP/SFTP Client,
Bookmarks, Weather, and Email Client. - Personal data apps: Chat App, Notes, Calendar, and Notebook.
Still queued for backend migration:
- Spreadsheet, Mind Map, Presentation, Pomodoro, Habit Tracker, Dictionary,
Translator, Photo Album, Camera, Voice Recorder, Music Player, Video Player,
PDF Reader, and Paint.
Deferred until there is a concrete sync, history, export, preset, or
leaderboard requirement:
- Calculator, Clock, Color Picker, Base64 Tool, QR Code Generator, Password
Generator, Regex Tester, JSON Editor, board games, puzzle games, White Noise,
and Metronome.
Validation
- Generated and checked README preview screenshots locally.
npm run buildpassed.GOCACHE=/Users/alysechen/alysechen/github/mochan-linux/.tmp/go-cache make build VERSION="v1.0.1-docs"passed.- The GitHub Release workflow is triggered by pushing the
v1.0.1tag.
Notes
- This patch does not change runtime application code.
- The in-app Browser remains a server-side proxy feature: authenticated browser
requests are fetched by the mochan-linux backend, so it can reach addresses
available from the server host.
v0.9.0
mochan-linux v0.9.0
本版本主线: 设置真接入。Settings 不再是纯 localStorage 的 stub —— 主题 / 语言 / 桌面壁纸由后端
/api/settings持久化(写到/var/lib/mochan/settings.json),换浏览器也保留。新增完整的壁纸桶/var/lib/mochan/wallpapers/,可以从设置面板里直接上传图片做壁纸。Settings 应用整个重写成多页签:外观 / 语言 / 关于,关于页接/api/sys/stat实时显示主机信息。
中文
后端 internal/settings
两个组件:
Store: 单文件 JSON 持久化。原子写入(写到.tmp然后os.Rename),0640 权限,只接受 enum 值合法的 patch(theme ∈ {ink, dark, light},language ∈ {zh, en})。Bucket: 用户上传的壁纸放在<DataDir>/wallpapers/。文件名校验拦截../// NUL,扩展名白名单(.jpg / .jpeg / .png / .webp / .gif / .bmp / .avif)。
端点
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /api/settings/ |
返回 {theme, language, wallpaper} |
| PATCH | /api/settings/ |
合并 patch,持久化,返回新状态 |
| GET | /api/settings/wallpapers/ |
列出 bundled + user 壁纸,返回 {name, url, size, source} |
| POST | /api/settings/wallpapers/ |
multipart 上传,字段名 file |
| GET | /api/settings/wallpapers/{name} |
流式返回用户上传的壁纸文件(带 5min Cache-Control) |
| DELETE | /api/settings/wallpapers/{name} |
删除用户壁纸 |
前端 apps/Settings.tsx 完全重写
旧 stub 是 372 行 localStorage UI 但实际不生效(theme 在 useSystemStore,Settings 自己又写一份),改成 200 行的三页签:
- 外观: 主题三按钮(水墨 / 夜色 / 宣纸)+ 壁纸 4 列网格(bundled 走
./wallpaper-xxx.jpg,user 走/api/settings/wallpapers/xxx),user 壁纸 hover 出现红色删除按钮。点选 = 立即生效(zustand 改 → useSettingsSync 钩子 debounce 300ms 写回服务器)。上传按钮调/api/settings/wallpapers/POST。 - 语言: 中文 / English 二选一。
- 关于: 水墨 Linux 品牌区 + GitHub / Releases 外链 + 实时主机信息卡(主机名 / OS / 内核 / 架构 / 已运行 / CPU 核数 / 内存 / 磁盘挂载数 / 负载 / 当前用户)。
useSettingsSync 钩子
// 在 Layout 里挂上,每次 booted 都跑一次
useSettingsSync()逻辑:
- 进入 Layout(已登录) → fetch
/api/settings/→ 拿到{theme, language, wallpaper}→ 用setTheme/setLanguage/setWallpaper灌进 zustand。服务端是真理之源,把 localStorage 作为暖启动缓存——先用 localStorage 渲染屏幕,再用服务端值覆盖,体感不闪烁。 - zustand
subscribe监听 theme / language / wallpaper 变化,300ms 防抖后 PATCH 服务器。 lastWritten记忆最近一次同步内容,避免回声(服务器响应触发的 setState 不应再 PATCH)。
副作用:StatusBar 上那个右上角的语言切换按钮现在也会真持久化到服务端——任何路径改的 zustand 都会被 hook 同步,Settings 应用不需要垄断这个能力。
桌面壁纸打通真 FS
Desktop / LockScreen / AuthGate 三处之前都硬编码 url(./${wallpaper}.jpg)。新增 wallpaperUrl(id) 帮手:
export function wallpaperUrl(id: string): string {
if (id.startsWith('wallpaper-')) return `./${id}.jpg`;
return `/api/settings/wallpapers/${encodeURIComponent(id)}`;
}useDesktopStore.WallpaperId 类型从五个字面量 union 放宽到 string,所以用户上传的任意图名都能作为合法 wallpaper id 存。
试一下
- Settings → 外观 → 上传新壁纸:选张本机图片 → 网格里立刻出现 → 点它 → 桌面 / 锁屏 / 登录页背景同步切换。
- 服务器端能验证:
ssh dochenmo 'cat /var/lib/mochan/settings.json && ls /var/lib/mochan/wallpapers/'看到{"theme":"ink","language":"zh","wallpaper":"<your.png>"}和实际文件。 - 在另一台机器(或匿名隐私窗口)登录:壁纸已经是你刚才设的——服务端拿回来的不是 localStorage。
故意不做的
- 端口设置: 改
MOCHAN_LISTEN涉及 systemd 重启 + 反向代理上游联动,不该是 in-app 一键。仍然走/etc/mochan/config.env+systemctl restart mochan。 - 密码修改: 暂未做。理由同上(密码哈希在
config.env,需要原子改文件 + 重启)。Stage 11 之前应当补,但本版本不投入。 - 2FA / OAuth: 仍是单密码,记忆里硬规则警告未变。
English
Backend
internal/settings adds a Store (atomic JSON write to <DataDir>/settings.json, 0640) and a Bucket for user-uploaded wallpapers under <DataDir>/wallpapers/. Endpoints under /api/settings/: GET / PATCH for the document, GET / POST / GET {name} / DELETE {name} for the wallpaper bucket. Patch values are validated against enums; uploads are filename-guarded against traversal and extension-whitelisted.
Frontend
apps/Settings.tsx rewritten as a tabbed page (Appearance / Language / About). Theme buttons, language toggle, wallpaper grid with hover-to-delete and an upload button. About tab renders live host info via /api/sys/stat.
A new useSettingsSync() hook runs in Layout. It bootstraps zustand stores from the server on entry, then debounce-writes back any local mutation (300 ms) — so the StatusBar language toggle persists too, not just the Settings app.
Desktop / LockScreen / AuthGate resolve wallpaper URLs through a new wallpaperUrl(id) helper that routes bundled IDs to the static bundle and any other ID to /api/settings/wallpapers/<name>.
Verify
ssh user@host 'cat /var/lib/mochan/settings.json && ls /var/lib/mochan/wallpapers/'After uploading a wallpaper from Settings, you'll see the file on disk and the JSON updated. Logging in from a fresh browser shows the same theme/wallpaper because the server is the source of truth — localStorage is just a warm cache.
Out of scope
- Listen port is intentionally not in the UI; changing it requires editing
/etc/mochan/config.envplus a service restart and a reverse-proxy update. - Password change is not in this release — same constraint (password hash lives in
config.env). - 2FA / OAuth still out of scope; this is single-user by design.
v0.8.0
mochan-linux v0.8.0
本版本主线: 终端不再因为网络抖一下就死。WebSocket 断了 PTY 不死,客户端用同一 session ID 重连即接回原 shell,服务端 256 KiB 环形缓冲会重放最近输出,xterm 看到的就像 tmux 一样无缝。
htop/vim/ 编译输出在地铁信号断、笔记本短暂休眠、CF 间歇性掐 WebSocket 时都不丢。
中文
后端: 会话与 WebSocket 解耦
internal/pty 重构,新增 session.go:
Session: 一个长寿命的 PTY 包装。持有*os.File(PTY) +*exec.Cmd(bash) +ringBuffer(256 KiB) + 订阅者集合。一个独立 goroutine 持续从 PTY 读,每块同时 (a) 写入环形缓冲、(b) 广播给所有订阅者通道。Manager: 进程级单例,持有map[string]*Session,带 30 秒一次的 reaper goroutine。reaper 把"订阅者数 == 0 且 lastIdle 超过 idleTTL"的 session 标记关闭(默认 idleTTL = 5 分钟)。/ws/pty: 不再每次 WebSocket 进来就pty.StartWithSize起新 shell。改为Manager.GetOrCreate(id, opts)—— 已存在就附加(共用 PTY + 缓冲),不存在就用这个 ID 创建新 session。
协议: 握手控制帧 + 二进制重放
升级成功后,服务端现在先发一个 JSON 文本帧:
{
"type": "attached",
"session_id": "abc123…",
"cols": 80,
"rows": 24,
"buffer_len": 12345
}紧接着发 buffer_len 字节的二进制帧,内容是环形缓冲的完整快照(可以是 0 字节)。
之后流量回到 v0.2.0 起就有的格式:
- 二进制帧 = PTY 输出/输入
- 文本帧 = JSON 控制消息(目前只有
{type:"resize", cols, rows})
前端: apps/Terminal.tsx 自动重连
- 组件挂载时用
crypto.randomUUID()生成 session ID,放在闭包里(不写 localStorage,见下)。 - WebSocket URL 带
?session=<id>,断开后跑指数退避重连(初始 600 ms,倍率 1.6,封顶 8 s,加随机抖动)。 - 重连成功在终端里打印
[已重新连接](绿色,2 行,不打扰主流)。 - 第一次连接收到
attached控制帧后,缓冲重放只是写进 xterm,xterm 自己根据 ANSI 序列(光标定位 / 清屏 / 重绘)正确恢复界面——TUI 比如 vim/htop 在 SIGWINCH 后也会自己全屏重绘,所以"重连后画面正确"是协议自然产物。
为什么 session ID 不持久化
刻意的:
- 多窗口需要独立 shell。如果 ID 写 localStorage,同一浏览器开两个终端窗口会争抢同一个 PTY,鼠标在哪个窗口打字都会"穿越"。
- 浏览器硬刷新后想要"接续昨天的会话"是
tmux/screen的工作,不该让本工具承担。tmux 已经做得很好,直接tmux new -s work+tmux attach -t work即可。 - 当 session 被 reaper 回收后,继续用同一 ID 重连服务端会新建一个同 ID 的 session(因为 ID 不可达 → 当作新建),静默切到一个空 shell,用户看到"重连成功但是 htop 没了"会很困惑。强制每次新组件挂载就新 ID,行为一致。
空闲回收: 5 分钟无客户端 → kill PTY
Manager.reapLoop 每 30 秒扫一次:订阅者数 == 0 且 lastIdle 超过 5 分钟的 session 调用 markClosed(),关 PTY、process.Kill + Wait、关所有订阅者通道、从 map 移除、写日志 pty: reaping idle session <id>。
正常场景:
- 你打开一个终端窗口 → 创建 session A → 你点关闭按钮(window 关闭) → WebSocket 断 → reaper 5 分钟后清掉 A。中间这 5 分钟你又打开一个新终端就是新 session(因为新组件 = 新 ID),A 自然死。
- 网络断 30 秒重连 → 你的客户端用同 ID 重连 → reaper 不会动这个 session,因为 lastIdle 在 30 秒前被刷新成 now,但更重要的是你重连后订阅者数从 0 变 1,reaper 条件不满足。
已知边界
- 服务进程崩溃 / 重启 → 所有 session 死。这是单进程内存型 session,不持久。systemd 重启服务后 PTY 全没,前端会看到重连失败一段时间然后 reconnect 时 server 给一个新 session ID(空 shell)。要扛进程崩溃就得用 tmux,这是一句话能解的事所以本版本不投入 disk-backed session。
- 共享一个 ID 的多客户端: 协议支持(同时广播给所有订阅者),但前端没暴露这个能力,每个 Terminal 组件实例都有独立 ID。
- 环形缓冲 256 KiB: 大概是 80×24 全屏 ANSI 重绘 5–8 次。tmux / vim 这类自带屏幕状态的应用没问题;
tail -f /var/log/big.log这种纯 append 流断 1 分钟可能丢失一些早期行(屏幕重绘可恢复但是 xterm scrollback 里只剩缓冲长度)。
English
Backend: PTY lifetime decoupled from WebSocket
internal/pty now has a Session/Manager pair. A Session wraps the PTY file, the *exec.Cmd, a 256 KiB ring buffer, and a subscriber map; a goroutine streams every PTY read into the buffer and fans it out to subscriber channels. A process-wide Manager owns the named-session map and runs a 30 s reaper that kills sessions whose subscriber count has been zero for >5 minutes.
Protocol: handshake control frame + binary replay
After upgrade, the server sends a JSON text frame {"type":"attached", "session_id":..., "cols":..., "rows":..., "buffer_len":N} followed by N bytes of binary replay. The rest of the protocol is unchanged.
Frontend: random session ID + exponential backoff
apps/Terminal.tsx mints a random session ID per component instance, attaches it to the WebSocket URL, and on close auto-reconnects with jittered exponential backoff (600 ms → 8 s, factor 1.6). After a successful reconnect, the terminal prints [已重新连接] inline.
Intentional non-features
- Session ID is not persisted to localStorage. Two terminal windows in the same browser get independent shells; a hard refresh starts a fresh shell. Use tmux for "want my shell back tomorrow."
- Sessions die with the server process — they live in memory. systemd restart kills all PTYs.
- Buffer is 256 KiB (~5–8 full-screen redraws); long pure-append streams may lose early lines on a 1-minute disconnect.
v0.6.0
mochan-linux v0.6.0
本版本主线: 应用之间真的串起来了。FileManager 双击文件不再永远落到 CodeMirror 模态框,按扩展名路由到对应的桌面 App:
.md进 MarkdownEditor、图片进 ImageViewer、源码 / 文本进 TextEditor。这些 App 在被这样打开时会直接走/api/fs/read(或图片的downloadURL)从主机文件系统加载,保存按钮也直接写回真文件——不再是浏览器下载。
中文
路由表 (lib/openFile.ts)
按扩展名分派,文件管理器双击触发:
| 扩展名 | 目标 App | 行为 |
|---|---|---|
.md / .markdown |
MarkdownEditor | /api/fs/read 加载,保存按钮 → /api/fs/write |
.jpg / .jpeg / .png / .gif / .webp / .svg / .bmp / .ico / .avif |
ImageViewer | /api/fs/download 直链作为 <img src>,自动测尺寸 |
.txt / .log / .conf / .cfg / .ini / .env / .sh / .bash / .zsh / .go / .py / .rs / .js / .jsx / .ts / .tsx / .json / .yaml / .yml / .toml / .html / .css / .sql / .rb / .php / .java / .kt / .c / .cpp / .h / .hpp / .cs 等 |
TextEditor | /api/fs/read 加载,Ctrl+S → /api/fs/write |
| 其它文本(≤8 MiB) | FileManager CodeMirror 模态 | 兜底,与 v0.3.0 一样 |
| 二进制 / 大文件 | 下载 | 兜底,与 v0.3.0 一样 |
数据通道: WindowData.payload
useWindowStore 的 WindowData 加了 payload?: Record<string, unknown> 字段。openWindow(appId, title, { payload: { path, source: 'filemanager' } }) 把上下文塞进去,App 用 usePayloadPath(windowId) 取出。
这是一个可选通道——没 payload 启动的窗口(开始菜单 / Dock 点击)走原来的本地存储行为完全不变。
三个被改造的 App
- TextEditor: 收到 payload 时,丢掉 localStorage 的草稿,改成显示远端文件内容。
Ctrl+S和 "保存" 按钮直接写/api/fs/write。无 payload 时保留原来的"下载到浏览器" 行为。 - MarkdownEditor: 同上。继续支持实时 Markdown 预览。
- ImageViewer: 收到 payload 时,把
/api/fs/download?path=...URL 当作<img src>直接渲染,顺便用Image()探测自然尺寸填进images数组。其它已有功能(缩放 / 旋转 / 翻转 / 拖放上传 / 幻灯片)不变。
为什么不路由 .json 到 JSONEditor?
JSONEditor 还是个本地 stub,没有 fs-aware 模式。.json 在路由表里被归到 TextEditor(CodeMirror 自带 JSON 高亮),后续可以单独把 JSONEditor 改造好再切。
未做
- 反向操作不存在: TextEditor 内置的 "新建/打开本地文件 / 下载" 按钮没有"保存到主机文件系统"的快捷入口。临时方案是从 FileManager 启动以拿到 payload。
- 没有 "用其它 App 打开" 的右键菜单——只有默认路由,改路由要改
lib/openFile.ts。 - ImageViewer 的"下一张/上一张"在 payload 模式下只有一张图,翻页无效;需要改成支持远端目录列表。
English
Extension routing (lib/openFile.ts)
Double-clicking a file in FileManager now routes by extension:
| Ext | Target App | Behaviour |
|---|---|---|
.md / .markdown |
MarkdownEditor | loads via /api/fs/read, save → /api/fs/write |
| common image extensions | ImageViewer | uses /api/fs/download as the <img> source, probes natural size |
| common text / code extensions | TextEditor | loads via /api/fs/read, Ctrl+S → /api/fs/write |
| other text (≤8 MiB) | FileManager CodeMirror modal | fallback (unchanged) |
| binary / oversize | download | fallback (unchanged) |
Data channel: WindowData.payload
useWindowStore.WindowData gained payload?: Record<string, unknown>. The opener writes openWindow(appId, title, { payload: { path, source: 'filemanager' } }); consumers read usePayloadPath(windowId). Apps launched without a payload keep their original local-storage behaviour.
Apps modified
TextEditor, MarkdownEditor, ImageViewer now opt in to the payload protocol. When launched with a path, they fetch the file from /api/fs and override the default content; save buttons write back to /api/fs/write instead of triggering a browser download.
Not yet
- TextEditor's stand-alone "save" still downloads to the browser unless it was launched with a payload.
- No right-click "open with…" menu yet — defaults are hard-coded in
lib/openFile.ts. - ImageViewer in payload mode shows a single image; the prev/next paging assumes a directory walk, which we haven't wired yet.
v0.7.0
mochan-linux v0.7.0
本版本主线: 手机能用了。窄屏 (< 768 px) 下窗口自动覆盖整屏(只在 StatusBar 与 Dock 之间留必要空间),拖动 / 调整大小 / 调整边把全部禁掉,标题栏的 12 px 红黄绿三个圆点放大到 22 px 以满足触屏点击区域。Dock 改成 56 px 高、横向滚动,图标列再多也能滑出来。
中文
WindowFrame 改造
新加 useIsMobile() 检测视口 < 768 px。命中时对每个窗口套一组 mobile-only 样式覆写:
| 字段 | 桌面 | 移动 |
|---|---|---|
left |
win.x |
0 |
top |
win.y |
28 (StatusBar 高度) |
width |
win.width |
100vw |
height |
win.height |
calc(100vh - 84px) (减去 StatusBar 28 + Dock 56) |
borderRadius |
8px |
0 |
| 拖动 | 标题栏 cursor=move | handleMouseDown 早退,无任何拖动反应 |
| 调整大小 | 渲染 8 个 resize handle | 不渲染 |
| 红黄绿圆点尺寸 | 12 × 12 px | 22 × 22 px |
无 payload 桌面行为完全不变。
Dock 改造
| 字段 | 桌面 | 移动 |
|---|---|---|
| 高度 | 64 px | 56 px |
| 圆角 | 16px 16px 0 0 |
12px 12px 0 0 |
| 最大宽度 | 90vw |
100vw |
| 横向滚动 | 不允许 | overflow-x-auto |
验证
把 Chrome devtools 切到 iPhone 14(390 × 844)分辨率,逐个 App 试:
- 登录页:已经是
max-w-sm居中表单,直接 OK,密码框 / 按钮 tap 命中正常。 - 终端:xterm.js 5.x 触屏点击会
term.focus()唤起系统软键盘,选择 / 长按复制原生支持。 - 文件管理器:列表自适应 100% 宽,操作按钮(下载 / 重命名 / 删除)tap 命中正常,文件名长会截断 + 悬浮 title。
- 系统监控 / 审计日志 / 任务管理器:卡片 / 表格在窄屏会横向滚,功能不残废,但视觉密度偏大。
- StatusBar:右上角图标在窄屏上会被语言开关 / 用户名挤压,但都还点得到。
没做的
Desktop桌面图标仍按桌面 drag-and-drop 设计,长按拖动暂未实现。AppLauncher分类网格在 < 768 px 下会单列偏挤,未做专门列宽适配。- 没有"手势返回"(滑动关闭窗口) / "三指切换" 这类原生体验。
- 没有 PWA
manifest.json/ Service Worker——本版本不离线、不可加到主屏。
这是一次可用性验收,不是移动端 UX 终态。后续 Stage 10(Settings 实页)会把"主题密度 / 触屏模式"这类系统级设置打通,届时一并优化。
English
WindowFrame mobile overrides
useIsMobile() detects viewport < 768 px. When true:
- left=0, top=28 (StatusBar), width=100vw, height=calc(100vh - 84px) (StatusBar 28 + Dock 56).
- Drag (title bar) and resize (8 edge/corner handles) are disabled.
- The 12-px traffic-light buttons grow to 22 px so they exceed Apple HIG's 44-px touch target across two finger axes when combined with padding. Acceptable.
- Border-radius 0 — fullscreen looks intentional.
Dock mobile mode
- 56-px tall (vs 64 px desktop), 100vw wide (vs 90vw),
overflow-x-autoso the long pinned + open + trash icon list can scroll horizontally. - Desktop behaviour unchanged.
Verified at 390 × 844
Login, terminal (xterm.js handles touch out of the box), file manager, audit log, system monitor, task manager — all reachable. Some density issues (StatusBar icons crowd the right edge, AppLauncher category grid is tight) but no functional regressions.
Not yet
Desktopicons still designed for mouse drag-and-drop. Long-press drag is not wired.- AppLauncher category grid is cramped on phone widths.
- No PWA
manifest.jsonor Service Worker. Offline / install-to-home-screen is out of scope here. - No native gesture mappings (swipe-to-close, etc.).
This is a viability pass to make the system usable from a phone in an emergency, not the final mobile UX.
v0.5.0
mochan-linux v0.5.0
本版本主线: 加上安全审计。任何敏感操作都会落到一份只追加的 JSONL 日志里——登录成功、登录失败、退出登录、文件写 / 删 / 移 / 上传、进程被杀,带真实客户端 IP(透过 Cloudflare 与反向代理的
X-Forwarded-For/CF-Connecting-IP)。新增审计日志桌面应用,可按事件类型筛选,5 秒自动刷新,顶栏统计登录成功 / 失败次数。
中文
后端 internal/audit
- JSONL 写入: 每行一个
Event,字段为time / type / actor / ip / outcome / detail。文件位于<DataDir>/audit.log(默认/var/lib/mochan/audit.log),权限 0640,只追加。 - 轮转: 单文件超过 10 MiB 时改名为
.1重开;最多保留一份历史,旧的.1会被覆盖。单用户低频场景下不需要 logrotate 那一套。 - 静默容错:
Logger.Log是 nil-safe,审计写入失败永远不会中断底层操作(登录、文件写等)。 - 真客户端 IP:
audit.ClientIP(r)按优先级取CF-Connecting-IP→X-Real-IP→X-Forwarded-For(取首段) →RemoteAddr。在linux.mochance.xyz(CF orange-cloud → NPM → mochan)上确认能拿到原始访问者 IP。
已捕获事件
| 类型 | 触发位置 | detail 字段 |
|---|---|---|
auth.login.success |
/api/auth/login 验证通过 |
(无,actor 即用户名) |
auth.login.fail |
/api/auth/login 401 前 |
(无,outcome=deny) |
auth.logout |
/api/auth/logout |
(无) |
fs.write |
/api/fs/write 成功后 |
path, size |
fs.mkdir |
/api/fs/mkdir 成功后 |
path, parents |
fs.delete |
/api/fs/?path=... DELETE 成功后 |
path, recursive |
fs.move |
/api/fs/move 成功后 |
from, to |
fs.upload |
/api/fs/upload 成功后 |
dir, files: [{name, size}] |
sys.kill |
/api/sys/kill 成功发信号后 |
pid, signal |
故意不记录的事件:fs.list / fs.read / fs.stat / fs.download / sys.stat / sys.processes —— 这些是高频读操作,记进审计日志反而把真正可疑的事件埋掉。
查询接口
GET /api/sys/audit/?limit=200&type=auth.login.fail—— 返回最近 N 条事件,按时间倒序。type可选,精确匹配。- 透明合并
audit.log与audit.log.1两个文件,所以即使最近发生过轮转也能回溯。 - 单次最多 5000 条;有截断时返回
more: true。
前端 apps/AuditLog.tsx
- 注册在"系统工具"分类,图标
ScrollText。 - 顶栏:事件类型下拉(
所有事件/ 9 种事件类型)、自动刷新开关(默认开,5 秒一次)、手动刷新按钮、清除筛选按钮。 - 表格:时间(本地时区) / 事件(带颜色的 chip) / 用户 / IP / 结果(
ok灰,deny红,error橙) / 详情(把detail按key=value拍平)。 - 顶栏右侧实时统计当前视图里登录成功 / 失败次数,方便一眼看出爆破。
端到端验证
部署到 https://linux.mochance.xyz 后用 /tmp/mochan-deploy/credentials.txt 跑了一次完整流程:
auth.login.fail admin 38.150.4.233 deny (wrong password)
auth.login.success admin 38.150.4.233 ok
fs.mkdir admin 38.150.4.233 ok {path:"/tmp/audit-test", parents:false}
fs.write admin 38.150.4.233 ok {path:"/tmp/audit-test/x.txt", size:2}
fs.delete admin 38.150.4.233 ok {path:"/tmp/audit-test", recursive:true}
auth.logout admin 38.150.4.233 ok
auth.login.success admin 38.150.4.233 ok (re-login)
仓库整理(同版本附赠)
- 删掉了 v0.1.0 → v0.4.0 三条 commit 里残留的
Co-Authored-By: Claudetrailer,https://github.com/Cmochance/mochan-linux/graphs/contributors现在只剩 Cmochance。
已知未做
- 暂未提供"导出审计日志为 CSV / 下载原始 JSONL 文件"的按钮 —— 现阶段直接
cat /var/lib/mochan/audit.log或走/api/sys/audit/?limit=5000拿就可以。 - 没有"按 IP 自动锁定"逻辑(类似 fail2ban) —— 仍然推荐在反向代理或操作系统层做。审计日志的目的是事后可追溯,不是 IPS。
- 终端会话结束 / WebSocket 断开尚未审计;
/ws/pty内部的 shell 命令也不会被审计(终端字面上是"shell 直连",审计意义有限,且记录所有键盘输入会冲掉真正的安全事件)。
English
Backend internal/audit
- JSONL append-only writer at
<DataDir>/audit.log(default/var/lib/mochan/audit.log), perms0640. Each row carriestime,type,actor,ip,outcome,detail. - Rotates to
.1when the active file exceeds 10 MiB; we keep one rotation. Adequate for single-user, low-volume audit; no logrotate dependency. Logger.Logis nil-safe — an audit-write failure never breaks the underlying operation (login, file write, etc.).- Real client IP:
audit.ClientIP(r)prefersCF-Connecting-IP→X-Real-IP→ first hop ofX-Forwarded-For→RemoteAddr. Verified end-to-end on thelinux.mochance.xyzdeployment (Cloudflare → NPM → mochan).
Captured events
auth.login.success, auth.login.fail, auth.logout, fs.write, fs.mkdir, fs.delete, fs.move, fs.upload, sys.kill. Each event records actor, IP, outcome (ok / deny / error) and event-specific detail (path, size, recursive, signal, etc.).
Deliberately not recorded: fs.list / fs.read / fs.stat / fs.download / sys.stat / sys.processes. High-frequency reads drown out the actually suspicious events.
Query endpoint
GET /api/sys/audit/?limit=200&type=auth.login.fail— newest-first, transparently mergesaudit.logandaudit.log.1so a recent rotation does not lose events.- Single response max 5000 rows;
more: trueindicates truncation.
Frontend apps/AuditLog.tsx
Registered as "审计日志" / "Audit Log" under system tools (ScrollText icon). Type-filter dropdown, 5 s auto-refresh toggle, manual refresh, clear-filter. Table renders time / typed-and-colored badge / actor / IP / outcome / flattened detail. Toolbar shows live login-success and login-fail counts.
Repo housekeeping (same release)
- Stripped
Co-Authored-By: Claudetrailer from the v0.1.0 → v0.4.0 commits. The contributor graph now correctly shows only Cmochance.
Known gaps
- No CSV / raw-JSONL export button yet — the file at
/var/lib/mochan/audit.logis plain JSONL, and/api/sys/audit/?limit=5000already returns the same content. - No IP-based auto-blocking (no fail2ban-equivalent). Audit is for forensic trail, not IPS — keep IP throttling at the reverse-proxy / OS layer.
- PTY sessions are not audited at the keystroke level — the keyboard stream goes straight into a real shell, and recording every keystroke would drown the security signal we actually care about.
Verify and install
curl -LO https://github.com/Cmochance/mochan-linux/releases/download/v0.5.0/mochan-linux-amd64.tar.gz
curl -LO https://github.com/Cmochance/mochan-linux/releases/download/v0.5.0/mochan-linux-amd64.tar.gz.sha256
sha256sum -c mochan-linux-amd64.tar.gz.sha256
tar xzf mochan-linux-amd64.tar.gzThe release was built by GitHub Actions (.github/workflows/release.yml) on tag push; both linux/amd64 and linux/arm64 artifacts are signed by their per-file .sha256.
v0.4.0 — Phase 3
mochan-linux v0.4.0
本版本主线:首次公开,把"浏览器即 Linux 桌面"从概念做到端到端可用。一周内连续推完 Phase 0–3,单 Go 二进制(13 MB,前端
embed.FS注入)就能部署一个让你从任何设备打开https://your.domain直接拿到真终端、真文件系统、真进程管理、真系统监控的水墨风工作站,专门用来测试自己开发的 Linux 应用。
中文
一、登录与认证 (v0.1.0 → v0.1.1)
- 登录门: bcrypt 密码 + HS256 JWT,Token 写到
HttpOnly Secure SameSite=Laxcookie。前端AuthGate启动时调/api/me,未登录显示水墨风登录屏,未通过认证看不到桌面任何内容。 - 退出登录: StatusBar 右上角用户下拉,点击调
/api/auth/logout清 cookie,立刻回到登录屏。 - CLI 子命令:
mochan hash-password从 stdin 读密码出 bcrypt 哈希,mochan gen-secret出 48 字节十六进制 JWT 密钥,避免明文密码进 shell history。 - Phase 0 安全修复: 早期前端的桌面在未认证时直接渲染(后端鉴权对,前端没 gate);这一版补齐前端 gate,把"看不到 UI 就是没访问权"做成硬保证。
二、真终端 (v0.2.0)
- 后端
/ws/pty: 基于coder/websocket+creack/pty,通过 cookie 或?token=传 JWT,握手时验证后调pty.StartWithSize起bash -l。运行身份是服务用户(mochan),有 NOPASSWD sudo 时浏览器里直接sudo apt install …测自己应用,无需 SSH。 - 前端
apps/Terminal.tsx:@xterm/xtermv5 +xterm-addon-fit,TERM=xterm-256color+COLORTERM=truecolor+LANG=C.UTF-8,htop/vim/tmux/lazygit全跑,窗口 resize 通过 JSON 控制帧{type:"resize", cols, rows}发回服务器走TIOCSWINSZ。 - 配色: 12 位 ANSI 调色板按水墨主题重调,不抢桌面注意力。
三、真文件系统 (v0.3.0)
- 后端
/api/fs/*:home/list/read/write/mkdir/delete/move/upload/download/stat。读 8 MiB 上限、写 32 MiB、上传 256 MiB;返回真实 OS 错误(403 / 404 / 409 / 413)。 - 不做 chroot: 整个主机文件系统可见,权限完全由 OS 决定。
mochan用户访问/root→ 403,访问/etc/hostname→ 200 文本,访问自己 home → 全权限。 - 前端
apps/FileManager.tsx: 替换之前的 20 行占位 stub。顶栏路径输入框 + 上一级 / 主目录 / 刷新;左侧栏快捷入口(主目录 / / //etc//var/log//tmp);表格视图带图标 / 大小 / 修改时间 / 权限串 / 操作(下载 / 重命名 / 删除);软链显示→ target;双击目录进入,双击文本进编辑器,双击非文本走下载。
四、代码编辑器、系统监控、进程管理 (v0.4.0)
- CodeMirror 6 编辑器 (
components/CodeEditor.tsx): 替换 v0.3.0 的 textarea。按文件扩展名自动识别 JS / TS / JSON / Python / HTML / CSS / Markdown / YAML 加载语言扩展,one-dark主题,行号 / 折叠 / 自动补全 / 搜索全开。FileManager 双击文本文件直接进。 /api/sys/stat: 基于gopsutil/v4,单次请求拿主机名 / 内核 / OS / 架构 / uptime / 1-5-15 负载 / CPU 总占用 + 每核占用 / 内存 / Swap / 所有非 pseudo 挂载点 / 网络累积字节。apps/SystemMonitor.tsx: 每 2 秒轮询,进度条颜色按占用阈值变化(≤60 % 蓝、60-85 % 橙、>85 % 红),网速通过两次采样差分本地计算。/api/sys/processes+/api/sys/kill: 进程列表带 PID / PPID / 用户 / 状态 / CPU% / RSS / 线程 / cmdline;kill 端点接受TERM/INT/HUP/KILL,拒绝 pid ≤ 1,权限不足返回 403。apps/TaskManager.tsx: 替换之前的伪进程列表。搜索 PID / 名称 / 用户 / cmdline;按 CPU / RSS / PID / 名称双向排序;杀进程对话框含信号选择器和说明(TERM 优雅 / INT 等价 Ctrl+C / HUP 重载 / KILL 强杀)。
五、部署与发布
- 单二进制: 前端
npm run build后cp -r web/dist server/internal/static/dist,再go build通过embed.FS注入。运行时只需要二进制 +/etc/mochan/config.env,无 Node 运行时、无 npm、无依赖目录。 - systemd unit:
After=docker.service(因为绑 Docker bridge IP172.17.0.1:38421,对应反向代理在容器里的常见情况) +EnvironmentFile=/etc/mochan/config.env,崩溃自动Restart=on-failure。 - install.sh: 交互式装服务用户(默认
mochan)+ NOPASSWD sudo + 二进制 + systemd unit + 启动 + curl/healthz自检,幂等可重跑。 - Nginx Proxy Manager 集成: 完整字段说明在
deploy/npm-proxy-host.md。已在 NPM Advanced 框踩过坑——文档明确告知只能贴完整 nginx 指令(proxy_read_timeout 86400s;不能写proxy_*_timeout),否则 NPM 会保存原文导致 nginx -t 失败 + SSL block 不生成 → Cloudflare 525 SSL handshake failed。 - 多架构 release:
.github/workflows/release.yml监听v*tag,并行构建linux/amd64+linux/arm64,各自打 tar.gz + sha256,上传到 Release。本版本预构建产物含mochan-linux-amd64.tar.gz、mochan-linux-arm64.tar.gz、SHA256SUMS。
六、生产部署示范
- 已在
https://linux.mochance.xyz跑通,运行宿主: DigitalOcean Droplet (Ubuntu 24.04.3, 2 vCPU, 4 GB)。 - 反向代理: Nginx Proxy Manager 容器 + Cloudflare orange-cloud(CF SSL/TLS = Full(strict))。
- 端口分配: mochan 绑
172.17.0.1:38421(Docker bridge IP,私网,外网不可达),NPM 反代linux.mochance.xyz→http://172.17.0.1:38421,WebSockets 升级开启。
已知未做
- 桌面应用之间的 inter-app 调用(FileManager 双击 .md 跳到 MarkdownEditor 等)尚未串起来,每个 app 独立运行。
- 多用户与会话隔离(每用户独立容器、配额)目前明确不在 scope 内——本项目就是单用户。
- 文件管理器未做拖拽多选 / 右键菜单。
- 终端会话不支持断线重连(暂时是切断重起)。
English
Headline of this release: first public cut, "browser as Linux desktop" taken from concept to end-to-end usable. Phases 0–3 shipped in one week. A single Go binary (13 MB, frontend embedded via
embed.FS) is enough to deploy a workstation reachable ashttps://your.domainfrom any device, giving you a real terminal, real filesystem, real process manager, and real system monitor — purpose-built for testing your own Linux applications.
1. Auth and login (v0.1.0 → v0.1.1)
- bcrypt password + HS256 JWT in an
HttpOnly Secure SameSite=Laxcookie. The ReactAuthGatecalls/api/meon mount; on 401 it shows an ink-style login screen, and the desktop renders nothing until login succeeds. - StatusBar user dropdown with "Log out" calls
/api/auth/logout, clears the cookie, and returns to the login screen. - CLI:
mochan hash-passwordreads from stdin and prints a bcrypt hash;mochan gen-secretprints a 48-byte hex JWT key. No plaintext passwords land in shell history. - Phase 0 frontend gap: an earlier build rendered the desktop with no auth check (the backend was already gated, but the frontend was not). Fixed in v0.1.1 — no UI without a verified token.
2. Real terminal (v0.2.0)
- Backend
/ws/ptyovercoder/websocket+creack/pty, JWT via cookie or?token=. Spawnsbash -las the service user (mochan); with NOPASSWD sudo configured, you cansudo apt install …directly from the browser to test your own apps without SSH. - Frontend
apps/Terminal.tsx:@xterm/xterm+ fit addon,TERM=xterm-256color,COLORTERM=truecolor,LANG=C.UTF-8.htop/vim/tmux/lazygitall work. Window resize sends{type:"resize", cols, rows}JSON control frames; the server appliesTIOCSWINSZ. - 12-color ANSI palette retuned to match the ink theme.
3. Real filesystem (v0.3.0)
- Backend
/api/fs/*:home,list,read,write,mkdir,delete,move,upload,download,stat. Read cap 8 MiB, write 32 MiB, upload 256 MiB. Errors surface as real OS conditions (403 / 404 / 409 / 413). - No chroot. The whole host FS is reachable; the OS enforces the boundary.
mochanreading/root→ 403; reading/etc/hostname→ 200 text; reading its own home → full access. - Frontend
apps/FileManager.tsxreplaces the 20-line placeholder. Path input + up / home / refresh in the toolbar; sidebar shortcuts (Home, /, /etc, /var/log, /tmp); table view with icon / size / mtime / permission string / actions (download / rename / delete); symlinks shown with→ target; double-click a folder to descend, a text file to open the editor, a binary to download.
4. Editor, system monitor, process manager (v0.4.0)
- CodeMirror 6 editor (
components/CodeEditor.tsx) replacing the v0.3.0 textarea. JS / TS / JSON / Python / HTML / CSS / Markdown / YAML language modes auto-loaded by extension,one-darktheme, line numbers / folding / autocompletion / search all on. FileManager opens text files into it. /api/sys/statbacked bygopsutil/v4. One request returns hostname / kernel / OS / arch / uptime / 1-5-15 load / aggregate and per-core CPU / memory / swap / every non-pseudo mountpoint / cumulative network bytes.apps/SystemMonitor.tsxpolls every 2 s; bar color shifts by threshold (≤60 % blue, 60–85 % orange, >85 % red). Network rate is computed locally from two consecutive samples./api/sys/processes+/api/sys/kill. Process list includes PID / PPID / user / status / CPU% / RSS / threads / cmdline. The kill endpoint acceptsTERM/INT/HUP/KILL, refuses pid ≤ 1, and surfaces permission errors as 403.apps/TaskManager.tsxreplaces the previous fake process list. Search across PID / name / user / cmdline; sort by CPU / RSS / PID / name; kill dialog with signal selector and short explanations (TERM graceful, INT = Ctrl+C, HUP reload, KILL non-catchable).
5. Deploy and release
- Single binary.
npm run build→cp -r web/dist server/internal/static/dist→go buildinjects the frontend throughembed.FS. The runtime needs only the binary plus/etc/mochan/config.env. No Node runtime, no npm, nonode_modules. - systemd unit with
After=docker.service(because the typical setup binds172.17.0.1:38421to be reachable from a containerized reverse proxy) andEnvironmentFile=/etc/mochan/config.env.Restart=on-failurefor crash recovery. - install.sh: idempotent — creates the service user (default
mochan), grants NOPASSWD sudo, installs the binary, writes the systemd unit, starts the service, curls/healthz. - Nginx Proxy Manager: full field-by-field setup in
deploy/npm-proxy-host.md. The doc explicitly warns that the Advanced Config field is pasted verbatim — you must use complete directives likeproxy_read_timeout 86400s;, not abbreviated patterns likeproxy_*_timeout, or NPM saves the bad text andnginx -tfails silently, thelisten 443 sslblock is suppressed, and Cloudflare returns "525 SSL handshake failed". - Multi-arch release.
.github/workflows/release.ymltriggers onv*tags, buildslinux/amd64andlinux/arm64in parallel, packages each as a.tar.gzwithsha256, and uploads them to the GitHub Release.
6. Reference deployment
Production instance: https://linux.mochance.xyz. Host: DigitalOcean Droplet (Ubuntu 24.04.3, 2 vCPU, 4 GB). Reverse proxy: containerized Nginx Proxy Manager. CDN: Cloudflare proxy (orange cloud) with SSL/TLS = Full (strict). mochan binds 172.17.0.1:38421 (Docker bridge IP, never reachable externally), NPM forwards with WebSocket upgrade enabled.
Known gaps
- Inter-app routing (e.g. double-clicking an
.mdin FileManager to launch MarkdownEditor) is not wired yet — apps are independent windows. - Multi-user is intentionally out of scope. This is single-user by design.
- File manager has no drag-multi-select or right-click context menu yet.
- Terminal sessions don't survive reconnect — they simply ...