作者: 白宦成

独立开发者, 自由职业者, 写作者

领证,其实并不会有什么不同

晚上睡觉的时候,老婆问我:“领证了感觉好像也没啥变化?”

我回答:“是的,的确没有什么变化”。


老婆过去一直认为领证是一个很重要的事情。从她的视角来看,领证让她觉得天津这个城市将会进一步给予她价值和存在感。

但在我看来,领证其实不会令我们的生活有太多的不同。领证并不会改变我们之间相处的模式、领证也并不会改变我们的生活状态。今天晚上该吃小鸡炖蘑菇,依然还是小鸡炖蘑菇。

另一个视角来看,领证也带来了我们生活中的不同。我们的心态开始发生变化,我们不再是一个独立的个体。我们开始有了一个新的身份:Some one‘s 老公 / 老婆。我们不再只为自己。我们可以成为彼此在医院需要签署病危通知书时的家属。

但无论如何,生活总是在向更好的方向演进下去。太阳照常升起,生活总是要日常的过下去。

一个人的永续职业

fountain pen on black lined paper

每个人在这个社会上都有一个属于自己的职业,这个职业成为别人认知我们的标签。

在过去的几十年里,我们看到,一个人可以从刚毕业做一份工作,一直做到退休。但在如今快节奏的时代当中,我们往往看到的是一个人加入一家公司,工作几年后,又离开这家公司,到一家新的公司去。

我们很难有一个固定的工作和职业来描述自己 —— 你到底是什么职业?

职业带来的收入是其次,关键是职业会给人不同的认知,以及职业是一种身份认同,我们需要依靠身份认同,在社会当中找到自己的位置。

那到底有没有什么职业是可以永续存在的?

对我而言,有三:

  1. 写作者:写作是一个相对稀缺的能力,且随着 AIGC 的出现,越来越多的人不擅长写作。写作的价值反而在提升。且写作对我来说,是一个持续表达的事情,无论如何,我都将持续写作下去。
  2. 创造者:我之前就说过,软件工程师是这个时代的手艺人,你可以使用自己的技能,来创造出一些新鲜的产品出来,去解决别人的问题,打造不同的产品和生态。创造者与你的年龄,工作时间无关,只与你自己是否还愿意创造有关。
  3. 投资者:投资只关乎你买入的资产,并跟随你的资产不断成长。与你的年龄,工作时间等都无关系。任何人都可以成为投资者,也可以一辈子成为投资者。

使用飞书消息卡片变量功能,批量数据快速录入消息卡片

在开发短链助手的时候,我需要实现一个查看当前用户创建的所有短链接的能力。这个依然希望通过消息卡片来完成。而作为一个 JSON,想要构建一套合适的内容,就变得十分的麻烦和复杂。

解构消息卡片

我要发送的消息卡片当中,可以区分为动态内容和静态内容,对于静态内容,我可能长期都不会变化,而静态内容,则会根据用户的数据发生变化。

如果整体都放在代码中生成,我就需要有一段又臭又长的代码来维护其中的变化的 JSON ,而我希望整个代码的简洁,不要有比较长的代码只是用来生成卡片的逻辑,所以就用上了消息卡片的新功能:循环对象数组。

而进一步看动态内容,则我们可以将其视为是变量 A 和变量 B 在不断的被重复赋予,最终形成了一行一行的结果。

而我们想要实现这样功能。首先,需要在卡片搭建工具中创建一个循环对象数组,并将其绑定在一个「多列布局」上。

绑定完成后,你的多列布局就有了被循环的可能性。

接下来你需要在多列布局中去构建你的每一行的结果,并在对应的位置绑定上变量,比如我这里就给多列布局防止了一个 Markdown 文本组件,并在这个文本组件中,填入了 ${source} 作为变量 A 进行填充。

当你根据你的需要,构建出需要的卡片结构后,点击右上角的保存并发布,就可以准备写代码来实现批量发送数据的逻辑了。

代码片段

这里的逻辑不复杂,首先需要从数据库中提取出需要用用作列表循环的数据,这里以 data.data 为例,data.data 是一个包含了 Object 的 Array,其中每一个 Object 都有 Postfix 和 Link 两个字段。这两个字段就是我们稍后要塞在卡片中的。

       // data.data = [{Postfix:"a",Link:"https://amazon.cn"}]
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })

最终我们构建出来,发给飞书服务器的 JSON 其实是这样子的,这段 JSON 就会和我们在卡片搭建工具中构建的 JSON 租和,自动进行拼接,从而实现我们想要的循环效果。

{
  "type": "template",
  "data": {
    "template_id": "ctp_AAmFBm5vnHfs",
    "template_variable": {
      "CONTENT": [
        {
          "source":"a",
          "target":"https://amazon.cn"
        },
        {
          "source":"b",
          "target":"https://baidu.com"
        }
      ]
    }
  }
}

文章中构建的出的卡片

构建出的卡片 JSON 是这样的,方便你参考:

{
  "elements": [
    {
      "tag": "markdown",
      "content": "你创建的链接如下:"
    },
    {
      "tag": "column_set",
      "flex_mode": "none",
      "background_style": "grey",
      "columns": [
        {
          "tag": "column",
          "width": "weighted",
          "weight": 1,
          "vertical_align": "top",
          "elements": [
            {
              "tag": "div",
              "text": {
                "content": "${source}",
                "tag": "lark_md"
              }
            }
          ]
        },
        {
          "tag": "column",
          "width": "weighted",
          "weight": 4,
          "vertical_align": "top",
          "elements": [
            {
              "tag": "div",
              "text": {
                "content": "${target}",
                "tag": "lark_md"
              }
            }
          ]
        }
      ],
      "_varloop": "${CONTENT}"
    }
  ],
  "header": {
    "template": "turquoise",
    "title": {
      "content": "链接清单",
      "tag": "plain_text"
    }
  },
  "card_link": {
    "url": "",
    "pc_url": "",
    "android_url": "",
    "ios_url": ""
  }
}

完整代码参考

import cloud from '@lafjs/cloud'
import axios from 'axios'

let appid = "";
let secret = ""

const lark = require('@larksuiteoapi/node-sdk');

const client = new lark.Client({
  appId: appid,
  appSecret: secret
});


export default async function (ctx: FunctionContext) {

  console.log("event",ctx.body);

  if (ctx.body.challenge) {
    return ctx.body
  }

  if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {
    if (ctx.body.action.name != "submit") return { code: 1 };
    try {
      // function to create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }

  if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {
    // 处理按钮
    if (ctx.body.event.event_key == "help") {
      try {
        let content = JSON.stringify({
          template_id: "ctp_AAmFBFOpYX0S"
        });
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S",
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "mylink") {
      try {
        // function to get all my link 
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: "{\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"创建短链接\",\"tag\":\"plain_text\"}},\"elements\":[{\"tag\":\"form\",\"name\":\"form_1\",\"elements\":[{\"tag\":\"input\",\"name\":\"postfix\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀\"},\"max_length\":10,\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"tag\":\"input\",\"name\":\"link\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接\"},\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"action_type\":\"form_submit\",\"name\":\"submit\",\"tag\":\"button\",\"text\":{\"content\":\"提交\",\"tag\":\"lark_md\"},\"type\":\"primary\",\"confirm\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"创建短链接\"},\"text\":{\"tag\":\"plain_text\",\"content\":\"确认提交吗\"}}}]}]}",
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }
  return { data: 'hi, laf' }
}

使用飞书消息卡片模板,减少代码硬编码 JSON

在开发短链助手时,一个很大的痛苦的点是我希望通过消息卡片来完成开发者的交互,这意味着我需要有大量的行为是和消息卡片来完成的。而消息卡片又不同于 HTML,是一个比较明确的 DSL。消息卡片更多是基于 JSON 提供的一套 Schema,将其放在代码中管理也是一个非常麻烦的事情。

好在最近飞书开放平台迭代了消息卡片模板的功能,我可以不用把 JSON 存在代码中,而是只在代码中存一个 Template ID ,从而降低我在代码中维护这段 JSON 的难度。

在卡片构建工具中新建卡片

首先,你需要打开消息卡片搭建工具,并在其中创建一个新的卡片(你可以使用其提供的卡片组的能力,来管理你的卡片们)。比如我就要这个卡片组来管理短链助手和其他场景的卡片。

创建卡片完成后,你可以在 UI 上点击保存并发布,你就将你的卡片消息模板发布到了飞书的服务器。

此时,你就可以在代码中使用了。点击页面中间的 ID,复制消息卡片模板 ID,将你的调用代码替换为对应的逻辑即可。

使用模板需要注意,将消息卡片中的 Content 从过去的卡片内容,替换为 template 的 JSON。比如,使用卡片 JSON 发送的时候,我们发送的数据可能是这样的:

{
    "receive_id": "oc_820faa21d7ed275b53d1727a0feaa917",
    "content": "{\"config\":{\"wide_screen_mode\":true},\"elements\":[{\"alt\":{\"content\":\"\",\"tag\":\"plain_text\"},\"img_key\":\"img_7ea74629-9191-4176-998c-2e603c9c5e8g\",\"tag\":\"img\"},{\"tag\":\"div\",\"text\":{\"content\":\"你是否曾因为一本书而产生心灵共振,开始感悟人生?\\n你有哪些想极力推荐给他人的珍藏书单?\\n\\n加入 **4·23 飞书读书节**,分享你的**挚爱书单**及**读书笔记**,**赢取千元读书礼**!\\n\\n📬 填写问卷,晒出你的珍藏好书\\n😍 想知道其他人都推荐了哪些好书?马上[入群围观](https://open.feishu.cn/)\\n📝 用[读书笔记模板](https://open.feishu.cn/)(桌面端打开),记录你的心得体会\\n🙌 更有惊喜特邀嘉宾 4月12日起带你共读\",\"tag\":\"lark_md\"}},{\"actions\":[{\"tag\":\"button\",\"text\":{\"content\":\"立即推荐好书\",\"tag\":\"plain_text\"},\"type\":\"primary\",\"url\":\"https://open.feishu.cn/\"},{\"tag\":\"button\",\"text\":{\"content\":\"查看活动指南\",\"tag\":\"plain_text\"},\"type\":\"default\",\"url\":\"https://open.feishu.cn/\"}],\"tag\":\"action\"}],\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"📚晒挚爱好书,赢读书礼金\",\"tag\":\"plain_text\"}}}",
    "msg_type": "interactive"
}

而在使用模板时,我们只需要很短的内容就可以:

{
      "receive_id": "ou_7d8a6exxxxccs",
      "msg_type": "interactive",
      "content": "{\"type\": \"template\", \"data\": { \"template_id\": \"ctp_xxxxxxxxxxxx\", \"template_variable\": {\"article_title\": \"这是文章标题内容\"} } }"
  }

这样你就可以把过去又臭又长的 JSON 变为一个简短小巧的 Template ID 来完成。

一些 Tips

在使用模板时,如果你的模板比较多,那么管理这些模板会比较成问题,一个比较好的办法是你可以考虑把 template ID 的编辑链接放在你的代码注释里,这样当你需要编辑 JSON 的时候,只需要点击代码中的链接就可以跳过来编辑了。

       client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S", // https://open.feishu.cn/tool/cardbuilder?templateId=ctp_AAmFBFOpYX0S
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })

如何巧用飞书消息卡片输入框实现一套业务交互逻辑

飞书开放平台最近开始内测了输入框的能力,基于输入框,为消息卡片提供了进一步业务系统打通的可能性,你可以不需要开发一整个网页应用,只需要借助飞书机器人和飞书消息卡片,就可以实现一套业务交互逻辑。

流程图示意

目标说明

这里首先确定要实现的逻辑:这里我要做的是一个短链接应用,功能很简单,点击下方的机器人菜单,并在弹出的窗口中输入对应的短链接后缀和要跳转的链接,点击确定就会帮我创建一个短链接。

具体效果如下:

如果后缀已经被占用,则展示如下内容:

在实现这个功能时,我首先使用了飞书提供的输入框组件的能力和表单组件能力,来实现整个业务交互,当然,你也可以根据业务形态,来选择合适的组件,构成一整个输入表单。

实现逻辑

整体的功能可以分为三步:

  1. 点击按钮:机器人需要响应点击事件,并发送一个带有输入框的消息卡片。
  2. 验证卡片输入内容:消息卡片中提供了输入框,但是用户的输入是否我们能用,需要设计一些验证的能力。
  3. 反馈用户是否创建成功:当我们创建成功后,需要给开发者提示,告诉他是否已经创建成功,帮助他结束整个流程。

接下来就是具体的实现步骤了。

点击按钮并回复卡片

首先,我先是使用了机器人的菜单功能,来实现在机器人底部配置菜单。你需要访问飞书开发者后台,找到机器人能力中的「机器人自定义菜单」,就可以配置一个机器人的自定义菜单了。机器人菜单支持跳转到指定链接,或者是推送事件,我选择推送事件,这样我就可以在服务端响应用户的创建的行为。这里我设定了事件内容为 create ,便于后续处理。

机器人菜单的处理则可以参考机器人菜单使用说明,通过订阅「机器人自定义事件」来完成对于相应行为的接受和对应的处理。

这部分的处理逻辑可以参考如下代码

// 判断请求体当中是否有 header 字段 && 来源的事件是否是机器人菜单
if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {

    // 请求的事件是否是创建短链接对应的事件。
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id, // 从事件体中提取事件的触发人
            msg_type: 'interactive',
            content: "", // 推送卡片 JSON
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }

对卡片输入内容进行校验

在完成卡片响应的设定后,接下来我实现的是校验的逻辑,这里分为两层:第一层是客户端可以完成的校验:比如短链接应该少于 10 个字符。第二层是只有客户端才能完成的校验。

1. 在本地校验文件长短

如果每次发起请求都需要发送到服务端进行校验,则有比较高的校验成本。好在消息卡片提供了本地校验的能力,你可以通过 max_length 字段来验证输入框长短.

这里我是使用输入框组件的字段,来验证输入的内容长度不得大于 10 。

2. 输入两个参数才发起请求

在消息卡片的输入框组件中,只要输入内容就会发现校验,因此我不能直接使用输入框组件,而是需要借助 form 组件,来实现用户输入两个内容再手动发起提交。则具体我构建的卡片 JSON 是这样的。

{
  "header": {
    "template": "turquoise",
    "title": {
      "content": "创建短链接",
      "tag": "plain_text"
    }
  },
  "elements": [
    {
      "tag": "form",
      "name": "form_1",
      "elements": [
        {
          "tag": "input",
          "name": "postfix",
          "placeholder": {
            "tag": "plain_text",
            "content": "请输入后缀"
          },
          "max_length": 10,
          "label": {
            "tag": "plain_text",
            "content": "请输入后缀:"
          },
          "label_position": "left",
          "value": {
            "k": "v"
          }
        },
        {
          "tag": "input",
          "name": "link",
          "placeholder": {
            "tag": "plain_text",
            "content": "请输入要跳转链接"
          },
          "label": {
            "tag": "plain_text",
            "content": "请输入要跳转链接:"
          },
          "label_position": "left",
          "value": {
            "k": "v"
          }
        },
        {
          "action_type": "form_submit",
          "name": "submit",
          "tag": "button",
          "text": {
            "content": "提交",
            "tag": "lark_md"
          },
          "type": "primary",
          "confirm": {
            "title": {
              "tag": "plain_text",
              "content": "创建短链接"
            },
            "text": {
              "tag": "plain_text",
              "content": "确认提交吗"
            }
          }
        }
      ]
    }
  ]
}

这部分的关键是用 form 组件包裹 Input 组件,从而规避了 Input 组件输入内容就会发送到服务端校验的问题。

3. 在服务端验证有无

这部分逻辑我在实现的时候相对简单,没有专门去进行校验(主要是因为我的短链接服务和机器人是两个不同的服务),而是通过短链服务返回 200 还是 401 来判断是否出现了重复的问题,所以这里只是简单的使用了一个 try catch 来完成校验。

需要注意的是,这里你会注意到,返回是直接返回了一段 JSON String,这是因为触发这个事件是通过消息卡片的回调能力,如果你在消息卡片的回调能力返回一个 JSON,就会直接把 UI 层面的卡片渲染为你返回的卡片结果。靠着这个功能,我来实现的成功与失败返回不同的内容。

if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {    
    try {
      // create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }

完整代码参考

整个机器人的部分的代码只有 170 余行,不多,供你参考

import cloud from '@lafjs/cloud'
import axios from 'axios'

let appid = "";
let secret = ""

const lark = require('@larksuiteoapi/node-sdk');

const client = new lark.Client({
  appId: appid,
  appSecret: secret
});


export default async function (ctx: FunctionContext) {

  console.log("event",ctx.body);

  if (ctx.body.challenge) {
    return ctx.body
  }

  if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {
    if (ctx.body.action.name != "submit") return { code: 1 };
    try {
      // function to create link

      if (status == 200) {
        return JSON.stringify({
          "type": "template",
          "data": {
            "template_id": "ctp_AAmFBm5vnlt0",
            "template_variable": {
              "source": ctx.body.action.form_value.postfix,
              "target": ctx.body.action.form_value.link
            }
          }
        })
      }
      return {};
    } catch (e) {
      return JSON.stringify({
        "type": "template",
        "data": {
          "template_id": "ctp_AAmFBm5vZYuo",
          "template_variable": {
            "POSTFIX": ctx.body.action.form_value.postfix
          }
        }
      });
    }
  }

  if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {
    // 处理按钮
    if (ctx.body.event.event_key == "help") {
      try {
        let content = JSON.stringify({
          template_id: "ctp_AAmFBFOpYX0S"
        });
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBFOpYX0S",
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "mylink") {
      try {
        // function to get all my link 
        let links = data.data.map(item => {
          return {
            source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
            target: item.Link
          }
        })
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnHfs",
                "template_variable": {
                  "CONTENT": links
                }
              }
            }),
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
    if (ctx.body.event.event_key == "create") {
      try {
        await client.request({
          method: "POST",
          url: "https://open.feishu.cn/open-apis/im/v1/messages",
          data: {
            receive_id: ctx.body.event.operator.operator_id.open_id,
            msg_type: 'interactive',
            content: "{\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"创建短链接\",\"tag\":\"plain_text\"}},\"elements\":[{\"tag\":\"form\",\"name\":\"form_1\",\"elements\":[{\"tag\":\"input\",\"name\":\"postfix\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀\"},\"max_length\":10,\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"tag\":\"input\",\"name\":\"link\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接\"},\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"action_type\":\"form_submit\",\"name\":\"submit\",\"tag\":\"button\",\"text\":{\"content\":\"提交\",\"tag\":\"lark_md\"},\"type\":\"primary\",\"confirm\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"创建短链接\"},\"text\":{\"tag\":\"plain_text\",\"content\":\"确认提交吗\"}}}]}]}",
          },
          params: {
            receive_id_type: 'open_id',
          },
        })
        return {};
      } catch (e) {
        console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
        return {};
      }
    }
  }
  return { data: 'hi, laf' }
}

使用 fresh 来提升你的 Golang 开发效率

Golang 作为一个编译型语言,在编写程序时,一个不太方便的点便是每次修改完代码,都需要重新编译才能测试效果。虽然你可以使用 go run main.go 命令来运行一个 go 文件,但由于项目往往文件比较多、修改时还是需要手动输入命令比较麻烦,所以给 Golang 的开发过程带来了不少的问题。

fresh 就是一个帮助你执行一些重复命令的命令行工具,有了 fresh ,你就可以不用自己手动执行 go run main.go,它在检测到文件发上了变化后,会自动帮你中断掉当前进程,并重新执行命令,帮你实现 live-reload 的效果。

fresh 的执行效果

https://github.com/gravityblast/fresh

安装

安装比较简单,执行 如下命令后,你就可以在任何地方执行 fresh 命令了。

go get github.com/pilu/fresh

执行

当你当前目录有 main.go 文件时,直接执行 fresh 就会自动执行 main.go 文件。不过,如果你想要自定义的话,也可以通过配置文件来完成。创建一个 sample.conf 文件,贴入如下配置,并执行 fresh -c sample.conf 就可以让 fresh 按照你的配置来执行命令。

root:              .
tmp_path:          ./tmp
build_name:        runner-build
build_log:         runner-build-errors.log
valid_ext:         .go, .tpl, .tmpl, .html
no_rebuild_ext:    .tpl, .tmpl, .html
ignored:           assets, tmp
build_delay:       600
colors:            1
log_color_main:    cyan
log_color_build:   yellow
log_color_runner:  green
log_color_watcher: magenta
log_color_app:<a href="https://github.com/gravityblast/fresh#usage"></a>

《高效邮件工作法》书摘

  • 高效能人士最常考虑的就是工作的“主导权”。
  • 高效能人士不喜欢无用功。这里所说的无用功,指的是原本不用做,却因为没有安排好而不得不做的所有工作。
  • 与其排出优先级,不如省下时间着手处理眼前的工作,反而更能迅速完成工作。
  • 例如,你打开的第一封邮件的内容是“请修改网站资料”,大约需要10分钟的时间。这时,如果你手头刚好有别的工作,可能会回复“收到,我会在××点之前处理,请您稍等”。但是高效能人士会先处理这件事。不是说回复“××点之前处理”不对,只是“之后有时间了再处理”这种想法,是拖延的思维,有些危险。从尽快完成工作的角度考虑,了解邮件内容后立刻着手处理更有效率。
  • 其实如果在“3W”的基础上再增加一些信息,邮件的效果会更好。这就是“6W+3H”。先在“3W”上加上“3W”: When……什么时间 Where……什么地点 Whom……对象是谁 再加上“3H”: How to……怎么做 How many……多少 How much……花费多少 利用这些来整理想要传达的信息吧。
  • 总之,回复邮件并不是越快越好,最佳的回复时机要结合“目的”来考虑。
  • 发送的邮件完美无瑕,这是我们的理想。但是如果因为追求完美而错过对方期望收到邮件的时间,这封邮件就失去了它的价值。我认为我们更应该重视速度,为了“目的”,牺牲一点“正确”也无可厚非。这么一想,写邮件时是否会更放松一些?
  • 因此,避免连词的过度使用,是缩短邮件的一个方法。有人觉得连词是邮件不可或缺的,但是即使没有连词,邮件内容也是流畅的。所以果断删除那些不必要的连词吧,上下文的连贯性并不会受到什么影响。
  • 其实想要尽快收到回复并不难。只要根据前文所列的看上去有些棘手的邮件的共同点,反其道而行之即可。具体来说,就是要使邮件内容易读懂、易判断、需求明确。此外,要明确地列举出回复的好处(更进一步说,最好再加上不回复的坏处)。
  • 高效能人士的语感十分敏锐,他们不仅可以准确地传达信息,还会有意识地考虑如何给对方留下好印象。因为就算你传达的信息无误,如果对方对你没有好印象,也不会心情愉悦地与你合作,反而可能拖延工作。要想提高工作效率,仅靠自己的努力是不够的,只有使对方心情愉快地与你合作,才能加快工作进程。
  • 高效能人士很少使用消极的语言,即便在传达消极的内容时,也会尽量使用积极的表达方式。例如,他们会将“按时集合不要迟到”换成“请您预留足够的时间,及时到达集合地点”。再比如,他们会将“请不要随意修改数据”改为“如果需要修改数据,请与负责人联系”。这样一来,不仅不会惹怒对方,还可以使其欣然采取我们所期望的行动。
  • 转换说法时,需要注意的要点很多,其中最关键的就是不要为自己的行为道歉,而要感谢对方的行为。比起“没能接到您的电话,非常抱歉”,“感谢您的来电”更能给人留下好印象。
  • 邮件只能用文字传达信息,因此,要避免对方将信息理解错误。如果遇到信息还没有完全确认就需要发邮件的情况,在前文的案例中,不要写“我认为付款时间是下月末”,而该写成“付款时间是下月末,我向会计确认之后再与您联络”。 当然,对于很多推测、预测、无法断言的内容,如不便断言的战略等,传达前需要事先表明“我预测”、“我个人认为”,或者“我确认一下细节再……”,这样文中就不用反复使用“我认为/我想”的表述了。
  • 话虽如此,胡乱打听却是不智的。这种时候,请使用“在可能的范围内”、“如果方便的话”等表述。
  • 大项目的运行必须事先设定“提前确定信息共享对象”、“工作汇报另行发送邮件”等基本规则。
  • 适合通过电话沟通的情况有: •可以当场回复的事情。 •紧急的事情。 •需要边解说边确认对方的理解程度的事情。 •因为存在感性因素,仅用文字表达容易引起纠纷的事情。 即使对方已经发来邮件,如果你认为电话沟通更快捷或更不容易引起误解,都可以主动进行电话沟通。

Eye Monitor 沉浸式工作打断神器

man covering face with both hands while sitting on bench

写代码最常见的问题是容易太沉浸,一写就是好几个小时不挪窝。 最近发现了一个应用: Eye Monitor ,可以很好的解决沉浸式写代码导致的久坐问题。

它和番茄钟有什么不同?

番茄钟在很多场景下, 也都是实现打断的功效,强制你在一个特定的时间范围内做事 / 休息。但和大多数番茄钟不同的是,这个应用实现了一些有意思的活动算法,来帮你计算时间。而番茄钟则是明确的按时间卡住,不论这段时间你是否有操作,时间到了就是到了。

实现的效果

Eye Monitor 实现的效果就是当你的“疲劳度”达到了 100%,就会给你全屏幕弹窗,强制让你休息。虽然你也可以继续跳过,但跳过本身并不会降低你的疲劳度,只是让你可以短时间的休息一下,然后再次提醒你休息。这个设计是有点意思的。

比如,这是我的休息统计,可以看到疲劳值会增加,中间如果你有休息,疲劳值则会降低,再次开始 Coding 则会再次提交。除了可以用来提醒自己休息,还可以不定时的看看自己什么时候在 Happy Hacking,什么时候在摸鱼~

使用 Find Duplicates 插件清理 Calibre 书库

two women facing security camera above mounted on structure

我在导入 Kindle 的图书时,错误的导入了两遍,导致我的 Calibre 仓库快速膨胀,因此,我希望清理掉其中重复的书籍,减少二次存储。

清理使用的是 Find Duplicates 这个插件

安装

在 Calibre 的「首选项」-「插件安装」页面,搜索 Find Duplicates,就可以找到这个插件,双击安装,并重新启动,即可使用该插件。

如果你默认选择的是在主菜单展示工具,则会像我一样,在页面顶部有一个新的入口

使用

使用非常简单,点击按钮,进入搜索页面,可以配置对比的元素,可以是二进制对比(更精准)或者是 基于某个特定的标签对比(比如 ISBN),也可以使用默认的标题作者对比。根据你的喜好,你可以选择合适的方式进行对比。

在下方的结果输出中,可以选择“Show All Groups at once with highlighting”,这样可以在一个列表中快速看到所有需要清理的图书,快速删除,达成目标。

点击确定,执行后的结果就是这样子的了,你只需要在重复的两本书中,选择你要删除的那一本即可。