关于 lencx

开源仓库:lencx/ChatGPT

ChatGPT 桌面运用开发的心路历程。
将项目从默默无闻,做到 37K+ Stars 尖端开源。
成功具有偶然,不可仿制性。
但许多要素凑在一起会让一些偶然成为必定,
期望经过这篇文章能够给咱们带来一些考虑。

布景铺垫

瞎折腾

一切技能的本质,都是为了处理问题,实战便是最好的学习。

Web → Rust → WebAssembly → Tauri → ChatGPT

  • 结缘 Rust:我非科班出身,结业后从培训组织接触 Web,开端入行前端开发。因计算机基础薄弱,故期望学习一门体系言语,来提高一些自己对底层的认知。
  • 开发 rsw 插件:rsw-rs 算是我用 Rust 开发的榜首个比较正式的工具。它是一个 CLI,旨在处理运用 Rust 开发 WebAssembly 时的热更新问题,提高开发体会。
  • Tauri 探究:在开发 rsw-rs 之后,感觉 WebAssembly 运用于实践生产对我来说似乎有点遥远。为了进一步在实战中学习 Rust,我开端学习 Tauri,它是根据 Rust 完结的跨渠道运用开发框架,能够运用 Web 技能(React、Vue 等)来开发运用。共享便是学习,我写下了 Tauri 教程 和 Rust 在前端 系列文章,也结识到了许多新朋友。

如此偶然

成功具有偶然,不可仿制性。

  • 偶然一:2022 年 11 月份 ChatGPT 发布,朋友圈陆陆续续有人在刷屏共享,刚开端没太在意(以为是营销手段)。后来仍是在好奇心驱下,我注册了一个账号,体会了一番。
  • 偶然二:体会过后,发现作业并不简略,然后就有了结合 Tauri 做桌面运用初步主意。在此之前,我现已研究 Tauri 大半年时刻,为了完结一些有趣的功用(加载长途 URL),乃至啃了许多 Tauri 源代码。
  • 偶然三:运用 ChatGPT 后,本能地开端了解它的一些周边生态(比方 prompt 或者插件),这也为桌面运用的功用迭代带来许多创意。
  • 偶然四:我赋闲了,所以有许多的时刻来开发这个项目。

这里有几个中心点:

  • 好奇心唆使:对一个新事物保持敏锐度非常重要,在领先的这段时刻里,你就有许多东西能够去做。也正是这份好奇心让我早早进场,为桌面运用的开发埋下种子。
  • 长于发现问题:在运用一个产品时能够站在用户的角度去考虑提出问题。任何产品,当你运用有痛点时,或许便是一次机会(比方:ChatGPT 想要输入 prompt,而 prompt 需求自己从别的地方不断地进行仿制粘贴,假如存在许多高频运用的 prompt,这将是低效的)。优柔寡断时,主张先迈出榜首步,思路往往会在做的进程中被翻开。
  • 恰好才干所及:技能是死的,而人是活的。技能要想发生价值有必要要依附于所能处理的问题。
  • 发散式思维:在了解一个新技能或事物后,必定要去了解它的生态和周边,这些生态都将是你创意和开发的源泉。

举一个不太恰当的例子:

国内开端大规模迸发 ChatGPT (全民 GPT)热潮应该是在 2023 年 2,3 月份左右。许多人其实并不清楚 ChatGPT 到底是什么东西,蜂拥而来,形成最大的一个问题便是“顺从会发生盲信”。利用所谓的信息差,AI 割韭菜也开端迎来了迸发式增长(朋友圈简直每天都会被各种付费 AI 课程,付费星球刷屏,标语都差不太多:AI 年代来临,假如你不学习就会被淘汰,购买咱们的 xxx,就能够让你掌握秒杀 90% 人的技能,AI 赚钱不是梦)。

割韭菜便是充分利用了信息差,尽管镰刀可恨,但韭菜就真的无辜不幸吗?镰刀之所以能够成为镰刀,是因为他们有普通人所不具备的才干:

  • 敏锐度:对信息的感知优于常人,能够蹭一切热点来完结变现的终究意图。
  • 举动力:敏捷落地,常见方法:
    • 常识付费(课程,常识星球等)
    • 流量裂变(只需共享小程序或网站就能够免费运用 xxx 功用)
  • 宣传力:营销案牍高手,长于烘托营建气氛,比方:
    • 紧迫感:AI 年代来临,截止到今日现已有 xxx 位小伙伴参加了,假如你不参加,就会被年代抛弃。
    • 增值服务:咱们内容假如做成课程,价格都在 xxx 元,现在你只需花费很小的钱,你就能够打包享受到 xxx,xxx 以及 xxx 服务,这些都是打包赠送。

技能原理

Tauri 简介

学习新技能,看文档是榜首要义(重要的作业说三遍:看文档!看文档!看文档!),不过只看 Tauri 文档,有点不太够用,有才干的仍是引荐去读一些 Tauri 源码和一些Tauri 开源项目,会发现许多小技巧。这里不过多打开,简略列举两个特点:

  • 跨渠道:Tauri 支撑 Windows、macOS 和 Linux,UI 部分运用 Web 技能(React、Vue 等)来开发。2.0 版别已支撑移动端(Android 和 iOS)。
  • 安装包体积小,内存占用小:Hello World 运用一般在 3M 左右。但调用体系内置浏览器,兼容性会差一些。
  • 体系菜单、体系托盘、权限办理、自动更新等等。

Electron VS Tauri

  • Electron = Node.js + Chromium
  • Tauri = Rust + Tao + Wry

    • Tao: 跨渠道运用程序窗口创立库,支撑一切首要渠道,如 Windows、macOS、Linux、iOS 和 Android。
    • Wry: 跨渠道 WebView 渲染库,支撑一切首要桌面渠道,如 Windows、macOS 和 Linux。

项目结构

项目结构简略,除规范的前端项目结构,外加 src-tauri 目录:

[Tauri-App]
├── [src] # 前端代码
│   ├── main.js # 入口
│   └── ...
├── [src-tauri] # Rust 代码
│   ├── [src]
│   │   ├── main.rs # 入口
│   │   └── ...
│   ├── build.rs
│   ├── Cargo.toml # Rust 配置文件,相似于 package.json
│   ├── tauri.conf.json # 运用配置文件,包括权限,更新,窗口配置等等
│   └── ...
├── vite.config.ts # Vite 配置文件
├── package.json # 描绘 Node.js 项目依靠和元数据的文件
└── ...

通讯方法

要完结 Web 网页到桌面运用的蜕变,和体系的通讯必不可少,首要有以下两种通讯方法:

  • tauri::command & invoke: 前端经过 invoke API 调用 Rust 的 command 方法。command 能够承受参数并回来值。
  • Event: emit & listen: 双向通讯(Rust ⇔ WebView),emit 发送作业,listen 监听作业。

Tauri 中一切的 API 都是异步的,在前端均以 Promise 的方法回来。

tauri::command & invoke

// src-tauri/src/main.rs
#[tauri::command]
fn hello(name: String) -> String {
  format!("Hello, {}!", name)
}
fn main() {
  tauri::Builder::default()
    // 注册命令
    .invoke_handler(tauri::generate_handler![hello])
    .run(tauri::generate_context!())
    .expect("failed to run app");
}
// src/main.js
import { invoke } from '@tauri-apps/api/tauri';
// 调用 Rust 的 hello 方法
// 输出:Hello, ChatGPT!
await invoke('hello', { name: 'ChatGPT' });

Event: emit & listen

JS ↔︎ JS

// src/main.js
import { emit, listen } from '@tauri-apps/api/event';
// 监听作业
const unlisten = await listen('click', (event) => {
  // output: Hello, ChatGPT!
  console.log(event.theMessage);
})
// 发送作业
emit('click', {
  theMessage: 'Hello, ChatGPT!',
})

JS 和 Rust 之间通讯

Rust → JS

// src-tauri/src/main.rs
// 获取特定窗口,发送作业
app.get_window("main").unwrap().emit("rust2js", Some("Hello from Rust!"));
// src/main.js
import { listen } from '@tauri-apps/api/event';
// 监听作业
listen('rust2js', (event) => {
  console.log(event.theMessage); // output: Hello from Rust!
})

JS → Rust

// src/main.js 
import { emit } from '@tauri-apps/api/event';
// 发送作业
emit('js2rust', {
  theMessage: 'Tauri is awesome!',
})
// src-tauri/src/main.rs
// 获取特定窗口,监听作业,json 数据会以字符串方法回来
app.get_window("main").unwrap().listen("js2rust", |msg| {
  // output: Event { id: EventHandler(xxxxxxxx), data: Some("{\"theMessage\":\"Tauri is awesome!\"}") }
  println!("js2rust: {:?}", msg);
});

中心完结

项目创意来自于机器人指令,假如常常玩 TG 或者 Discord 的朋友应该都比较了解(经过输入斜杠指令来调用机器人的功用。比方:/help/start/ping 等等)。而 ChatGPT 常常需求重复性输入 Prompt,所以我想到了经过指令的方法来调用 Prompt 的功用(据我所知,这个功用应该是我最早完结,后来就出现了许多相似浏览器插件)。

桌面运用是根据 Tauri 的套壳完结,简略来说便是直接在 WebView 中加载网站 URL。经过注入脚本的方法来完结对网站功用的扩展。首要有以下几点:

  • 怎么加载 URL 到窗口?
  • 加载的网址中怎么注入脚本?
  • 注入脚本中怎么调用 Tauri API?

运用入口

// src-tauri/src/main.rs
#[tauri::command]
pub fn hello(name: String) {
  println!("Hello, {}!", name);
}
fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![hello]) // 注册命令
    .plugin() // 注册插件,假如命令过多能够考虑写成插件,方便办理
    .setup() // 初始化
    .system_tray() // 体系托盘
    .menu() // 体系菜单
    .on_menu_event() // 菜单作业
    .on_system_tray_event() // 托盘作业
    .on_window_event() // 窗口作业
    .run(tauri::generate_context!())
    .expect("error while running ChatGPT application");
}
// src/main.js
import { invoke } from '@tauri-apps/api';
await invoke('hello', { name: 'lencx' });

加载 URL 并注入脚本

// src-tauri/src/main.rs
tauri::Builder::default()
  .setup(|app| {
    tauri::WindowBuilder::new(
      app,
      "main", // 窗口 ID
      tauri::WindowUrl::App("https://chat.openai.com".into()) // 加载 URL
    )
      .initialization_script(include_str!("./scripts/core.js")) // 注入脚本
      .title("ChatGPT") // 标题
      .inner_size(800.0, 600.0) // 窗口巨细
      .resizable(true) // 是否可调整窗口巨细
      .build()
      .unwrap();
  })
  .run(tauri::generate_context!())
  .expect("error while running ChatGPT application");

注入脚本中调用 Tauri API

这一部分比较复杂,因为 Tuari 的架构规划本身便是为安全而生的,所以假如运用程序挑选经过加载长途 URL 的方法来创立窗口时,Tauri 不会为该窗口注入 Tauri API(留意:Tauri 1.3.0 版别略有变更,具体请检查 Announcing Tauri 1.3.0)。这部分是从源码中取得的技巧,经过 Tauri 露出的 __TAURI_POST_MESSAGE__ 底层 API 来模仿出上层 invoke API。代码有点多,也是整个运用的灵魂地点:

// src-tauri/src/scripts/core.js
// 生成仅有标识符
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
// 转换回调函数,回来一个仅有的标识符
function transformCallback(callback = () => {}, once = false) {
  const identifier = uid();
  const prop = `_${identifier}`;
  Object.defineProperty(window, prop, {
    value: (result) => {
      if (once) {
        Reflect.deleteProperty(window, prop);
      }
      return callback(result)
    },
    writable: false,
    configurable: true,
  })
  return identifier;
}
// 模仿 invoke API
async function invoke(cmd, args) {
  return new Promise((resolve, reject) => {
    if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
    const callback = transformCallback((e) => {
      resolve(e);
      Reflect.deleteProperty(window, `_${error}`);
    }, true)
    const error = transformCallback((e) => {
      reject(e);
      Reflect.deleteProperty(window, `_${callback}`);
    }, true)
    window.__TAURI_POST_MESSAGE__({
      cmd,
      callback,
      error,
      ...args
    });
  });
}
// 模仿体系弹窗 API
async function message(message) {
  invoke('messageDialog', {
    __tauriModule: 'Dialog',
    message: {
      cmd: 'messageDialog',
      message: message.toString(),
      title: null,
      type: null,
      buttonLabel: null
    }
  });
}
// 将模仿的 API 挂载到 window 对象
window.uid = uid;
window.invoke = invoke;
window.message = message;
window.transformCallback = transformCallback;

Tauri 套壳 ChatGPT,代码完结到这里,整个运用程序的中心逻辑就算跑通了。即:

  1. tauri::WindowBuilder::new 加载 URL https://chat.openai.com
  2. initialization_script 注入脚本
  3. invoke 调用 tauri::command
  4. tauri::command 完结操作体系文件读写

开源浅思

程序员最不缺的便是编码力和创造力,但能够成为独立开发者的人却少之又少。我以为,首要有以下原因:

  • 眼高手低,或不屑于去做。那不便是个套壳吗,有什么可搞的?
  • 缺少开发独立产品思维,尽管在公司做过的项目挺多,但自己独立完结整个产品闭环时却有点茫然(功用完结,页面交互,界面排版,项目架构,项目推广等等)。
  • 对信息的敏锐性,和技能的学习力下降。上班现已那么卷了,下班或周末就会挑选躺平,不愿意走出舒适区。
  • 缺乏共享认识。尽管平常技能群,各大社区没少吹牛,但能够正真沉淀下来的东西少之又少。
  • 以及其他一些要素。

举动大于幻想

当时我在 Tauri 群里聊开发桌面运用的主意时,有些群友表明不看好,以为现已有人开发过了,你完全能够给他人做贡献(提 PR)。而不是重复造轮子,同期相似项目还有两个:

  • sonnylazuardi/chat-ai-desktop:根据 Tauri 开发
  • vincelwt/chatgpt-mac:根据 Electron 开发

做一件作业时,身边必定会出现一些不和谐的声响,但他们的观念并不能够左右你的举动。就个人而言,我不喜欢被束缚,因为提 PR 就意味着你有必要依照他人的主意去做,作业会变得不可控(经过/回绝)。我创立项意图初衷并不是为了服务于人(也没想着会火),首要是为验证自己的一些主意。迈出榜首步,你将拥有无限或许。遇到问题处理问题,一个问题会衍生出一个新问题,这些实战会让你敏捷成长

社区的力量

关于没有任何布景的人而言,项目前期想要取得关注是很困难的一件作业。这时候就需求凭借外力,来协助自己打破 0 到 1 的问题。在前期我做了两件事(向两个开源项目提 issues):

  • liady/ChatGPT-pdf:PDF,图片导出功用
  • f/awesome-chatgpt-prompts:斜杠指令的 prompt 数据源

首要对两个库作者的作业表明感谢,并奉告他们我现已将他们所做的作业集成到了 ChatGPT 运用中。Awesome ChatGPT Prompts 作者以为我这个主意很棒,并表明愿意在 README 中增加我的项目链接(彼此成就,才干走的更远)。

项目前期仍是比较辛苦的,尽管我仅用半天时刻,就发布了 v0.1.0 版别,但接下来就进入了快速迭代期(开发功用,考虑交互,回复 issues,Fix Bug 等等)。遇到棘手问题需求查找许多材料,归于边学边开发。那一段时刻,人都魔怔了,每天睁开眼睛榜首件事就看项目新增了多少 issues。随着项目开展,也有一些小伙伴参与进来,贡献 PR,献计献策。

在这里我想感谢每一个参与或支撑开源的人,正是因为 TA 们带来的一丝丝温暖,才干使开源生态不断开展壮大。做开源是很有成就感的作业,你的一举一动都有改动国际的或许

产品思维

摘自 《流量暗码:ChatGPT 开源的一些考虑》

  • 产品闭环:它能够很小,功用能够很粗陋,可是有必要要形成最小闭环,保证其可用性(产品中心功用能够正常运用)。
  • 速度要快:开发速度,更新速度,问题相应速度都要快,因为它能够协助你抢占榜首波用户(种子用户堆集很重要,能够形成口碑,协助产品二次传播)。
  • 用户体会:这是需求花心思的,尽管你是一名开发者,可是你更是一名运用者。所以没有产品,你便是产品;没有规划,你便是规划(你便是用户,乃至你要比用户更懂用户,学会取舍)。
  • 产品方案:你对产品未来方向的规划,方案参加什么牛逼的功用,需求在文档里写清楚。它就适当于是在给用户画饼,能够打动一些想要长时刻跟随它的用户(留意:画饼不代表天马行空的主意,而是根据实践情况,可完结但因时刻原因暂时无法完结的方案)。
  • 差异化:因为当你发现机会的时候,他人或许早现已在里面开端收割了,所以产品功用的差异化,将是你的打破口(人无我有,人有我有优)。
  • 稳定性:产品的初期的架构很重要,它或许会随同其一生。重构有时候并不实际,因为它需求牵扯到许多的历史包袱,数据兼容,人力本钱等等(可扩展性很重要)。

怎么学习?

现在的咱们正在面对各种碎片化的冲击。海量信息,短视频让人的思维愈发碎片化(许多人表明很难静下心来读一篇大几千字,上万字的文章,更别谈考虑或输出了)。“卷”这个字也是近些年最火的一个字,没有信息让人焦虑,信息爆破会让人变得愈加焦虑。

我也是在开发桌面运用之后,才开端接触 AI 这个范畴。写的文章多了(大约输出了几十篇 AI 系列文章),也莫名成了他人眼中的大佬(自己有多菜只有自己清楚)。

不知道常识学习 = 扩展阅览 + 信息源 + 已有常识 + 经历推导

  • 扩展阅览:善用搜素引擎 ChatGPT,检索文章中的不知道术语或名词(不过我更倾向于在 ChatGPT 给出定论后,自己再用搜索引擎复核一下)
  • 信息源:尽或许去靠近信息源,关注范畴大牛。信息具有时效性,二手信息会形成信息差,交智商税,走弯路是必定的。
  • 共享输出:共享是最高效的学习方法。动手写或给他人讲,都会让你发现许多之前留意不到的细节(看往往是浮于外表,细节和坑都隐藏在更深处)。

什么是价值?

将价值简略粗犷地与金钱划上等号,我不知是对是错,但丢了根基,一切都不过是空中楼阁罢了。

在我看来价值是一个很笼统的东西,可是往往人们都喜欢用成果去衡量一个东西的价值(比方有多少用户,赚了多少钱等等)。举一个简略的例子:我常常混迹在 GitHub 社区,也看到过许多很牛的项目,是它们撑起了海量的上层运用,可是它们的关注度却不高,你觉得它们有价值吗?也有许多博眼球项目,含金量不高,却取得了巨大的关注度,你觉得它们价值高吗?

在这个以成果为导向的国际里,不论做什么作业,都避免不了被问到:“你做这个东西有什么价值?”。当你有了比较心,得失心,在做一件作业时就会变得畏手畏脚,乃至不屑去做。想的多不是坏事,当你在试图最大化利益时,往往也会丢掉许多或许性。人们常说的机会是什么?我以为它便是:一个人在堆集常识,学习技能的同时,长于用开展的眼光去调查这个快速变化的国际,当你有才干去处理某个问题时,它对你来说便是一次机会。

结束语

身为一名程序员我很骄傲,尽管足不出户,指尖却有着能够改动国际 (或许有点大了) 自己的力量。即便不能完结,将其作为尽力的目标也不错。