月度归档:2023年08月

使用 Ruby 替代 Node.js 写一些脚本

使用 Ruby 替代 Node.js 写一些脚本

根据不同的场景,我会使用不同语言来完成功能的编写。

对于一次性、低频、对于性能要求不高的批处理场景,过去我喜欢使用 Node.js 配合 NPM 来完成。

主要的原因是:

  1. Node.js 拥有丰富的包的生态,可以让我少写很多代码。
  2. npm run 命令比较短,可以方便的构建出需要的快速参数

而最近 Node.js 脚本写的太多,比较烦了,所以考虑用 Ruby 来替代 Node.js 写一些脚本,完成一些短期项目开发。

和 Node.js 相比,Ruby 有其好处,也有其坏处。好处在于

  1. 原生同步执行,我可以不用担心和关注 Callback Hell。虽然有了 async/await 时,已经好很多了。但还是原生的更好。
  2. 可以用更加简单的语法完成脚本。毕竟脚本主要还是随时修改随时可用,简短但能用的脚本可以提升脚本的可维护性。
项目Node.jsRuby
包管理器NPMGems
执行命令npm run xxx借助 Makefile 完成
第三方包的数量
异步/同步默认异步默认同步

接下来一段时间,就拿 Ruby 来跑脚本啦!

憨夺型投资者

憨夺型投资者

上周看了《憨夺型投资者》,其中印象最为深刻的是一句话

情况好,赢得多;情况不好,输得少

对于我们绝大多数普通人来说,这个可能是最有价值的投资建议。当然, 也是最难达成的投资建议之一。

赚大钱可遇不可求,但亏小钱确实是有可能做到的,比如:

  • 给自己的投资留足安全边际。
  • 在做事时,关注你自己的利基市场 Niche。
  • 在行业低谷期买入,等行业兴盛起来时赚钱。

用书中的原话是这样的:

这就是憨夺型投资模式的框架,具体如下:

  • 投资现有的业务
  • 收购变化缓慢的行业中经营模式简单的企业
  • 在不景气的行业里对经营不善的企业进行抄底投资
  • 投资具有持久竞争优势的行业
  • 看准有利时机投大注
  • 注重套利
  • 买入以远低于内在价值折价出售的企业
  • 寻找风险低、不确定性高的业务
  • 模仿好过创新

以及,在这本书中了解到了凯利公式,有空要好好研究一下。

在我看来,在职做独立开发者也算是憨夺型投资者的一种方式,其中的成本是你自己的时间成本,但因为你有 Day Job,并不是全职投入,所以即使失败,也不至于输的很惨。你的优势,则是你的成本足够低。

插件多次加载导致的 WordPres 后台加载缓慢

插件多次加载导致的 WordPres 后台加载缓慢

WordPress Jetpack 的一个陈年 Bug – 在 wp-Options 中生成大量的数据 中,我提到,问题的根源并不是数据库,虽然在数据库中产生了大量的数据,但并没有真正意义上拖慢系统的进程, 那到底是什么拖慢了进程?

随着对系统的深入排查,我发现一个异常的事情,在进入管理后台时,出现了插件更新插件列表的情况。这个并不多见。因为管理后台的进入不应该涉及到对于插件列表的更新。此外,我发现这个查询的 SQL 巨长,且包含了大量的查询。

于是评估,这个可能才是导致系统缓慢的真正原因。熟悉 WordPress 的读者一定知道, WordPress 对于插件的加载,是通过在数据库中存储了一组插件路径,每次启动时通过这组插件路径来加载插件。这样对于 WordPress 来说,可以快速加载插件。

但在这次的异常场景中,同一个内容的插件被加载了多次,就算这个插件比较小,可能依然会导致计算时间。此外,WordPress 引入插件后,会加载对应的 Hook 、 Filter 和 Action,则可能导致同一个 Action 被频繁触发,造成额外的计算量。从而使得虽然数据库查询时间不长,但整体耗时却巨长无比。

而这个问题的处理倒是也比较简单:

  1. 对 active_plugins 进行备份,避免修改跪了。
  2. 对于 active_plugins 进行清空,此时 WordPress 会彻底不加载任何插件。
  3. 仿照之前的列表,启用所有的插件。

通过对于active_plugins的清理,将启动速度成功降低至 2 秒左右,将系统性能提升了 80 %。

WordPress Jetpack 的一个陈年 Bug – 在 wp-Options 中生成大量的数据

WordPress Jetpack 的一个陈年 Bug – 在 wp-Options 中生成大量的数据

最近在处理一个 WordPress 系统访问下降的问题时,发现了一个奇怪的现象:一个只有很少的页面的网站,数据库备份竟然足足有 9.5 GB。我当时的第一反应是:数据库性能极差导致的站点性能不好。

不过,到数据库打开后发现, 虽然有大量的条目生成,但因为no autoload,所以其实并不会被自动加载到缓存中,从而也不会让网站的性能有太多的下降。

想想也合理,数据库中包含了数十万条记录,如果都加载到内存里,可能 PHP 默认的 1024MB 的运行内存直接被打爆了,所以问题不在此。

不过,虽然问题的核心不是它,但如此海量的脏数据对于系统依然是无价值和无意义的,于是乎我便将这些脏数据删除,数据库的大小从 9.52 GB 骤降至 34.8 MB,进入到一个正常的数据库大小区间了。

删除脏数据的命令如下:

DELETE FROM wp_options WHERE option_name LIKE '%jpsq_sync-%'

相关链接

  • https://wordpress.org/support/topic/wordpress-database-error-commands-out-of-syn/
  • https://wordpress.org/support/topic/jpsq_sync-table-constantly-generated-to-the-db/
  • https://gist.github.com/bhubbard/894040fec6421f891f1f88f2c6428ef0
《大教堂与集市》书摘

《大教堂与集市》书摘

  • 他教导我:要尊重能力,要珍视和捍卫自由,特别是:昆虫才讲究技能专一。
  • 要想对世界做出实质性的改变,开源需要做到这两点:一是要让人们广泛使用开源软件;二是要让用户知道并理解这种软件开发模式能给他们带来的益处。
  • 现在第一版已经停印了,其修订版和增补版是《新黑客字典》(The New Hacker’s Dictionary),由MIT出版社于1996年出版(3rd edition,ISBN 0-262-68092-0)。
  • 1.好的软件作品,往往源自于开发者的个人需要。
  • 优秀的程序员知道写什么,卓越的程序员知道改写(和重用)什么。
  • 卓越程序员们有个很重要的特征是“建设性懒惰”,他们知道人们要的是结果而不是勤奋,而从一个部分可行的方案开始,明显要比从零开始容易得多。
  • 3.“计划好扔掉一个吧,迟早你会这么做的。”(Fred Brooks,《人月神话》第11章)
  • 或者可以这么说:在你第一次把问题解决的时候,你往往并不了解这个问题,第二次你才可能知道怎么把事情做好。所以,如果你想做对事情,至少要再做一次。 1
  • 4.如果你有正确的态度,有趣的事情自然会找到你。
  • 当你对一个程序不再感兴趣时,你最后的责任就是把它交给一个可以胜任的接棒者。
  • 6.把你的用户当成开发合作者对待,如果想让代码质量快速提升并有效排错,这是最省心的途径。
  • 不只是Emacs,还有其他一些软件产品也使用了两层架构和两级用户群,内核使用大教堂模式开发,工具箱(toolbox)使用集市模式开发,比如数据分析和可视化展现的商业化工具MATLAB就是这样,MATLAB和其他类似产品的用户们发现,创新、酝酿和行动最频繁发生的地方总是在产品的开放部分,而这部分的改进也总是由庞大而多样化的用户群完成。
  • 尽早和尽量频繁发布是Linux开发模式中至关重要的一部分,绝大多数开发者(包括我)都习惯性地认为:除非是很小的项目,这么做有害无益,因为软件的早期版本几乎都是问题版本(buggy version),如果早早发布,恐怕会耗尽用户们的耐心。
  • Linus的直接目标就是将投入排错和开发的“人时”(person-hour)最大化,即便这样做可能导致代码不稳定,或者可能因为一些难以消除的严重bug导致用户群流失,Linus也在所不惜,他相信:
  • 大教堂建筑者看来,bug是棘手的、难以发现的、隐藏在深处的,要经过几个人数月的全心投入和仔细检查,才能有点信心说已经剔除了所有错误。而发布间隔越长,倘若等待已久的发布版本并不完美,人们的失望就越发不可避免。
  • 这里隐含的问题是开发者和测试者对程序有着不匹配的思维模式,测试者是从外往内看,程序员是从内往外看。对于不开放源码的软件开发,开发者与测试者往往局限于自己的角色,各说各话,都对对方倍感沮丧。
  • 传统软件开发在组织结构上的根本问题由Brooks定律一语道破:“在一个已经延期的项目上增加人手,只会让项目更加延期。”
  • Brooks定律指出,随着开发人员数目的增长,项目复杂度和沟通成本按照人数的平方增加,而工作成果只会呈线性增长。
  • 聪明的数据结构配上愚笨的代码,远比反过来要好得多。
  • Brooks在《人月神话》的第9章里说:“让我看你的流程图但不让我看表,我会仍然搞不明白。给我看你的表,一般我就不再需要你的流程图了,表能让人一目了然。”历经30年的术语/文化变迁,这个道理依旧没变。
  • 先说说前面我提到的用这个项目验证我所发现的关于Linus成功的理论,(你可能会问)我是如何做的呢?有这么几个办法: ·我尽早发布并频繁发布(几乎从来没有低于10天一次的频率,在高强度开发阶段会一天一次)。 ·我把每一个因fetchmail联系我的人都加到beta列表(是指beta测试人员邮件列表——译者注)中。 ·每次发布新版本时,我都向beta列表发送朋友对话般的通知,鼓励他们参与。 ·我听取beta测试者们的意见,征求他们关于设计决策的看法,当他们发来补丁和反馈时给他们以热情回应。 这些简单措施立刻收到了回报。
  • 如果你把beta测试者当做最珍贵的资源对待,他们就会成为你最珍贵的资源。
  • 11.仅次于拥有好主意的是,识别来自用户的好主意,有时后者会更好。
  • 很有趣的是,如果你发自内心地谦逊,并承认你欠别人很多,你将很快发现世界会这样对待你:他们认为是你发明了整个软件,而且你对自己的天赋有着得体的谦虚。我们可以看到这一点在Linus身上体现得有多好!
  • 通常,那些最有突破性和最有创新力的解决方案来自于你认识到你对问题的基本观念是错的。
  • 设计上的完美不是没有东西可以再加,而是没有东西可以再减。”
  • 当你的代码变得既好又简单,你就知道你做对了,
  • 如果你采用快速迭代开发模式,开发和改进过程就可能成为排错过程的一个特例——修复软件原先在功能或概念上的“疏漏型bug”(bug of omission)。
  • 15.写网关类软件时,尽可能不要干扰数据流,而且绝不要扔掉信息,除非接收方强迫你这么做。
  • 我并不是因此而不喜欢英语语法,相反,提及它正是为了打破传统观念。有了更便宜的计算资源,简洁就不该成为最终目标。对现如今的计算机语言来说,是否便于人类使用要比是否节省计算资源更重要。
  • 当你的语言还远不是图灵完备(Turing-complete)的时候,语法糖[4]会让你受益良多。
  • 17.系统的安全性只取决于它所拥有的秘密。谨防虚假的秘密。
  • 当开始建设社区的时候,你需要拿出一个像样的承诺。程序此时并不需要特别好,它可以简陋、有错、不完整,文档可以少得可怜。但它至少要做到:(a)能运行,(b)让潜在的合作开发者相信,这个软件在可预见的未来,能演变成一个非常棒的东西。
  • 在软件设计上表现得聪明而有原创性,容易养成一个习惯——在应该保持软件健壮性和简单性的时候,你往往下意识把它弄得既华丽又复杂。
  • 此言不虚:最好的程序一开始只是作者对自己每天遭遇问题的个人解决方案,程序流传开来则是因为作者遇到的问题成了一大类用户的典型问题。
  • 想要解决一个有趣的问题,先去找一个让你感兴趣的问题。
  • 一个在封闭项目中只靠自己的开发者,将远远落后于这种开发者:他们知道如何创建一个开放的、有改进能力的环境,在这个环境中,上百人(甚至上千人)反馈并提供设计空间拓展、代码贡献、bug定位以及软件的其他改进。
  • “齐心协力”正是Linux这种项目所需要的——对Internet上(可以看成是无政府主义者的天堂)的志愿者们使用“命令原则”是根本行不通的。
  • Linux黑客们致力于最大化的“效用函数”,其目的并不是经典意义上的经济价值,而是自我满足和黑客声望这些无形的东西。(有人把这种动机称为“利他”,但他们忽视了一个事实,即“利他”本身是“利他者”自我满足的外在表现。)
  • fetchmail和Linux核心项目都表明,如果对参与者的“自我”做适当奖赏,一个优秀的开发者或协调者可以利用Internet获取多开发者的好处,而不会让项目陷入混乱不堪。
  • 19.如果开发协调者有一个至少像Internet这样好的沟通媒介,并且知道如何不靠强制来领导,那么多人合作必然强于单兵作战。
  • 未来软件产业的经济关键是服务价值。
  • 一些人认为购买传统模式产品会带来这样的保障:如果项目出错,有人会负责,并为可能的损失买单。
  • 即便很常见,也不要因为可以起诉某人就觉得心安,你想要的不是官司,而是能用的软件。
  • 为弄明白这点,我们需要了解软件开发管理者是如何看待自己工作的,我有位朋友看上去在这方面做得很好,她说软件管理有五个功能: ·明确目标并让大家朝同一个方向努力。 ·监督并确保关键细节不被遗漏。 ·激励人们去做那些乏味但必要的“体力活”。 ·组织人员部署并获得最佳生产力。 ·调配项目所需的资源。 显然所有这些目标都是有价值的,但在开源模式及其所在的社会语境中,人们会惊奇地发现这些目标毫无意义,我们按颠倒过来的顺序分析。
  • 如果传统、闭源、严格管理模式的软件开发真的想靠这种由“无聊”部分组成的马其诺防线来防御,那么它之所以在某个应用领域能继续生存下去,只是因为还没人发现这些问题是真正有趣的,并且还没人发现迂回包抄的路径。一旦有开源力量介入这些领域,用户就会发现终于有人是因为问题自身的魅力而去解决它的,就像其他所有需要创造力的工作,若论激励效果,问题自身的魅力比单纯的金钱要有效得多。
  • 软件工程中最广为人知的一条大众定理是:传统软件项目中的60%到70%,要么是从未被完成,要么被他们的用户拒绝。如果这个比例还算靠谱的话(我还没见过任何一个有经验的项目管理者对此提出过异议),那么大多数项目把目标设定得要么太不现实,要么完全错误。
  • 如果你在工作过程中感到恐惧和厌恶(即便你以自嘲的形式来表达——比如悬挂呆伯特玩偶),就应该意识到过程已经出了问题。快乐、幽默和玩兴是真正的资产,前面我之所以写“快乐部落”(happy horde)并不是为了首字母押韵,而用一只憨态可掬的企鹅作为Linux吉祥物也绝不仅仅是为了搞笑。
  • 极度热忱的人可能会说:“自由软件是我的生命!我活着就是为了创造有用的、优美的程序和信息资源,并把它们贡献给社会。”中热忱度的人可能会说:“开源是件好事,我愿意花大量时间帮助它成功。”低热忱度的人则可能说:“开源有时候还不错,我也玩这个,我尊敬那些创造它的人。” 差异还体现在敌对性上:反对商业软件,以及反对那些试图支配商业软件市场的公司。
  • 他们有效定义了“自由软件”概念,并有意赋予其对抗意味(后来出现的“开放源码”叫法则有意避免这点)。
  • Linux快速成长的额外好处是吸引了一大批新黑客,他们对Linux忠心耿耿,而把FSF计划视为过气的兴趣,尽管这一波黑客将Linux系统称为“GNU一代的选择”,他们中的大多数更愿意效仿Torvalds而不是Stallman。
  • 1997年,“Debian自由软件准则”提炼了这些共同要素,并形成了开放源码定义(OSD,参见http://www.opensource.org)。 定义指出,开源许可证必须保护任何个人或团体无条件修改开源软件(以及发布修改后软件版本)的权利。
  • 所以,OSD(以及与OSD一致的版权声明,如GPL、BSD许可证、Perl的艺术许可证(Artistic License))隐含的规则是“任何人能干任何事”(anyone can hack anything),没有任何事情可以阻止人们获取任意开源产品(如自由软件基金会的gcc编译器)、复制其源码、推进其向不同方向演进,并都可声称是该产品。
  • 这种演进上的分化称为“分支”(fork),分支最重要的特点是它派生出一个随后不能交换代码的竞争项目,并导致开发社区潜在的分裂。(
  • ·分化一个项目会遇到强大的社会压力,只有在极为必要的情况下才使用,而且要重新命名和做出大量的公开解释。
  • ·在没有项目主持人认可的情况下发布更新是令人不悦的,除非是特殊情况(如本质上不重要的移植bug修复)。 ·在项目历史、致谢表或维护列表中移除某个人的名字是绝对不可以的,除非当事人明确表示同意。
  • 一个软件项目的“所有者”就是在社区中众所周知的对软件版本改动有唯一发布权的那个人。
  • 开源活动确实存在一种帮助人们变得更有钱的可能,但也只是对这种可能提供有价值的线索而已。有时,某人在黑客文化中获得的声誉会在真实世界中产生经济意义:比如带来一个更好的工作机会、一份咨询合同或一纸出版协议。
  • 在分析“声誉竞争”前顺便提一下,我并不是要贬低或忽视这种纯粹美学上的满足:设计优美的软件并让它运行。黑客们都经历过这种满足并乐在其中。如果某人没有这种意义上的动力,他根本就不可能成为一名黑客,正如不喜欢音乐的人永远不会成为作曲家一样。
  • 本文第一版在互联网上发布后,一位匿名读者评论道:“别为名声工作,如果你做得好,名声将伴随结果而来”。
  • 我们已经了解心智层开垦的产出,就是黑客礼物文化中的同侪声望以及所有它带来的二次收益和额外作用。
  • 从这个理解出发,我们可以把黑客所沿袭的Lockean财产权习惯看做是一种将声誉激励最大化的手段——确保同侪将名誉赋给应得之人,而不会赋给不该得到的人。
  • ·项目产生分支是不好的,因为分化前的项目贡献者会面临声誉风险,若要控制该风险,他们只能在分化后的两个子项目上同时保持活跃。(通常这是不现实的,因为它让人困惑或难以实施)。
  • ·发布“流氓”补丁(或者更糟糕的“流氓”二进制文件)会让项目所有者陷入声誉风险,即便官方代码是完美的,所有者仍然会因补丁中的bug而被抨击(见书后注释4)。 ·偷偷将某人的名字从项目中移除,是黑客文化中最极端的恶行。这相当窃贼偷盗了受害者赠予的礼物,并说是他自己的。
  • 一位读者曾指出,分支很少能有一个以上的后代存活下来(活下来是指能长期拥有一定的“市场份额”),这促使项目所有参与方合作并避免分化,因为如果产生分化,很难预先知道谁会落败,一旦落败,就只能看着他们曾经大量的工作完全消失或者默默凋零。 他还指出一个不争的事实,即分支很容易产生争论和对抗,这足以引发对项目团队的社会压力。争论和对抗都会妨碍团队合作,而团队合作正是每个贡献者要达到自己目标所必须的。
  • 这揭示了黑客文化一个有趣的方面,它有意识地不信任或者看不起“自我主义”或者基于自我的动机。“自我推销”往往会遭到批判,即便整个社区可能从中获得好处。
  • 相比之下,在黑客社区中,一个人的作品就是他的宣言。这里有着严格的精英意识(技术最好的人胜出),这里的信条是让质量说话,让黑客最自豪的是代码“好使”(just works),是让任何称职程序员都能看到的好东西,所以,黑客文化的知识库增长迅猛。
  • 出于非常类似的原因,抨击作者而非代码是不合常规的,这一点微妙而有趣,黑客们会没有顾忌地在意识形态或个人差异上互相攻击,但从未听说有哪个黑客曾公开攻击另一个人的技术能力(
  • 攻击他人能力的禁忌(学术圈没有这个禁忌)比起自我表现禁忌(这一点学术圈也有)更有揭示意义,因为我们可以将其关联到学术圈与黑客圈在沟通和支撑结构的差异上。
  • 谈吐柔和也是有用的,如果某人希望成为一个成功项目的维护者,他必须让社区信服他良好的判断力,因为维护者的主要工作是判断他人的代码,谁愿意将代码贡献给一个明显不能正确判断他们自己代码质量的人?或者一个试图从项目中沽名钓誉的人?潜在的贡献者希望项目领导人在客观采用他人代码时,能够谦逊而有风度地说:“是的,这个的确比我的代码好,就用这个了”——然后将荣誉给予应得之人。
  • 从全球看来,这两个倾向(“填补空白”和“类别杀手”)是开源项目发展的总体趋势。
  • 在第三个千年的开始,我们大可预言开源会转向最后一块处女地——写给非技术人员的程序。
  • 声誉竞争模型解释了一个常被引用的格言,即“自称是黑客不代表你就是黑客,只有其他黑客认为你是黑客,你才是黑客”
  • 如果它不能像我所预期的那样工作,那就不是好的——不管它多么聪明和有原创性。
  • 这条规则使得开源软件倾向于长期停留在beta版,开发者只有在确信软件不会有很多问题时,才会发布1.0版。开源世界的1.0版意味“开发者愿意拿自己的名誉赌它好使”,而闭源世界的1.0版则意味着“如果你很谨慎,不要用这版”。
  • 在心智层的拓展性工作要比在某功能域内(对现有作品)的重复性工作好。
  • 能进入主要发行版的作品比不能进入的好。在所有主要发行版中都包含的作品最令人尊敬。
  • 使用”是最真实的赞美,类别杀手比同类竞争者好。
  • 如果作品好到没人再想使用其他备选,作者将会获得巨大的威望。那些被最广泛使用的原创型类别杀手,会被纳入所有的主要发行版中,并获得最大可能的同侪尊重。成功做到这点超过一次的人,将会被人们半开玩笑、半认真地称为“大神”(demigods)。
  • 相比那些只挑有趣和简单工作的人,长期致力于艰苦和乏味工作(如调试、写文档)的人更令人钦佩。
  • 重要的功能扩展比低层次的修补好。 这条规则似乎是针对一次性工作的评价。相对于修补bug而言,给软件增加功能特性有可能得到更多回报——除非这个bug异常令人厌恶或者难以寻找,因为将这种bug找出来本身就证明了非凡的技术和才能。但当这些工作是长期行为的话,一个长期关注和排除bug(甚至是普通bug)的人,其地位要高于那些花费相近工作量在增加简单功能上的人。
  • 财产权不仅仅是社会约定或游戏,而是至关重要的防范暴力冲突的进化机制。(
  • 所有权声明(就像领土标记)是一种述行式(performative)行为,一种宣布防御边界的方法。
  • 黑客们常说“责任背后是权力”,一个合作开发者在承担起维护某个子系统的责任后,通常有机会掌控子系统及其对外接口的实现,其决策仅受项目领导人(同时也是架构师)的修正。
  • 这个领域的其他研究者把目光投向了黑客们非常在意的自主性和创意自由度问题,“如果一个人越是感受到自主性受限”,罗切斯特大学心理学副教授Richard Ryan说,“其创造力就会越少。” 对任何一个任务,如果它更像是手段而不是它本身的话,往往会降低人们的积极性。即便是赢取比赛或获得同侪尊敬,如果觉得获取胜利只是为了求得回报,也一样会觉得没意思(这也许可以解释为什么黑客文化禁止那些毫不隐瞒的对尊重的追求和索取)。
  • 最终,当自由市场经济开始创造出足够的财富盈余时,大量程序员可以生活在后稀缺的礼物文化中,而软件产品的工业\工厂模式注定走向衰亡。
  • 事实上,获取最高软件生产力的药方看上去自相矛盾而又颇具禅意:如果你想获得最有效率的产品,你必须放弃促进程序员生产力。做好他们的后勤,让他们自己做主,并忘掉最后期限。
  • 指出技术会变得越来越便宜和有效,早期设计中需要投入的物理资源被越来越多地替换成信息内容。
  • 在尝试使用经济学分析软件产品时,大多数人会想当然地运用“工厂模型”,它建立在如下基本假设之上: ·大多数开发者的薪金由软件销售价值支付。 ·软件销售价值和它的开发投入成比例。 换句话说,人们十分倾向于假设软件具备典型批量商品的价值特点,但这些假设都可以被证伪。
  • 相比之下,当一个软件产品供应商退出市场(或仅仅是产品不再延续)后,消费者愿意为其产品支付的价格很快会降低到零,而不管其理论上的使用价值或该类产品的开发成本如何。(要想检验一下这个论断,可以看看你附近软件商店里的打折专柜)。
  • 换句话说,软件很大程度上是一个服务行业,虽然长期以来都毫无根据地被错认为是制造行业。
  • 这个问题很像是F.A.Hayek所提“计算问题”的翻版——它需要一个超能力(superbeing)存在:既能评估补丁的实用价值,又能可信地设定其价格以促成交易。
  • 开源项目的复杂性和沟通成本基本上完全是参与开发人数的函数,大多数从来不看源码的终端用户实际上并不会带来什么成本,但会增加项目邮件列表上愚蠢问题的比例,好在可以通过维护FAQ(常见问题)列表较为轻松地解决这个问题,而且通常大可不必理会那些明显没有读过FAQ的提问者(这些已经是通行做法)。
  • 也许是,也许不是。真正应该考虑的问题是:你从分散开发负担中获取的益处是否超过了因(“搭便车”行为导致的)竞争加剧而带来的损失,一些人往往在这个权衡中失算:(a)忽略了社区开发带来的功能改进。(b)不把已经支出的开发成本当做沉没成本。根据假设,不管怎样,你都是要付出开发成本的,所以把它归入开放源码(如果你这样认为)的成本是不对的。
  • 另一个经常被提及的担心是,将某些特别的财会功能开源,会不会导致商业机密方案的泄露?其实这不关开源闭源的事,这是糟糕设计带来的问题。财会软件如果编写得当,商业知识是不会在代码中体现的,它应该由一个模型(schema)或描述语言表达,然后由财会引擎执行实现(作为很相近的一个例子,考虑数据模型将业务知识和数据库引擎相分离的做法),这种功能上的分离使你不但可以保护住王冠上的宝石(即你的商业模型),还能从开放引擎中获得最大收益。
  • 使用价值和销售价值之间的差别,让我们注意到这样一个关键事实:在从闭源转向开源的过程中,受到威胁的仅仅是销售价值,而非使用价值。
  • 困难存在于开源开发社会契约的内在特点。有三个相辅相成的原因,使得主流的开源许可证不允许对对开源软件使用、再发布和修改施加限制,从而影响直接销售收入的获取。要理解这些原因,我们必须研究许可证演变所处的社会语境,也即互联网黑客文化(http://www.tuxedo.org/~esr/faqs/hacker-howto.html)。
  • 第一个原因与“对等性”有关,大多数开源开发者并不反对别人利用他们的礼物获利,只是要求不能有任何人(代码创始人可能会例外)站在一个特权地位上牟利。
  • 第二个原因则与“非有意后果”有关。黑客已经观察到,那些对商业使用或销售进行限制并收费(这是最常见的征费方式,乍看上去并无不妥)的许可证有着令人扫兴的效果。特别是这条规定给某些活动(如将开源软件系列发布在便宜的CD-ROM上)笼上了一层法律阴影,而这些活动正是我们非常愿意鼓励的事。更普遍地讲,如果对软件的使用/销售/修改/发布(以及其他在许可证中描述的复杂情况)加以限制,会使人们总是小心翼翼防范那些不确定的和潜在的法律风险(当人们接触的软件包越多,这个问题就越严重)。这个结果是有害的,因此,在强大的社会压力下,许可证将会变得越来越简单和无限制。
  • 与保持同侪评价这种礼物文化动力(“开垦心智层”一文中所描述的)相关。
  • 根据“大教堂与集市”一文的分析,开源获取高收益的条件大约有如下几种:(a)当可靠性/稳定性/可扩展性至关重要时,(b)没有其他方法比独立同行评审能更便捷易行地验证设计和实现正确性时(多数稍具规模的程序都适用这条)。 当软件对消费者越来越重要时,消费者会在理性上希望避开垄断供应者,这导致他们对开源的兴趣变大(开源供应商的市场竞争力会因此增强),所以,另一个判断标准是:(c)当软件成为对业务起关键作用的资产(比如存在于很多企业的MIS部门中)时。
  • 我们注意到,提供独特或高度差异化服务的供应商更担心其他竞争者拷贝他们的方法,而关键算法和知识库已经公开化时就不会这样。所以,(e)当关键方法(或能实现同等功能的方法)属于公共知识时,开源更可能胜出。
  • 这样的公司是最没有必要开源的:它拥有自己独特的能创造价值的软件技术(完全不满足(e)),它对故障不是很敏感(a),它有其他办法(不通过独立的同行评审)验证软件的正确性(b),它不是关键业务(c),也不会因为网络效应或人们普遍使用而获得价值上的实质增长(
  • 这个问题之所以严重,是因为对任意给定种类的软件产品,开源合作能够吸引的用户群和专家群都是有限的,而社区往往有黏性,如果两个在功能上大致等同的产品先后开源,先开源的往往会吸引最多的用户和最有激情的合作开发者,后开源的只能吃剩饭。社区之所以有黏性,是因为用户对软件已经熟悉,而开发者已经在代码上投入了太多时间。
  • 概括来说,基础架构的开放和共享,使每个参与者都得到了竞争上的好处,一是参与者能以较低成本生产出可扩展的产品和服务,二是参与者的市场定位可以让客户放心,他们很少会面临这样的尴尬境遇:由于供应商更改了战略或战术,导致产品被抛弃而无人照管。
  • 有时候,要想成为一只更大的青蛙,最佳办法就是让水池更快变大,这就是技术公司参与公开标准(完全可以将开源软件看成是可执行标准)的经济原因。
  • 开源似乎注定要成为一种普遍的做法,究其原因,更多是源自于客户需要和市场压力,而非供应端的效率。
  • 传统软件产业的战略家们是无法理解这种行为的(不顾其市场增长效应),因为他们成长在将(通过专利和商业秘密保护起来的)知识产权看成企业王冠上宝石的文化之中。为什么资助一项研究,却让每个竞争对手都可以无偿享用其结果呢?
  • 做正确的事”会给公司带来什么好处?答案本身既不令人惊讶也不难以验证,其他产业里也有这种看上去大公无私的行为,这些公司相信他们换来的是名声。
  • 为分析软件市场自身,有必要将软件服务按技术标准化程度进行分类,而这和软件服务的市场化(commoditize)程度存在密切关系。 这种分类与人们通常所说的“应用”(完全没有市场化、已开放的技术标准太弱或不存在)、“基础架构”(市场化服务、强标准)和“中间件”(部分市场化、有效但不完全的技术标准)有着很好的对应。在今天,典型的例子是字处理软件(应用)、TCP/IP协议栈(基础架构)和数据库引擎(中间件)。
  • 我们早先所做的收益回报分析表明:基础架构、应用和中间件将会以不同的方式变革,并展现出不同的开、闭源并存及平衡现象。在特定软件领域,开源能否流行,将取决于软件是否有实质性的网络效应、软件失效的代价如何以及软件作为资本货物的业务关键性程度。 如果将这个启发式分析方法运用到软件市场的各个部分(而不是单个产品),我们可以做出如下的大胆预言:
  • 基础架构(互联网、Web、操作系统、跨越竞争者界限的低层通信软件)将会几乎全部开源,并由用户联盟和盈利性发布/服务机构(如Redhat所扮演的角色)共同维护。
  • 应用,则非常倾向于继续封闭。当一个未公开算法或技术的使用价值足够高(且软件不稳定带来的相关成本足够低、供应商垄断带来的相关风险足可容忍)时,用户会继续为此类闭源软件付费。这种情况最有可能发生在自成一体的垂直市场应用中(其网络效应也较弱)。前面提到的锯木软件就是一例,1999年最热门和最有前景的生物识别软件则是另一例。
  • 中间件(像数据库、开发工具或可定制的应用协议栈顶端)将处于开闭源混杂的状态,这类软件走向闭源还是开源,似乎更取决于软件失效的代价,代价越高,其走向开放的市场压力就越大。
  • 他认为技术进步的趋势是更小、更轻和更有效,能够让人们“费力越来越少,收获越来越多,以至于最终可以毫不费力地获得所有东西”。
  • 虽然还处于早期阶段,但对我来说,推进战略所需的详细战术已经非常清楚了(我们在首次会议中明确讨论了这些战术),关键点如下。 1.忘掉自底向上,开始自顶向下
  • 网景的这次突破行动采用了相反的做法:战略决策者(Jim Barksdale)拿定主意,然后向下属强制推行这个愿景。
  • 2.Linux是我们最好的例证 我们必须大力宣扬Linux。是的,开源世界里还有其他一些不错的东西,这场运动也会向它们致敬,但Linux有着最好的知名度,有着最广泛的软件库,以及最大的开发社区。如果Linux都不能帮助突破,说实话,其他的就更指望不上了。
  • 抓住财富500强 除了财富500强,市场中另有一部分也很能花钱(最明显的例子是小企业和自由职业者),但这部分市场过于分散而且很难抓住。财富500强不只是有钱,而且有集中的和相对容易获取的钱,因此软件产业在很大程度上会按照财富500强的意愿行事。所以,我们首先应该说服财富500强。 4.赢得那些效劳财富500强的有威望媒体 把目标选定为财富500强,意味着我们需要赢得那些给上层决策者和投资人营造舆论环境的媒体。特别是《纽约时报》、《华尔街日报》、《经济学人》、《福布斯》以及《巴伦周刊》等等。 从这点看来,争取技术行业刊物是必要的,但远远不够,若要席卷华尔街,一个重要和基本的条件是先鼓动起精英主流媒体。
  • 5.说服黑客,游击市场 很明显,说服黑客社区自身与说服主流一样重要。如果只是一个或几个代表言之凿凿而大多数草根黑客并不买账,那可就差点意思了。 6.使用“Open Source”认证标识,保持纯净度 我们面临的一个威胁是:微软或其他大供应商可能会采取“拥抱并拓展”(embrace and extend)策略破坏“Open Source”一词,使它失去我们要传达的理念。所以Bruce Perens和我一开始就决定把这一术语注册成认证标识并把它和“开源定义”(Open Source Definition,也即Debian Free Software Guidelines的拷贝)绑定。这样我们可以利用法律诉讼的威慑力吓跑那些可能的滥用者。
  • 行业刊物很明显对开源也有了更正确的认识,正如Zawinski那句名言所说的:“开源(很伟大,但它)并不能点石成金。”
  • 两者最根本的区别是:黑客搞建设,骇客搞破坏。
  • 做一名黑客有很多乐趣,但这是一种需要努力才能获得的乐趣。而努力需要动力,成功运动员的动力来自于控制自己身体和超越自己过往生理极限的愉悦。
  • 在黑客文化中,假名是失败者的标识。
  • Peter Seebach维护着一个优秀的黑客FAQ(http://www.plethora.net/~seebs/faqs/hacker.html),用来帮助那些不懂得如何与黑客相处的管理者。我写的“黑客圈简史”(http://www.tuxedo.org/~esr/writings/hacker-history/hacker-history.html)和“大教堂与集市”(http://www.tuxedo.org/~esr/writings/cathedral-bazaar/index.html),对Linux开发和开源文化如何运转做了阐述,在“开垦心智层”(http://www.tuxedo.org/~esr/writings/homesteading/)中则对此话题做了更直接的探讨。
《憨夺型投资者》书摘

《憨夺型投资者》书摘

  • 要知道,这是一个业务模式非常稳定的生意,长期以来,现金流和利润率已经经过了市场和时间的验证,这不复杂。
  • 他努力工作、赚钱养家,坚持储蓄,然后用所有的积蓄投入到一桩只赚不赔的生意里。
  • 他的从商经历属于典型的“少投注、投大注、只挑最好的投”。这也体现了低风险、高回报投资的特点:情况好,赢得多;情况不好,输得少。
  • 如果布兰森可以在几乎没有额外资本投入的前提下,投资建立维珍大西洋航空公司,那么你也肯定能够在你选定的行业中用最少的资本开始创业。你所需要的就是用一流的创意和妥善的方案来弥补资本不足的缺憾。
  • 情况好,赢得多;情况不好,输得少。
  • 如果你简单地用一下马尔瓦公式来衡量,就会发现,投资前你一定要弄明白两件事情: ·迅速拒绝你面前的投资要求; ·从最低额度的投资开始,经过几十年的发展,你会变得非常有钱。 该说的都说了。
  • 帕特尔老爹和巴菲特的出身和创业背景差异巨大,但却最终得出了相同的结论:投资变化缓慢行业中的经营模式简单的企业。
  • 确定投资对象的关键问题不是评估行业对社会的影响力或者判断行业的增长前景,而是要确定任何给定公司的竞争优势,最重要的是这种竞争优势是可持续性的。那些具有持久竞争优势的产品和服务能给投资者带来更加丰厚的回报。
  • 对我们而言,投资行为就好比我们去和赌马彩金下注系统展开博弈。我们下注是希望有一半的概率获胜,从而赢得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%的投资选择。像阿周那一样,我们必须集中精力研究那些简单熟悉的企业信息,我们必须在力所能及的范围内做出正确判断,而不受那些外在的噪声干扰。在我们熟悉的范围内,阅读有关书籍、报纸杂志、公司报表和行业信息等,说不定就能碰到一些有利于我们的公司信息,如果我们觉得这个公司有投资的潜力,公司的股价会比其内在价值低很多,这就表明我们买入这只股票的时间到了。
领证,其实并不会有什么不同

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

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

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


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

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

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

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

一个人的永续职业

一个人的永续职业

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

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

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

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

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

对我而言,有三:

  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

使用飞书消息卡片模板,减少代码硬编码 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' }
}