Files
ykxmz/_build/halo_publish.py
T
xmz7891 2061f9525f 2.3.0:手表界面精简 + 播放器 + 鼠标重做
手表端
- 功能页从 10 个杂项精简到 7 个(屏幕/鼠标/媒体/文件/电脑/语音/电源)
- 删掉:游戏封锁、待办、掷骰子、抽奖、壁纸、显示器开关、定时关机、
  剪贴板历史、性能/磁盘/电量、文件夹浏览、教室限制等
- 改成显式列按钮,不再把电脑端所有功能组无脑铺到表盘
- 远程鼠标控制重做:滑动区 + 左键/滚轮/右键三按钮
  滚轮点按=中键、上下滑=滚动
- 新增视频/音乐播放器(手表本地播放):进度拖动、左右滑快进退10秒、
  点画面播放暂停、上一/下一、播完自动下一首

电脑端
- 新增 kill_all(结束所有进程),白名单保护系统关键进程和自身
- 新增 media_list / media_pull(播放器数据源)
  安全边界:只允许媒体目录,用 relative_to 判断,防同前缀兄弟目录绕过

中转站
- known_devices 从 ID 集合改成「设备ID -> 配对码」映射
- 修复:管理员改码后关机超过 6 小时,改码会被超时清理冲掉
- 老格式 state 文件平滑升级
2026-10-18 11:39:14 +08:00

340 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''通过 Halo API 把文章直接写进去。
== 为什么要走 API ==
用户试过复制粘贴,但 Halo 富文本编辑器老把内容吞进代码块。
与其来回折腾,不如**直接调 API 建文章** —— 一次到位,
而且以后想再发文章也能复用这个脚本。
== ⚠ 密码怎么处理 ==
密码存在 `_build/.halo_password`(**已经加进 .gitignore**)。
脚本本身**不含任何密码** —— 从文件读。
这一点很重要:这个密码和用户的中转站管理密码**是同一个**,
写进仓库就等于全泄漏了。
== Halo 2.x 的建文章 API ==
POST /apis/api.console.halo.run/v1alpha1/posts
Authorization: Basic base64(user:pass)
Content-Type: application/json
请求体是 `PostRequest`:
{
"post": { <- Post 资源本身
"apiVersion": "content.halo.run/v1alpha1",
"kind": "Post",
"metadata": {"name": "post-xxx"},
"spec": {
"title": "...",
"slug": "...",
"publish": true,
"publishTime": "...",
"visible": "PUBLIC",
"allowComment": true,
"excerpt": {"autoGenerate": true, "raw": ""},
"categories": [], "tags": [], "htmlMetas": []
}
},
"content": { <- 正文单独放
"raw": "...",
"content": "...",
"rawType": "markdown"
}
}
**注意 `content` 要同时给 `raw`(源码)和 `content`(渲染后的 HTML)**。
`raw` 给 Markdown,`content` 给 HTML —— Halo 前端直接读 `content`。
两个都给的话,编辑器里也能正常显示源码。
'''
from __future__ import annotations
import base64
import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402
PWFILE = ROOT / "_build" / ".halo_password"
# ---------------------------------------------------------------- 文章正文
# 用 Markdown 写(API 的 raw 字段就是 Markdown),Halo 会渲染成网页。
# 内容只提官网,不提任何管理后台 —— 对外材料清洁度要求。
TITLE = "远控软件正式上线"
SLUG = "remote-control-launch"
EXCERPT = ("一个为站在讲台前的人做的远程控制工具。手表抬一下手腕,"
"就能操作教室那台电脑:远程鼠标、投图到大屏、语音播报、"
"静默自动更新。官网 yk.xxmxxs.top")
MD = """一个为「站在讲台前的人」做的远程控制工具。手表抬一下手腕,就能操作教室那台电脑。
## 它是干什么的
很多场景下,人不在电脑前面,但需要操作那台电脑:
- 上课时站在教室中间,想切个课件、放张图
- 坐在办公室,想看看教室那台电脑还开着没
- 手上拿着东西,腾不出手去碰鼠标键盘
**这个软件就是解决这个的。**
电脑端装一次,之后一直安静地在后台跑;手表端装上之后,输入 6 位配对码就能连上。
## 能做什么
### 远程鼠标控制
手表变成触摸板:
- **拖动** —— 移动鼠标
- **单击** —— 左键
- **双击** —— 右键
- 下面还有专门的**左键 / 右键 / 滚轮上 / 滚轮下**按钮
精度够用,配合屏幕镜像一起看,基本可以完全代替鼠标。
### 一键显示桌面
快捷栏上有一格「显示桌面」,点一下就是电脑上按 `Win + D` 的效果。
需要把投影切回桌面、或者临时藏起一堆窗口的时候,一下就行。
### 电脑端静默自动更新
这是最省心的一点:
**手表上点一下「更新电脑端」,电脑就会自己下载新版、静默安装、重启。**
- 全程**不弹任何窗口**
- 中转地址、配对码、所有设置**一个都不变**
- 更新完自动回来继续跑
不用再跑到电脑前手动装一遍。
### 顶部快捷栏
任何页面顶上都有一条栏,**4 个格子全部可以自己定义**:
| 可选动作 | 说明 |
|---|---|
| 传文件 | 从手表把文件发到电脑 |
| 屏幕镜像 | 看电脑屏幕 |
| 鼠标控制 | 进触摸板模式 |
| 音效 | 让电脑放一声 |
| 关机 | 锁屏 / 关机 / 重启 |
| 语音播报 | 让电脑念一段字 |
| 投图到电脑 | 手表选图,全屏投到大屏 |
| 显示桌面 | 等于 Win + D |
在设置页里点一下就能换。
### 投图到大屏
手表上选一张图,直接全屏显示在电脑(也就是投影)上。
上课讲题、展示作业、临时放张图,比插 U 盘快得多。
### 语音播报
输入文字,电脑用系统自带的语音念出来。
「上课了」「下课了」「大家看一下黑板」——说一遍不如让电脑说一遍。
### 让电脑放个音效
内置了一批**程序生成**的提示音和音效(不是网上下载的版权素材),也可以放自己电脑上的音乐。
### 其他
- 剪贴板互传(手表和电脑之间)
- 电源控制(锁屏、关机、重启)
- 文件互传
- 静音、音量加减、播放控制
## 几个设计上的取舍
### 电脑端不弹任何窗口
屏幕被观看的时候**不显示提示条**,整个软件在后台完全没有可见窗口。
这一点是刻意的:要的就是「像不存在一样」。如果这是**你自己的电脑**,你需要一个能看见它在做什么的开关 —— 设置里有,默认关着。
### 支持 Windows 7
老机器也能装。这一点花了些功夫:**内嵌的 Python 运行时用的是 3.8**(3.9 开始就不支持 Win7 了),打包时还有一道检查专门盯着这个 —— 出包前会验证「有没有那个 Win7 上不存在的 DLL」,有就拒绝出包。
### 静默更新为什么要校验
自更新最大的风险是**下载中途出问题**,装上一个坏程序 —— 而它还会自己继续更新。
所以升级包会校验 SHA-256,**对不上就放弃这一轮,保持现状**。现状至少是能用的。
## 怎么装
**官网下载:<https://yk.xxmxxs.top>**
- **电脑端** —— 下载安装包,双击,填一个 6 位配对码就完事
- **手表端** —— 下载 APK 安装,输入刚才那个 6 位码
装完之后:
1. 手表连上电脑
2. 会先到一个选择页:**远控** / **静默更新被控端**
3. 点「远控」进控制页
## 说在最后
这个软件最初是给自己上课用的 —— 站在讲台上,想切个东西还得走回电脑前面,很烦。
后来发现能用的地方比想的多,就整理出来放在官网上,有需要的人自己拿。
**有问题、想要什么功能,欢迎留言。**
"""
def md_to_html(text: str) -> str:
"""把这份固定的 Markdown 转成 HTML。
⚠ 这里**不是在写通用 Markdown 解析器** —— 那样要引第三方库,
而我们的正文是我自己写的、结构已知。用简单的正则按块处理就够了,
而且不会因为库的行为差异出意外。
支持:## / ### 标题、**粗体**、`代码`、- 列表、1. 列表、
表格、> 引用、<https://...> 自动链接、空行分段。
"""
import html as _html
import re as _re
def inline(s: str) -> str:
# 先转义,避免正文里的 < > 破坏 HTML
s = _html.escape(s, quote=False)
s = _re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
s = _re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
s = _re.sub(r"&lt;(https?://[^&]+?)&gt;", r'<a href="\1">\1</a>', s)
return s
out = []
lines = text.split("\n")
i = 0
while i < len(lines):
ln = lines[i]
if not ln.strip():
i += 1
continue
# 表格
if ln.startswith("|") and i + 1 < len(lines) and \
set(lines[i + 1].replace("|", "").strip()) <= set("-: "):
head = [c.strip() for c in ln.strip("|").split("|")]
i += 2
rows = []
while i < len(lines) and lines[i].startswith("|"):
rows.append([c.strip() for c in lines[i].strip("|").split("|")])
i += 1
out.append("<table><thead><tr>" +
"".join("<th>%s</th>" % inline(c) for c in head) +
"</tr></thead><tbody>" +
"".join("<tr>" + "".join("<td>%s</td>" % inline(c)
for c in r) + "</tr>"
for r in rows) +
"</tbody></table>")
continue
# 标题
m = _re.match(r"^(#{2,4})\s+(.*)$", ln)
if m:
lvl = len(m.group(1))
out.append("<h%d>%s</h%d>" % (lvl, inline(m.group(2)), lvl))
i += 1
continue
# 列表
if _re.match(r"^[-*]\s+", ln):
items = []
while i < len(lines) and _re.match(r"^[-*]\s+", lines[i]):
items.append(inline(_re.sub(r"^[-*]\s+", "", lines[i])))
i += 1
out.append("<ul>" + "".join("<li>%s</li>" % x for x in items) +
"</ul>")
continue
if _re.match(r"^\d+\.\s+", ln):
items = []
while i < len(lines) and _re.match(r"^\d+\.\s+", lines[i]):
items.append(inline(_re.sub(r"^\d+\.\s+", "", lines[i])))
i += 1
out.append("<ol>" + "".join("<li>%s</li>" % x for x in items) +
"</ol>")
continue
# 引用
if ln.startswith(">"):
out.append("<blockquote>%s</blockquote>"
% inline(ln.lstrip("> ").strip()))
i += 1
continue
# 普通段落(连续的合成一段)
para = [ln.strip()]
i += 1
while i < len(lines) and lines[i].strip() and \
not _re.match(r"^(#{2,4}\s|[-*]\s|\d+\.\s|>|\|)", lines[i]):
para.append(lines[i].strip())
i += 1
out.append("<p>%s</p>" % inline(" ".join(para)))
return "\n".join(out)
def main() -> int:
print("=" * 62)
print(" 通过 Halo API 发布文章")
print("=" * 62)
print()
if not PWFILE.is_file():
print(" ✗ 找不到 %s" % PWFILE)
return 1
pw = PWFILE.read_text(encoding="utf-8").strip()
print(" 密码已从 %s 读到(长度 %d)" % (PWFILE.name, len(pw)))
# 1) 先测认证
print()
print("── 1. 测认证 ──")
out = run(r'''
curl -s -o /dev/null -w " basic auth 打 posts -> HTTP %{http_code}\n" \
--max-time 10 -u "xmz7891:__PW__" \
http://127.0.0.1:3001/apis/api.console.halo.run/v1alpha1/posts
curl -s -o /dev/null -w " basic auth 打 users/- -> HTTP %{http_code}\n" \
--max-time 10 -u "xmz7891:__PW__" \
http://127.0.0.1:3001/apis/api.console.halo.run/v1alpha1/users/-
'''.replace("__PW__", pw), timeout=200, quiet=True)
print(out[1] if isinstance(out, tuple) else out)
# 2) 看现在有几篇文章
print("── 2. 现在的文章 ──")
out = run(r'''
curl -s --max-time 10 -u "xmz7891:__PW__" \
"http://127.0.0.1:3001/apis/api.console.halo.run/v1alpha1/posts?page=0&size=20" \
| python3 -c "
import json,sys
try:
d = json.load(sys.stdin)
except Exception as e:
print(' 解析失败:', e); sys.exit()
items = d.get('items') or []
print(' 共', d.get('total', len(items)), '篇')
for p in items:
sp = p.get('spec', {})
print(' -', sp.get('title'), '| publish =', sp.get('publish'),
'| name =', (p.get('metadata') or {}).get('name'))
"
'''.replace("__PW__", pw), timeout=200, quiet=True)
print(out[1] if isinstance(out, tuple) else out)
return 0
if __name__ == "__main__":
sys.exit(main())