作为知名的 菩萨Cloudflare深受大家的喜爱,其中 worker 也让人摸索出了可以科学上网的方法

准备工作

  • 一个顶级域名
  • 一个Cloudflare账号

使用方法

添加站点,把域名解析在cloudflare

在worker中创建Worker

填写名称,点击部署

部署成功,点击编辑代码

填入以下代码,
注意,修改userID 代码第七行
只需在Windows系统中 按下 "Win + R", 键入

Powershell -NoExit -Command "[guid]::NewGuid()"

即可生成userID

// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->.
// @ts-ignore
import { connect } from 'cloudflare:sockets';

// How to generate your own UUID:
// [Windows] Press "Win + R", input cmd and run:  Powershell -NoExit -Command "[guid]::NewGuid()"
let userID = '6f08a7a9-bfde-4df6-920d-877a08d63f32';

let proxyIP = '103.200.112.108';


if (!isValidUUID(userID)) {
    throw new Error('uuid is not valid');
}

export default {
    /**
     * @param {import("@cloudflare/workers-types").Request} request
     * @param {{UUID: string, PROXYIP: string}} env
     * @param {import("@cloudflare/workers-types").ExecutionContext} ctx
     * @returns {Promise<Response>}
     */
    async fetch(request, env, ctx) {
        try {
            userID = env.UUID || userID;
            proxyIP = env.PROXYIP || proxyIP;
            const upgradeHeader = request.headers.get('Upgrade');
            if (!upgradeHeader || upgradeHeader !== 'websocket') {
                const url = new URL(request.url);
                switch (url.pathname) {
                    case '/':
                        return new Response(JSON.stringify(request.cf), { status: 200 });
                    case `/${userID}`: {
                        const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));
                        return new Response(`${vlessConfig}`, {
                            status: 200,
                            headers: {
                                "Content-Type": "text/plain;charset=utf-8",
                            }
                        });
                    }
                    default:
                        return new Response('Not found', { status: 404 });
                }
            } else {
                return await vlessOverWSHandler(request);
            }
        } catch (err) {
            /** @type {Error} */ let e = err;
            return new Response(e.toString());
        }
    },
};
/**
 * 
 * @param {import("@cloudflare/workers-types").Request} request
 */
async function vlessOverWSHandler(request) {

    /** @type {import("@cloudflare/workers-types").WebSocket[]} */
    // @ts-ignore
    const webSocketPair = new WebSocketPair();
    const [client, webSocket] = Object.values(webSocketPair);

    webSocket.accept();

    let address = '';
    let portWithRandomLog = '';
    const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
        console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '');
    };
    const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';

    const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);

    /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
    let remoteSocketWapper = {
        value: null,
    };
    let udpStreamWrite = null;
    let isDns = false;

    // ws --> remote
    readableWebSocketStream.pipeTo(new WritableStream({
        async write(chunk, controller) {
            if (isDns && udpStreamWrite) {
                return udpStreamWrite(chunk);
            }
            if (remoteSocketWapper.value) {
                const writer = remoteSocketWapper.value.writable.getWriter()
                await writer.write(chunk);
                writer.releaseLock();
                return;
            }

            const {
                hasError,
                message,
                portRemote = 443,
                addressRemote = '',
                rawDataIndex,
                vlessVersion = new Uint8Array([0, 0]),
                isUDP,
            } = processVlessHeader(chunk, userID);
            address = addressRemote;
            portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '
                } `;
            if (hasError) {
                // controller.error(message);
                throw new Error(message); // cf seems has bug, controller.error will not end stream
                // webSocket.close(1000, message);
                return;
            }
            // if UDP but port not DNS port, close it
            if (isUDP) {
                if (portRemote === 53) {
                    isDns = true;
                } else {
                    // controller.error('UDP proxy only enable for DNS which is port 53');
                    throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream
                    return;
                }
            }
            // ["version", "附加信息长度 N"]
            const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);
            const rawClientData = chunk.slice(rawDataIndex);

            // TODO: support udp here when cf runtime has udp support
            if (isDns) {
                const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);
                udpStreamWrite = write;
                udpStreamWrite(rawClientData);
                return;
            }
            handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);
        },
        close() {
            log(`readableWebSocketStream is close`);
        },
        abort(reason) {
            log(`readableWebSocketStream is abort`, JSON.stringify(reason));
        },
    })).catch((err) => {
        log('readableWebSocketStream pipeTo error', err);
    });

    return new Response(null, {
        status: 101,
        // @ts-ignore
        webSocket: client,
    });
}

/**
 * Handles outbound TCP connections.
 *
 * @param {any} remoteSocket 
 * @param {string} addressRemote The remote address to connect to.
 * @param {number} portRemote The remote port to connect to.
 * @param {Uint8Array} rawClientData The raw client data to write.
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
 * @param {Uint8Array} vlessResponseHeader The VLESS response header.
 * @param {function} log The logging function.
 * @returns {Promise<void>} The remote socket.
 */
async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {
    async function connectAndWrite(address, port) {
        /** @type {import("@cloudflare/workers-types").Socket} */
        const tcpSocket = connect({
            hostname: address,
            port: port,
        });
        remoteSocket.value = tcpSocket;
        log(`connected to ${address}:${port}`);
        const writer = tcpSocket.writable.getWriter();
        await writer.write(rawClientData); // first write, nomal is tls client hello
        writer.releaseLock();
        return tcpSocket;
    }

    // if the cf connect tcp socket have no incoming data, we retry to redirect ip
    async function retry() {
        const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote)
        // no matter retry success or not, close websocket
        tcpSocket.closed.catch(error => {
            console.log('retry tcpSocket closed error', error);
        }).finally(() => {
            safeCloseWebSocket(webSocket);
        })
        remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);
    }

    const tcpSocket = await connectAndWrite(addressRemote, portRemote);

    // when remoteSocket is ready, pass to websocket
    // remote--> ws
    remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
}

/**
 * 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer
 * @param {string} earlyDataHeader for ws 0rtt
 * @param {(info: string)=> void} log for ws 0rtt
 */
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
    let readableStreamCancel = false;
    const stream = new ReadableStream({
        start(controller) {
            webSocketServer.addEventListener('message', (event) => {
                if (readableStreamCancel) {
                    return;
                }
                const message = event.data;
                controller.enqueue(message);
            });

            // The event means that the client closed the client -> server stream.
            // However, the server -> client stream is still open until you call close() on the server side.
            // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
            webSocketServer.addEventListener('close', () => {
                // client send close, need close server
                // if stream is cancel, skip controller.close
                safeCloseWebSocket(webSocketServer);
                if (readableStreamCancel) {
                    return;
                }
                controller.close();
            }
            );
            webSocketServer.addEventListener('error', (err) => {
                log('webSocketServer has error');
                controller.error(err);
            }
            );
            // for ws 0rtt
            const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
            if (error) {
                controller.error(error);
            } else if (earlyData) {
                controller.enqueue(earlyData);
            }
        },

        pull(controller) {
            // if ws can stop read if stream is full, we can implement backpressure
            // https://streams.spec.whatwg.org/#example-rs-push-backpressure
        },
        cancel(reason) {
            // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
            // 2. if readableStream is cancel, all controller.close/enqueue need skip,
            // 3. but from testing controller.error still work even if readableStream is cancel
            if (readableStreamCancel) {
                return;
            }
            log(`ReadableStream was canceled, due to ${reason}`)
            readableStreamCancel = true;
            safeCloseWebSocket(webSocketServer);
        }
    });

    return stream;

}

// https://xtls.github.io/development/protocols/vless.html
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw

/**
 * 
 * @param { ArrayBuffer} vlessBuffer 
 * @param {string} userID 
 * @returns 
 */
function processVlessHeader(
    vlessBuffer,
    userID
) {
    if (vlessBuffer.byteLength < 24) {
        return {
            hasError: true,
            message: 'invalid data',
        };
    }
    const version = new Uint8Array(vlessBuffer.slice(0, 1));
    let isValidUser = false;
    let isUDP = false;
    if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {
        isValidUser = true;
    }
    if (!isValidUser) {
        return {
            hasError: true,
            message: 'invalid user',
        };
    }

    const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];
    //skip opt for now

    const command = new Uint8Array(
        vlessBuffer.slice(18 + optLength, 18 + optLength + 1)
    )[0];

    // 0x01 TCP
    // 0x02 UDP
    // 0x03 MUX
    if (command === 1) {
    } else if (command === 2) {
        isUDP = true;
    } else {
        return {
            hasError: true,
            message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
        };
    }
    const portIndex = 18 + optLength + 1;
    const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);
    // port is big-Endian in raw data etc 80 == 0x005d
    const portRemote = new DataView(portBuffer).getUint16(0);

    let addressIndex = portIndex + 2;
    const addressBuffer = new Uint8Array(
        vlessBuffer.slice(addressIndex, addressIndex + 1)
    );

    // 1--> ipv4  addressLength =4
    // 2--> domain name addressLength=addressBuffer[1]
    // 3--> ipv6  addressLength =16
    const addressType = addressBuffer[0];
    let addressLength = 0;
    let addressValueIndex = addressIndex + 1;
    let addressValue = '';
    switch (addressType) {
        case 1:
            addressLength = 4;
            addressValue = new Uint8Array(
                vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
            ).join('.');
            break;
        case 2:
            addressLength = new Uint8Array(
                vlessBuffer.slice(addressValueIndex, addressValueIndex + 1)
            )[0];
            addressValueIndex += 1;
            addressValue = new TextDecoder().decode(
                vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
            );
            break;
        case 3:
            addressLength = 16;
            const dataView = new DataView(
                vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
            );
            // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
            const ipv6 = [];
            for (let i = 0; i < 8; i++) {
                ipv6.push(dataView.getUint16(i * 2).toString(16));
            }
            addressValue = ipv6.join(':');
            // seems no need add [] for ipv6
            break;
        default:
            return {
                hasError: true,
                message: `invild  addressType is ${addressType}`,
            };
    }
    if (!addressValue) {
        return {
            hasError: true,
            message: `addressValue is empty, addressType is ${addressType}`,
        };
    }

    return {
        hasError: false,
        addressRemote: addressValue,
        addressType,
        portRemote,
        rawDataIndex: addressValueIndex + addressLength,
        vlessVersion: version,
        isUDP,
    };
}


/**
 * 
 * @param {import("@cloudflare/workers-types").Socket} remoteSocket 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket 
 * @param {ArrayBuffer} vlessResponseHeader 
 * @param {(() => Promise<void>) | null} retry
 * @param {*} log 
 */
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {
    // remote--> ws
    let remoteChunkCount = 0;
    let chunks = [];
    /** @type {ArrayBuffer | null} */
    let vlessHeader = vlessResponseHeader;
    let hasIncomingData = false; // check if remoteSocket has incoming data
    await remoteSocket.readable
        .pipeTo(
            new WritableStream({
                start() {
                },
                /**
                 * 
                 * @param {Uint8Array} chunk 
                 * @param {*} controller 
                 */
                async write(chunk, controller) {
                    hasIncomingData = true;
                    // remoteChunkCount++;
                    if (webSocket.readyState !== WS_READY_STATE_OPEN) {
                        controller.error(
                            'webSocket.readyState is not open, maybe close'
                        );
                    }
                    if (vlessHeader) {
                        webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
                        vlessHeader = null;
                    } else {
                        // seems no need rate limit this, CF seems fix this??..
                        // if (remoteChunkCount > 20000) {
                        //     // cf one package is 4096 byte(4kb),  4096 * 20000 = 80M
                        //     await delay(1);
                        // }
                        webSocket.send(chunk);
                    }
                },
                close() {
                    log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
                    // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.
                },
                abort(reason) {
                    console.error(`remoteConnection!.readable abort`, reason);
                },
            })
        )
        .catch((error) => {
            console.error(
                `remoteSocketToWS has exception `,
                error.stack || error
            );
            safeCloseWebSocket(webSocket);
        });

    // seems is cf connect socket have error,
    // 1. Socket.closed will have error
    // 2. Socket.readable will be close without any data coming
    if (hasIncomingData === false && retry) {
        log(`retry`)
        retry();
    }
}

/**
 * 
 * @param {string} base64Str 
 * @returns 
 */
function base64ToArrayBuffer(base64Str) {
    if (!base64Str) {
        return { error: null };
    }
    try {
        // go use modified Base64 for URL rfc4648 which js atob not support
        base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
        const decode = atob(base64Str);
        const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
        return { earlyData: arryBuffer.buffer, error: null };
    } catch (error) {
        return { error };
    }
}

/**
 * This is not real UUID validation
 * @param {string} uuid 
 */
function isValidUUID(uuid) {
    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    return uuidRegex.test(uuid);
}

const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CLOSING = 2;
/**
 * Normally, WebSocket will not has exceptions when close.
 * @param {import("@cloudflare/workers-types").WebSocket} socket
 */
function safeCloseWebSocket(socket) {
    try {
        if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
            socket.close();
        }
    } catch (error) {
        console.error('safeCloseWebSocket error', error);
    }
}

const byteToHex = [];
for (let i = 0; i < 256; ++i) {
    byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
    return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
function stringify(arr, offset = 0) {
    const uuid = unsafeStringify(arr, offset);
    if (!isValidUUID(uuid)) {
        throw TypeError("Stringified UUID is invalid");
    }
    return uuid;
}


/**
 * 
 * @param {import("@cloudflare/workers-types").WebSocket} webSocket 
 * @param {ArrayBuffer} vlessResponseHeader 
 * @param {(string)=> void} log 
 */
async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {

    let isVlessHeaderSent = false;
    const transformStream = new TransformStream({
        start(controller) {

        },
        transform(chunk, controller) {
            // udp message 2 byte is the the length of udp data
            // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
            for (let index = 0; index < chunk.byteLength;) {
                const lengthBuffer = chunk.slice(index, index + 2);
                const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
                const udpData = new Uint8Array(
                    chunk.slice(index + 2, index + 2 + udpPakcetLength)
                );
                index = index + 2 + udpPakcetLength;
                controller.enqueue(udpData);
            }
        },
        flush(controller) {
        }
    });

    // only handle dns udp for now
    transformStream.readable.pipeTo(new WritableStream({
        async write(chunk) {
            const resp = await fetch('https://1.1.1.1/dns-query',
                {
                    method: 'POST',
                    headers: {
                        'content-type': 'application/dns-message',
                    },
                    body: chunk,
                })
            const dnsQueryResult = await resp.arrayBuffer();
            const udpSize = dnsQueryResult.byteLength;
            // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
            const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
            if (webSocket.readyState === WS_READY_STATE_OPEN) {
                log(`doh success and dns message length is ${udpSize}`);
                if (isVlessHeaderSent) {
                    webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
                } else {
                    webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
                    isVlessHeaderSent = true;
                }
            }
        }
    })).catch((error) => {
        log('dns udp has error' + error)
    });

    const writer = transformStream.writable.getWriter();

    return {
        /**
         * 
         * @param {Uint8Array} chunk 
         */
        write(chunk) {
            writer.write(chunk);
        }
    };
}

/**
 * 
 * @param {string} userID 
 * @param {string | null} hostName
 * @returns {string}
 */
function getVLESSConfig(userID, hostName) {
    const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`
    return `
################################################################
v2ray
---------------------------------------------------------------
${vlessMain}
---------------------------------------------------------------
################################################################
clash-meta
---------------------------------------------------------------
- type: vless
  name: ${hostName}
  server: ${hostName}
  port: 443
  uuid: ${userID}
  network: ws
  tls: true
  udp: false
  sni: ${hostName}
  client-fingerprint: chrome
  ws-opts:
    path: "/?ed=2048"
    headers:
      host: ${hostName}
---------------------------------------------------------------
################################################################
`;
}

获得VLESS

部署成功之后,绑定一个域名

访问 https://域名/uuid 即可看到vless链接信息


typecho 貌似没有免费的S3对象储存的插件,但是找到了一个兰空图床的插件

使用

搭建LskyPro

这里选择使用1panel创建Docker容器
或者使用拉取编译好的镜像

docker run -d \
    --name lsky-pro \
    --restart unless-stopped \
    -p 8089:8089 \
    -v $PWD/lsky:/var/www/html \
    -e WEB_PORT=8089 \
    halcyonazure/lsky-pro-docker:latest

安装过程略过不提.

设置好反代域名
此处以api.imsun.org为例

创建储存

创建储存策略,以UCloud的对象储存US3为例
储存策略 = AWS S3
访问域名 = CDN加速域名
AccessKeyIdSecretAccessKey控制台 获取到的公钥 和 私钥.
连接地址https://s3-cn-gd.ufileos.com
区域(region) = us-east-1
储存桶名称 为自己创建的名称

获取token

使用API在线工具
Getman
根据API文档,使用POST
URL为 https://api.imsun.org/api/v1/tokens

把以下信息填入body

email: 账号
password: 密码

获得 token

"token": "2|7pcz6d1q7HCadfYpVX5f2HrYOCxaocvbeskP6at"

启用插件

插件下载地址

LskyProV2.zip

Api填写兰空图床的地址
Token填写上一步获取到的token

Bearer 2|7pcz6d1q7HCadfYpVX5f2HrYOCxaocvbeskP6at

储存ID填写兰空图床储存策略前的ID即可

总结

通过兰空图床也可以实现诸如通过webdav和alist实现存储在阿里云盘或者天翼云盘等操作.这里就不一一演示了.

引用

  1. https://www.coldyun.cn/archives/140.html
  2. https://github.com/HalcyonAzure/lsky-pro-docker

前言

起因是wordpress会自动安装一个名为wpcode的插件,每次删除卸载之后仍会自动安装,有一次直接导致网站无法访问,百度不得其解.重装也不起效.
于是弃之.
转投typecho的怀抱.

环境

PHP 8.2
Mysql 5.5

转换

先安装Typecho .
1.下载插件 WordpressToTypecho,配置好数据库的相关设置,
参考 https://docs.typecho.org/import
2.点击转换成功之后,把附件资料从WordPresswp-content/uploads目录下全部移动到Typechousr/uploads目录下,保持目录结构不变。
3.在数据库中执行

update typecho_contents set text=replace(text,'wp-content/uploads','usr/uploads')

4.设置伪静态,保持与wordpress相同的链接结构

使用

插件

1.AAEditor Markdown编辑器功能强大
2.AISummary 使用ChatGPT或者Gemini生成文章摘要.开箱即用.
看到木木老师有分享这个网站,可以使用我的aff注册,谢谢
https://burn.hair/register?aff=a8fr

新用户赠送 2500000 token,约等于 5 USD
邀请用户注册,双方各得 1000000 token,约等于 2 USD

3.Links 友情链接插件
4.ShortLinks 把外链转换成内链
php8.2报错需修改Plugin.php

//$str = str_replace(array("\r\n", "\r", "\n"), "|", $textarea);

// 检查 $textarea 是否为 null,如果是,设为一个空字符串
$textarea = $textarea ?? '';
// 然后你可以对其使用 str_replace
$str = str_replace(array("\r\n", "\r", "\n"), "|", $textarea);

5.SiteMap 网站地图
6.WordsCounter 字数统计
7.cosUploadV5 腾讯COS对象储存

主题

主题使用开源的 Matcha
1.修改
想让首页显示的摘要为AI生成的摘要
matcha/includes/posts.php中的

<div class="post-content" itemprop="articleBody">
<?php Matcha::excerpt($this); ?>
</div>

替换为

<div class="post-content" itemprop="articleBody">
    <?php
    // 判断是否存在自定义字段summary并输出,否则输出自动生成的摘要
    if($this->fields->summary){
        echo $this->fields->summary;
    } else {
        Matcha::excerpt($this);
    }
    ?>
</div>

主题使用Sunny Lite!时,修改article.php

  <?php echo get_Abstract($this,300); ?>

替换为

<?php echo $this->fields->summary;?>

2.显示页面加载时间
在主题的 Functions.php 中加入以下代码

/**
* 页面加载时间
*/
function timer_start() {
global $timestart;
$mtime = explode( ' ', microtime() );
$timestart = $mtime[1] + $mtime[0];
return true;
}
timer_start();
function timer_stop( $display = 0, $precision = 3 ) {
global $timestart, $timeend;
$mtime = explode( ' ', microtime() );
$timeend = $mtime[1] + $mtime[0];
$timetotal = number_format( $timeend - $timestart, $precision );
$r = $timetotal < 1 ? $timetotal * 1000 . " ms" : $timetotal . " s";
if ( $display ) {
echo $r;
}
return $r;
}

footer.php 合适位置插入

<?php echo timer_stop();?>

或者

<a href="javascript:(0)" id="pagetimes"></a>
<script>
document.getElementById('pagetimes').innerHTML = '<img src="https://img.shields.io/badge/页面加载耗时:-<?php echo timer_stop();?>-green">';
</script>

3.使用memos API增加说说页面
把 page-memos.php放在模板matcha

然后在后台新建独立页面-选择模板
添加字段中自定义
memos= memos地址 默认为https://memos.imsun.org
memostag = 使用的tag 可以选择默认,默认为说说
creatorId = memos的ID,默认为1
memosname = 显示的昵称
memosava = 显示的头像

下载地址

4.关闭非中文语系的评论
查找comments.php文件中

<?php if($this->allow('comment')): ?>

替换为

<?php if($this->allow('comment') && stripos($_SERVER['HTTP_ACCEPT_LANGUAGE'], 'zh') > -1): ?>

5.打赏
根据插件Donate提取

<!--打赏  -->
<script type="text/javascript" src="https://blogcdn.loliko.cn/donate/index_wx.js?121"></script>
<link rel="stylesheet" type="text/css" href="https://blogcdn.loliko.cn/donate/style_wx.css?121" />
<div class="donate-panel"> 
  <div id="donate-btn">赏</div> 
  <div id="qrcode-panel" style="display: none;"> 
    <div class="qrcode-body"> 
      <div class="donate-memo"> 
      <span id="donate-close">关闭</span> 
    </div> 
    <div class="donate-qrpay"> 
     <img id="wxqr" src="https://blogcdn.loliko.cn/donate/2in12.png" /> 
    </div> 
     </div> 
   </div> 
</div> 

参考
1.https://docs.typecho.org/import


前言

为了更加方便的同步内容,可以作备份之用.

声明

代码和功能实现来自于@大大的蜗牛 原理是使用webhook. 在发布内容时,触发脚本运行.

步骤

同步脚本

脚本中API_HOST为memos的API AUTHORIZATION为memos中Token CONTENT_URL111363033003475492为mastodon的用户的ID 获得用户ID的方法可以参见

[article id="1469"]

#!/bin/sh

# API 和 Token
API_HOST="https://memos.ee/api/v1/memo"
AUTHORIZATION="Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiamtqb3kiLCJpc3MiOiJtZW1vcyIsInN1YiI6IjEiLCJhdWQiOlsidXNlci5hY2Nlc3MtdG9rZW4iXSwiaWF0IjoxNjk3ODc0NTk2fQ.jNGMDE1YVX4Qj6hNhmrxb63WlRM5kGX10k_qRXH6ID4"

# 原始内容
CONTENT_URL="https://09j.cn/api/v1/accounts/111363033003475492/statuses?limit=1"
CONTENT=$(curl --connect-timeout 60 -s $CONTENT_URL | jq -r &#039;.[0]&#039;)
# mastodon
MASTODON_URL=$(echo $CONTENT | grep -oP &#039;https:\/\/09j\.cn\/@[^\/]+\/\d+&#039;)
DUDU_CONTENT="[自动转发自我的Mastodon]($MASTODON_URL)"

MENTIONS=$(echo $CONTENT | jq -r &#039;.mentions[]&#039;)
if [ ! -z "$MENTIONS" ]; then
  echo "Skipping status mention! $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")"
  echo ======================================================
  exit 0
fi

MEDIA=$(echo $CONTENT | jq -r &#039;.media_attachments&#039;)
# 判断 Media 的内容
if [ "$MEDIA" != "null" ]; then
  MEDIAS=$(echo $CONTENT | jq -r &#039;.media_attachments[] | select(.type=="image") | .url&#039;)
  # 拼接图片
  images=""
  for url in $MEDIAS; do
    images="$images![image]($url)\n"
  done
  TEXT=$(echo "$CONTENT" | jq -r &#039;.content&#039; | sed &#039;s/ +/ /g&#039; | lynx -dump -stdin -nonumbers -nolist | tr -d &#039;\n&#039; | sed &#039;/^$/N;s/\n\n/\n/g&#039; | sed &#039;s/^[[:space:]]*//;s/[[:space:]]*$//&#039;)
  TEXT="$TEXT\n$DUDU_CONTENT"
  TEXT="$TEXT\n$images"

else
   # 普通内容
  TEXT=$(echo "$CONTENT" | jq -r &#039;.content&#039; | sed &#039;s/ +/ /g&#039; | lynx -dump -stdin -nonumbers -nolist | tr -d &#039;\n&#039; | sed &#039;/^$/N;s/\n\n/\n/g&#039; | sed &#039;s/^[[:space:]]*//;s/[[:space:]]*$//&#039;)
  TEXT="${TEXT}\n$DUDU_CONTENT"
fi

curl -X POST \
  -H "Accept: application/json" \
  -H "Authorization: $AUTHORIZATION" \
  -d "{ \"content\": \"$TEXT\" }" \
  $API_HOST

echo Sync Mastodon to Memos Successful! $(TZ=UTC-8 date +"%Y-%m-%d"" ""%T")
echo ======================================================

我稍微做了一点修改.在发布到memos的同时贴上原本mastodon的原文链接. 由于我不会写规则,就随意写了匹配规则 我的实例为09j.cn 不需要则删除以下

MASTODON_URL=$(echo $CONTENT | grep -oP &#039;https:\/\/09j\.cn\/@[^\/]+\/\d+&#039;)
DUDU_CONTENT="[自动转发自我的Mastodon]($MASTODON_URL)" 
TEXT="$TEXT\n$DUDU_CONTENT"

即可

部署webhook

Docker镜像是根据官方dockerfile增加了中文支持,

推荐使用docker-compose部署 编辑docker-compose.yaml内容为

services:
  webhook:
    image: jkjoy/webhook
    container_name: webhook
    command: -verbose -hooks=hooks.yml -hotreload
    environment:
      - TZ=Asia/Chongqing #中国时区
      - LANG=C.UTF-8  #中文支持
    volumes:
      - ./config:/config:ro
    ports:
      - 9000:9000
    restart: always

在根目录下创建config目录,并在config下创建hooks.yml文件并编辑内容为

- id: memos
  execute-command: "/config/memos.sh"
  command-working-directory: "/"

把脚本内容保存为memos.sh保存在config目录下

然后在docker-compose.yaml所在的根目录下 运行docker compose up -d即可

使用Webhook

hooks.yaml为webhook的配置

其中的execute-command为可执行脚本

webhook的访问地址格式为

服务器 ip:端口/hooks/ID

以127.0.0.1为例 访问http://127.0.0.1:9000/hooks/memos

设置mastodon

在管理员后台中管理-webhooks-新增对端 对端URL填入http://127.0.0.1:9000/hooks/memos 已启用事件选择status.created 点击新增即可在发布新的嘟嘟时同步内容到memos了.

其他

同理也可以使用webhook在发布memos时候同步到其他拥有API的服务中了.


EasyPanel

官方网站 EasyPanel.io EasyPanel是一款基于docker的可视化面板. 拥有付费免费两种模式

本文介绍以免费模式为主(主要是收费不菲)

从首页的介绍可以看到他的特色就是通过直观的界面部署应用,管理数据库以及签发SSL证书.

安装

  • 建议在纯净的linux系统下安装

通过命令行

curl -sSL https://get.easypanel.io | sh

即可完成安装,官方给出的配置要求内存大于2G,根据实测,1G的vps运行起来问题也不大.

由于是容器化的部署方式,各个应用之间独立运行.NICE.

使用

访问

安装之后通过

ip:3000

访问面板,初次访问会要求创建管理员账号和密码.

登录

进入面板会发现很简洁
进入面板

创建

创建项目点击Create Project填写项目名称,确定,进入该项目

模板

点击templates会发现这里有很多常用的应用模板,只要点击就可以部署.

譬如memos uptime wordpress Flarum GoToSocial Umami Vaultwarden 等上百款应用

服务

常见的模板应用大多都是全世界著名的应用.国内的某些应用,或者不是很常见的应用该如何部署

此处以gatus为例

点击APP,填写名称,确认

点击General
gatus的docker镜像为twinproduction/gatus:latest
Docker images中填入twinproduction/gatus:latest
SAVE保存.

点击Domain-ADD Domain-HOST填写域名

全部填写完成之后,点SAVE保存.

点击Advanced-Mounts-ADD VOLUME Mounts

Name为宿主机名称可以自己设置

Mount Path为Docker

挂载目录 /data

点击ADD File Mounts,其中 Mount Path为挂载路径,此处填写为/config/config.yaml

Content为yaml格式的配置文件 与 config.yaml内容对应

以下为示例内容可自行修改

storage:
  type: sqlite
  path: /data/data.db

ui:
  buttons:
    - name: "Home"
      link: "https://www.imsun.org"

endpoints:
  - name: bloghb
    group: core
    url: "https://blog.hb.cn"
    interval: 3m
    conditions:
      - "[STATUS] == 200"

  - name: blogcn
    group: core
    url: "https://blog.asbid.cn"
    interval: 3m
    conditions:
      - "[STATUS] == 200"

  - name: blogsd
    group: core
    url: "https://blog.sd.cn"
    interval: 3m
    conditions:
      - "[STATUS] == 200"

点击SAVE保存

点击Deploy.

完成部署 别忘记在DNS处解析域名

gatus演示

https://status.0tz.top/

总结

优势:不用折腾反代,不用担心证书到期,常用应用傻瓜式部署


Pleroma是什么

Pleroma是一个联合社交网络平台,与GNU社交和其他OStatus实现兼容。它是AGPLv3许可下的免费软件。它实际上由两个组件组成:后端(名称简单为Pleroma)和面向用户的前端(名称为Pleroma-FE).它的主要优点是重量轻、速度快.

准备工作

  • Docker
  • git

步骤

拉取仓库

git clone https://github.com/jkjoy/docker-pleroma.git
cd docker-pleroma

创建文件夹并赋权


mkdir uploads config
chown -R 911:911 uploads

Pleroma需要citext PostgreSQL拓展


docker-compose up -d db
docker exec -i pleroma_db psql -U pleroma -c CREATE EXTENSION IF NOT EXISTS citext;
docker-compose down

修改docker-compose.yml文件中的ENV内容


DOMAIN: example.com   //实例使用的域名
INSTANCE_NAME: Pleroma  //实例的名称
ADMIN_EMAIL: admin@example.com //管理员邮箱
NOTIFY_EMAIL: notify@example.com //通知邮箱
DB_USER: pleroma  //可以保持默认
DB_PASS: ChangeMe! //可以保持默认
DB_NAME: pleroma //可以保持默认

保存好之后执行


docker-compose up -d

查看容器的状态


docker logs -f pleroma_web

反向代理

反向代理127.0.0.1:4000以宝塔为例

创建一个管理员账号

以下示例中 fakeadmin 为用户名 admin@test.net 为邮箱


docker exec -it pleroma_web sh ./bin/pleroma_ctl user new fakeadmin admin@test.net --admin

执行之后会出现一个链接,点击即可重置管理员密码 ### 更改前端

Pleroma-FE不太喜欢,选用 Soapbox作为前端


cd static
curl -O https://dl.soapbox.pub/main/soapbox.zip

unzip soapbox.zip

刷新即可

演示地址

https://tot.yt


通过memos的自定义样式更改字体为霞鹜文楷

由于cdn.staticfile.org出现了跨域的问题导致很多图标和字体无法正常显示.

加上ucloud买了一年100G的CDN流量.

自己反正也很少用,就自建一个霞鹜文楷的CDN

把以下代码复制粘贴进memos的自定义样式中保存即可.

@import '//cdn.09j.cn/lxgw-wenkai-webfont/style.css';
body {
font-family: "LXGW WenKai", sans-serif;
}

 


Gotosocial是什么

GoToSocial 是一个使用 Golang 编写的 ActivityPub 社交网络服务器,它是一个轻量级、安全的联邦社交网络入口,可让用户保持联系、发布和分享图片、文章等内容。GoToSocial 强调用户的隐私和自由,不会跟踪用户的行为,也不会为了向用户展示广告而收集他们的数据。 使用 GoToSocial 可以让用户进入联邦社交网络的世界,联邦网络是一种基于协议的社交网络结构,它允许用户从一个社交网络实例互相跟随、交流和分享内容。这种结构可以让用户自由选择社交网络平台,同时避免某个平台垄断市场。用户可以在不同的实例之间进行跟随和互动,这样就可以更好地保护用户的隐私和自由。

部署方式

使用docker-compose部署

创建安装目录并更改权限

mkdir -p /var/www/gotosocial/data && cd /var/www/gotosocial && chown 1000:1000 ./data

配置 docker-compose.yaml 文件

version: "3.3"

services:
  gotosocial:
    image: superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    networks:
      - gotosocial
    environment:
      GTS_HOST: social.example.com
      GTS_DB_TYPE: sqlite
      GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
      GTS_LETSENCRYPT_ENABLED: "false"
      GTS_STORAGE_BACKEND: "s3"
      GTS_STORAGE_S3_BUCKET: "BUCKET名称"
      GTS_STORAGE_S3_ENDPOINT: "#S3 API"
      GTS_STORAGE_S3_ACCESS_KEY: "#api-tokens"
      GTS_STORAGE_S3_SECRET_KEY: "#api-tokens"
      GTS_STORAGE_S3_PROXY: "true"
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - ./data:/gotosocial/storage
    restart: "always"

networks:
  gotosocial:
    ipam:
      driver: default

支持S3存储

运行

docker compose up -d

创建用户

docker exec -it gotosocial ./gotosocial admin account create --username admin --email YOUR@EMAIL.COM --password  SOME_VERY_GOOD_PASSWD ;

自行更改用户名`密码`邮箱地址

admin改成自己需要的用户名

增加管理员权限

docker exec -it gotosocial ./gotosocial admin account promote --username admin

反向代理

反代127.0.0.1:8080即可 此处就不赘述了.

客户端

https://login.ima.cm

  • 大家都可以使用

演示

https://ima.cm


官方给出的一键安装由于墙的原因可能无法安装成功。
所以找到了一个国内镜像的一键安装脚本

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

最近去CyberPanel官方看到,版本已经到V1.6版本,且基本的功能应该算成熟。不过目前的用户也并不是很多,这个应该与官方的实际推广宣传力度有关系,当然还有很多国内用户的使用习惯,因为毕竟作为面板而言我们选择一个习惯的就可以,也没有必要使用太多的。在这篇文章中,将体验CyberPanel免费面板的安装与基本功能应用。

1、配置要求

根据官方的要求,需要服务器系统使用的Centos 7.x、Python 2.7、内存512MB、硬盘10GB以上。

2、一键安装

实测暂时支持 centos Ubuntu 其他暂不支持

sh <(curl https://cyberpanel.net/install.sh || wget -O - https://cyberpanel.net/install.sh)

可以直接一键安装最新版本。 安装完毕,看到登入地址和用户名和密码。

3、CyberPanel登入

忘记密码如何修改密码?执行

adminPass 123456789

修改为123456789
登入CyberPanel面板,我们可以选择Chinese简体中文。因为面板自带语言包的,我们登入进入后看到简体中文。看到CyberPanel面板后台,体验还是比较好的。毕竟OpenLiteSpeed团队,华人还是比较多的,所以还是有针对性的希望让中文用户使用。