<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[小破站]]></title><description><![CDATA[生活游记]]></description><link>https://tinsfox.com</link><image><url>https://tinsfox.comhttps://avatars.githubusercontent.com/u/33956589?v=4</url><title>小破站</title><link>https://tinsfox.com</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Thu, 25 Jun 2026 13:29:19 GMT</lastBuildDate><atom:link href="https://tinsfox.com/feed" rel="self" type="application/rss+xml"/><pubDate>Thu, 25 Jun 2026 13:29:19 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[用 Mac mini + Cloudflare Tunnel 搭建零成本 MR 预览环境]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tinsfox.com/posts/default/mac-mini-cloudflare-tunnel-mr-preview">https://tinsfox.com/posts/default/mac-mini-cloudflare-tunnel-mr-preview</a></blockquote><div><blockquote><p>每个 Merge Request 自动生成一个在线可访问的预览环境，团队可以直接在浏览器里验收功能，无需本地启动项目。</p></blockquote>
<h2 id="">我们要解决什么问题</h2><p>前后端全栈项目的 Code Review 有个痛点：<strong>光看代码 diff 很难判断功能是否正确</strong>。尤其是 UI 变更、API 联调、端到端流程验证，往往需要 Reviewer 本地拉代码、装依赖、启动服务才能验证。这极大地拖慢了 Merge Request 的流转速度。</p><p>我们希望做到：<strong>创建 MR 的那一刻，就自动生成一个只有这个 MR 代码版本的在线预览环境</strong>。Reviewer 打开链接就能看到效果，产品经理也能直接参与验收。</p><h2 id="">整体架构</h2><pre class=""><code class="">GitLab MR ──→ GitLab CI ──→ Mac mini Runner
                                │
                    ┌───────────┼───────────┐
                    │           │           │
              Docker Build  Docker Run  Health Check
                    │           │           │
                    └───────────┼───────────┘
                                │
                     Cloudflare Tunnel API
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                   │
    feature-mr-42.preview.xxx   │   fix-bug-mr-43.preview.xxx
              │                 │                   │
              └─────────────────┼─────────────────┘
                                │
                          用户浏览器访问
</code></pre>
<p>核心思路：<strong>用一台 Mac mini 作为 GitLab Runner，在上面跑 Docker 容器，再通过 Cloudflare Tunnel 把每个 MR 的容器暴露到公网</strong>。</p><h2 id="">为什么选择这个方案</h2><table><thead><tr><th> 方案 </th><th> 成本 </th><th> 复杂度 </th><th> 限制 </th></tr></thead><tbody><tr><td> 每次部署到 K8s 命名空间 </td><td> 高（集群资源） </td><td> 高（需要 Ingress 配置） </td><td> 命名空间管理复杂 </td></tr><tr><td> Vercel/Netlify Preview </td><td> 低 </td><td> 低 </td><td> 只支持纯前端项目 </td></tr><tr><td> <strong>Mac mini + Cloudflare Tunnel</strong> </td><td> <strong>极低</strong> </td><td> <strong>中等</strong> </td><td> <strong>容器数量受单机资源限制</strong> </td></tr></tbody></table><p>我们的项目是前后端一体部署（Node.js 服务 + Vite 前端打包到 <code>public/</code>），不是纯静态站点，所以 Vercel Preview 不适用。而每次为 MR 单独开 K8s 资源又太重。Mac mini 方案恰到好处：一台机器就能并行跑十几个预览环境。</p><h2 id="">逐步实现</h2><h3 id="1-gitlab-ci-pipeline-">1. GitLab CI Pipeline 触发</h3><p>只在 Merge Request 事件时触发预览部署：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">workflow:
  rules:
    - if: &#x27;$CI_PIPELINE_SOURCE == &quot;merge_request_event&quot;&#x27;
    - if: &#x27;$CI_PIPELINE_SOURCE == &quot;web&quot;&#x27;
    - when: never
</code></pre>
<p>预览环境用 <code>resource_group</code> 来串行化隧道操作（Cloudflare Tunnel 配置是共享资源），但允许不同 MR 并行部署：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml">deploy_preview_mr:
  resource_group: ai-canvas-preview-tunnel
  interruptible: true
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    url: $DYNAMIC_ENVIRONMENT_URL
    on_stop: stop_preview_mr
    auto_stop_in: 7 days
</code></pre>
<p><code>environment</code> 块是关键——它让 GitLab 在 MR 页面显示一个 &quot;View environment&quot; 按钮，点击直接跳转到预览 URL。<code>auto_stop_in: 7 days</code> 确保过期 MR 不会一直占用资源。</p><h3 id="2-">2. 生成唯一的预览域名</h3><p>每个 MR 需要一个唯一的子域名，格式为 <code>{branch-slug}-mr-{iid}.{domain}</code>：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">SOURCE_BRANCH_SLUG=&quot;$(
  printf &#x27;%s&#x27; &quot;${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}&quot; |
    tr &#x27;[:upper:]&#x27; &#x27;[:lower:]&#x27; |
    sed -E &#x27;s/[^a-z0-9-]+/-/g; s/-+/-/g; s/^-//; s/-$//&#x27;
)&quot;

# DNS 标签最长 63 字符，预留 &quot;-mr-{iid}&quot; 的空间
MAX_SOURCE_BRANCH_SLUG_LENGTH=$((63 - 4 - ${#CI_MERGE_REQUEST_IID}))
if [ &quot;${#SOURCE_BRANCH_SLUG}&quot; -gt &quot;${MAX_SOURCE_BRANCH_SLUG_LENGTH}&quot; ]; then
  SOURCE_BRANCH_SLUG=&quot;$(printf &#x27;%s&#x27; &quot;${SOURCE_BRANCH_SLUG}&quot; | cut -c &quot;1-${MAX_SOURCE_BRANCH_SLUG_LENGTH}&quot; | sed -E &#x27;s/-$//&#x27;)&quot;
fi

PREVIEW_HOST=&quot;${SOURCE_BRANCH_SLUG}-mr-${CI_MERGE_REQUEST_IID}.${PREVIEW_DOMAIN_ROOT}&quot;
</code></pre>
<p>例如分支 <code>feat/video-export</code> 的 MR #42 会得到域名 <code>feat-video-export-mr-42.preview.example.com</code>。</p><h3 id="3-docker-">3. Docker 构建与容器管理</h3><p>在 Mac mini 上直接构建和运行 Docker 容器：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 构建时注入 MR 标签，方便后续清理
docker build \
  --label ai-canvas.preview=true \
  --label &quot;ai-canvas.preview.mr=${CI_MERGE_REQUEST_IID}&quot; \
  --label &quot;ai-canvas.preview.sha=${CI_COMMIT_SHORT_SHA}&quot; \
  -t &quot;${PREVIEW_IMAGE}&quot; \
  .

# 运行容器，限制资源
docker run -d \
  --name &quot;${PREVIEW_CONTAINER_NAME}&quot; \
  --restart unless-stopped \
  --network &quot;${PREVIEW_DOCKER_NETWORK}&quot; \
  --memory=1g --memory-swap=1g --cpus=0.5 \
  --label ai-canvas.preview=true \
  --label &quot;ai-canvas.preview.mr=${CI_MERGE_REQUEST_IID}&quot; \
  --env-file &quot;${PREVIEW_RUNTIME_ENV_FILE}&quot; \
  -e &quot;REDIS_KEY_PREFIX=preview:mr-${CI_MERGE_REQUEST_IID}:&quot; \
  -e &quot;BETTER_AUTH_URL=https://${PREVIEW_HOST}&quot; \
  &quot;${PREVIEW_IMAGE}&quot;
</code></pre>
<p>注意几个设计细节：</p><ul><li><strong>资源限制</strong>：每个容器最多 1GB 内存、0.5 CPU，防止单个预览拖垮宿主机</li><li><strong>Docker Label</strong>：用标签标记 MR 编号和 commit SHA，后续精准清理</li><li><strong>Redis Key Prefix</strong>：不同 MR 用不同的 key 前缀，共享 Redis 实例不会互相干扰</li><li><strong>动态环境变量</strong>：<code>BETTER_AUTH_URL</code>、<code>CORS_ALLOWED_ORIGINS</code> 等都根据预览域名动态生成</li></ul><h3 id="4-">4. 环境变量安全管理</h3><p>预览环境需要真实的服务端密钥（数据库、Redis、第三方 API 等）。我们把敏感配置存为 GitLab File Variable，在 CI 中通过脚本校验后写入临时文件：</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// scripts/gitlab/normalize-preview-env.mjs
// 校验必要的环境变量是否存在、URL 格式是否正确、枚举值是否合法
const requiredKeys = new Set([
  &#x27;DATABASE_URL&#x27;, &#x27;REDIS_URL&#x27;, &#x27;BETTER_AUTH_SECRET&#x27;,
  &#x27;TOS_ACCESS_KEY_ID&#x27;, &#x27;FEISHU_APP_ID&#x27;, ...
])

const missingKeys = [...requiredKeys].filter(key =&gt; !env.has(key))
if (missingKeys.length &gt; 0) {
  fail(`Missing required keys: ${missingKeys.join(&#x27;, &#x27;)}.`)
}

// 输出文件权限设为 0600，仅 owner 可读写
writeFileSync(outputPath, ..., { mode: 0o600 })
</code></pre>
<p>这样做的好处是：</p><ul><li>敏感信息不硬编码在 CI 配置里</li><li>有完整的校验逻辑，缺配置时提前报错而不是运行时才挂</li><li>共享数据库和 Redis（开发环境实例），不同 MR 通过 key 前缀隔离</li></ul><h3 id="5-cloudflare-tunnel-">5. Cloudflare Tunnel 动态路由</h3><p>这是整个方案最精巧的部分。我们在 Mac mini 上跑了一个常驻的 <code>cloudflared</code> 容器，它维护着一条到 Cloudflare 的隧道。然后通过 Cloudflare API 动态修改隧道的 ingress 规则，把不同的子域名路由到不同的预览容器：</p><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// scripts/gitlab/preview-tunnel.mjs
async function up() {
  // 获取当前隧道配置
  const current = await getConfiguration()

  // 添加新规则：preview-host → preview-container:3000
  const previewRule = {
    hostname: context.previewHost,
    service: `http://${context.previewContainerName}:3000`,
    originRequest: {
      connectTimeout: 60,
      disableChunkedEncoding: true,
      httpHostHeader: context.previewHost,
    },
  }

  // 替换同域名的旧规则，保留其他 MR 的规则
  config.ingress = [
    ...rules.filter(rule =&gt; rule.hostname !== context.previewHost),
    previewRule,
    catchAll,  // 必须以 catch-all 结尾
  ]

  await putConfiguration(config)
}
</code></pre>
<p><code>down</code> 操作则反过来：移除对应域名的 ingress 规则。这样同一个 Cloudflare Tunnel 可以同时服务多个 MR 预览环境，互不干扰。</p><h3 id="6-">6. 双重健康检查</h3><p>部署完成后有两次健康检查，确保用户访问时服务已经完全就绪：</p><p><strong>第一轮：容器内部健康检查</strong>（Docker 网络内）</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 从另一个容器通过 Docker 网络访问，不经过公网
for i in $(seq 1 60); do
  if docker run --rm --network &quot;${PREVIEW_DOCKER_NETWORK}&quot; curlimages/curl:latest \
    -fsS -H &quot;Host: ${PREVIEW_HOST}&quot; \
    &quot;http://${PREVIEW_CONTAINER_NAME}:3000/api/health&quot; &gt;/dev/null; then
    break
  fi
  sleep 2
done
</code></pre>
<p><strong>第二轮：公网健康检查</strong>（通过 Cloudflare Tunnel）</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 通过公网 URL 访问，验证端到端链路
for i in $(seq 1 60); do
  if curl -fsS &quot;https://${PREVIEW_HOST}/api/health&quot; &gt;/dev/null; then
    break
  fi
  sleep 2
done
</code></pre>
<p>如果第二轮公网检查失败，会自动回滚隧道配置到上一个容器（如果存在的话），或者清理隧道入口，避免指向一个不可用的容器。</p><h3 id="7-">7. 自动清理机制</h3><p>预览环境有完善的清理策略：</p><pre class="language-yaml lang-yaml"><code class="language-yaml lang-yaml"># MR 关闭/合并时手动或自动清理
stop_preview_mr:
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    action: stop
  script:
    - node scripts/gitlab/preview-tunnel.mjs down  # 移除隧道路由
    - docker rm -f $(容器列表)                       # 删除容器
    - docker rmi $(镜像列表)                         # 删除镜像

# 手动清理 7 天前的旧镜像
cleanup_preview_images:
  script:
    - docker image prune -a -f --filter label=ai-canvas.preview=true --filter until=168h
</code></pre>
<p>此外，每次部署新 commit 时也会自动清理同一个 MR 的旧容器和旧镜像：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 删除同一 MR 的旧容器
for cid in $(docker ps -aq --filter &quot;label=ai-canvas.preview.mr=${CI_MERGE_REQUEST_IID}&quot;); do
  sha=&quot;$(docker inspect -f &#x27;{{ index .Config.Labels &quot;ai-canvas.preview.sha&quot; }}&#x27; &quot;${cid}&quot;)&quot;
  if [ &quot;${sha}&quot; != &quot;${CI_COMMIT_SHORT_SHA}&quot; ]; then
    docker rm -f &quot;${cid}&quot;
  fi
done
</code></pre>
<h3 id="8-">8. 容器数量限制</h3><p>单机资源有限，需要防止预览容器过多：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">PREVIEW_MAX_CONTAINERS=&quot;${PREVIEW_MAX_CONTAINERS:-10}&quot;
TOTAL_PREVIEW_CONTAINER_COUNT=&quot;$(docker ps -q --filter label=ai-canvas.preview=true | wc -l)&quot;

if [ &quot;${TOTAL_PREVIEW_CONTAINER_COUNT}&quot; -ge &quot;${PREVIEW_MAX_CONTAINERS}&quot; ]; then
  # 如果当前 MR 已有预览容器，允许更新
  # 否则拒绝部署，提示先清理旧环境
fi
</code></pre>
<h3 id="9--schema-">9. 数据库 Schema 变更检测</h3><p>因为多个 MR 共享同一个开发数据库，如果某个 MR 修改了数据库 Schema 但没有提前执行迁移，其他 MR 的预览可能会报错。我们在部署时自动检测：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">SCHEMA_CHANGED_FILES=&quot;$(
  git diff --name-only &quot;${CI_MERGE_REQUEST_DIFF_BASE_SHA}&quot; &quot;${CI_COMMIT_SHA}&quot; |
    grep -E &#x27;^(apps/server/drizzle/|apps/server/src/lib/db/schema/)&#x27; || true
)&quot;
if [ -n &quot;${SCHEMA_CHANGED_FILES}&quot; ]; then
  echo &#x27;WARNING: This MR changes database schema files.&#x27;
  echo &#x27;Apply the matching migration on the shared test DB before validating preview behavior.&#x27;
fi
</code></pre>
<h2 id="">最终效果</h2><p>开发者的工作流变成这样：</p><ol start="1"><li>在分支上开发功能，推送到 GitLab</li><li>创建 Merge Request</li><li>GitLab CI 自动在 Mac mini 上构建并部署预览环境</li><li>MR 页面出现 &quot;View environment&quot; 按钮，点击即可访问预览</li><li>Reviewer 和产品经理直接在预览环境上验收</li><li>MR 合并或关闭后，预览环境自动清理</li></ol><pre class=""><code class="">Merge Request #42
├── 💬 Discussion
├── 📝 Code Changes
└── 🌐 View environment → https://feat-video-export-mr-42.preview.example.com
</code></pre>
<h2 id="">总结</h2><table><thead><tr><th> 组件 </th><th> 作用 </th></tr></thead><tbody><tr><td> Mac mini Runner </td><td> 提供 Docker 运行环境，成本极低 </td></tr><tr><td> Cloudflare Tunnel </td><td> 免费的内网穿透，动态路由到不同容器 </td></tr><tr><td> Docker Label </td><td> 精准管理容器生命周期（按 MR 维度清理） </td></tr><tr><td> 双重健康检查 </td><td> 确保端到端链路可用 </td></tr><tr><td> 环境变量规范化 </td><td> 安全地共享敏感配置 </td></tr><tr><td> GitLab Environment </td><td> MR 页面直接展示预览链接 </td></tr></tbody></table><p>整个方案的核心开销就是一台 Mac mini 和一个 Cloudflare 免费隧道。对于中小团队来说，这是一个性价比极高的 MR 预览环境方案。</p><hr/><p><em>如果你的团队也在做全栈项目、也有 Code Review 验收的痛点，不妨试试这个方案。欢迎交流。</em></p></div><p style="text-align:right"><a href="https://tinsfox.com/posts/default/mac-mini-cloudflare-tunnel-mr-preview#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tinsfox.com/posts/default/mac-mini-cloudflare-tunnel-mr-preview</link><guid isPermaLink="true">https://tinsfox.com/posts/default/mac-mini-cloudflare-tunnel-mr-preview</guid><dc:creator><![CDATA[TinsFox]]></dc:creator><pubDate>Sun, 24 May 2026 03:20:01 GMT</pubDate></item><item><title><![CDATA[2024 End 2025 Start]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tinsfox.com/posts/default/2024-2025">https://tinsfox.com/posts/default/2024-2025</a></blockquote><div><h2 id="2024">2024</h2><ul><li>写了个 electron 项目，被归档了（更换产品形态）。</li><li>写了个火急火燎的 web 发了几个版本后无声无息归档了。跟产品对接不顺利，可能是要求有点多。被 hr 约谈了。整得有点怀疑自我，开始会焦虑</li><li>成了一块砖，那里需要去哪里</li><li>写了些开源，有些用户有关注🥰</li><li>白嫖社区赠票去了 HDC</li><li>去杭州参加了 #AdventureX 只会用 AI 提效的我像个傻子🫠不过认识了很多很棒的人</li><li>启动了业余项目尝试</li><li>团队整顿，我的要求不再是特立独行的要求。在年末的小尾巴被邀请任职业务线技术负责人，开始新的挑战（适应中</li></ul><h2 id="2025">2025</h2><ul><li>跑多几次步，最近动不动就不舒服，发烧</li><li>学着管理团队</li><li>看书，以前不爱看书</li><li>继续做开源</li><li>做一些（垃圾）内容输出</li><li>尝试不同的项目</li></ul><p>感谢  @akazwz_ ak 哥 和 酱酱（不玩推的大佬） 陪我玩🥰</p><p>还有个丢个 bug 过去会帮我看甚至 debug 的朋友 玄山 🌚</p><p>Finny 感谢各位网友😁 新年快乐🎉</p></div><p style="text-align:right"><a href="https://tinsfox.com/posts/default/2024-2025#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tinsfox.com/posts/default/2024-2025</link><guid isPermaLink="true">https://tinsfox.com/posts/default/2024-2025</guid><dc:creator><![CDATA[TinsFox]]></dc:creator><pubDate>Tue, 31 Dec 2024 15:12:02 GMT</pubDate></item><item><title><![CDATA[内网穿透]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tinsfox.com/posts/homelab/internet-penetration">https://tinsfox.com/posts/homelab/internet-penetration</a></blockquote><div><h2 id="">前言</h2><p>经常会有一些服务想要部署在局域网里面的电脑上，但是又需要从公网访问服务。我们通常会有几种选择</p><ol start="1"><li>购买 VPS。买了 VPS 之后，一般是会给你分配一个 IPv4 地址的，通过这个地址，就可以访问部署在这个 VPS 上的服务了。</li><li>使用 Serverless 服务或者是 PaaS 服务</li><li>部署在手头上已有的设备上，通过网络穿透的方式把 <strong>内网</strong> 的服务暴露在 <strong>公网</strong> 上</li></ol><blockquote><p>本文不会讲解什么是内网/公网</p></blockquote>
<p>本文会简单记录 (cloudflare tunnel)[https://www.cloudflare.com/products/tunnel/] 的基本使用
(cloudflare tunnel 文档)[https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/]</p>
<h2 id="">安装</h2></div><p style="text-align:right"><a href="https://tinsfox.com/posts/homelab/internet-penetration#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tinsfox.com/posts/homelab/internet-penetration</link><guid isPermaLink="true">https://tinsfox.com/posts/homelab/internet-penetration</guid><dc:creator><![CDATA[TinsFox]]></dc:creator><pubDate>Mon, 07 Oct 2024 08:44:06 GMT</pubDate></item><item><title><![CDATA[Uni App 项目搭建之 CLI]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tinsfox.com/posts/default/uni-app-cli-project-setup">https://tinsfox.com/posts/default/uni-app-cli-project-setup</a></blockquote><div><h1 id="uni-app--cli">Uni App 项目搭建之 CLI</h1><hr/><blockquote><h2 id="">博客已经很久没有更新了，下面的命令可能已经不再适用</h2></blockquote>
<h2 id="">项目搭建</h2><h3 id="">方式一</h3><p>通过<a href="https://www.dcloud.io/hbuilderx.html"><code>HBuilderX</code></a> 可视化界面生成 <a href="https://uniapp.dcloud.io/quickstart-hx?id=%e5%88%9b%e5%bb%bauni-app">传送门</a></p><h3 id="">方式二</h3><p>通过 <code>vue-cli</code> 命令行 <a href="https://uniapp.dcloud.io/quickstart-cli">传送门</a></p><p>使用正式版</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">vue create -p dcloudio/uni-preset-vue my-project
</code></pre>
<p>使用alpha版</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">vue create -p dcloudio/uni-preset-vue#alpha my-alpha-project
</code></pre>
<h3 id="">对比</h3><table><thead><tr><th>     </th><th> <strong>可视化界面</strong>           </th><th> <strong>cli脚手架</strong>        </th></tr></thead><tbody><tr><td> 优点. </td><td> 方便，傻瓜式操作            </td><td> 可定制性更强            </td></tr><tr><td> 缺点  </td><td> 无法使用自动化部署(下面有个设想方案) </td><td> 创建的时候需要加入参数控制创建版本 </td></tr></tbody></table><h3 id="">总结</h3><h4 id="">尝试解决缺点</h4><ul><li>上面提到的可视化界面创建的项目无法使用自动化部署</li></ul><p>思路：初步设想方案是可以在项目中使用命令启动项目</p><ol start="1"><li>使用你喜欢的包管理器进行初始化</li></ol><pre class="language-shell lang-shell"><code class="language-shell lang-shell"># 初始化 包管理
yarn init # npm init
</code></pre>
<ol start="2"><li>在 <code>package.json</code> 中添加一些<code>scripts</code></li></ol><pre class="language-json lang-json"><code class="language-json lang-json">{
&quot;scripts&quot;: {
     &quot;dev:mp-weixin&quot;: &quot;cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch&quot;,
   }
 }
</code></pre>
<ol start="3"><li>启动脚本</li></ol><p>这里只要你启动成功了，那么说明这个方案是可行的，但是研究了一下，在工具创建的项目它依赖的环境都是工具集成的，所以直接运行应该是找不到依赖，实验没有继续下去，有兴趣的朋友可以尝试一下，但是我认为这个应该是不行的 手动滑稽.jpg</p><ul><li>cli创建的项目无法自动打开模拟器</li></ul><p>思路：使用模拟器的<a href="https://developers.weixin.qq.com/miniprogram/dev/devtools/cli.html">命令行工具</a>来解决，这里使用<code>shelljs</code>编写脚本</p><ol start="1"><li>安装依赖</li></ol><pre class="language-shell lang-shell"><code class="language-shell lang-shell">npm install shell fs
</code></pre>
<ol start="2"><li>在<code>src</code>下创建<code>shell.js</code></li></ol><pre class="language-javascript lang-javascript"><code class="language-javascript lang-javascript">// shell.js 这是一个js文件，使用shelljs来执行shell
const shell = require(&#x27;shelljs&#x27;);
const fs = require(&#x27;fs&#x27;);
const BLUE_COLOR = &quot;\033[36m&quot;
const RED_COLOR = &quot;\033[31m&quot;
const GREEN_COLOR = &quot;\033[32m&quot;
const VIOLET_COLOR = &quot;\033[35m&quot;
const line = `# ######################################################################`
const RES = &quot;\033[0m&quot;
const args = require(&#x27;minimist&#x27;)(process.argv.slice(2)) // 获取命令行脚本执行参数
const darwinCliPath = &quot;/Applications/wechatwebdevtools.app/Contents/MacOS/cli&quot; // macOS系统开发者工具位置
const winCliPath = &quot;xxxx&quot; # windows系统开发者工具位置
let path = {
  devPath: `${process.cwd()}/dist/dev/mp-weixin`, // 获取开发环境下的打包目录
  prodPath: `${process.cwd()}/dist/build/mp-weixin` // 获取生产环境下的打包目录
}
let cliPath = &#x27;&#x27;
/**
 * 检测系统平台
 */
function checkPlatform() {
  if (process.platform == &quot;Linux&quot; || process.platform == &#x27;darwin&#x27;) {
    cliPath = darwinCliPath
  } else {
    cliPath = darwinCliPath
  }
}
/**
* 脚本主方法
*/
function main() {
  lineEcho(GREEN_COLOR)
  shell.echo(`${BLUE_COLOR}#                       Uni-app build Script                       #${RES}`)
  lineEcho(GREEN_COLOR)
  const env = args[&#x27;env&#x27;]
  checkPlatform()
  build(env)
}
/**
* 打开模拟器
*/
function openCli() {
  shell.exec(`${cliPath} open`)
}
/**
* 开发项目
*/
function openProject(path) {
  openCli()
  shell.exec(`${cliPath} open --project ${path}`)
}
/**
* 进行构建
*/
function build(env) {
  shell.echo(`${BLUE_COLOR}#                       当前构建环境是${env}                        #${RES}`)
  let realPath = path[`${env}Path`]
  switch (env) {
    case &#x27;dev&#x27;:
      fs.exists(realPath, function (exists) {
        if (exists) {
          shell.echo(`${VIOLET_COLOR}#                       正在开发构建                       #${RES}`);
          openProject(realPath)
          shell.exec(&#x27;yarn dev:mp-weixin&#x27;);
        }
        if (!exists) {
          shell.echo(`${VIOLET_COLOR}#                         初次构建                          #${RES}`);
          shell.exec(&#x27;yarn dev:mp-weixin-no-watch&#x27;);
          openProject(realPath)
          shell.exec(&#x27;yarn dev:mp-weixin&#x27;);
        }
      })
      break;
    case &#x27;prod&#x27;:
      shell.echo(`${VIOLET_COLOR}#                     正在发布小程序，请稍后                       #${RES}`);
      shell.exec(&#x27;yarn build:mp-weixin&#x27;);
      openProject(realPath)
      break;
    case &#x27;upload&#x27;:
      lineEcho(GREEN_COLOR)

      shell.echo(`${VIOLET_COLOR}#                     正在发布小程序，请稍后                       #${RES}`);
      lineEcho(GREEN_COLOR)
      shell.echo(`${VIOLET_COLOR}#                            正在打包                               #${RES}`);
      lineEcho(GREEN_COLOR)

      shell.exec(&#x27;yarn build:mp-weixin&#x27;);
      shell.echo(`${VIOLET_COLOR}#                         打包完成                               #${RES}`);
      lineEcho(GREEN_COLOR)

      upload();
      break
    default:
      lineEcho(RED_COLOR)
      shell.echo(`${RED_COLOR}#                       脚本启动失败，请手动编译                       #${RES}`);
      break;
  }
}

function lineEcho(color) {
  shell.echo(`${color}${line}${RES}`);
}
/**
* 打包直接上传体验版
*/
function upload() {
  lineEcho(GREEN_COLOR)
  shell.echo(`${VIOLET_COLOR}#                       打包发布体验版微信小程序                       #${RES}`);
  lineEcho(GREEN_COLOR)
  try {
    const versionConfig = JSON.parse(fs.readFileSync(&#x27;./src/version.json&#x27;, &#x27;utf8&#x27;))
    lineEcho(GREEN_COLOR)
    console.log(&#x27;小程序的版本是&#x27;, versionConfig.version);
    console.log(&#x27;本次更新内容&#x27;, versionConfig.description);
    console.log(&#x27;author&#x27;, versionConfig.author);
    lineEcho(GREEN_COLOR)
    const {
      version,
      description
    } = versionConfig
    let realPath = path[`prodPath`]
    lineEcho(GREEN_COLOR)
    shell.echo(`${GREEN_COLOR}#                       打包完成，上传中                       #${RES}`);
    shell.exec(` ${cliPath} upload --project ${realPath} -v ${version} -d ${description}`)
    shell.echo(`${GREEN_COLOR}#                       上传完成                       #${RES}`);
    lineEcho(GREEN_COLOR)
  } catch (err) {
    console.error(err)
  }
}

main()
</code></pre>
<ol start="3"><li>在 <code>package.json</code> 中添加<code>scripts</code></li></ol><pre class="language-json lang-json"><code class="language-json lang-json">{
      &quot;dev:shell&quot;: &quot;node ./src/shell.js --env=dev&quot;,
    &quot;build:shell&quot;: &quot;node ./src/shell.js --env=prod&quot;,
    &quot;prod:upload&quot;: &quot;node ./src/shell.js --env=upload&quot;,
    &quot;build-upload&quot;: &quot;node ./src/shell.js --env=upload&quot;,
    &quot;dev:mp-weixin-no-watch&quot;: &quot;cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build&quot;,
}
</code></pre>
<ol start="4"><li>如果你需要避免看到模拟器，然后直接生产上传体验版，添加<code>/src/version.json</code></li></ol><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;version&quot;: &quot;版本号&quot;,
  &quot;author&quot;: &quot;我是谁&quot;,
  &quot;description&quot;: &quot;我做了什么&quot;
}
</code></pre>
<ol start="5"><li>enjoy it！</li></ol><p>| script       | 作用                                                         |</p><p>| ------------ | ------------------------------------------------------------ |</p><p>| dev:shell    | 进行开发构建并使用微信开发工具打开                           |</p><p>| build:shell  | 进行生产构建并使用微信开发工具打开                           |</p><p>| build-upload | 进行生产构建直接读取<code>/src/version.json</code>文件注入上传信息，上传体验版 |</p><p>两种创建的方式各有优缺点，但也是有方案可以解决的。但是我个人来说更加喜欢使用<code>vue-cli</code>来创建项目，使用shell来辅助（微信开发者工具是不是会抽风卡住不懂，使用脚本进行发版效率显著提高）</p></div><p style="text-align:right"><a href="https://tinsfox.com/posts/default/uni-app-cli-project-setup#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tinsfox.com/posts/default/uni-app-cli-project-setup</link><guid isPermaLink="true">https://tinsfox.com/posts/default/uni-app-cli-project-setup</guid><dc:creator><![CDATA[TinsFox]]></dc:creator><pubDate>Wed, 25 Sep 2024 08:48:28 GMT</pubDate></item><item><title><![CDATA[n8n 转发 Gmail 邮件到 telegram]]></title><description><![CDATA[<link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B89B074F-CD7E-47B4-82F3-88B280F26484_2/X4acPDA7nxTrbU3SyuR9rhpQxA2tBzCNIzX8rZP6IpMz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B5B0D691-D19B-4C30-9180-C1F76111F255_2/uzhiqwsiNRZXMhpkiZDOvF86DkkwP6DAhC6JZBsKuhAz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/E80B08AE-4E48-4214-834E-81A2EB290CD0_2/kQ4DuuIDZ5lZvD8NoD06JXA3byeIthGQzJA9UGeo8lIz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/4D32905E-B47B-4A35-8F35-FA706E8A1136_2/Z5qCSx7kyUmudOpv15sdAgFb5zJmqb1I1hLVugUTJlUz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/AE047452-2EB9-451D-B050-4DD7F99C52CA_2/sq0yxgqpO2sf8WXS7NX4OERObv99S099FUQpIFfTyHoz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/9B5791B2-63D7-45D2-9853-5F4BC5E2066E_2/Hv4whiJHZn0mct663BwkGiyslX9tVCCEqFiOKlDrhgEz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/BC05EE71-42A3-4376-B233-57941DC152F0_2/8EYNeKphl280kXpAqLYWMFyXzcxVwl0F3tgpdc886pIz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/8781E6C9-9505-4462-B47A-225E0091D744_2/TmtnjHy809QHlxSw0K42wSFYx7IxDTKfBlPW0HkMwmoz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/EA05E8A6-BC9A-4E61-8761-644033BEF0B6_2/Ac5jvni99ya1T4DxAo8Pupj3U2RfAcYoVxckara8n64z/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/D1565731-3F7E-41D0-9AF2-0F7D150733D2_2/B5MwlzOSoxTaQ54tIQiy6j4awnmaABxIjoPbLi53U3Uz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/7F42DFB7-1CC6-4AD2-AC48-C6709F79BF86_2/2DDXmymDVHNkI4yHqA03i4snyezmP3iy0kvSpIdzDYoz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/1123F914-AAC9-4608-A814-0904C7227F1C_2/iOWyQfZte45F7LkvgrWXuBU4waqYXYaXuYPpXs8guT4z/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B469A670-E6B9-4BF5-86CE-80272D5FA848_2/Jnoh8RFMdURmNEc2UauOQyQXkKst2ylhdaSwOV4TGdwz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/2986BFEC-A569-4E18-9C34-3DE07DD5AB2B_2/IY0oQ5otFDxIjysv1uBIcoJFpHMyPeK75QBBDaSG27wz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/C38863DF-F26A-49A4-B723-3A6101E538EC_2/lprsDoeOyd5OQ3ROVmBZ6KHHIQqAASdzcW3BZzPLbfQz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/61EA0341-A627-4BC6-ADE5-CFDD3C3BD85B_2/2vEZOpLnEfIRikxQM7bw7la8GAyAwEhocytIZuehEx0z/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/22485069-6DA1-4F44-B81B-0566651D8FBF_2/97OUpxpgANjGyjLIE8xxf91psx37IWE9Q8AV5Rc7oUQz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/2B81D03E-7477-46E9-9F7A-25AEDA00887E_2/7LoRnjfl8MyYMTjyexxvTU9hjlxNtPeyXSeP92dx8xUz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/D1466523-96B3-4C6E-B11F-236E4828041B_2/9Vox3X2p8xyb3KJeupP3LNdUNDuK6NVZhvnRJk6DxHAz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/88A51C66-0BE5-44A9-B586-E882A23F1920_2/PmZIr561DtywaZS3paVU8L8vobj3acs2022wxmGkwC0z/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/6C962B39-D7CA-49D6-B5DB-D32550569ED0_2/ec1ycoxaGqJ9B6C8rQQ2YmhPTPRW5nfrkYbvCGjv7W0z/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/7C2680E7-6E65-4FBE-B5C1-B97E3F939948_2/t9hxuO4R8FYLrF6xf6hDQO0tOHuN5jmPlTpKCP8Czicz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/9723F69C-A5D4-484A-BE72-E4E00F14F6AA_2/CjdVuiF2GWI11WyLxpjAUAHkeS5SYaLGCGHuz3HjRDIz/Image.png"/><link rel="preload" as="image" href="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/31368F5D-988E-4B43-8712-16B0B2BB8564_2/DTgDIYTAzUDdrRULCEQJhl9suVEntjkRTlKiOUwWbFMz/Image.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tinsfox.com/posts/default/n8n-gmail-to-telegram-forwarding">https://tinsfox.com/posts/default/n8n-gmail-to-telegram-forwarding</a></blockquote><div><p>TL;DR</p><ol start="1"><li>部署 n8n</li><li>创建 谷歌服务</li><li>创建 n8n workflow</li><li>设置 Gmail Trigger</li><li>设置转发到 telegram</li></ol><h2 id="-n8n">部署 n8n</h2><p>这里使用的是 <code>docker-compose</code> 部署，同时，因为我之前就已经完成了内网穿透，所以跟公网访问的效果是一样的，如果你不知道怎么穿透，请使用 <strong>公网服务器</strong> 部署或者是使用 n8n 的 cloud 服务</p><p>使用 <code>docker-compose</code> 部署 n8n 需要两个文件，<code>docker-compose.yml</code> 和 <code>.env</code></p><ol start="1"><li><code>docker-compose.yml</code></li></ol><pre class="language-dockerfile lang-dockerfile"><code class="language-dockerfile lang-dockerfile"># docker-compose.yml
version: &quot;3.7&quot;

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: always
    # ports:
    #   - &quot;127.0.0.1:5678:5678&quot;
    environment:
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      # - N8N_PROTOCOL=https # 因为我在外部处理了 https， 所以把它的注释了
      - NODE_ENV=production
      - WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
    volumes:
      - ./n8n_data:/home/node/.n8n # 把 n8n 的数据挂在到当前的目录下的 n8n_data 中
</code></pre>
<ol start="2"><li><code>.env</code></li></ol><pre class="language-dockerfile lang-dockerfile"><code class="language-dockerfile lang-dockerfile"># 你的域名
DOMAIN_NAME=example.com

# 设置访问 n8n 的域名
SUBDOMAIN=n8n

# 上面的例子设置之后访问 n8n 的域名是 https://n8n.example.com

# 设置时区
GENERIC_TIMEZONE=Asia/Shanghai

# The email address to use for the SSL certificate creation
SSL_EMAIL=user@example.com # 不知道干啥的，应该是申请 SSL 的，但是我不需要，就没有改了
</code></pre>
<ol start="3"><li>启动 n8n 容器 <code>docker-compose up -d</code> 然后访问 <a href="https://n8n.example.com">https://n8n.example.com</a> 就可以进入你部署的 n8n 了，要注意，你时候加了 SSL ，如果没有的话，访问的是 <a href="http://n8n.example.com">http://n8n.example.com</a></li></ol><h2 id="-google-oauth2-single-service">创建 Google OAuth2 single service</h2><p>官方文档在这👉 <a href="https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/">https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/</a></p><blockquote><p>原理是创建一个 OAuth App ，把谷歌账号授权给这个 App ，然后App 开启 Gmail 的相关权限，App 就可以获得 Gmail 的访问了</p></blockquote>
<h4 id="">如果你的语言不是中文的，可以在右上角找入口把语言改成中文，或者对照上方的官方文档</h4><p>开始我们的操作吧~</p><ol start="1"><li>首先，点击菜单，找到 <strong>API 和服务</strong> ⇒  <strong>凭据</strong>，进入进入（如下图）</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B89B074F-CD7E-47B4-82F3-88B280F26484_2/X4acPDA7nxTrbU3SyuR9rhpQxA2tBzCNIzX8rZP6IpMz/Image.png" alt="Image.png" height="898" width="2030"/></p><ol start="1"><li>点击 <strong>创建凭据</strong>，选择 <strong>OAuth 客户端 ID</strong>，然后填写好创建的表单（如下图）</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B5B0D691-D19B-4C30-9180-C1F76111F255_2/uzhiqwsiNRZXMhpkiZDOvF86DkkwP6DAhC6JZBsKuhAz/Image.png" alt="Image.png" height="728" width="1882"/></p><ol start="1"><li>填写表单，应用网络类型选择“Web 应用”，名称随便填。<strong>已获授权的重定向 URI</strong> 填写 n8n 生成的地址，形如 <a href="https://n8n.example.com/rest/oauth2-credential/callback">https://n8n.example.com/rest/oauth2-credential/callback</a> 把 <a href="https://n8n.example.com">https://n8n.example.com</a> 修改成你自己的 n8n 网址（如果你不想自己写，可以先到后面找一下 n8n 帮你生成的）（如下图）</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/E80B08AE-4E48-4214-834E-81A2EB290CD0_2/kQ4DuuIDZ5lZvD8NoD06JXA3byeIthGQzJA9UGeo8lIz/Image.png" alt="Image.png" height="1810" width="2408"/></p><ol start="1"><li>创建完成之后，会弹窗，::记下:: <strong>客户端 ID</strong>(Client ID) 和 <strong>客户端密钥</strong>(Client Secret)</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/4D32905E-B47B-4A35-8F35-FA706E8A1136_2/Z5qCSx7kyUmudOpv15sdAgFb5zJmqb1I1hLVugUTJlUz/Image.png" alt="Image.png" height="1460" width="1848"/></p><ol start="1"><li>创建了 App 之后，你可能需要补充一个授权页面，我已经填写过了，所以没有提示，不然在后面的编辑 OAuth App 上方会有提示（如下图）</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/AE047452-2EB9-451D-B050-4DD7F99C52CA_2/sq0yxgqpO2sf8WXS7NX4OERObv99S099FUQpIFfTyHoz/Image.png" alt="Image.png"/></p><ol start="1"><li>点击添加用户，把你需要转发的 Gmail 邮箱填写进去（如下图）</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/9B5791B2-63D7-45D2-9853-5F4BC5E2066E_2/Hv4whiJHZn0mct663BwkGiyslX9tVCCEqFiOKlDrhgEz/Image.png" alt="Image.png"/></p><ol start="7"><li>启动 Gmail API，在菜单点击 <strong>库</strong>，然后进入到一个页面，在搜索框输入 gmail api 回车搜索，找到 gmail api 点击进去，点击启用</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/BC05EE71-42A3-4376-B233-57941DC152F0_2/8EYNeKphl280kXpAqLYWMFyXzcxVwl0F3tgpdc886pIz/Image.png" alt="Image.png"/></p><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/8781E6C9-9505-4462-B47A-225E0091D744_2/TmtnjHy809QHlxSw0K42wSFYx7IxDTKfBlPW0HkMwmoz/Image.png" alt="Image.png"/></p><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/EA05E8A6-BC9A-4E61-8761-644033BEF0B6_2/Ac5jvni99ya1T4DxAo8Pupj3U2RfAcYoVxckara8n64z/Image.png" alt="Image.png"/></p><ol start="8"><li>至此，创建 Google OAuth2 single service 已经完成了（如果没有记错的话，逃~）</li></ol><h2 id="-n8n-workflow">创建 n8n workflow</h2><ol start="1"><li>进入首页，点击 Add Workflow</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/D1565731-3F7E-41D0-9AF2-0F7D150733D2_2/B5MwlzOSoxTaQ54tIQiy6j4awnmaABxIjoPbLi53U3Uz/Image.png" alt="Image.png"/></p><ol start="1"><li>会有一个默认的 trigger ，把它删掉，我们需要的是 Gmail trigger</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/7F42DFB7-1CC6-4AD2-AC48-C6709F79BF86_2/2DDXmymDVHNkI4yHqA03i4snyezmP3iy0kvSpIdzDYoz/Image.png" alt="Image.png"/></p><ol start="1"><li>删除了之后，点击下图 的 1，在 2 的搜索框输入 gmail ，点击出现的 “Gmail”</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/1123F914-AAC9-4608-A814-0904C7227F1C_2/iOWyQfZte45F7LkvgrWXuBU4waqYXYaXuYPpXs8guT4z/Image.png" alt="Image.png"/></p><ol start="4"><li>Gmail 的 trigger 只有一个，那就是<strong>收到邮件</strong>，选择就好了</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/B469A670-E6B9-4BF5-86CE-80272D5FA848_2/Jnoh8RFMdURmNEc2UauOQyQXkKst2ylhdaSwOV4TGdwz/Image.png" alt="Image.png"/></p><ol start="1"><li>点击之后，需要把上面创建的 Google OAuth2 single service 绑定进去</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/2986BFEC-A569-4E18-9C34-3DE07DD5AB2B_2/IY0oQ5otFDxIjysv1uBIcoJFpHMyPeK75QBBDaSG27wz/Image.png" alt="Image.png"/></p><ol start="1"><li>点击 “Select Credential”，点击 “Create New Credential”</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/C38863DF-F26A-49A4-B723-3A6101E538EC_2/lprsDoeOyd5OQ3ROVmBZ6KHHIQqAASdzcW3BZzPLbfQz/Image.png" alt="Image.png"/></p><ol start="7"><li>就会出现下图的表单，需要把上面创建的信息填到这里来，这里的 OAuth Redirect URL 就是在创建 App 的时候需要用到的 ::已获授权重定向URI::</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/61EA0341-A627-4BC6-ADE5-CFDD3C3BD85B_2/2vEZOpLnEfIRikxQM7bw7la8GAyAwEhocytIZuehEx0z/Image.png" alt="Image.png"/></p><ol start="8"><li>添加好了之后，可以进行测试，会返回 Gmail 的最新一封邮件，如果出现错误的话，需要检查你的 n8n 时候可以公网访问 等等，要么是创建 OAuth App 的时候有什么不对，要么就是 n8n 填错了 Client ID 或者 Client Secret</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/22485069-6DA1-4F44-B81B-0566651D8FBF_2/97OUpxpgANjGyjLIE8xxf91psx37IWE9Q8AV5Rc7oUQz/Image.png" alt="Image.png"/></p><ol start="1"><li>设置转发到 telegram
<ol start="1"><li>找到 telegram action</li></ol></li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/2B81D03E-7477-46E9-9F7A-25AEDA00887E_2/7LoRnjfl8MyYMTjyexxvTU9hjlxNtPeyXSeP92dx8xUz/Image.png" alt="Image.png"/></p><pre class=""><code class="">  2. 选择发送消息，这里我选的是 text ，具体要什么的话，可以根据自己的需求，慢慢调试</code></pre><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/D1466523-96B3-4C6E-B11F-236E4828041B_2/9Vox3X2p8xyb3KJeupP3LNdUNDuK6NVZhvnRJk6DxHAz/Image.png" alt="Image.png"/></p><ol start="1"><li>创建 telegram 的凭证，这里比较简单，根据创建 telegram bot 的教程来就可以了，获取到 Access Token 跟 Chat ID</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/88A51C66-0BE5-44A9-B586-E882A23F1920_2/PmZIr561DtywaZS3paVU8L8vobj3acs2022wxmGkwC0z/Image.png" alt="Image.png"/></p><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/6C962B39-D7CA-49D6-B5DB-D32550569ED0_2/ec1ycoxaGqJ9B6C8rQQ2YmhPTPRW5nfrkYbvCGjv7W0z/Image.png" alt="Image.png"/></p><ol start="3"><li>设置要转发邮件的内容，我们需要先知道可以获得邮件的什么内容才能把对应的字段转发过去，所以需要点击一下 “Execute previous nodes” 来触发 Gmail 收到邮件的时候的场景，获得数据</li></ol><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/7C2680E7-6E65-4FBE-B5C1-B97E3F939948_2/t9hxuO4R8FYLrF6xf6hDQO0tOHuN5jmPlTpKCP8Czicz/Image.png" alt="Image.png"/></p><pre class=""><code class="">  4. 然后就可以在左边把要发的数据拖到右边的 Text 去（如果要添加更多的字段的话，应该是  Additional Fields， 这里我也还没有研究🤪）</code></pre><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/9723F69C-A5D4-484A-BE72-E4E00F14F6AA_2/CjdVuiF2GWI11WyLxpjAUAHkeS5SYaLGCGHuz3HjRDIz/Image.png" alt="Image.png"/></p><pre class=""><code class="">  5. 保存之后回到整个 Workflow 页面，点击底部的测试，就会开始测试整个 Workflow ，顺利的话，你的 tg 就会收到机器人给你发的消息，到这里就成功啦~
  6. 假的！ 还需要在右上角对这个 Workflow 进行启用</code></pre><p><img src="https://res.craft.do/user/full/a601b7fc-0737-d1d7-47a7-d7adfc39c9fd/doc/E23B753F-619E-4A47-A8B4-7EC3F18D15FC/31368F5D-988E-4B43-8712-16B0B2BB8564_2/DTgDIYTAzUDdrRULCEQJhl9suVEntjkRTlKiOUwWbFMz/Image.png" alt="Image.png"/></p><h2 id="end">END</h2><p>到这里就结束啦~ Enjoy it</p></div><p style="text-align:right"><a href="https://tinsfox.com/posts/default/n8n-gmail-to-telegram-forwarding#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tinsfox.com/posts/default/n8n-gmail-to-telegram-forwarding</link><guid isPermaLink="true">https://tinsfox.com/posts/default/n8n-gmail-to-telegram-forwarding</guid><dc:creator><![CDATA[TinsFox]]></dc:creator><pubDate>Wed, 25 Sep 2024 07:47:33 GMT</pubDate></item></channel></rss>