作者归档:白宦成

关于白宦成

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

d2b5ca33bd970f64a6301fa75ae2eb22 27

《憨夺型投资者》书摘

  • 要知道,这是一个业务模式非常稳定的生意,长期以来,现金流和利润率已经经过了市场和时间的验证,这不复杂。
  • 他努力工作、赚钱养家,坚持储蓄,然后用所有的积蓄投入到一桩只赚不赔的生意里。
  • 他的从商经历属于典型的“少投注、投大注、只挑最好的投”。这也体现了低风险、高回报投资的特点:情况好,赢得多;情况不好,输得少。
  • 如果布兰森可以在几乎没有额外资本投入的前提下,投资建立维珍大西洋航空公司,那么你也肯定能够在你选定的行业中用最少的资本开始创业。你所需要的就是用一流的创意和妥善的方案来弥补资本不足的缺憾。
  • 情况好,赢得多;情况不好,输得少。
  • 如果你简单地用一下马尔瓦公式来衡量,就会发现,投资前你一定要弄明白两件事情: ·迅速拒绝你面前的投资要求; ·从最低额度的投资开始,经过几十年的发展,你会变得非常有钱。 该说的都说了。
  • 帕特尔老爹和巴菲特的出身和创业背景差异巨大,但却最终得出了相同的结论:投资变化缓慢行业中的经营模式简单的企业。
  • 确定投资对象的关键问题不是评估行业对社会的影响力或者判断行业的增长前景,而是要确定任何给定公司的竞争优势,最重要的是这种竞争优势是可持续性的。那些具有持久竞争优势的产品和服务能给投资者带来更加丰厚的回报。
  • 对我们而言,投资行为就好比我们去和赌马彩金下注系统展开博弈。我们下注是希望有一半的概率获胜,从而赢得3倍的回报。你要寻找的就是有明显代价差异的赌博。这就是投资行为本身的含义。你得清楚赌博概率和相应的回报要远远高于你的赔率。这就是价值投资。
  • 低风险和高不确定性对投资者而言是最佳拍档。
  • 这就是憨夺型投资模式的框架,具体如下: (1)投资现有的业务 (2)收购变化缓慢的行业中经营模式简单的企业 (3)在不景气的行业里对经营不善的企业进行抄底投资 (4)投资具有持久竞争优势的行业 (5)看准有利时机投大注 (6)注重套利 (7)买入以远低于内在价值折价出售的企业 (8)寻找风险低、不确定性高的业务
  • (9)模仿好过创新
  • 拥有某些企业的所有权是创造财富最好的办法。无须你持续投入时间和精力,反而还有最广泛的选择权,摩擦成本很低,购买一些上市企业的股票,显然是低风险、高回报的憨夺型投资模式。
  • 对付这种两难局面的憨夺型投资模式非常简单:只投资简单的企业,按照保守估算,这些企业未来的现金流很容易预测。什么样的企业经营模式较为简单?仁者见仁,智者见智。
  • 简约是非常有利的竞争优势。亨利·梭罗(Henry Thoreau)早早就意识到这一点,他曾说:“我们的生活因为太多的琐事而纷扰不断……简单点,再简单点。”
  • 为了打赢这场心理战,最有效的武器就是买进经营模式简单的企业,我会说服自己为何这样做赚钱的概率更高,一下子赔很多钱的概率会很低。我甚至把整个论证过程写下来。如果这个论证过程一个自然段都写不完,那么这笔投资就会出现问题。如果要我打开Excel表格,进行一连串数字的验证,那么这绝对是对我的投资想法亮红灯的时候。
  • 在正确地认定市场经常有效后,(学术界和华尔街的投资人士)错误地得出市场永远都是有效的结论。市场经常有效和永远有效之间有着天壤之别。
  • 市场不可能完全有效,因为人类控制着买卖关系确定的定价机制。人类会在极端恐惧和极端贪婪这两种情绪之间摇摆不定。当集体极端恐惧出现时,资产的定价就会低于其内在价值;当集体极端贪婪出现时,资产的定价就会高于其内在价值。
  • 我们如何确定哪些企业和行业处于低潮?有很多种方法,我们在这里先介绍六种。 (1)如果你每天都有阅读财经新闻的习惯,你会发现很多有关上市企业的信息。很多这些新闻报道反映了特定行业或者企业的负面信息。
  • (2)《价值线》(Value Line)每周都会发布一期过去13个星期里股票价格跌幅最大的简讯,名列其中的股票可谓是低迷公司的集锦,每次发布40家公司的名单,这些公司在短短3个月的时间里经历了从20%~70%不等的股价跌幅,跌幅最大的企业很有可能就是最不景气的企业,同时发布的信息还包括同期市盈率最低、面值贬值最大、收益最高的企业名录等。并非所有榜上有名的企业都病入膏肓,但是如果企业交易的市盈率达到3,那么它就值得我们进一步分析和观察。
  • (3)《投资组合播报》(Portfolio Reports)(见www.portfolioreports.com)每月会出一份报告,其中披露了80位最负盛名的价值经理最青睐的十大股票名称。该份报告的信息源是各种不同依法需要公开的机构投资者公报等。《
  • (4)如果你不想出钱订阅《投资组合播报》,你可以直接通过查阅机构投资者发布的公报,比如《美国证监会表13-F》(SEC Form 13-F)。
  • (6)最后,你还可以阅读乔尔·格林布拉特撰写的《赢得市场手册》(The Little Book That Beats the Market)。
  • 投资者会竭尽全力利用一切商机来获得超额的利润。令人讽刺的事情是在追逐最佳利润的同时,他们也扼杀了产生超额利润的源泉。
  • 我们如何知道一家企业有其隐性的竞争优势,如何确定这些竞争优势呢?通过查阅其财务报表,通常能够看出点端倪。拥有持久竞争优势的企业,就像那个在C镇上开理发店的理发师一样,能够从投资的资本中获得较高的回报。资产负债表能够告诉我们企业使用的资本。收益和现金流量表能够表明这些资本带来的回报。
  • 我们假设你可以用1美元去投注,你可以得到下面几种回报的可能。 ·80%的概率,赚21美元; ·10%的概率,赚7.5美元; ·10%的概率,赔掉所有。
  • 平均收益/最佳收益=每次应该投注的金额
  • 威廉·庞德斯通(William Poundstone)曾写过一本好书,名为《财富公式》(Fortune’s Formula)1,很值得一读。庞德斯通精辟地描述了凯利公式的内涵。美盛集团(Legg Mason)的迈克尔·莫布森(Michael Mauboussin)写了一篇论文2来阐明凯利公式。他这样说,如果你掷硬币,正面朝上,你能获得2美元;正面朝下,需要花费1美元。如果你碰到这样的情况,你该拿多少钱来投注?
  • 明智的投资人会在出现有利于自身的投资机会的时候,下大注。他们有了较好的回报期望时,他们就会下大注。其他时间,他们都不会出手。就这么简单。
  • 在投资中,永远没有稳赚一说。就算是当今最有潜力的蓝筹股,明天也有衰落的时候。投资就看回报,就像玩21点一样。
  • 同理,如果你投资了某只价位过高或者过低的股票,最终股价会回归到与其内在价值差不多的水平,从中投资者可以获利或者失利。我们可以把它视为投资的黄金规律,并充分尊重这一规律。因此,如果我们能确定某个企业在未来两三年的内在价值水平,且能以比较低的价位折价购买,我们的投资利润就能保证。在确定具体下注的数额时,凯利公式非常有用。
  • 憨夺型投资方式的关键就是少投注、投大注、看准时机投注。凯利公式也支持这种方法。这种方法在股票市场中进行被动投资非常管用。最后,正如查理·芒格经常所说的:“逆向投资,得反其道而行。”
  • 凯利公式会告诉我们投资的上限比例,我们就能比较妥善地把握实现财富目标的最佳时机。想要尽快致富,还没有赔光的风险,憨夺型投资是最好的办法。如果你投资的比例超过凯利公式提示的结果,可以肯定的是,如果你不断重复投资,你肯定会把自己的资产输得一分不剩。
  • 投资和赌博一样,主要是看有怎样的回报。看准投资成本和风险偏低、回报偏高的投资机遇,集中下注,这是创造财富的关键。首先要用凯利公式来确定投资额占净资产额的上限。由于在股权市场中有多项下注机会,凯利公式无法解决的投资波动问题可以通过集中投资组合缓解。
  • 由于在股权市场中有多项下注机会,凯利公式无法解决的投资波动问题可以通过集中投资组合缓解。
  • 这两个例子里,企业都建立了一个品牌,拥有稳定的客源和收入。即使最初的套利利差小时,企业依然可以依靠品牌的力量继续存在发展。然而,未来几十年里,企业的增长率要快过总体经济增长率,将变得很难。
  • 创业者发现现有的套利机会,由此创办伟大的企业,套利机会可以成为推动创业的动力。
  • 巴菲特先生特别擅长投资具有持久竞争优势和套利利差的企业。尽管如此,即使在伯克希尔公司,有些绝佳的企业也丧失了它们的竞争优势,比如蓝筹印花公司(Blue Chip Stamps)和世界图书出版公司(World Book)。
  • 我们知道所有憨夺型套利利差最终都会消失。这里的关键问题是:套利利差持续的时间和竞争优势的可持续性。正如巴菲特先生所说的: 投资的关键不是评估一个行业如何影响社会,或者其增长的速度,而是确定公司的竞争优势是什么,最重要的是这种竞争优势的可持续性。
  • 所有的竞争优势最终会消失。即使看似能长久的憨夺型套利利差也会消失,但这并不意味着我们就不投资,或者投资无法得到较好的回报了。我们需要明白竞争优势持续的时间是10个月还是10年。套利利差越大越好,持续的时间越久越好。憨夺型和传统型套利交易的区别主要在于套利利差的持久性和具体的价差。
  • 一定要寻找各种套利机会。通过套利,你能在毫无风险的情况下获得较高的投资回报。请充分利用这种低风险、高回报的套利交易的利差。
  • 格雷厄姆对于安全边际的痴迷是可以理解的。尽可能降低失利的风险,同时提高盈利的概率是非常有效的手段。正因为如此,巴菲特先生的净资产超过了400亿美元。他就是用追求风险最低、回报最大的投资方法来积累现在的财富的。通常情况下,资产的交易价格等于或者高于其内在价值。这里的关键是耐心等待,等到资产的市场价位跌到远远低于内在价值的水平。
  • 对我而言,任何有关技术的投资,只要行业性质不可预计或者变化很快,5秒钟不到,我就会立马回绝。
  • 快速变化的行业是投资的天敌,这就是为什么憨夺型创业者只关注那些长远来看变化不大的行业。
  • 微软公司对于外部发生的创新反应迅猛,都能以迅雷不及掩耳的速度消除潜在的危险。他们在投资创新行动前,会先弄清楚客户对别家公司创新成果的认可程度。这是非常有效的战略。一位曾经在微软公司工作的管理人员告诉我,微软一旦设定了明确的目标,就能出成果。公司仿制Netware或者Lotus1-2-3时,对产品的外观或者收入有明确的规定。这就是明确树立、实现目标的典范。
  • 而每次微软公司试图引领行业潮流或者创新的时候,就会状况百出。它所倡导的“.Net”项目,计划一直不清楚,开展多年也没有什么进展。微软的Vista操作系统应该是具有革命意义的产品,要是它赶不上苹果当前的产品,我也不会感到惊讶。
  • 微软擅长模仿和业务开拓。在跟踪敌方产品、消除潜在威胁方面,成功率高达90%。谷歌与微软的战斗最终结果如何,我们无法预测。微软公司现在有6万多名员工,它虽然一向反对官僚作风,但是这样庞大的机构也不免落入俗套。如果现在我只有两个选择,要么投资谷歌,要么投资微软,我会毫不犹豫地选择微软。这是一场创新和模仿者之间的竞争。优秀的模仿者能够成就长存的事业;而创新充满了变数,克隆是确定的。
  • 实际上,如果我们在两年后以高于40万美元的价格出售这个加油站,这样的回报比我们持有10年,并在10年后以100万美元的价格出售而言,很难去弥补那些复利不执行带来的资本损失。虽说如此,我们依然要非常有耐心,但也不能无止境地等待下去。我的结论是:两三年是让赔本的投资者止损的最佳等待时间。
  • 有关于价值投资最好的书是乔尔·格林布拉特撰写的《赢得市场手册》。我会默认阅读本书的人已经读过《赢得市场手册》。
  • 三年后,如果投资依然不见好转,原因通常是我们对企业的内在价值或者内在价值关键的推动因素的判断失误,也有可能多年来公司的内在价值的确下降了。一旦三年的期限已过,不要犹豫承担已有的损失。吃一堑长一智。这样的损失会告诉你如何成为更好的投资者。虽然我们学习他人的教训能提高自己,但是自己体验到的教训能够最大限度地推动自己的成长。随着时间的流逝,不断地从自己的失败经验中吸取教训,你会逐渐发现你成功逃过查克拉乌约阵的概率越来越高。
  • 在击败查克拉乌约阵后,要退出来很简单。在买入股票后三年内,会出现股价和内在价值交汇的时候,这就是我们获得较高年化收益的机会。每当股价和公司内在价值的差距小于10%时,一定要赶紧卖出退场。当市场价格一旦逼近其内在价值的时候,就要赶紧卖出股票。唯一例外的情况是税收的考虑。如果你在追寻短期的回报,你应该持股,一直到实现了长期收益或者股价超过内在价值的数额足以支付额外的税金为止。
  • 世界上的很多财富都是因为集中持有某一个公司的股票而获得的。如果你了解这个企业的情况,你不需要持有很多其他公司的股票。
  • 真正的投资机遇很少见,因此一旦出现,你就要好好把握,一定要拿出你财富的一大部分去投资。
  • 有句话说得好,“少投注、投大注、看准了再投注”。当你认定机会非常有利时,你要倾尽所有,全力以赴。凯利公式会引导你到底买入持有多少股票。请以凯利公式提示的数额1/4为准。
  • 我很喜欢读斯文森写的《不落俗套的成功:最好的个人投资方法》(Unconventional Success:A fundamental Approach to Personal Investment)一书。
  • 神奇公式能提使你憨夺每美元只需用50美分支付的投资机会。我们可以大大简化投资过程,只要分析一下神奇公式提示的股票。随着时间的流逝,我们也能成为大富翁。我极力向大家推荐这种方法。这种方法很简单。你可以在小水桶里钓鱼,结果比绝大多数指数投资回报都好。
  • 憨夺型投资者只投资简单熟悉的行业。这点要求足以排除99%的投资选择。像阿周那一样,我们必须集中精力研究那些简单熟悉的企业信息,我们必须在力所能及的范围内做出正确判断,而不受那些外在的噪声干扰。在我们熟悉的范围内,阅读有关书籍、报纸杂志、公司报表和行业信息等,说不定就能碰到一些有利于我们的公司信息,如果我们觉得这个公司有投资的潜力,公司的股价会比其内在价值低很多,这就表明我们买入这只股票的时间到了。
d2b5ca33bd970f64a6301fa75ae2eb22 26

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

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

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


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

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

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

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

fountain pen on black lined paper

一个人的永续职业

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

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

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

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

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

对我而言,有三:

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

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

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

ql9bov

解构消息卡片

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

wsyp71

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

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

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

dg2nvw

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

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

e3w12l

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

代码片段

这里的逻辑不复杂,首先需要从数据库中提取出需要用用作列表循环的数据,这里以 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',
          },
        })
Code language: JavaScript (javascript)

最终我们构建出来,发给飞书服务器的 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"
        }
      ]
    }
  }
}
Code language: JSON / JSON with Comments (json)

文章中构建的出的卡片

构建出的卡片 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": ""
  }
}
Code language: JSON / JSON with Comments (json)

完整代码参考

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' }
}
Code language: JavaScript (javascript)
0c0ca4a0ac1f249860b29e295dd55260

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

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

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

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

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

7rmbt4

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

o69het

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

lv64d7

使用模板需要注意,将消息卡片中的 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"
}
Code language: JSON / JSON with Comments (json)

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

{
      "receive_id": "ou_7d8a6exxxxccs",
      "msg_type": "interactive",
      "content": "{\"type\": \"template\", \"data\": { \"template_id\": \"ctp_xxxxxxxxxxxx\", \"template_variable\": {\"article_title\": \"这是文章标题内容\"} } }"
  }
Code language: JSON / JSON with Comments (json)

这样你就可以把过去又臭又长的 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',
          },
        })
Code language: CSS (css)
0c0ca4a0ac1f249860b29e295dd55260

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

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

流程图示意

w0052z

目标说明

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

4rbg8i

具体效果如下:

dqk8d2

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

50tl60

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

实现逻辑

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

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

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

点击按钮并回复卡片

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

ogypfv

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

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

// 判断请求体当中是否有 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 {};
      }
    }
  }
Code language: JavaScript (javascript)

对卡片输入内容进行校验

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

1. 在本地校验文件长短

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

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

du1glf

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": "确认提交吗"
            }
          }
        }
      ]
    }
  ]
}
Code language: JSON / JSON with Comments (json)

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

7yro9r

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
          }
        }
      });
    }
  }
Code language: JavaScript (javascript)

完整代码参考

整个机器人的部分的代码只有 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' }
}
Code language: JavaScript (javascript)
5e54199359bbafe0ef692365a9bcffb6

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

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

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

fresh 的执行效果

v1btrb

https://github.com/gravityblast/fresh

安装

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

go get github.com/pilu/fresh
Code language: JavaScript (javascript)

执行

当你当前目录有 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>
Code language: HTML, XML (xml)
d2b5ca33bd970f64a6301fa75ae2eb22 25

《高效邮件工作法》书摘

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

Eye Monitor 沉浸式工作打断神器

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

它和番茄钟有什么不同?

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

实现的效果

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

8n7rqv

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

40muwy