ToB企服应用市场:ToB评测及商务社交产业平台

标题: 应用集成-在Hexo、Hugo博客框架中使用Gitalk基于Github上仓库项目的issue无 [打印本页]

作者: 守听    时间: 2022-9-16 17:17
标题: 应用集成-在Hexo、Hugo博客框架中使用Gitalk基于Github上仓库项目的issue无
关注「WeiyiGeek」公众号
设为「特别关注」每天带你玩转网络安全运维、应用开发、物联网IOT学习!
希望各位看友【关注、点赞、评论、收藏、投币】,助力每一个梦想。

本章目录

目录

作者: WeiyiGeek [唯一极客]
首发地址: https://mp.weixin.qq.com/s/2LLVDf7Fj4cX3IRZUtUfnA
原文地址: https://blog.weiyigeek.top/2022/5-28-588.html
0x00 Gitalk - 基于Github的评论系统

描述: 我想对于所有使用hexo、Hugo或者WordPress自建博客的博主来说GitTalk应该不陌生,GitTalk通过Github的OpenAPI以及issues功能实现社区评论确实还是很方便的,除开对国内访问速度较慢就没啥毛病,但是考虑到新手朋友此处还是简单介绍一下。
1.快速介绍

描述: Gitalk 是一个基于 Github Issue 和 Preact 的现代评论组件。
功能:
项目地址:https://github.com/gitalk/gitalk
帮助文档:https://github.com/gitalk/gitalk/blob/master/readme-cn.md
温馨提示: 当前 Gitalk 最新版本为 1.7.2 (Mar 3, 2021), 如后续随着时间推移,可能会有些许变化,建议参考官网(https://github.com/gitalk/gitalk/tags)

2.安装部署

描述:安装引用Gitalk评论系统的两种方式,
安装实践
  1. [/code]
  2. [list]
  3. [*]方式2.使用 npm 安装 gital 模块。
  4. [/list][code]# 模块安装
  5. npm i --save gitalk
  6. # 项目导入
  7. import 'gitalk/dist/gitalk.css'
  8. import Gitalk from 'gitalk'
复制代码
配置实践
首先,您需要为商店评论选择一个公共 github 存储库(已存在或创建一个新存储库),然后创建一个 GitHub 应用程序,如果你没有,点击这里 (https://github.com/settings/applications/new) 注册一个新的。
  1. Application name : BlogTalk
  2. Homepage URL : https://blog.weiyigeek.top
  3. Application description : 欢迎访问 WeiyiGeek blog\'s [blog.weiyigeek.top] talk about , 欢迎留言骚扰哟,亲!
  4. Authorization callback URL : https://blog.weiyigeek.top
复制代码

注意:您必须在授权回调 URL 字段中指定网站域 url。
然后,创建完成后你将获取Client ID 与 Client Secret,如下所示:

注意:后续更新修改可以进行访问 Settings/Developer settings ( https://github.com/settings/developers )
最后,创建一个公共仓库此处我创建的是blogtalk,创建完后在项目的(https://github.com/WeiyiGeek/blogtalk/settings)中启用 issue 即可


使用方式1.将如下代码添加到您的页面:
  1. <head>
  2.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  3.    
  4. </head>
  5. <body>
  6.    
  7.    
  8. </body>
复制代码
使用方式2.在React中使用
  1. import GitalkComponent from "gitalk/dist/gitalk-component";
  2. // 并使用类似的组件
  3. <GitalkComponent options={{
  4.   clientID: "...",
  5.   // ...
  6.   // options below
  7. }} />
复制代码
温馨提示: Gitalk 对象实例化参数参考 (https://github.com/gitalk/gitalk#options)

3.使用实践

在 Hexo 中使用
描述: 此处以我的博客[https://blog.weiyigeek.top] 为例进行演示配置,此处笔者使用的是 hexo + mellow 主题 , 已经经过二次魔改(有需要该博客主题请在公众号回复【mellow博客主题】或者访问 https://weiyigeek.top/wechat.html?key=mellow博客主题 )。
  1. # gittalk 评论系统
  2. gitalk:
  3.   enable: true
  4.   owner: WeiyiGeek  # github账号
  5.   repo: blogtalk    # 留言仓库
  6.   proxy: /github/login/oauth/access_token # 反向代理解决跨域问题,后续将会主要讲解哦。
  7.   oauth:
  8.     client_id: 8d8e965c******97026d3       # Github App Auth ID
  9.     client_secret: e9c6141cb1f02f721********d01cb4d7a8f069 #  Github App Auth secret
  10.   perPage: 15
复制代码
  1. var gitalk = new Gitalk({
  2.   clientID: '<%- theme.gitalk.oauth.client_id %>',
  3.   clientSecret: '<%- theme.gitalk.oauth.client_secret %>',
  4.   repo: '<%- theme.gitalk.repo %>',
  5.   owner: '<%- theme.gitalk.owner %>',
  6.   admin: ['<%- theme.gitalk.owner %>'],
  7.   id: location.pathname,
  8.   proxy: '<%- theme.gitalk.proxy %>',
  9.   distractionFreeMode: true
  10. })
  11. #  hexo g 生成静态文件后的样子
  12. # var gitalk = new Gitalk({
  13. #   clientID: '8d8e965c******97026d3',
  14. #   clientSecret: 'e9c6141cb1f02f721********d01cb4d7a8f069',
  15. #   repo: 'blogtalk',
  16. #   owner: 'WeiyiGeek',
  17. #   admin: ['WeiyiGeek'],
  18. #   id: location.pathname,
  19. #   proxy: '/github/login/oauth/access_token',
  20. #   distractionFreeMode: false
  21. # })
  22. # 创建 gitalk-container
  23. gitalk.render('gitalk-container')
复制代码
温馨提示: 建议将distractionFreeMode设置为false,因为True真心难看。
温馨提示: 为了 Github Apps ID 与 Secrets 的安全,我们需要针对上面 new Gitalk 实例化参数进行js加密混淆 (http://www.esjson.com/jsEncrypt.html)

n.入坑出坑

1.使用Gitalk进行Github的Oauth认证无法跨域获取Token问题解决办法

描述: 在最开始之初我们也是使用官方演示代码中,使用的第三方提供的CORS代理服务,他会默认放行所有CORS请求,但是随着而来的问题是登陆会出现网络错误 Error: Network Error 或者在使用时出现 Forbidden 错误 (https://github.com/gitalk/gitalk/issues/514) 。
目前由于该CORS代理服务遭到滥用,因此做了限制,导致GitTalk失效,在实践中发现如下CORS代理服务其要么有限制要么根本不能使用,所以实践的朋友们就不要像使用如下CORS代理服务:
  1. # 限流
  2. https://cors-anywhere.herokuapp.com/https://github.com/login/oauth/access_token
  3. # 被墙
  4. https://cors-anywhere.azm.workers.dev/https://github.com/login/oauth/access_token
复制代码
温馨提示: CORS Anywhere 是一个 NodeJS 代理,它将 CORS 标头添加到代理请求中。 项目地址 (https://github.com/Rob--W/cors-anywhere)

在 百度 CSDN 中捡了一圈垃圾之后,还是没有最好的解决方案,然后通过某种方式Google了一下,找到两种替代的方式利用cloudflare worker (不幸得是默认的cf worker的域名workers.dev被墙了)或者 Vercel 搭建在线代理(无vps推荐使用Vercel) 或者 使用VPS中的nginx服务器来反代 https://github.com (比较推荐-当前博主正在使用)。
方式1.没有VPS或者自己的服务器(想白嫖的)
描述: 在 cloudflare (https://dash.cloudflare.com/login/) 上创建一个免费的在线代理来解决gitalk授权登录跨域问题,利用CloudFlare Worker创建在线代理,不需要我们有服务器,也不需要搭建Node.js服务,只需要注册一个CloudFlare账号,创建一个Worker,部署一个JS脚本就可以了,简单方便,下面我们就来看看如何创建吧。

创建好之后我们便可编辑其 Worker 服务代码,如下代码也可通过 https://github.com/WeiyiGeek/SecOpsDev/tree/master/Application/Blog/Hexo/Gitalk 获得。
  1. const exclude = [];     // Regexp for blacklisted urlsconst include = [/^https?:\/\/.*weiyigeek\.top$/, /^https?:\/\/localhost/]; // Regexp for whitelisted origins e.g.const apiKeys = {        EZWTLwVEqFnaycMzdhBz: {                name: 'Test App',                expired: false,                expiresAt: new Date('2023-01-01'),                exclude: [], // Regexp for blacklisted urls                include: ["^http.?://www.weiyigeek.top$", "weiyigeek.top$", "^https?://localhost/"], // Regexp for whitelisted origins        },};// Config is all above this line.// It should not be necessary to change anything below.function verifyCredentials(request) {        // Throws exception on verification failure.        const requestApiKey = request.headers.get('x-cors-proxy-api-key');        if (!Object.keys(apiKeys).includes(requestApiKey)) {                throw new UnauthorizedException('Invalid authorization key.');        }        if (apiKeys[requestApiKey].expired) {                throw new UnauthorizedException('Expired authorization key.');        }        if (apiKeys[requestApiKey].expiresAt && apiKeys[requestApiKey].expiresAt.getTime() < Date.now()) {                throw new UnauthorizedException(`Expired authorization key.\nKey was valid until: ${apiKeys[requestApiKey].expiresAt}`);        }        return apiKeys[requestApiKey];}function checkRequiredHeadersPresent(request) {        // Throws exception on verification failure.        if (!request.headers.get('Origin') && !request.headers.get('x-requested-with')) {                throw new BadRequestException('Missing required request header. Must specify one of: origin,x-requested-with');        }}function UnauthorizedException(reason) {        this.status = 401;        this.statusText = 'Unauthorized';        this.reason = reason;}function BadRequestException(reason) {        this.status = 400;        this.statusText = 'Bad Request';        this.reason = reason;}function isListed(uri, listing) {        let returnValue = false;        console.log(uri);        if (typeof uri === 'string') {                for (const m of listing) {                        if (uri.match(m) !== null) {                                returnValue = true;                        }                }        } else { //   Decide what to do when Origin is null                returnValue = true; // True accepts null origins false rejects them.        }        return returnValue;}function fix(myHeaders, request, isOPTIONS) {        myHeaders.set('Access-Control-Allow-Origin', request.headers.get('Origin'));        if (isOPTIONS) {                myHeaders.set('Access-Control-Allow-Methods', request.headers.get('access-control-request-method'));                const acrh = request.headers.get('access-control-request-headers');                if (acrh) {                        myHeaders.set('Access-Control-Allow-Headers', acrh);                }                myHeaders.delete('X-Content-Type-Options');        }        return myHeaders;}function parseURL(requestUrl) {        const match = requestUrl.match(/^(?:(https?:)?\/\/)?(([^/?]+?)(?::(\d{0,5})(?=[/?]|$))?)([/?][\S\s]*|$)/i);        //<head>
  2.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  3.    
  4. </head>
  5. <body>
  6.    
  7.    
  8. </body>              ^^^^^^^          ^^^^^^^^      ^^^^^^^<head>
  9.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  10.    
  11. </head>
  12. <body>
  13.    
  14.    
  15. </body>^^^^^^^^^^^^        //<head>
  16.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  17.    
  18. </head>
  19. <body>
  20.    
  21.    
  22. </body>            1:protocol       3:hostname     4:port<head>
  23.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  24.    
  25. </head>
  26. <body>
  27.    
  28.    
  29. </body> 5:path + query string        //<head>
  30.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  31.    
  32. </head>
  33. <body>
  34.    
  35.    
  36. </body><head>
  37.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  38.    
  39. </head>
  40. <body>
  41.    
  42.    
  43. </body>              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //<head>
  44.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  45.    
  46. </head>
  47. <body>
  48.    
  49.    
  50. </body><head>
  51.     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/gitalk@1/dist/gitalk.css">
  52.    
  53. </head>
  54. <body>
  55.    
  56.    
  57. </body>            2:host        if (!match) {                console.log('no match');                throw new BadRequestException('Invalid URL for proxy request.');        }        console.log('parseURL:match:', match);        if (!match[1]) {                console.log('nothing in match group 1');                if (/^https?:/i.test(requestUrl)) {                        console.log('The pattern at top could mistakenly parse "http:///" as host="http:" and path=///.');                        throw new BadRequestException('Invalid URL for proxy request.');                }                // Scheme is omitted.                if (requestUrl.lastIndexOf('//', 0) === -1) {                        console.log('"//" is omitted');                        requestUrl = '//' + requestUrl;                }                requestUrl = (match[4] === '443' ? 'https:' : 'http:') + requestUrl;        }        const parsed = new URL(requestUrl);        if (!parsed.hostname) {                console.log('"http://:1/" and "http:/notenoughslashes" could end up here.');                throw new BadRequestException('Invalid URL for proxy request.');        }        return parsed;}async function proxyRequest(request, activeApiKey) {        const isOPTIONS = (request.method === 'OPTIONS');        const originUrl = new URL(request.url);        const origin = request.headers.get('Origin');        // ParseURL throws when the url is invalid        const fetchUrl = parseURL(request.url.replace(originUrl.origin, '').slice(1));        // Throws if it fails the check        checkRequiredHeadersPresent(request);        // Excluding urls which are not allowed as destination urls        // Exclude origins which are not int he included ones        if (isListed(fetchUrl.toString(), [...exclude, ...(activeApiKey?.exclude || [])]) || !isListed(origin, [...include, ...(activeApiKey?.include || [])])) {                throw new BadRequestException('Origin or Destination URL is not allowed.');        }        let corsHeaders = request.headers.get('x-cors-headers');        if (corsHeaders !== null) {                try {                        corsHeaders = JSON.parse(corsHeaders);                } catch {}        }        if (!originUrl.pathname.startsWith('/')) {                throw new BadRequestException('Pathname does not start with "/"');        }        const recvHpaireaders = {};        for (const pair of request.headers.entries()) {                if ((pair[0].match('^origin') === null)        && (pair[0].match('eferer') === null)        && (pair[0].match('^cf-') === null)        && (pair[0].match('^x-forw') === null)        && (pair[0].match('^x-cors-headers') === null)                ) {                        recvHpaireaders[pair[0]] = pair[1];                }        }        if (corsHeaders !== null) {                for (const c of Object.entries(corsHeaders)) {                        recvHpaireaders[c[0]] = c[1];                }        }        const newRequest = new Request(request, {                headers: recvHpaireaders,        });        const response = await fetch(fetchUrl, newRequest);        let myHeaders = new Headers(response.headers);        const newCorsHeaders = [];        const allh = {};        for (const pair of response.headers.entries()) {                newCorsHeaders.push(pair[0]);                allh[pair[0]] = pair[1];        }        newCorsHeaders.push('cors-received-headers');        myHeaders = fix(myHeaders, request, isOPTIONS);        myHeaders.set('Access-Control-Expose-Headers', newCorsHeaders.join(','));        myHeaders.set('cors-received-headers', JSON.stringify(allh));        const body = isOPTIONS ? null : await response.arrayBuffer();        return new Response(body, {                headers: myHeaders,                status: (isOPTIONS ? 200 : response.status),                statusText: (isOPTIONS ? 'OK' : response.statusText),        });}function homeRequest(request) {        const isOPTIONS = (request.method === 'OPTIONS');        const originUrl = new URL(request.url);        const origin = request.headers.get('Origin');        const remIp = request.headers.get('CF-Connecting-IP');        const corsHeaders = request.headers.get('x-cors-headers');        let myHeaders = new Headers();        myHeaders = fix(myHeaders, request, isOPTIONS);        let country = false;        let colo = false;        if (typeof request.cf !== 'undefined') {                country = typeof request.cf.country === 'undefined' ? false : request.cf.country;                colo = typeof request.cf.colo === 'undefined' ? false : request.cf.colo;        }        return new Response(                'CLOUDFLARE-CORS-ANYWHERE\n\n'        + 'Source:\nhttps://github.com/chrisspiegl/cloudflare-cors-anywhere\n\n'        + 'Usage:\n'        + originUrl.origin + '/{uri}\n'        + 'Header x-cors-proxy-api-key must be set with valid api key\n'        + 'Header origin or x-requested-with must be set\n\n'        // + 'Limits: 100,000 requests/day\n'        // + '          1,000 requests/10 minutes\n\n'        + (origin === null ? '' : 'Origin: ' + origin + '\n')        + 'Ip: ' + remIp + '\n'        + (country ? 'Country: ' + country + '\n' : '')        + (colo ? 'Datacenter: ' + colo + '\n' : '') + '\n'        + ((corsHeaders === null) ? '' : '\nx-cors-headers: ' + JSON.stringify(corsHeaders)),                {status: 200, headers: myHeaders},        );}async function handleRequest(request) {        const {protocol, pathname} = new URL(request.url);        // In the case of a "Basic" authentication, the exchange MUST happen over an HTTPS (TLS) connection to be secure.        if (protocol !== 'https:' || request.headers.get('x-forwarded-proto') !== 'https') {                throw new BadRequestException('Must use a HTTPS connection.');        }        switch (pathname) {                case '/favicon.ico':                case '/robots.txt':                        return new Response(null, {status: 204});                case '/':                        return homeRequest(request);                default: {                        // Not 100% sure if this is a good idea…                        // Right now all OPTIONS requests are just simply replied to because otherwise they fail.                        // This is necessary because apparently, OPTIONS requests do not carry the `x-cors-proxy-api-key` header so this can not be authorized.                        if (request.method === 'OPTIONS') {                                return new Response(null, {                                        headers: fix(new Headers(), request, true),                                        status: 200,                                        statusText: 'OK',                                });                        }                        // The "x-cors-proxy-api-key" header is sent when authenticated.                        //if (request.headers.has('x-cors-proxy-api-key')) {                                // Throws exception when authorization fails.                                //const activeApiKey = verifyCredentials(request);                                // Only returns this response when no exception is thrown.                                return proxyRequest(request);                        //}                        // Not authenticated.                        //throw new UnauthorizedException('Valid x-cors-proxy-api-key header has to be provided.');                }        }}addEventListener('fetch', async event => {        event.respondWith(                handleRequest(event.request).catch(error => {                        const message = error.reason || error.stack || 'Unknown Error';                        return new Response(message, {                                status: error.status || 500,                                statusText: error.statusText || null,                                headers: {                                        'Content-Type': 'text/plain;charset=UTF-8',                                        // Disables caching by default.                                        'Cache-Control': 'no-store',                                        // Returns the "Content-Length" header for HTTP HEAD requests.                                        'Content-Length': message.length,                                },                        });                }),        );});
复制代码
部署结果: https://cors-anywhere.weiyigeek.workers.dev/

温馨提示: cloudflare 构建无服务器应用程序免费版本每天限额10万次请求,所有为了避免其它 people 恶意使用,请在使用时设置访问白名单, 上述源码来源于 (https://github.com/chrisspiegl/cloudflare-cors-anywhere)。
温馨提示: 除了使用 cloudflare 还可以使用 Vercel 免费部署node.js项目解决跨域问题,你可参考该项目 (https://github.com/Dedicatus546/cors-server) ,此处就不在累述。

方式2.有公网VPS、服务器
描述: 由于我自己有VPS所以就不借用 cloudflare 与 Vercel,因为其国内网络原因,时而通畅时而有缓慢 , 此处我将使用Nginx服务在blog.conf配置Nginx文件中加入如下location指令片段
  1. # https - www.weiyigeek.top
  2. server {
  3.   listen       80;
  4.   listen       443 ssl http2;
  5.   server_name  blog.weiyigeek.top;
  6.    
  7.   # CORS
  8.   add_header Access-Control-Allow-Origin '*.weiyigeek.top';
  9.   add_header Access-Control-Allow-Methods 'GET,POST,OPTIONS';
  10.   add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
  11. ...
  12.   # Gitalk Auth Use
  13.   location /github {
  14.     if ($request_method = 'OPTIONS') {
  15.       return 204;
  16.     }
  17.     proxy_pass https://github.com/; # 注意尾部斜杠不能少
  18.   }
  19. ...
  20.   error_page  403          /warn/403.html;
  21.   error_page  404          /warn/404.html;
  22.   error_page  500 502 503  /warn/500.html;
  23.   error_page  504          /warn/504.html;
  24. }
复制代码
配置完成后检测blog.conf配置以及重载nginx服务 nginx -t && nginx -s reload, 然后修改Hexo 主题中的 _config.yaml 将 Gitalk 的 proxy 配置为 proxy: /github/login/oauth/access_token 即可。
  1. # gittalk 评论系统
  2. gitalk:
  3.   enable: true
  4.   owner: WeiyiGeek  # github账号
  5.   repo: blogtalk    # 留言仓库
  6.   proxy: /github/login/oauth/access_token # 关键点 (前台请求是https://blog.weiyigeek.top/github/login/oauth/access_token, 而实际请求是https://github.com/login/oauth/access_token,所以解决了跨域问题)
  7.   oauth:
  8.     client_id: 8d8e965c******97026d3       # Github App Auth ID
  9.     client_secret: e9c6141cb1f02f721********d01cb4d7a8f069 #  Github App Auth secret
  10.   perPage: 15
复制代码
之后,我们需要批量初始每篇文章issue根据其路径/2020/3-20-658.html,此处采用了gitalk-auto-init.js脚本进行批量初始化文章issue。
温馨提示: 下述 gitalk-auto-init.js 脚本可以通过如下连接( https://github.com/WeiyiGeek/SecOpsDev/tree/master/Application/Blog/Hexo/Gitalk )进行获取
脚本依赖:
  1. $ npm i -S hexo-generator-sitemap
  2. $ npm i -D md5 moment request xml-parser
  3. + moment@2.29.2
  4. + request@2.88.2
  5. + md5@2.3.0
  6. + xml-parser@1.2.1
  7. added 55 packages from 70 contributors in 8.467s
复制代码
配置运行:
  1. // gitalk-auto-init.js 脚本部分片段
  2. // 配置信息
  3. const config = {
  4.   username: 'weiyigeek',   // GitHub repository 所有者,可以是个人或者组织。对应Gitalk配置中的owner
  5.   repo: "blogtalk",         // 储存评论issue的github仓库名,仅需要仓库名字即可。对应 Gitalk配置中的repo
  6.   token: 'ghp_wnpWqL********6RIf0NR5iD',   // 前面在Github中的 personal access token
  7.   sitemap: path.join(__dirname, './public/sitemap.xml'), // 自己站点的 sitemap 文件地址
  8.   cache: true, // 是否启用缓存,启用缓存会将已经初始化的数据写入配置的 gitalkCacheFile 文件,下一次直接通过缓存文件判断
  9.   gitalkCacheFile: path.join(__dirname, './gitalk-init-cache.json'), // 用于保存 gitalk 已经初始化的 id 列表
  10.   gitalkErrorFile: path.join(__dirname, './gitalk-init-error.json'), // 用于保存 gitalk 初始化报错的数据
  11. };
  12. // sitemap.xml 示例
  13. <?xml version="1.0" encoding="UTF-8"?>
  14. <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  15.     <url>
  16.     <loc>https://blog.weiyigeek.top/2020/5-28-588.html</loc>
  17.     <lastmod>2022-08-15T14:39:08.638Z</lastmod>
  18.     <title>Ingress-Nginx进阶学习实践扩充配置记录</title>
  19.   </url>
  20.     ....
  21. </urlset>
复制代码

执行结果:
  1. --------- 运行结果 ---------
  2. 报错数据: 1 条。参考文件 /mnt/e/githubProject/blog/gitalk-init-error.json。
  3. 本次成功: 27 条。
  4. 写入缓存: 90 条,已初始化 63 条,本次成功: 27 条。参考文件 /mnt/e/githubProject/blog/gitalk-init-cache.json。
复制代码
我们也可以通过 blogtalk 项目中 issue (https://github.com/WeiyiGeek/blogtalk/issues) 查看初始化结果以及最新评论。

在初始化issue完成之后,我们可以找到一篇 https://blog.weiyigeek.top/about/ 文章进行留言验证。

原文地址: https://blog.weiyigeek.top/2022/5-28-588.html
本文至此完毕,更多技术文章,尽情期待下一章节!
WeiyiGeek Blog 个人博客 - 为了能到远方,脚下的每一步都不能少 】
欢迎各位志同道合的朋友一起学习交流【点击加入交流群】,如文章有误请在下方留下您宝贵的经验知识!
作者主页: 【 https://weiyigeek.top
博客地址: 【 https://blog.weiyigeek.top 】

专栏书写不易,如果您觉得这个专栏还不错的,请给这篇专栏 【点个赞、投个币、收个藏、关个注,转个发,留个言】(人间六大情),这将对我的肯定,谢谢!。
<ul>echo "【点个赞】,动动你那粗壮的拇指或者芊芊玉手,亲!"
printf("%s", "【投个币】,万水千山总是情,投个硬币行不行,亲!")
fmt.Printf("【收个藏】,阅后即焚不吃灰,亲!")
console.info("【转个发】,让更多的志同道合的朋友一起学习交流,亲!")
System.out.println("【关个注】,后续浏览查看不迷路哟,亲!")

<strong>cout




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4