Compare commits

...

2 Commits

Author SHA1 Message Date
xfy
460088fa43 docs(skill): deploy-to-linux 添加手动部署模式
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
用户指定服务器后,agent 只做第 0 步只读探测,然后按探测结果
输出填实占位符的完整部署命令清单(分本地/服务器两块),
用户自行复制执行。同步更新 description 触发关键词。
2026-07-21 14:13:49 +08:00
xfy
ccee9131a9 fix(highlight): 编译期内嵌自定义语法,修复线上 Docker 镜像代码高亮缺失
生产镜像是 FROM scratch 的静态 musl 二进制,运行时不存在 syntaxes/ 目录;
CARGO_MANIFEST_DIR 烘焙的是构建机路径(Docker 内为 /build),运行时
add_from_folder 必然失败,Kotlin/Swift/TypeScript/TSX/JSX/Zig/Vue 七种
语言静默回退为纯文本。

改为 include_str! 编译期嵌入 .sublime-syntax,彻底消除运行时文件依赖;
新增两个守护测试:内嵌列表与 syntaxes/ 目录一致性、自定义语法均已加载。
2026-07-21 14:10:38 +08:00
2 changed files with 83 additions and 10 deletions

View File

@ -3,7 +3,9 @@ name: deploy-to-linux
description: |
将 yggdrasil 部署/更新到任意 Linux 服务器时使用。本地 arm64 用 Docker Rosetta 构建
x86 镜像导出传输到目标服务器Docker 或 Podman后用 docker compose 运行,
前置反代自动签发 HTTPS。触发关键词"部署"、"deploy"、"发布到服务器"、"上线"、"docker compose"。
前置反代自动签发 HTTPS。支持自动部署agent 直接执行与手动部署agent 探测后输出
完整命令清单,用户自行执行)两种模式。
触发关键词:"部署"、"deploy"、"发布到服务器"、"上线"、"docker compose"、"手动部署"。
allowed-tools:
- Bash
- Read
@ -19,6 +21,23 @@ metadata:
从本地 arm64 构建器构建 `linux/amd64` 镜像,传输到目标 Linux 服务器并以 docker compose 运行。前置反代负责 HTTPS。
## 部署模式(先确认用哪种)
| 模式 | 触发 | 行为 |
|---|---|---|
| **自动部署**(默认) | "部署到 X"、"更新线上" | agent 逐步执行下文全部 ssh/本地命令,直接完成部署 |
| **手动部署** | "手动部署"、"给我部署命令"、"我自己来执行" | agent 只做**第 0 步探测**ssh 只读信息采集),然后根据探测结果把**完整部署命令清单**输出给用户,用户自己复制执行 |
### 手动部署模式的输出要求
探测完成后,输出一份**可直接复制执行**的命令清单,必须满足:
1. **分块标注执行位置**`# 本地执行`构建、导出、scp`# 服务器执行`导入、tag、compose up分开成独立代码块用户在不同终端跑。
2. **占位符全部填实**`<host>`、socket 路径(`/var/run/docker.sock` vs `/run/podman/podman.sock`、反代方案A/B/C、部署目录root vs 非 root按探测结果定死不留待定项。只有域名/邮箱这类用户秘密可以留 `<你的域名>` 占位,并单独列在清单开头的「需要你替换的值」里。
3. **按本文顺序串联**:本地构建 → 构建验证 → 导出传输 → 服务器导入 → runner 去前缀 → compose 编排(含生成好的 `.env` 和按探测结果裁剪过的 `docker-compose.yml` 完整内容)→ 启动 → 验证清单 → 清理。更新场景只给"更新流程"那一节。
4. **命令原样可用**:服务器端命令去掉 `ssh <host> '...'` 包装用户已登录服务器直接跑保留单引号内的原始命令fish 兼容性约束不变。
5. **不替用户执行任何写操作**agent 只跑第 0 步探测命令(只读),构建/传输/启动都由用户手动执行。
**这是项目专属 skill**:镜像名、`LANGUAGES` 注册表硬编码、runner 沙箱、nginx-proxy 约定都绑定到 yggdrasil 本身但服务器探测、构建、传输、compose 编排是通用流程。
## 第 0 步:探测目标服务器(每次部署必做)

View File

@ -8,18 +8,36 @@ pub mod server {
use std::sync::LazyLock;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::parsing::{SyntaxDefinition, SyntaxSet};
use syntect::util::LinesWithEndings;
/// 全局语法集合,懒加载时合并内置语法与 `syntaxes/` 目录下的自定义语法。
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
/// 编译期内嵌的自定义语法定义(文件名 stem → .sublime-syntax 内容)。
///
/// 生产镜像是 `FROM scratch` 的静态 musl 二进制,容器内不存在 `syntaxes/`
/// 目录;而 `CARGO_MANIFEST_DIR` 烘焙的是构建机路径Docker 里是 /build
/// 运行时 `add_from_folder` 注定失败、这些语言静默回退为纯文本。
/// 因此改为 `include_str!` 编译期嵌入,彻底消除运行时文件依赖。
/// 列表与 `syntaxes/` 目录的一致性由测试 `custom_syntax_list_matches_directory` 保证。
pub(crate) const CUSTOM_SYNTAXES: &[(&str, &str)] = &[
("JSX", include_str!("../syntaxes/JSX.sublime-syntax")),
("Kotlin", include_str!("../syntaxes/Kotlin.sublime-syntax")),
("Swift", include_str!("../syntaxes/Swift.sublime-syntax")),
("TSX", include_str!("../syntaxes/TSX.sublime-syntax")),
("TypeScript", include_str!("../syntaxes/TypeScript.sublime-syntax")),
("Vue", include_str!("../syntaxes/Vue.sublime-syntax")),
("Zig", include_str!("../syntaxes/Zig.sublime-syntax")),
];
/// 全局语法集合,懒加载时合并内置语法与内嵌的自定义语法。
pub(crate) static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
// 使用 CARGO_MANIFEST_DIR 派生的绝对路径,避免运行时工作目录不确定导致加载失败
let syntaxes_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes");
tracing::info!("Loading custom syntaxes from: {}", syntaxes_dir);
match builder.add_from_folder(syntaxes_dir, true) {
Ok(()) => tracing::info!("Custom syntaxes loaded successfully"),
Err(e) => tracing::warn!("Failed to load custom syntaxes: {:?}", e),
for (name, src) in CUSTOM_SYNTAXES {
match SyntaxDefinition::load_from_str(src, true, Some(name)) {
Ok(def) => {
builder.add(def);
}
Err(e) => tracing::warn!("Failed to load embedded syntax {}: {:?}", name, e),
}
}
let built = builder.build();
tracing::info!(
@ -123,6 +141,42 @@ pub mod server {
mod tests {
use super::server::*;
#[test]
fn custom_syntax_list_matches_directory() {
// CUSTOM_SYNTAXES 必须与 syntaxes/ 目录下的 .sublime-syntax 文件一一对应,
// 防止新增/删除语法文件后忘记同步内嵌列表(仿 migrations 数组的编译期校验)。
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes");
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
.expect("syntaxes/ 目录应存在")
.filter_map(|e| {
let p = e.ok()?.path();
if p.extension()? == "sublime-syntax" {
Some(p.file_stem()?.to_string_lossy().into_owned())
} else {
None
}
})
.collect();
on_disk.sort();
let mut embedded: Vec<&str> = CUSTOM_SYNTAXES.iter().map(|(name, _)| *name).collect();
embedded.sort();
assert_eq!(embedded, on_disk, "CUSTOM_SYNTAXES 与 syntaxes/ 目录不一致");
}
#[test]
fn custom_syntaxes_are_loaded() {
// 内嵌语法必须真正进入 SyntaxSet守护生产环境回退纯文本的回归
for (name, _) in CUSTOM_SYNTAXES {
let lower = name.to_lowercase();
assert!(
SYNTAX_SET.find_syntax_by_name(name).is_some()
|| SYNTAX_SET.find_syntax_by_extension(&lower).is_some(),
"自定义语法 {} 未加载",
name
);
}
}
#[test]
fn highlight_code_rust() {
let result = highlight_code("fn main() {}", Some("rust"));