oasis_test_v2 2.2.111 → 2.2.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,12 @@
10
10
  # 环境变量:
11
11
  # LARK_BIN — 真实 lark-cli 绝对路径(避免递归调用自身)
12
12
  # FEISHU_CREDS_FILE — 凭据文件路径(内含 export LARKSUITE_CLI_* 行)
13
+ #
14
+ # 按 Agent 限制可用模块(产品反馈「飞书连接器·按 Agent 的权限模型」):凭据文件里有
15
+ # OASIS_FEISHU_ALLOW 时,只放行所选模块的 `lark-cli <domain>` 与 `lark-cli api <METHOD> /open-apis/<service>/…`,
16
+ # 机器人与用户身份一样拦(bot 的企业 token 能调应用开通的全集,飞书侧分不开)。放行表由 inject() 按
17
+ # FEISHU_AUTH_SELECTION 算好写入(feishuModulePolicyEnv),这里不认识任何模块,只查表。
18
+ # 被拦时照 lark-cli 的错误形状往 stderr 打一段 JSON、退出码 3(与 lark-cli 自己的鉴权失败同码)。
13
19
 
14
20
  set -euo pipefail
15
21
 
@@ -21,10 +27,94 @@ source "$(dirname "$(realpath "$0")")/../_base/wrapper-base.sh"
21
27
  oasis_resolve_bin
22
28
  oasis_check_available
23
29
 
30
+ # 放行表只认凭据文件里的:先清掉 agent 环境里可能带进来的同名变量。
31
+ unset OASIS_FEISHU_ALLOW OASIS_FEISHU_NEED OASIS_FEISHU_MODULES
32
+
24
33
  # Load feishu credentials into THIS process only (not visible to the agent).
25
34
  if [ -n "${FEISHU_CREDS_FILE:-}" ] && [ -f "${FEISHU_CREDS_FILE}" ]; then
26
35
  # shellcheck disable=SC1090
27
36
  source "${FEISHU_CREDS_FILE}"
28
37
  fi
29
38
 
39
+ # 任何模块都放行的命令(帮助、查参数、内置技能、自检);与 scopes.ts FEISHU_NEUTRAL_COMMANDS 同一份。
40
+ FEISHU_NEUTRAL_COMMANDS=" help schema skills whoami doctor completion auth config profile update "
41
+ # 任何模块都放行的接口 service;与 scopes.ts FEISHU_NEUTRAL_API_SERVICES 同一份。
42
+ FEISHU_NEUTRAL_API=" authen auth "
43
+
44
+ feishu_blocked() {
45
+ # $1 = 放行表里的键(cmd:<domain> / api:<service>);$2 = 给人看的命令写法(已过滤成安全字符)。
46
+ local key="$1" shown="$2" need="" message hint
47
+ local entry
48
+ for entry in ${OASIS_FEISHU_NEED:-}; do
49
+ if [ "${entry%%=*}" = "$key" ]; then need="${entry#*=}"; break; fi
50
+ done
51
+ if [ -n "$need" ]; then
52
+ message="本 agent 的飞书连接器没有开放「${need}」模块,${shown} 已被 Oasis 拦下(机器人与用户身份都不可用)。当前开放的模块:${OASIS_FEISHU_MODULES:-无}。"
53
+ hint="如需使用,请让配置人在 Oasis 里这个 agent 的飞书连接器详情点「重新连接」,勾选「${need}」后完成授权。"
54
+ else
55
+ message="${shown} 不属于任何可勾选的飞书功能模块;本 agent 的飞书调用已按模块限制,Oasis 不放行它。当前开放的模块:${OASIS_FEISHU_MODULES:-无}。"
56
+ hint="请改用已开放模块对应的 lark-cli 命令;确需此能力请联系 Oasis 管理员。"
57
+ fi
58
+ printf '{\n "ok": false,\n "error": {\n "type": "oasis_module_blocked",\n "message": "%s",\n "hint": "%s"\n }\n}\n' "$message" "$hint" >&2
59
+ exit 3
60
+ }
61
+
62
+ # 只把「看起来像命令名 / service 名」的字符串回显出来,其余一律写成占位——它要进 JSON。
63
+ feishu_safe() {
64
+ case "$1" in
65
+ ''|*[!A-Za-z0-9_.-]*) printf '%s' "该命令" ;;
66
+ *) printf '%s' "$1" ;;
67
+ esac
68
+ }
69
+
70
+ feishu_enforce_modules() {
71
+ [ -n "${OASIS_FEISHU_ALLOW:-}" ] || return 0 # 没保存过模块选择:不限制
72
+ local args=("$@") i=0 n=$# cmd=""
73
+ # 跳过命令前的全局参数(--profile X、-h/--help、-v/--version)。
74
+ while [ "$i" -lt "$n" ]; do
75
+ case "${args[$i]}" in
76
+ --profile) i=$((i + 2)) ;;
77
+ -*) i=$((i + 1)) ;;
78
+ *) cmd="${args[$i]}"; break ;;
79
+ esac
80
+ done
81
+ [ -n "$cmd" ] || return 0 # 只有 --help / --version 之类
82
+ case "$FEISHU_NEUTRAL_COMMANDS" in *" $cmd "*) return 0 ;; esac
83
+
84
+ if [ "$cmd" = "api" ]; then
85
+ # lark-cli api <METHOD> <path> [flags]:取 api 之后第二个位置参数为 path(跳过带值的参数)。
86
+ local pos=0 path="" a
87
+ i=$((i + 1))
88
+ while [ "$i" -lt "$n" ]; do
89
+ a="${args[$i]}"
90
+ case "$a" in
91
+ --as|--data|--file|--format|--jq|-q|--output|-o|--page-delay|--page-limit|--page-size|--params|--profile) i=$((i + 2)); continue ;;
92
+ -*) i=$((i + 1)); continue ;;
93
+ esac
94
+ pos=$((pos + 1))
95
+ if [ "$pos" -eq 2 ]; then path="$a"; break; fi
96
+ i=$((i + 1))
97
+ done
98
+ [ -n "$path" ] || return 0 # 没给 path:lark-cli 自己报用法错,不会发请求
99
+ # 归一成 <service>/…:lark-cli 接受完整 URL、/open-apis/ 前缀可省、前导斜杠可省。
100
+ local p="$path"
101
+ p="${p#http://}"; p="${p#https://}"
102
+ case "$path" in http://*|https://*) p="${p#*/}" ;; esac
103
+ while [ "${p#/}" != "$p" ]; do p="${p#/}"; done
104
+ p="${p#open-apis/}"
105
+ while [ "${p#/}" != "$p" ]; do p="${p#/}"; done
106
+ # 路径里带 .. 或百分号编码的,不去猜它最后落到哪个 service——直接拒。
107
+ case "$p" in *..*|*%*|*\\*) feishu_blocked "api:?" "lark-cli api(路径含 .. 或编码字符)" ;; esac
108
+ local svc="${p%%[/?#]*}"
109
+ case "$FEISHU_NEUTRAL_API" in *" $svc "*) return 0 ;; esac
110
+ case "$OASIS_FEISHU_ALLOW" in *" api:$svc "*) return 0 ;; esac
111
+ feishu_blocked "api:$svc" "lark-cli api …/open-apis/$(feishu_safe "$svc")/…"
112
+ fi
113
+
114
+ case "$OASIS_FEISHU_ALLOW" in *" cmd:$cmd "*) return 0 ;; esac
115
+ feishu_blocked "cmd:$cmd" "lark-cli $(feishu_safe "$cmd")"
116
+ }
117
+
118
+ feishu_enforce_modules "$@"
119
+
30
120
  exec "$LARK_BIN" "$@"
package/dist/index.js CHANGED
@@ -19611,405 +19611,6 @@ var init_host_isolation = __esm({
19611
19611
  }
19612
19612
  });
19613
19613
 
19614
- // ../connectors/src/feishu/index.ts
19615
- async function fetchFeishuTenantAccessToken(appId, appSecret) {
19616
- try {
19617
- const resp = await fetch(FEISHU_TENANT_TOKEN_URL, {
19618
- method: "POST",
19619
- headers: { "Content-Type": "application/json; charset=utf-8" },
19620
- body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
19621
- signal: AbortSignal.timeout(TENANT_TOKEN_TIMEOUT_MS)
19622
- });
19623
- const data = await resp.json();
19624
- if (!resp.ok || data.code !== 0 || typeof data.tenant_access_token !== "string" || !data.tenant_access_token) {
19625
- log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: HTTP ${resp.status} code=${String(data.code)} ${String(data.msg ?? "")}`.trim());
19626
- return null;
19627
- }
19628
- return data.tenant_access_token;
19629
- } catch (err) {
19630
- log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: ${String(err?.message ?? err).slice(0, 200)}`);
19631
- return null;
19632
- }
19633
- }
19634
- function previousTenantToken(credsFile, appId) {
19635
- if (!credsFile) return null;
19636
- try {
19637
- const prev = readSessionCredsFile(credsFile);
19638
- if (prev["LARKSUITE_CLI_APP_ID"] !== appId) return null;
19639
- return prev["LARKSUITE_CLI_TENANT_ACCESS_TOKEN"] || null;
19640
- } catch {
19641
- return null;
19642
- }
19643
- }
19644
- function parseFeishuTokenResponse(data, nowMs) {
19645
- const accessToken = String(data["access_token"] ?? "");
19646
- const refreshToken = String(data["refresh_token"] ?? "");
19647
- const expiresInSec = Number(data["expires_in"] ?? 7200);
19648
- const refreshExpiresInSec = Number(
19649
- data["refresh_token_expires_in"] ?? data["refresh_expires_in"] ?? 30 * 24 * 3600
19650
- );
19651
- const expiresAtMs = nowMs + expiresInSec * 1e3;
19652
- return {
19653
- credentials: {
19654
- ...accessToken ? { user_access_token: accessToken } : {},
19655
- ...refreshToken ? { refresh_token: refreshToken } : {},
19656
- user_token_expires_at: String(expiresAtMs),
19657
- refresh_token_expires_at: String(nowMs + refreshExpiresInSec * 1e3)
19658
- },
19659
- expiresAt: new Date(expiresAtMs)
19660
- };
19661
- }
19662
- var import_node_child_process13, import_node_util4, import_node_path12, import_node_url, import_promises5, execFileAsync2, __dirname2, FEISHU_WELL_KNOWN_SKILLS_BASE, FEISHU_TOKEN_URL, FEISHU_TENANT_TOKEN_URL, FEISHU_USER_OPEN_ID_VAR, FEISHU_AUTHORIZED_BY_VAR, FEISHU_USER_AUTH_INVALID_VAR, FEISHU_REFRESH_REVOKED_CODES, TENANT_TOKEN_TIMEOUT_MS, FeishuConnector;
19663
- var init_feishu = __esm({
19664
- "../connectors/src/feishu/index.ts"() {
19665
- "use strict";
19666
- import_node_child_process13 = require("node:child_process");
19667
- import_node_util4 = require("node:util");
19668
- import_node_path12 = require("node:path");
19669
- import_node_url = require("node:url");
19670
- import_promises5 = require("node:fs/promises");
19671
- init_base();
19672
- init_logger();
19673
- execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process13.execFile);
19674
- __dirname2 = (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(__esm_import_meta_url));
19675
- FEISHU_WELL_KNOWN_SKILLS_BASE = "https://open.feishu.cn/.well-known/skills";
19676
- FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
19677
- FEISHU_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
19678
- FEISHU_USER_OPEN_ID_VAR = "FEISHU_USER_OPEN_ID";
19679
- FEISHU_AUTHORIZED_BY_VAR = "FEISHU_AUTHORIZED_BY";
19680
- FEISHU_USER_AUTH_INVALID_VAR = "FEISHU_USER_AUTH_INVALID";
19681
- FEISHU_REFRESH_REVOKED_CODES = /* @__PURE__ */ new Set([20024, 20026, 20037, 20064, 20073, 20074]);
19682
- TENANT_TOKEN_TIMEOUT_MS = 5e3;
19683
- FeishuConnector = class extends CliConnector {
19684
- config = {
19685
- slug: "feishu",
19686
- type: "cli",
19687
- // schema 要**声明本连接器实际消费的全部凭据**,不只是「让用户手填的那几个」。
19688
- // 历史上这里只写了 app_id,而 inject() 其实还要 app_secret / user_access_token
19689
- // (后两者由控制台 OAuth 流写进变量表,用户不手填)——于是 secret 标记缺失,
19690
- // 密钥黑名单只能在 serve.ts 另手写一份。现在黑名单从 sensitiveVars 推导,schema 必须诚实。
19691
- schema: [
19692
- { key: "app_id", description: "\u98DE\u4E66 App ID\uFF08\u7559\u7A7A\u5219\u81EA\u52A8\u521B\u5EFA\u65B0\u5E94\u7528\uFF09", secret: false, required: false },
19693
- { key: "app_secret", description: "\u98DE\u4E66 App Secret\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\uFF0C\u975E\u624B\u586B\uFF09", secret: true, required: true },
19694
- { key: "user_access_token", description: "\u98DE\u4E66\u7528\u6237 token\uFF08OAuth \u6388\u6743\u540E\u5199\u5165\uFF0C~2h \u8F6E\u6362\uFF09", secret: true, required: false }
19695
- ]
19696
- };
19697
- commandName = "lark-cli";
19698
- requiredCommands = ["lark-cli"];
19699
- envPrefix = "FEISHU_";
19700
- /** server 侧从 actor 变量表组装凭据的声明(见 provision.ts)。这些会随 job 下发到节点。 */
19701
- credentialSources = [
19702
- { key: "app_id", from: "FEISHU_APP_ID", required: true },
19703
- { key: "app_secret", from: "FEISHU_APP_SECRET", required: true },
19704
- { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
19705
- ];
19706
- /**
19707
- * 续期材料——**只在 server 侧读,不下发节点**。refresh_token 能换出新的 user token,
19708
- * 属于"长期材料",没有任何理由让它出现在 agent 所在的机器上。
19709
- */
19710
- refreshCredentialSources = [
19711
- // App 凭据是**组织级**共享的(个人变量优先,缺失回落全局默认);用户 token 类只认个人变量。
19712
- { key: "app_id", from: "FEISHU_APP_ID", orgDefault: true, required: true },
19713
- { key: "app_secret", from: "FEISHU_APP_SECRET", orgDefault: true, required: true },
19714
- { key: "refresh_token", from: "FEISHU_REFRESH_TOKEN", required: true },
19715
- { key: "user_token_expires_at", from: "FEISHU_USER_TOKEN_EXPIRES_AT", required: false },
19716
- { key: "refresh_token_expires_at", from: "FEISHU_REFRESH_TOKEN_EXPIRES_AT", required: false },
19717
- { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
19718
- ];
19719
- /** 过期判定只认这个键——必须与注入时消费的 token 同源(见接口注释里的历史坑)。 */
19720
- expiresAtCredentialKey = "user_token_expires_at";
19721
- /**
19722
- * 下发前 user token 至少还要剩 40 分钟,不够就换新(Bug 0201)。
19723
- *
19724
- * 必须 > `SESSION_CREDENTIAL_RENEW_INTERVAL_MS`(30 分钟):会话中途续期每 30 分钟问一次 server,
19725
- * 问的就是这条门槛。门槛若只有通用的 5 分钟,一张还剩 20 分钟的 token 会被原样发回,
19726
- * 20 分钟后失效、要再等 10 分钟才到下一拍——最坏断档近 25 分钟(`99991677 token expire`)。
19727
- * 取 40 = 30 分钟间隔 + 10 分钟调度余量:任一拍之后剩余 > 40,到下一拍仍 > 10。
19728
- *
19729
- * 不像 GitHub 那样取全寿命(每次都换):飞书每次刷新都会轮换 refresh_token,
19730
- * 换得越勤,「飞书已轮换、我们还没落库」的窗口就越常被碰上,所以只取刚好够用的值。
19731
- */
19732
- minRemainingLifetimeMs = 40 * 6e4;
19733
- /**
19734
- * 只由服务端定时任务续期(Bug 0318):refresh_token 一次性、每刷必轮换,派单与会话中途再各刷一次就是
19735
- * 多个写者并发打断续期链;懒续期又让闲置超过 refresh_token 寿命的员工悄悄失效。派单只读库里最新的 token。
19736
- */
19737
- scheduledRefreshOnly = true;
19738
- /** refresh_token 自身 ~30d 过期,过了只能重新授权。 */
19739
- refreshMaterialExpiresAtCredentialKey = "refresh_token_expires_at";
19740
- /**
19741
- * refresh_token 过期后该清掉的键——**只清这个员工的授权产物**。
19742
- * app_id / app_secret 刻意不在其中:它们代表组织级 App 凭据,与其他员工的连接无关。
19743
- */
19744
- clearOnExpiredCredentialKeys = [
19745
- "user_access_token",
19746
- "refresh_token",
19747
- "user_token_expires_at",
19748
- "refresh_token_expires_at"
19749
- ];
19750
- /**
19751
- * 绝不允许进入 agent 进程环境的原始变量。
19752
- * 含 APP_ID 与几个过期时间戳:它们本身不算密钥,但同属飞书凭据材料,一并挡在 agent 之外,
19753
- * 避免 agent 绕开 wrapper 自行拼凭据(沿用既有行为)。
19754
- */
19755
- sensitiveVars = [
19756
- "FEISHU_APP_ID",
19757
- "FEISHU_APP_SECRET",
19758
- "FEISHU_USER_ACCESS_TOKEN",
19759
- "FEISHU_REFRESH_TOKEN",
19760
- "FEISHU_USER_TOKEN_EXPIRES_AT",
19761
- "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
19762
- FEISHU_USER_OPEN_ID_VAR,
19763
- FEISHU_AUTHORIZED_BY_VAR,
19764
- FEISHU_USER_AUTH_INVALID_VAR
19765
- ];
19766
- /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
19767
- hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
19768
- /** inject() 写进 sessionEnv 的全部键——通用 cleanup() 按它逐个删。 */
19769
- injectedEnvKeys = [
19770
- "LARK_BIN",
19771
- "FEISHU_CREDS_FILE",
19772
- "FEISHU_RUN_TMPDIR",
19773
- "LARKSUITE_CLI_CONFIG_DIR"
19774
- ];
19775
- /**
19776
- * 自检:拿 user_access_token 问飞书「我是谁」,回 `{ok, identity, data:{name, open_id}}`。
19777
- *
19778
- * **不能用 `lark-cli auth status`**:注入外部凭据后整个 auth 命名空间会被拒
19779
- * ("credentials are provided externally and do not support interactive management"),
19780
- * 自检会恒为失败。实测 2026-08-03 踩到——`auth status` 只在**读宿主机 profile** 时才有输出,
19781
- * 恰恰是我们要隔离掉的那条路径。
19782
- */
19783
- verifyArgs = ["api", "GET", "/open-apis/authen/v1/user_info"];
19784
- /** 厂商技能包源——平台自动拉取飞书官方技能包时用。 */
19785
- wellKnownSkillsBase = FEISHU_WELL_KNOWN_SKILLS_BASE;
19786
- larkBin;
19787
- fetchTenantToken;
19788
- /** `fetchTenantToken` 只给测试替换(不打真飞书);生产一律用 {@link fetchFeishuTenantAccessToken}。 */
19789
- constructor(larkBin, opts = {}) {
19790
- super(__dirname2);
19791
- this.larkBin = larkBin ?? this.resolveLarkBin();
19792
- this.fetchTenantToken = opts.fetchTenantToken ?? fetchFeishuTenantAccessToken;
19793
- }
19794
- /**
19795
- * 解析 user_info 的 JSON。lark-cli 的 `ok:false` 也是**退出码 0** 的正常输出,
19796
- * 所以必须在这里判 ok——只看子进程退出码会把「凭据失效」当成自检通过。
19797
- */
19798
- parseIdentity(stdout) {
19799
- let parsed;
19800
- try {
19801
- parsed = JSON.parse(stdout);
19802
- } catch {
19803
- return null;
19804
- }
19805
- const obj2 = parsed;
19806
- if (obj2.ok !== true) return null;
19807
- const openId = typeof obj2.data?.open_id === "string" ? obj2.data.open_id : "";
19808
- if (!openId) return null;
19809
- const name = typeof obj2.data?.name === "string" ? obj2.data.name : "";
19810
- return name ? `${name} (${openId})` : openId;
19811
- }
19812
- async connect(options = {}) {
19813
- if (this.isSimulated()) {
19814
- return { credentials: { app_id: options["app_id"] ?? "cli_simulated" } };
19815
- }
19816
- const app_id = options["app_id"] ?? await this.initNewApp();
19817
- await this.runAuthLogin(app_id);
19818
- return { credentials: { app_id } };
19819
- }
19820
- /**
19821
- * Agent run 开始:把凭据作为 lark-cli「外部凭据」写进凭据文件(不进 agent env)。
19822
- * credentials 接受:app_id, app_secret, user_access_token?(及其 FEISHU_* 别名)。
19823
- * 企业 token 不随 job 下发,由这里用 app_id/app_secret 现换(换不到只是本轮没有 bot 身份,不抛)。
19824
- *
19825
- * 注入的 sessionEnv 键(全部非敏感):
19826
- * FEISHU_CREDS_FILE — 凭据文件路径,wrapper source 它
19827
- * FEISHU_RUN_TMPDIR — 本轮临时目录,cleanup 整个删掉
19828
- * LARK_BIN — 真实 lark-cli 绝对路径(防 wrapper 递归调用自身)
19829
- */
19830
- async inject(credentials2, sessionEnv) {
19831
- const appId = credentials2["app_id"] ?? credentials2["FEISHU_APP_ID"];
19832
- const appSecret = credentials2["app_secret"] ?? credentials2["FEISHU_APP_SECRET"];
19833
- if (!appId) throw new Error("feishu inject: missing app_id");
19834
- if (!appSecret) throw new Error("feishu inject: missing app_secret");
19835
- const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
19836
- const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
19837
- const creds = await writeSessionCredsFile(this.config.slug, {
19838
- LARKSUITE_CLI_APP_ID: appId,
19839
- LARKSUITE_CLI_APP_SECRET: appSecret,
19840
- ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
19841
- ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {}
19842
- }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
19843
- const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
19844
- await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
19845
- sessionEnv.set("LARK_BIN", await this.resolveAbsoluteLarkBin());
19846
- sessionEnv.set("FEISHU_CREDS_FILE", creds.file);
19847
- sessionEnv.set("FEISHU_RUN_TMPDIR", creds.dir);
19848
- sessionEnv.set("LARKSUITE_CLI_CONFIG_DIR", configDir);
19849
- }
19850
- /**
19851
- * 用 refresh_token 换一对新的 user token(飞书 v2 OAuth)。
19852
- *
19853
- * **纯换取,不落库**——持久化由调用方做(server 侧 `oauth.ts`)。这样平台知识
19854
- * (端点、字段名、~2h/~30d 的寿命)全部收在连接器里,与 GitHub App 的续期共用
19855
- * `_base/refresh.ts` 那套判定。
19856
- *
19857
- * 返回 null=没有可用的续期材料(未完成授权)。平台报错则抛,由上层收敛成 failed。
19858
- */
19859
- async refreshCredentials(credentials2) {
19860
- const appId = credentials2["app_id"];
19861
- const appSecret = credentials2["app_secret"];
19862
- const refreshToken = credentials2["refresh_token"];
19863
- if (!appId || !appSecret || !refreshToken) return null;
19864
- const resp = await fetch(FEISHU_TOKEN_URL, {
19865
- method: "POST",
19866
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
19867
- body: new URLSearchParams({
19868
- grant_type: "refresh_token",
19869
- refresh_token: refreshToken,
19870
- client_id: appId,
19871
- client_secret: appSecret
19872
- }).toString()
19873
- });
19874
- const data = await resp.json();
19875
- if (!resp.ok || data["error"]) {
19876
- const code2 = Number(data["code"]);
19877
- const msg = `feishu refresh failed: HTTP ${resp.status} code=${String(data["code"] ?? "")} ${String(data["error"] ?? "")}`.trim();
19878
- if (data["error"] === "invalid_grant" || FEISHU_REFRESH_REVOKED_CODES.has(code2)) throw new CredentialRevokedError(msg);
19879
- throw new Error(msg);
19880
- }
19881
- return parseFeishuTokenResponse(data, Date.now());
19882
- }
19883
- async initNewApp() {
19884
- await new Promise((resolve11, reject) => {
19885
- const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["config", "init", "--new"], { stdio: "inherit" });
19886
- proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli config init exited ${code2}`)));
19887
- proc.on("error", reject);
19888
- });
19889
- const { stdout } = await execFileAsync2(this.larkBin, ["config", "show", "--json"]);
19890
- const cfg = JSON.parse(stdout);
19891
- if (!cfg.appId) throw new Error("feishu connector: could not determine app_id after config init");
19892
- return cfg.appId;
19893
- }
19894
- async runAuthLogin(app_id) {
19895
- await new Promise((resolve11, reject) => {
19896
- const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["--profile", app_id, "auth", "login"], { stdio: "inherit" });
19897
- proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli auth login exited ${code2}`)));
19898
- proc.on("error", reject);
19899
- });
19900
- }
19901
- resolveLarkBin() {
19902
- return process.env["LARK_CLI_BIN"] ?? "lark-cli";
19903
- }
19904
- async resolveAbsoluteLarkBin() {
19905
- if (this.larkBin.includes("/")) return this.larkBin;
19906
- return this.resolveRealBin(this.larkBin);
19907
- }
19908
- };
19909
- }
19910
- });
19911
-
19912
- // ../connectors/src/feishu/user-auth.ts
19913
- async function startFeishuDeviceAuthorization(input) {
19914
- const basicAuth = Buffer.from(`${input.appId}:${input.appSecret}`).toString("base64");
19915
- const resp = await (input.fetchImpl ?? fetch)(FEISHU_DEVICE_AUTHORIZATION_URL, {
19916
- method: "POST",
19917
- headers: {
19918
- "Content-Type": "application/x-www-form-urlencoded",
19919
- "Authorization": `Basic ${basicAuth}`
19920
- },
19921
- body: new URLSearchParams({ client_id: input.appId, scope: input.scopes.join(" ") }).toString()
19922
- });
19923
- const data = await resp.json();
19924
- if (!resp.ok || data["error"]) {
19925
- throw new Error(String(data["error_description"] ?? data["error"] ?? "device authorization failed"));
19926
- }
19927
- const verificationUrl = String(data["verification_uri_complete"] ?? data["verification_uri"] ?? "");
19928
- if (!verificationUrl) throw new Error("no verification_uri in response");
19929
- const expiresIn = Number(data["expires_in"]);
19930
- const interval = Number(data["interval"]);
19931
- return {
19932
- verificationUrl,
19933
- deviceCode: String(data["device_code"] ?? ""),
19934
- ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {},
19935
- ...Number.isFinite(interval) && interval > 0 ? { interval } : {}
19936
- };
19937
- }
19938
- async function pollFeishuDeviceToken(input) {
19939
- const resp = await (input.fetchImpl ?? fetch)(FEISHU_TOKEN_URL, {
19940
- method: "POST",
19941
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
19942
- body: new URLSearchParams({
19943
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
19944
- device_code: input.deviceCode,
19945
- client_id: input.appId,
19946
- client_secret: input.appSecret
19947
- }).toString()
19948
- });
19949
- const data = await resp.json();
19950
- const errStr = data["error"];
19951
- if (errStr === "authorization_pending" || errStr === "slow_down") return { status: "pending" };
19952
- if (!resp.ok || errStr) throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
19953
- if (!data["access_token"]) throw new Error("no access_token in response");
19954
- return { status: "ok", data };
19955
- }
19956
- async function fetchFeishuUserIdentity(accessToken, fetchImpl = fetch) {
19957
- try {
19958
- const resp = await fetchImpl(FEISHU_USER_INFO_URL, { headers: { authorization: `Bearer ${accessToken}` } });
19959
- const ui = await resp.json();
19960
- if (!resp.ok || ui.code !== 0) return {};
19961
- const name = ui.data?.name || ui.data?.en_name;
19962
- return {
19963
- ...ui.data?.open_id ? { openId: ui.data.open_id } : {},
19964
- ...name ? { name } : {}
19965
- };
19966
- } catch {
19967
- return {};
19968
- }
19969
- }
19970
- async function sendFeishuBotTextToUser(input) {
19971
- const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
19972
- if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0Cbot \u79C1\u4FE1\u672A\u53D1\u51FA");
19973
- const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_SEND_MESSAGE_URL}?receive_id_type=open_id`, {
19974
- method: "POST",
19975
- headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
19976
- body: JSON.stringify({ receive_id: input.openId, msg_type: "text", content: JSON.stringify({ text: input.text }) })
19977
- });
19978
- const body2 = await resp.json();
19979
- if (body2.code !== 0) throw new Error(`feishu bot \u79C1\u4FE1\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
19980
- }
19981
- async function lookupFeishuOpenIdByEmail(input) {
19982
- const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
19983
- if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0C\u672A\u80FD\u6309\u90AE\u7BB1\u67E5\u98DE\u4E66\u7528\u6237");
19984
- const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_BATCH_GET_ID_URL}?user_id_type=open_id`, {
19985
- method: "POST",
19986
- headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
19987
- body: JSON.stringify({ emails: [input.email], include_resigned: false })
19988
- });
19989
- const body2 = await resp.json();
19990
- if (body2.code !== 0) throw new Error(`feishu \u6309\u90AE\u7BB1\u67E5\u7528\u6237\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
19991
- const hit = body2.data?.user_list?.find((u) => u.email?.toLowerCase() === input.email.toLowerCase() && u.user_id);
19992
- return hit?.user_id ?? null;
19993
- }
19994
- function feishuUserAuthInvalidText(input) {
19995
- const head = `\u300C${input.agentName}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF08\u6388\u6743\u88AB\u64A4\u9500\u6216\u957F\u65F6\u95F4\u65E0\u6CD5\u7EED\u671F\uFF09\uFF0C\u4EE5\u4F60\u8EAB\u4EFD\u8C03\u7528\u98DE\u4E66\u7684\u529F\u80FD\u6682\u65F6\u7528\u4E0D\u4E86\uFF0Cbot \u8EAB\u4EFD\u4E0D\u53D7\u5F71\u54CD\u3002`;
19996
- return input.link ? `${head}
19997
- \u70B9\u8FD9\u4E2A\u94FE\u63A5\u91CD\u65B0\u6388\u6743\uFF08\u9700\u7528\u6536\u5230\u8FD9\u6761\u79C1\u4FE1\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
19998
- ${input.link}` : `${head}
19999
- \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20000
- }
20001
- var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20002
- var init_user_auth = __esm({
20003
- "../connectors/src/feishu/user-auth.ts"() {
20004
- "use strict";
20005
- init_feishu();
20006
- FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
20007
- FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20008
- FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20009
- FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20010
- }
20011
- });
20012
-
20013
19614
  // ../connectors/src/feishu/scopes.ts
20014
19615
  function scopesForDomains(domains) {
20015
19616
  const set = /* @__PURE__ */ new Set(["offline_access"]);
@@ -20042,6 +19643,11 @@ function scopesForSelection(sel, includeSendAsUser) {
20042
19643
  return scopes;
20043
19644
  }
20044
19645
  async function appHasScope(appId, appSecret, scope2, fetchImpl = fetch) {
19646
+ return (await fetchFeishuAppScopes(appId, appSecret, fetchImpl)).some(
19647
+ (s2) => s2.scope_name === scope2 && (!s2.scope_type || s2.scope_type === "user")
19648
+ );
19649
+ }
19650
+ async function fetchFeishuAppScopes(appId, appSecret, fetchImpl = fetch) {
20045
19651
  const tokenResp = await fetchImpl("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
20046
19652
  method: "POST",
20047
19653
  headers: { "Content-Type": "application/json; charset=utf-8" },
@@ -20056,9 +19662,7 @@ async function appHasScope(appId, appSecret, scope2, fetchImpl = fetch) {
20056
19662
  });
20057
19663
  const body2 = await resp.json();
20058
19664
  if (body2.code !== 0) throw new Error(`application/v6/scopes failed: code=${body2.code} msg=${body2.msg ?? ""}`);
20059
- return (body2.data?.scopes ?? []).some(
20060
- (s2) => s2.scope_name === scope2 && (!s2.scope_type || s2.scope_type === "user")
20061
- );
19665
+ return body2.data?.scopes ?? [];
20062
19666
  }
20063
19667
  function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20064
19668
  const granted = typeof tokenResponse["scope"] === "string" ? tokenResponse["scope"].split(/\s+/).filter(Boolean) : void 0;
@@ -20069,7 +19673,64 @@ function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20069
19673
  ...granted ? { grantedScopes: granted } : {}
20070
19674
  };
20071
19675
  }
20072
- var FEISHU_BASIC_SCOPES, FEISHU_DOMAIN_SCOPES, FEISHU_USER_SCOPES, FEISHU_USER_SCOPE_STRING, FEISHU_SEND_AS_USER_SCOPE, FEISHU_AUTH_SELECTION_VAR, AUTO_APPROVED;
19676
+ function feishuModulePolicyEnv(modules) {
19677
+ const picked = modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]);
19678
+ if (picked.length === 0) return null;
19679
+ const allow = /* @__PURE__ */ new Set();
19680
+ for (const m2 of picked) {
19681
+ for (const c of FEISHU_MODULE_ACCESS[m2].commands) allow.add(`cmd:${c}`);
19682
+ for (const s2 of FEISHU_MODULE_ACCESS[m2].apiServices) allow.add(`api:${s2}`);
19683
+ }
19684
+ const need2 = /* @__PURE__ */ new Map();
19685
+ for (const [m2, a] of Object.entries(FEISHU_MODULE_ACCESS)) {
19686
+ for (const k2 of [...a.commands.map((c) => `cmd:${c}`), ...a.apiServices.map((s2) => `api:${s2}`)]) {
19687
+ if (allow.has(k2)) continue;
19688
+ need2.set(k2, [...need2.get(k2) ?? [], (FEISHU_MODULE_LABELS[m2] ?? m2).replace(/\s+/g, "")]);
19689
+ }
19690
+ }
19691
+ return {
19692
+ OASIS_FEISHU_ALLOW: ` ${[...allow].sort().join(" ")} `,
19693
+ OASIS_FEISHU_NEED: ` ${[...need2].sort(([a], [b2]) => a.localeCompare(b2)).map(([k2, v2]) => `${k2}=${v2.join("/")}`).join(" ")} `,
19694
+ OASIS_FEISHU_MODULES: picked.map((m2) => FEISHU_MODULE_LABELS[m2] ?? m2).join("\u3001")
19695
+ };
19696
+ }
19697
+ function feishuSelectedModules(raw) {
19698
+ if (!raw) return [];
19699
+ try {
19700
+ const parsed = JSON.parse(raw);
19701
+ return Array.isArray(parsed.modules) ? parsed.modules.filter((m2) => typeof m2 === "string" && !!FEISHU_MODULE_ACCESS[m2]) : [];
19702
+ } catch {
19703
+ return [];
19704
+ }
19705
+ }
19706
+ function feishuModuleAccessReport(input) {
19707
+ const granted = input.grantedScopes ? new Set(input.grantedScopes) : null;
19708
+ const tenant = (input.appScopes ?? []).filter((s2) => s2.scope_type === "tenant" && s2.scope_name);
19709
+ return input.modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]).map((m2) => {
19710
+ const prefixes = FEISHU_MODULE_ACCESS[m2].botScopePrefixes;
19711
+ const mine = tenant.filter((s2) => prefixes.some((p2) => s2.scope_name.startsWith(p2)));
19712
+ const bot = input.appScopes === null ? "unknown" : mine.some((s2) => s2.grant_status === void 0 || s2.grant_status === 1) ? "granted" : mine.length > 0 ? "pending" : "missing";
19713
+ const missingUserScopes = granted ? (FEISHU_DOMAIN_SCOPES[m2]?.scopes ?? []).filter((s2) => !granted.has(s2)) : [];
19714
+ const user = input.userAuthInvalid ? "invalid" : !input.hasUserAuth ? "none" : missingUserScopes.length > 0 ? "partial" : "granted";
19715
+ return {
19716
+ id: m2,
19717
+ label: FEISHU_MODULE_LABELS[m2] ?? m2,
19718
+ bot,
19719
+ user,
19720
+ missingUserScopes: user === "partial" ? missingUserScopes : []
19721
+ };
19722
+ });
19723
+ }
19724
+ function feishuSendAsUserState(sel) {
19725
+ if (!sel.sendAsUser) return "off";
19726
+ if (!sel.sendAsUserRequested) return "app_not_enabled";
19727
+ if (sel.grantedScopes && !sel.grantedScopes.includes(FEISHU_SEND_AS_USER_SCOPE)) return "not_granted";
19728
+ return "on";
19729
+ }
19730
+ function feishuAppPermissionConsoleUrl(appId) {
19731
+ return `https://open.feishu.cn/app/${encodeURIComponent(appId)}/auth`;
19732
+ }
19733
+ var FEISHU_BASIC_SCOPES, FEISHU_DOMAIN_SCOPES, FEISHU_USER_SCOPES, FEISHU_USER_SCOPE_STRING, FEISHU_SEND_AS_USER_SCOPE, FEISHU_AUTH_SELECTION_VAR, AUTO_APPROVED, FEISHU_MODULE_ACCESS, FEISHU_MODULE_LABELS;
20073
19734
  var init_scopes = __esm({
20074
19735
  "../connectors/src/feishu/scopes.ts"() {
20075
19736
  "use strict";
@@ -20472,6 +20133,453 @@ var init_scopes = __esm({
20472
20133
  FEISHU_SEND_AS_USER_SCOPE = "im:message.send_as_user";
20473
20134
  FEISHU_AUTH_SELECTION_VAR = "FEISHU_AUTH_SELECTION";
20474
20135
  AUTO_APPROVED = new Set(FEISHU_BASIC_SCOPES);
20136
+ FEISHU_MODULE_ACCESS = {
20137
+ im: { commands: ["im"], apiServices: ["im"], botScopePrefixes: ["im:"] },
20138
+ calendar: { commands: ["calendar"], apiServices: ["calendar"], botScopePrefixes: ["calendar:"] },
20139
+ docs: { commands: ["docs", "drive", "markdown", "mindnotes"], apiServices: ["docx", "doc", "docs", "drive", "mindnote", "mindnotes", "suite"], botScopePrefixes: ["docs:", "docx:", "space:"] },
20140
+ drive: { commands: ["drive", "markdown"], apiServices: ["drive"], botScopePrefixes: ["drive:"] },
20141
+ sheets: { commands: ["sheets"], apiServices: ["sheets"], botScopePrefixes: ["sheets:"] },
20142
+ base: { commands: ["base"], apiServices: ["bitable", "base"], botScopePrefixes: ["base:", "bitable:"] },
20143
+ wiki: { commands: ["wiki"], apiServices: ["wiki"], botScopePrefixes: ["wiki:"] },
20144
+ slides: { commands: ["slides"], apiServices: ["slides", "slides_ai"], botScopePrefixes: ["slides:"] },
20145
+ task: { commands: ["task"], apiServices: ["task"], botScopePrefixes: ["task:"] },
20146
+ approval: { commands: ["approval"], apiServices: ["approval"], botScopePrefixes: ["approval:"] },
20147
+ mail: { commands: ["mail"], apiServices: ["mail"], botScopePrefixes: ["mail:"] },
20148
+ minutes: { commands: ["minutes"], apiServices: ["minutes"], botScopePrefixes: ["minutes:"] },
20149
+ vc: { commands: ["vc", "note"], apiServices: ["vc"], botScopePrefixes: ["vc:"] },
20150
+ contact: { commands: ["contact"], apiServices: ["contact", "search"], botScopePrefixes: ["contact:"] },
20151
+ okr: { commands: ["okr"], apiServices: ["okr"], botScopePrefixes: ["okr:"] },
20152
+ attendance: { commands: ["attendance"], apiServices: ["attendance"], botScopePrefixes: ["attendance:"] },
20153
+ board: { commands: ["whiteboard"], apiServices: ["board"], botScopePrefixes: ["board:"] },
20154
+ spark: { commands: ["apps"], apiServices: ["spark"], botScopePrefixes: ["spark:"] }
20155
+ };
20156
+ FEISHU_MODULE_LABELS = {
20157
+ im: "\u5373\u65F6\u6D88\u606F",
20158
+ calendar: "\u65E5\u5386",
20159
+ docs: "\u4E91\u6587\u6863",
20160
+ drive: "\u4E91\u76D8",
20161
+ sheets: "\u7535\u5B50\u8868\u683C",
20162
+ base: "\u591A\u7EF4\u8868\u683C",
20163
+ wiki: "\u77E5\u8BC6\u5E93",
20164
+ slides: "\u5E7B\u706F\u7247",
20165
+ task: "\u4EFB\u52A1",
20166
+ approval: "\u5BA1\u6279",
20167
+ mail: "\u90AE\u4EF6",
20168
+ minutes: "\u5999\u8BB0",
20169
+ vc: "\u89C6\u9891\u4F1A\u8BAE",
20170
+ contact: "\u901A\u8BAF\u5F55",
20171
+ okr: "OKR",
20172
+ attendance: "\u8003\u52E4",
20173
+ board: "\u767D\u677F",
20174
+ spark: "Spark \u5E94\u7528"
20175
+ };
20176
+ }
20177
+ });
20178
+
20179
+ // ../connectors/src/feishu/index.ts
20180
+ async function fetchFeishuTenantAccessToken(appId, appSecret) {
20181
+ try {
20182
+ const resp = await fetch(FEISHU_TENANT_TOKEN_URL, {
20183
+ method: "POST",
20184
+ headers: { "Content-Type": "application/json; charset=utf-8" },
20185
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
20186
+ signal: AbortSignal.timeout(TENANT_TOKEN_TIMEOUT_MS)
20187
+ });
20188
+ const data = await resp.json();
20189
+ if (!resp.ok || data.code !== 0 || typeof data.tenant_access_token !== "string" || !data.tenant_access_token) {
20190
+ log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: HTTP ${resp.status} code=${String(data.code)} ${String(data.msg ?? "")}`.trim());
20191
+ return null;
20192
+ }
20193
+ return data.tenant_access_token;
20194
+ } catch (err) {
20195
+ log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: ${String(err?.message ?? err).slice(0, 200)}`);
20196
+ return null;
20197
+ }
20198
+ }
20199
+ function previousTenantToken(credsFile, appId) {
20200
+ if (!credsFile) return null;
20201
+ try {
20202
+ const prev = readSessionCredsFile(credsFile);
20203
+ if (prev["LARKSUITE_CLI_APP_ID"] !== appId) return null;
20204
+ return prev["LARKSUITE_CLI_TENANT_ACCESS_TOKEN"] || null;
20205
+ } catch {
20206
+ return null;
20207
+ }
20208
+ }
20209
+ function parseFeishuTokenResponse(data, nowMs) {
20210
+ const accessToken = String(data["access_token"] ?? "");
20211
+ const refreshToken = String(data["refresh_token"] ?? "");
20212
+ const expiresInSec = Number(data["expires_in"] ?? 7200);
20213
+ const refreshExpiresInSec = Number(
20214
+ data["refresh_token_expires_in"] ?? data["refresh_expires_in"] ?? 30 * 24 * 3600
20215
+ );
20216
+ const expiresAtMs = nowMs + expiresInSec * 1e3;
20217
+ return {
20218
+ credentials: {
20219
+ ...accessToken ? { user_access_token: accessToken } : {},
20220
+ ...refreshToken ? { refresh_token: refreshToken } : {},
20221
+ user_token_expires_at: String(expiresAtMs),
20222
+ refresh_token_expires_at: String(nowMs + refreshExpiresInSec * 1e3)
20223
+ },
20224
+ expiresAt: new Date(expiresAtMs)
20225
+ };
20226
+ }
20227
+ var import_node_child_process13, import_node_util4, import_node_path12, import_node_url, import_promises5, execFileAsync2, __dirname2, FEISHU_WELL_KNOWN_SKILLS_BASE, FEISHU_TOKEN_URL, FEISHU_TENANT_TOKEN_URL, FEISHU_USER_OPEN_ID_VAR, FEISHU_AUTHORIZED_BY_VAR, FEISHU_USER_AUTH_INVALID_VAR, FEISHU_REFRESH_REVOKED_CODES, TENANT_TOKEN_TIMEOUT_MS, FeishuConnector;
20228
+ var init_feishu = __esm({
20229
+ "../connectors/src/feishu/index.ts"() {
20230
+ "use strict";
20231
+ import_node_child_process13 = require("node:child_process");
20232
+ import_node_util4 = require("node:util");
20233
+ import_node_path12 = require("node:path");
20234
+ import_node_url = require("node:url");
20235
+ import_promises5 = require("node:fs/promises");
20236
+ init_base();
20237
+ init_logger();
20238
+ init_scopes();
20239
+ execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process13.execFile);
20240
+ __dirname2 = (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(__esm_import_meta_url));
20241
+ FEISHU_WELL_KNOWN_SKILLS_BASE = "https://open.feishu.cn/.well-known/skills";
20242
+ FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
20243
+ FEISHU_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
20244
+ FEISHU_USER_OPEN_ID_VAR = "FEISHU_USER_OPEN_ID";
20245
+ FEISHU_AUTHORIZED_BY_VAR = "FEISHU_AUTHORIZED_BY";
20246
+ FEISHU_USER_AUTH_INVALID_VAR = "FEISHU_USER_AUTH_INVALID";
20247
+ FEISHU_REFRESH_REVOKED_CODES = /* @__PURE__ */ new Set([20024, 20026, 20037, 20064, 20073, 20074]);
20248
+ TENANT_TOKEN_TIMEOUT_MS = 5e3;
20249
+ FeishuConnector = class extends CliConnector {
20250
+ config = {
20251
+ slug: "feishu",
20252
+ type: "cli",
20253
+ // schema 要**声明本连接器实际消费的全部凭据**,不只是「让用户手填的那几个」。
20254
+ // 历史上这里只写了 app_id,而 inject() 其实还要 app_secret / user_access_token
20255
+ // (后两者由控制台 OAuth 流写进变量表,用户不手填)——于是 secret 标记缺失,
20256
+ // 密钥黑名单只能在 serve.ts 另手写一份。现在黑名单从 sensitiveVars 推导,schema 必须诚实。
20257
+ schema: [
20258
+ { key: "app_id", description: "\u98DE\u4E66 App ID\uFF08\u7559\u7A7A\u5219\u81EA\u52A8\u521B\u5EFA\u65B0\u5E94\u7528\uFF09", secret: false, required: false },
20259
+ { key: "app_secret", description: "\u98DE\u4E66 App Secret\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\uFF0C\u975E\u624B\u586B\uFF09", secret: true, required: true },
20260
+ { key: "user_access_token", description: "\u98DE\u4E66\u7528\u6237 token\uFF08OAuth \u6388\u6743\u540E\u5199\u5165\uFF0C~2h \u8F6E\u6362\uFF09", secret: true, required: false },
20261
+ { key: "auth_selection", description: "\u6388\u6743\u65F6\u52FE\u9009\u7684\u529F\u80FD\u6A21\u5757\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\u7684\u660E\u6587 JSON\uFF0C\u6309\u5B83\u9650\u5236 agent \u53EF\u7528\u6A21\u5757\uFF09", secret: false, required: false }
20262
+ ]
20263
+ };
20264
+ commandName = "lark-cli";
20265
+ requiredCommands = ["lark-cli"];
20266
+ envPrefix = "FEISHU_";
20267
+ /** server 侧从 actor 变量表组装凭据的声明(见 provision.ts)。这些会随 job 下发到节点。 */
20268
+ credentialSources = [
20269
+ { key: "app_id", from: "FEISHU_APP_ID", required: true },
20270
+ { key: "app_secret", from: "FEISHU_APP_SECRET", required: true },
20271
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false },
20272
+ // 按 Agent 限制可用模块:inject 据此写放行表,wrapper 拦(个人选择优先,缺失回落组织连接的选择)。
20273
+ { key: "auth_selection", from: FEISHU_AUTH_SELECTION_VAR, required: false }
20274
+ ];
20275
+ /**
20276
+ * 续期材料——**只在 server 侧读,不下发节点**。refresh_token 能换出新的 user token,
20277
+ * 属于"长期材料",没有任何理由让它出现在 agent 所在的机器上。
20278
+ */
20279
+ refreshCredentialSources = [
20280
+ // App 凭据是**组织级**共享的(个人变量优先,缺失回落全局默认);用户 token 类只认个人变量。
20281
+ { key: "app_id", from: "FEISHU_APP_ID", orgDefault: true, required: true },
20282
+ { key: "app_secret", from: "FEISHU_APP_SECRET", orgDefault: true, required: true },
20283
+ { key: "refresh_token", from: "FEISHU_REFRESH_TOKEN", required: true },
20284
+ { key: "user_token_expires_at", from: "FEISHU_USER_TOKEN_EXPIRES_AT", required: false },
20285
+ { key: "refresh_token_expires_at", from: "FEISHU_REFRESH_TOKEN_EXPIRES_AT", required: false },
20286
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
20287
+ ];
20288
+ /** 过期判定只认这个键——必须与注入时消费的 token 同源(见接口注释里的历史坑)。 */
20289
+ expiresAtCredentialKey = "user_token_expires_at";
20290
+ /**
20291
+ * 下发前 user token 至少还要剩 40 分钟,不够就换新(Bug 0201)。
20292
+ *
20293
+ * 必须 > `SESSION_CREDENTIAL_RENEW_INTERVAL_MS`(30 分钟):会话中途续期每 30 分钟问一次 server,
20294
+ * 问的就是这条门槛。门槛若只有通用的 5 分钟,一张还剩 20 分钟的 token 会被原样发回,
20295
+ * 20 分钟后失效、要再等 10 分钟才到下一拍——最坏断档近 25 分钟(`99991677 token expire`)。
20296
+ * 取 40 = 30 分钟间隔 + 10 分钟调度余量:任一拍之后剩余 > 40,到下一拍仍 > 10。
20297
+ *
20298
+ * 不像 GitHub 那样取全寿命(每次都换):飞书每次刷新都会轮换 refresh_token,
20299
+ * 换得越勤,「飞书已轮换、我们还没落库」的窗口就越常被碰上,所以只取刚好够用的值。
20300
+ */
20301
+ minRemainingLifetimeMs = 40 * 6e4;
20302
+ /**
20303
+ * 只由服务端定时任务续期(Bug 0318):refresh_token 一次性、每刷必轮换,派单与会话中途再各刷一次就是
20304
+ * 多个写者并发打断续期链;懒续期又让闲置超过 refresh_token 寿命的员工悄悄失效。派单只读库里最新的 token。
20305
+ */
20306
+ scheduledRefreshOnly = true;
20307
+ /** refresh_token 自身 ~30d 过期,过了只能重新授权。 */
20308
+ refreshMaterialExpiresAtCredentialKey = "refresh_token_expires_at";
20309
+ /**
20310
+ * refresh_token 过期后该清掉的键——**只清这个员工的授权产物**。
20311
+ * app_id / app_secret 刻意不在其中:它们代表组织级 App 凭据,与其他员工的连接无关。
20312
+ */
20313
+ clearOnExpiredCredentialKeys = [
20314
+ "user_access_token",
20315
+ "refresh_token",
20316
+ "user_token_expires_at",
20317
+ "refresh_token_expires_at"
20318
+ ];
20319
+ /**
20320
+ * 绝不允许进入 agent 进程环境的原始变量。
20321
+ * 含 APP_ID 与几个过期时间戳:它们本身不算密钥,但同属飞书凭据材料,一并挡在 agent 之外,
20322
+ * 避免 agent 绕开 wrapper 自行拼凭据(沿用既有行为)。
20323
+ */
20324
+ sensitiveVars = [
20325
+ "FEISHU_APP_ID",
20326
+ "FEISHU_APP_SECRET",
20327
+ "FEISHU_USER_ACCESS_TOKEN",
20328
+ "FEISHU_REFRESH_TOKEN",
20329
+ "FEISHU_USER_TOKEN_EXPIRES_AT",
20330
+ "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
20331
+ FEISHU_USER_OPEN_ID_VAR,
20332
+ FEISHU_AUTHORIZED_BY_VAR,
20333
+ FEISHU_USER_AUTH_INVALID_VAR
20334
+ ];
20335
+ /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
20336
+ hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
20337
+ /** inject() 写进 sessionEnv 的全部键——通用 cleanup() 按它逐个删。 */
20338
+ injectedEnvKeys = [
20339
+ "LARK_BIN",
20340
+ "FEISHU_CREDS_FILE",
20341
+ "FEISHU_RUN_TMPDIR",
20342
+ "LARKSUITE_CLI_CONFIG_DIR"
20343
+ ];
20344
+ /**
20345
+ * 自检:拿 user_access_token 问飞书「我是谁」,回 `{ok, identity, data:{name, open_id}}`。
20346
+ *
20347
+ * **不能用 `lark-cli auth status`**:注入外部凭据后整个 auth 命名空间会被拒
20348
+ * ("credentials are provided externally and do not support interactive management"),
20349
+ * 自检会恒为失败。实测 2026-08-03 踩到——`auth status` 只在**读宿主机 profile** 时才有输出,
20350
+ * 恰恰是我们要隔离掉的那条路径。
20351
+ */
20352
+ verifyArgs = ["api", "GET", "/open-apis/authen/v1/user_info"];
20353
+ /** 厂商技能包源——平台自动拉取飞书官方技能包时用。 */
20354
+ wellKnownSkillsBase = FEISHU_WELL_KNOWN_SKILLS_BASE;
20355
+ larkBin;
20356
+ fetchTenantToken;
20357
+ /** `fetchTenantToken` 只给测试替换(不打真飞书);生产一律用 {@link fetchFeishuTenantAccessToken}。 */
20358
+ constructor(larkBin, opts = {}) {
20359
+ super(__dirname2);
20360
+ this.larkBin = larkBin ?? this.resolveLarkBin();
20361
+ this.fetchTenantToken = opts.fetchTenantToken ?? fetchFeishuTenantAccessToken;
20362
+ }
20363
+ /**
20364
+ * 解析 user_info 的 JSON。lark-cli 的 `ok:false` 也是**退出码 0** 的正常输出,
20365
+ * 所以必须在这里判 ok——只看子进程退出码会把「凭据失效」当成自检通过。
20366
+ */
20367
+ parseIdentity(stdout) {
20368
+ let parsed;
20369
+ try {
20370
+ parsed = JSON.parse(stdout);
20371
+ } catch {
20372
+ return null;
20373
+ }
20374
+ const obj2 = parsed;
20375
+ if (obj2.ok !== true) return null;
20376
+ const openId = typeof obj2.data?.open_id === "string" ? obj2.data.open_id : "";
20377
+ if (!openId) return null;
20378
+ const name = typeof obj2.data?.name === "string" ? obj2.data.name : "";
20379
+ return name ? `${name} (${openId})` : openId;
20380
+ }
20381
+ async connect(options = {}) {
20382
+ if (this.isSimulated()) {
20383
+ return { credentials: { app_id: options["app_id"] ?? "cli_simulated" } };
20384
+ }
20385
+ const app_id = options["app_id"] ?? await this.initNewApp();
20386
+ await this.runAuthLogin(app_id);
20387
+ return { credentials: { app_id } };
20388
+ }
20389
+ /**
20390
+ * Agent run 开始:把凭据作为 lark-cli「外部凭据」写进凭据文件(不进 agent env)。
20391
+ * credentials 接受:app_id, app_secret, user_access_token?(及其 FEISHU_* 别名)。
20392
+ * 企业 token 不随 job 下发,由这里用 app_id/app_secret 现换(换不到只是本轮没有 bot 身份,不抛)。
20393
+ *
20394
+ * 注入的 sessionEnv 键(全部非敏感):
20395
+ * FEISHU_CREDS_FILE — 凭据文件路径,wrapper source 它
20396
+ * FEISHU_RUN_TMPDIR — 本轮临时目录,cleanup 整个删掉
20397
+ * LARK_BIN — 真实 lark-cli 绝对路径(防 wrapper 递归调用自身)
20398
+ */
20399
+ async inject(credentials2, sessionEnv) {
20400
+ const appId = credentials2["app_id"] ?? credentials2["FEISHU_APP_ID"];
20401
+ const appSecret = credentials2["app_secret"] ?? credentials2["FEISHU_APP_SECRET"];
20402
+ if (!appId) throw new Error("feishu inject: missing app_id");
20403
+ if (!appSecret) throw new Error("feishu inject: missing app_secret");
20404
+ const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
20405
+ const modulePolicy = feishuModulePolicyEnv(
20406
+ feishuSelectedModules(credentials2["auth_selection"] ?? credentials2[FEISHU_AUTH_SELECTION_VAR])
20407
+ );
20408
+ const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
20409
+ const creds = await writeSessionCredsFile(this.config.slug, {
20410
+ LARKSUITE_CLI_APP_ID: appId,
20411
+ LARKSUITE_CLI_APP_SECRET: appSecret,
20412
+ ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
20413
+ ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {},
20414
+ ...modulePolicy ?? {}
20415
+ }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
20416
+ const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
20417
+ await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
20418
+ sessionEnv.set("LARK_BIN", await this.resolveAbsoluteLarkBin());
20419
+ sessionEnv.set("FEISHU_CREDS_FILE", creds.file);
20420
+ sessionEnv.set("FEISHU_RUN_TMPDIR", creds.dir);
20421
+ sessionEnv.set("LARKSUITE_CLI_CONFIG_DIR", configDir);
20422
+ }
20423
+ /**
20424
+ * 用 refresh_token 换一对新的 user token(飞书 v2 OAuth)。
20425
+ *
20426
+ * **纯换取,不落库**——持久化由调用方做(server 侧 `oauth.ts`)。这样平台知识
20427
+ * (端点、字段名、~2h/~30d 的寿命)全部收在连接器里,与 GitHub App 的续期共用
20428
+ * `_base/refresh.ts` 那套判定。
20429
+ *
20430
+ * 返回 null=没有可用的续期材料(未完成授权)。平台报错则抛,由上层收敛成 failed。
20431
+ */
20432
+ async refreshCredentials(credentials2) {
20433
+ const appId = credentials2["app_id"];
20434
+ const appSecret = credentials2["app_secret"];
20435
+ const refreshToken = credentials2["refresh_token"];
20436
+ if (!appId || !appSecret || !refreshToken) return null;
20437
+ const resp = await fetch(FEISHU_TOKEN_URL, {
20438
+ method: "POST",
20439
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20440
+ body: new URLSearchParams({
20441
+ grant_type: "refresh_token",
20442
+ refresh_token: refreshToken,
20443
+ client_id: appId,
20444
+ client_secret: appSecret
20445
+ }).toString()
20446
+ });
20447
+ const data = await resp.json();
20448
+ if (!resp.ok || data["error"]) {
20449
+ const code2 = Number(data["code"]);
20450
+ const msg = `feishu refresh failed: HTTP ${resp.status} code=${String(data["code"] ?? "")} ${String(data["error"] ?? "")}`.trim();
20451
+ if (data["error"] === "invalid_grant" || FEISHU_REFRESH_REVOKED_CODES.has(code2)) throw new CredentialRevokedError(msg);
20452
+ throw new Error(msg);
20453
+ }
20454
+ return parseFeishuTokenResponse(data, Date.now());
20455
+ }
20456
+ async initNewApp() {
20457
+ await new Promise((resolve11, reject) => {
20458
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["config", "init", "--new"], { stdio: "inherit" });
20459
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli config init exited ${code2}`)));
20460
+ proc.on("error", reject);
20461
+ });
20462
+ const { stdout } = await execFileAsync2(this.larkBin, ["config", "show", "--json"]);
20463
+ const cfg = JSON.parse(stdout);
20464
+ if (!cfg.appId) throw new Error("feishu connector: could not determine app_id after config init");
20465
+ return cfg.appId;
20466
+ }
20467
+ async runAuthLogin(app_id) {
20468
+ await new Promise((resolve11, reject) => {
20469
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["--profile", app_id, "auth", "login"], { stdio: "inherit" });
20470
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli auth login exited ${code2}`)));
20471
+ proc.on("error", reject);
20472
+ });
20473
+ }
20474
+ resolveLarkBin() {
20475
+ return process.env["LARK_CLI_BIN"] ?? "lark-cli";
20476
+ }
20477
+ async resolveAbsoluteLarkBin() {
20478
+ if (this.larkBin.includes("/")) return this.larkBin;
20479
+ return this.resolveRealBin(this.larkBin);
20480
+ }
20481
+ };
20482
+ }
20483
+ });
20484
+
20485
+ // ../connectors/src/feishu/user-auth.ts
20486
+ async function startFeishuDeviceAuthorization(input) {
20487
+ const basicAuth = Buffer.from(`${input.appId}:${input.appSecret}`).toString("base64");
20488
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_DEVICE_AUTHORIZATION_URL, {
20489
+ method: "POST",
20490
+ headers: {
20491
+ "Content-Type": "application/x-www-form-urlencoded",
20492
+ "Authorization": `Basic ${basicAuth}`
20493
+ },
20494
+ body: new URLSearchParams({ client_id: input.appId, scope: input.scopes.join(" ") }).toString()
20495
+ });
20496
+ const data = await resp.json();
20497
+ if (!resp.ok || data["error"]) {
20498
+ throw new Error(String(data["error_description"] ?? data["error"] ?? "device authorization failed"));
20499
+ }
20500
+ const verificationUrl = String(data["verification_uri_complete"] ?? data["verification_uri"] ?? "");
20501
+ if (!verificationUrl) throw new Error("no verification_uri in response");
20502
+ const expiresIn = Number(data["expires_in"]);
20503
+ const interval = Number(data["interval"]);
20504
+ return {
20505
+ verificationUrl,
20506
+ deviceCode: String(data["device_code"] ?? ""),
20507
+ ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {},
20508
+ ...Number.isFinite(interval) && interval > 0 ? { interval } : {}
20509
+ };
20510
+ }
20511
+ async function pollFeishuDeviceToken(input) {
20512
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_TOKEN_URL, {
20513
+ method: "POST",
20514
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20515
+ body: new URLSearchParams({
20516
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
20517
+ device_code: input.deviceCode,
20518
+ client_id: input.appId,
20519
+ client_secret: input.appSecret
20520
+ }).toString()
20521
+ });
20522
+ const data = await resp.json();
20523
+ const errStr = data["error"];
20524
+ if (errStr === "authorization_pending" || errStr === "slow_down") return { status: "pending" };
20525
+ if (!resp.ok || errStr) throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
20526
+ if (!data["access_token"]) throw new Error("no access_token in response");
20527
+ return { status: "ok", data };
20528
+ }
20529
+ async function fetchFeishuUserIdentity(accessToken, fetchImpl = fetch) {
20530
+ try {
20531
+ const resp = await fetchImpl(FEISHU_USER_INFO_URL, { headers: { authorization: `Bearer ${accessToken}` } });
20532
+ const ui = await resp.json();
20533
+ if (!resp.ok || ui.code !== 0) return {};
20534
+ const name = ui.data?.name || ui.data?.en_name;
20535
+ return {
20536
+ ...ui.data?.open_id ? { openId: ui.data.open_id } : {},
20537
+ ...name ? { name } : {}
20538
+ };
20539
+ } catch {
20540
+ return {};
20541
+ }
20542
+ }
20543
+ async function sendFeishuBotTextToUser(input) {
20544
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20545
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0Cbot \u79C1\u4FE1\u672A\u53D1\u51FA");
20546
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_SEND_MESSAGE_URL}?receive_id_type=open_id`, {
20547
+ method: "POST",
20548
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20549
+ body: JSON.stringify({ receive_id: input.openId, msg_type: "text", content: JSON.stringify({ text: input.text }) })
20550
+ });
20551
+ const body2 = await resp.json();
20552
+ if (body2.code !== 0) throw new Error(`feishu bot \u79C1\u4FE1\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20553
+ }
20554
+ async function lookupFeishuOpenIdByEmail(input) {
20555
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20556
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0C\u672A\u80FD\u6309\u90AE\u7BB1\u67E5\u98DE\u4E66\u7528\u6237");
20557
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_BATCH_GET_ID_URL}?user_id_type=open_id`, {
20558
+ method: "POST",
20559
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20560
+ body: JSON.stringify({ emails: [input.email], include_resigned: false })
20561
+ });
20562
+ const body2 = await resp.json();
20563
+ if (body2.code !== 0) throw new Error(`feishu \u6309\u90AE\u7BB1\u67E5\u7528\u6237\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20564
+ const hit = body2.data?.user_list?.find((u) => u.email?.toLowerCase() === input.email.toLowerCase() && u.user_id);
20565
+ return hit?.user_id ?? null;
20566
+ }
20567
+ function feishuUserAuthInvalidText(input) {
20568
+ const head = `\u300C${input.agentName}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF08\u6388\u6743\u88AB\u64A4\u9500\u6216\u957F\u65F6\u95F4\u65E0\u6CD5\u7EED\u671F\uFF09\uFF0C\u4EE5\u4F60\u8EAB\u4EFD\u8C03\u7528\u98DE\u4E66\u7684\u529F\u80FD\u6682\u65F6\u7528\u4E0D\u4E86\uFF0Cbot \u8EAB\u4EFD\u4E0D\u53D7\u5F71\u54CD\u3002`;
20569
+ return input.link ? `${head}
20570
+ \u70B9\u8FD9\u4E2A\u94FE\u63A5\u91CD\u65B0\u6388\u6743\uFF08\u9700\u7528\u6536\u5230\u8FD9\u6761\u79C1\u4FE1\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
20571
+ ${input.link}` : `${head}
20572
+ \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20573
+ }
20574
+ var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20575
+ var init_user_auth = __esm({
20576
+ "../connectors/src/feishu/user-auth.ts"() {
20577
+ "use strict";
20578
+ init_feishu();
20579
+ FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
20580
+ FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20581
+ FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20582
+ FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20475
20583
  }
20476
20584
  });
20477
20585
 
@@ -211892,6 +212000,54 @@ var init_registry_audit_sink = __esm({
211892
212000
  }
211893
212001
  });
211894
212002
 
212003
+ // ../server/src/feishu-module-access.ts
212004
+ async function feishuModuleAccess(store, actorId, fetchScopes = fetchFeishuAppScopes) {
212005
+ const read3 = (key2) => store.revealResolvedVariable(key2, actorId).catch(() => null);
212006
+ const appId = await read3("FEISHU_APP_ID");
212007
+ const appSecret = await read3("FEISHU_APP_SECRET");
212008
+ if (!appId || !appSecret) return { configured: false };
212009
+ const rawSelection = await read3(FEISHU_AUTH_SELECTION_VAR);
212010
+ const modules = feishuSelectedModules(rawSelection);
212011
+ let parsed = {};
212012
+ try {
212013
+ parsed = rawSelection ? JSON.parse(rawSelection) : {};
212014
+ } catch {
212015
+ parsed = {};
212016
+ }
212017
+ const grantedScopes = Array.isArray(parsed.grantedScopes) ? parsed.grantedScopes.filter((s2) => typeof s2 === "string") : void 0;
212018
+ const hasUserAuth = Boolean(await read3("FEISHU_USER_ACCESS_TOKEN") || await read3("FEISHU_REFRESH_TOKEN"));
212019
+ const userAuthInvalid = Boolean(await read3(FEISHU_USER_AUTH_INVALID_VAR));
212020
+ let appScopes = null;
212021
+ let botCheckError;
212022
+ if (modules.length > 0) {
212023
+ try {
212024
+ appScopes = await fetchScopes(appId, appSecret);
212025
+ } catch (e) {
212026
+ botCheckError = String(e?.message ?? e).slice(0, 200);
212027
+ }
212028
+ }
212029
+ return {
212030
+ configured: true,
212031
+ restricted: modules.length > 0,
212032
+ modules: feishuModuleAccessReport({ modules, grantedScopes, hasUserAuth, userAuthInvalid, appScopes }),
212033
+ hasUserAuth,
212034
+ userAuthInvalid,
212035
+ sendAsUser: feishuSendAsUserState({
212036
+ sendAsUser: parsed.sendAsUser === true,
212037
+ sendAsUserRequested: parsed.sendAsUserRequested === true,
212038
+ grantedScopes
212039
+ }),
212040
+ appConsoleUrl: feishuAppPermissionConsoleUrl(appId),
212041
+ ...botCheckError ? { botCheckError } : {}
212042
+ };
212043
+ }
212044
+ var init_feishu_module_access = __esm({
212045
+ "../server/src/feishu-module-access.ts"() {
212046
+ "use strict";
212047
+ init_src3();
212048
+ }
212049
+ });
212050
+
211895
212051
  // ../server/src/github-app-inventory.ts
211896
212052
  async function collectOasisGithubApps(svc) {
211897
212053
  const rows = (await svc.listVariables()).filter((r) => r.scope === "global" || r.scope === "personal");
@@ -218085,6 +218241,17 @@ ${composed}`;
218085
218241
  else html(false, result.error);
218086
218242
  return;
218087
218243
  }
218244
+ if (url.pathname === "/api/connectors/feishu/app/permissions" && req.method === "POST") {
218245
+ try {
218246
+ const { actorId } = JSON.parse(rawBody || "{}");
218247
+ const svc = await connectorActorsService();
218248
+ if (!svc) throw new Error("actors service unavailable");
218249
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(await feishuModuleAccess(svc, actorId)));
218250
+ } catch (e) {
218251
+ res.writeHead(500).end(JSON.stringify({ error: String(e) }));
218252
+ }
218253
+ return;
218254
+ }
218088
218255
  if (url.pathname === "/api/connectors/feishu/login/start" && req.method === "POST") {
218089
218256
  const startBody = JSON.parse(rawBody);
218090
218257
  const { appId, actorId } = startBody;
@@ -218578,6 +218745,7 @@ var init_server3 = __esm({
218578
218745
  init_assembly();
218579
218746
  init_child_live_turn();
218580
218747
  init_registry_audit_sink();
218748
+ init_feishu_module_access();
218581
218749
  init_src3();
218582
218750
  init_github_app_inventory();
218583
218751
  init_github_app_timing();
@@ -300776,7 +300944,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
300776
300944
  }
300777
300945
  }
300778
300946
  }
300779
- var CLI_VERSION = "2.2.111" ? "2.2.111" : "dev";
300947
+ var CLI_VERSION = "2.2.112" ? "2.2.112" : "dev";
300780
300948
  function apiHeaders(token2, company) {
300781
300949
  const headers = {
300782
300950
  authorization: `Bearer ${token2}`,
@@ -304947,7 +305115,7 @@ function shimScript() {
304947
305115
  }
304948
305116
 
304949
305117
  // src/index.ts
304950
- var PKG_VERSION = true ? "2.2.111" : "dev";
305118
+ var PKG_VERSION = true ? "2.2.112" : "dev";
304951
305119
  var LOCAL_BIN = localBin();
304952
305120
  var NPM_PREFIX = npmPrefix();
304953
305121
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.111",
3
+ "version": "2.2.112",
4
4
  "description": "Oasis node daemon + CLI — background daemon, auto-start, full server CLI",
5
5
  "bin": {
6
6
  "oasis": "./dist/index.js"
@@ -26,6 +26,6 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "oasisRelease": {
29
- "sourceHead": "05ed7c1a9a41f19b4919fc9398095d0b3ceefaf7"
29
+ "sourceHead": "278bdc2558b23bd381c4c3007a8d40330244edc1"
30
30
  }
31
31
  }