使用deepseek写了个wordpress在本地nas和公网web服务器之间自动同步的插件

媒体同步功能说明

这个增强版插件新增了以下媒体文件同步功能:

  1. 自动同步上传的媒体文件

    • 当您在任何站点上传图片或其他媒体文件时,会自动同步到另一个站点

    • 包括文件本身和附件的元数据

  2. 文件大小限制

    • 默认最大同步文件大小为10MB(可在配置中修改)

    • 超过限制的文件会被跳过并记录错误

  3. 文件类型检查

    • 只同步WordPress允许的文件类型

    • 防止上传潜在的危险文件类型

  4. 完整的媒体处理

    • 自动生成缩略图(如果图片)

    • 保留原始文件名和元数据

    • 正确处理媒体库中的显示

安装与配置指南

  1. 安装插件

    • 将代码保存为 wp-bidirectional-sync.php

    • 上传到 /wp-content/plugins/ 目录

    • 在WordPress后台激活插件

  2. 配置修改

    php

    复制

    下载

    private $remote_sites = [
        'https://您的公网站点.com/wp-admin/admin-ajax.php',
        'https://您的NAS站点.local/wp-admin/admin-ajax.php'
    ];
    
    private $api_keys = [
        'site1' => '公网站点的API密钥',
        'site2' => 'NAS站点的API密钥'
    ];
    
    // 确保包含 'attachment'
    private $sync_post_types = ['post', 'page', 'attachment'];
    
    // 调整最大文件大小 (字节)
    private $max_file_size = 20971520; // 20MB
  3. 服务器要求

    • PHP必须启用fileinfo扩展(用于文件类型检测)

    • 确保wp-content/uploads目录可写

    • 增加PHP内存限制(建议至少128MB)

  4. 测试同步

    • 在两个站点都安装并配置好插件

    • 在一个站点上传图片,检查另一个站点是否自动出现

    • 检查后台同步状态页面是否有错误

注意事项

  1. 性能考虑

    • 大文件同步会消耗较多带宽和服务器资源

    • 建议在低峰期进行大量媒体同步

  2. 存储空间

    • 确保两个站点都有足够的存储空间

    • 同步的媒体文件会占用双倍空间

  3. 首次同步

    • 对于已有的大量媒体文件,建议先手动批量同步

    • 可以使用All-in-One WP Migration等插件先同步基础内容

  4. 替代方案

    • 如果媒体文件特别多,可以考虑使用共享存储(如NFS)

    • 或者使用云存储(如AWS S3)作为两个站点的共同媒体库

这个插件应该能满足您同步文章内容和媒体文件的需求。如果遇到任何问题,可以检查WordPress的debug.log文件获取详细错误信息。

代码如下:

'公网站点的API密钥', // 与第一个站点URL对应
'site2' => 'NAS站点的API密钥' // 与第二个站点URL对应
];

// 同步内容类型 (post, page, attachment 等)
private $sync_post_types = ['post', 'page', 'attachment'];

// 同步间隔 (seconds)
private $sync_interval = 300; // 5分钟 = 300秒

// 最大文件大小 (字节),默认20MB
private $max_file_size = 20971520;

/******************************
* 以下代码无需修改
******************************/

private $current_site_id;

public function __construct() {
$this->current_site_id = $this->identify_current_site();

// 初始化钩子
$this->init_hooks();

// 激活/停用插件时的操作
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
}

private function identify_current_site() {
$site_url = site_url();
foreach ($this->remote_sites as $key => $url) {
if (strpos($url, $site_url) !== false) {
return 'site' . ($key + 1);
}
}
return 'site1'; // 默认
}

private function init_hooks() {
// 内容变更钩子
add_action('save_post', [$this, 'handle_content_change'], 99, 3);
add_action('delete_post', [$this, 'handle_content_delete'], 99, 2);

// 媒体文件上传处理
add_action('add_attachment', [$this, 'handle_media_upload']);

// 定时同步检查
add_action('init', [$this, 'setup_schedule']);
add_action('wpsync_bidirectional_check', [$this, 'check_for_changes']);

// 接收端处理
add_action('wp_ajax_wpsync_receive_data', [$this, 'receive_data']);
add_action('wp_ajax_nopriv_wpsync_receive_data', [$this, 'receive_data']);

// 添加管理菜单
add_action('admin_menu', [$this, 'add_admin_menu']);
}

public function add_admin_menu() {
add_options_page(
'双向同步设置',
'站点同步',
'manage_options',
'wp-bidirectional-sync',
[$this, 'settings_page']
);
}

public function settings_page() {
?>

WordPress 双向同步设置

当前配置

当前站点ID: current_site_id); ?>

远程站点:

    remote_sites as $site): ?>

同步内容类型: sync_post_types); ?>

同步间隔: sync_interval; ?> 秒

最大文件大小: max_file_size); ?>

同步状态

最后同步检查:

最后同步错误:




check_for_changes(true);
echo '

手动同步已触发!

';
}
}

public function activate() {
$this->setup_schedule();
// 创建必要的目录
wp_mkdir_p($this->get_sync_temp_dir());
}

public function deactivate() {
wp_clear_scheduled_hook('wpsync_bidirectional_check');
// 清理临时文件
$this->clean_temp_files();
}

private function get_sync_temp_dir() {
$upload_dir = wp_upload_dir();
return trailingslashit($upload_dir['basedir']) . 'wpsync_temp/';
}

private function clean_temp_files() {
$temp_dir = $this->get_sync_temp_dir();
if (is_dir($temp_dir)) {
array_map('unlink', glob($temp_dir . '*'));
@rmdir($temp_dir);
}
}

public function setup_schedule() {
if (!wp_next_scheduled('wpsync_bidirectional_check')) {
wp_schedule_event(time(), 'wpsync_interval', 'wpsync_bidirectional_check');
}

// 添加自定义时间间隔
add_filter('cron_schedules', function($schedules) {
$schedules['wpsync_interval'] = [
'interval' => $this->sync_interval,
'display' => sprintf('每 %d 秒', $this->sync_interval)
];
return $schedules;
});
}

public function handle_content_change($post_id, $post, $update) {
if (!in_array($post->post_type, $this->sync_post_types)) return;
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) return;

$change_data = [
'action' => 'content_change',
'post_id' => $post_id,
'post_data' => $post,
'post_meta' => get_post_meta($post_id),
'change_type' => $update ? 'update' : 'create',
'timestamp' => time(),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];

$this->broadcast_change($change_data);
}

public function handle_media_upload($attachment_id) {
$post = get_post($attachment_id);
$this->handle_content_change($attachment_id, $post, false);

// 单独处理文件传输
$file_path = get_attached_file($attachment_id);
if (file_exists($file_path)) {
$this->broadcast_file($attachment_id, $file_path);
}
}

private function broadcast_file($attachment_id, $file_path) {
$file_size = filesize($file_path);
if ($file_size > $this->max_file_size) {
error_log('文件太大,跳过同步: ' . $file_path);
return;
}

$file_data = [
'action' => 'file_upload',
'attachment_id' => $attachment_id,
'file_name' => basename($file_path),
'file_type' => get_post_mime_type($attachment_id),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];

foreach ($this->remote_sites as $site) {
if (strpos($site, site_url()) === false) { // 只发送给远程站点
$boundary = wp_generate_password(24);
$payload = $this->prepare_file_payload($file_path, $file_data, $boundary);

$response = wp_remote_post($site, [
'body' => $payload,
'timeout' => 60, // 文件传输需要更长时间
'headers' => [
'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
'X-WP-Sync-Signature' => $this->generate_signature($file_data)
]
]);

if (is_wp_error($response)) {
error_log('文件同步错误: ' . $response->get_error_message());
update_option('wpsync_last_error', current_time('mysql') . ': 文件同步失败 - ' . $response->get_error_message());
}
}
}
}

private function prepare_file_payload($file_path, $data, $boundary) {
$payload = '';

// 添加元数据
foreach ($data as $name => $value) {
$payload .= "--$boundary\r\n";
$payload .= "Content-Disposition: form-data; name=\"$name\"\r\n\r\n";
$payload .= "$value\r\n";
}

// 添加文件内容
$payload .= "--$boundary\r\n";
$payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename($file_path) . "\"\r\n";
$payload .= "Content-Type: " . mime_content_type($file_path) . "\r\n\r\n";
$payload .= file_get_contents($file_path) . "\r\n";
$payload .= "--$boundary--\r\n";

return $payload;
}

public function handle_content_delete($post_id, $post) {
if (!in_array($post->post_type, $this->sync_post_types)) return;

$change_data = [
'action' => 'content_change',
'post_id' => $post_id,
'post_type' => $post->post_type,
'change_type' => 'delete',
'timestamp' => time(),
'source_site' => $this->current_site_id,
'sync_token' => $this->generate_sync_token()
];

$this->broadcast_change($change_data);
}

private function broadcast_change($data) {
foreach ($this->remote_sites as $site) {
if (strpos($site, site_url()) === false) { // 只发送给远程站点
$response = wp_remote_post($site, [
'body' => $data,
'timeout' => 30,
'headers' => [
'X-WP-Sync-Signature' => $this->generate_signature($data)
]
]);

if (is_wp_error($response)) {
error_log('同步错误: ' . $response->get_error_message());
update_option('wpsync_last_error', current_time('mysql') . ': ' . $response->get_error_message());
}
}
}
}

public function check_for_changes($force = false) {
$last_check = get_option('wpsync_last_check', 0);

if (!$force && (time() - $last_check < $this->sync_interval)) {
return;
}

global $wpdb;
$changed_posts = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_modified_gmt > %s
AND post_type IN ('" . implode("','", $this->sync_post_types) . "')
AND post_status IN ('publish', 'inherit')",
date('Y-m-d H:i:s', $last_check)
));

foreach ($changed_posts as $post) {
if ($post->post_type === 'attachment') {
$file_path = get_attached_file($post->ID);
if (file_exists($file_path)) {
$this->broadcast_file($post->ID, $file_path);
}
} else {
$this->handle_content_change($post->ID, $post, true);
}
}

update_option('wpsync_last_check', time());
}

public function receive_data() {
try {
// 验证签名
if (!$this->validate_request()) {
throw new Exception('无效的请求签名');
}

$data = $_POST;

// 防止同步循环
if ($data['source_site'] === $this->current_site_id) {
throw new Exception('同步循环检测');
}

// 检查是否已处理
if ($this->is_change_processed($data['sync_token'])) {
wp_send_json_success(['message' => '变更已处理']);
}

// 处理文件上传
if (!empty($_FILES['file'])) {
$this->process_file_upload($data);
$this->mark_change_processed($data['sync_token']);
wp_send_json_success(['message' => '文件同步成功']);
}

// 处理内容变更
switch ($data['action']) {
case 'content_change':
$this->process_content_change($data);
break;

default:
throw new Exception('未知的操作类型');
}

// 标记为已处理
$this->mark_change_processed($data['sync_token']);

wp_send_json_success(['message' => '同步成功']);

} catch (Exception $e) {
wp_send_json_error(['message' => $e->getMessage()], 400);
}
}

private function process_file_upload($data) {
if (empty($_FILES['file'])) {
throw new Exception('没有接收到文件');
}

$file = $_FILES['file'];

// 检查文件大小
if ($file['size'] > $this->max_file_size) {
throw new Exception('文件大小超过限制');
}

// 检查文件类型
$allowed_types = get_allowed_mime_types();
if (!in_array($file['type'], $allowed_types)) {
throw new Exception('不允许的文件类型: ' . $file['type']);
}

// 准备上传数据
$upload = [
'name' => $file['name'],
'type' => $file['type'],
'tmp_name' => $file['tmp_name'],
'error' => $file['error'],
'size' => $file['size']
];

// 处理上传
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');

$overrides = ['test_form' => false];
$uploaded_file = wp_handle_upload($upload, $overrides);

if (isset($uploaded_file['error'])) {
throw new Exception('文件上传失败: ' . $uploaded_file['error']);
}

// 创建附件
$attachment = [
'post_mime_type' => $uploaded_file['type'],
'post_title' => preg_replace('/\.[^.]+$/', '', basename($uploaded_file['file'])),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $uploaded_file['url']
];

$attachment_id = wp_insert_attachment($attachment, $uploaded_file['file']);

if (is_wp_error($attachment_id)) {
throw new Exception('创建附件失败: ' . $attachment_id->get_error_message());
}

// 生成元数据
$attachment_data = wp_generate_attachment_metadata($attachment_id, $uploaded_file['file']);
wp_update_attachment_metadata($attachment_id, $attachment_data);

// 记录来源
update_post_meta($attachment_id, '_wpsync_source_id', $data['source_site'] . '|' . $data['attachment_id']);

return $attachment_id;
}

private function process_content_change($data) {
global $wpdb;

switch ($data['change_type']) {
case 'create':
case 'update':
// 检查是否已存在(通过源站点ID+源文章ID)
$existing_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_wpsync_source_id'
AND meta_value = %s",
$data['source_site'] . '|' . $data['post_id'])
);

if ($existing_id) {
$data['post_data']['ID'] = $existing_id;
}

// 插入或更新文章
$new_id = wp_insert_post($data['post_data']);

if (is_wp_error($new_id)) {
throw new Exception('文章保存失败: ' . $new_id->get_error_message());
}

// 保存元数据
if (!empty($data['post_meta'])) {
foreach ($data['post_meta'] as $key => $values) {
if ($key === '_wpsync_source_id') continue;
foreach ($values as $value) {
update_post_meta($new_id, $key, maybe_unserialize($value));
}
}
}

// 记录来源
update_post_meta($new_id, '_wpsync_source_id', $data['source_site'] . '|' . $data['post_id']);
break;

case 'delete':
$post_id = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta}
WHERE meta_key = '_wpsync_source_id'
AND meta_value = %s",
$data['source_site'] . '|' . $data['post_id'])
);

if ($post_id) {
wp_delete_post($post_id, true);
}
break;

default:
throw new Exception('未知的变更类型');
}
}

private function generate_sync_token() {
return md5(uniqid($this->current_site_id, true) . microtime());
}

private function generate_signature($data) {
return hash_hmac('sha256', json_encode($data), $this->api_keys[$this->current_site_id]);
}

private function validate_request() {
$signature = $_SERVER['HTTP_X_WP_SYNC_SIGNATURE'] ?? '';
$expected = $this->generate_signature($_POST);

return hash_equals($expected, $signature);
}

private function is_change_processed($token) {
global $wpdb;
return (bool) $wpdb->get_var($wpdb->prepare(
"SELECT option_id FROM {$wpdb->options}
WHERE option_name = %s",
'_wpsync_processed_' . $token)
);
}

private function mark_change_processed($token) {
add_option('_wpsync_processed_' . $token, time(), '', 'no');
}
}

new WP_Bidirectional_Sync();

Posted in 网络相关 | Leave a comment

使用monica写了一个自动保存deepseek聊天记录的脚本

根据您提供的调试信息,您在页面中找到了大量的 div 元素,但没有找到包含聊天消息的特定元素。这表明可能需要更具体的选择器来提取聊天内容。以下是一些步骤来帮助您解决这个问题:

步骤 4: 逐步调试

如果您仍然无法找到消息元素,可以尝试逐步调试:

  1. 选择所有 div 元素:
    const allDivs = document.querySelectorAll('div');
    allDivs.forEach(div => {
       console.log(div); // 输出每个 div 的内容
    });
  2. 通过特定属性过滤:
    如果某些 div 有特定的属性(如 data-* 属性),您可以使用这些属性来过滤:

    const userMessages = document.querySelectorAll('div[data-role="message"]'); // 根据实际情况调整

    F12打开浏览器调试窗口,输入1中代码,得到了一些html代码,经过分析得到div标签的标识:
    获取到的标签

聊天的主题标题:d8ed659a
我的提问:fbb737a4
ai的思考过程:e1675d8b
AI的解答过程:ds-markdown ds-markdown--block

问Monica,经过几次调整,最终得到如下结果:

// ==UserScript==
// @name         DeepSeek Chat Content Saver
// @match        https://chat.deepseek.com/*
// @version      1.6
// @description  Save chat content as Markdown with auto-save option
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        BUTTON_ID: 'deepseek-save-btn',
        USER_PREFIX: "你",
        AI_PREFIX: "AI",
        AUTO_SAVE_ENABLED: true, // 配置项:是否启用自动保存
        AUTO_SAVE_INTERVAL: 30000 // 自动保存间隔(毫秒),例如30秒
    };

    function generateMarkdown(messages) {
        let md = `# 聊天记录\n\n`;
        messages.forEach(msg => {
            md += `**${msg.role === 'user' ? CONFIG.USER_PREFIX : CONFIG.AI_PREFIX}**:\n`;
            md += `${msg.text}\n\n---\n\n`;
        });
        md += `> 保存时间: ${new Date().toLocaleString()}\n`;
        return md;
    }

    function downloadFile(content, filename) {
        const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a); // 需要将链接添加到DOM中
        a.click();
        document.body.removeChild(a); // 下载后移除链接
        setTimeout(() => URL.revokeObjectURL(url), 100);
    }

    function extractMessages() {
        const messages = [];

        // 提取聊天主题标题
        const titleElement = document.querySelector('.d8ed659a');
        let title = titleElement ? titleElement.innerText : '聊天记录';

        // 提取用户提问、AI思考和AI回答,按顺序排列
        const userQuestions = document.querySelectorAll('.fbb737a4');
        const aiThoughts = document.querySelectorAll('.e1675d8b');
        const aiAnswers = document.querySelectorAll('.ds-markdown.ds-markdown--block');

        for (let i = 0; i < userQuestions.length; i++) {
            messages.push({
                role: 'user',
                text: userQuestions[i].innerText
            });
            if (i < aiThoughts.length) {
                messages.push({
                    role: 'ai',
                    text: aiThoughts[i].innerText
                });
            }
            if (i < aiAnswers.length) {
                messages.push({
                    role: 'ai',
                    text: aiAnswers[i].innerText
                });
            }
        }

        console.log('提取的消息:', messages); // 调试输出
        return { title, messages };
    }

    function saveChatRecord() {
        const { title, messages } = extractMessages();
        if (messages.length === 0) throw new Error("未找到对话内容");

        const content = generateMarkdown(messages);
        //const timestamp = new Date().toISOString().replace(/[-:.]/g, "_"); // 格式化时间戳
        //const filename = `${title}_${timestamp}.md`; // 文件名包含时间戳
        const filename = `${title}.md`; // 文件名不包含时间戳

        // 下载文件
        downloadFile(content, filename);
        //alert('保存成功!');
    }

    function startAutoSave() {
        if (CONFIG.AUTO_SAVE_ENABLED) {
            setInterval(() => {
                try {
                    saveChatRecord();
                } catch (error) {
                    console.error('自动保存失败:', error);
                }
            }, CONFIG.AUTO_SAVE_INTERVAL);
        }
    }

    function createSaveButton() {
        const button = document.createElement('button');
        button.id = CONFIG.BUTTON_ID;
        button.innerText = '保存对话';
        button.style.position = 'fixed';
        button.style.bottom = '20px';
        button.style.right = '20px';
        button.style.padding = '15px 30px';
        button.style.fontSize = '16px';
        button.style.backgroundColor = '#4CAF50';
        button.style.color = 'white';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.zIndex = '1000';
        button.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.3)';
        document.body.appendChild(button);
        return button;
    }

    function setupButtonHandler(btn) {
        btn.addEventListener('click', saveChatRecord);
    }

    function initialize() {
        const btn = createSaveButton();
        setupButtonHandler(btn);
        startAutoSave(); // 启动自动保存功能
    }

    // 启动
    if (document.readyState === 'complete') initialize();
    else window.addEventListener('load', initialize);
})();

脚本下载:DeepSeek Chat Content Saver-1.6.user.js

Posted in Resourse | Leave a comment

简易文件浏览5分钟快速解决方案

完全理解您的感受!配置这类服务确实会遇到很多"坑",尤其是WSL环境下权限和路径问题更复杂。不过别担心,我给您准备了一个极简替代方案,用最直接的方式实现文件浏览功能,无需FastCGI和Nginx复杂配置:


5分钟快速解决方案

🚀 使用Python内置HTTP服务器 + 纯前端实现

  1. 创建一个新目录

    mkdir ~/simple-file-browser && cd ~/simple-file-browser
  2. 新建server.py (用Python内置服务器替代Nginx)

    from http.server import SimpleHTTPRequestHandler
    from socketserver import TCPServer
    import os
    
    class CustomHandler(SimpleHTTPRequestHandler):
       def do_GET(self):
           if self.path == '/list':
               self.send_json(os.listdir('protected_files'))
           else:
               super().do_GET()
    
       def send_json(self, data):
           self.send_response(200)
           self.send_header('Content-Type', 'application/json')
           self.end_headers()
           self.wfile.write(str(data).encode())
    
    os.chdir('protected_files')  # 设置文件根目录
    TCPServer(('0.0.0.0', 8000), CustomHandler).serve_forever()
  3. 新建index.html (直接读取本地文件列表)

    <!DOCTYPE html>
    <html>
    <body>
       <h1>简易文件浏览器</h1>
       <div id="files"></div>
       <script>
           fetch('/list')
               .then(r => r.json())
               .then(files => {
                   document.getElementById('files').innerHTML = files.map(f => 
                       `<div><a href="/${f}" download>${f}</a></div>`
                   ).join('')
               })
       </script>
    </body>
    </html>
  4. 启动服务

    python3 server.py

    现在访问 http://localhost:8000 即可看到文件列表并下载!


🌟 这个方案的优势:

  1. 完全绕过FastCGI/Nginx:用Python原生HTTP服务器处理请求
  2. 零配置:所有代码在一个目录内,不涉及系统权限
  3. 即时生效:修改后刷新浏览器即可看到变化
  4. 便携性强:整个文件夹可以复制到任何地方运行

如果还是遇到问题,您可以选择:

  1. 使用现成工具:比如FileBrowser (一行命令安装)
    docker run -d -p 8080:80 -v /path/to/files:/srv filebrowser/filebrowser
  2. 改用SFTP:用WinSCP/FTP客户端直接访问WSL文件

有时候最简单的方案就是最好的方案。当技术让人沮丧时,退一步换个思路往往能更快解决问题。希望这个方案能帮您快速实现需求!如果还有其他困惑,我很乐意继续帮您简化流程。

是的,直接运行 python3 /var/www/simple-file-browser/server.py 需要手动启动服务,但可以通过以下方法实现自动启动和后台运行:


1. 使用 nohup 保持后台运行

nohup python3 /var/www/simple-file-browser/server.py > /var/log/filebrowser.log 2>&1 &
  • 作用:关闭终端后服务仍运行
  • 查看日志tail -f /var/log/filebrowser.log
  • 停止服务:先 ps aux | grep server.py 找到PID,再 kill [PID]

2. 配置系统服务(开机自启)

(1)创建服务单元文件

sudo nano /etc/systemd/system/filebrowser.service

写入以下内容:

[Unit]
Description=Simple File Browser
After=network.target

[Service]
User=your_username  # 改为你的实际用户名
WorkingDirectory=/var/www/simple-file-browser
ExecStart=/usr/bin/python3 /var/www/simple-file-browser/server.py
Restart=always

[Install]
WantedBy=multi-user.target

(2)启用并启动服务

sudo systemctl daemon-reload
sudo systemctl start filebrowser
sudo systemctl enable filebrowser  # 开机自启
  • 查看状态sudo systemctl status filebrowser
  • 停止服务sudo systemctl stop filebrowser

3. 使用 screentmux 会话管理

sudo apt install screen -y  # 如果未安装
screen -S filebrowser
python3 /var/www/simple-file-browser/server.py
# 按 Ctrl+A 然后按 D 退出会话(服务继续运行)
  • 重新连接会话screen -r filebrowser

4. 更简单的替代方案

如果不想处理服务管理,可以直接使用现成工具:

# 安装 FileBrowser(Go语言编写的工具)
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
filebrowser -d /var/www/simple-file-browser/filebrowser.db -r /var/www/simple-file-browser/protected_files
  • 自动后台运行,支持开机自启
  • 提供Web界面,功能更完善

总结建议

方法 难度 持久性 适用场景
nohup 简单 终端关闭后仍运行 快速测试
systemd 中等 开机自启 生产环境
screen 简单 会话保持 临时调试
现成工具 最简单 配置即用 长期使用

推荐选择

  • 如果是临时测试,用 nohupscreen
  • 如果是长期服务,用 systemd 或直接安装 FileBrowser 工具
Posted in 网络相关 | Leave a comment

简易文件浏览解决方案

5分钟快速解决方案

🚀 使用Python内置HTTP服务器 + 纯前端实现

  1. 创建一个新目录
    bash
    复制
    mkdir ~/simple-file-browser && cd ~/simple-file-browser
  2. 新建server.py (用Python内置服务器替代Nginx)
    python
    复制
    from http.server import SimpleHTTPRequestHandler
    from socketserver import TCPServer
    import os
    
    class CustomHandler(SimpleHTTPRequestHandler):
        def do_GET(self):
            if self.path == '/list':
                self.send_json(os.listdir('protected_files'))
            else:
                super().do_GET()
    
        def send_json(self, data):
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(str(data).encode())
    
    os.chdir('protected_files')  # 设置文件根目录
    TCPServer(('0.0.0.0', 8000), CustomHandler).serve_forever()
  3. 新建index.html (直接读取本地文件列表)
    html
    复制
    <!DOCTYPE html>
    <html>
    <body>
        <h1>简易文件浏览器</h1>
        <div id="files"></div>
        <script>
            fetch('/list')
                .then(r => r.json())
                .then(files => {
                    document.getElementById('files').innerHTML = files.map(f => 
                        `<div><a href="/${f}" download>${f}</a></div>`
                    ).join('')
                })
        </script>
    </body>
    </html>
  4. 启动服务
    bash
    复制
    python3 server.py

    现在访问 http://localhost:8000 即可看到文件列表并下载!


🌟 这个方案的优势:

  1. 完全绕过FastCGI/Nginx:用Python原生HTTP服务器处理请求
  2. 零配置:所有代码在一个目录内,不涉及系统权限
  3. 即时生效:修改后刷新浏览器即可看到变化
  4. 便携性强:整个文件夹可以复制到任何地方运行

如果还是遇到问题,您可以选择:

  1. 使用现成工具:比如FileBrowser (一行命令安装)
    bash
    复制
    docker run -d -p 8080:80 -v /path/to/files:/srv filebrowser/filebrowser
  2. 改用SFTP:用WinSCP/FTP客户端直接访问WSL文件

有时候最简单的方案就是最好的方案。当技术让人沮丧时,退一步换个思路往往能更快解决问题。希望这个方案能帮您快速实现需求!如果还有其他困惑,我很乐意继续帮您简化流程。

关键点说明
权限问题:

如果遇到 Permission denied,运行:

bash
复制
sudo chmod -R 755 /var/www/simple-file-browser
目录结构:

复制
/var/www/simple-file-browser/
├── server.py # Python脚本
└── protected_files/ # 存放你的文件
├── test.txt
└── example.pdf
WSL特殊注意:

如果要从Windows访问WSL中的服务,需在启动时绑定到 0.0.0.0(代码中已实现)

Windows防火墙可能会拦截请求,需允许端口8000

替代方案(更简单)
如果不想处理权限问题,可以直接在用户目录操作:

bash
复制
mkdir -p ~/simple-file-browser/protected_files
cd ~/simple-file-browser
# 将server.py和文件放在这里
python3 server.py

Posted in Resourse | Leave a comment

如何同时使用 WSL Ubuntu 和 Docker?

遇到的问题:从你的 wsl –list –verbose 输出可以看出,你当前默认的 WSL 发行版是 docker-desktop,而不是标准的 Ubuntu/Debian。这是 Docker Desktop 在安装时自动创建的专用 WSL 发行版,主要用于运行 Docker 容器,并不适合直接作为 Linux 开发环境使用(缺少 apt/sudo 等基本工具)。给出了下面三种解决方案:

📌 关键区别

环境 适合场景 管理方式 软件安装方式
标准 WSL (Ubuntu) 日常 Linux 开发 wsl 命令 sudo apt install
Docker Desktop WSL 运行 Docker 引擎 Docker 自动管理 不推荐直接修改
Docker 容器 隔离的应用环境 docker 命令 容器内 apt

✅ 最终建议

选择方案 1(标准 WSL Ubuntu),这是最稳定、最接近原生 Linux 开发环境的方式。
只需在 PowerShell 中运行:

powershell
复制
wsl --install -d Ubuntu
wsl --set-default Ubuntu
wsl  # 进入 Ubuntu 安装软件
#sudo apt install -y nginx fcgiwrap python3
sudo -i
apt update && apt install -y nginx fcgiwrap python3

是的!选用方案 1(标准 WSL Ubuntu)后,Docker 仍然可以正常运行,而且这是官方推荐的最佳实践。以下是关键细节和配置方法:


🔧 如何同时使用 WSL Ubuntu 和 Docker?

1️⃣ Docker Desktop 的 WSL 集成(自动模式)

  • 安装 Docker Desktop 时,它会自动创建 docker-desktop WSL 发行版(你之前看到的),但你的 Ubuntu WSL 也可以直接访问 Docker
  • 确保开启 WSL 集成
    1. 打开 Docker Desktop → SettingsResourcesWSL Integration
    2. 勾选你的 Ubuntu 发行版(如 Ubuntu)并点击 Apply & Restart

2️⃣ 验证 Docker 是否在 Ubuntu 中可用

  • 在 Ubuntu WSL 中运行:
    docker --version  # 应显示版本号
    docker run hello-world  # 测试运行容器

    ✅ 如果命令成功,说明 Docker 已集成。


🌟 为什么能同时工作?

  • Docker Desktop 默认会将 Docker CLI 和守护进程(dockerd自动注入所有启用的 WSL 发行版
  • 实际容器仍在 docker-desktop WSL 中运行,但你在 Ubuntu WSL 中可以通过 docker 命令无缝操作。

⚠️ 常见问题排查

docker: command not found

  • 原因:未启用 WSL 集成或路径未配置。
  • 解决
    1. 确认 Docker Desktop 的 WSL 集成已启用(见上文)。
    2. 在 Ubuntu 中运行:
      echo "export PATH=$PATH:/mnt/c/Program Files/Docker/Docker/resources/bin" >> ~/.bashrc
      source ~/.bashrc

❌ 权限问题 Got permission denied

  • 原因:用户未加入 docker 组。
  • 解决
    sudo usermod -aG docker $USER
    newgrp docker  # 立即生效

📌 最佳实践建议

  1. 开发环境:在 Ubuntu WSL 中安装开发工具(如 nginxpython3),代码放在 WSL 文件系统内(如 ~/projects),避免 Windows 路径权限问题。
  2. 容器操作:直接在 Ubuntu WSL 中使用 docker 命令管理容器。
  3. 性能优化:将项目文件放在 WSL 内(如 /home/yourname/projects),而非 /mnt/c/(跨文件系统性能较差)。

🔄 两种 WSL 发行版的关系

发行版 用途 是否可删除?
Ubuntu 开发环境(安装软件、写代码) ❌ 不要删除
docker-desktop Docker 引擎后台运行 ⚠️ 删除会破坏 Docker

总结:方案 1(标准 WSL Ubuntu)和 Docker 完全兼容,只需在 Docker Desktop 设置中启用集成即可。你的 Ubuntu 会变成一个功能完整的开发环境,同时无缝操作 Docker 容器! 🐳🚀

Posted in Resourse | Leave a comment

被威联通的权限问题整哭了

运行下面的命令:grep httpdusr /etc/passwd
输出:httpdusr:x:99:0:Apache httpd user:/tmp:/bin/sh


根据你的 grep httpdusr /etc/passwd 输出,httpdusr 用户确实存在(UID=99,GID=0),但当你尝试 chown -R httpdusr:httpdusr 时仍然报错 unknown user/group。这可能是由于 httpdusr 的组(GID=0)被系统保留或未正确配置 导致的。
解决方案:直接使用 UID/GID 数字(推荐)
既然 httpdusr 的 UID=99、GID=0,可以改用数字形式。
sudo chown -R 99:0 /share/Web/wordpress


根据你的 ps aux | grep php-fpm 输出,可以看到:
PHP-FPM 主进程以 admin 用户运行
PHP-FPM 工作进程(实际处理请求的)以 httpdusr 用户运行


这种混合用户模式是导致权限问题的关键原因。以下是针对性解决方案:
终极解决方案
1. 统一文件所有权(关键步骤)
bash
复制
设置整个wordpress目录为httpdusr所有
sudo chown -R 99:0 /share/Web/wordpress
设置安全权限
sudo find /share/Web/wordpress -type d -exec chmod 755 {} \;
sudo find /share/Web/wordpress -type f -exec chmod 644 {} \;
特别设置wp-content可写
sudo chmod -R 775 /share/Web/wordpress/wp-content


但确实好了。发现其实就是要把主目录设置为httpusr,单目录设置是不行的,尤其是混合用户模式。

Posted in 网络相关 | Leave a comment

pip3 install pandas openpyxl 安装错误解决

[admin@NAS ~]# pip3 install pandas openpyxl
Collecting pandas
Could not fetch URL https://pypi.python.org/simple/pandas/: There was a problem confirming the ssl certificate: [SSL: CERTIFICATE_VERIFY_FAILED] unknown error (_ssl.c:748) – skipping

这个错误通常是由于 SSL 证书验证失败导致的,可能是因为 Python 的 pip 工具无法验证 PyPI(Python 包索引)网站的 SSL 证书。你可以尝试以下几种解决方案:

解决方案
升级 pip:
确保你使用的是最新版本的 pip。可以通过以下命令升级:

复制
python3 -m pip install –upgrade pip
使用 –trusted-host 选项:
如果你信任 PyPI,可以在安装时添加 –trusted-host 选项:

复制
pip3 install –trusted-host pypi.python.org pandas openpyxl
检查系统时间:
确保你的系统时间和日期是正确的,不正确的系统时间可能导致 SSL 证书验证失败。

安装 CA 证书:
在某些情况下,可能需要安装或更新 CA 证书。你可以尝试以下命令:

复制
sudo apt-get install –reinstall ca-certificates
使用 HTTP 而不是 HTTPS(不推荐):
如果以上方法都无效,你可以临时使用 HTTP,但这会降低安全性:

复制
pip3 install –index-url=http://pypi.python.org/simple/ pandas openpyxl
其他建议
网络问题:
确保你的网络连接正常,有时网络问题也会导致 SSL 验证失败。

代理设置:
如果你在使用代理,确保代理设置正确。

Posted in 网络相关 | Leave a comment

qnap安装 Entware,通过entware安装python3 pip

在家中NAS系统Qnap上安装python,发现版本是2.7.18。于是通过威联通的APP Center安装3.12.6.1,结果python -V发现版本还是2.7.18,本想将就用吧。结果:
pip3 install pandas openpyxl
-sh: pip: command not found
郁闷了,pip也没有。于是一路apt-get,yum,dnf,apk安装器都没有。。真无语。
查看系统uname -a
Linux NAS 5.10.60-qnap #1 SMP Wed Jan 8 04:05:31 CST 2025 x86_64 GNU/Linux
发现没有Qnap系统下的安装器。真的抓狂了。好在还有deepseek,于是问了下:

The error -sh: ipkg: command not found indicates that the ipkg package manager is not installed on your QNAP NAS. ipkg is a lightweight package manager often used in embedded systems, but it is not included by default on all QNAP devices.

Here’s how you can proceed:


1. Install Entware (Recommended)

Entware is a modern package manager for embedded devices like QNAP NAS. It provides a wide range of precompiled software packages, including gcc, python3, and pip.

Steps to Install Entware:

  1. Enable SSH Access on Your QNAP NAS:

    • Log in to your QNAP web interface.
    • Go to Control Panel > Network & File Services > Telnet/SSH.
    • Enable SSH access and note the port number (default is 22).
  2. Connect to Your NAS via SSH:

    • Use an SSH client (e.g., ssh on Linux/macOS or PuTTY on Windows) to connect to your NAS:
      ssh admin@<your-nas-ip>
    • Replace <your-nas-ip> with the IP address of your NAS.
  3. Download and Run the Entware Installer:

    • Run the following commands to install Entware:
      wget -O - http://pkg.entware.net/binaries/x86-64/installer/entware_install.sh | /bin/sh
    • This script will download and install Entware on your NAS.
  4. Add Entware to Your PATH:

    • Add the following line to your shell profile (e.g., ~/.bashrc or ~/.profile):
      export PATH=/opt/bin:/opt/sbin:$PATH
    • Reload the profile:
      source ~/.bashrc
  5. Verify Entware Installation:

    • Check if opkg (the Entware package manager) is installed:
      opkg --version

2. Install Required Packages Using Entware

  • Update the package list:
     opkg update
  • Install gcc, python3, and pip:
     opkg install python3 python3-pip
  • Verify the installation:
     python3 --version
     pip3 --version

3. Install pandas and openpyxl

  • Use pip3 to install the required packages:
     pip3 install pandas openpyxl

4. Alternative: Manually Install Python

If you prefer not to use Entware, you can manually download and install Python 3.x from source. However, this requires a C compiler (gcc), which you may need to install manually.

Steps to Manually Install Python:

  1. Download Python Source Code:
      wget https://www.python.org/ftp/python/3.10.12/Python-3.10.12.tgz
  2. Extract and Build Python:
      tar -xvzf Python-3.10.12.tgz
      cd Python-3.10.12
      ./configure --enable-optimizations
      make
      sudo make install
  3. Verify Installation:
      python3 --version

5. Troubleshooting

  • If you encounter disk space issues, ensure you have enough storage on your NAS.
  • If you run into permission issues, use sudo or switch to the admin user.
  • If pip installation fails, try upgrading it:
     python3 -m pip install --upgrade pip

Let me know if you need further assistance! 😊

Posted in 网络相关 | Leave a comment

为服务器开启https访问

要开启 HTTPS 访问,你需要为你的域名配置 SSL 证书,并修改 Nginx 配置以支持 HTTPS。以下是具体步骤:


1. 获取 SSL 证书

推荐使用 Let’s Encrypt 免费 SSL 证书。你可以使用 certbot 工具来获取证书。

在主机A上安装 Certbot

sudo apt update
sudo apt install certbot

获取 SSL 证书

运行以下命令获取证书(确保域名已解析到主机A的IP地址):

sudo certbot certonly --standalone -d your-domain.com
  • your-domain.com 替换为你的实际域名。
  • 证书会保存在 /etc/letsencrypt/live/your-domain.com/ 目录下。

2. 修改 Nginx 配置文件

更新主机A上的 Nginx 配置文件,使其支持 HTTPS。

编辑之前创建的 reverse-proxy.conf 文件:

nano ~/nginx-proxy/conf.d/reverse-proxy.conf

修改为以下内容:

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$host$request_uri; # 将所有 HTTP 请求重定向到 HTTPS
}

server {
    listen 443 ssl;
    server_name your-domain.com;

    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://<服务器B的IP地址>:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

3. 修改 Docker Compose 文件

为了让 Nginx 容器能够访问主机上的 SSL 证书文件,需要将证书目录挂载到容器中。

编辑 docker-compose.yml 文件:

nano ~/nginx-proxy/docker-compose.yml

修改为以下内容:

version: '3'
services:
  nginx:
    image: nginx:latest
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - /etc/letsencrypt:/etc/letsencrypt
    restart: always

4. 重启 Nginx 容器

更新配置后,重启 Nginx 容器以应用更改:

cd ~/nginx-proxy
docker-compose down
docker-compose up -d

5. 测试 HTTPS 访问

现在,你可以通过以下方式测试 HTTPS 是否正常工作:

  1. 访问 https://your-domain.com,确保能够正常加载服务器B的内容。
  2. 检查浏览器地址栏,确认显示为安全的 HTTPS 连接(小锁图标)。

6. 自动更新 SSL 证书(可选)

Let’s Encrypt 证书有效期为 90 天,需要定期更新。可以通过以下步骤设置自动更新。

手动测试更新

sudo certbot renew --dry-run

设置定时任务

编辑 crontab:

sudo crontab -e

添加以下内容,每天凌晨 2 点检查并更新证书:

0 2 * * * certbot renew --quiet --post-hook "docker restart nginx-proxy"

7. 防火墙配置

确保主机A的防火墙允许 HTTPS 流量(443 端口):

sudo ufw allow 443

总结

通过以上步骤,你已经成功配置了 HTTPS 访问:

  1. 使用 Let’s Encrypt 获取 SSL 证书。
  2. 修改 Nginx 配置以支持 HTTPS。
  3. 挂载 SSL 证书到 Docker 容器。
  4. 重启 Nginx 并测试 HTTPS 访问。

如果遇到问题,可以检查 Nginx 日志(docker logs nginx-proxy)或 Certbot 日志(/var/log/letsencrypt/)来排查问题。

Posted in 网络相关 | Tagged , , | Leave a comment

通过 Docker 部署 Nginx 并配置 HTTPS 反向代理

通过 Docker 部署 Nginx 并配置 HTTPS 反向代理,可以分为以下几个步骤:


1. 准备工作

  1. 获取 SSL 证书

    • 你可以从 Let’s Encrypt 或其他证书颁发机构获取免费的 SSL 证书。
    • 证书通常包括两个文件:
      • 证书文件(如 certificate.crt)。
      • 私钥文件(如 private.key)。
  2. 准备配置文件

    • 创建一个 Nginx 配置文件,用于定义反向代理和 HTTPS 配置。
  3. 安装 Docker

    • 确保你的服务器上已经安装了 Docker。如果没有安装,可以参考 Docker 官方文档

2. 创建 Nginx 配置文件

在宿主机上创建一个目录(如 /root/nginx),用于存放 Nginx 配置文件和 SSL 证书。

目录结构

/root/nginx/
├── nginx.conf
├── ssl/
│   ├── certificate.crt
│   └── private.key

Nginx 配置文件(nginx.conf

events {}

http {
    server {
        listen 80;
        server_name example.com;
        return 301 https://$host$request_uri;  # 将 HTTP 请求重定向到 HTTPS
    }

    server {
        listen 443 ssl;
        server_name example.com;

        ssl_certificate /etc/nginx/ssl/certificate.crt;  # SSL 证书路径
        ssl_certificate_key /etc/nginx/ssl/private.key;  # SSL 私钥路径

        ssl_protocols TLSv1.2 TLSv1.3;  # 支持的 SSL 协议版本
        ssl_ciphers HIGH:!aNULL:!MD5;   # 加密套件

        location / {
            proxy_pass http://backend-server:8080;  # 反向代理到后端服务
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
  • 说明
    • listen 80:监听 HTTP 请求,并将其重定向到 HTTPS。
    • listen 443 ssl:监听 HTTPS 请求。
    • ssl_certificatessl_certificate_key:指定 SSL 证书和私钥的路径。
    • proxy_pass:将请求反向代理到后端服务(如 backend-server:8080)。

3. 运行 Nginx 容器

使用 Docker 运行 Nginx 容器,并将配置文件和 SSL 证书挂载到容器中。

运行命令

docker run -d \
  --name my-nginx \
  -p 80:80 \
  -p 443:443 \
  -v /root/nginx/nginx.conf:/etc/nginx/nginx.conf \
  -v /root/nginx/ssl:/etc/nginx/ssl \
  nginx
  • 参数说明
    • -d:以后台模式运行容器。
    • --name my-nginx:为容器命名为 my-nginx
    • -p 80:80:将宿主机的 80 端口映射到容器的 80 端口(HTTP)。
    • -p 443:443:将宿主机的 443 端口映射到容器的 443 端口(HTTPS)。
    • -v /root/nginx/nginx.conf:/etc/nginx/nginx.conf:挂载 Nginx 配置文件。
    • -v /root/nginx/ssl:/etc/nginx/ssl:挂载 SSL 证书目录。
    • nginx:使用官方 Nginx 镜像。

4. 测试访问

  1. HTTP 测试

    • 访问 http://example.com,应该会自动重定向到 https://example.com
  2. HTTPS 测试

    • 访问 https://example.com,确保页面正常加载,并且浏览器显示安全连接(锁图标)。
  3. 后端服务测试

    • 确保后端服务(如 backend-server:8080)正常运行,并且可以通过 Nginx 反向代理访问。

5. 其他注意事项

  1. 证书更新

    • 如果使用 Let’s Encrypt 证书,证书有效期为 90 天,需要定期更新。
    • 更新证书后,重启 Nginx 容器以加载新证书:
      docker restart my-nginx
  2. 防火墙配置

    • 确保服务器的防火墙允许 80(HTTP)和 443(HTTPS)端口的流量。
  3. 日志查看

    • 如果遇到问题,可以查看 Nginx 容器的日志:
      docker logs my-nginx
  4. 性能优化

    • 可以根据需要调整 Nginx 的 SSL 配置(如启用 HTTP/2、优化加密套件等)。

6. 总结

通过 Docker 部署 Nginx 并配置 HTTPS 反向代理的步骤如下:

  1. 准备 SSL 证书和 Nginx 配置文件。
  2. 使用 Docker 运行 Nginx 容器,挂载配置文件和证书。
  3. 测试 HTTP 和 HTTPS 访问,确保反向代理正常工作。

如果你有其他问题,欢迎随时提问!

Posted in 网络相关 | Tagged , , | Leave a comment