V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
hamsterbase
V2EX  ›  分享创造

文字直播用 chatgpt 开发一个笔记软件。 希望可以在睡觉前部署上线。

  •  
  •   hamsterbase ·
    carytrivett · 2023-06-08 20:53:00 +08:00 · 3918 次点击
    这是一个创建于 528 天前的主题,其中的信息可能已经有所发展或是发生改变。

    下面是之前的聊天记录

    Q: 我现在希望开发一个卡片笔记软件。UI 界面类似于 imessage 。 请问叫什么名字比较好?

    A 为了取一个好名字,需要考虑软件的特点和目标用户。考虑到你的软件类似于 iMessage ,那么你可以考虑使用类似的命名方式。以下是一些可能的建议:

    CardMessage NoteChat CardNote SnapNote MemoTalk NoteMessage 在考虑具体的名字时,可以考虑软件的品牌形象,功能特点等方面,以便从众多同类软件中脱颖而出

    1. 搜了一下,memo talk 之前没人开发过,就叫这个名字吧。

    2. 建了一个 GitHub 仓库 https://github.com/hamsterbase/memo-talk

    Q: sha256 转化为字符串,长度是多少。

    A:

    SHA256 是一种加密算法,它生成的哈希值( hash value )通常表示为 64 个字符的十六进制字符串( hexadecimal string ),因此 SHA256 转化为字符串后的长度是 64 个字符。每个字符是 0-9 和 A-F 之间的一个 16 进制数字,因此它们可以表示 256 个可能的值( 16 的 4 次方)。

    5

    Q:

    我希望你能帮我编写一下服务端的代码。

    需要有以下四个接口接口为

    1. 获取文件夹列表

    2. 根据文件夹名字,文件名获取文件内容

    3. 根据文件夹名字,文件名删除文件

    4. 根据文件夹名字,文件名删除文件

    要求。

    1. 基于 koa 编写
    2. 使用 typescript 编写
    3. 文件名,文件名必须为 64 个字符,每个字符是 0-9 和 A-F 之间的一个 16 进制数字
    4. 文件夹名和文件名,文件内容都通过 body 传递。

    A: 以下是一个基于 Koa 和 TypeScript 的简单实现,其中包含了四个接口:

    import Koa from "koa";
    import bodyParser from "koa-bodyparser";
    
    const app = new Koa();
    
    // 定义文件夹列表
    let folders: string[] = [];
    
    // 定义文件内容
    let files: { [key: string]: string } = {};
    
    // 获取文件夹列表
    app.use(async (ctx, next) => {
      if (ctx.path === "/folders" && ctx.method === "GET") {
        ctx.body = folders;
        ctx.status = 200;
      } else {
        await next();
      }
    });
    
    // 获取文件内容
    app.use(async (ctx, next) => {
      if (ctx.path === "/files" && ctx.method === "GET") {
        const folderName = ctx.request.body.folderName;
        const fileName = ctx.request.body.fileName;
        const filePath = `${folderName}/${fileName}`;
        if (files[filePath]) {
          ctx.body = files[filePath];
          ctx.status = 200;
        } else {
          ctx.body = "File not found";
          ctx.status = 404;
        }
      } else {
        await next();
      }
    });
    
    // 添加文件
    app.use(bodyParser());
    app.use(async (ctx, next) => {
      if (ctx.path === "/files" && ctx.method === "POST") {
        const folderName = ctx.request.body.folderName;
        const fileName = ctx.request.body.fileName;
        const fileContent = ctx.request.body.fileContent;
        const filePath = `${folderName}/${fileName}`;
        if (files[filePath]) {
          ctx.body = "File already exists";
          ctx.status = 409;
        } else {
          files[filePath] = fileContent;
          ctx.status = 201;
        }
      } else {
        await next();
      }
    });
    
    // 删除文件
    app.use(async (ctx, next) => {
      if (ctx.path === "/files" && ctx.method === "DELETE") {
        const folderName = ctx.request.body.folderName;
        const fileName = ctx.request.body.fileName;
        const filePath = `${folderName}/${fileName}`;
        if (files[filePath]) {
          delete files[filePath];
          ctx.status = 204;
        } else {
          ctx.body = "File not found";
          ctx.status = 404;
        }
      } else {
        await next();
      }
    });
    
    app.listen(3000, () => {
      console.log("Server is running on port 3000");
    });
    

    请注意,此代码仅供参考,可能需要根据具体需求进行修改和调整。

    62 条回复    2023-06-19 13:37:38 +08:00
    hamsterbase
        1
    hamsterbase  
    OP
       2023-06-08 20:58:11 +08:00
    6. 他没把文件写到本地,所以改一下 prompt

    Q:

    我希望你能帮我编写一下服务端的代码。

    需要有以下四个接口接口为

    1. 获取文件夹列表

    2. 根据文件夹名字,文件名获取文件内容

    3. 根据文件夹名字,文件名删除文件

    4. 根据文件夹名字,文件名删除文件

    要求。

    1. 基于 koa 编写
    2. 使用 typescript 编写
    3. 文件名,文件名必须为 64 个字符,每个字符是 0-9 和 A-F 之间的一个 16 进制数字
    4. 文件夹名和文件名,文件内容都通过 body 传递。
    5. 我希望你能用 koa-router 编写
    6. 文件希望写入到本地的 data 文件夹下
    7. API 必须以 public/api/v1 开头

    Q:

    ```ts
    import Koa from "koa";
    import Router from "koa-router";
    import fs from "fs/promises";
    import path from "path";

    const app = new Koa();
    const router = new Router();

    const DATA_FOLDER = "data";

    // 获取文件夹列表
    router.get("/public/api/v1/folders", async (ctx) => {
    try {
    const folders = await fs.readdir(DATA_FOLDER);
    ctx.body = folders;
    } catch (err) {
    ctx.status = 500;
    ctx.body = "Internal Server Error";
    console.error(err);
    }
    });

    // 根据文件夹名字,文件名获取文件内容
    router.get(
    "/public/api/v1/folders/:folderName/files/:fileName",
    async (ctx) => {
    const { folderName, fileName } = ctx.params;
    const filePath = path.join(DATA_FOLDER, folderName, fileName);
    try {
    const fileContent = await fs.readFile(filePath, "utf-8");
    ctx.body = fileContent;
    } catch (err) {
    if (err.code === "ENOENT") {
    ctx.status = 404;
    ctx.body = "File Not Found";
    } else {
    ctx.status = 500;
    ctx.body = "Internal Server Error";
    console.error(err);
    }
    }
    }
    );

    // 根据文件夹名字,文件名删除文件
    router.del(
    "/public/api/v1/folders/:folderName/files/:fileName",
    async (ctx) => {
    const { folderName, fileName } = ctx.params;
    const filePath = path.join(DATA_FOLDER, folderName, fileName);
    try {
    await fs.unlink(filePath);
    ctx.body = "File Deleted";
    } catch (err) {
    if (err.code === "ENOENT") {
    ctx.status = 404;
    ctx.body = "File Not Found";
    } else {
    ctx.status = 500;
    ctx.body = "Internal Server Error";
    console.error(err);
    }
    }
    }
    );

    // 根据文件夹名字,文件名删除文件
    router.post(
    "/public/api/v1/folders/:folderName/files/:fileName",
    async (ctx) => {
    const { folderName, fileName } = ctx.params;
    const filePath = path.join(DATA_FOLDER, folderName, fileName);
    try {
    await fs.mkdir(path.join(DATA_FOLDER, folderName), { recursive: true });
    await fs.writeFile(filePath, ctx.request.body);
    ctx.body = "File Created";
    } catch (err) {
    ctx.status = 500;
    ctx.body = "Internal Server Error";
    console.error(err);
    }
    }
    );

    app.use(router.routes());
    app.use(router.allowedMethods());

    app.listen(3000, () => {
    console.log("Server listening on port 3000");
    });
    ```

    接口说明:

    获取文件夹列表:GET /public/api/v1/folders
    根据文件夹名字,文件名获取文件内容:GET /public/api/v1/folders/:folderName/files/:fileName
    根据文件夹名字,文件名删除文件:DELETE /public/api/v1/folders/:folderName/files/:fileName
    根据文件夹名字,文件名删除文件:POST /public/api/v1/folders/:folderName/files/:fileName
    其中,文件名和文件夹名都是 64 个字符的 16 进制数字字符串。文件内容通过 Request Body 传递,文件保存在本地的 data 文件夹下。如果出现错误,会返回适当的 HTTP 状态码和错误信息。
    hamsterbase
        2
    hamsterbase  
    OP
       2023-06-08 21:06:13 +08:00
    7. 之后不贴 chatgpt 的回答了,复制粘贴太麻烦啦。

    Q:

    我希望你能帮我编写一下服务端的代码。

    参数命名规则为 folder , file , content 。

    需要有以下 4 个接口为。

    1. 获取 folder 获取文件列表。

    2. 根据 folder ,file 获取 content

    3. 根据 folder ,file 删除文件

    4. 根据 folder ,file, content 写入文件

    要求。

    1. 基于 koa 编写,使用 typescript 编写
    2. folder , file 必须为 64 个字符,每个字符是 0-9 和 A-F 之间的一个 16 进制数字.
    3. 我希望你能用 koa-router 编写
    4. 文件希望写入到本地的 data 文件夹下
    5. 规则为 /data/folder/file
    6. API 必须以 public/api/v1 开头
    7. content 限制最大 1mb
    8. folder, file, content 都通过 body 获取,不要用 query,path 获取。
    hamsterbase
        3
    hamsterbase  
    OP
       2023-06-08 21:14:25 +08:00
    8. koa-router 好像没有类型,换 @koa/router. 初始化项目,把 chatgpt 回答拷贝一下

    https://github.com/hamsterbase/memo-talk/commit/2cc9318a7255e2dafef2716e78b2e844953042cc
    hamsterbase
        4
    hamsterbase  
    OP
       2023-06-08 21:25:59 +08:00
    9 。chatgpt 没有写文件名校验逻辑,让他单独写一个
    帮我用 ts 写一个字符串检测函数。 字符串长度必须为 64 个字符,每个字符是 0-9 和 A-F 之间的一个 16 进制数字.
    忽略大小写。 最终输出转换为小写后的结果。 如果不满足,则抛错。
    hamsterbase
        5
    hamsterbase  
    OP
       2023-06-08 21:44:29 +08:00
    10. 白嫖 render.com , 服务器上线了 https://memo-talk.onrender.com
    hamsterbase
        6
    hamsterbase  
    OP
       2023-06-08 21:49:29 +08:00
    11. 帮我想一下 memo-talk 前端仓库的名字

    以下是一些可能的 Memo-Talk 前端仓库的名字:

    - memo-talk-frontend:一个简单而明确的仓库名称,指示该仓库包含 Memo-Talk 的前端代码。
    - memo-talk-web:一个更简洁的仓库名称,指示该仓库包含 Memo-Talk 的 Web 界面代码。
    - memo-talk-ui:一个仓库名称,指示该仓库包含 Memo-Talk 的用户界面部分。
    - memo-talk-client:一个仓库名称,指示该仓库包含 Memo-Talk 的客户端代码。
    - memo-talk-portal:一个仓库名称,指示该仓库包含 Memo-Talk 的 Web 门户代码。
    你可以根据你的喜好和项目的具体需求选择其中任何一个名称。无论你选择哪个名称,都需要确保它能够准确地描述该仓库的内容,并且易于理解和记忆。此外,你可能还需要考虑使用标准的命名约定和格式,以便其他开发人员能够轻松地找到和使用你的仓库。
    hamsterbase
        7
    hamsterbase  
    OP
       2023-06-08 21:50:35 +08:00
    hamsterbase
        8
    hamsterbase  
    OP
       2023-06-08 21:56:47 +08:00
    13. 使用 vite 的 ts + react 模板,数据核心准备使用 yjs
    hamsterbase
        9
    hamsterbase  
    OP
       2023-06-08 21:57:58 +08:00
    14. 前端上线啦,可以访问了。 开始写前端逻辑

    https://memo-talk-web.onrender.com/
    hamsterbase
        10
    hamsterbase  
    OP
       2023-06-08 22:00:34 +08:00
    15.

    Q: 帮我设计一个 ts interface. NoteCore

    1. 新建笔记,返回 ID
    2. 根据 ID 返回笔记内容。
    3. 获取笔记列表.
    4. 根据 ID 删除笔记
    hamsterbase
        11
    hamsterbase  
    OP
       2023-06-08 22:08:09 +08:00
    16. 设计一下核心的数据结构。 这个是我自己设计的


    export interface MemoTalk {
    id: string;
    content: string;
    createTime: number;
    }

    /**
    * MemoTalkCore 核心为一个 Y.Doc 对象
    * 保存的时候 encode 为字符串
    * 可以合并别的 MemoTalkCore, 入参为字符串
    */
    export interface MemoTalkCore {
    createMemoTalk(content: string): string;

    getMemoTalkById(id: string): MemoTalk;

    getMemoTalkList(): MemoTalk[];

    deleteMemoTalkById(id: string): void;

    /**
    * 把整个数据库序列化为字符串
    */
    encode(): string;

    merge(data: string): void;
    }
    hamsterbase
        12
    hamsterbase  
    OP
       2023-06-08 22:24:58 +08:00
    17. 在 copilot 的帮助下,完成了核心逻辑的实现


    const enum YDocKey {
    /**
    * 避免 id 重复
    */
    id = "id",
    memoTalks = "memoTalks",
    }

    export class MemoTalkCore implements IMemoTalkCore {
    private ydoc: Y.Doc;
    constructor() {
    this.ydoc = new Y.Doc();
    }

    createMemoTalk(content: string): string {
    if (typeof content !== "string") {
    throw new Error("content must be string");
    }
    const idMap = this.ydoc.getMap(YDocKey.id);
    let id: string = nanoid();
    while (idMap.has(id)) {
    id = nanoid();
    }
    const memoTalksArray = this.ydoc.getArray<string>(YDocKey.memoTalks);

    const memoTalk = this.ydoc.getMap(id);
    memoTalk.set("content", content ?? "");
    memoTalk.set("createTime", Date.now());
    memoTalksArray.push([id]);
    return id;
    }

    getMemoTalkById(id: string): MemoTalk {
    const memoTalk = this.ydoc.getMap(id);
    return {
    id,
    content: memoTalk.get("content") as string,
    createTime: memoTalk.get("createTime") as number,
    };
    }

    getMemoTalkList(): MemoTalk[] {
    const memoTalksArray = this.ydoc.getArray<string>(YDocKey.memoTalks);
    return memoTalksArray.map((id) => this.getMemoTalkById(id));
    }

    deleteMemoTalkById(id: string): void {
    const memoTalksArray = this.ydoc.getArray<string>(YDocKey.memoTalks);
    memoTalksArray.delete(memoTalksArray.toArray().indexOf(id));
    }

    encode(): string {
    return fromUint8Array(Y.encodeStateAsUpdate(this.ydoc));
    }

    merge(data: string): void {
    Y.applyUpdate(this.ydoc, toUint8Array(data));
    }
    }
    hamsterbase
        13
    hamsterbase  
    OP
       2023-06-08 22:29:11 +08:00
    18 。 帮我为 IMemoTalkCore 编写集成单元测试。 使用 jest


    ```ts
    /**
    * MemoTalkCore 核心为一个 Y.Doc 对象
    * 保存的时候 encode 为字符串
    * 可以合并别的 MemoTalkCore, 入参为字符串
    */
    export interface IMemoTalkCore {
    createMemoTalk(content: string): string;

    getMemoTalkById(id: string): MemoTalk;

    getMemoTalkList(): MemoTalk[];

    deleteMemoTalkById(id: string): void;

    /**
    * 把整个数据库序列化为字符串
    */
    encode(): string;

    merge(data: string): void;
    }
    ```
    hamsterbase
        14
    hamsterbase  
    OP
       2023-06-08 22:43:46 +08:00
    19. 在 chatgpt 的帮助下测出了一个 bug, 如果删除了,再次获取的时候应该返回 null 。 所有测试都通过啦
    hamsterbase
        15
    hamsterbase  
    OP
       2023-06-08 22:46:45 +08:00
    20. 完善一下单元测试

    Q: 帮我用 jest 编写测试。

    如果重复删除,应该抛错
    hamsterbase
        16
    hamsterbase  
    OP
       2023-06-08 23:01:26 +08:00
    21 Q: 帮我写一个 react 的页面,需要用 typescript 开发。


    最顶上是一个提示框。 红色的字 “此项目还在开发中,请不要使用”

    中间为一个消息窗口。 消息都靠右。

    消息为圆角。消息的背景为蓝色。 参考 imessage

    页面背景为白色。

    最下面是一个 input 框。 里面有一个按钮。 按钮上面的字为 “发送”


    颜色要好看一点。 圆角保存统一。

    props 为 memotalks:MemoTalk, onCreateMemoTalk(content:string):void

    export interface MemoTalk {
    id: string;
    content: string;
    createTime: number;
    }
    hamsterbase
        17
    hamsterbase  
    OP
       2023-06-08 23:09:34 +08:00
    hamsterbase
        18
    hamsterbase  
    OP
       2023-06-08 23:41:55 +08:00
    23: 前端最基础的功能开发玩了,还没接入服务器。


    1. 支持新增消息了, 消息更新后会储存到 local storage 里。
    2. 刷新页面后支持恢复消息

    在线地址: https://memo-talk-web.onrender.com/

    明天晚上继续开始直播。

    1. 接入服务
    2. 支持设置密码和账户
    3. 支持下载当前数据库,支持上传数据库。
    findex
        19
    findex  
       2023-06-09 05:40:32 +08:00 via iPhone
    关注一下
    ShuaiYH
        20
    ShuaiYH  
       2023-06-09 08:59:48 +08:00
    @hamsterbase #5 来试试 zeabur.com 哈哈
    llsongll
        21
    llsongll  
       2023-06-09 09:44:08 +08:00
    感觉不错~
    hamsterbase
        22
    hamsterbase  
    OP
       2023-06-09 10:51:29 +08:00 via Android
    @findex

    建议改一下默认授权。 上来就要求我私有库权限。 我选择拒绝。
    hamsterbase
        23
    hamsterbase  
    OP
       2023-06-09 10:53:08 +08:00 via Android
    @ShuaiYH

    你们默认的权限要的太多了。 建议只授权 public 项目 + 邮箱。 等用户需要部署私有项目的时候,再请求进一步的授权。
    之前我就是被这个方案劝退了。
    HulkLee
        24
    HulkLee  
       2023-06-09 20:22:08 +08:00
    哈哈哈 今天晚上的几点开始,蹲一波
    hamsterbase
        25
    hamsterbase  
    OP
       2023-06-09 20:47:12 +08:00
    @HulkLee 开始开始。~~~
    hamsterbase
        26
    hamsterbase  
    OP
       2023-06-09 20:51:20 +08:00
    1. 今天的目标: 发布 1.0 !!!

    1. 接入 antd mobile ,重写 UI
    2. 接入后端,实现登录功能.
    1. 服务器不储存任何账户密码。只储存用户的 token ,token 由前端生成。
    2. 把 token 作为文件夹名
    3. token 的生成规则:sha256( `${sha256(user)} ${sha256(password)}`)
    4. 文件储存之前使用 aes 加密。 加密 为 sha256(password)
    3. 接入 PWA , 让软件可离线使用
    hamsterbase
        27
    hamsterbase  
    OP
       2023-06-09 20:57:47 +08:00
    2. localstorage 存储量有限,准备基于 IndexedDB 实现本地存储


    Q: 帮我基于 IndexedDB 实现一个 ISettingService 吧,我要把所有的配置都存在 IndexedDB 里面。

    export type SettingsValue = string | boolean;

    export enum StorageKeys {
    backend = "backend",
    "backend.hamsterbase.entrypoint" = "backend.hamsterbase.entrypoint",
    "backend.hamsterbase.token" = "backend.hamsterbase.token",
    "backend.notion.token" = "backend.notion.token",
    "backend.notion.databaseId" = "backend.notion.databaseId",
    "backend.notion.license" = "backend.notion.license",
    autoOn = "autoOn",
    autoOnBlockList = "autoOnBlockList",
    }

    export interface ISettingService {
    _serviceBrand: undefined;

    get<V extends SettingsValue>(key: string, defaultValue: V): Promise<V>;

    set(key: string, value: SettingsValue): Promise<void>;

    readConfig<T extends Record<StorageKeys, SettingsValue>>(
    defaultValue: T
    ): Promise<T>;
    }
    hamsterbase
        28
    hamsterbase  
    OP
       2023-06-09 21:00:22 +08:00
    好的,我可以帮您实现一个基于 IndexedDB 的 ISettingService 。以下是实现代码示例:

    typescript
    Copy
    export interface ConfigDatabase {
    settings: Record<string, SettingsValue>;
    }

    export class IndexedDBSettingService implements ISettingService {
    private readonly dbName: string = "my-settings-db";
    private readonly dbVersion: number = 1;
    private readonly storeName: string = "settings";
    private db: IDBDatabase;

    constructor() {
    this.openDB();
    }

    private async openDB(): Promise<void> {
    return new Promise<void>((resolve, reject) => {
    const request = indexedDB.open(this.dbName, this.dbVersion);

    request.onerror = (event) => {
    console.error("Error opening database", event);
    reject(event);
    };

    request.onsuccess = (event) => {
    this.db = request.result;
    resolve();
    };

    request.onupgradeneeded = (event) => {
    const db = request.result;
    if (!db.objectStoreNames.contains(this.storeName)) {
    db.createObjectStore(this.storeName);
    }
    };
    });
    }

    public async get<V extends SettingsValue>(
    key: string,
    defaultValue: V
    ): Promise<V> {
    return new Promise<V>((resolve, reject) => {
    const transaction = this.db.transaction(this.storeName, "readonly");
    const objectStore = transaction.objectStore(this.storeName);

    const request = objectStore.get(key);

    request.onerror = (event) => {
    console.error(`Error getting value for key "${key}"`, event);
    reject(event);
    };

    request.onsuccess = (event) => {
    const value = request.result !== undefined ? request.result : defaultValue;
    resolve(value);
    };
    });
    }

    public async set(key: string, value: SettingsValue): Promise<void> {
    return new Promise<void>((resolve, reject) => {
    const transaction = this.db.transaction(this.storeName, "readwrite");
    const objectStore = transaction.objectStore(this.storeName);

    const request = objectStore.put(value, key);

    request.onerror = (event) => {
    console.error(`Error setting value for key "${key}"`, event);
    reject(event);
    };

    request.onsuccess = (event) => {
    resolve();
    };
    });
    }

    public async readConfig<T extends Record<StorageKeys, SettingsValue>>(
    defaultValue: T
    ): Promise<T> {
    const config: T = {} as T;

    // Read all values from the database and merge with default values
    for (const key in defaultValue) {
    if (defaultValue.hasOwnProperty(key)) {
    const value = await this.get(key, defaultValue[key]);
    config[key as keyof T] = value as T[keyof T];
    }
    }

    return config;
    }

    public async clearAll(): Promise<void> {
    return new Promise<void>((resolve, reject) => {
    const transaction = this.db.transaction(this.storeName, "readwrite");
    const objectStore = transaction.objectStore(this.storeName);

    const request = objectStore.clear();

    request.onerror = (event) => {
    console.error("Error clearing settings", event);
    reject(event);
    };

    request.onsuccess = (event) => {
    resolve();
    };
    });
    }
    }
    在这个实现中,我们创建了一个名为 my-settings-db 的 IndexedDB 数据库,并创建了一个名为 settings 的对象存储。在 get 和 set 方法中,我们分别使用事务来读取和写入数据。在 readConfig 方法中,我们遍历默认配置对象中的所有键,并使用 get 方法来获取每个键的值。最后,我们还为 IndexedDBSettingService 类添加了一个清除所有设置的 clearAll 方法。

    请注意,由于 IndexedDB 是异步的,因此所有方法都返回一个 Promise 。此外,如果 IndexedDB 连接失败或出现错误,这些方法将会拒绝 Promise 并输出错误信息。
    hamsterbase
        29
    hamsterbase  
    OP
       2023-06-09 21:18:45 +08:00
    3. 对 chatgpt 写的代码不太放心,准备写一个单元测试

    Q: 帮我用 mocha 写单元测试

    ```ts
    export interface ISettingService {
    get<V extends SettingsValue>(key: string, defaultValue: V): Promise<V>;

    set(key: string, value: SettingsValue): Promise<void>;

    readConfig<T extends Record<string, SettingsValue>>(
    defaultValue: T
    ): Promise<T>;
    }
    ```
    hamsterbase
        30
    hamsterbase  
    OP
       2023-06-09 21:28:27 +08:00
    4. 本来准备在 vite 里跑 mocha ,试了一下发现不太行,准备换一条路。 用假的 indexedDB 测试。 测试继续在 vitest 里跑。
    hamsterbase
        31
    hamsterbase  
    OP
       2023-06-09 21:37:09 +08:00
    5. 逻辑和测试都让 chatgpt 写好了, 看了一下测试似乎没问题

    他给的是在 IndexedDBSettingService 构造函数里调用 openDB, 我改成了每次 set , get 的时候初始化,似乎没问题。

    https://github.com/hamsterbase/memo-talk-web/commit/31cc5a97a31d69f015f6ecfd089fb3147c1db7dd
    hamsterbase
        32
    hamsterbase  
    OP
       2023-06-09 21:57:01 +08:00
    6. 使用 vite 的多页面模式开发。 初始化一下 settings 页面。 配置一下 vite 的构建配置


    https://github.com/hamsterbase/memo-talk-web/commit/d7024d70c6c63cca47aa550175504aa95e22f3eb
    echoless
        33
    echoless  
       2023-06-09 21:59:11 +08:00
    https://github.com/swuecho/chat

    你用我这个 gpt 套🉑️, 可以直接导出 markdown 或者分享对话. 好看多了
    hamsterbase
        34
    hamsterbase  
    OP
       2023-06-09 22:35:31 +08:00
    7. 因为是极简的稍后读工具,所以在不校验账户密码。 只需要确保账户密码非空就行了

    简单完成了账户密码的保存功能。 未来会加上警告,用户协议。 目前暂时先不加.

    因为 antdmobile 是在 2019 年之后出的,chatgpt 估计不认识,就手写了

    https://github.com/hamsterbase/memo-talk-web/commit/c5559845d27cdeba278c9969330c474e26db9ac6
    hamsterbase
        35
    hamsterbase  
    OP
       2023-06-09 23:12:22 +08:00
    8. 简单更新了一下首页的 UI , 然后把储存从 localstorage 换到了 IndexedDB
    开始真正对接服务器了。

    第一版做的简单一点,在页面加载的时候异步合并一下服务器的数据,合并后把数据上传到服务器。
    hamsterbase
        36
    hamsterbase  
    OP
       2023-06-09 23:21:16 +08:00
    9 。 开始编写服务器需要的一些工具函数


    Q:
    帮我用 crypto-js 编写函数。

    输入账户,密码。 生成两个 key

    第一个 key sha256(memo:sha256(username):sha256(password):talk);
    第二 key 。sha256(memo:password:talk

    用 ts 编写

    第一个 key 名字为 userToken , 第二个 key 名字为 加密密钥(用英文)
    hamsterbase
        37
    hamsterbase  
    OP
       2023-06-09 23:26:37 +08:00
    10. 帮我编写一个 aes 加密函数. 输入 key 和 data, 返回加密后的数据. 输入 data 和 key , 返回解密后的数据

    基于 crypto-js 和 ts
    hamsterbase
        38
    hamsterbase  
    OP
       2023-06-09 23:28:01 +08:00
    11: 帮上面的 encryptedData ,decryptedData 写测试。 随机的 key 和 data 。 加密和解密后数据不变
    hamsterbase
        39
    hamsterbase  
    OP
       2023-06-09 23:29:59 +08:00
    12:帮我写一个 sha256 的函数。 基于 ts . crypto-js. 输入字符串,输出 hex
    hamsterbase
        40
    hamsterbase  
    OP
       2023-06-09 23:32:10 +08:00
    13: POE 一天一次的 gpt4 体验卡

    帮我基于下面的代码,写一个 js sdk 出来。


    https://github.com/hamsterbase/memo-talk/blob/main/server/index.ts
    hamsterbase
        41
    hamsterbase  
    OP
       2023-06-09 23:33:06 +08:00
    ```
    class FileAPISDK {
    constructor(baseUrl = "http://localhost:9999") {
    this.baseUrl = baseUrl;
    }

    async getList(folder) {
    const response = await fetch(`${this.baseUrl}/public/api/v1/folder`, {
    method: "POST",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify({ folder }),
    });

    this._handleErrors(response);
    return await response.json();
    }

    async getFile(folder, file) {
    const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
    method: "GET",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify({ folder, file }),
    });

    this._handleErrors(response);
    return await response.text();
    }

    async deleteFile(folder, file) {
    const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
    method: "DELETE",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify({ folder, file }),
    });

    this._handleErrors(response);
    return await response.text();
    }

    async createFile(folder, file, content) {
    const response = await fetch(`${this.baseUrl}/public/api/v1/file`, {
    method: "POST",
    headers: {
    "Content-Type": "application/json",
    },
    body: JSON.stringify({ folder, file, content }),
    });

    this._handleErrors(response);
    return await response.text();
    }

    _handleErrors(response) {
    if (!response.ok) {
    throw new Error(`API request failed with status ${response.status}`);
    }
    }
    }
    ```
    hamsterbase
        42
    hamsterbase  
    OP
       2023-06-09 23:38:14 +08:00
    14. 发现服务器忘记配置跨域了。

    q:koajs 如何开启跨区请求。
    hamsterbase
        43
    hamsterbase  
    OP
       2023-06-10 00:11:08 +08:00
    11. 服务器问题修复后,开始对接。 核心的云同步逻辑非常简单


    const sdk = new FileAPISDK(url);

    // 生成加密密钥
    // userToken 用来标记用户身份,也是服务器文件夹的名字
    // encryptionKey 是文件加密密码,encryptionKey 不会发送到服务
    const { userToken, encryptionKey } = generateKeys(username, passowrd);

    // 获取服务器数据库列表
    const files: string[] = await sdk.getList(userToken);

    const mergedFiles: string[] = [];

    // 把服务器上的文件和本地合并,合并后加入等待删除列表
    for (const file of files) {
    try {
    const remoteData = await sdk.getFile(userToken, file);
    const database = decryptData(remoteData, encryptionKey);
    memoTalkCore.merge(database);
    mergedFiles.push(file);
    } catch (error) {
    console.log(error);
    }
    }

    // 获取本地最新数据,加密
    const currentData = encryptData(memoTalkCore.encode(), encryptionKey);
    const fileName = sha256(currentData);

    // 把加密后的文档存到服务器
    await sdk.createFile(userToken, fileName, currentData);

    // 因为数据已经合并到本地了,所以把服务器的删掉
    for (const mergedFile of mergedFiles) {
    try {
    await sdk.deleteFile(userToken, mergedFile);
    } catch (error) {
    console.log(error);
    }
    }
    }
    hamsterbase
        44
    hamsterbase  
    OP
       2023-06-10 00:22:54 +08:00
    12. 经过 2 天 6 小时的开发。 一个本地优先的极简卡片笔记就开发完了。(只有最核心功能)

    可以访问 https://memo-talk-web.onrender.com 体验

    注意: 项目还没正式发布,只能拿来玩。,不要在这里储存任何数据。

    1. 纯 web 实现, 压缩后不到 173 k 。
    2. 支持离线编辑,再次联网后自动同步。
    3. 两台设备输入相同的账户密码。 刷新页面刷新后,他们会自动同步到同一个状态。
    4. 所有数据在本地加密,服务器不储存明文数据。
    HulkLee
        45
    HulkLee  
       2023-06-10 15:04:34 +08:00
    牛,面向 chatgpt 编程。
    llsongll
        46
    llsongll  
       2023-06-12 10:32:41 +08:00
    nb ,我就想着有这种笔记,跟微信聊天似的,能搜索,快捷输入,(最好能匹配像是 alfred ,utools 这种工具,安卓可以一键输入。)
    temberature
        47
    temberature  
       2023-06-12 13:20:29 +08:00
    @llsongll 直接用微信不行吗 :)
    llsongll
        48
    llsongll  
       2023-06-12 20:03:36 +08:00
    @temberature #47 微信不是有监测吗。。。而且微信以前有插件可以用 alfred 输入,现在好像不行了。
    temberature
        49
    temberature  
       2023-06-13 15:46:36 +08:00
    @llsongll 这些确实也都是问题。但反过来说,为什么要像微信聊天呢~没有这个要求有很多应用差不多吧
    llsongll
        50
    llsongll  
       2023-06-13 21:14:20 +08:00
    @temberature #49 嗯呢,我也有用 Obsidian 的 memos ,也有类似的效果,alfred 也可以在不打开 ob 的情况下快捷输入。就是虽然界面体验比微信的感觉差了好多
    temberature
        51
    temberature  
       2023-06-13 21:21:25 +08:00
    @llsongll 具体能说出来界面差在哪里吗 :)
    dvbs2000
        52
    dvbs2000  
       2023-06-13 22:16:16 +08:00
    能麻烦直接把对话的 url 共享出来么 谢谢~ gpt 现在是有共享功能的。直接就可以共享
    coolcfan
        53
    coolcfan  
       2023-06-14 00:17:52 +08:00
    @llsongll 之前推很大的 Flomo 是不是就是这种
    llsongll
        54
    llsongll  
       2023-06-14 09:38:54 +08:00
    @coolcfan #53 类似,我说的 memos 就是 flome 差不多的开源版
    llsongll
        55
    llsongll  
       2023-06-14 10:31:28 +08:00
    @temberature #51 我其实也说不出来个具体,可以简单描述下我需要的功能。比如 1 、微信的联系人可以看做 ob 不同的库,例:inbox ;剪藏;日记; note 等等,这个栏可折叠可隐藏可快捷键切换,我以前用 typora 时,就是分文件夹的,打标签的话,搜索的时候会多输入,再一个限制自己少分类,我一般都是用 alfred 搜索的,倒是无所谓,但是手机端搜索就会麻烦,这样会好点。
    2 、输入框在下面,焦点永远在输入框内,在写长文的时候可放大输入框,隐藏聊天记录,写完发送自动显示聊天记录,恢复刚才的界面。
    3 、消息框,右键复制、删除、引用、转发到其他库、多选
    4 、图片、文件、文字,标签和多选批量打标签
    5 、搜索:可在单独库搜索,可全部库搜索。可搜文件,图片,视频,链接,可日期排序。可定位到当时聊天的地方,可定位在 finder 中。微信或者 notion 那种弹出式搜索就非常舒服,ob 侧边栏搜索超级难受。
    6 、显示问题,我是用笔记本的,比较少用台式,我认为屏幕太大,会分散焦点,在我 13 寸屏幕上,显示三分之二或者五分之三会是一个比较合适的大小。
    ---
    以上这些应该是基本功能,下面我是常用的功能,每个笔记软件我都期待的。
    1 、速度,我调教后的 ob ,使用了 6 个插件,一个小主题,因为老电脑所以打开还是比较慢,但是常驻后台会占用我为数不多的内存,所以,要么打开速度快,要么占用低,比较个人,穷逼的苦楚。
    2 、快捷输入,mac:alfred 可以快捷输入到任何一个 md 文件中。手机端,启动软件后直接弹出输入框,因为要查看的话,不建议多点一下输入框外的部分,输入的话更需要快速,否则对于我这种老年人,怕不是过一会就忘了。。。
    3 、mac 和手机端剪藏,这个感觉 md 的都不太好,所以我现在都手动复制,不太了解。
    4 、本地。同步对我来说不是刚需,md 的同步都会被检测到,如何要同步反而数据库其实更安全。
    5 、界面简洁好看,我的 ob 比微信界面还小,没有侧边栏,标签栏,全部都隐藏了,光秃秃的。
    ---
    大概想的就是这些,我可能说的不太有逻辑性,你凑乎看看。。。其实刚需就只有快捷输入,和快捷搜索。其他的都是体验类,能忍受,但会随时想换一个试试。
    temberature
        56
    temberature  
       2023-06-15 13:18:57 +08:00
    @llsongll 我还在用 12 年的 mac :)
    基础功能主要考验细节把控能力,微信到今天打磨的确实挺不错的。
    我主要关注的还是核心需求,快分为两种,一种绝对值的快,类似技术性能上的参数;一种是能产生质变的快,笔记这件事,根本限制还是来自大脑的认知能力,人又不擅长同时处理多件事情,所以快可以让人集中应对更丰富内聚的内容。
    llsongll
        57
    llsongll  
       2023-06-17 19:26:47 +08:00
    @temberature #56 哈哈哈,我在用 13 年的 mac 。你说的对,ob 、vc 等对于老电脑来说,速度确实不丝滑。我想要的快捷输入和小界面,就像你说的集中应对。不过我是个设计师,所以可能对界面美观度要求更高一点。我找过一些软件,比如 nvALT ; FSnotes ;打开速度都很快,只是一些细节,样式不太好,所以我只拿来记录一些摘抄。你可以试一试,后者基于前者开发
    Oz37sW2w3MIZf56o
        58
    Oz37sW2w3MIZf56o  
       2023-06-17 22:58:07 +08:00
    歪个楼,求一个可以上 ChatGPT 不封号的梯子
    temberature
        59
    temberature  
       2023-06-18 16:32:35 +08:00
    @chenyduan 最近好多了,网络一般只是不让用,疯号好多是因为黑卡
    temberature
        60
    temberature  
       2023-06-18 16:37:09 +08:00
    @llsongll 感谢推荐,其实我在开发一个 IM 架构的给自己用,就是好奇为什么你也有这种需求,样式我喜欢简洁的那种,一般都不会太丑。
    llsongll
        61
    llsongll  
       2023-06-18 22:15:58 +08:00
    @temberature #60 哇~厉害,希望有机会我可以体验一下
    temberature
        62
    temberature  
       2023-06-19 13:37:38 +08:00
    @llsongll 等我再调整调整 :)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   857 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 21:43 · PVG 05:43 · LAX 13:43 · JFK 16:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.