包含关键字 typecho 的文章

我实在是受不了了,每次锁屏在解锁,都会在右上角弹出来一次,设置关了,还是弹出来。

image

尝试过一下方法,只对了一半,本来 2 个弹窗,现在只弹一个了。

defaults write com.apple.SoftwareUpdate MajorOSUserNotificationDate -date "2031-01-22 23:22:47 +0000"
defaults write com.apple.SoftwareUpdate UserNotificationDate -date "2031-01-22 23:22:47 +0000"
defaults write com.apple.appstored LastUpdateNotification -date "2031-01-22 23:22:47 +0000"

问问大家有没有更好的解决办法。

本项目是一个 Cloudflare Worker 应用,作为麦当劳 MCP (Model Context Protocol) 服务器的客户端。提供了一个基于网页的用户界面,用于管理 Token、选择工具并可视化执行结果。

mcd-worker

一键部署

Deploy to Cloudflare Workers

  • 网页界面: 采用麦当劳标志性的红黄配色风格。
  • Token 管理:
    • 自动保存 Token 到浏览器本地存储 (LocalStorage)。
    • 支持通过 URL 参数传入 (?token=...)。
    • Token 申请地址: https://open.mcd.cn/mcp
  • 工具选择: 下拉菜单内置了所有可用的麦当劳 MCP 工具。
  • 结果可视化:
    • Markdown 视图: 渲染后的文本,支持表格展示。
    • JSON 视图: 格式化显示原始 JSON 响应数据。
  • API 接口: 提供 /api/execute 接口供程序调用。

引言

在人工智能时代,机器学习模型已成为数据驱动决策的核心引擎,但随之而来的隐私风险也日益凸显。其中,模型反演攻击(Model Inversion Attack, MIA)作为一类典型的隐私攻击,已成为学术界和产业界关注的焦点。这种攻击最早于2015年由Fredrikson等人在医疗图像领域的开创性工作中提出,攻击者无需直接访问训练数据,仅通过模型的输出信号(如概率分布、logits、embedding或中间表示)即可重构出高度相关的输入特征。常见表现形式包括:生成某一类别的“原型样本”(如典型人脸轮廓)、恢复敏感属性(例如年龄、种族或医疗诊断标记),抑或从向量表示中逆推出原始文本片段或图像细节。

什么是模型反演攻击?

模型反演攻击(Model Inversion Attack, MIA)是一种隐私攻击,让攻击者通过模型输出(如概率分布、logits、embedding或中间表示)“逆向”重建输入特征,而非直接访问训练数据。核心在于输出信号提供优化线索,即使模型不可逆。形式化描述:

  • 模型: f_\theta(x) \rightarrow y ,其中 y 可以是概率、logits、向量表示或中间激活;
  • 攻击目标:定义损失 \mathcal{L}(x) ,例如最大化目标类别概率、最小化与某 embedding 的距离、或匹配某层特征;
  • 反演过程:通过迭代更新 x 来最小化 \mathcal{L}(x) (白盒可直接用梯度;黑盒可通过查询估计方向/梯度)。

MIA按信息暴露强度可概括4种:

  1. 白盒反演:可访问参数/梯度/中间层;
  2. 黑盒-分数反演:可查询概率或 logits;
  3. 表示反演:可访问 embedding 或中间表示(检索/RAG、端云协同、分层推理常见);
  4. 黑盒-标签反演:仅返回 top-1 label。

除此之外,MIA 可作用于不同数据形态:图像(重建类别原型或敏感属性)、文本(从 embedding/打分反推关键词片段或属性)、图数据(恢复节点属性、边关系或子图结构)。

以上理论概述了MIA的多种形式和风险,但要真正体会其威力,还需通过实际案例验证。以下我们聚焦黑白盒场景下的图像MIA,能直观展示从噪声到“原型”的反演过程,并为后续防御提供基础。

反演案例:白盒的图像模型反演攻击

本实验采用合成彩色图像数据集,包括1000张样本、10个类别,每张图像尺寸为64x64x3(RGB),每个类别通过不同的主色调(如红色、橙色等)结合椭圆形状和纹理进行区分,并添加少量高斯噪声以增强真实性;目标模型为简单的CNN分类器,结构包括多层卷积(Conv2d+ReLU+MaxPool)、自适应平均池化、展平和全连接层(Linear+ReLU+Dropout),训练后准确率接近100%。

攻击者希望从一个训练好的图像分类模型中,反演出模型"认为"的各个类别的典型特征。攻击的核心思想是利用模型的梯度信息,从随机噪声优化出让模型"满意"的图像。

白盒

把反演想成“对输入做训练”:我们不改模型参数,只改输入图像 x,让模型越来越确信它是目标类 t。损失里有三部分:主项推动目标概率变大,TV(x) 让图像别变成满屏噪点(更平滑),L2 让像素别爆炸。具体做法就是从一张随机噪声图开始,反复计算一次前向得到 P(y\mid x),再反向得到“改哪些像素最能提高目标概率”的梯度,然后按梯度更新,并把像素裁剪回合法范围。重复足够多次后,噪声会被“雕刻”成模型最容易识别的特征组合。

代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import os

# ==================== 配置 ====================
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# ==================== 目标模型定义 ====================
class SimpleCNN(nn.Module):
    """目标分类模型"""
    def __init__(self, num_classes=10):
        super(SimpleCNN, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(64, 128, 3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(128, 256, 3, padding=1),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((4, 4))
        )
        self.classifier = nn.Sequential(
            nn.Flatten(),
            nn.Linear(256 * 4 * 4, 512),
            nn.ReLU(),
            nn.Dropout(0.5),
            nn.Linear(512, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

# ==================== 数据生成 ====================
def generate_synthetic_data(num_samples=1000, num_classes=10, img_size=64):
    """
    生成合成彩色图像数据集
    - 每个类别使用不同的主色调作为区分特征
    - 类别0: 偏红, 类别1: 偏橙, 类别2: 偏黄褐, ...
    """
    # 10个类别的主色调 (RGB, 范围0-1)
    class_colors = [
        (0.9, 0.3, 0.3),   # 类别0: 红色
        (0.9, 0.6, 0.3),   # 类别1: 橙色
        (0.9, 0.9, 0.3),   # 类别2: 黄色
        (0.3, 0.9, 0.3),   # 类别3: 绿色
        (0.3, 0.9, 0.9),   # 类别4: 青色
        (0.3, 0.3, 0.9),   # 类别5: 蓝色
        (0.9, 0.3, 0.9),   # 类别6: 紫色
        (0.6, 0.3, 0.3),   # 类别7: 深红
        (0.3, 0.6, 0.3),   # 类别8: 深绿
        (0.3, 0.3, 0.6),   # 类别9: 深蓝
    ]

    images = []
    labels = []

    for i in range(num_samples):
        label = i % num_classes
        img = np.zeros((img_size, img_size, 3), dtype=np.float32)

        # 深色背景
        img[:, :] = [0.1, 0.1, 0.15]

        # 绘制椭圆形彩色区域(类别特征区域)
        center_y, center_x = img_size // 2, img_size // 2
        for y in range(img_size):
            for x in range(img_size):
                if ((x - center_x) / 20) ** 2 + ((y - center_y) / 25) ** 2 < 1:
                    img[y, x] = class_colors[label]
                    # 添加类别特定的纹理变化
                    img[y, x, 0] += 0.05 * np.sin(x * 0.5 + label)
                    img[y, x, 1] += 0.05 * np.cos(y * 0.5 + label)

        # 添加少量高斯噪声
        img += np.random.randn(img_size, img_size, 3) * 0.02
        img = np.clip(img, 0, 1)

        images.append(img)
        labels.append(label)

    images = torch.FloatTensor(np.array(images)).permute(0, 3, 1, 2)
    labels = torch.LongTensor(labels)

    return images, labels

# ==================== 模型训练 ====================
def train_target_model(model, train_images, train_labels, epochs=30):
    """训练目标分类模型"""
    print("\n[1] 训练目标模型...")

    dataset = torch.utils.data.TensorDataset(train_images, train_labels)
    dataloader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    criterion = nn.CrossEntropyLoss()

    model.train()
    for epoch in range(epochs):
        total_loss = 0
        correct = 0
        total = 0

        for images, labels in dataloader:
            images, labels = images.to(device), labels.to(device)

            optimizer.zero_grad()
            outputs = model(images)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

        if (epoch + 1) % 10 == 0:
            print(f"  Epoch [{epoch+1}/{epochs}] Loss: {total_loss/len(dataloader):.4f} "
                  f"Acc: {100.*correct/total:.2f}%")

    print(f"  训练完成,最终准确率: {100.*correct/total:.2f}%")
    return model

# ==================== 模型反演攻击 ====================
class ModelInversionAttack:
    """模型反演攻击类"""

    def __init__(self, model, img_size=64):
        self.model = model
        self.img_size = img_size
        self.model.eval()

    def total_variation_loss(self, x):
        """
        总变差损失 - 使生成图像更平滑
        计算相邻像素的差异,惩罚高频噪声
        """
        diff_h = torch.abs(x[:, :, 1:, :] - x[:, :, :-1, :])
        diff_w = torch.abs(x[:, :, :, 1:] - x[:, :, :, :-1])
        return torch.mean(diff_h) + torch.mean(diff_w)

    def invert(self, target_class, num_iterations=1000, lr=0.1, 
               tv_weight=0.001, l2_weight=0.0001):
        """
        执行模型反演攻击

        参数:
            target_class: 目标类别(要反演的类别)
            num_iterations: 优化迭代次数
            lr: 学习率
            tv_weight: 总变差损失权重
            l2_weight: L2正则化权重

        返回:
            inverted_image: 反演得到的图像
            history: 优化历史(用于可视化)
        """
        # Step 1: 从随机噪声初始化
        x = torch.randn(1, 3, self.img_size, self.img_size, device=device) * 0.5
        x.requires_grad = True

        optimizer = torch.optim.Adam([x], lr=lr)
        scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=300, gamma=0.5)

        history = {'prob': [], 'loss': []}
        best_x = None
        best_prob = 0

        for i in range(num_iterations):
            optimizer.zero_grad()

            # Step 2: 前向传播,获取预测概率
            outputs = self.model(x)
            probs = F.softmax(outputs, dim=1)
            target_prob = probs[0, target_class]

            # Step 3: 计算损失
            # 主损失:最大化目标类别概率 = 最小化负对数概率
            ce_loss = -torch.log(target_prob + 1e-8)
            # 正则化:总变差损失(平滑)+ L2范数(防止极端值)
            tv_loss = self.total_variation_loss(x)
            l2_loss = torch.norm(x)

            loss = ce_loss + tv_weight * tv_loss + l2_weight * l2_loss

            # Step 4: 反向传播,更新图像
            loss.backward()
            optimizer.step()
            scheduler.step()

            # Step 5: 像素值裁剪到有效范围
            with torch.no_grad():
                x.data = torch.clamp(x.data, -1, 1)

            # 记录历史
            history['prob'].append(target_prob.item())
            history['loss'].append(loss.item())

            # 保存最佳结果
            if target_prob.item() > best_prob:
                best_prob = target_prob.item()
                best_x = x.detach().clone()

        return best_x, history, best_prob

# ==================== 攻击评估 ====================
def evaluate_attack(model, inverted_images, target_classes):
    """
    评估攻击效果

    成功标准:模型对反演图像的预测类别 == 目标类别
    """
    print("\n[3] 评估攻击效果...")

    model.eval()
    success_count = 0

    with torch.no_grad():
        for i, (img, target) in enumerate(zip(inverted_images, target_classes)):
            outputs = model(img.to(device))
            probs = F.softmax(outputs, dim=1)
            predicted = outputs.argmax(dim=1).item()
            target_prob = probs[0, target].item()

            success = (predicted == target)
            if success:
                success_count += 1

            status = "✓ SUCCESS" if success else "✗ FAILED"
            print(f"  Class {target}: Predicted={predicted}, "
                  f"Prob={target_prob*100:.1f}%, {status}")

    success_rate = success_count / len(target_classes)
    print(f"\n  总体攻击成功率: {success_rate*100:.1f}% ({success_count}/{len(target_classes)})")

    return success_rate

# ==================== 主函数 ====================
def main():
    print("=" * 60)
    print("图像模型反演攻击 (Image Model Inversion Attack)")
    print("=" * 60)

    # 参数配置
    num_classes = 10
    img_size = 64
    num_samples = 1000

    # 1. 生成数据
    print("\n[0] 生成合成数据...")
    train_images, train_labels = generate_synthetic_data(
        num_samples=num_samples, 
        num_classes=num_classes, 
        img_size=img_size
    )
    print(f"  数据集: {num_samples} 样本, {num_classes} 类别, {img_size}x{img_size} 像素")

    # 2. 训练目标模型
    model = SimpleCNN(num_classes=num_classes).to(device)
    model = train_target_model(model, train_images, train_labels, epochs=30)

    # 3. 执行反演攻击
    print("\n[2] 执行模型反演攻击...")
    attacker = ModelInversionAttack(model, img_size=img_size)

    inverted_images = []
    histories = []

    for target in range(num_classes):
        print(f"  反演类别 {target}...", end=" ")
        inv_img, history, best_prob = attacker.invert(
            target_class=target,
            num_iterations=800,
            lr=0.1,
            tv_weight=0.001,
            l2_weight=0.0001
        )
        inverted_images.append(inv_img)
        histories.append(history)
        print(f"最终概率: {best_prob*100:.1f}%")

    # 4. 评估攻击
    target_classes = list(range(num_classes))
    success_rate = evaluate_attack(model, inverted_images, target_classes)

    print("\n" + "=" * 60)
    print(f"攻击完成!成功率: {success_rate*100:.1f}%")
    print("=" * 60)

    return success_rate, inverted_images, histories

if __name__ == "__main__":
    success_rate, inverted_images, histories = main()

实验结果

Generating synthetic color image data...
Generated 1000 samples, 10 classes
Image shape: torch.Size([1000, 3, 64, 64])

=== Training Target Model ===
Epoch [5/30] Loss: 0.0039 Acc: 100.00%
Epoch [10/30] Loss: 0.0928 Acc: 97.30%
Epoch [15/30] Loss: 0.0006 Acc: 100.00%
Epoch [20/30] Loss: 0.0003 Acc: 100.00%
Epoch [25/30] Loss: 0.0006 Acc: 100.00%
Epoch [30/30] Loss: 0.0001 Acc: 100.00%
Final Training Accuracy: 100.00%
Model saved to results/image_mia/target_model.pth

--- Inverting class 0 ---
Class 0: 100%|███████████████████████████████| 800/800 [00:07<00:00, 112.83it/s, prob=1.0000, loss=0.0065]
Final probability for class 0: 1.0000

--- Inverting class 1 ---
Class 1: 100%|███████████████████████████████| 800/800 [00:05<00:00, 136.30it/s, prob=1.0000, loss=0.0064]
Final probability for class 1: 1.0000

--- Inverting class 2 ---
Class 2: 100%|███████████████████████████████| 800/800 [00:05<00:00, 145.61it/s, prob=1.0000, loss=0.0050]
Final probability for class 2: 1.0000

--- Inverting class 3 ---
Class 3: 100%|███████████████████████████████| 800/800 [00:05<00:00, 138.02it/s, prob=1.0000, loss=0.0081]
Final probability for class 3: 1.0000

--- Inverting class 4 ---
Class 4: 100%|███████████████████████████████| 800/800 [00:07<00:00, 108.70it/s, prob=1.0000, loss=0.0059]
Final probability for class 4: 1.0000

--- Inverting class 5 ---
Class 5: 100%|███████████████████████████████| 800/800 [00:06<00:00, 114.32it/s, prob=1.0000, loss=0.0054]
Final probability for class 5: 1.0000

--- Inverting class 6 ---
Class 6: 100%|███████████████████████████████| 800/800 [00:06<00:00, 122.65it/s, prob=1.0000, loss=0.0066]
Final probability for class 6: 1.0000

--- Inverting class 7 ---
Class 7: 100%|███████████████████████████████| 800/800 [00:07<00:00, 109.51it/s, prob=1.0000, loss=0.0056]
Final probability for class 7: 1.0000

--- Inverting class 8 ---
Class 8: 100%|███████████████████████████████| 800/800 [00:06<00:00, 124.94it/s, prob=1.0000, loss=0.0047]
Final probability for class 8: 1.0000

--- Inverting class 9 ---
Class 9: 100%|███████████████████████████████| 800/800 [00:06<00:00, 115.32it/s, prob=1.0000, loss=0.0050]
Final probability for class 9: 1.0000

=== Attack Evaluation ===
Class 0: Predicted=0, Prob=1.0000, Success=True
Class 1: Predicted=1, Prob=1.0000, Success=True
Class 2: Predicted=2, Prob=1.0000, Success=True
Class 3: Predicted=3, Prob=1.0000, Success=True
Class 4: Predicted=4, Prob=1.0000, Success=True
Class 5: Predicted=5, Prob=1.0000, Success=True
Class 6: Predicted=6, Prob=1.0000, Success=True
Class 7: Predicted=7, Prob=1.0000, Success=True
Class 8: Predicted=8, Prob=1.0000, Success=True
Class 9: Predicted=9, Prob=1.0000, Success=True

Overall Attack Success Rate: 100.00%

可视化结果

inversion_results

第一行:原始训练样本(10个类别,每个类别有明显的颜色特征) 第二行:反演生成的图像(从随机噪声优化得到,看起来像噪声) 第三行:优化曲线(显示目标概率从10%上升到100%的过程)

结果分析

可以看到,我们10个类别全部成功反演,每个反演图像都被模型以100%置信度识别为目标类别。攻击成功率为100%。也许我们还会有疑问:为什么反演图像看起来像噪声?肉眼和原来的颜色毫无关系?这是因为模型的"视觉"与人类不同——它依赖统计模式和特征分布,而非直观形状或颜色。虽然人眼看是噪声,但这些图像包含了模型学到的类别特征的统计表示、训练数据分布的某些信息,甚至可能泄露敏感属性(如人脸识别中的面部轮廓)。这就是模型反演攻击的意义:它揭示了AI模型无意中"出卖"训练数据的隐私风险,提醒我们在部署时需谨慎暴露输出信号。

反演案例:黑盒的图像模型反演攻击

在白盒实验中,攻击者可以访问模型参数和梯度。但现实中更常见的是黑盒场景:攻击者只能通过API查询模型输出(概率分数或标签),无法触碰内部结构。这大幅增加了攻击难度——没有梯度,如何优化?

本实验采用黑盒-分数反演(查询概率,最常见的API形式),使用MNIST手写数字数据集(10类,28×28灰度图)。核心思路是用有限差分法估计梯度:在输入上加/减小扰动,查询两次概率,通过概率差异近似梯度方向。

核心问题是没有梯度,如何知道"往哪个方向改图像"?我们的解决方案是通过有限差分梯度估计.

对于每个采样方向 u(随机单位向量):
    1. 查询 P(y=t | x + ε·u)  → p_plus
    2. 查询 P(y=t | x - ε·u)  → p_minus
    3. 估计梯度:g += (p_plus - p_minus) / (2ε) · u

重复40次取平均,得到近似梯度

代价是每步优化需要约80次API查询(40个方向×2次查询),总计约32,000次查询。

黑盒

代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms
import numpy as np
from tqdm import tqdm
import os

os.makedirs("results/blackbox", exist_ok=True)
device = torch.device("cpu")

# 目标模型定义
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 32, 3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool(x)
        x = F.relu(self.conv2(x))
        x = self.pool(x)
        x = x.view(x.size(0), -1)
        x = F.relu(self.fc1(x))
        return self.fc2(x)

def train_model():
    """训练目标模型"""
    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.1307,), (0.3081,))
    ])
    train_ds = datasets.MNIST(root="./data", train=True, download=True, transform=transform)
    loader = torch.utils.data.DataLoader(train_ds, batch_size=128, shuffle=True)

    model = SimpleCNN().to(device)
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

    for epoch in range(5):
        for x, y in loader:
            x, y = x.to(device), y.to(device)
            optimizer.zero_grad()
            loss = F.cross_entropy(model(x), y)
            loss.backward()
            optimizer.step()

    return model

# 黑盒API封装
class BlackboxAPI:
    """只暴露query()方法返回softmax概率,模拟真实API"""
    def __init__(self, model):
        self.model = model
        self.model.eval()

    def query(self, x):
        with torch.no_grad():
            return F.softmax(self.model(x), dim=1)

# 黑盒反演攻击
class BlackboxInversion:
    def __init__(self, api, img_size=28):
        self.api = api
        self.img_size = img_size

    def total_variation_loss(self, x):
        diff_h = torch.abs(x[:, :, 1:, :] - x[:, :, :-1, :]).mean()
        diff_w = torch.abs(x[:, :, :, 1:] - x[:, :, :, :-1]).mean()
        return diff_h + diff_w

    def estimate_gradient(self, x, target_class, eps=0.02, samples=40):
        """有限差分法估计梯度"""
        grad = torch.zeros_like(x)
        for _ in range(samples):
            u = torch.randn_like(x)
            u = u / torch.norm(u)
            p_plus = self.api.query(x + eps * u)[0, target_class].item()
            p_minus = self.api.query(x - eps * u)[0, target_class].item()
            grad += (p_plus - p_minus) / (2 * eps) * u
        return grad / samples

    def invert(self, target_class, num_iterations=400, lr=0.15, 
               eps=0.02, samples=40, tv_weight=0.001, l2_weight=0.0001):
        x = torch.randn(1, 1, self.img_size, self.img_size, device=device) * 0.5 + 0.5
        x = x.clamp(0, 1)
        x.requires_grad = True
        optimizer = torch.optim.Adam([x], lr=lr)
        history = {'prob': []}

        for i in tqdm(range(num_iterations), desc=f"Class {target_class}"):
            optimizer.zero_grad()
            grad_cls = self.estimate_gradient(x, target_class, eps, samples)
            tv_loss = self.total_variation_loss(x)
            l2_loss = torch.norm(x)

            x.grad = -grad_cls  # 最大化概率
            tv_grad = torch.autograd.grad(tv_loss, x, retain_graph=True)[0]
            l2_grad = torch.autograd.grad(l2_loss, x, retain_graph=True)[0]
            x.grad = x.grad + tv_weight * tv_grad + l2_weight * l2_grad

            optimizer.step()
            x.data.clamp_(0, 1)
            history['prob'].append(self.api.query(x)[0, target_class].item())

        return x.detach(), history

# 主函数
def main():
    model = train_model()
    api = BlackboxAPI(model)
    attacker = BlackboxInversion(api)

    for target in range(10):
        inv_img, history = attacker.invert(target_class=target)
        print(f"Class {target}: Final prob = {history['prob'][-1]:.4f}")

if __name__ == "__main__":
    main()

实验结果

=== 训练目标模型 ===
  Epoch [1/5] Loss: 0.1604 Acc: 95.06%
  Epoch [2/5] Loss: 0.0455 Acc: 98.58%
  Epoch [3/5] Loss: 0.0312 Acc: 99.03%
  Epoch [4/5] Loss: 0.0235 Acc: 99.24%
  Epoch [5/5] Loss: 0.0171 Acc: 99.47%

=== 执行黑盒反演攻击 ===

--- 反演类别 0 ---
Class 0: 100%|████████████████| 400/400 [00:19<00:00, prob=0.9321]
类别 0 最终概率: 0.9321, 查询次数: 32400

--- 反演类别 1 ---
Class 1: 100%|████████████████| 400/400 [00:16<00:00, prob=0.9566]
类别 1 最终概率: 0.9566, 查询次数: 32400

... (类别2-8省略)

--- 反演类别 9 ---
Class 9: 100%|████████████████| 400/400 [00:14<00:00, prob=0.9494]
类别 9 最终概率: 0.9494, 查询次数: 32400

=== 攻击评估 ===
Class 0: Predicted=0, Prob=0.9321, ✓ SUCCESS
Class 1: Predicted=1, Prob=0.9566, ✓ SUCCESS
Class 2: Predicted=2, Prob=0.9621, ✓ SUCCESS
Class 3: Predicted=3, Prob=0.9728, ✓ SUCCESS
Class 4: Predicted=4, Prob=0.9625, ✓ SUCCESS
Class 5: Predicted=5, Prob=0.9773, ✓ SUCCESS
Class 6: Predicted=6, Prob=0.9443, ✓ SUCCESS
Class 7: Predicted=7, Prob=0.9339, ✓ SUCCESS
Class 8: Predicted=8, Prob=0.9694, ✓ SUCCESS
Class 9: Predicted=9, Prob=0.9494, ✓ SUCCESS

总体攻击成功率: 100.0% (10/10)

结果分析

黑盒攻击虽然查询代价增加40倍,但仍然100%成功,说明即使只暴露API概率输出,模型反演攻击仍然可行。

防御

防御MIA的目标可概括为两点:降低输出信号的可优化性(减少攻击反馈),并提高攻击成本与可检测性。以下从输出侧、访问侧和训练侧分类,列出实用方法。

输出最小化

  • 仅返回top-k标签:避免全概率向量,只返top-1/ top-k(k<5)标签+粗粒度置信(e.g., 高/中/低)。


    • 原理:破坏连续反馈,梯度估计失效。
    • 效果:黑盒分数攻击难度翻倍。
    • 代价:损失解释性,适用于生产API。
    • 输出扰动:在概率/logits上加噪声(e.g., Laplace噪声)。

    • 原理:引入不确定性,干扰优化过程。

    • 效果:降低成功率20-30%(Astra Security)。
    • 代价:轻微精度降(<1%)。

访问控制与审计

针对黑盒查询密集型攻击,提高成本。

  • 速率限流与配额:限制QPS/日查询量(e.g., 1000/天)。


    • 原理:MIA需数万查询,限流迫使攻击中断。
    • 效果:黑盒攻击成本增10倍(Practical DevSecOps)。
    • 代价:影响高频用户,需分级授权。
    • 异常检测与审计:监控查询模式(e.g., 重复优化同一类)。

    • 原理:MIA行为特征明显(如高频扰动)。

    • 效果:实时封禁,结合ML检测准确率>90%(Medium文章)。
    • 代价:需日志系统,隐私合规。

训练侧策略

  • 差分隐私(DP)训练:添加噪声到梯度(e.g., DP-SGD)。


    • 原理:界定隐私预算(ε<1),防止过拟合敏感特征。
    • 效果:攻击重构准确率降50%+(Witness.ai)。
    • 代价:模型精度降2-5%,训练时间增。
    • 表示层约束:用对抗训练或降维(如PCA)减少embedding敏感信息。

    • 原理:模糊表示向量,逆映射难度增。

    • 效果:针对表示反演有效(ComSoc期刊)。
    • 代价:需重新训练,任务性能微降。
    • 联邦学习:分布式训练,仅聚合梯度。

    • 原理:数据不集中,减少单点泄露。

    • 效果:多方场景下MIA风险降(Defence.AI)。
    • 代价:通信开销大,适用于分布式系统

仅限广西移动
打开中国移动 APP
首页直接搜索 10 元 300GB
可随时退订

10 元 300g,居然有 3000 分钟通话和 1500 条短信。而且还有 5G-A 速率包,qci6+千兆

image

我的工作流是一个围绕 superpowers 插件Loop,superpowers 的理念是:先思考再动手。当你提出一个需求,不会急于写代码,而是先退一步问你"你真正想要实现什么",通过对话梳理出完整的设计方案,再分步执行。

核心设计是 masterworker 分离。

  • 脑暴会话 (master):专注于思考和设计,输出高质量的设计文档和执行计划
  • 执行会话 (worker):专注于代码实现,执行详细的计划

1、需求录入 - 首先我会在 Zed 上进行需求录入,采用 md 格式。这一步非常重要,我大概有 30% 的时间花在需求录入上,我会把能想到的关于此需求的背景、最终目标、可行的技术方案、风险点、外部 API 文档等等一切资源,都在需求文档中说明。对于需求文档,我不会太在意格式,会有比较多口语化的表达。

2、脑暴阶段 - 把需求 MD 喂给 Claude,调用 /superpowers:brainstorm 和 claude 进行思维碰撞。这个阶段不写任何代码,只讨论设计方案和实现细节,最终输出 design.mdimplement.md,保证最终的实现方案是完美符合我的预期的。

3、 执行阶段 - 这里我会选择新起一个 ClaudeCode 会话,而不是在脑暴会话中进行代码实现。新会话的好处:一、原先脑暴会话已经经过多轮对话了,一般情况下上下文会比较满,新会话响应更快,并且不会“犯傻”;二、implement.md 足够详细,无需额外上下文

4、 CodeReview - 在 Zed 中进行代码审查和功能验收。关于代码审查,对于一些代码细节和实现原理,这里我会使用 zed-agent 来辅助我进行代码 review,当然,你也可以在终端新建一个 ClaudeCode 会话或者使用 Zed 的 Claude Agent。原则是尽量不在脑暴和执行会话中引入太多不必要的问题,保持这两个会话的「干净」。发现问题后,将改进项写入新的需求 MD

5、 LOOP - 改进项 MD 喂回脑暴会话,开始下一轮脑暴迭代

非常简单,但是效果超群。充分的前期设计可以提升 AI 的效率和质量,避免多次的来回拉扯。

举个真实案例:我用这套工作流将个人博客从 Quarz 框架迁移到 Astro 框架。脑暴阶段确认好设计方案后,我让 Claude 执行计划,然后就去睡午觉了。醒来发现 Claude Code 已经完美完成任务——中间零中断,一次成功,共计 5000+ 行代码变更。

任务要求:给定一个字符串“ILoveCangjie”,编写一个仓颉应用程序,来统计该字符串的字符数。

习题解题思路1

练习步骤:

  • 定义字符串变量s;
  • 遍历该变量s里面的字符。可以使用字符串的toArray()函数将字符串转为字节数组;
  • 每遍历一次,即统计了一次字符数;
  • 打印最终的遍历次数,即得出了该字符串的字符数。

代码参考:见“count_the_number_of_characters”应用。

习题解题思路2

字符串的size属性,可以直接获取字符串 UTF-8 编码后的字节长度。

public prop size: Int64

参考引用

背景介绍

前段时间上架的“智能带办”鸿蒙应用,应用亮点是根据用户输入要做的事情自动自动生成需要带的东西。在使用过程中发现有些东西不能立马拿到的需要在未来某个时刻拿的需要提醒。重新设计产品时发现,自己实现提醒功能还是挺复杂的,如果用云端方案接入通知方式不仅复杂而且可控性太差。这个时候想到接入日程,通过系统能力来实现功能。

接入后向左滑动待办物品,出现日历按钮,效果如图:
image.png

点击按钮弹出日程计划日期和时间:
image.png
选择完日期和事件自动自动完成日程创建。下面先介绍鸿蒙日历能力。

鸿蒙日历📅 能力介绍

简介

HarmonyOS 的 Calendar Kit(日历服务)是一套系统级的日程管理接口,旨在将出行、餐饮、运动等各类与时间相关的服务与系统日历无缝集成,实现统一的时间视图与提醒能力。核心能力如下:

  • 账户管理:支持创建、查询和删除日历账户。应用可创建专属账户(返回唯一 accountId),删除账户将同时清除其下所有日程。
  • 日程管理 (CRUD):在指定账户下,支持日程的全生命周期管理,包括创建(返回唯一 eventId)、删除、更新和查询。创建时可设置标题、时间、地点、提醒及重复规则等属性。
  • 一键服务:通过永久性授权,可将带 DeepLink 的“一键服务”写入日历。当日程临近或到期,系统会在日历、通知、卡片等位置展示服务按钮,用户点击即可直达服务页面,实现从“看到日程”到“完成服务”的闭环。
    我们的应用主要用到账户和日程管理,使用日历需要如下权限:
申请权限支持的日历账户操作范围支持的日程操作范围
ohos.permission.READ_CALENDAR读取系统默认及当前应用创建的日历账户。读取上述账户下当前应用创建的日程。
ohos.permission.WRITE_CALENDAR增、删、改当前应用创建的日历账户。增、删、改上述账户下当前应用创建的日程。
ohos.permission.READ_WHOLE_CALENDAR读取设备上所有日历账户。读取所有应用创建的日程。
ohos.permission.WRITE_WHOLE_CALENDAR增、删、改设备上所有日历账户。增、删、改所有应用创建的日程。

我们只需要读取我们自己应用创建的日历账号下日程即可,所以申请前两个权限即可。

账户管理

日历账户‌用于存储和管理个人或团队的日程,通过日历账户,用户可以方便地查看、编辑和共享日程信息。日历管理器CalendarManager用于管理日历账户Calendar。日历账户主要包含账户信息CalendarAccount和配置信息CalendarConfig。

我们可以创建属于应用特有的日历账户,还可以对日历账户进行新增、删除、更新和查询。此外,每个日程Event归属于某一个特定的日历账户,可以通过日历账户对账户下面的日程进行管理。

@ohos.calendarManager提供了日历账户管理的相关接口,常用到的接口如下表:

接口名称描述
getCalendarManager(context: Context): CalendarManager根据上下文获取日历管理器对象CalendarManager,用于管理日历。
createCalendar(calendarAccount: CalendarAccount): Promise<Calendar>根据日历账户信息,创建一个Calendar对象,使用Promise异步回调。
getCalendar(calendarAccount?: CalendarAccount): Promise<Calendar>获取默认Calendar对象或者指定Calendar对象,使用Promise异步回调。

默认Calendar是日历存储首次运行时创建的,若创建Event时不关注其Calendar归属,则无须通过createCalendar()创建Calendar,直接使用默认Calendar。
getAllCalendars(): Promise<Calendar[]>获取当前应用所有创建的Calendar对象以及默认Calendar对象,使用Promise异步回调。
deleteCalendar(calendar: Calendar): Promise<void>删除指定Calendar对象,使用Promise异步回调。
getConfig(): CalendarConfig获取日历配置信息。
setConfig(config: CalendarConfig): Promise<void>设置日历配置信息,使用Promise异步回调。
getAccount(): CalendarAccount获取日历账户信息。
日程管理

日程指特定的事件或者活动安排,日程管理即对这些事件、活动进行规划和控制,能更有效地利用相关资源、提高生产力和效率,使人们更好地管理时间和任务。Calendar Kit中的日程Event归属于某个对应的日历账户Calendar,一个日历账户下可以有多个日程,一个日程只属于一个Calendar。取到日历账户对象之后,即可对该账户下的日程进行管理,包括日程的创建、删除、修改、查询等操作。在创建、修改日程时,支持对日程的标题、开始时间、结束时间、日程类型、日程地点、日程提醒时间、日程重复规则等相关信息进行设置,以便进行更丰富更有效的日程管理。

@ohos.calendarManager提供了日程管理的相关接口,常用到的接口如下表:

接口名称描述
getCalendarManager(context: Context): CalendarManager根据上下文获取CalendarManager对象,用于管理日历。
createCalendar(calendarAccount: CalendarAccount): Promise<Calendar>根据日历账户信息,创建一个Calendar对象,使用Promise异步回调。
addEvent(event: Event): Promise<number>创建日程,入参Event不填日程id,使用Promise异步回调。
editEvent(event: Event): Promise<number>通过跳转到日程创建界面创建单个日程,入参Event不填日程id,使用Promise异步回调。
deleteEvent(id: number): Promise<void>删除指定日程id的日程,使用Promise异步回调。
updateEvent(event: Event): Promise<void>更新日程,使用Promise异步回调。
getEvents(eventFilter?: EventFilter, eventKey?: (keyof Event)[]): Promise<Event[]>获取Calendar下符合查询条件的Event,使用Promise异步回调。

智能带办接入过程

1、导入依赖

接入账号管理,首先需要导入相关依赖:

import { abilityAccessCtrl, AbilityConstant, common, PermissionRequestResult, Permissions, UIAbility, Want } from '@kit.AbilityKit';
import { calendarManager } from '@kit.CalendarKit';
2、创建日历管理类
  
const TAG = 'CalendarService';  
  
/**  
 * Service to manage Calendar Kit operations. */
   export class CalendarService {  
  private static calendar: calendarManager.Calendar | undefined = undefined;  
  //账号信息
  private static readonly calendarAccount: calendarManager.CalendarAccount = {  
    name: 'IntelligentTodo',  
    type: calendarManager.CalendarType.LOCAL,  
    displayName: '智能带办'  
  };  
  
  /**  
   * 添加日程到日历
   * @param title The title of the todo.  
   * @param description The description of the todo.  
   * @param startTime The start time (milliseconds).   * @param endTime The end time (milliseconds).   * @returns The event ID.    */  
     public static async addEvent(title: string, description: string, startTime: number, endTime: number): Promise<number> {  
    const mgr = AppStorage.get<calendarManager.CalendarManager>('calendarMgr');  
    if (!mgr) {  
      Logger.e(TAG, 'calendarMgr is not initialized in AppStorage');  
      throw new Error('日历服务未准备就绪');  
    }  
  
    // Request permissions first  
    const context = AppStorage.get<common.UIAbilityContext>('abilityContext');  
    if (context) {  
      const granted = await CalendarService.checkAndRequestPermissions(context);  
      if (!granted) {  
        throw new Error('未获得日历权限');  
      }  
    }  
    try {  
      if (!CalendarService.calendar) {  
        CalendarService.calendar = await CalendarService.getOrCreateCalendar(mgr);  
      }  
  
      const event: calendarManager.Event = {  
        title: title,  
        description: description,  
        type: calendarManager.EventType.NORMAL,  
        startTime: startTime,  
        endTime: endTime,  
        reminderTime: [10] // Default 10 minutes reminder  
      };  
  
      if (!CalendarService.calendar) {  
        throw new Error('日历对象初始化失败');  
      }  
      const eventId = await CalendarService.calendar.addEvent(event);  
      Logger.i(TAG, `Succeeded in adding event, id -> ${eventId}`);  
      return eventId;  
    } catch (error) {  
      Logger.e(TAG, `Failed to add event: ${JSON.stringify(error)}`);  
      throw new Error(JSON.stringify(error));  
    }  
  }  
  /**  
   * 创建日历对象
   */  
     private static async getOrCreateCalendar(mgr: calendarManager.CalendarManager): Promise<calendarManager.Calendar> {  
    try {  
      // Try to find if our account already exists  
      const calendars = await mgr.getAllCalendars();  
      const existing = calendars.find(c => {  
        const acc = c.getAccount();  
        return acc.name === CalendarService.calendarAccount.name;  
      });  
      if (existing) {  
        return existing;  
      }  
  
      // Create new account if not exists  
      const newCalendar = await mgr.createCalendar(CalendarService.calendarAccount);  
      const config: calendarManager.CalendarConfig = {  
        enableReminder: true,  
        color: '#aabbcc'  
      };  
      await newCalendar.setConfig(config);  
      return newCalendar;  
    } catch (err) {  
      Logger.e(TAG, `getOrCreateCalendar error: ${JSON.stringify(err)}`);  
      // Fallback to default calendar if creation fails  
      return await mgr.getCalendar();  
    }  
  }  
  /**  
   * 动态获取权限
  */  
     private static async checkAndRequestPermissions(context: common.UIAbilityContext): Promise<boolean> {  
    const permissions: Array<Permissions> = ['ohos.permission.READ_CALENDAR', 'ohos.permission.WRITE_CALENDAR'];  
    const atManager = abilityAccessCtrl.createAtManager();  
    try {  
      const result = await atManager.requestPermissionsFromUser(context, permissions);  
      const grantStatus = result.authResults;  
      return grantStatus.every(s => s === 0);  
    } catch (err) {  
      Logger.e(TAG, `requestPermissionsFromUser error: ${JSON.stringify(err)}`);  
      return false;  
    }  
  }}

getOrCreateCalendar根据上下文获取日程管理器对象calendarMgr,用于对日历账户进行相关管理操作。官方推荐在EntryAbility.ets文件中进行操作,我们这里进行独立封装,对权限做到精准控制,在使用时再申请。接着根据日历账户信息,创建一个日历账户Calendar对象。创建日历账户之前,我们需要先根据账户信息进行查询,如果账户不存在则抛出异常信息,捕获到异常再进行日历账户的创建,否则可能会出现账户重复创建的问题。
日历账户创建之后,日历账户颜色默认为黑色,不指定日历账户颜色可能导致部分版本/设备深色模式下显示效果不佳。开发者需要调用setConfig()接口设置日历配置信息,包括是否打开日历账户下的日程提醒能力、设置日历账户颜色。

const calendarAccounts: calendarManager.CalendarAccount = {
  name: 'MyCalendar',
  type: calendarManager.CalendarType.LOCAL,
  displayName: 'MyCalendar'
};
// 日历配置信息
calendarMgr?.getCalendar(calendarAccounts, (err, data) => {
  //获取日历账户
  if (err) {
    hilog.error(DOMAIN, 'testTag', `Failed to get calendar, Code is ${err.code}, message is ${err.message}`);
  } else {
    const config: calendarManager.CalendarConfig = {
      // 打开日程提醒
      enableReminder: true,
      // 设置日历账户颜色
      color: '#aabbcc'
    };
    // 设置日历配置信息
    data.setConfig(config).then(() => {
      hilog.info(DOMAIN, 'testTag', '%{public}s', `Succeeded in setting config, data->${JSON.stringify(config)}`);
    }).catch((err: BusinessError) => {
      hilog.error(DOMAIN, 'testTag', `Failed to set config. Code: ${err.code}, message: ${err.message}`);
    })
  }
});

addEvent方法封装了在当前日历账户下添加日历日程,注意入参中不需要填写日程id。创建日程时,支持设置日程的标题、开始时间、结束时间、日程类型、日程地点、日程提醒时间、日程重复规则等相关信息。程创建成功后会返回一个日程id,作为日程的唯一标识,后续可按照日程id进行指定日程的更新或删除。
目前支持以下两种方式来创建日程。
方式一:可以在日历账户下通过addEvent()或addEvents()接口创建日程。其中可使用addEvent()接口创建单个日程,也可以使用addEvents()接口批量创建日程,此处以创建单个日程为例。
方式二:在获取到日历管理器对象后,可通过editEvent()接口创建单个日程。调用此接口创建日程时,会跳转到日程创建页面,在日程创建页面进行相关操作完成日程的创建, editEvent()不支持自定义周期性日程创建。

我们采用方式一,用户点击日程时弹窗日期时间选择器,用户选择时间后调用addEvent方法创建日程。

总结

本次功能迭代的核心思路是“借力系统能力,优化产品体验”。面对自建提醒功能的复杂性,我们选择接入鸿蒙Calendar Kit,将待办事项转化为系统日程。通过左滑待办项快速创建日程,并利用系统日历实现稳定、统一的提醒。这一方案通过封装CalendarService精准管理权限与账户,既大幅降低了开发复杂度和维护成本,又为用户提供了原生、可靠的提醒服务,是“站在系统肩膀上”高效解决通用需求的典型实践。

关联帖子: 【💰】每天分享一个 LINUX DO 邀请码给本站活跃用户

分享一个 V2EX 邀请码给 2 友。目前只有一个金币,只能生成一个邀请码。

只限 6 级以上且拥有除了种子用户徽章以外其他徽章的活跃用户

满足条件的可以留下邮箱地址。(邮箱地址请用 Base64 加密功能加密)

screenshot-2026-01-22-22-45-34

再重申一遍,

只限 6 级以上且拥有除了种子用户徽章以外其他徽章的活跃用户

接上文:自定义 Emoji 重绘版 https://2libra.com/post/personal-works/QhAL4AQ

@Jimmy
原:image|200 现:imege|200

原:image|200 现:image|200

image|200 image|200

接下来还会继续重绘已有表情里面不太好的,以及现在还没有的其他新表情。大家有想用的表情可以发出来,我慢慢弄。大家也可以自己在 chatGPT 传几张之前的表情图片,使用以下提示词试一下自行重绘。

请阅读并分析以上表情图片的绘画风格,记住它们的绘画风格。接下来我会把我需要重绘的其他表情图片发给你,你需要在完全保持原表情表达的意思基础上,将后续我发送给你的表情图片的绘画风格,修改为之前你记住的的绘画风格,并且面部表情和我发给你的图片要尽量保持一致。现在你准备好了后就可以回复我:“请发送你需要重绘的表情图片,我将按照我记住的风格来重绘它,并且和你发给我的图片尽量保持一致”

主要更新:

Apple Watch 独立播放

  • 无需 iPhone 即可播放
  • 手表端完整控制(播放/暂停/进度/章节)
  • 适合跑步、健身等场景

格式支持扩展

  • PDF 阅读 + TTS 朗读
  • MOBI / AZW / AZW3 (Kindle 格式)
  • DOCX (Word 文档)
  • HTML / HTM (网页文件)
  • RTF (富文本)

核心功能

  • 离线 + 在线双模式
  • 播放速度调节 (0.5x - 2.0x)
  • CarPlay 车载支持
  • 本地文件管理

下载地址

iOS: https://apps.apple.com/us/app/audiotome/id6755520834

Android: https://play.google.com/store/apps/details?id=com.audiotome.audio_tome

交流群:

群二维码


评论留下平台+邮箱,送激活码

欢迎试用反馈

以下内容仅为个人交易复盘,不构成投资建议。
强烈不建议任何人入市参与。
本人只是日内小仓位操作,纯练手,切勿抱有赚钱幻想。

今天更新较晚,主要原因是开仓后行情出现回落,价格被短期压制。期间通过操作,直至刚刚完成解套,并顺势获取了一定利润。

整体行情来看,价格处于缓慢下行过程中,但下方支撑较为明显,跌势难以有效展开,结构上更偏向震荡偏弱。

原本在回本后有离场的想法,但考虑到交易纪律,仍按既定计划继续执行。实际卖出后,行情快速走出一根下行 K 线,波动幅度较大,情绪冲击明显。

正如一贯所坚持的,止盈是弹性的,其余环节以纪律为先。

分享主要记录的是当下的心理状态,而非具体操作细节。操作本身并不算精细,但策略核心始终围绕市场变化展开;市场在变,而人的心理模式相对稳定,这也是复盘的重点所在。

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

买 NAS 不玩 Docker 乐趣少一半。

但 Docker 的镜像(简单理解为软件安装包吧)是放在国外服务器保存的,我们要下载这些镜像全凭运气。

绿联 NAS 虽然推荐了一个加速器(https://docker.1ms.run),但有些镜像还是搜到下载不到。

比如 memos 这款高颜值的笔记工具,我下载了几次都失败了。

先别急删掉 Docker,我们多配置几个镜像加速器就可以了。

绿联 NAS 的 Docker 镜像加速器配置方法很简单。

打开 Docker,切换到「镜像」页面,点击右上角的“齿轮”按钮。

在「镜像仓库」这里,点击下图箭头所指的「加速器配置」按钮。

把这堆地址都填进去,点击「确定」按钮就行了~

回到「镜像」面板,搜索你想安装的镜像就能下载了。


以上就是本文的全部内容啦,想了解更多NAS玩法可以关注《NAS邪修》👏

最后推荐一下玩 NAS 的工友,在 NAS 上装一个 n8n 接入大模型,可以帮你定时定候完成各种工作,比如签到啦、写文章啦、生成海报和视频啦、自动发布到各大平台啦~

想了解 n8n 的工友可以关注我的专栏👉 《n8n修炼手册》

点赞 + 关注 + 收藏 = 学会了

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

Memos 是一款开源免费、隐私优先的轻量化笔记工具,支持 Docker 一键部署到 NAS(数据本地存储,完全自主掌控)。它支持纯文本和 Markdown 格式,可通过标签、日历分类笔记,还能实现笔记引用、插入图片 / 附件等实用功能,低资源占用不拖慢设备,记想法、列待办、存资料都合适。

本文使用群晖 NAS 部署 memos,其他 NAS 或者在电脑用 Docker 部署的方法大同小异。

首先在“File Station”的“docker”文件夹里创建一个“memos”文件夹。

然后打开”Container Manager“创建一个新项目,相关配置如下图所示。

输入以下代码(注意代码格式!!!)

services:
  memos:
    image: neosmemo/memos:latest
    container_name: memos
    volumes:
      - .:/var/opt/memos
    ports:
      - 5230:5230

开启“通过 Web Station 设置网页门户”

接下来打开“web Station”新建一个“网络门户”。

相关配置如下图所示。

端口设置一个不跟其他项目冲突的数字即可,我用的是 2345

完成上面的操作后,打开浏览器,输入 NAS的IP + 端口(本例用的是2345),就可以使用 memos了。

首次使用需要创建一个账号。

在登录页下方可以设置 memos 系统使用什么语言以及主题色。

登录后就可以开始写笔记了。它支持 Markdown 语法,挺适合用来写博客的。


以上就是本文的全部内容啦,想了解更多NAS玩法可以关注《NAS邪修》👏

最后推荐一下玩 NAS 的工友,在 NAS 上装一个 n8n 接入大模型,可以帮你定时定候完成各种工作,比如签到啦、写文章啦、生成海报和视频啦、自动发布到各大平台啦~

想了解 n8n 的工友可以关注我的专栏👉 《n8n修炼手册》

点赞 + 关注 + 收藏 = 学会了

​基于最新鸿蒙系统的技术书籍《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》上市啦,要知道 HarmonyOS 6 在一个多月前的10月22日才正式发布,因此这本鸿蒙教程可谓贴近最新的 HarmonyOS 6 系统。

当前 HarmonyOS 6 的装机量迅猛增长,有望在春节前突破5000万台大关,可见鸿蒙系统的应用开发将越来越流行,甚至借助国产化的浪潮,未来在国内移动操作系统领域一举夺魁也不是不可能。

有鉴于此,博主精心编撰了 HarmonyOS 6 的应用开发教程《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》,从基础到高级,从理论到实战,从 UI 到 AI ,仅需一本书籍,即可让读者掌握鸿蒙应用的常见开发技能。

鸿蒙应用开发与安卓应用开发同为App开发,比如鸿蒙版微信和安卓版微信都是即时通信App,二者在实现技术上并无多少本质区别。所以《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》一书以《Android Studio开发实战:从零基础到App上线(第3版)》为蓝本,把安卓系统的App教程改造为鸿蒙系统的App教程,以便安卓开发者能够按图索骥迅速上手。欣喜的是,《Android Studio开发实战:从零基础到App上线(第3版)》提到的安卓开发技术,绝大部分都能在鸿蒙系统找到对应的平替技术,而且还是更简单的代码实现。

作为《Android Studio开发实战:从零基础到App上线(第3版)》一书的鸿蒙姊妹篇,《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》仍然采取了由浅入深、循序渐进的章节体例,其中前8章是基础部分,主要讲解 DevEco Studio 的环境搭建、ArkTS语言编程基础、鸿蒙App开发的各种常用组件、鸿蒙App开发的页面转场和消息交互、鸿蒙App的几种数据存储方式等;后8章是进阶部分,主要讲解鸿蒙App开发的后台任务、手势交互、动画特效、网络通信、多媒体、感知定位、人工智能、多端部署等。

曾经有老读者咨询“从零基础到App上线”系列书籍的第4版何时面世,现在博主终于可以说,“从零基础到App上线”的第4版已经出版啦,而且第4版是鸿蒙版本的“从零基础到App上线”,它就叫做《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》,该书把安卓版教程平替为鸿蒙版教程,也是一个勇敢的尝试。

《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》在讲解知识点的同时给出了大量实战范例,方便读者迅速将所学的知识运用到实际开发中。通过本书的学习,读者能够掌握3类主流App的基本开发技术,包括购物App(电子商务)、聊天App(即时通信)、娱乐App(短视频分享)。另外,能够学会开发一些趣味应用,包括计算器、录音笔、电子相册、打牌游戏、指南针、水平仪、卫星浑天仪、登山助手、附近交友、速记助手、人脸识别等等。可见《Android Studio开发实战:从零基础到App上线(第3版)》一书提到的实战项目,本书基本提供了对应的鸿蒙版App。

《鸿蒙HarmonyOS 6应用开发:从零基础到App上线》的随书源码包括客户端部分和服务端部分,其中客户端的App代码基于 DevEco Studio 6.0.0 Release 开发,并使用 API 20 的 SDK (HarmonyOS 6.0.0)编译与调试通过,测试机型包括 Mate 60 Pro 和 nova 12 Pro 。配套的服务端源码采用 Java WEB 框架,结合 MySQL 数据库,并基于 IDEA 开发。

📦 AppPorts

https://github.com/wzh4869/AppPorts
外置硬盘拯救世界!/ External drives save the world!

一款专为 macOS 设计的应用程序迁移与链接工具。
轻松将庞大的应用程序迁移至外部存储,同时保持系统无感运行。

✨ 简介

Mac 的内置存储空间寸土寸金。AppPorts 允许您一键将 /Applications 目录下的应用程序迁移到外部移动硬盘、SD 卡或 NAS ,并在原位置保留应用入口,让系统误以为应用仍在本地。

对 macOS 系统而言,应用依然“存在”于本地,您可以像往常一样启动它们,但实际占用的却是廉价的外部存储空间。

🚀 核心功能

  • 📦 应用瘦身:一键将几十 GB 的大型应用(如 Logic Pro, Xcode, 游戏等)迁移至外置硬盘。
  • 🔗 Contents 链接:采用 Contents 目录链接 方案,专为适配 macOS 机制设计。
    • 原理:在本地保留 .app 文件夹结构,仅将内部的 Contents 数据目录链接至外部存储。
    • 空间占用:本地仅保留文件夹索引信息,占用空间受文件系统块大小限制(通常可忽略不计)。
    • 兼容性:Finder 不会显示快捷方式小箭头,且支持 macOS 26 的 "App 菜单" 显示。
  • 🛡️ 安全机制
    • 自动识别并锁定 系统应用,防止误操作破坏系统。
    • 迁移前检测 运行状态,防止损坏正在运行的应用。
  • ↩️ 随时还原:只需点击“还原”,即可将应用完整迁回本地磁盘,符号链接自动移除。
  • 🎨 现代 UI
    • 原生 SwiftUI 开发,丝滑流畅。
    • 完美适配 深色模式
    • 支持 中英双语,可随系统或手动切换。
  • ♿️ 无障碍优化
    • VoiceOver 深度适配:支持列表行整体朗读、转子快捷操作。
    • 语义化界面:屏蔽装饰性图标干扰,状态标签支持清晰的语音播报。
    • 盲文支持:新增 Braille 语言选项,界面文字可直接显示为点字。
  • 🌍 全球化支持
    • 20+ 种语言支持
    • 本地化单位:文件大小会自动遵循当前语言的数字和单位格式习惯。
  • 🔍 快速检索:内置搜索栏,快速定位本地或外部应用。

🏆 为什么选择 AppPorts ?

相较于市面上其他方案,AppPorts 采用了独特的 Contents 链接 技术,兼顾了美观、兼容性与系统整洁度。

方案 AppPorts 传统软链
Finder 图标 原生 (无箭头) ❌ 有箭头 (快捷方式)
Launchpad 完美索引 ⚠️ 经常失效
App 菜单 (macOS 26) 完美支持 ❌ 不支持
文件系统整洁度 极佳 (仅 1 个链接) ✅ 极佳 (仅 1 个链接)
维护与还原 毫秒级 ✅ 毫秒级

📸 截图

欢迎页 主界面
Welcome Main
深色模式 语言切换
Dark Lang

🛠️ 安装与运行

系统要求

  • macOS 14.0 (Sonoma) 或更高版本。

下载安装

请前往 Releases 页面下载最新版本的 AppPorts.dmg

⚠️ “AppPorts”已损坏,无法打开

如果打开应用时遇到此提示(且系统建议移到废纸篓),这是因为应用没有进行开发者签名,被 macOS 的 Gatekeeper 机制拦截。
(注意:以下命令假设您已将 AppPorts 拖入 应用程序 文件夹)
您需要在终端运行以下命令来移除隔离属性,即可正常打开:

xattr -rd com.apple.quarantine /Applications/AppPorts.app

⚠️ 权限说明

首次运行时,AppPorts 需要 “完全磁盘访问权限” 才能读写 /Applications 目录。

  1. 打开 系统设置 -> 隐私与安全性
  2. 选择 完全磁盘访问权限
  3. 点击 + 号,添加 AppPorts 并开启开关。
  4. 重启 AppPorts 。

目前表情的像素我是控制在 350px,在一些 RSS 开启 webview 上浏览时就没啥好体验,尝试通过行内样式减小图片显示宽度好像也没生效。

图片宽高像素降低 -> 图片体积减小 -> 图片变糊force_smile

SVG 显示效果最好,不过 SVG 导出即便做了优化,尺寸还是较大,个别 emoji 超过 1M,就算 cdn 首次加载也会消耗一些流量。

行情 API 的正确使用方式

常见问题

在行情系统开发中,常见以下问题:

  • 首页行情列表每秒轮询 K 线接口获取最新价
  • 所有页面都建立 WebSocket 连接以实现"实时更新"
  • 系统启动时直接订阅 WebSocket,但未获取可用品种列表
  • 页面切换时旧的 WebSocket 连接未关闭

这些问题的根源在于缺乏正确的使用心智模型。
即不清楚在什么阶段该使用什么接口。

大多数 API 文档会说明接口返回的数据结构。
但不会说明接口的适用场景和使用时机。


行情 API 的本质:数据分层

行情 API 不是接口的集合,而是一套数据分层系统。

构建行情系统时,系统在不同阶段对数据的需求完全不同:

1. 数据使用阶段

  • 启动阶段:系统需要获取可交易品种列表
  • 展示阶段:页面需要显示当前价格
  • 实时阶段:需要在价格变化时主动推送

2. 数据类型

  • 快照:当前时刻的价格、涨跌幅(适合列表、首页)
  • 历史:过去一段时间的价格走势(适合图表、回测)
  • 持续流:价格变化时主动推送(适合实时盯盘、交易执行)

3. 系统复杂度

  • REST API:简单、稳定、易维护,需要主动轮询
  • WebSocket:实时、高效,但需要处理连接管理、重连、心跳

理解这三个维度,可以明确每个接口的适用场景。


行情 API 的分层设计

1. 可用交易品种(Symbols)

系统启动的第一步是获取可用品种列表。

硬编码品种代码会导致以下问题:

  • 退市品种无法及时移除
  • 新上市品种无法及时添加

建议在系统启动时调用品种列表接口,并缓存结果。

多市场统一命名的价值

统一的命名规则可以用同一套代码逻辑处理不同市场的数据:

  • 港股:700.HK9988.HK
  • 美股:AAPL.USTSLA.US
  • 外汇:EURUSDGBPUSD

这避免了为每个市场编写适配层。

接口示例

GET /v1/symbols/available?market=HK&limit=10

使用建议

  • 系统启动时调用一次,缓存结果
  • 不要在每次查询行情前调用此接口
  • 定期更新建议频率为每天一次

多市场统一 symbol 示意图


2. Ticker(实时快照)

Ticker 接口适用于大部分"显示当前价格"的场景。
行情列表页、首页概览、定时刷新的看板。

使用 K 线接口获取最新价存在以下问题:

  • K 线接口返回的数据结构更复杂
  • 需要处理时间对齐问题
  • 无法一次查询多个品种

Ticker 接口的优势:

  • 返回数据轻量
  • 一次请求可查询多个品种(通常支持 50 个左右)
  • 不需要处理时间对齐问题

接口示例

GET /v1/market/ticker?symbols=700.HK,AAPL.US

返回数据

{
  "code": 0,
  "message": "success",
  "data": [
    {
      "symbol": "700.HK",
      "last_price": "602.5",
      "volume_24h": "16003431",
      "high_24h": "606",
      "low_24h": "598",
      "timestamp": 1768982936000
    }
  ]
}

使用建议

只要不是需要实时价格跳动的场景。
Ticker + 定时刷新(5-10 秒)即可满足需求。

Ticker 适用场景示意图

3. K 线(结构化历史)

K 线接口的核心参数是 interval(时间间隔)。
决定了数据的颗粒度。

不同的分析场景对数据颗粒度的要求不同:

  • 1m:1 分钟 K 线,适合短线交易、实时图表
  • 1h:1 小时 K 线,适合日内分析
  • 1d:日 K 线,适合中长期分析、回测

接口示例

GET /v1/market/kline?symbol=AAPL.US&interval=1d&limit=30

使用建议

  • 图表展示:使用 limit 参数(如"显示最近 30 天")
  • 历史回测:使用时间范围参数(如"2023 年 1 月到 3 月的数据")

K线 vs 实时数据对比图

4. WebSocket(实时流)

WebSocket 的代价包括:

  • 维护长连接(心跳、重连、异常处理)
  • 处理订阅管理
  • 处理消息队列
  • 处理网络波动

适用场景

  • 实时盯盘(延迟要求在秒级以内)
  • 价格预警(价格触发阈值时需要立即通知)
  • 高频数据监控(需要毫秒级数据更新)

不适用场景

  • 行情列表页
  • 历史图表
  • 低频监控

以上场景使用 REST API + 定时刷新即可。

接口示例

const ws = new WebSocket('wss://api.example.com/v1/realtime?api_key=YOUR_API_KEY');

ws.onopen = () => {
  ws.send(JSON.stringify({
    cmd: 'subscribe',
    data: { channel: 'ticker', symbols: ['700.HK'] }
  }));
};

设计限制

外汇品种通常仅支持 ticker 频道(不支持 depth 和 trade)。
因为外汇市场是 OTC 市场,没有集中的订单簿。
股票和加密货币支持 ticker、depth、trade 三种频道。

REST vs WebSocket 使用边界图

完整使用路径示例

以港股行情监控系统为例:

Step 1:启动时拉取可用品种

GET /v1/symbols/available?market=HK&limit=100

目的:获取系统支持的港股品种,缓存到本地。


Step 2:页面展示用 Ticker + K 线

首页行情列表

GET /v1/market/ticker?symbols=700.HK,9988.HK,3690.HK

图表展示

GET /v1/market/kline?symbol=700.HK&interval=1d&limit=30

刷新策略:每 5-10 秒刷新一次 Ticker。
K 线按需加载(用户切换图表时才加载)。

刷新频率建议:

  • 太快(如每秒刷新)会增加服务器压力
  • 太慢(如 30 秒)数据实时性不足
  • 5-10 秒是平衡点

Step 3:关键模块用 WebSocket

仅在需要实时推送的场景建立 WebSocket 连接:

ws.send(JSON.stringify({
  cmd: 'subscribe',
  data: { channel: 'ticker', symbols: ['700.HK'] }
}));

退出实时监控页面时,必须取消订阅并关闭连接。
否则会导致连接数超限,影响新用户建立连接。

完整使用路径流程图

常见错误

1. 过度使用 WebSocket

错误做法:系统启动就建立 WebSocket,订阅所有品种。

问题:首页显示 50 个品种的行情。
订阅所有品种会导致用户量上升时服务器连接数超限。

正确做法:大部分场景使用 REST API。
仅在需要实时推送的模块使用 WebSocket。


2. K 线接口滥用

错误做法:每秒调用 K 线接口获取最新价。

问题:K 线接口是为历史数据设计的。
不是为实时价格设计的。
频繁调用浪费资源,且可能因时间对齐问题导致数据不准确。

正确做法:K 线用于历史数据和图表。
实时价格使用 Ticker 或 WebSocket。


3. Symbol 不缓存

错误做法:每次查询行情前都调用 /v1/symbols/available

问题:可用品种列表通常不会频繁变化。
每次都查询是浪费。

正确做法:启动时调用一次,缓存结果。
定期(如每天)更新。


4. Interval 选择不当

错误做法:不管什么场景都使用 1m(1 分钟 K 线)。

问题:1 分钟 K 线数据量大。
如果只是查看"最近一个月的走势",使用日 K 线即可。
使用 1 分钟 K 线浪费带宽,增加前端渲染压力。

正确做法

  • 实时图表:1m5m
  • 日内分析:1h
  • 中长期分析:1d

5. 混淆行情 API 与交易 API

错误做法:直接使用行情数据做下单决策。
不考虑延迟和数据完整性。

问题:行情 API 提供的是市场数据。
主要用于展示和分析。
交易操作(下单、撤单)需要对接交易所的交易 API。

正确做法:行情 API 用于数据展示和策略分析。
交易操作使用交易 API。

常见错误示意图

总结

使用行情 API 时,首先明确当前处于哪个阶段:

  • 启动系统
  • 展示页面
  • 实时监控

根据阶段选择合适的接口。
可以避免系统设计不合理导致的性能问题和维护困难。


系列说明

本文是「行情 API 的工程化使用方式」系列的第一篇。
后续将继续讲解:

  • WebSocket 实战:连接管理、心跳机制、数据补偿
  • K 线数据的正确使用方式:interval 选择、时间对齐、数据缓存策略
  • 行情系统的性能优化实践:从接口调用到前端渲染的完整优化方案
  • 多市场行情数据的统一处理:如何用一套代码处理港股、美股、外汇的差异

参考资料

本文基于 TickDB API v1.0.0 撰写。
完整接口参数说明、错误码处理、API 参考:


为什么标准化要把均值设为0、方差设为1?

先说均值。均值就是平均数,所有观测值加起来除以个数。

μ是均值,n是数据点总数,xᵢ是每个数据点,所以均值就是数据的重心位置。比如均值是20,那20就是平衡点。这不是说所有点到20的距离相等而是说两边的"重量"刚好在20这个位置抵消掉。

而方差衡量的是数据有多分散,定义是每个值与均值偏差的平方的平均值。

n是数据点总数,xᵢ是每个数据点,μ是均值。

那均值为0有什么用?

可以把数据想象成坐标系里的一团“点云”。每个值减去均值(x — μ)之后,整团云就被平移到了原点位置。数据不再飘在某个角落而是以原点为中心分布。

这对很多机器学习算法都有好处,尤其是用梯度下降的时候。数据居中之后优化过程更平衡、收敛也更快。因为特征要是一开始就偏离原点很远,训练起来会麻烦不少。

那方差为1呢?

这是为了防止某个特征"欺负"其他特征。

举个例子:年龄和薪资两个特征,年龄范围10-70,薪资范围10,000-70,000。直接喂给模型的话,模型会觉得薪资比年龄重要1000倍(数字大嘛)。但这两个特征本来是独立的,凭什么薪资就更重要?

所以标准化就是除以标准差,让所有特征的方差都变成1。这样年龄和薪资就在同一个量级上了,变化幅度差不多。年龄有个小波动,不会因为薪资数字大就被模型无视掉。

可视化效果:

标准化之前,特征1(红色,小尺度)和特征2(蓝色,大尺度)放一起,红色那条几乎看不见。标准化之后,两个特征尺度一致,都能清晰显示出来。模型终于可以公平对待它们了。

什么时候需要标准化?逻辑回归、神经网络、KNN这类用梯度下降的算法,标准化影响最大。

总结一下:

均值为0让数据居中,方差为1让特征尺度统一。两者配合,算法学得更快,也不会偏心某个特征。至于什么时候该用标准化、什么时候该用MinMaxScaler,老实说我也还在摸索。

https://avoid.overfit.cn/post/957b1b35bc1047e185dab369ae8d84ed

作者:vaishnavi

如果你是把 Dokploy 装在自己的服务器上,用了一段时间,大概率会遇到一个问题:
它要怎么升级,才不折腾?

答案其实很简单。

Dokploy 官方已经把升级流程写进了安装脚本里,不用拉代码,也不用自己停服务。一行命令就够了:

curl -sSL https://dokploy.com/install.sh | sh -s update

image.png

我自己升级时的体验是:配置没丢,服务照常起来,过程也没什么存在感。对已经在跑项目的机器来说,这点很重要。

当然,有个前提。
如果你和当前版本差得太远,或者这次升级涉及结构性改动,最好先扫一眼文档,看看有没有明确提到需要手动处理的地方。否则大多数情况下,直接跑就行。

官方对这个升级方式的说明在这里:
https://docs.dokploy.com/docs/core/manual-installation#manual-upgrade

自部署用 Dokploy,本来就是图一个省心。升级这件事,它现在确实做到了,越来越喜欢 Dokploy 了,哈哈哈。

image.png