Jelajahi Sumber

created feisu-server

warrior 1 hari lalu
induk
melakukan
2ed8ef7c60

+ 170 - 0
modules/fmode-feishu-api/build.sh

@@ -0,0 +1,170 @@
+#!/bin/bash
+
+###############################################################################
+# fmode-feishu-api 构建脚本
+#
+# 功能:
+# 1. 使用esbuild打包所有ESM模块
+# 2. 使用terser进行代码混淆和压缩
+# 3. 生成可发布到npm或CDN的dist文件
+# 4. 支持Deno和Node.js环境
+#
+# 使用方法:
+#   chmod +x build.sh
+#   ./build.sh
+#
+# 输出:
+#   ./dist/{version}/fmode-feishu-api.js      - 打包后的文件
+#   ./dist/{version}/fmode-feishu-api.min.js  - 混淆压缩后的文件
+###############################################################################
+
+# 设置错误时退出
+set -e
+
+# 颜色输出
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+echo -e "${GREEN}==================================================${NC}"
+echo -e "${GREEN}   fmode-feishu-api 构建脚本${NC}"
+echo -e "${GREEN}==================================================${NC}"
+
+# 检查是否在正确的目录
+if [ ! -f "package.json" ]; then
+    echo -e "${RED}错误: package.json 不存在${NC}"
+    echo -e "${YELLOW}请确保在 fmode-feishu-api 目录下运行此脚本${NC}"
+    exit 1
+fi
+
+# 读取版本号
+VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "1.0.0")
+if [ -z "$VERSION" ]; then
+    echo -e "${RED}错误: 无法从 package.json 读取版本号${NC}"
+    exit 1
+fi
+
+echo -e "${GREEN}版本号: ${VERSION}${NC}"
+
+# 创建dist/{version}目录
+DIST_DIR="dist/${VERSION}"
+echo -e "${YELLOW}[1/5] 创建 ${DIST_DIR} 目录...${NC}"
+rm -rf "${DIST_DIR}"
+mkdir -p "${DIST_DIR}"
+
+# 检查是否安装了依赖
+if [ ! -d "node_modules" ]; then
+    echo -e "${YELLOW}[2/5] 安装依赖...${NC}"
+    npm install
+else
+    echo -e "${GREEN}[2/5] 依赖已安装,跳过...${NC}"
+fi
+
+# 使用esbuild打包 esm
+echo -e "${YELLOW}[3/5] 使用esbuild打包 ESM 格式...${NC}"
+npx esbuild src/index.ts \
+    --bundle \
+    --format=esm \
+    --platform=node \
+    --target=node16 \
+    --outfile="${DIST_DIR}/fmode-feishu-api.js" \
+    --sourcemap \
+    --external:express
+
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✓ ESM 打包成功${NC}"
+else
+    echo -e "${RED}✗ ESM 打包失败${NC}"
+    exit 1
+fi
+
+# 使用esbuild打包 cjs
+echo -e "${YELLOW}[3/5] 使用esbuild打包 CJS 格式...${NC}"
+npx esbuild src/index.ts \
+    --bundle \
+    --format=cjs \
+    --platform=node \
+    --target=node16 \
+    --outfile="${DIST_DIR}/fmode-feishu-api.cjs" \
+    --sourcemap \
+    --external:express
+
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✓ CJS 打包成功${NC}"
+else
+    echo -e "${RED}✗ CJS 打包失败${NC}"
+    exit 1
+fi
+
+# 使用terser压缩和混淆 ESM
+echo -e "${YELLOW}[4/5] 使用terser压缩混淆 ESM...${NC}"
+npx terser "${DIST_DIR}/fmode-feishu-api.js" \
+    --compress \
+    --mangle \
+    --output "${DIST_DIR}/fmode-feishu-api.min.js" \
+    --source-map "content='${DIST_DIR}/fmode-feishu-api.js.map',url='fmode-feishu-api.min.js.map'"
+
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✓ ESM 压缩混淆成功${NC}"
+else
+    echo -e "${RED}✗ ESM 压缩混淆失败${NC}"
+    exit 1
+fi
+
+# 使用terser压缩和混淆 CJS
+echo -e "${YELLOW}[4/5] 使用terser压缩混淆 CJS...${NC}"
+npx terser "${DIST_DIR}/fmode-feishu-api.cjs" \
+    --compress \
+    --mangle \
+    --output "${DIST_DIR}/fmode-feishu-api.min.cjs" \
+    --source-map "content='${DIST_DIR}/fmode-feishu-api.cjs.map',url='fmode-feishu-api.min.cjs.map'"
+
+if [ $? -eq 0 ]; then
+    echo -e "${GREEN}✓ CJS 压缩混淆成功${NC}"
+else
+    echo -e "${RED}✗ CJS 压缩混淆失败${NC}"
+    exit 1
+fi
+
+# 显示文件大小
+echo -e "${YELLOW}[5/5] 构建完成,文件信息:${NC}"
+echo ""
+ls -lh "${DIST_DIR}/"
+echo ""
+
+# 计算文件大小
+ORIGINAL_SIZE=$(stat -c%s "${DIST_DIR}/fmode-feishu-api.js" 2>/dev/null || stat -f%z "${DIST_DIR}/fmode-feishu-api.js" 2>/dev/null)
+MINIFIED_SIZE=$(stat -c%s "${DIST_DIR}/fmode-feishu-api.min.js" 2>/dev/null || stat -f%z "${DIST_DIR}/fmode-feishu-api.min.js" 2>/dev/null)
+
+if [ ! -z "$ORIGINAL_SIZE" ] && [ ! -z "$MINIFIED_SIZE" ]; then
+    REDUCTION=$(echo "scale=2; (1 - $MINIFIED_SIZE / $ORIGINAL_SIZE) * 100" | bc 2>/dev/null || echo "N/A")
+    echo -e "${GREEN}压缩率: ${REDUCTION}%${NC}"
+fi
+
+echo ""
+echo -e "${GREEN}==================================================${NC}"
+echo -e "${GREEN}   构建成功!${NC}"
+echo -e "${GREEN}==================================================${NC}"
+echo ""
+echo -e "${YELLOW}输出文件:${NC}"
+echo -e "  - ${DIST_DIR}/fmode-feishu-api.js      (ESM开发版,带source map)"
+echo -e "  - ${DIST_DIR}/fmode-feishu-api.min.js  (ESM生产版,已压缩混淆)"
+echo -e "  - ${DIST_DIR}/fmode-feishu-api.cjs     (CJS开发版,带source map)"
+echo -e "  - ${DIST_DIR}/fmode-feishu-api.min.cjs (CJS生产版,已压缩混淆)"
+echo ""
+echo -e "${YELLOW}使用方法:${NC}"
+echo -e "  ${GREEN}# Deno / Node.js ESM${NC}"
+echo -e "  import { createFeishuRouter } from './${DIST_DIR}/fmode-feishu-api.min.js';"
+echo ""
+echo -e "  ${GREEN}# Node.js CJS${NC}"
+echo -e "  const { createFeishuRouter } = require('./${DIST_DIR}/fmode-feishu-api.min.cjs');"
+echo ""
+echo -e "  ${GREEN}# 上传到CDN${NC}"
+echo -e "  ./upload.sh"
+echo ""
+echo -e "  ${GREEN}# CDN 引用示例${NC}"
+echo -e "  import { createFeishuRouter } from 'https://repos.fmode.cn/x/fmode-feishu-api/${VERSION}/fmode-feishu-api.min.js?code=xxxxxxx';"
+echo ""
+
+exit 0

+ 11 - 0
modules/fmode-feishu-api/deno.json

@@ -0,0 +1,11 @@
+{
+  "imports": {
+    "express": "npm:express@4.21.2",
+    "npm:express": "npm:express@4.21.2"
+  },
+  "compilerOptions": {
+    "allowJs": true,
+    "lib": ["deno.window"],
+    "strict": false
+  }
+}

+ 67 - 0
modules/fmode-feishu-api/docs/feishu-api-docs/tenant_access_token.md

@@ -0,0 +1,67 @@
+# 自建应用获取 tenant_access_token
+
+自建应用通过此接口获取 `tenant_access_token`。
+
+## 注意事项
+
+`tenant_access_token` 的最大有效期是 2 小时。
+
+- 剩余有效期小于 30 分钟时,调用本接口会返回一个新的 `tenant_access_token`,这会同时存在两个有效的 `tenant_access_token`。
+- 剩余有效期大于等于 30 分钟时,调用本接口会返回原有的 `tenant_access_token`。
+
+## 请求
+
+基本 |  
+---|---
+HTTP URL | https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal
+HTTP Method | POST
+支持的应用类型 | Custom App
+权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用** | 无
+
+### 请求头
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+Content-Type | string | 是 | **固定值**:"application/json; charset=utf-8"
+
+### 请求体
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+app_id | string | 是 | 应用唯一标识,创建应用后获得。有关`app_id` 的详细介绍。请参考[通用参数](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology)介绍<br>**示例值:** "cli_slkdjalasdkjasd"
+app_secret | string | 是 | 应用秘钥,创建应用后获得。有关 `app_secret` 的详细介绍,请参考[通用参数](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/terminology)介绍<br>**示例值:** "dskLLdkasdjlasdKK"
+
+### 请求体示例
+
+```json
+{
+    "app_id": "cli_slkdjalasdkjasd",
+    "app_secret": "dskLLdkasdjlasdKK"
+}
+```
+
+## 响应
+
+### 响应体
+
+名称 | 类型 | 描述
+---|---|---
+code | int | 错误码,非 0 取值表示失败
+msg | string | 错误描述
+tenant_access_token | string | 租户访问凭证
+expire | int | `tenant_access_token` 的过期时间,单位为秒
+
+### 响应体示例
+
+```json
+{
+    "code": 0,
+    "msg": "ok",
+    "tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
+    "expire": 7200
+}
+```
+
+### 错误码
+
+有关错误码的详细介绍,请参考[通用错误码](https://open.feishu.cn/document/ukTMukTMukTM/ugjM14COyUjL4ITN)介绍。

+ 349 - 0
modules/fmode-feishu-api/docs/feishu-api-docs/user_access_token.md

@@ -0,0 +1,349 @@
+# 获取 user_access_token
+OAuth 令牌接口,可用于获取 <code>user_access_token</code> 以及 <code>refresh_token</code>。<code>user_access_token</code> 为用户访问凭证,使用该凭证可以以用户身份调用 OpenAPI。<code>refresh_token</code> 为刷新凭证,可以用来获取新的 <code>user_access_token</code>。
+
+- 获取 `user_access_token` 前需要先获取授权码,详见[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)。请注意授权码的有效期为 5 分钟,且只能被使用一次。
+- 用户授权时,用户必须拥有[应用的使用权限](https://open.feishu.cn/document/home/introduction-to-scope-and-authorization/availability),否则调用本接口将会报错误码 20010。
+- 获取到的 `user_access_token` 存在有效期,如何刷新 <code>user_access_token</code> 详见[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)。
+- 如果你需要获取用户信息,详见[获取用户信息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get)。
+**注意事项**:本接口实现遵循 [RFC 6749 - The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) ,你可以使用[标准的 OAuth 客户端库](https://oauth.net/code/)进行接入(**推荐**)
+
+## 请求
+
+基本 | &nbsp;
+---|---
+HTTP URL | https://open.feishu.cn/open-apis/authen/v2/oauth/token
+HTTP Method | POST
+接口频率限制 | [1000 次/分钟、50 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
+支持的应用类型 | Custom App、Store App
+权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用<br>** | 无
+字段权限要求 | `refresh_token` 以及 `refresh_token_expires_in` 字段仅在具备以下权限时返回:<br>offline_access(offline_access)
+
+### 请求头
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+Content-Type | string | 是 | 请求体类型。<br>**固定值:**`application/json; charset=utf-8`
+
+### 请求体
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+grant_type | string | 是 | 授权类型。<br>**固定值:**`authorization_code`
+client_id | string | 是 | 应用的 App ID。应用凭证 App ID 和 App Secret 获取方式:<br>1. 登录[飞书开发者后台](https://open.feishu.cn/app)。<br>2. 进入应用详情页,在左侧导航栏,单击 **凭证与基础信息**。<br>3. 在 **应用凭证** 区域,获取并保存 **App ID** 和 **App Secret**。<br>**示例值:**`cli_a5ca35a685b0x26e`
+client_secret | string | 是 | 应用的 App Secret。应用凭证 App ID 和 App Secret 获取方式:<br>1. 登录[飞书开发者后台](https://open.feishu.cn/app)。<br>2. 进入应用详情页,在左侧导航栏,单击 **凭证与基础信息**。<br>3. 在 **应用凭证** 区域,获取并保存 **App ID** 和 **App Secret**。<br>**示例值:**`baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy`
+code | string | 是 | 授权码,详见[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)。<br>**示例值:**`a61hb967bd094dge949h79bbexd16dfe`
+redirect_uri | string | 否 | 在构造授权页页面链接时所拼接的应用回调地址。<br>**示例值:**`https://example.com/api/oauth/callback`
+code_verifier | string | 否 | 在发起授权前,本地生成的随机字符串,用于 PKCE(Proof Key for Code Exchange)流程。使用 PKCE 时,该值为必填项。  <br>有关 PKCE 的详细介绍,请参阅 [RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients](https://datatracker.ietf.org/doc/html/rfc7636)。<br>**长度限制:** 最短 43 字符,最长 128 字符<br>**可用字符集:** [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"<br>**示例值:**`TxYmzM4PHLBlqm5NtnCmwxMH8mFlRWl_ipie3O0aVzo`
+scope | string | 否 | 该参数用于缩减 `user_access_token` 的权限范围。<br>例如:<br>1. 在[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时通过 `scope` 参数授权了 `contact:user.base:readonly contact:contact.base:readonly contact:user.employee:readonly` 三个权限。<br>2. 在当前接口可通过 `scope` 参数传入 `contact:user.base:readonly`,将 `user_access_token` 的权限缩减为 `contact:user.base:readonly` 这一个。<br>**注意**:<br>- 如果不指定当前参数,生成的 `user_access_token` 将包含用户授权时的所有权限。<br>- 当前参数不能传入重复的权限,否则会接口调用会报错,返回错误码 20067。<br>- 当前参数不能传入未授权的权限(即[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时用户已授权范围外的其他权限),否则接口调用会报错,返回错误码 20068。<br>- 多次调用当前接口缩减权限的范围不会叠加。例如,用户授予了权限 A 和 B,第一次调用该接口缩减为权限 A,则 `user_access_token` 只包含权限 A;第二次调用该接口缩减为权限 B,则 `user_access_token` 只包含权限 B。              <br>- 生效的权限列表可通过本接口返回值 scope 查看。<br>**格式要求:** 以空格分隔的 `scope` 列表<br>**示例值:**`auth:user.id:read task:task:read`
+
+### 请求体示例
+```json
+{
+    "grant_type": "authorization_code",
+    "client_id": "cli_a5ca35a685b0x26e",
+    "client_secret": "baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy",
+    "code": "a61hb967bd094dge949h79bbexd16dfe",
+    "redirect_uri": "https://example.com/api/oauth/callback",
+    "code_verifier": "TxYmzM4PHLBlqm5NtnCmwxMH8mFlRWl_ipie3O0aVzo"
+}
+```
+## 响应
+响应体类型为 `application/json; charset=utf-8`。
+
+### 响应体
+**注意事项**:**响应体中的 `access_token` 和 `refresh_token` 长度较长**,一般在 1~2KB 之间,且可能由于 `scope` 数量的变多或后续变更导致长度进一步增加,建议预留 4KB 的存储容量
+
+名称 | 类型 | 描述
+---|---|---
+code | int | 错误码,为 0 时表明请求成功,非 0 表示失败,请参照下文[错误码](#错误码)一节进行相应处理
+access_token | string | 即 `user_access_token`,仅在请求成功时返回
+expires_in | int | 即 `user_access_token` 的有效期,单位为秒,仅在请求成功时返回<br>**注意事项**:建议使用该字段以确定 `user_access_token` 的过期时间,不要硬编码有效期
+refresh_token | string | 用于刷新 `user_access_token`,详见[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)。该字段仅在请求成功且用户授予 `offline_access` 权限时返回。<br>**注意事项**:如果你在获取 `user_access_token` 时设置了 `scope` 请求参数,且需要返回 `refresh_token`,则需要在 `scope` 参数中包括 `offline_access`。另外,`refresh_token` 仅能被使用一次。
+refresh_token_expires_in | int | 即 `refresh_token` 的有效期,单位为秒,仅在返回 `refresh_token` 时返回。<br>**注意事项**:建议在到期前调用[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token) 接口获取新的 `refresh_token`。
+token_type | string | 值固定为 `Bearer`,仅在请求成功时返回
+scope | string | 本次请求所获得的 `access_token` 所具备的权限列表,以空格分隔,仅在请求成功时返回
+error | string | 错误类型,仅在请求失败时返回
+error_description | string | 具体的错误信息,仅在请求失败时返回
+
+### 响应体示例
+
+成功响应示例:
+
+```json
+{
+    "code": 0,
+    "access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg",
+    "expires_in": 7200, // 非固定值,请务必根据响应体中返回的实际值来确定 access_token 的有效期
+    "refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA",
+    "refresh_token_expires_in": 604800, // 非固定值,请务必根据响应体中返回的实际值来确定 refresh_token 的有效期
+    "scope": "auth:user.id:read offline_access task:task:read user_profile",
+    "token_type": "Bearer"
+}
+```
+
+失败响应示例:
+```json
+{
+    "code": 20050,
+    "error": "server_error",
+    "error_description": "An unexpected server error occurred. Please retry your request."
+}
+```
+
+### 错误码
+
+HTTP 状态码 | 错误码 | 描述 | 排查建议
+---|---|---|---
+400 | 20001 | The request is missing a required parameter. | 必要参数缺失,请检查请求时传入的参数是否有误
+400 | 20002 | The client secret is invalid. | 应用认证失败,请检查提供的 `client_id` 与 `client_secret` 是否正确
+400 | 20003 | The authorization code is not found. Please note that an authorization code can only be used once. | 无效的授权码,请检查授权码是否有效,注意授权码仅能使用一次
+400 | 20004 | The authorization code has expired. | 授权码已经过期,请在授权码生成后的 5 分钟内使用
+400 | 20008 | The user does not exist. | 用户不存在,请检查发起授权的用户的当前状态
+400 | 20009 | The specified app is not installed. | 租户未安装应用,请检查应用状态
+400 | 20010 | The user does not have permission to use this app. | 用户无应用使用权限,请检查发起授权的用户是否仍具有应用使用权限
+400 | 20024 | The provided authorization code or refresh token does not match the provided client ID. | 提供的授权码与 `client_id` 不匹配,请勿混用不同应用的凭证
+400 | 20036 | The specified grant_type is not supported. | 无效的 `grant_type`,请检查请求体中 `grant_type` 字段的取值
+400 | 20048 | The specified app does not exist. | 应用不存在,请检查应用状态
+400 | 20049 | PKCE code challenge failed. | PKCE 校验失败,请检查请求体中 `code_verifier` 字段是否存在且有效
+500 | 20050 | An unexpected server error occurred. Please retry your request. | 内部服务错误,请稍后重试,如果持续报错请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
+400 | 20063 | The request is malformed. Please check your request. | 请求体中缺少必要字段,请根据具体的错误信息补齐字段
+400 | 20065 | The authorization code has been used. Please note that an authorization code can only be used once. | 授权码已被使用,授权码仅能使用一次,请检查是否有被重复使用
+400 | 20066 | The user status is invalid. | 用户状态非法,请检查发起授权的用户的当前状态
+400 | 20067 | The provided scope list contains duplicate scopes. Please ensure all scopes are unique. | 无效的 `scope` 列表,其中存在重复项,请确保传入的 `scope` 列表中没有重复项
+400 | 20068 | The provided scope list contains scopes that are not permitted. Please ensure all scopes are allowed. | 无效的 `scope` 列表,其中存在用户未授权的权限。当前接口 `scope` 参数传入的权限必须是[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时 `scope` 参数值的子集。<br>例如,在获取授权码时,用户授权了权限 A、B,则当前接口 `scope` 可传入的值只有权限 A、B,若传入权限 C 则会返回当前错误码。
+400 | 20069 | The specified app is not enabled. | 应用未启用,请检查应用状态
+400 | 20070 | Multiple authentication methods were provided. Please only use one to proceed. | 请求时同时使用了 `Basic Authentication` 和 `client_secret` 两种身份验证方式。请仅使用 `client_id`、`client_secret` 身份验证方式调用本接口。
+400 | 20071 | The provided redirect URI does not match the one used during authorization. | 无效的 `redirect_uri`,请确保 `redirect_uri` 与[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时传入的 `redirect_uri` 保持一致
+503 | 20072 | The server is temporarily unavailable. Please retry your request. | 服务暂不可用,请稍后重试
+
+## 代码示例
+**注意事项**:此处提供的代码示例**仅供参考**,请勿直接在生产环境使用
+
+### Golang
+
+运行下面示例程序的步骤:
+1. 点击下方代码块右上角复制按钮,将代码复制到本地文件中,保存为 `main.go`;
+2. 参照注释部分,完成配置;
+3. 在 `main.go` 所在目录下新建 `.env` 文件,内容如下:
+    ```bash
+    APP_ID=cli_xxxxxx # 仅为示例值,请使用你的应用的 App ID,获取方式:开发者后台 -> 基础信息 -> 凭证与基础信息 -> 应用凭证 -> App ID
+	APP_SECRET=xxxxxx # 仅为示例值,请使用你的应用的 App Secret,获取方式:开发者后台 -> 基础信息 -> 凭证与基础信息 -> 应用凭证 -> App Secret
+    ```
+4. 在 `main.go` 所在目录执行以下命令:
+	```bash
+    go mod init oauth-test
+	go get github.com/gin-gonic/gin
+	go get github.com/gin-contrib/sessions
+	go get github.com/gin-contrib/sessions/cookie
+	go get github.com/joho/godotenv
+	go get golang.org/x/oauth2
+	go run main.go
+    ```
+5. 浏览器打开 [http://localhost:8080](http://localhost:8080) ,按照页面提示完成授权流程;
+
+```javascript
+package main
+
+import (
+    "context"
+    "encoding/json"
+    "fmt"
+    "log"
+    "math/rand"
+    "net/http"
+    "os"
+
+"github.com/gin-contrib/sessions"
+    "github.com/gin-contrib/sessions/cookie"
+    "github.com/gin-gonic/gin"
+    _ "github.com/joho/godotenv/autoload"
+    "golang.org/x/oauth2"
+)
+
+var oauthEndpoint = oauth2.Endpoint{
+    AuthURL:  "https://accounts.feishu.cn/open-apis/authen/v1/authorize",
+    TokenURL: "https://open.feishu.cn/open-apis/authen/v2/oauth/token",
+}
+
+var oauthConfig = &oauth2.Config{
+    ClientID:     os.Getenv("APP_ID"),
+    ClientSecret: os.Getenv("APP_SECRET"),
+    RedirectURL:  "http://localhost:8080/callback", // 请先添加该重定向 URL,配置路径:开发者后台 -> 开发配置 -> 安全设置 -> 重定向 URL -> 添加
+    Endpoint:     oauthEndpoint,
+    Scopes:       []string{"offline_access"}, // 如果你不需要 refresh_token,请注释掉该行,否则你需要先申请 offline_access 权限方可使用,配置路径:开发者后台 -> 开发配置 -> 权限管理      
+}
+
+func main() {
+    r := gin.Default()
+
+// 使用 Cookie 存储 session
+    store := cookie.NewStore([]byte("secret")) // 此处仅为示例,务必不要硬编码密钥
+    r.Use(sessions.Sessions("mysession", store))
+
+r.GET("/", indexController)
+    r.GET("/login", loginController)
+    r.GET("/callback", oauthCallbackController)
+
+fmt.Println("Server running on http://localhost:8080")
+    log.Fatal(r.Run(":8080"))
+}
+
+func indexController(c *gin.Context) {
+    c.Header("Content-Type", "text/html; charset=utf-8")
+    var username string
+    session := sessions.Default(c)
+    if session.Get("user") != nil {
+       username = session.Get("user").(string)
+    }
+    html := fmt.Sprintf(`<html><head><style>body{font-family:Arial,sans-serif;background:#f4f4f4;margin:0;display:flex;justify-content:center;align-items:center;height:100vh}.container{text-align:center;background:#fff;padding:30px;border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,0.1)}a{padding:10px 20px;font-size:16px;color:#fff;background:#007bff;border-radius:5px;text-decoration:none;transition:0.3s}a:hover{background:#0056b3}}</style></head><body>[返回主页](/)
+</body></html>`, user.Data.Name)
+    c.String(http.StatusOK, html)
+}
+```
+
+
+
+
+
+
+# 刷新 user_access_token
+OAuth 令牌接口,可用于刷新 <code>user_access_token</code> 以及获取新的 <code>refresh_token</code>。
+
+- `user_access_token` 为用户访问凭证,使用该凭证可以以用户身份调用 OpenAPI,该凭证存在有效期,可通过 `refresh_token` 进行刷新。
+- 用户授权时,用户必须拥有[应用的使用权限](https://open.feishu.cn/document/home/introduction-to-scope-and-authorization/availability),否则调用本接口将会报错误码 20010。
+- `refresh_token` 用于获取新的 `user_access_token`,且仅能使用一次。在获取新的 `user_access_token` 时会返回新的 `refresh_token`,原 `refresh_token` 立即失效。
+
+- 首次获取 `refresh_token` 的方式参见[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)。
+**注意事项**:本接口实现遵循 [RFC 6749 - The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749) ,你可以使用[标准的 OAuth 客户端库](https://oauth.net/code/)进行接入(**推荐**)
+
+## 前置工作
+### 开通 offline_access 权限
+获取 `refresh_token` 需前往开放平台应用后台的**权限管理**模块开通 `offline_access` 权限,并在[发起授权](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时在 `scope` 参数中声明该权限。
+
+![开通 offline_access 权限.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f8b75edae2c682ab98b6984170707a64_gYt5eNq84a.png?height=703&lazyload=true&width=1867)
+
+在开通 `offline_access` 权限后,如需获取 `refresh_token`,具体的请求参数设置如下:
+1. 首先在[发起授权](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时,授权链接的`scope` 参数中必须拼接 `offline_access`,例如:
+```
+https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=cli_a5d611352af9d00b&redirect_uri=https%3A%2F%2Fexample.com%2Fapi%2Foauth%2Fcallback&scope=bitable:app:readonly%20offline_access
+```
+2. 在[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)时,
+	+ 如果不需要缩减权限,即该接口的 `scope` 参数为空,则无需做其他操作,即可正常获得 `refresh_token`;
+	+ 如果需要缩减权限,即该接口的 `scope` 参数不为空,
+		+ 且需要获取 `refresh_token`,则此处的 `scope` 参数中需要拼接 `offline_access`;
+		+ 如不需要获取 `refresh_token`,则无需特殊处理;
+3. 在[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)时,同第二步的逻辑。
+
+### 开启刷新 user_access_token 的安全设置
+**注意事项**:- 如果你看不到此开关则无需关注,其默认处于开启状态。
+- 完成配置后需要发布应用使配置生效。具体操作参见[发布企业自建应用](https://open.feishu.cn/document/home/introduction-to-custom-app-development/self-built-application-development-process#baf09c7d)、[发布商店应用](https://open.feishu.cn/document/uMzNwEjLzcDMx4yM3ATM/uYjMyUjL2IjM14iNyITN)。
+
+前往开放平台应用后台的**安全设置**模块,打开刷新 `user_access_token` 的开关。
+
+![开启刷新 user_access_token 的安全设置.png](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/194824525c33e70cd796579744571c2d_WbBjYQW3zR.png?height=796&lazyload=true&width=1907)
+
+## 请求
+**注意事项**:为了避免刷新 `user_access_token` 的行为被滥用,在用户授权应用 365 天后,应用必须通过用户[重新授权](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)的方式来获取 `user_access_token` 与 `refresh_token`。如果 `refresh_token` 到期后继续刷新`user_access_token`将报错(错误码为 20037),可参考以下[错误码描述信息](#错误码)进行处理。
+**注意事项**:刷新后请更新本地 `user_access_token` 和 `refresh_token`,原令牌将无法再使用(`user_access_token` 会有一分钟的豁免时间以供应用完成令牌轮转)。
+
+基本 | &nbsp;
+---|---
+HTTP URL | https://open.feishu.cn/open-apis/authen/v2/oauth/token
+HTTP Method | POST
+接口频率限制 | [1000 次/分钟、50 次/秒](https://open.feishu.cn/document/ukTMukTMukTM/uUzN04SN3QjL1cDN)
+支持的应用类型 | Custom App、Store App
+权限要求<br>**调用该 API 所需的权限。开启其中任意一项权限即可调用<br>** | 无
+字段权限要求 | `refresh_token` 以及 `refresh_token_expires_in` 字段仅在具备以下权限时返回:<br>offline_access(offline_access)
+
+### 请求头
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+Content-Type | string | 是 | 请求体类型。<br>**固定值:**`application/json; charset=utf-8`
+
+### 请求体
+
+名称 | 类型 | 必填 | 描述
+---|---|---|---
+grant_type | string | 是 | 授权类型。<br>**固定值:**`refresh_token`
+client_id | string | 是 | 应用的 App ID,可以在开发者后台中的应用详情页面找到该值。<br>**示例值:**`cli_a5ca35a685b0x26e`
+client_secret | string | 是 | 应用的 App Secret,可以在开发者后台中的应用详情页面找到该值,详见:[如何获取应用的 App ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-app-id)。<br>**示例值:**`baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy`
+refresh_token | string | 是 | 刷新令牌,用于刷新 `user_access_token` 以及 `refresh_token`。<br>**注意事项**:请务必注意本接口仅支持[获取 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)和[刷新 user_access_token](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/refresh-user-access-token)接口返回的 `refresh_token`<br>**示例值:**`eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA`
+scope | string | 否 | 该参数用于缩减 `user_access_token` 的权限范围。<br>例如:<br>1. 在[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时通过 `scope` 参数授权了 `contact:user.base:readonly contact:contact.base:readonly contact:user.employee:readonly` 三个权限。<br>2. 在当前接口可通过 `scope` 参数传入 `contact:user.base:readonly`,将 `user_access_token` 的权限缩减为 `contact:user.base:readonly` 这一个。<br>**注意**:<br>- 如果不指定当前参数,生成的 `user_access_token` 将包含用户授权时的所有权限。<br>- 当前参数不能传入重复的权限,否则会接口调用会报错,返回错误码 20067。<br>- 当前参数不能传入未授权的权限(即[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时用户已授权范围外的其他权限),否则接口调用会报错,返回错误码 20068。<br>- 多次调用当前接口缩减权限的范围不会叠加。例如,用户授予了权限 A 和 B,第一次调用该接口缩减为权限 A,则 `user_access_token` 只包含权限 A;第二次调用该接口缩减为权限 B,则 `user_access_token` 只包含权限 B。              <br>- 生效的权限列表可通过本接口返回值 scope 查看。<br>**格式要求:** 以空格分隔的 `scope` 列表<br>**示例值:**`auth:user.id:read task:task:read`
+
+### 请求体示例
+```json
+{
+    "grant_type": "refresh_token",
+    "client_id": "cli_a5ca35a685b0x26e",
+    "client_secret": "baBqE5um9LbFGDy3X7LcfxQX1sqpXlwy",
+    "refresh_token": "eyJhbGciOiJFUzI1NiIs**********XXOYOZz1mfgIYHwM8ZJA"
+}
+```
+## 响应
+响应体类型为 `application/json; charset=utf-8`。
+
+### 响应体
+
+名称 | 类型 | 描述
+---|---|---
+code | int | 错误码,为 0 时表明请求成功,非 0 表示失败,请参照下文错误码一节妥善处理
+access_token | string | 即 `user_access_token`,仅在请求成功时返回
+expires_in | int | 即 `user_access_token` 的有效期,单位为秒,仅在请求成功时返回<br>**注意事项**:建议使用该字段以确定 `user_access_token` 的过期时间,不要硬编码有效期
+refresh_token | string | 用于刷新 `user_access_token`,该字段仅在请求成功且用户授予 `offline_access` 权限时返回:<br>offline_access(offline_access)<br>**注意事项**:如果你在获取 `user_access_token` 时设置了 `scope` 请求参数,且需要返回 `refresh_token`,则需要在 `scope` 参数中包括 `offline_access`。另外,`refresh_token` 仅能被使用一次。
+refresh_token_expires_in | int | 即 `refresh_token` 的有效期,单位为秒,仅在返回 `refresh_token` 时返回。<br>**注意事项**:建议在到期前重新调用当前接口获取新的 `refresh_token`。
+token_type | string | 值固定为 `Bearer`,仅在请求成功时返回
+scope | string | 本次请求所获得的 `access_token` 所具备的权限列表,以空格分隔,仅在请求成功时返回
+error | string | 错误类型,仅在请求失败时返回
+error_description | string | 具体的错误信息,仅在请求失败时返回
+
+### 响应体示例
+
+成功响应示例:
+
+```json
+{
+    "code": 0,
+    "access_token": "eyJhbGciOiJFUzI1NiIs**********X6wrZHYKDxJkWwhdkrYg",
+    "expires_in": 7200, // 非固定值,请务必根据响应体中返回的实际值来确定 access_token 的有效期
+    "refresh_token": "eyJhbGciOiJFUzI1NiIs**********VXOYOZYZmfgIYHWM0ZJA",
+    "refresh_token_expires_in": 604800, // 非固定值,请务必根据响应体中返回的实际值来确定 refresh_token 的有效期
+    "scope": "auth:user.id:read offline_access task:task:read user_profile",
+    "token_type": "Bearer"
+}
+```
+
+失败响应示例:
+```json
+{
+    "code": 20050,
+    "error": "server_error",
+    "error_description": "An unexpected server error occurred. Please retry your request."
+}
+```
+
+### 错误码
+
+HTTP 状态码 | 错误码 | 描述 | 排查建议
+---|---|---|---
+400 | 20001 | The request is missing a required parameter. | 必要参数缺失,请检查请求时传入的参数是否有误
+400 | 20002 | The client secret is invalid. | 应用认证失败,请检查提供的 `client_id` 与 `client_secret` 是否正确。获取方式参见 [如何获取应用的 App ID](https://open.feishu.cn/document/uAjLw4CM/ugTN1YjL4UTN24CO1UjN/trouble-shooting/how-to-obtain-app-id)。
+400 | 20008 | The user does not exist. | 用户不存在,请检查发起授权的用户的当前状态
+400 | 20009 | The specified app is not installed. | 租户未安装应用,请检查应用状态
+400 | 20010 | The user does not have permission to use this app. | 用户无应用使用权限,请检查发起授权的用户是否仍具有应用使用权限
+400 | 20024 | The provided authorization code or refresh token does not match the provided client ID. | 提供的 `refresh_token` 与 `client_id` 不匹配,请勿混用不同应用的凭证
+400 | 20026 | The refresh token passed is invalid. Please check the value. | 请检查请求体中 `refresh_token` 字段的取值<br>请注意本接口仅支持 v2 版本接口下发的 `refresh_token`
+400 | 20036 | The specified grant_type is not supported. | 无效的 `grant_type`,请检查请求体中 `grant_type` 字段的取值
+400 | 20037 | The refresh token passed has expired. Please generate a new one. | `refresh_token` 已过期,请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token`
+400 | 20048 | The specified app does not exist. | 应用不存在,请检查应用状态
+500 | 20050 | An unexpected server error occurred. Please retry your request. | 内部服务错误,请稍后重试,如果持续报错请联系[技术支持](https://applink.feishu.cn/TLJpeNdW)
+400 | 20063 | The request is malformed. Please check your request. | 请求体中缺少必要字段,请根据具体的错误信息补齐字段
+400 | 20064 | The refresh token has been revoked. Please note that a refresh token can only be used once. | `refresh_token` 已被撤销,请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token`
+400 | 20066 | The user status is invalid. | 用户状态非法,请检查发起授权的用户的当前状态
+400 | 20067 | The provided scope list contains duplicate scopes. Please ensure all scopes are unique. | 无效的 `scope` 列表,其中存在重复项,请确保传入的 `scope` 列表中没有重复项
+400 | 20068 | The provided scope list contains scopes that are not permitted. Please ensure all scopes are allowed. | 无效的 `scope` 列表,其中存在用户未授权的权限。当前接口 `scope` 参数传入的权限必须是[获取授权码](https://open.feishu.cn/document/common-capabilities/sso/api/obtain-oauth-code)时 `scope` 参数值的子集。<br>例如,在获取授权码时,用户授权了权限 A、B,则当前接口 `scope` 可传入的值只有权限 A、B,若传入权限 C 则会返回当前错误码。
+400 | 20069 | The specified app is not enabled. | 应用未启用,请检查应用状态
+400 | 20070 | Multiple authentication methods were provided. Please only use one to proceed. | 请求时同时使用了 `Basic Authentication` 和 `client_secret` 两种身份验证方式。请仅使用 `client_id`、`client_secret` 身份验证方式调用本接口。
+503 | 20072 | The server is temporarily unavailable. Please retry your request. | 服务暂不可用,请稍后重试
+400 | 20073 | The refresh token has been used. Please note that a refresh token can only be used once. | 请[重新发起授权流程](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/authentication-management/access-token/get-user-access-token)以获取新的 `refresh_token`
+400 | 20074 | The specified app is not allowed to refresh token. | 请在应用管理后台检查是否开启了刷新 `user_access_token` 开关,注意发版后生效

+ 11 - 0
modules/fmode-feishu-api/docs/task/202603101127.md

@@ -0,0 +1,11 @@
+# 飞书文档参考资料
+- [自建应用获取 tenant_access_token](https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal)
+- [自建应用获取 app_access_token](https://open.feishu.cn/document/server-docs/authentication-management/access-token/app_access_token_internal)
+- [获取授权码](https://open.feishu.cn/document/authentication-management/access-token/obtain-oauth-code)
+- [获取 user_access_token](https://open.feishu.cn/document/authentication-management/access-token/get-user-access-token)
+- [刷新 user_access_token](https://open.feishu.cn/document/authentication-management/access-token/refresh-user-access-token)
+- [重新获取 app_ticket](https://open.feishu.cn/document/server-docs/authentication-management/access-token/app_ticket_resend)
+- [获取用户信息](https://open.feishu.cn/document/server-docs/authentication-management/login-state-management/get?appId=cli_a9253658eef99cd2)
+
+> 请您参考当前api\api-common\dingtalk\fmode-dingtalk中实现方式和技巧,实现飞书的网页应用免登录和扫码登录两种方式。
+

+ 268 - 0
modules/fmode-feishu-api/docs/使用指南.md

@@ -0,0 +1,268 @@
+# 飞书API集成模块使用指南
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+npm install express
+```
+
+### 2. 引入模块并配置
+
+```typescript
+import express from 'express';
+import { createFeishuRouter } from './fmode-server/modules/fmode-feishu-api/src/index.ts';
+
+const app = express();
+
+// 创建飞书路由(直接传入配置)
+const feishuRouter = createFeishuRouter({
+  'cli_a1b2c3d4e5f6g7h8': {
+    appId: 'cli_a1b2c3d4e5f6g7h8',
+    appSecret: 'your_app_secret_here',
+    company: 'your_company_id',
+    enabled: true
+  }
+});
+
+// 挂载到Express应用
+app.use('/api/feishu', feishuRouter);
+
+app.listen(3000, () => {
+  console.log('服务器启动在端口 3000');
+});
+```
+
+### 3. 测试模块是否正常加载
+
+访问 `http://localhost:3000/api/feishu/test` 查看模块状态。
+
+## 配置说明
+
+### 单个应用配置
+
+```typescript
+const feishuRouter = createFeishuRouter({
+  'cli_xxx': {
+    appId: 'cli_xxx',           // 飞书应用ID
+    appSecret: 'your_secret',    // 飞书应用密钥
+    company: 'company_id',       // 公司ID(用于用户管理)
+    enabled: true                // 是否启用
+  }
+});
+```
+
+### 多个应用配置
+
+```typescript
+const feishuRouter = createFeishuRouter({
+  'app1': {
+    appId: 'cli_xxx1',
+    appSecret: 'secret1',
+    company: 'company1',
+    enabled: true
+  },
+  'app2': {
+    appId: 'cli_xxx2',
+    appSecret: 'secret2',
+    company: 'company2',
+    enabled: true
+  }
+});
+```
+
+## OAuth2扫码登录
+
+### 前端实现
+
+```html
+<!DOCTYPE html>
+<html>
+<head>
+  <title>飞书登录</title>
+</head>
+<body>
+  <button onclick="loginWithFeishu()">飞书扫码登录</button>
+
+  <script>
+    function loginWithFeishu() {
+      const appId = 'cli_a1b2c3d4e5f6g7h8';
+      const redirectUri = encodeURIComponent('http://localhost:3000/callback');
+      const state = Math.random().toString(36).substring(7);
+
+      const authUrl = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=${appId}&redirect_uri=${redirectUri}&state=${state}`;
+      window.location.href = authUrl;
+    }
+  </script>
+</body>
+</html>
+```
+
+### 后端API调用
+
+```typescript
+// POST /api/feishu/oauth2/login
+fetch('/api/feishu/oauth2/login', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json'
+  },
+  body: JSON.stringify({
+    appId: 'cli_a1b2c3d4e5f6g7h8',
+    code: '授权码'
+  })
+})
+.then(res => res.json())
+.then(data => {
+  console.log('登录成功:', data.data.sessionToken);
+});
+```
+
+## 网页应用免登录
+
+### 前端实现
+
+```html
+<!DOCTYPE html>
+<html>
+<head>
+  <title>飞书应用</title>
+  <script src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.16.js"></script>
+</head>
+<body>
+  <h1>飞书应用</h1>
+  <div id="user-info"></div>
+
+  <script>
+    const appId = 'cli_a1b2c3d4e5f6g7h8';
+
+    tt.ready(() => {
+      tt.requestAuthCode({
+        appId: appId,
+        success: (res) => {
+          const code = res.code;
+          loginWithCode(code);
+        },
+        fail: (err) => {
+          console.error('获取授权码失败:', err);
+        }
+      });
+    });
+
+    function loginWithCode(code) {
+      fetch('/api/feishu/oauth2/feishu', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          appId: appId,
+          code: code
+        })
+      })
+      .then(res => res.json())
+      .then(data => {
+        if (data.code === 1) {
+          const userInfo = data.data.userInfo;
+          document.getElementById('user-info').innerHTML = `
+            <p>欢迎,${userInfo.name}!</p>
+            <img src="${userInfo.avatarUrl}" width="50" />
+          `;
+        }
+      });
+    }
+  </script>
+</body>
+</html>
+```
+
+## API接口说明
+
+### 1. OAuth2扫码登录
+- **接口**: `POST /api/feishu/oauth2/login`
+- **参数**: `{ appId, code }`
+- **返回**: 用户信息、令牌信息、sessionToken
+
+### 2. 网页应用免登录
+- **接口**: `POST /api/feishu/oauth2/feishu`
+- **参数**: `{ appId, code }`
+- **返回**: 用户信息、sessionToken
+
+### 3. 刷新OAuth2令牌
+- **接口**: `POST /api/feishu/oauth2/refresh_token`
+- **参数**: `{ appId, refreshToken }`
+
+### 4. 同步用户信息
+- **接口**: `POST /api/feishu/user/sync`
+- **参数**: `{ appId, userInfo }`
+
+### 5. 转发API请求
+- **接口**: `POST /api/feishu/forward`
+- **参数**: `{ appId, path, method, query, body }`
+
+### 6. 获取Token状态
+- **接口**: `POST /api/feishu/token/status`
+- **参数**: `{ appId }`
+
+### 7. 强制刷新Token
+- **接口**: `POST /api/feishu/token/refresh`
+- **参数**: `{ appId }`
+
+## 与旧版本的区别
+
+### 旧版本(繁琐)
+
+```typescript
+import { createFeishuRouter, feishuConfig } from 'fmode-feishu-api';
+
+// 需要先单独配置
+feishuConfig.setAppConfig('cli_xxx', {
+  appId: 'cli_xxx',
+  appSecret: 'secret',
+  company: 'company_id',
+  enabled: true
+});
+
+// 然后创建路由
+const feishuRouter = createFeishuRouter();
+app.use('/api/feishu', feishuRouter);
+```
+
+### 新版本(简洁)
+
+```typescript
+import { createFeishuRouter } from 'fmode-feishu-api';
+
+// 直接传入配置,一步完成
+const feishuRouter = createFeishuRouter({
+  'cli_xxx': {
+    appId: 'cli_xxx',
+    appSecret: 'secret',
+    company: 'company_id',
+    enabled: true
+  }
+});
+
+app.use('/api/feishu', feishuRouter);
+```
+
+## 常见问题
+
+### 1. 如何获取飞书应用ID和密钥?
+访问 [飞书开放平台](https://open.feishu.cn/) 创建企业自建应用。
+
+### 2. 如何配置回调地址?
+在飞书开放平台的应用配置中,设置"重定向URL"。
+
+### 3. Token过期怎么办?
+- `tenant_access_token`: 自动管理,有效期2小时,提前5分钟自动刷新
+- `user_access_token`: 使用 `/oauth2/refresh_token` 接口刷新
+
+### 4. 如何处理多个飞书应用?
+在 `createFeishuRouter` 中传入多个应用配置即可。
+
+## 技术支持
+
+- [飞书开放平台文档](https://open.feishu.cn/document/)
+- [项目源码](./src/)

+ 40 - 0
modules/fmode-feishu-api/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "@fmode/feishu-api",
+  "version": "1.0.0",
+  "description": "飞书 API 集成模块 - 提供飞书OAuth2登录、网页免登录等功能",
+  "type": "module",
+  "main": "src/index.ts",
+  "exports": {
+    ".": "./src/index.ts",
+    "./client": "./src/client.ts",
+    "./config": "./src/config.ts",
+    "./token-manager": "./src/token-manager.ts",
+    "./user-manager": "./src/user-manager.ts",
+    "./router": "./src/router.ts"
+  },
+  "scripts": {
+    "build": "./build.sh",
+    "upload": "./upload.sh",
+    "test": "echo \"No tests yet\""
+  },
+  "keywords": [
+    "feishu",
+    "lark",
+    "oauth2",
+    "login",
+    "api",
+    "fmode",
+    "express"
+  ],
+  "author": "fmode team",
+  "license": "MIT",
+  "dependencies": {
+    "express": "^4.18.2"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.17",
+    "@types/node": "^20.0.0",
+    "esbuild": "^0.19.0",
+    "terser": "^5.19.0"
+  }
+}

+ 439 - 0
modules/fmode-feishu-api/src/client.ts

@@ -0,0 +1,439 @@
+/**
+ * 飞书API客户端模块
+ *
+ * 功能说明:
+ * - 封装飞书开放平台的所有API调用
+ * - 支持tenant_access_token和user_access_token
+ * - 提供用户信息、OAuth2认证等功能
+ * - 统一错误处理和日志记录
+ *
+ * @module client
+ * @author fmode
+ * @date 2026
+ */
+
+/**
+ * Tenant Access Token 响应接口
+ */
+export interface TenantAccessTokenResponse {
+  tenantAccessToken: string;
+  expire: number;
+}
+
+/**
+ * App Access Token 响应接口
+ */
+export interface AppAccessTokenResponse {
+  appAccessToken: string;
+  expire: number;
+}
+
+/**
+ * User Access Token 响应接口
+ */
+export interface UserAccessTokenResponse {
+  accessToken: string;
+  refreshToken: string;
+  expiresIn: number;
+  tokenType: string;
+  refreshExpiresIn: number;
+}
+
+/**
+ * 用户信息接口
+ */
+export interface FeishuUserInfo {
+  name: string;
+  enName?: string;
+  avatarUrl: string;
+  avatarThumb?: string;
+  avatarMiddle?: string;
+  avatarBig?: string;
+  openId: string;
+  unionId: string;
+  email?: string;
+  enterpriseEmail?: string;
+  userId: string;
+  mobile?: string;
+  tenantKey?: string;
+}
+
+/**
+ * 登录用户信息响应接口
+ */
+export interface LoginUserInfoResponse {
+  accessToken: string;
+  tokenType: string;
+  expiresIn: number;
+  refreshToken: string;
+  refreshExpiresIn: number;
+  scope: string;
+}
+
+/**
+ * API 转发请求选项接口
+ */
+export interface ForwardRequestOptions {
+  path: string;
+  method?: string;
+  query?: Record<string, any>;
+  body?: Record<string, any>;
+}
+
+/**
+ * 飞书API客户端类
+ * 提供所有飞书API的封装方法
+ */
+export class FeishuClient {
+  private baseUrl: string;
+  private tokenCache: Map<string, any>;
+
+  /**
+   * 构造函数
+   * 初始化飞书API基础URL
+   */
+  constructor() {
+    this.baseUrl = 'https://open.feishu.cn/open-apis';
+    this.tokenCache = new Map<string, any>();
+  }
+
+  /**
+   * 获取tenant_access_token(企业自建应用)
+   *
+   * @static
+   * @async
+   * @param appId - 应用ID
+   * @param appSecret - 应用密钥
+   * @returns Promise<TenantAccessTokenResponse>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  static async getTenantAccessToken(appId: string, appSecret: string): Promise<TenantAccessTokenResponse> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
+
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json; charset=utf-8'
+        },
+        body: JSON.stringify({
+          app_id: appId,
+          app_secret: appSecret
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`获取tenant_access_token失败: ${result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        tenantAccessToken: result.tenant_access_token,
+        expire: result.expire
+      };
+    } catch (error) {
+      console.error('获取tenant_access_token异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取app_access_token(应用商店应用)
+   *
+   * @static
+   * @async
+   * @param appId - 应用ID
+   * @param appSecret - 应用密钥
+   * @returns Promise<AppAccessTokenResponse>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  static async getAppAccessToken(appId: string, appSecret: string): Promise<AppAccessTokenResponse> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal';
+
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json; charset=utf-8'
+        },
+        body: JSON.stringify({
+          app_id: appId,
+          app_secret: appSecret
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`获取app_access_token失败: ${result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        appAccessToken: result.app_access_token,
+        expire: result.expire
+      };
+    } catch (error) {
+      console.error('获取app_access_token异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * OAuth2方式获取用户访问令牌
+   *
+   * @static
+   * @async
+   * @param appId - 应用ID
+   * @param appSecret - 应用密钥
+   * @param code - 授权码
+   * @param grantType - 授权类型
+   * @returns Promise<UserAccessTokenResponse>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  static async getUserAccessToken(
+    appId: string,
+    appSecret: string,
+    code: string,
+    grantType: string = 'authorization_code'
+  ): Promise<UserAccessTokenResponse> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token';
+
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json; charset=utf-8'
+        },
+        body: JSON.stringify({
+          grant_type: grantType,
+          client_id: appId,
+          client_secret: appSecret,
+          code: code
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`获取user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        accessToken: result.access_token,
+        refreshToken: result.refresh_token,
+        expiresIn: result.expires_in,
+        tokenType: result.token_type,
+        refreshExpiresIn: result.refresh_token_expires_in
+      };
+    } catch (error) {
+      console.error('获取user_access_token异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 刷新用户访问令牌
+   *
+   * @static
+   * @async
+   * @param appId - 应用ID
+   * @param appSecret - 应用密钥
+   * @param refreshToken - 刷新令牌
+   * @returns Promise<UserAccessTokenResponse>
+   * @throws {Error} 刷新失败时抛出错误
+   */
+  static async refreshUserAccessToken(
+    appId: string,
+    appSecret: string,
+    refreshToken: string
+  ): Promise<UserAccessTokenResponse> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/authen/v2/oauth/token';
+
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json; charset=utf-8'
+        },
+        body: JSON.stringify({
+          grant_type: 'refresh_token',
+          client_id: appId,
+          client_secret: appSecret,
+          refresh_token: refreshToken
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`刷新user_access_token失败: ${result.error_description || result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        accessToken: result.access_token,
+        refreshToken: result.refresh_token,
+        expiresIn: result.expires_in,
+        tokenType: result.token_type,
+        refreshExpiresIn: result.refresh_token_expires_in
+      };
+    } catch (error) {
+      console.error('刷新user_access_token异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取用户信息(使用user_access_token)
+   *
+   * @static
+   * @async
+   * @param userAccessToken - 用户访问令牌
+   * @returns Promise<FeishuUserInfo>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  static async getUserInfo(userAccessToken: string): Promise<FeishuUserInfo> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/authen/v1/user_info';
+
+      const response = await fetch(url, {
+        method: 'GET',
+        headers: {
+          'Authorization': `Bearer ${userAccessToken}`,
+          'Content-Type': 'application/json; charset=utf-8'
+        }
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`获取用户信息失败: ${result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        name: result.data.name,
+        enName: result.data.en_name,
+        avatarUrl: result.data.avatar_url,
+        avatarThumb: result.data.avatar_thumb,
+        avatarMiddle: result.data.avatar_middle,
+        avatarBig: result.data.avatar_big,
+        openId: result.data.open_id,
+        unionId: result.data.union_id,
+        email: result.data.email,
+        enterpriseEmail: result.data.enterprise_email,
+        userId: result.data.user_id,
+        mobile: result.data.mobile,
+        tenantKey: result.data.tenant_key
+      };
+    } catch (error) {
+      console.error('获取用户信息异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取登录用户身份(网页免登)
+   *
+   * @static
+   * @async
+   * @param tenantAccessToken - 租户访问令牌
+   * @param code - 免登授权码
+   * @returns Promise<LoginUserInfoResponse>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  static async getLoginUserInfo(tenantAccessToken: string, code: string): Promise<LoginUserInfoResponse> {
+    try {
+      const url = 'https://open.feishu.cn/open-apis/authen/v1/access_token';
+
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Authorization': `Bearer ${tenantAccessToken}`,
+          'Content-Type': 'application/json; charset=utf-8'
+        },
+        body: JSON.stringify({
+          grant_type: 'authorization_code',
+          code: code
+        })
+      });
+
+      const result = await response.json();
+
+      if (result.code !== 0) {
+        throw new Error(`获取登录用户身份失败: ${result.msg}, code: ${result.code}`);
+      }
+
+      return {
+        accessToken: result.data.access_token,
+        tokenType: result.data.token_type,
+        expiresIn: result.data.expires_in,
+        refreshToken: result.data.refresh_token,
+        refreshExpiresIn: result.data.refresh_expires_in,
+        scope: result.data.scope
+      };
+    } catch (error) {
+      console.error('获取登录用户身份异常:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 转发请求到飞书API
+   *
+   * @static
+   * @async
+   * @param accessToken - 访问令牌
+   * @param options - 请求选项
+   * @returns Promise<any>
+   * @throws {Error} 请求失败时抛出错误
+   */
+  static async forwardRequest(accessToken: string, options: ForwardRequestOptions): Promise<any> {
+    try {
+      const { path, method = 'GET', query, body } = options;
+
+      let url = `https://open.feishu.cn/open-apis${path}`;
+
+      if (query && typeof query === 'object') {
+        const queryParams = new URLSearchParams();
+        Object.keys(query).forEach(key => {
+          if (query[key] !== undefined && query[key] !== null) {
+            queryParams.append(key, query[key]);
+          }
+        });
+        const queryString = queryParams.toString();
+        if (queryString) {
+          url += `?${queryString}`;
+        }
+      }
+
+      const requestOptions: RequestInit = {
+        method: method.toUpperCase(),
+        headers: {
+          'Authorization': `Bearer ${accessToken}`,
+          'Content-Type': 'application/json; charset=utf-8'
+        }
+      };
+
+      if (body && typeof body === 'object' && method.toUpperCase() !== 'GET') {
+        requestOptions.body = JSON.stringify(body);
+      }
+
+      console.log('转发飞书API请求:', { url, method, body });
+
+      const response = await fetch(url, requestOptions);
+      const result = await response.json();
+
+      console.log('飞书API响应:', result);
+
+      return result;
+    } catch (error) {
+      console.error('转发请求异常:', error);
+      throw error;
+    }
+  }
+}
+
+/**
+ * 默认导出客户端类
+ */
+export default FeishuClient;

+ 330 - 0
modules/fmode-feishu-api/src/config.ts

@@ -0,0 +1,330 @@
+/**
+ * 飞书应用配置管理模块
+ *
+ * 功能说明:
+ * - 管理飞书应用的密钥、配置信息等
+ * - 支持动态添加、更新、删除应用配置
+ * - 提供权限验证、环境区分等功能
+ * - 配置信息通过setter方法动态设置,不在模块中硬编码密钥
+ *
+ * @module config
+ * @author fmode
+ * @date 2026
+ */
+
+/**
+ * 飞书应用配置接口
+ */
+export interface FeishuAppConfig {
+  appId: string;
+  appSecret: string;
+  company?: string;
+  appName?: string;
+  enabled?: boolean;
+  environment?: 'development' | 'production';
+  callbackUrl?: string;
+  permissions?: string[];
+  createdAt?: Date;
+  updatedAt?: Date;
+}
+
+/**
+ * 配置验证结果接口
+ */
+export interface ConfigValidationResult {
+  valid: boolean;
+  missing: string[];
+}
+
+/**
+ * 飞书配置管理类
+ * 负责管理所有飞书应用的配置信息
+ */
+export class FeishuConfig {
+  private apps: Map<string, FeishuAppConfig>;
+
+  /**
+   * 构造函数
+   * 初始化应用配置存储Map
+   */
+  constructor() {
+    this.apps = new Map<string, FeishuAppConfig>();
+  }
+
+  /**
+   * 获取应用配置
+   *
+   * @param appId - 应用ID
+   * @returns 应用配置对象
+   * @throws {Error} 应用配置不存在或已禁用时抛出错误
+   */
+  getAppConfig(appId: string): FeishuAppConfig {
+    const config = this.apps.get(appId);
+    if (!config) {
+      throw new Error(`应用配置不存在: ${appId}`);
+    }
+    if (config.enabled === false) {
+      throw new Error(`应用已禁用: ${appId}`);
+    }
+    return config;
+  }
+
+  /**
+   * 添加或更新应用配置
+   *
+   * @param appId - 应用ID
+   * @param config - 应用配置对象
+   */
+  setAppConfig(appId: string, config: FeishuAppConfig): void {
+    const existingConfig = this.apps.get(appId);
+    const newConfig: FeishuAppConfig = {
+      ...config,
+      appId,
+      enabled: config.enabled !== undefined ? config.enabled : true,
+      updatedAt: new Date(),
+      createdAt: existingConfig?.createdAt || new Date()
+    };
+    this.apps.set(appId, newConfig);
+    console.log(`应用配置已更新: ${appId}`);
+  }
+
+  /**
+   * 批量设置应用配置
+   *
+   * @param configs - 配置对象,key为appId,value为配置
+   */
+  setAppConfigs(configs: Record<string, FeishuAppConfig>): void {
+    Object.keys(configs).forEach(appId => {
+      this.setAppConfig(appId, configs[appId]);
+    });
+  }
+
+  /**
+   * 删除应用配置
+   *
+   * @param appId - 应用ID
+   * @throws {Error} 应用配置不存在时抛出错误
+   */
+  removeAppConfig(appId: string): void {
+    if (!this.apps.has(appId)) {
+      throw new Error(`应用配置不存在: ${appId}`);
+    }
+    this.apps.delete(appId);
+    console.log(`应用配置已删除: ${appId}`);
+  }
+
+  /**
+   * 获取所有已启用的应用配置列表
+   *
+   * @returns 应用配置列表
+   */
+  getAllApps(): FeishuAppConfig[] {
+    return Array.from(this.apps.values()).filter(app => app.enabled !== false);
+  }
+
+  /**
+   * 根据环境获取应用配置
+   *
+   * @param environment - 环境名称(development/production)
+   * @returns 应用配置列表
+   */
+  getAppsByEnvironment(environment: 'development' | 'production'): FeishuAppConfig[] {
+    return Array.from(this.apps.values()).filter(
+      app => app.enabled !== false && app.environment === environment
+    );
+  }
+
+  /**
+   * 验证应用是否有指定权限
+   *
+   * @param appId - 应用ID
+   * @param permission - 权限名称
+   * @returns 是否有权限
+   */
+  hasPermission(appId: string, permission: string): boolean {
+    const config = this.getAppConfig(appId);
+    return config.permissions ? config.permissions.includes(permission) : false;
+  }
+
+  /**
+   * 从环境变量加载配置
+   */
+  loadFromEnvironment(): void {
+    const envPrefix = 'FEISHU_APP_';
+
+    Object.keys(process.env).forEach(envKey => {
+      if (envKey.startsWith(envPrefix)) {
+        const parts = envKey.split('_');
+        if (parts.length >= 4) {
+          const appId = parts[2];
+          const configKey = parts.slice(3).join('_').toLowerCase();
+          const configValue = process.env[envKey];
+
+          if (!this.apps.has(appId)) {
+            this.apps.set(appId, {
+              appId,
+              appSecret: '',
+              appName: appId,
+              enabled: true,
+              environment: (process.env.NODE_ENV as 'development' | 'production') || 'development',
+              permissions: ['user'],
+              createdAt: new Date(),
+              updatedAt: new Date()
+            });
+          }
+
+          const config = this.apps.get(appId)!;
+          switch (configKey) {
+            case 'id':
+            case 'appid':
+              config.appId = configValue || '';
+              break;
+            case 'secret':
+            case 'appsecret':
+              config.appSecret = configValue || '';
+              break;
+            case 'company':
+              config.company = configValue;
+              break;
+            case 'name':
+              config.appName = configValue;
+              break;
+            case 'callback_url':
+              config.callbackUrl = configValue;
+              break;
+          }
+        }
+      }
+    });
+
+    console.log('从环境变量加载飞书配置完成');
+  }
+
+  /**
+   * 从数据库加载配置(如果使用Parse)
+   *
+   * @async
+   * @returns Promise<void>
+   */
+  async loadFromDatabase(): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        console.warn('Parse不可用,跳过从数据库加载飞书配置');
+        return;
+      }
+
+      const FeishuApp = Parse.Object.extend('FeishuApp');
+      const query = new Parse.Query(FeishuApp);
+      query.equalTo('enabled', true);
+      const results = await query.find();
+
+      results.forEach((app: any) => {
+        const config: FeishuAppConfig = {
+          appId: app.get('appId'),
+          appName: app.get('appName'),
+          appSecret: app.get('appSecret'),
+          company: app.get('company'),
+          enabled: app.get('enabled'),
+          environment: app.get('environment') || 'production',
+          callbackUrl: app.get('callbackUrl'),
+          permissions: app.get('permissions') || ['user'],
+          createdAt: app.createdAt,
+          updatedAt: app.updatedAt
+        };
+        this.apps.set(config.appId, config);
+      });
+
+      console.log(`从数据库加载了 ${results.length} 个飞书应用配置`);
+    } catch (error) {
+      console.error('从数据库加载飞书配置失败:', error);
+    }
+  }
+
+  /**
+   * 保存配置到数据库
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<void>
+   * @throws {Error} 保存失败时抛出错误
+   */
+  async saveToDatabase(appId: string): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        console.warn('Parse不可用,跳过保存飞书配置到数据库');
+        return;
+      }
+
+      const config = this.getAppConfig(appId);
+      const FeishuApp = Parse.Object.extend('FeishuApp');
+      const query = new Parse.Query(FeishuApp);
+      query.equalTo('appId', appId);
+      let appRecord = await query.first();
+
+      if (!appRecord) {
+        appRecord = new FeishuApp();
+      }
+
+      appRecord.set('appId', config.appId);
+      appRecord.set('appName', config.appName);
+      appRecord.set('appSecret', config.appSecret);
+      appRecord.set('company', config.company);
+      appRecord.set('enabled', config.enabled);
+      appRecord.set('environment', config.environment);
+      appRecord.set('callbackUrl', config.callbackUrl);
+      appRecord.set('permissions', config.permissions);
+
+      await appRecord.save();
+      console.log(`飞书应用配置已保存到数据库: ${appId}`);
+    } catch (error) {
+      console.error('保存飞书配置到数据库失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 初始化配置管理器
+   *
+   * @async
+   * @returns Promise<void>
+   */
+  async initialize(): Promise<void> {
+    // 1. 从环境变量加载配置
+    this.loadFromEnvironment();
+
+    // 2. 从数据库加载配置(如果可用)
+    await this.loadFromDatabase();
+
+    console.log(`飞书配置管理器初始化完成,加载了 ${this.apps.size} 个应用配置`);
+  }
+
+  /**
+   * 检查配置是否完整
+   *
+   * @param appId - 应用ID
+   * @returns 验证结果对象
+   */
+  validateConfig(appId: string): ConfigValidationResult {
+    const config = this.apps.get(appId);
+    const missing: string[] = [];
+
+    if (!config) {
+      return { valid: false, missing: ['配置不存在'] };
+    }
+
+    if (!config.appId) missing.push('appId');
+    if (!config.appSecret) missing.push('appSecret');
+
+    return {
+      valid: missing.length === 0,
+      missing
+    };
+  }
+}
+
+/**
+ * 默认导出配置管理类
+ */
+export default FeishuConfig;

+ 47 - 0
modules/fmode-feishu-api/src/index.ts

@@ -0,0 +1,47 @@
+/**
+ * fmode-feishu-api - 飞书集成模块
+ *
+ * 主入口文件,导出所有公共API
+ *
+ * 使用示例:
+ * ```typescript
+ * import { createFeishuRouter } from 'fmode-feishu-api';
+ *
+ * const feishuRouter = createFeishuRouter({
+ *   'cli_xxx': {
+ *     appId: 'cli_xxx',
+ *     appSecret: 'your_secret',
+ *     company: 'company_id',
+ *     enabled: true
+ *   }
+ * });
+ *
+ * app.use('/api/feishu', feishuRouter);
+ * ```
+ *
+ * @module fmode-feishu-api
+ * @author fmode
+ * @version 1.0.0
+ * @date 2026
+ * @license MIT
+ */
+
+// 导出路由创建函数(主要API)
+export { createFeishuRouter } from './router.ts';
+export type { FeishuRouterConfig } from './router.ts';
+
+// 导出类型定义
+export type { FeishuAppConfig } from './config.ts';
+export type {
+  FeishuUserInfo,
+  UserAccessTokenResponse,
+  TenantAccessTokenResponse
+} from './client.ts';
+
+// 导出客户端类(用于高级用法)
+export { FeishuClient } from './client.ts';
+
+/**
+ * 默认导出路由创建函数
+ */
+export { createFeishuRouter as default } from './router.ts';

+ 507 - 0
modules/fmode-feishu-api/src/router.ts

@@ -0,0 +1,507 @@
+/**
+ * 飞书Express路由模块
+ *
+ * 功能说明:
+ * - 提供所有飞书相关的HTTP API路由
+ * - 支持OAuth2登录、用户信息获取等功能
+ * - 自动处理token管理和用户会话
+ *
+ * @module router
+ * @author fmode
+ * @date 2026
+ */
+
+import express from 'express';
+import type { Request, Response } from 'express';
+import type { FeishuAppConfig } from './config.ts';
+import { FeishuConfig } from './config.ts';
+import { FeishuTokenManager } from './token-manager.ts';
+import { FeishuUserManager } from './user-manager.ts';
+import { FeishuClient } from './client.ts';
+
+/**
+ * 飞书路由配置接口
+ */
+export interface FeishuRouterConfig {
+  [appId: string]: FeishuAppConfig;
+}
+
+/**
+ * 创建飞书路由实例
+ *
+ * @param config - 飞书应用配置(支持多个应用)
+ * @returns Express Router实例
+ */
+export function createFeishuRouter(config: FeishuRouterConfig): express.Router {
+  const router = express.Router();
+
+  // 创建配置管理器实例
+  const feishuConfig = new FeishuConfig();
+  feishuConfig.setAppConfigs(config);
+
+  // 创建Token管理器实例(传入配置管理器)
+  const feishuTokenManager = new FeishuTokenManager(feishuConfig);
+
+  // 创建用户管理器实例(传入配置管理器)
+  const feishuUserManager = new FeishuUserManager(feishuConfig);
+
+  console.log(`加载飞书 API 路由,配置了 ${Object.keys(config).length} 个应用`);
+
+  /**
+   * GET /test
+   * 测试端点,验证飞书模块是否正常加载
+   */
+  router.get('/test', (req: Request, res: Response) => {
+    const apps = Object.keys(config).map(appId => ({
+      appId,
+      company: config[appId].company,
+      enabled: config[appId].enabled
+    }));
+
+    res.json({
+      message: "飞书 API 模块已加载",
+      version: "1.0.0",
+      timestamp: new Date().toISOString(),
+      apps: apps,
+      endpoints: {
+        oauth2Login: "POST /oauth2/login - OAuth2扫码登录",
+        oauth2Feishu: "POST /oauth2/feishu - 网页应用免登录",
+        oauth2Refresh: "POST /oauth2/refresh_token - 刷新令牌",
+        userSync: "POST /user/sync - 同步用户信息",
+        forward: "POST /forward - 转发API请求",
+        tokenStatus: "POST /token/status - 获取Token状态",
+        tokenRefresh: "POST /token/refresh - 强制刷新Token",
+        callback: "ALL /callback - 事件回调"
+      }
+    });
+  });
+
+  /**
+   * 通用错误响应函数
+   */
+  function goWrong(response: Response, msg: string): void {
+    response.status(500);
+    response.json({
+      code: 500,
+      mess: msg
+    });
+  }
+
+  /**
+   * 记录认证事件到OpenEvent表
+   */
+  async function recordAuthEvent(appId: string, action: string, data: any): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        return;
+      }
+
+      const OpenEvent = Parse.Object.extend('OpenEvent');
+      const eventRecord = new OpenEvent();
+
+      eventRecord.set('provider', 'feishu');
+      eventRecord.set('eventType', 'auth_event');
+      eventRecord.set('appId', appId);
+      eventRecord.set('action', action);
+      eventRecord.set('status', 'success');
+      eventRecord.set('eventData', data);
+      eventRecord.set('createdAt', new Date());
+      eventRecord.set('updatedAt', new Date());
+
+      await eventRecord.save();
+      console.log(`记录认证事件到OpenEvent: ${appId}, action: ${action}`);
+    } catch (error) {
+      console.error('记录认证事件失败:', error);
+    }
+  }
+
+  /**
+   * OAuth2扫码登录
+   */
+  router.post('/oauth2/login', async function (req: Request, res: Response) {
+    try {
+      const { appId, code } = req.body;
+
+      if (!appId || !code) {
+        goWrong(res, "缺少appId或code参数");
+        return;
+      }
+
+      const appConfig = feishuConfig.getAppConfig(appId);
+      console.log(`使用应用配置: ${appId}`);
+
+      if (typeof code !== 'string' || code.length < 10) {
+        goWrong(res, "授权码格式无效");
+        return;
+      }
+
+      console.log(`开始OAuth2登录流程,appId: ${appId}, code: ${code.substring(0, 8)}...`);
+
+      const tokenInfo = await FeishuClient.getUserAccessToken(
+        appConfig.appId,
+        appConfig.appSecret,
+        code,
+        'authorization_code'
+      );
+
+      console.log(`成功获取OAuth2令牌,有效期: ${tokenInfo.expiresIn}秒`);
+
+      const userInfo = await FeishuClient.getUserInfo(tokenInfo.accessToken);
+
+      console.log(`成功获取用户信息,用户: ${userInfo.name} (${userInfo.unionId})`);
+
+      const userSessionInfo = await feishuUserManager.getUserInfoSessionToken(userInfo, appId);
+
+      console.log(`用户登录处理完成,用户ID: ${userSessionInfo.user.id}`);
+
+      await recordAuthEvent('oauth2_' + appId, 'oauth2_login', {
+        appId: appId,
+        code: code.substring(0, 8) + '...',
+        userInfo: {
+          name: userInfo.name,
+          unionId: userInfo.unionId,
+          openId: userInfo.openId
+        },
+        tokenExpireIn: tokenInfo.expiresIn,
+        userId: userSessionInfo.user.id,
+        username: userSessionInfo.user.get('username')
+      });
+
+      res.json({
+        code: 1,
+        mess: "登录成功",
+        data: {
+          userInfo: {
+            ...userInfo,
+            objectId: userSessionInfo.user.id,
+            username: userSessionInfo.user.get('username'),
+            nickname: userSessionInfo.user.get('nickname'),
+            mobile: userSessionInfo.user.get('mobile'),
+            email: userSessionInfo.user.get('email'),
+            avatar: userSessionInfo.user.get('avatar')
+          },
+          tokenInfo: {
+            accessToken: tokenInfo.accessToken,
+            refreshToken: tokenInfo.refreshToken,
+            expiresIn: tokenInfo.expiresIn
+          },
+          sessionToken: userSessionInfo.sessionToken.get('sessionToken')
+        }
+      });
+
+    } catch (error: any) {
+      console.error('OAuth2登录失败:', error);
+
+      if (error.message?.includes('invalid_grant')) {
+        goWrong(res, "授权码已过期或无效,请重新授权");
+      } else if (error.message?.includes('invalid_client')) {
+        goWrong(res, "应用信息无效,请检查appId和appSecret");
+      } else {
+        goWrong(res, error.message || "OAuth2登录失败");
+      }
+    }
+  });
+
+  /**
+   * OAuth2刷新访问令牌
+   */
+  router.post('/oauth2/refresh_token', async function (req: Request, res: Response) {
+    try {
+      const { appId, refreshToken } = req.body;
+
+      if (!appId || !refreshToken) {
+        goWrong(res, "缺少appId或refreshToken参数");
+        return;
+      }
+
+      const appConfig = feishuConfig.getAppConfig(appId);
+
+      console.log(`开始刷新OAuth2令牌,appId: ${appId}`);
+
+      const tokenInfo = await FeishuClient.refreshUserAccessToken(
+        appConfig.appId,
+        appConfig.appSecret,
+        refreshToken
+      );
+
+      console.log(`成功刷新OAuth2令牌,新有效期: ${tokenInfo.expiresIn}秒`);
+
+      await recordAuthEvent('oauth2_' + appId, 'oauth2_refresh', {
+        appId: appId,
+        newExpireIn: tokenInfo.expiresIn
+      });
+
+      res.json({
+        code: 1,
+        mess: "令牌刷新成功",
+        data: {
+          accessToken: tokenInfo.accessToken,
+          refreshToken: tokenInfo.refreshToken,
+          expiresIn: tokenInfo.expiresIn
+        }
+      });
+
+    } catch (error: any) {
+      console.error('刷新OAuth2令牌失败:', error);
+
+      if (error.message?.includes('invalid_grant')) {
+        goWrong(res, "刷新令牌已过期或无效,请重新登录");
+      } else {
+        goWrong(res, error.message || "刷新令牌失败");
+      }
+    }
+  });
+
+  /**
+   * 飞书网页免登OAuth2接口
+   */
+  router.post('/oauth2/feishu', async function (req: Request, res: Response) {
+    try {
+      const { appId, code } = req.body;
+
+      if (!appId || !code) {
+        goWrong(res, "缺少appId或code参数");
+        return;
+      }
+
+      const appConfig = feishuConfig.getAppConfig(appId);
+      console.log(`使用应用配置: ${appId}`);
+
+      const tenantAccessToken = await feishuTokenManager.getTenantAccessToken(appId);
+
+      if (typeof code !== 'string' || code.length < 10) {
+        goWrong(res, "授权码格式无效");
+        return;
+      }
+
+      console.log(`开始网页免登OAuth2流程,appId: ${appId}, code: ${code.substring(0, 8)}...`);
+
+      const loginInfo = await FeishuClient.getLoginUserInfo(tenantAccessToken, code);
+
+      console.log(`成功获取登录用户身份,access_token有效期: ${loginInfo.expiresIn}秒`);
+
+      const userInfo = await FeishuClient.getUserInfo(loginInfo.accessToken);
+
+      console.log(`成功获取飞书用户信息,unionId: ${userInfo.unionId}, userId: ${userInfo.userId}`);
+
+      const userSessionInfo = await feishuUserManager.getUserInfoSessionToken(userInfo, appId);
+
+      console.log(`用户登录处理完成,用户ID: ${userSessionInfo.user.id}`);
+
+      await recordAuthEvent('weblogin_' + appId, 'weblogin_oauth2', {
+        appId: appId,
+        code: code.substring(0, 8) + '...',
+        userInfo: {
+          name: userInfo.name,
+          unionId: userInfo.unionId,
+          openId: userInfo.openId
+        },
+        userId: userSessionInfo.user.id,
+        username: userSessionInfo.user.get('username')
+      });
+
+      res.json({
+        code: 1,
+        mess: "登录成功",
+        data: {
+          userInfo: {
+            ...userInfo,
+            objectId: userSessionInfo.user.id,
+            username: userSessionInfo.user.get('username'),
+            nickname: userSessionInfo.user.get('nickname'),
+            mobile: userSessionInfo.user.get('mobile'),
+            email: userSessionInfo.user.get('email'),
+            avatar: userSessionInfo.user.get('avatar')
+          },
+          sessionToken: userSessionInfo.sessionToken.get('sessionToken')
+        }
+      });
+
+    } catch (error: any) {
+      console.error('飞书网页免登失败:', error);
+
+      if (error.message?.includes('code')) {
+        goWrong(res, "授权码已过期或无效,请重新授权");
+      } else if (error.message?.includes('应用配置中缺少company信息')) {
+        goWrong(res, "应用配置不完整,请联系管理员");
+      } else {
+        goWrong(res, error.message || "网页免登失败");
+      }
+    }
+  });
+
+  /**
+   * 同步飞书用户信息
+   */
+  router.post('/user/sync', async function (req: Request, res: Response) {
+    try {
+      const { userInfo, appId } = req.body;
+
+      if (!userInfo || !appId) {
+        goWrong(res, "缺少userInfo或appId参数");
+        return;
+      }
+
+      if (!userInfo.unionId || !userInfo.openId) {
+        goWrong(res, "userInfo中缺少必要的unionId或openId");
+        return;
+      }
+
+      console.log(`开始同步飞书用户,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}, appId: ${appId}`);
+
+      const userSessionInfo = await feishuUserManager.getUserInfoSessionToken(userInfo, appId);
+
+      console.log(`用户同步完成,用户ID: ${userSessionInfo.user.id}`);
+
+      await recordAuthEvent('sync_' + appId, 'user_sync', {
+        appId: appId,
+        userInfo: {
+          unionId: userInfo.unionId,
+          openId: userInfo.openId,
+          name: userInfo.name
+        },
+        userId: userSessionInfo.user.id,
+        username: userSessionInfo.user.get('username')
+      });
+
+      res.json({
+        code: 1,
+        mess: "用户同步成功",
+        data: {
+          userInfo: userSessionInfo.userInfo,
+          sessionToken: userSessionInfo.sessionToken.get('sessionToken')
+        }
+      });
+
+    } catch (error: any) {
+      console.error('同步飞书用户失败:', error);
+      goWrong(res, error.message || "用户同步失败");
+    }
+  });
+
+  /**
+   * 转发请求到飞书API
+   */
+  router.post('/forward', async function (req: Request, res: Response) {
+    try {
+      const { appId, path, method = 'GET', query, body } = req.body;
+
+      if (!appId || !path) {
+        goWrong(res, "缺少appId或path参数");
+        return;
+      }
+
+      const tenantAccessToken = await feishuTokenManager.getTenantAccessToken(appId);
+
+      const result = await FeishuClient.forwardRequest(tenantAccessToken, {
+        path,
+        method,
+        query,
+        body
+      });
+
+      await recordAuthEvent(appId, 'api_forward', {
+        path: path,
+        method: method,
+        success: result.code === 0
+      });
+
+      res.json({
+        code: 1,
+        mess: "成功",
+        data: { result }
+      });
+    } catch (error: any) {
+      console.error('转发请求失败:', error);
+      goWrong(res, error.message || "转发请求失败");
+    }
+  });
+
+  /**
+   * 获取token状态
+   */
+  router.post('/token/status', async function (req: Request, res: Response) {
+    try {
+      const { appId } = req.body;
+
+      if (!appId) {
+        goWrong(res, "缺少appId参数");
+        return;
+      }
+
+      const status = await feishuTokenManager.getTokenStatus(appId);
+
+      res.json({
+        code: 1,
+        mess: "成功",
+        data: status
+      });
+    } catch (error: any) {
+      console.error('获取token状态失败:', error);
+      goWrong(res, error.message || "获取token状态失败");
+    }
+  });
+
+  /**
+   * 强制刷新token
+   */
+  router.post('/token/refresh', async function (req: Request, res: Response) {
+    try {
+      const { appId } = req.body;
+
+      if (!appId) {
+        goWrong(res, "缺少appId参数");
+        return;
+      }
+
+      const tenantAccessToken = await feishuTokenManager.forceRefreshToken(appId);
+
+      res.json({
+        code: 1,
+        mess: "成功",
+        data: { tenantAccessToken }
+      });
+    } catch (error: any) {
+      console.error('刷新token失败:', error);
+      goWrong(res, error.message || "刷新token失败");
+    }
+  });
+
+  /**
+   * 飞书事件回调
+   */
+  router.all('/callback', async function (req: Request, res: Response) {
+    try {
+      const Parse = (global as any).Parse;
+
+      if (Parse) {
+        const openEvent = new Parse.Object('OpenEvent');
+        openEvent.set('provider', 'feishu');
+        openEvent.set('eventType', 'callback');
+        openEvent.set('receivedAt', new Date());
+        openEvent.set('requestData', {
+          method: req.method,
+          query: req.query,
+          body: req.body,
+          headers: req.headers
+        });
+
+        await openEvent.save();
+        console.log('飞书回调事件已记录到OpenEvent表');
+      }
+
+      res.send('success');
+    } catch (error) {
+      console.error('处理飞书回调失败:', error);
+      res.status(500).send('error');
+    }
+  });
+
+  return router;
+}
+
+/**
+ * 默认导出创建路由的工厂函数
+ */
+export default createFeishuRouter;

+ 429 - 0
modules/fmode-feishu-api/src/token-manager.ts

@@ -0,0 +1,429 @@
+/**
+ * 飞书Token管理器模块
+ *
+ * 功能说明:
+ * - 负责tenant_access_token的获取、缓存、刷新和持久化
+ * - 支持多层缓存机制(内存+数据库)
+ * - 自动过期检测和刷新
+ * - 提供Token状态查询接口
+ *
+ * @module token-manager
+ * @author fmode
+ * @date 2026
+ */
+
+import type { FeishuAppConfig, FeishuConfig } from './config.ts';
+import { FeishuClient } from './client.ts';
+import type { TenantAccessTokenResponse } from './client.ts';
+
+/**
+ * Token信息接口
+ */
+export interface TokenInfo {
+  tenantAccessToken: string;
+  expire: number;
+  createdAt: Date;
+  expiredAt: Date;
+}
+
+/**
+ * Token状态接口
+ */
+export interface TokenStatus {
+  appId: string;
+  hasMemoryCache: boolean;
+  hasDatabaseCache: boolean;
+  isExpired: boolean | null;
+  expiredAt?: Date;
+  expire?: number;
+  lastUpdated: number | null;
+}
+
+/**
+ * 缓存项接口
+ */
+interface CacheItem {
+  data: TokenInfo;
+  timestamp: number;
+}
+
+/**
+ * 飞书Token管理器类
+ * 负责管理所有应用的tenant_access_token
+ */
+export class FeishuTokenManager {
+  private memoryCache: Map<string, CacheItem>;
+  private cacheTimeout: number;
+  private refreshThreshold: number;
+  private configManager: any;
+
+  /**
+   * 构造函数
+   * 初始化缓存和配置参数
+   * @param configManager - 配置管理器实例(必填)
+   */
+  constructor(configManager: FeishuConfig) {
+    this.memoryCache = new Map<string, CacheItem>();
+    this.cacheTimeout = 3600000; // 1小时
+    this.refreshThreshold = 300000; // 5分钟
+    this.configManager = configManager;
+  }
+
+  /**
+   * 获取tenant_access_token(自动处理缓存和刷新)
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<string> tenant_access_token
+   * @throws {Error} 获取失败时抛出错误
+   */
+  async getTenantAccessToken(appId: string): Promise<string> {
+    try {
+      // 1. 检查内存缓存
+      const cachedToken = this.getFromMemoryCache(appId);
+      if (cachedToken && !this.isTokenExpired(cachedToken)) {
+        console.log(`从内存缓存获取tenant_access_token: ${appId}`);
+        return cachedToken.tenantAccessToken;
+      }
+
+      // 2. 检查数据库缓存
+      const dbToken = await this.getFromDatabase(appId);
+      if (dbToken && !this.isTokenExpired(dbToken)) {
+        console.log(`从数据库获取tenant_access_token: ${appId}`);
+        this.updateMemoryCache(appId, dbToken);
+        return dbToken.tenantAccessToken;
+      }
+
+      // 3. 缓存过期或不存在,获取新token
+      console.log(`获取新的tenant_access_token: ${appId}`);
+      const newToken = await this.fetchNewToken(appId);
+
+      // 4. 持久化token到数据库
+      await this.persistToken(appId, newToken);
+
+      // 5. 更新内存缓存
+      this.updateMemoryCache(appId, newToken);
+
+      // 6. 记录token获取事件到OpenEvent
+      await this.recordTokenEvent(appId, newToken, 'token_refresh');
+
+      return newToken.tenantAccessToken;
+    } catch (error) {
+      console.error(`获取tenant_access_token失败: ${appId}`, error);
+      throw error;
+    }
+  }
+
+
+  /**
+   * 从飞书API获取新的tenant_access_token
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<TokenInfo> token信息对象
+   * @throws {Error} 获取失败时抛出错误
+   */
+  async fetchNewToken(appId: string): Promise<TokenInfo> {
+    const config = this.configManager.getAppConfig(appId);
+
+    console.log('获取Token配置:', {
+      appId,
+      hasAppSecret: !!config.appSecret
+    });
+
+    const result = await FeishuClient.getTenantAccessToken(config.appId, config.appSecret);
+
+    const tokenInfo: TokenInfo = {
+      tenantAccessToken: result.tenantAccessToken,
+      expire: result.expire,
+      createdAt: new Date(),
+      expiredAt: new Date(Date.now() + (result.expire - 300) * 1000)
+    };
+
+    console.log(`成功获取tenant_access_token: ${appId}, 有效期: ${result.expire}秒`);
+    return tokenInfo;
+  }
+
+  /**
+   * 持久化token到数据库
+   *
+   * @async
+   * @param appId - 应用ID
+   * @param tokenInfo - token信息对象
+   * @returns Promise<void>
+   */
+  async persistToken(appId: string, tokenInfo: TokenInfo): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        console.warn('Parse不可用,跳过token持久化');
+        return;
+      }
+
+      const OpenEvent = Parse.Object.extend('OpenEvent');
+      const query = new Parse.Query(OpenEvent);
+      query.equalTo('provider', 'feishu');
+      query.equalTo('appId', appId);
+      query.equalTo('eventType', 'token_auth');
+      query.descending('createdAt');
+      query.limit(1);
+
+      const existingRecord = await query.first();
+
+      if (existingRecord) {
+        existingRecord.set('accessToken', tokenInfo.tenantAccessToken);
+        existingRecord.set('expiredAt', tokenInfo.expiredAt);
+        existingRecord.set('expire', tokenInfo.expire);
+        existingRecord.set('updatedAt', new Date());
+        existingRecord.set('status', 'active');
+        existingRecord.set('errorMessage', null);
+        await existingRecord.save();
+        console.log(`更新token记录到OpenEvent表: ${appId}`);
+      } else {
+        const tokenRecord = new OpenEvent();
+        tokenRecord.set('provider', 'feishu');
+        tokenRecord.set('eventType', 'token_auth');
+        tokenRecord.set('appId', appId);
+        tokenRecord.set('accessToken', tokenInfo.tenantAccessToken);
+        tokenRecord.set('expiredAt', tokenInfo.expiredAt);
+        tokenRecord.set('expire', tokenInfo.expire);
+        tokenRecord.set('status', 'active');
+        tokenRecord.set('createdAt', new Date());
+        tokenRecord.set('updatedAt', new Date());
+        await tokenRecord.save();
+        console.log(`创建新token记录到OpenEvent表: ${appId}`);
+      }
+    } catch (error) {
+      console.error('持久化token失败:', error);
+    }
+  }
+
+  /**
+   * 从数据库获取token
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<TokenInfo | null>
+   */
+  async getFromDatabase(appId: string): Promise<TokenInfo | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        return null;
+      }
+
+      const OpenEvent = Parse.Object.extend('OpenEvent');
+      const query = new Parse.Query(OpenEvent);
+      query.equalTo('provider', 'feishu');
+      query.equalTo('appId', appId);
+      query.equalTo('eventType', 'token_auth');
+      query.equalTo('status', 'active');
+      query.greaterThan('expiredAt', new Date());
+      query.descending('createdAt');
+      query.limit(1);
+
+      const record = await query.first();
+
+      if (record) {
+        return {
+          tenantAccessToken: record.get('accessToken'),
+          expire: record.get('expire'),
+          expiredAt: record.get('expiredAt'),
+          createdAt: record.get('createdAt')
+        };
+      }
+
+      return null;
+    } catch (error) {
+      console.error('从数据库获取token失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 从内存缓存获取token
+   *
+   * @param appId - 应用ID
+   * @returns TokenInfo | null
+   */
+  getFromMemoryCache(appId: string): TokenInfo | null {
+    const cached = this.memoryCache.get(appId);
+    if (cached && (Date.now() - cached.timestamp < this.cacheTimeout)) {
+      return cached.data;
+    }
+    return null;
+  }
+
+  /**
+   * 更新内存缓存
+   *
+   * @param appId - 应用ID
+   * @param tokenInfo - token信息对象
+   */
+  updateMemoryCache(appId: string, tokenInfo: TokenInfo): void {
+    this.memoryCache.set(appId, {
+      data: tokenInfo,
+      timestamp: Date.now()
+    });
+  }
+
+  /**
+   * 检查token是否过期
+   *
+   * @param tokenInfo - token信息对象
+   * @returns boolean
+   */
+  isTokenExpired(tokenInfo: TokenInfo): boolean {
+    if (!tokenInfo || !tokenInfo.expiredAt) {
+      return true;
+    }
+    return new Date() >= new Date(tokenInfo.expiredAt.getTime() - this.refreshThreshold);
+  }
+
+  /**
+   * 记录token相关事件到OpenEvent
+   *
+   * @async
+   * @param appId - 应用ID
+   * @param tokenInfo - token信息
+   * @param action - 事件类型
+   * @returns Promise<void>
+   */
+  async recordTokenEvent(appId: string, tokenInfo: TokenInfo, action: string): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        return;
+      }
+
+      const OpenEvent = Parse.Object.extend('OpenEvent');
+      const eventRecord = new OpenEvent();
+
+      eventRecord.set('provider', 'feishu');
+      eventRecord.set('eventType', 'token_event');
+      eventRecord.set('appId', appId);
+      eventRecord.set('action', action);
+      eventRecord.set('status', 'success');
+      eventRecord.set('expire', tokenInfo.expire);
+      eventRecord.set('expiredAt', tokenInfo.expiredAt);
+      eventRecord.set('createdAt', new Date());
+      eventRecord.set('updatedAt', new Date());
+
+      await eventRecord.save();
+      console.log(`记录token事件到OpenEvent: ${appId}, action: ${action}`);
+    } catch (error) {
+      console.error('记录token事件失败:', error);
+    }
+  }
+
+  /**
+   * 清理过期的token记录
+   *
+   * @async
+   * @returns Promise<void>
+   */
+  async cleanupExpiredTokens(): Promise<void> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        return;
+      }
+
+      const OpenEvent = Parse.Object.extend('OpenEvent');
+      const query = new Parse.Query(OpenEvent);
+      query.equalTo('provider', 'feishu');
+      query.equalTo('eventType', 'token_auth');
+      query.lessThan('expiredAt', new Date());
+
+      const expiredRecords = await query.find();
+
+      for (const record of expiredRecords) {
+        record.set('status', 'expired');
+        record.set('updatedAt', new Date());
+        await record.save();
+      }
+
+      console.log(`清理了 ${expiredRecords.length} 个过期的token记录`);
+    } catch (error) {
+      console.error('清理过期token失败:', error);
+    }
+  }
+
+  /**
+   * 强制刷新指定应用的token
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<string> 新的tenant_access_token
+   * @throws {Error} 刷新失败时抛出错误
+   */
+  async forceRefreshToken(appId: string): Promise<string> {
+    try {
+      console.log(`强制刷新token: ${appId}`);
+
+      this.memoryCache.delete(appId);
+
+      const newToken = await this.fetchNewToken(appId);
+
+      await this.persistToken(appId, newToken);
+
+      this.updateMemoryCache(appId, newToken);
+
+      await this.recordTokenEvent(appId, newToken, 'token_force_refresh');
+
+      return newToken.tenantAccessToken;
+    } catch (error) {
+      console.error(`强制刷新token失败: ${appId}`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取token状态信息
+   *
+   * @async
+   * @param appId - 应用ID
+   * @returns Promise<TokenStatus>
+   * @throws {Error} 获取失败时抛出错误
+   */
+  async getTokenStatus(appId: string): Promise<TokenStatus> {
+    try {
+      const cachedToken = this.getFromMemoryCache(appId);
+      const dbToken = await this.getFromDatabase(appId);
+
+      return {
+        appId,
+        hasMemoryCache: !!cachedToken,
+        hasDatabaseCache: !!dbToken,
+        isExpired: cachedToken ? this.isTokenExpired(cachedToken) : null,
+        expiredAt: cachedToken?.expiredAt || dbToken?.expiredAt,
+        expire: cachedToken?.expire || dbToken?.expire,
+        lastUpdated: cachedToken ? Date.now() : null
+      };
+    } catch (error) {
+      console.error(`获取token状态失败: ${appId}`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * 初始化token管理器
+   *
+   * @async
+   * @returns Promise<void>
+   */
+  async initialize(): Promise<void> {
+    await this.cleanupExpiredTokens();
+
+    setInterval(() => {
+      this.cleanupExpiredTokens().catch(console.error);
+    }, 30 * 60 * 1000);
+
+    console.log('飞书Token管理器初始化完成');
+  }
+}
+
+/**
+ * 默认导出Token管理器类
+ */
+export default FeishuTokenManager;

+ 521 - 0
modules/fmode-feishu-api/src/user-manager.ts

@@ -0,0 +1,521 @@
+/**
+ * 飞书用户管理器模块
+ *
+ * 功能说明:
+ * - 负责飞书OAuth2登录用户的获取或创建
+ * - SessionToken管理
+ * - 用户信息同步和更新
+ * - 支持多公司/组织用户管理
+ *
+ * @module user-manager
+ * @author fmode
+ * @date 2026
+ */
+
+import crypto from 'crypto';
+import type { FeishuConfig } from './config.ts';
+import type { FeishuUserInfo } from './client.ts';
+
+/**
+ * 用户会话信息接口
+ */
+export interface UserSessionInfo {
+  user: any;
+  sessionToken: any;
+  userInfo: {
+    objectId: string;
+    username: string;
+    nickname: string;
+    mobile?: string;
+    email?: string;
+    avatar: string;
+    company: any;
+    sessionToken: string;
+  };
+}
+
+/**
+ * 飞书用户管理器类
+ * 负责用户的查找、创建、更新和sessionToken管理
+ */
+export class FeishuUserManager {
+  private configManager: any;
+
+  /**
+   * 构造函数
+   * @param configManager - 配置管理器实例(必填)
+   */
+  constructor(configManager: FeishuConfig) {
+    this.configManager = configManager;
+  }
+
+
+  /**
+   * 根据飞书用户信息获取或创建用户,并生成sessionToken
+   *
+   * @async
+   * @param userInfo - 飞书用户信息
+   * @param appId - 飞书应用ID
+   * @returns Promise<UserSessionInfo>
+   * @throws {Error} 处理失败时抛出错误
+   */
+  async getUserInfoSessionToken(userInfo: FeishuUserInfo, appId: string): Promise<UserSessionInfo> {
+    try {
+      const appConfig = this.configManager.getAppConfig(appId);
+      const company = appConfig.company;
+
+      if (!company) {
+        throw new Error(`应用配置中缺少company信息: ${appId}`);
+      }
+
+      console.log(`处理飞书用户登录,unionId: ${userInfo.unionId}, openId: ${userInfo.openId}, company: ${company}`);
+
+      let user = await this.findOrCreateUser(userInfo, company);
+
+      const sessionToken = await this.generateSessionToken(user);
+
+      await this.saveUserData(user, userInfo);
+
+      console.log(`用户登录成功,userId: ${user.id}, username: ${user.get('username')}`);
+
+      return {
+        user: user,
+        sessionToken: sessionToken,
+        userInfo: {
+          objectId: user.id,
+          username: user.get('username'),
+          nickname: user.get('nickname'),
+          mobile: user.get('mobile'),
+          email: user.get('email'),
+          avatar: user.get('avatar'),
+          company: user.get('company'),
+          sessionToken: sessionToken.get('sessionToken'),
+        }
+      };
+
+    } catch (error) {
+      console.error('获取用户sessionToken失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 查找或创建用户
+   *
+   * @async
+   * @param userInfo - 飞书用户信息
+   * @param company - 公司ID
+   * @returns Promise<any> Parse用户对象
+   * @throws {Error} 查找或创建失败时抛出错误
+   */
+  async findOrCreateUser(userInfo: FeishuUserInfo, company: string): Promise<any> {
+    try {
+      let username = `${company}_${userInfo.openId}`;
+
+      let user = await this.findUserByUnionId(userInfo.unionId, company);
+
+      if (!user) {
+        user = await this.findUserByOpenId(userInfo.openId, company);
+      }
+
+      if (!user) {
+        user = await this.findUserByUsername(username);
+      }
+
+      if (!user && userInfo.mobile) {
+        user = await this.findUserByMobile(userInfo.mobile, company);
+      }
+
+      if (user) {
+        console.log(`找到现有用户,更新信息: ${user.id}`);
+        await this.updateUserInfo(user, userInfo);
+        return user;
+      }
+
+      console.log(`创建新用户,unionId: ${userInfo.unionId}`);
+      return await this.createNewUser(userInfo, company);
+
+    } catch (error) {
+      console.error('查找或创建用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 生成用户默认密码
+   *
+   * @param username - 用户名
+   * @returns 默认密码
+   */
+  generateDefaultPassword(username: string): string {
+    if (!username || username.length < 6) {
+      return username.padStart(6, '0');
+    }
+    return username.slice(-6);
+  }
+
+  /**
+   * 通过unionId查找用户
+   *
+   * @async
+   * @param unionId - 飞书用户unionId
+   * @param company - 公司ID
+   * @returns Promise<any | null>
+   */
+  async findUserByUnionId(unionId: string, company: string): Promise<any | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query("_User");
+      query.equalTo("data.feishu.unionId", unionId);
+      query.equalTo("company", {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: company
+      });
+
+      return await query.first({ useMasterKey: true });
+    } catch (error) {
+      console.error('通过unionId查找用户失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 通过openId查找用户
+   *
+   * @async
+   * @param openId - 飞书用户openId
+   * @param company - 公司ID
+   * @returns Promise<any | null>
+   */
+  async findUserByOpenId(openId: string, company: string): Promise<any | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query("_User");
+      query.equalTo("data.feishu.openId", openId);
+      query.equalTo("company", {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: company
+      });
+
+      return await query.first({ useMasterKey: true });
+    } catch (error) {
+      console.error('通过openId查找用户失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 通过用户名查找用户
+   *
+   * @async
+   * @param username - 用户名
+   * @returns Promise<any | null>
+   */
+  async findUserByUsername(username: string): Promise<any | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query("_User");
+      query.equalTo("username", username);
+
+      return await query.first({ useMasterKey: true });
+    } catch (error) {
+      console.error('通过username查找用户失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 通过手机号查找用户
+   *
+   * @async
+   * @param mobile - 手机号
+   * @param company - 公司ID
+   * @returns Promise<any | null>
+   */
+  async findUserByMobile(mobile: string, company: string): Promise<any | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query("_User");
+      query.equalTo("mobile", mobile);
+      query.equalTo("company", {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: company
+      });
+
+      return await query.first({ useMasterKey: true });
+    } catch (error) {
+      console.error('通过手机号查找用户失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 更新现有用户信息
+   *
+   * @async
+   * @param user - Parse用户对象
+   * @param userInfo - 飞书用户信息
+   * @returns Promise<void>
+   * @throws {Error} 更新失败或用户被冻结时抛出错误
+   */
+  async updateUserInfo(user: any, userInfo: FeishuUserInfo): Promise<void> {
+    try {
+      if (userInfo.name) user.set('nickname', userInfo.name);
+      if (userInfo.mobile) user.set('mobile', userInfo.mobile);
+      if (userInfo.avatarUrl) user.set('avatar', userInfo.avatarUrl);
+
+      const data = user.get('data') || {};
+      data.feishu = {
+        unionId: userInfo.unionId,
+        openId: userInfo.openId,
+        name: userInfo.name,
+        enName: userInfo.enName,
+        avatarUrl: userInfo.avatarUrl,
+        mobile: userInfo.mobile,
+        email: userInfo.email,
+        userId: userInfo.userId,
+        lastLoginAt: new Date(),
+        updatedAt: new Date()
+      };
+      user.set('data', data);
+
+      if (user.get('status') === 'freeze') {
+        throw new Error('该账户已被冻结');
+      }
+
+      await user.save(null, { useMasterKey: true });
+      console.log(`用户信息更新成功: ${user.id}`);
+    } catch (error) {
+      console.error('更新用户信息失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 创建新用户
+   *
+   * @async
+   * @param userInfo - 飞书用户信息
+   * @param company - 公司ID
+   * @returns Promise<any> Parse用户对象
+   * @throws {Error} 创建失败时抛出错误
+   */
+  async createNewUser(userInfo: FeishuUserInfo, company: string): Promise<any> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const UserClass = Parse.Object.extend("_User");
+      const user = new UserClass();
+
+      const username = `${company}_${userInfo.openId}`;
+      const defaultPassword = this.generateDefaultPassword(username);
+
+      user.set("username", username);
+      user.set("password", defaultPassword);
+      user.set("nickname", userInfo.name || `飞书用户_${userInfo.openId.slice(-6)}`);
+      user.set("mobile", userInfo.mobile);
+      user.set("avatar", userInfo.avatarUrl || 'https://s1-imfile.feishucdn.com/static-resource/v1/default_avatar.png');
+      user.set("type", 'user');
+      user.set("status", 'normal');
+
+      user.set("company", {
+        __type: 'Pointer',
+        className: 'Company',
+        objectId: company
+      });
+
+      user.set("data", {
+        feishu: {
+          unionId: userInfo.unionId,
+          openId: userInfo.openId,
+          name: userInfo.name,
+          enName: userInfo.enName,
+          avatarUrl: userInfo.avatarUrl,
+          mobile: userInfo.mobile,
+          email: userInfo.email,
+          userId: userInfo.userId,
+          createdAt: new Date(),
+          lastLoginAt: new Date(),
+        }
+      });
+
+      await user.save(null, { useMasterKey: true });
+      console.log(`新用户创建成功: ${user.id}, username: ${username}, 默认密码: ${defaultPassword}`);
+
+      return user;
+    } catch (error) {
+      console.error('创建新用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 生成sessionToken
+   *
+   * @async
+   * @param user - Parse用户对象
+   * @returns Promise<any> Session对象
+   * @throws {Error} 生成失败时抛出错误
+   */
+  async generateSessionToken(user: any): Promise<any> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const SessionClass = Parse.Object.extend('ession');
+      const session = new SessionClass();
+
+      const salt = user.id + '_' + (new Date().getTime() / 1000).toFixed();
+      const md5 = crypto.createHash('md5').update(salt, 'utf8').digest('hex');
+      const sessionToken = "r:" + md5;
+
+      console.log(`生成SessionToken: ${sessionToken} for user: ${user.id}`);
+
+      session.set("user", {
+        __type: 'Pointer',
+        className: '_User',
+        objectId: user.id
+      });
+      session.set("sessionToken", sessionToken);
+
+      const expiresAt = new Date();
+      expiresAt.setFullYear(expiresAt.getFullYear() + 1);
+      session.set("expiresAt", expiresAt);
+
+      session.set("createdWith", {
+        "action": "login",
+        "authProvider": "feishu"
+      });
+      session.set("restricted", false);
+      session.className = "_Session";
+      const savedSession = await session.save(null, { useMasterKey: true });
+      if (!savedSession) {
+        throw new Error('Session创建失败');
+      }
+
+      return savedSession;
+    } catch (error) {
+      console.error('生成sessionToken失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 保存用户附加信息到data字段
+   *
+   * @async
+   * @param user - Parse用户对象
+   * @param userInfo - 飞书用户信息
+   * @returns Promise<void>
+   */
+  async saveUserData(user: any, userInfo: FeishuUserInfo): Promise<void> {
+    try {
+      const data = user.get('data') || {};
+
+      if (!data.feishu) {
+        data.feishu = {};
+      }
+
+      data.feishu[userInfo.openId] = {
+        ...userInfo,
+        lastLoginAt: new Date(),
+        updatedAt: new Date()
+      };
+
+      data.feishu.unionId = userInfo.unionId;
+      data.feishu.currentOpenId = userInfo.openId;
+
+      user.set('data', data);
+      await user.save(null, { useMasterKey: true });
+
+      console.log(`用户数据保存成功: ${user.id}`);
+    } catch (error) {
+      console.error('保存用户数据失败:', error);
+    }
+  }
+
+  /**
+   * 验证sessionToken
+   *
+   * @async
+   * @param sessionToken - 会话令牌
+   * @returns Promise<any | null>
+   */
+  async validateSessionToken(sessionToken: string): Promise<any | null> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query('_Session');
+      query.equalTo('sessionToken', sessionToken);
+      query.greaterThan('expiresAt', new Date());
+      query.include('user');
+
+      const session = await query.first({ useMasterKey: true });
+
+      if (session && session.get('user')) {
+        return session.get('user');
+      }
+
+      return null;
+    } catch (error) {
+      console.error('验证sessionToken失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 通过unionId查找所有关联用户
+   *
+   * @async
+   * @param unionId - 飞书统一ID
+   * @returns Promise<any[]>
+   */
+  async findUsersByUnionId(unionId: string): Promise<any[]> {
+    try {
+      const Parse = (global as any).Parse;
+      if (!Parse) {
+        throw new Error('Parse不可用');
+      }
+
+      const query = new Parse.Query("_User");
+      query.equalTo("data.feishu.unionId", unionId);
+
+      return await query.find({ useMasterKey: true });
+    } catch (error) {
+      console.error('通过unionId查找用户失败:', error);
+      return [];
+    }
+  }
+}
+
+/**
+ * 默认导出用户管理器类
+ */
+export default FeishuUserManager;

+ 161 - 0
modules/fmode-feishu-api/upload.sh

@@ -0,0 +1,161 @@
+#!/bin/bash
+
+###############################################################################
+# fmode-feishu-api CDN上传脚本
+#
+# 功能:
+# 1. 上传打包后的文件到七牛云CDN
+# 2. 支持上传指定版本或最新版本
+#
+# 使用方法:
+#   chmod +x upload.sh
+#   ./upload.sh              # 上传最新版本
+#   ./upload.sh 1.0.0        # 上传指定版本
+#
+# 前置条件:
+#   需要安装并配置 qshell 工具
+#   参考: https://developer.qiniu.com/kodo/tools/1302/qshell
+###############################################################################
+
+# 设置错误时退出
+set -e
+
+# 颜色输出
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+RED='\033[0;31m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+echo -e "${GREEN}==================================================${NC}"
+echo -e "${GREEN}   fmode-feishu-api CDN上传脚本${NC}"
+echo -e "${GREEN}==================================================${NC}"
+
+# 检查qshell是否安装
+if ! command -v qshell &> /dev/null; then
+    echo -e "${RED}错误: qshell 未安装${NC}"
+    echo -e "${YELLOW}请先安装 qshell: https://developer.qiniu.com/kodo/tools/1302/qshell${NC}"
+    exit 1
+fi
+
+# 检查是否在正确的目录
+if [ ! -f "package.json" ]; then
+    echo -e "${RED}错误: package.json 不存在${NC}"
+    echo -e "${YELLOW}请确保在 fmode-feishu-api 目录下运行此脚本${NC}"
+    exit 1
+fi
+
+# 获取版本号(从参数或package.json)
+if [ -n "$1" ]; then
+    VERSION="$1"
+    echo -e "${YELLOW}使用指定版本: ${VERSION}${NC}"
+else
+    VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "1.0.0")
+    echo -e "${YELLOW}使用package.json版本: ${VERSION}${NC}"
+fi
+
+# 检查dist目录是否存在
+DIST_DIR="dist/${VERSION}"
+if [ ! -d "${DIST_DIR}" ]; then
+    echo -e "${RED}错误: ${DIST_DIR} 目录不存在${NC}"
+    echo -e "${YELLOW}请先运行 ./build.sh 构建项目${NC}"
+    exit 1
+fi
+
+# CDN配置
+BUCKET="nova-repos"
+CDN_PREFIX="x/fmode-feishu-api/${VERSION}"
+
+echo ""
+echo -e "${BLUE}上传配置:${NC}"
+echo -e "  Bucket: ${BUCKET}"
+echo -e "  CDN路径: ${CDN_PREFIX}"
+echo -e "  本地目录: ${DIST_DIR}"
+echo ""
+
+# 确认上传
+read -p "确认上传到CDN?(y/N) " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+    echo -e "${YELLOW}取消上传${NC}"
+    exit 0
+fi
+
+echo ""
+echo -e "${YELLOW}开始上传文件...${NC}"
+
+# 上传文件列表
+FILES=(
+    "fmode-feishu-api.js"
+    "fmode-feishu-api.js.map"
+    "fmode-feishu-api.min.js"
+    "fmode-feishu-api.min.js.map"
+    "fmode-feishu-api.cjs"
+    "fmode-feishu-api.cjs.map"
+    "fmode-feishu-api.min.cjs"
+    "fmode-feishu-api.min.cjs.map"
+)
+
+UPLOAD_COUNT=0
+FAILED_COUNT=0
+
+# 上传每个文件
+for FILE in "${FILES[@]}"; do
+    LOCAL_FILE="${DIST_DIR}/${FILE}"
+
+    if [ ! -f "${LOCAL_FILE}" ]; then
+        echo -e "${YELLOW}⊘ 跳过: ${FILE} (文件不存在)${NC}"
+        continue
+    fi
+
+    CDN_KEY="${CDN_PREFIX}/${FILE}"
+
+    echo -e "${BLUE}↑ 上传: ${FILE}${NC}"
+
+    if qshell rput "${BUCKET}" "${CDN_KEY}" "${LOCAL_FILE}" --overwrite; then
+        echo -e "${GREEN}✓ 成功: ${FILE}${NC}"
+        UPLOAD_COUNT=$((UPLOAD_COUNT + 1))
+    else
+        echo -e "${RED}✗ 失败: ${FILE}${NC}"
+        FAILED_COUNT=$((FAILED_COUNT + 1))
+    fi
+
+    echo ""
+done
+
+# 显示结果
+echo -e "${GREEN}==================================================${NC}"
+if [ $FAILED_COUNT -eq 0 ]; then
+    echo -e "${GREEN}   上传完成!${NC}"
+else
+    echo -e "${YELLOW}   上传完成(有${FAILED_COUNT}个文件失败)${NC}"
+fi
+echo -e "${GREEN}==================================================${NC}"
+echo ""
+echo -e "${BLUE}上传统计:${NC}"
+echo -e "  成功: ${UPLOAD_COUNT} 个文件"
+echo -e "  失败: ${FAILED_COUNT} 个文件"
+echo ""
+echo -e "${YELLOW}CDN访问地址:${NC}"
+echo -e "  ${GREEN}# ESM (生产环境)${NC}"
+echo -e "  https://repos.fmode.cn/${CDN_PREFIX}/fmode-feishu-api.min.js"
+echo ""
+echo -e "  ${GREEN}# ESM (开发环境)${NC}"
+echo -e "  https://repos.fmode.cn/${CDN_PREFIX}/fmode-feishu-api.js"
+echo ""
+echo -e "  ${GREEN}# CJS (生产环境)${NC}"
+echo -e "  https://repos.fmode.cn/${CDN_PREFIX}/fmode-feishu-api.min.cjs"
+echo ""
+echo -e "${YELLOW}使用示例:${NC}"
+echo -e "  ${GREEN}# Deno / Node.js ESM${NC}"
+echo -e "  import { createFeishuRouter } from 'https://repos.fmode.cn/${CDN_PREFIX}/fmode-feishu-api.min.js?code=xxxxxxx';"
+echo ""
+echo -e "  ${GREEN}# Node.js CJS${NC}"
+echo -e "  const { createFeishuRouter } = require('https://repos.fmode.cn/${CDN_PREFIX}/fmode-feishu-api.min.cjs?code=xxxxxxx');"
+echo ""
+
+if [ $FAILED_COUNT -eq 0 ]; then
+    exit 0
+else
+    exit 1
+fi

+ 14 - 2
server.ts

@@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs';
 const cors = require('cors');
 import { createTikHubCustomizeRoutes } from './modules/fmode-tikhub-server/src/mod';
 import { createFeishuRoutes } from './api/module/feishu';
+import { createFeishuRouter } from './modules/fmode-feishu-api/src/index';
 // import { createPinterestRouter } from './modules/fmode-brightdata-server/src/index';
 
 const Parse = require('parse/node');
@@ -66,7 +67,7 @@ app.get('/', (req: any, res: any) => {
     message: 'Fmode Server - Gongzuo API',
     version: '1.0.0',
     timestamp: new Date().toISOString(),
-    endpoints: ['/api', '/health', '/api/tikhub', '/api/feishu']
+    endpoints: ['/api', '/health', '/api/tikhub', '/api/feishu', '/api/feishu-v2']
   });
 });
 
@@ -76,9 +77,20 @@ app.use('/api/tikhub', createTikHubCustomizeRoutes({
     apiKey: 'tKIbAsEM8X+GmE2vHqGW7D/ICwK1Q5V4viKFrWiPB6HholGdLFqZJmmyNw=='
   }));
 
-// 挂载飞书模块路由
+// 挂载飞书模块路由(旧版)
 app.use('/api/feishu', createFeishuRoutes());
 
+// 挂载飞书模块路由(新版 fmode-feishu-api)
+app.use('/api/feishu-v2', createFeishuRouter({
+  'cli_a9253658eef99cd2': {
+    appId: 'cli_a9253658eef99cd2',
+    appSecret: '9ci7yeo4bA81ew63gC1OHhbhVWlNb2yx',
+    company: 'test_company',
+    enabled: true,
+    environment: 'development'
+  }
+}));
+
 // 挂载 Pinterest 模块路由
 // app.use('/api/pinterest', createPinterestRouter());