爱意满满的作品展示区。
penzi

我做了一个飞书多维表格/钉钉 AI 表格的简单替代品

  •  
  •   penzi · 1 day ago · 1098 views

    Github: https://github.com/autable/autable

    主页以及文档: https://autable.github.io/

    最近我在尝试把一些公司内部的流程迁移到钉钉 AI 表格, 然而发现并不顺利

    1. 钉钉审批流程同步到 AI 表格之后, 会产生多条相同编号的记录, 钉钉的表单和自动化没有任何办法处理这个问题, 因为钉钉并没有把 unique 的 record id 暴露出来
    2. 钉钉 AI 表格的 AI 非常傻, 经常做不到一些简单的表单定义和自动化流程定义
    3. nocode 的自动化对于会写代码的人完全是负生产力
    4. 最大的问题, 即使付费, 默认最高也只有单表 5 万条记录

    我也调研了一下开源方案, 一个问题就是 OIDC 登录基本都是付费功能, 商业版 self-host 普遍定价比钉钉/飞书的最高级别的订阅还贵

    最终, 我搓了一个符合我需求的 AI 表格

    1. 支持表格, 自动化, 表单和字段级别的权限配置
    2. 自动化, 表格都使用简单的 js 定义, AI 友好的方式
    3. 支持公式字段, 公式同样使用 js 表达
    4. 存储使用 sqlite, 运维成本低, 最初的容灾方案每天备份一次就行了
    5. 支持 OIDC, GPL 3.0 license, 永远不用担心开原版缺失关键功能

    目前刚跑通一个使用案例, 可以使用自动化同步钉钉 AI 表格的数据, 已经可以渐进式迁移, 并且可以借助这个打通钉钉内部所有数据的同步

    下面是一个周期性同步钉钉 AI 表格的 workflow.js 例子

    function instances(info) {
      return {
        timer: "time.schedule",
        dingTable: "dingtalk.notable.records.list",
        fields: "table.field.create",
        rows: "table.row.upsert"
      };
    }
    
    function trigger(info) {
      return {
        instance: "timer",
        params: {
          interval_ms: 5 * 60 * 1000
        }
      };
    }
    
    function run(info) {
      const table = "同步表";
      let nextToken = "";
    
      while (true) {
        const page = info.instance("dingTable").exec({
          max_results: 100,
          ...(nextToken ? { next_token: nextToken } : {})
        });
    
        const records = page.records || [];
    
        info.instance("fields").exec({
          table,
          fields: fieldsOf(records)
        });
    
        for (const record of records) {
          info.instance("rows").exec({
            table,
            match_field: "dingtalk_record_id",
            values: valuesOf(record)
          });
        }
    
        nextToken = page.next_token || "";
        if (!page.has_more || !nextToken) break;
      }
    
      return { ok: true, table };
    }
    
    function fieldsOf(records) {
      const fields = {
        dingtalk_record_id: "string"
      };
    
      for (const record of records) {
        for (const name of Object.keys(record.fields || {})) {
          fields[name] = "string";
        }
      }
    
      return fields;
    }
    
    function valuesOf(record) {
      return {
        dingtalk_record_id: String(record.id || ""),
        ...Object.fromEntries(
          Object.entries(record.fields || {}).map(([name, value]) => [
            name,
            stableStringify(value)
          ])
        )
      };
    }
    
    Supplement 1  ·  1 day ago
    现在是一个非常原始的开发阶段, 建议不要用, 可能后续有很多破坏性修改
    5 replies    2026-06-23 11:42:31 +08:00
    michealzh
        1
    michealzh  
       1 day ago
    我更需要飞书的文档 目前还没有看到替代品
    iOCZS
        2
    iOCZS  
       1 day ago
    instances(info),trigger(info)参数都没用到
    penzi
        3
    penzi  
    OP
       1 day ago
    @iOCZS 嗯, AI 乱搓的, 现在看好像用不到

    @michealzh 我试过 https://github.com/outline/outline, 但是维护这样的服务很麻烦, 不如直接用大厂的
    512357301
        4
    512357301  
       13h 15m ago via Android
    nocodb 呢,试过吗,它不限制数量,不过权限系统比较简陋
    penzi
        5
    penzi  
    OP
       1h 10m ago
    @512357301 所有开源的类似产品的 OIDC 都是付费功能, nocodb 的商业授权 24 美金/人/每月
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3598 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 38ms · UTC 04:53 · PVG 12:53 · LAX 21:53 · JFK 00:53
    ♥ Do have faith in what you're doing.