oasis_test_v2 2.2.112 → 2.2.114

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.
@@ -11,13 +11,15 @@
11
11
  # LARK_BIN — 真实 lark-cli 绝对路径(避免递归调用自身)
12
12
  # FEISHU_CREDS_FILE — 凭据文件路径(内含 export LARKSUITE_CLI_* 行)
13
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),这里不认识任何模块,只查表。
14
+ # 按 Agent 限制可用模块(产品反馈「飞书连接器·按 Agent 的权限模型」):只放行所选模块的
15
+ # `lark-cli <domain> …` 与 `lark-cli api <METHOD> /open-apis/<service>/…`,机器人与用户身份一样拦
16
+ # (bot 的企业 token 能调应用开通的全集,飞书侧分不开)。放行表由 inject() 按 FEISHU_AUTH_SELECTION
17
+ # 算好写入凭据文件(feishuModulePolicyEnv),这里不认识任何模块,只按规则表(OASIS_FEISHU_RULES)查。
18
+ # 共用的 drive 命令/接口按子命令、路径细分。**没有放行表、没有有效选择都按一个模块都没开放拦**,不会退成不限制。
18
19
  # 被拦时照 lark-cli 的错误形状往 stderr 打一段 JSON、退出码 3(与 lark-cli 自己的鉴权失败同码)。
19
20
 
20
21
  set -euo pipefail
22
+ shopt -s extglob # 放行规则用 +([!/]) / ?(/*) 表示「一段」「零到多段」
21
23
 
22
24
  OASIS_BIN_VAR="LARK_BIN"
23
25
  OASIS_CMD_NAME="lark-cli"
@@ -28,7 +30,7 @@ oasis_resolve_bin
28
30
  oasis_check_available
29
31
 
30
32
  # 放行表只认凭据文件里的:先清掉 agent 环境里可能带进来的同名变量。
31
- unset OASIS_FEISHU_ALLOW OASIS_FEISHU_NEED OASIS_FEISHU_MODULES
33
+ unset OASIS_FEISHU_POLICY OASIS_FEISHU_ALLOW OASIS_FEISHU_RULES OASIS_FEISHU_LABELS OASIS_FEISHU_MODULES
32
34
 
33
35
  # Load feishu credentials into THIS process only (not visible to the agent).
34
36
  if [ -n "${FEISHU_CREDS_FILE:-}" ] && [ -f "${FEISHU_CREDS_FILE}" ]; then
@@ -41,34 +43,82 @@ FEISHU_NEUTRAL_COMMANDS=" help schema skills whoami doctor completion auth confi
41
43
  # 任何模块都放行的接口 service;与 scopes.ts FEISHU_NEUTRAL_API_SERVICES 同一份。
42
44
  FEISHU_NEUTRAL_API=" authen auth "
43
45
 
46
+ feishu_reply_blocked() {
47
+ printf '{\n "ok": false,\n "error": {\n "type": "oasis_module_blocked",\n "message": "%s",\n "hint": "%s"\n }\n}\n' "$1" "$2" >&2
48
+ exit 3
49
+ }
50
+
44
51
  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 管理员。"
52
+ # $1 = 规则要求的模块(逗号分隔,空 = 不属于任何模块);$2 = 给人看的命令写法(已过滤成安全字符)。
53
+ local mods="$1" shown="$2" need="" m entry label
54
+ local reconnect="请让配置人在 Oasis 里这个 agent 的飞书连接器详情点「重新连接」"
55
+ case "${OASIS_FEISHU_POLICY:-}" in
56
+ ok) ;;
57
+ none)
58
+ feishu_reply_blocked "本 agent 的飞书连接器还没有保存过可用模块的选择,Oasis 按「一个模块都没开放」处理,${shown} 已被拦下(机器人与用户身份都不可用)。" \
59
+ "${reconnect},勾选需要的模块后完成授权。" ;;
60
+ invalid)
61
+ feishu_reply_blocked "本 agent 的飞书连接器保存的模块选择无法识别,Oasis 按「一个模块都没开放」处理,${shown} 已被拦下(机器人与用户身份都不可用)。" \
62
+ "${reconnect},重新勾选模块后完成授权。" ;;
63
+ *)
64
+ feishu_reply_blocked "本次运行没有拿到飞书模块放行表,Oasis 不放行 ${shown}(机器人与用户身份都不可用)。" \
65
+ "请重新发起任务;仍出现请联系 Oasis 管理员检查飞书连接器。" ;;
66
+ esac
67
+ if [ -n "$mods" ]; then
68
+ for m in ${mods//,/ }; do
69
+ label="$m"
70
+ for entry in ${OASIS_FEISHU_LABELS:-}; do
71
+ if [ "${entry%%=*}" = "$m" ]; then label="${entry#*=}"; break; fi
72
+ done
73
+ need="${need:+${need}」或「}${label}"
74
+ done
75
+ feishu_reply_blocked "本 agent 的飞书连接器没有开放「${need}」模块,${shown} 已被 Oasis 拦下(机器人与用户身份都不可用)。当前开放的模块:${OASIS_FEISHU_MODULES:-无}。" \
76
+ "如需使用,${reconnect},勾选「${need}」后完成授权。"
57
77
  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
78
+ feishu_reply_blocked "${shown} 不属于任何可勾选的飞书功能模块;本 agent 的飞书调用已按模块限制,Oasis 不放行它。当前开放的模块:${OASIS_FEISHU_MODULES:-无}。" \
79
+ "请改用已开放模块对应的 lark-cli 命令;共用的 drive 子命令须紧跟在 drive 之后写。确需此能力请联系 Oasis 管理员。"
60
80
  }
61
81
 
62
82
  # 只把「看起来像命令名 / service 名」的字符串回显出来,其余一律写成占位——它要进 JSON。
63
83
  feishu_safe() {
64
84
  case "$1" in
65
- ''|*[!A-Za-z0-9_.-]*) printf '%s' "该命令" ;;
85
+ ''|*[!A-Za-z0-9_.+-]*) printf '%s' "该命令" ;;
86
+ *) printf '%s' "$1" ;;
87
+ esac
88
+ }
89
+
90
+ # 按规则表判 $1(cmd/<domain>/… 或 api/<METHOD>/<service>/…):命中且所需模块里有一个已放行 → 放行;
91
+ # 命中但都没放行 → 拦并说缺哪个;一条都没命中 → 拦(不属于任何模块)。
92
+ feishu_check_key() {
93
+ local key="$1" shown="$2" entry pat mods m
94
+ set -f
95
+ for entry in ${OASIS_FEISHU_RULES:-}; do
96
+ pat="${entry%=*}"; mods="${entry##*=}"
97
+ # shellcheck disable=SC2254
98
+ case "$key" in
99
+ $pat)
100
+ set +f
101
+ if [ "${OASIS_FEISHU_POLICY:-}" = "ok" ]; then
102
+ for m in ${mods//,/ }; do
103
+ case "${OASIS_FEISHU_ALLOW:-}" in *" $m "*) return 0 ;; esac
104
+ done
105
+ fi
106
+ feishu_blocked "$mods" "$shown" ;;
107
+ esac
108
+ done
109
+ set +f
110
+ feishu_blocked "" "$shown"
111
+ }
112
+
113
+ # 命令行里的一个词能不能原样进规则 key:只允许命令名字符,其余写成 ?(不会命中任何规则)。
114
+ feishu_seg() {
115
+ case "$1" in
116
+ ''|*[!A-Za-z0-9_.+-]*) printf '?' ;;
66
117
  *) printf '%s' "$1" ;;
67
118
  esac
68
119
  }
69
120
 
70
121
  feishu_enforce_modules() {
71
- [ -n "${OASIS_FEISHU_ALLOW:-}" ] || return 0 # 没保存过模块选择:不限制
72
122
  local args=("$@") i=0 n=$# cmd=""
73
123
  # 跳过命令前的全局参数(--profile X、-h/--help、-v/--version)。
74
124
  while [ "$i" -lt "$n" ]; do
@@ -82,8 +132,8 @@ feishu_enforce_modules() {
82
132
  case "$FEISHU_NEUTRAL_COMMANDS" in *" $cmd "*) return 0 ;; esac
83
133
 
84
134
  if [ "$cmd" = "api" ]; then
85
- # lark-cli api <METHOD> <path> [flags]:取 api 之后第二个位置参数为 path(跳过带值的参数)。
86
- local pos=0 path="" a
135
+ # lark-cli api <METHOD> <path> [flags]:api 之后第一、二个位置参数为 method、path(跳过带值的参数)。
136
+ local pos=0 method="" path="" a
87
137
  i=$((i + 1))
88
138
  while [ "$i" -lt "$n" ]; do
89
139
  a="${args[$i]}"
@@ -92,27 +142,49 @@ feishu_enforce_modules() {
92
142
  -*) i=$((i + 1)); continue ;;
93
143
  esac
94
144
  pos=$((pos + 1))
145
+ if [ "$pos" -eq 1 ]; then method="$a"; fi
95
146
  if [ "$pos" -eq 2 ]; then path="$a"; break; fi
96
147
  i=$((i + 1))
97
148
  done
98
149
  [ -n "$path" ] || return 0 # 没给 path:lark-cli 自己报用法错,不会发请求
99
- # 归一成 <service>/…:lark-cli 接受完整 URL、/open-apis/ 前缀可省、前导斜杠可省。
150
+ # 归一成 <service>/…:lark-cli 接受完整 URL、/open-apis/ 前缀可省、前导斜杠可省;连续斜杠并成一个。
100
151
  local p="$path"
101
152
  p="${p#http://}"; p="${p#https://}"
102
153
  case "$path" in http://*|https://*) p="${p#*/}" ;; esac
103
154
  while [ "${p#/}" != "$p" ]; do p="${p#/}"; done
104
155
  p="${p#open-apis/}"
105
156
  while [ "${p#/}" != "$p" ]; do p="${p#/}"; done
106
- # 路径里带 .. 或百分号编码的,不去猜它最后落到哪个 service——直接拒。
107
- case "$p" in *..*|*%*|*\\*) feishu_blocked "api:?" "lark-cli api(路径含 .. 或编码字符)" ;; esac
108
- local svc="${p%%[/?#]*}"
157
+ p="${p%%[?#]*}"
158
+ while [ "${p//\/\//\/}" != "$p" ]; do p="${p//\/\//\/}"; done
159
+ # 路径里带 ..、. 段或百分号编码的,不去猜它最后落到哪个接口——直接拒。
160
+ case "/$p/" in *..*|*%*|*\\*|*/./*) feishu_blocked "" "lark-cli api(路径含 .. 或编码字符)" ;; esac
161
+ local svc="${p%%/*}"
109
162
  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")/…"
163
+ case "$p" in *[!A-Za-z0-9_./:-]*) feishu_blocked "" "lark-cli api(路径含非法字符)" ;; esac
164
+ # 方法名转大写(macOS 自带 bash 3.2 没有 ${x^^})。
165
+ method="$(feishu_seg "$method" | tr '[:lower:]' '[:upper:]')"
166
+ feishu_check_key "api/$method/$p" "lark-cli api …/open-apis/$(feishu_safe "$svc")/…"
167
+ return 0
112
168
  fi
113
169
 
114
- case "$OASIS_FEISHU_ALLOW" in *" cmd:$cmd "*) return 0 ;; esac
115
- feishu_blocked "cmd:$cmd" "lark-cli $(feishu_safe "$cmd")"
170
+ # lark-cli <domain> [子命令] [方法]:只取紧跟在 domain 后面的词(drive 这类共用 domain 按子命令分模块);
171
+ # domain 后面先出现参数的,key 里记成「-」,共用 domain 的细分规则一条都不会命中。
172
+ local key="cmd/$(feishu_seg "$cmd")" depth=0 j=$((i + 1)) only_help=1 w
173
+ while [ "$j" -lt "$n" ]; do
174
+ case "${args[$j]}" in -h|--help) ;; *) only_help=0 ;; esac
175
+ j=$((j + 1))
176
+ done
177
+ [ "$only_help" -eq 1 ] && [ $((i + 1)) -lt "$n" ] && return 0 # lark-cli <domain> --help
178
+ j=$((i + 1))
179
+ while [ "$j" -lt "$n" ] && [ "$depth" -lt 2 ]; do
180
+ w="${args[$j]}"
181
+ case "$w" in -*) key="$key/-"; break ;; esac
182
+ key="$key/$(feishu_seg "$w")"
183
+ depth=$((depth + 1)); j=$((j + 1))
184
+ done
185
+ local shown="lark-cli $(feishu_safe "$cmd")"
186
+ [ "$depth" -ge 1 ] && shown="$shown $(feishu_safe "${args[$((i + 1))]}")"
187
+ feishu_check_key "$key" "$shown"
116
188
  }
117
189
 
118
190
  feishu_enforce_modules "$@"
package/dist/index.js CHANGED
@@ -19673,49 +19673,69 @@ function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
19673
19673
  ...granted ? { grantedScopes: granted } : {}
19674
19674
  };
19675
19675
  }
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
- };
19676
+ function feishuModuleRules() {
19677
+ const own3 = Object.entries(FEISHU_MODULE_ACCESS).flatMap(([m2, a]) => [
19678
+ ...a.commands.map((c) => ({ pattern: `cmd/${c}/**`, modules: [m2] })),
19679
+ ...a.apiServices.map((s2) => ({ pattern: `api/*/${s2}/**`, modules: [m2] }))
19680
+ ]);
19681
+ return [...own3, ...FEISHU_SHARED_RULES];
19682
+ }
19683
+ function ruleToExtglob(pattern) {
19684
+ const tail = pattern.endsWith("/**");
19685
+ const body2 = (tail ? pattern.slice(0, -3) : pattern).split("/").map((seg) => seg === "*" ? "+([!/])" : seg).join("/");
19686
+ return tail ? `${body2}?(/*)` : body2;
19696
19687
  }
19697
- function feishuSelectedModules(raw) {
19698
- if (!raw) return [];
19688
+ function feishuParseSelection(raw) {
19689
+ if (!raw) return { state: "none", modules: [] };
19690
+ let parsed;
19699
19691
  try {
19700
- const parsed = JSON.parse(raw);
19701
- return Array.isArray(parsed.modules) ? parsed.modules.filter((m2) => typeof m2 === "string" && !!FEISHU_MODULE_ACCESS[m2]) : [];
19692
+ parsed = JSON.parse(raw);
19702
19693
  } catch {
19703
- return [];
19694
+ return { state: "invalid", modules: [] };
19704
19695
  }
19696
+ const modules = Array.isArray(parsed?.modules) ? [...new Set(parsed.modules.filter((m2) => typeof m2 === "string" && !!FEISHU_MODULE_ACCESS[m2]))] : [];
19697
+ return modules.length > 0 ? { state: "ok", modules } : { state: "invalid", modules: [] };
19698
+ }
19699
+ function feishuModulePolicyEnv(selection) {
19700
+ const picked = selection.state === "ok" ? selection.modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]) : [];
19701
+ const label2 = (m2) => (FEISHU_MODULE_LABELS[m2] ?? m2).replace(/\s+/g, "");
19702
+ return {
19703
+ OASIS_FEISHU_POLICY: picked.length > 0 ? "ok" : selection.state === "ok" ? "invalid" : selection.state,
19704
+ OASIS_FEISHU_ALLOW: ` ${picked.join(" ")} `,
19705
+ OASIS_FEISHU_RULES: feishuModuleRules().map((r) => `${ruleToExtglob(r.pattern)}=${r.modules.join(",")}`).join(" "),
19706
+ OASIS_FEISHU_LABELS: Object.keys(FEISHU_MODULE_ACCESS).map((m2) => `${m2}=${label2(m2)}`).join(" "),
19707
+ OASIS_FEISHU_MODULES: picked.map((m2) => FEISHU_MODULE_LABELS[m2] ?? m2).join("\u3001")
19708
+ };
19705
19709
  }
19706
19710
  function feishuModuleAccessReport(input) {
19707
19711
  const granted = input.grantedScopes ? new Set(input.grantedScopes) : null;
19708
- const tenant = (input.appScopes ?? []).filter((s2) => s2.scope_type === "tenant" && s2.scope_name);
19712
+ const tenant = /* @__PURE__ */ new Map();
19713
+ for (const s2 of input.appScopes ?? []) {
19714
+ if (s2.scope_type !== "tenant" || !s2.scope_name) continue;
19715
+ const ok3 = s2.grant_status === void 0 || s2.grant_status === 1;
19716
+ tenant.set(s2.scope_name, (tenant.get(s2.scope_name) ?? false) || ok3);
19717
+ }
19709
19718
  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";
19719
+ const gaps = [];
19720
+ let anyGranted = false;
19721
+ let anyPending = false;
19722
+ for (const cap of FEISHU_MODULE_ACCESS[m2].botCapabilities) {
19723
+ if (cap.anyOf.some((s2) => tenant.get(s2) === true)) {
19724
+ anyGranted = true;
19725
+ continue;
19726
+ }
19727
+ const pending = cap.anyOf.some((s2) => tenant.has(s2));
19728
+ if (pending) anyPending = true;
19729
+ gaps.push({ label: cap.label, state: pending ? "pending" : "missing", scopes: [...cap.anyOf] });
19730
+ }
19731
+ const bot = input.appScopes === null ? "unknown" : gaps.length === 0 ? "granted" : anyGranted ? "partial" : anyPending ? "pending" : "missing";
19713
19732
  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";
19733
+ const user = input.userAuthInvalid ? "invalid" : !input.hasUserAuth ? "none" : !granted ? "unknown" : missingUserScopes.length > 0 ? "partial" : "granted";
19715
19734
  return {
19716
19735
  id: m2,
19717
19736
  label: FEISHU_MODULE_LABELS[m2] ?? m2,
19718
19737
  bot,
19738
+ missingBotCapabilities: bot === "unknown" ? [] : gaps,
19719
19739
  user,
19720
19740
  missingUserScopes: user === "partial" ? missingUserScopes : []
19721
19741
  };
@@ -19730,7 +19750,7 @@ function feishuSendAsUserState(sel) {
19730
19750
  function feishuAppPermissionConsoleUrl(appId) {
19731
19751
  return `https://open.feishu.cn/app/${encodeURIComponent(appId)}/auth`;
19732
19752
  }
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;
19753
+ 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, DRIVE_DOCS_ONLY_SHORTCUTS, DRIVE_DOCS_ONLY_RESOURCES, DRIVE_SHARED_SHORTCUTS, FEISHU_SHARED_RULES, FEISHU_MODULE_LABELS;
19734
19754
  var init_scopes = __esm({
19735
19755
  "../connectors/src/feishu/scopes.ts"() {
19736
19756
  "use strict";
@@ -20134,25 +20154,154 @@ var init_scopes = __esm({
20134
20154
  FEISHU_AUTH_SELECTION_VAR = "FEISHU_AUTH_SELECTION";
20135
20155
  AUTO_APPROVED = new Set(FEISHU_BASIC_SCOPES);
20136
20156
  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
- };
20157
+ im: { commands: ["im"], apiServices: ["im"], botCapabilities: [
20158
+ { label: "\u53D1\u6D88\u606F", anyOf: ["im:message:send_as_bot", "im:message", "im:message:send"] },
20159
+ { label: "\u8BFB\u6D88\u606F", anyOf: ["im:message:readonly", "im:message", "im:message.group_msg", "im:message.p2p_msg:readonly", "im:message.group_at_msg:readonly"] },
20160
+ { label: "\u7FA4\u4FE1\u606F", anyOf: ["im:chat:readonly", "im:chat:read", "im:chat"] }
20161
+ ] },
20162
+ calendar: { commands: ["calendar"], apiServices: ["calendar"], botCapabilities: [
20163
+ { label: "\u8BFB\u65E5\u7A0B", anyOf: ["calendar:calendar:readonly", "calendar:calendar", "calendar:calendar:read", "calendar:calendar.event:read"] },
20164
+ { label: "\u5199\u65E5\u7A0B", anyOf: ["calendar:calendar", "calendar:calendar.event:create", "calendar:calendar.event:update"] }
20165
+ ] },
20166
+ docs: { commands: ["docs", "mindnotes"], apiServices: ["docx", "doc", "docs", "mindnote", "mindnotes", "suite"], botCapabilities: [
20167
+ { label: "\u8BFB\u6587\u6863", anyOf: ["docx:document:readonly", "docx:document", "docs:document.content:read", "docs:doc:readonly", "docs:doc"] },
20168
+ { label: "\u5199\u6587\u6863", anyOf: ["docx:document", "docx:document:create", "docx:document:write_only", "docs:doc"] }
20169
+ ] },
20170
+ drive: { commands: ["markdown"], apiServices: [], botCapabilities: [
20171
+ { label: "\u4E0B\u8F7D\u6587\u4EF6", anyOf: ["drive:drive:readonly", "drive:drive", "drive:file:download", "drive:file:readonly", "drive:file"] },
20172
+ { label: "\u4E0A\u4F20\u6587\u4EF6", anyOf: ["drive:drive", "drive:file:upload", "drive:file"] }
20173
+ ] },
20174
+ sheets: { commands: ["sheets"], apiServices: ["sheets"], botCapabilities: [
20175
+ { label: "\u8BFB\u8868\u683C", anyOf: ["sheets:spreadsheet:readonly", "sheets:spreadsheet", "sheets:spreadsheet:read"] },
20176
+ { label: "\u5199\u8868\u683C", anyOf: ["sheets:spreadsheet", "sheets:spreadsheet:write_only", "sheets:spreadsheet:create"] }
20177
+ ] },
20178
+ base: { commands: ["base"], apiServices: ["bitable", "base"], botCapabilities: [
20179
+ { label: "\u8BFB\u8BB0\u5F55", anyOf: ["bitable:app:readonly", "bitable:app", "base:record:read", "base:app:read"] },
20180
+ { label: "\u5199\u8BB0\u5F55", anyOf: ["bitable:app", "base:record:create", "base:record:update"] }
20181
+ ] },
20182
+ wiki: { commands: ["wiki"], apiServices: ["wiki"], botCapabilities: [
20183
+ { label: "\u8BFB\u77E5\u8BC6\u5E93", anyOf: ["wiki:wiki:readonly", "wiki:wiki", "wiki:node:read", "wiki:space:read"] },
20184
+ { label: "\u5199\u77E5\u8BC6\u5E93", anyOf: ["wiki:wiki", "wiki:node:create", "wiki:space:write_only"] }
20185
+ ] },
20186
+ slides: { commands: ["slides"], apiServices: ["slides", "slides_ai"], botCapabilities: [
20187
+ { label: "\u8BFB\u5E7B\u706F\u7247", anyOf: ["slides:presentation:read", "slides:presentation:readonly", "slides:presentation"] },
20188
+ { label: "\u5199\u5E7B\u706F\u7247", anyOf: ["slides:presentation:update", "slides:presentation:write_only", "slides:presentation:create", "slides:presentation"] }
20189
+ ] },
20190
+ task: { commands: ["task"], apiServices: ["task"], botCapabilities: [
20191
+ { label: "\u8BFB\u4EFB\u52A1", anyOf: ["task:task:read", "task:task:readonly", "task:task"] },
20192
+ { label: "\u5199\u4EFB\u52A1", anyOf: ["task:task:write", "task:task"] }
20193
+ ] },
20194
+ approval: { commands: ["approval"], apiServices: ["approval"], botCapabilities: [
20195
+ { label: "\u8BFB\u5BA1\u6279", anyOf: ["approval:instance:read", "approval:approval:readonly", "approval:approval"] },
20196
+ { label: "\u5904\u7406\u5BA1\u6279", anyOf: ["approval:instance:write", "approval:task:write", "approval:approval"] }
20197
+ ] },
20198
+ mail: { commands: ["mail"], apiServices: ["mail"], botCapabilities: [
20199
+ { label: "\u8BFB\u90AE\u4EF6", anyOf: ["mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly", "mail:user_mailbox"] }
20200
+ ] },
20201
+ minutes: { commands: ["minutes"], apiServices: ["minutes"], botCapabilities: [
20202
+ { label: "\u8BFB\u5999\u8BB0", anyOf: ["minutes:minutes:readonly", "minutes:minutes", "minutes:minutes.search:read"] }
20203
+ ] },
20204
+ vc: { commands: ["vc", "note"], apiServices: ["vc"], botCapabilities: [
20205
+ { label: "\u8BFB\u4F1A\u8BAE", anyOf: ["vc:meeting:readonly", "vc:meeting", "vc:meeting.meetingevent:read", "vc:record:readonly"] }
20206
+ ] },
20207
+ contact: { commands: ["contact"], apiServices: ["contact"], botCapabilities: [
20208
+ { label: "\u8BFB\u901A\u8BAF\u5F55", anyOf: ["contact:user.base:readonly", "contact:contact:readonly_as_app", "contact:contact:readonly", "contact:contact", "contact:user.basic_profile:readonly"] }
20209
+ ] },
20210
+ okr: { commands: ["okr"], apiServices: ["okr"], botCapabilities: [
20211
+ { label: "\u8BFB OKR", anyOf: ["okr:okr:readonly", "okr:okr", "okr:okr:read"] }
20212
+ ] },
20213
+ attendance: { commands: ["attendance"], apiServices: ["attendance"], botCapabilities: [
20214
+ { label: "\u8BFB\u8003\u52E4", anyOf: ["attendance:task:readonly", "attendance:record:read", "attendance:task"] }
20215
+ ] },
20216
+ board: { commands: ["whiteboard"], apiServices: ["board"], botCapabilities: [
20217
+ { label: "\u8BFB\u767D\u677F", anyOf: ["board:whiteboard:node:read"] },
20218
+ { label: "\u5199\u767D\u677F", anyOf: ["board:whiteboard:node:create"] }
20219
+ ] },
20220
+ spark: { commands: ["apps"], apiServices: ["spark"], botCapabilities: [
20221
+ { label: "\u8BFB\u5E94\u7528", anyOf: ["spark:app:read"] },
20222
+ { label: "\u5199\u5E94\u7528", anyOf: ["spark:app:write"] }
20223
+ ] }
20224
+ };
20225
+ DRIVE_DOCS_ONLY_SHORTCUTS = [
20226
+ // 评论
20227
+ "+add-comment",
20228
+ "+add-reply",
20229
+ "+batch-query-comments",
20230
+ "+delete-reply",
20231
+ "+list-comments",
20232
+ "+list-replies",
20233
+ "+react-reply",
20234
+ "+resolve-comment",
20235
+ "+restore-comment",
20236
+ "+update-reply",
20237
+ // 协作者与分享权限
20238
+ "+apply-permission",
20239
+ "+member-add",
20240
+ "+member-list",
20241
+ "+member-remove",
20242
+ "+permission-get-setting",
20243
+ // 导入导出、搜索、密级
20244
+ "+export",
20245
+ "+export-download",
20246
+ "+import",
20247
+ "+search",
20248
+ "+secure-label-list",
20249
+ "+secure-label-update"
20250
+ ];
20251
+ DRIVE_DOCS_ONLY_RESOURCES = [
20252
+ "file.comment.reply.reactions",
20253
+ "file.comment.replys",
20254
+ "file.comments",
20255
+ "permission.members",
20256
+ "permission.public",
20257
+ "user"
20258
+ ];
20259
+ DRIVE_SHARED_SHORTCUTS = ["+copy", "+create-folder", "+create-shortcut", "+delete", "+move", "+update-title", "+inspect", "+task_result"];
20260
+ FEISHU_SHARED_RULES = [
20261
+ ...DRIVE_DOCS_ONLY_SHORTCUTS.map((s2) => ({ pattern: `cmd/drive/${s2}/**`, modules: ["docs"] })),
20262
+ ...DRIVE_DOCS_ONLY_RESOURCES.map((r) => ({ pattern: `cmd/drive/${r}/*/**`, modules: ["docs"] })),
20263
+ ...DRIVE_SHARED_SHORTCUTS.map((s2) => ({ pattern: `cmd/drive/${s2}/**`, modules: ["docs", "drive"] })),
20264
+ ...["copy", "create_folder", "patch"].map((m2) => ({ pattern: `cmd/drive/files/${m2}/**`, modules: ["docs", "drive"] })),
20265
+ { pattern: "cmd/drive/metas/*/**", modules: ["docs", "drive"] },
20266
+ // drive 后面先写参数再写子命令的(`drive --as bot +add-comment`),认不出子命令归谁——谁都不放行。
20267
+ { pattern: "cmd/drive/-/**", modules: [] },
20268
+ { pattern: "cmd/drive/**", modules: ["drive"] },
20269
+ ...[
20270
+ "drive/v1/permissions/**",
20271
+ "drive/v2/permissions/**",
20272
+ "drive/permission/**",
20273
+ "drive/v1/files/*/comments/**",
20274
+ "drive/v1/files/*/new_comments/**",
20275
+ "drive/v2/files/*/comments/**",
20276
+ "drive/v2/files/*/likes/**",
20277
+ "drive/v1/files/*/subscribe/**",
20278
+ "drive/v1/files/*/get_subscribe/**",
20279
+ "drive/v1/files/*/delete_subscribe/**",
20280
+ "drive/v1/files/*/subscriptions/**",
20281
+ "drive/v1/user/**",
20282
+ "drive/v1/import_tasks/**",
20283
+ "drive/v1/export_tasks/**",
20284
+ "drive/v1/medias/**",
20285
+ "drive/v2/files/*/secure_label/**",
20286
+ "drive/v2/my_secure_labels/**"
20287
+ ].map((p2) => ({ pattern: `api/*/${p2}`, modules: ["docs"] })),
20288
+ ...[
20289
+ "*/drive/v1/files/create_folder/**",
20290
+ "*/drive/v1/files/create_shortcut/**",
20291
+ "*/drive/v1/files/task_check/**",
20292
+ "*/drive/v1/files/*/move/**",
20293
+ "*/drive/v1/files/*/copy/**",
20294
+ "DELETE/drive/v1/files/*",
20295
+ "PATCH/drive/v1/files/*",
20296
+ "*/drive/v1/metas/**",
20297
+ "*/drive/v2/files/query_by_token/**",
20298
+ "*/drive/explorer/**"
20299
+ ].map((p2) => ({ pattern: `api/${p2}`, modules: ["docs", "drive"] })),
20300
+ { pattern: "api/*/drive/**", modules: ["drive"] },
20301
+ { pattern: "api/*/search/v1/user/**", modules: ["contact"] },
20302
+ { pattern: "api/*/search/v2/message/**", modules: ["im"] },
20303
+ { pattern: "api/*/search/v2/doc_wiki/**", modules: ["docs", "wiki"] }
20304
+ ];
20156
20305
  FEISHU_MODULE_LABELS = {
20157
20306
  im: "\u5373\u65F6\u6D88\u606F",
20158
20307
  calendar: "\u65E5\u5386",
@@ -20330,7 +20479,8 @@ var init_feishu = __esm({
20330
20479
  "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
20331
20480
  FEISHU_USER_OPEN_ID_VAR,
20332
20481
  FEISHU_AUTHORIZED_BY_VAR,
20333
- FEISHU_USER_AUTH_INVALID_VAR
20482
+ FEISHU_USER_AUTH_INVALID_VAR,
20483
+ "FEISHU_AUTH_PENDING_APPROVAL"
20334
20484
  ];
20335
20485
  /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
20336
20486
  hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
@@ -20403,7 +20553,7 @@ var init_feishu = __esm({
20403
20553
  if (!appSecret) throw new Error("feishu inject: missing app_secret");
20404
20554
  const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
20405
20555
  const modulePolicy = feishuModulePolicyEnv(
20406
- feishuSelectedModules(credentials2["auth_selection"] ?? credentials2[FEISHU_AUTH_SELECTION_VAR])
20556
+ feishuParseSelection(credentials2["auth_selection"] ?? credentials2[FEISHU_AUTH_SELECTION_VAR])
20407
20557
  );
20408
20558
  const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
20409
20559
  const creds = await writeSessionCredsFile(this.config.slug, {
@@ -20411,7 +20561,7 @@ var init_feishu = __esm({
20411
20561
  LARKSUITE_CLI_APP_SECRET: appSecret,
20412
20562
  ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
20413
20563
  ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {},
20414
- ...modulePolicy ?? {}
20564
+ ...modulePolicy
20415
20565
  }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
20416
20566
  const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
20417
20567
  await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
@@ -20522,7 +20672,8 @@ async function pollFeishuDeviceToken(input) {
20522
20672
  const data = await resp.json();
20523
20673
  const errStr = data["error"];
20524
20674
  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"));
20675
+ if (errStr) throw new FeishuDeviceFlowError(errStr, String(data["error_description"] ?? errStr));
20676
+ if (!resp.ok) throw new Error(String(data["error_description"] ?? "login failed"));
20526
20677
  if (!data["access_token"]) throw new Error("no access_token in response");
20527
20678
  return { status: "ok", data };
20528
20679
  }
@@ -20571,7 +20722,7 @@ function feishuUserAuthInvalidText(input) {
20571
20722
  ${input.link}` : `${head}
20572
20723
  \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20573
20724
  }
20574
- var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20725
+ var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL, FeishuDeviceFlowError;
20575
20726
  var init_user_auth = __esm({
20576
20727
  "../connectors/src/feishu/user-auth.ts"() {
20577
20728
  "use strict";
@@ -20580,6 +20731,78 @@ var init_user_auth = __esm({
20580
20731
  FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20581
20732
  FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20582
20733
  FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20734
+ FeishuDeviceFlowError = class extends Error {
20735
+ constructor(code2, message) {
20736
+ super(message);
20737
+ this.code = code2;
20738
+ this.name = "FeishuDeviceFlowError";
20739
+ }
20740
+ };
20741
+ }
20742
+ });
20743
+
20744
+ // ../connectors/src/feishu/pending-approval.ts
20745
+ function feishuPendingApprovalScopes(input) {
20746
+ const granted = input.granted ? new Set(input.granted) : null;
20747
+ const missing = input.requested.filter((s2) => s2 !== "offline_access" && !granted?.has(s2));
20748
+ if (!input.appScopes) return missing.filter((s2) => !AUTO_APPROVED2.has(s2));
20749
+ const byName = userScopeIndex(input.appScopes);
20750
+ return missing.filter((s2) => byName.has(s2) && !isGranted(byName.get(s2)));
20751
+ }
20752
+ function feishuPendingApprovalSettled(pending, appScopes) {
20753
+ const byName = userScopeIndex(appScopes);
20754
+ return pending.pendingScopes.every((s2) => isGranted(byName.get(s2)));
20755
+ }
20756
+ function feishuPendingApprovalModules(pending) {
20757
+ const waiting = new Set(pending.pendingScopes);
20758
+ return pending.modules.filter((m2) => (FEISHU_DOMAIN_SCOPES[m2]?.scopes ?? []).some((s2) => waiting.has(s2)));
20759
+ }
20760
+ async function readFeishuPendingApproval(store, actorId) {
20761
+ const raw = await store.revealVariable(FEISHU_AUTH_PENDING_APPROVAL_VAR, actorId).catch(() => null);
20762
+ if (!raw) return null;
20763
+ try {
20764
+ const p2 = JSON.parse(raw);
20765
+ return typeof p2?.at === "string" && typeof p2.appId === "string" && Array.isArray(p2.modules) && Array.isArray(p2.pendingScopes) ? p2 : null;
20766
+ } catch {
20767
+ return null;
20768
+ }
20769
+ }
20770
+ async function writeFeishuPendingApproval(store, actorId, pending) {
20771
+ await store.putVariable({
20772
+ key: FEISHU_AUTH_PENDING_APPROVAL_VAR,
20773
+ value: JSON.stringify(pending),
20774
+ ...actorId ? { scope: "personal", actorId } : { scope: "global" },
20775
+ connectorId: "feishu",
20776
+ encrypted: false
20777
+ });
20778
+ }
20779
+ async function clearFeishuPendingApproval(store, actorId) {
20780
+ await store.deleteVariable(FEISHU_AUTH_PENDING_APPROVAL_VAR, actorId);
20781
+ }
20782
+ function feishuApprovalGrantedText(input) {
20783
+ const head = `\u7BA1\u7406\u5458\u5DF2\u6279\u51C6\u300C${input.agentName}\u300D\u7533\u8BF7\u7684\u98DE\u4E66\u6743\u9650\uFF08${moduleNames(input.modules)}\uFF09\u3002\u8FD8\u5DEE\u4F60\u70B9\u4E00\u6B21\u6388\u6743\uFF0C\u8FD9\u90E8\u5206\u529F\u80FD\u624D\u80FD\u4EE5\u4F60\u7684\u8EAB\u4EFD\u4F7F\u7528\u3002`;
20784
+ return input.link ? `${head}
20785
+ \u70B9\u8FD9\u4E2A\u94FE\u63A5\u8865\u4E0A\u6743\u9650\uFF08\u9700\u7528\u6536\u5230\u8FD9\u6761\u79C1\u4FE1\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
20786
+ ${input.link}` : `${head}
20787
+ \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u8FDE\u63A5\u98DE\u4E66\u3002`;
20788
+ }
20789
+ function feishuPendingApprovalNotice(modules) {
20790
+ return `${moduleNames(modules)}\u542B\u9700\u98DE\u4E66\u7BA1\u7406\u5458\u5BA1\u6279\u7684\u6743\u9650\uFF0C\u5DF2\u63D0\u4EA4\u5BA1\u6279\u3002\u6279\u51C6\u540E\u4F1A\u5728\u98DE\u4E66\u79C1\u4FE1\u901A\u77E5\u4F60\uFF0C\u70B9\u4E00\u4E0B\u5373\u53EF\u8865\u4E0A\u8FD9\u90E8\u5206\u6743\u9650\uFF1B\u5DF2\u6709\u7684\u98DE\u4E66\u80FD\u529B\u4E0D\u53D7\u5F71\u54CD\u3002`;
20791
+ }
20792
+ var FEISHU_AUTH_PENDING_APPROVAL_VAR, FEISHU_PENDING_APPROVAL_WATCH_MS, FEISHU_APPROVED_SUPPLEMENT_KEEP_MS, AUTO_APPROVED2, userScopeIndex, isGranted, moduleNames;
20793
+ var init_pending_approval = __esm({
20794
+ "../connectors/src/feishu/pending-approval.ts"() {
20795
+ "use strict";
20796
+ init_scopes();
20797
+ FEISHU_AUTH_PENDING_APPROVAL_VAR = "FEISHU_AUTH_PENDING_APPROVAL";
20798
+ FEISHU_PENDING_APPROVAL_WATCH_MS = 30 * 24 * 36e5;
20799
+ FEISHU_APPROVED_SUPPLEMENT_KEEP_MS = 7 * 24 * 36e5;
20800
+ AUTO_APPROVED2 = new Set(FEISHU_BASIC_SCOPES);
20801
+ userScopeIndex = (appScopes) => new Map(
20802
+ appScopes.filter((s2) => s2.scope_name && (!s2.scope_type || s2.scope_type === "user")).map((s2) => [s2.scope_name, s2])
20803
+ );
20804
+ isGranted = (s2) => !!s2 && (s2.grant_status === void 0 || s2.grant_status === 1);
20805
+ moduleNames = (modules) => modules.map((m2) => FEISHU_MODULE_LABELS[m2] ?? m2).join("\u3001") || "\u6240\u7533\u8BF7\u7684";
20583
20806
  }
20584
20807
  });
20585
20808
 
@@ -24504,6 +24727,7 @@ var init_src3 = __esm({
24504
24727
  init_host_isolation();
24505
24728
  init_feishu();
24506
24729
  init_user_auth();
24730
+ init_pending_approval();
24507
24731
  init_scopes();
24508
24732
  init_oauth();
24509
24733
  init_scheduled_refresh();
@@ -24525,6 +24749,14 @@ var init_src3 = __esm({
24525
24749
  });
24526
24750
 
24527
24751
  // ../server/src/feishu-reauth.ts
24752
+ async function supplementSelection(store, actorId) {
24753
+ const pending = await readFeishuPendingApproval(store, actorId);
24754
+ if (!pending) return null;
24755
+ const appId = await store.revealResolvedVariable("FEISHU_APP_ID", actorId).catch(() => null);
24756
+ if (appId !== pending.appId) return null;
24757
+ const sel = normalizeFeishuAuthSelection({ modules: pending.modules, sendAsUser: pending.sendAsUser });
24758
+ return sel ? { sel, sendAsUser: pending.sendAsUserRequested, pending } : null;
24759
+ }
24528
24760
  async function reauthSelection(store, actorId) {
24529
24761
  const raw = await store.revealVariable(FEISHU_AUTH_SELECTION_VAR, actorId).catch(() => null);
24530
24762
  if (raw) {
@@ -24565,17 +24797,31 @@ function createFeishuReauth(deps) {
24565
24797
  };
24566
24798
  }
24567
24799
  await withConnectorCredentialLock("feishu", claims.actorId, async () => {
24800
+ const pending2 = claims.purpose === "supplement" ? await readFeishuPendingApproval(store, claims.actorId) : null;
24568
24801
  await persistFeishuUserAuthorization(store, claims.actorId, data, now(), {
24569
24802
  openId: who.openId,
24570
- ...who.name ? { userName: who.name } : {}
24803
+ ...who.name ? { userName: who.name } : {},
24804
+ ...pending2?.applicant ? { authorizedBy: pending2.applicant } : {}
24571
24805
  });
24806
+ const record10 = feishuAuthSelectionRecord(sel, sendAsUserRequested, data);
24572
24807
  await store.putVariable({
24573
24808
  key: FEISHU_AUTH_SELECTION_VAR,
24574
- value: JSON.stringify(feishuAuthSelectionRecord(sel, sendAsUserRequested, data)),
24809
+ value: JSON.stringify(record10),
24575
24810
  ...claims.actorId ? { scope: "personal", actorId: claims.actorId } : { scope: "global" },
24576
24811
  connectorId: "feishu",
24577
24812
  encrypted: false
24578
24813
  });
24814
+ if (pending2) {
24815
+ const granted = record10.grantedScopes ? new Set(record10.grantedScopes) : null;
24816
+ const still = granted ? pending2.pendingScopes.filter((x2) => !granted.has(x2)) : [];
24817
+ if (still.length === 0) await clearFeishuPendingApproval(store, claims.actorId);
24818
+ else {
24819
+ const next = { ...pending2, pendingScopes: still };
24820
+ delete next.approvedAt;
24821
+ delete next.notice;
24822
+ await writeFeishuPendingApproval(store, claims.actorId, next);
24823
+ }
24824
+ }
24579
24825
  });
24580
24826
  await deps.onRestored?.(claims, { openId: who.openId, ...who.name ? { userName: who.name } : {} }).catch((e) => warn(`onRestored: ${String(e)}`));
24581
24827
  return { state: "restored", ...who.name ? { userName: who.name } : {} };
@@ -24607,7 +24853,15 @@ function createFeishuReauth(deps) {
24607
24853
  if (!store) return { ok: false, status: 404, message: "\u627E\u4E0D\u5230\u8FD9\u4E2A\u94FE\u63A5\u6240\u5C5E\u7684\u7EC4\u7EC7\u3002" };
24608
24854
  const app = await appCredentials(store, claims.actorId);
24609
24855
  if (!app) return { ok: false, status: 409, message: "\u8FD9\u540D Agent \u7684\u98DE\u4E66\u5E94\u7528\u51ED\u636E\u5DF2\u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u91CD\u65B0\u6388\u6743\u3002\u8BF7\u5230 Oasis \u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5\u98DE\u4E66\u3002" };
24610
- const { sel, sendAsUser } = await reauthSelection(store, claims.actorId);
24856
+ let picked;
24857
+ if (claims.purpose === "supplement") {
24858
+ const sup = await supplementSelection(store, claims.actorId);
24859
+ if (!sup) return { ok: false, status: 410, message: "\u8FD9\u6B21\u8865\u6388\u6743\u5DF2\u7ECF\u5B8C\u6210\u6216\u5DF2\u5931\u6548\u3002\u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u67E5\u770B\uFF0C\u9700\u8981\u65F6\u91CD\u65B0\u8FDE\u63A5\u98DE\u4E66\u3002" };
24860
+ picked = sup;
24861
+ } else {
24862
+ picked = await reauthSelection(store, claims.actorId);
24863
+ }
24864
+ const { sel, sendAsUser } = picked;
24611
24865
  const auth = await (deps.start ?? startFeishuDeviceAuthorization)({
24612
24866
  appId: app.appId,
24613
24867
  appSecret: app.appSecret,
@@ -24622,7 +24876,7 @@ function createFeishuReauth(deps) {
24622
24876
  const intervalSec = auth.interval ?? DEFAULT_POLL_INTERVAL_SEC;
24623
24877
  void watch(auth.deviceCode, entry, { store, ...app, sel, sendAsUser, intervalSec });
24624
24878
  const agentName = await deps.agentName?.(claims.companyId, claims.actorId).catch(() => void 0) ?? claims.actorId ?? "\u7EC4\u7EC7";
24625
- return { ok: true, verificationUrl: auth.verificationUrl, deviceCode: auth.deviceCode, agentName, intervalSec };
24879
+ return { ok: true, verificationUrl: auth.verificationUrl, deviceCode: auth.deviceCode, agentName, intervalSec, purpose: claims.purpose };
24626
24880
  },
24627
24881
  /** 页面轮询结果。票与 deviceCode 必须是同一次 begin 发出来的。 */
24628
24882
  status(token2, deviceCode) {
@@ -24639,17 +24893,18 @@ function createFeishuReauth(deps) {
24639
24893
  function feishuReauthPageHtml(input) {
24640
24894
  const head = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>\u91CD\u65B0\u6388\u6743\u98DE\u4E66</title>
24641
24895
  <style>body{font-family:system-ui,-apple-system,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1.25rem;color:#1f2329;line-height:1.6}a.btn{display:inline-block;background:#1f2329;color:#fff;padding:.6rem 1.2rem;border-radius:.5rem;text-decoration:none}#s{margin-top:1.25rem}</style></head><body>`;
24642
- if (!input.ok) return `${head}<h2>\u65E0\u6CD5\u91CD\u65B0\u6388\u6743</h2><p>${escapeHtml(input.message)}</p></body></html>`;
24643
- const cfg = JSON.stringify({ t: input.token, d: input.deviceCode, i: Math.max(2, input.intervalSec) }).replace(/</g, "\\u003c");
24644
- return `${head}<h2>\u91CD\u65B0\u6388\u6743\u98DE\u4E66\uFF1A${escapeHtml(input.agentName)}</h2>
24645
- <p>\u8BF7\u7528<strong>\u539F\u6388\u6743\u4EBA</strong>\u7684\u98DE\u4E66\u8D26\u53F7\u5B8C\u6210\u6388\u6743\u3002\u6362\u6210\u522B\u7684\u8D26\u53F7\u6388\u6743\u4F1A\u88AB\u62D2\u7EDD\uFF0C\u539F\u6388\u6743\u4FDD\u6301\u4E0D\u53D8\u3002</p>
24896
+ if (!input.ok) return `${head}<h2>\u65E0\u6CD5\u5B8C\u6210\u6388\u6743</h2><p>${escapeHtml(input.message)}</p></body></html>`;
24897
+ const supplement = input.purpose === "supplement";
24898
+ const cfg = JSON.stringify({ t: input.token, d: input.deviceCode, i: Math.max(2, input.intervalSec), s: supplement }).replace(/</g, "\\u003c");
24899
+ return `${head}<h2>${supplement ? "\u8865\u6388\u6743\u98DE\u4E66\uFF08\u7BA1\u7406\u5458\u5DF2\u6279\u51C6\uFF09" : "\u91CD\u65B0\u6388\u6743\u98DE\u4E66"}\uFF1A${escapeHtml(input.agentName)}</h2>
24900
+ <p>\u8BF7\u7528<strong>${supplement ? "\u7533\u8BF7\u4EBA" : "\u539F\u6388\u6743\u4EBA"}</strong>\u7684\u98DE\u4E66\u8D26\u53F7\u5B8C\u6210\u6388\u6743\u3002\u6362\u6210\u522B\u7684\u8D26\u53F7\u6388\u6743\u4F1A\u88AB\u62D2\u7EDD\uFF0C\u539F\u6388\u6743\u4FDD\u6301\u4E0D\u53D8\u3002</p>
24646
24901
  <p><a class="btn" href="${escapeHtml(input.verificationUrl)}" target="_blank" rel="noopener">\u53BB\u98DE\u4E66\u6388\u6743</a></p>
24647
24902
  <p id="s">\u7B49\u5F85\u4F60\u5728\u98DE\u4E66\u70B9\u300C\u6388\u6743\u300D\u2026</p>
24648
24903
  <script>(function(){var c=${cfg};var el=document.getElementById("s");
24649
24904
  function tick(){fetch("reauth/status",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({t:c.t,deviceCode:c.d})})
24650
24905
  .then(function(r){return r.json()}).then(function(b){
24651
24906
  if(b.state==="pending"){setTimeout(tick,c.i*1000);return}
24652
- if(b.state==="restored"){el.textContent="\u5DF2\u6062\u590D\uFF1A"+(b.userName?b.userName+" \u7684":"")+"\u98DE\u4E66\u7528\u6237\u6388\u6743\u91CD\u65B0\u751F\u6548\uFF0C\u53EF\u4EE5\u5173\u6389\u8FD9\u4E2A\u9875\u9762\u4E86\u3002";return}
24907
+ if(b.state==="restored"){el.textContent=(c.s?"\u5DF2\u8865\u4E0A\uFF1A":"\u5DF2\u6062\u590D\uFF1A")+(b.userName?b.userName+" \u7684":"")+(c.s?"\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5305\u542B\u7BA1\u7406\u5458\u6279\u51C6\u7684\u6743\u9650":"\u98DE\u4E66\u7528\u6237\u6388\u6743\u91CD\u65B0\u751F\u6548")+"\uFF0C\u53EF\u4EE5\u5173\u6389\u8FD9\u4E2A\u9875\u9762\u4E86\u3002";return}
24653
24908
  el.textContent=b.message||"\u6388\u6743\u6CA1\u6709\u5B8C\u6210\uFF0C\u53EF\u4EE5\u91CD\u65B0\u70B9\u94FE\u63A5\u518D\u8BD5\u3002"}).catch(function(){setTimeout(tick,c.i*1000)})}
24654
24909
  setTimeout(tick,c.i*1000)})();</script></body></html>`;
24655
24910
  }
@@ -212007,10 +212262,10 @@ async function feishuModuleAccess(store, actorId, fetchScopes = fetchFeishuAppSc
212007
212262
  const appSecret = await read3("FEISHU_APP_SECRET");
212008
212263
  if (!appId || !appSecret) return { configured: false };
212009
212264
  const rawSelection = await read3(FEISHU_AUTH_SELECTION_VAR);
212010
- const modules = feishuSelectedModules(rawSelection);
212265
+ const { state: selection, modules } = feishuParseSelection(rawSelection);
212011
212266
  let parsed = {};
212012
212267
  try {
212013
- parsed = rawSelection ? JSON.parse(rawSelection) : {};
212268
+ parsed = rawSelection ? JSON.parse(rawSelection) ?? {} : {};
212014
212269
  } catch {
212015
212270
  parsed = {};
212016
212271
  }
@@ -212028,7 +212283,7 @@ async function feishuModuleAccess(store, actorId, fetchScopes = fetchFeishuAppSc
212028
212283
  }
212029
212284
  return {
212030
212285
  configured: true,
212031
- restricted: modules.length > 0,
212286
+ selection,
212032
212287
  modules: feishuModuleAccessReport({ modules, grantedScopes, hasUserAuth, userAuthInvalid, appScopes }),
212033
212288
  hasUserAuth,
212034
212289
  userAuthInvalid,
@@ -212048,6 +212303,42 @@ var init_feishu_module_access = __esm({
212048
212303
  }
212049
212304
  });
212050
212305
 
212306
+ // ../server/src/feishu-pending-approval.ts
212307
+ async function recordFeishuPendingApproval(input) {
212308
+ const requested = scopesForSelection(input.selection, input.sendAsUserRequested);
212309
+ let appScopes = null;
212310
+ try {
212311
+ appScopes = await (input.fetchScopes ?? fetchFeishuAppScopes)(input.appId, input.appSecret);
212312
+ } catch (e) {
212313
+ (input.warn ?? ((m2) => console.warn(`[feishu-oauth] ${m2}`)))(`\u5E94\u7528\u6743\u9650\u5217\u8868\u67E5\u8BE2\u5931\u8D25\uFF0C\u5F85\u5BA1\u6279\u6309\u514D\u5BA1\u96C6\u5408\u4F30\u7B97: ${String(e)}`);
212314
+ }
212315
+ const pendingScopes = feishuPendingApprovalScopes({ requested, granted: input.granted, appScopes });
212316
+ if (pendingScopes.length === 0) {
212317
+ if (input.granted) {
212318
+ await withConnectorCredentialLock("feishu", input.actorId, () => clearFeishuPendingApproval(input.store, input.actorId));
212319
+ }
212320
+ return null;
212321
+ }
212322
+ const record10 = {
212323
+ at: new Date(input.now).toISOString(),
212324
+ appId: input.appId,
212325
+ ...input.applicant ? { applicant: input.applicant } : {},
212326
+ modules: input.selection.modules,
212327
+ sendAsUser: input.selection.sendAsUser,
212328
+ sendAsUserRequested: input.sendAsUserRequested,
212329
+ pendingScopes
212330
+ };
212331
+ await withConnectorCredentialLock("feishu", input.actorId, () => writeFeishuPendingApproval(input.store, input.actorId, record10));
212332
+ const modules = feishuPendingApprovalModules(record10);
212333
+ return { modules, scopes: pendingScopes, notice: feishuPendingApprovalNotice(modules) };
212334
+ }
212335
+ var init_feishu_pending_approval = __esm({
212336
+ "../server/src/feishu-pending-approval.ts"() {
212337
+ "use strict";
212338
+ init_src3();
212339
+ }
212340
+ });
212341
+
212051
212342
  // ../server/src/github-app-inventory.ts
212052
212343
  async function collectOasisGithubApps(svc) {
212053
212344
  const rows = (await svc.listVariables()).filter((r) => r.scope === "global" || r.scope === "personal");
@@ -218349,12 +218640,40 @@ ${composed}`;
218349
218640
  const pollSvc = await connectorActorsService();
218350
218641
  const appSecret = pollSvc ? await pollSvc.revealResolvedVariable("FEISHU_APP_SECRET", actorId) : null;
218351
218642
  if (!appSecret) throw new Error("FEISHU_APP_SECRET not found \u2014 please complete app setup first");
218352
- const polled = await pollFeishuDeviceToken({ appId, appSecret, deviceCode });
218643
+ let polled;
218644
+ try {
218645
+ polled = await pollFeishuDeviceToken({ appId, appSecret, deviceCode });
218646
+ } catch (e) {
218647
+ if (!(e instanceof FeishuDeviceFlowError) || !pollSvc) throw e;
218648
+ const pendingApproval2 = await recordFeishuPendingApproval({
218649
+ store: pollSvc,
218650
+ actorId,
218651
+ appId,
218652
+ appSecret,
218653
+ selection: startedSelection.selection,
218654
+ sendAsUserRequested: startedSelection.sendAsUserRequested,
218655
+ ...isHumanActorId(actor) ? { applicant: actor } : {},
218656
+ now: Date.now()
218657
+ });
218658
+ const expired = e.code === "expired_token" || e.code === "invalid_grant";
218659
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(pendingApproval2 ? {
218660
+ ok: false,
218661
+ final: true,
218662
+ pendingApproval: { modules: pendingApproval2.modules, scopes: pendingApproval2.scopes },
218663
+ notice: expired ? `\u6388\u6743\u9875\u5DF2\u8D85\u65F6\u3002\u5982\u679C\u4F60\u5728\u6388\u6743\u9875\u70B9\u4E86\u300C\u7533\u8BF7\u300D\uFF1A${pendingApproval2.notice}\u6CA1\u70B9\u7684\u8BDD\u91CD\u65B0\u53D1\u8D77\u6388\u6743\u5373\u53EF\u3002` : pendingApproval2.notice
218664
+ } : {
218665
+ ok: false,
218666
+ final: true,
218667
+ error: expired ? "\u6388\u6743\u8D85\u65F6\uFF08\u8BBE\u5907\u7801\u5DF2\u8FC7\u671F\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743" : `\u98DE\u4E66\u6CA1\u6709\u5B8C\u6210\u6388\u6743\uFF08${e.message.slice(0, 200)}\uFF09\uFF0C\u53EF\u4EE5\u91CD\u65B0\u53D1\u8D77\u6388\u6743`
218668
+ }));
218669
+ return;
218670
+ }
218353
218671
  if (polled.status === "pending") {
218354
218672
  res.writeHead(200).end(JSON.stringify({ ok: false, error: "authorization_pending" }));
218355
218673
  return;
218356
218674
  }
218357
218675
  const data = polled.data;
218676
+ let pendingApproval = null;
218358
218677
  const svc = await connectorActorsService();
218359
218678
  if (svc) {
218360
218679
  if (actorId) {
@@ -218379,6 +218698,23 @@ ${composed}`;
218379
218698
  connectorId: "feishu",
218380
218699
  encrypted: false
218381
218700
  });
218701
+ if (record10.grantedScopes) {
218702
+ try {
218703
+ pendingApproval = await recordFeishuPendingApproval({
218704
+ store: svc,
218705
+ actorId,
218706
+ appId,
218707
+ appSecret,
218708
+ selection: startedSelection.selection,
218709
+ sendAsUserRequested: startedSelection.sendAsUserRequested,
218710
+ ...isHumanActorId(actor) ? { applicant: actor } : {},
218711
+ granted: record10.grantedScopes,
218712
+ now: Date.now()
218713
+ });
218714
+ } catch (e) {
218715
+ console.error(`[feishu-oauth] \u5F85\u5BA1\u6279\u8BB0\u5F55\u5199\u5165\u5931\u8D25: ${String(e)}`);
218716
+ }
218717
+ }
218382
218718
  if (!who.openId) console.error("[feishu-oauth] user_info \u672A\u53D6\u5230\u6388\u6743\u4EBA open_id\uFF1A\u6388\u6743\u5931\u6548\u65F6\u65E0\u6CD5\u79C1\u4FE1\u672C\u4EBA\u3001\u4E5F\u7B7E\u4E0D\u51FA\u4E00\u952E\u91CD\u65B0\u6388\u6743\u94FE\u63A5");
218383
218719
  try {
218384
218720
  const secret = await svc.revealResolvedVariable("FEISHU_APP_SECRET", actorId);
@@ -218403,7 +218739,12 @@ ${composed}`;
218403
218739
  }
218404
218740
  }
218405
218741
  }
218406
- res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true, appId, stored: true }));
218742
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
218743
+ ok: true,
218744
+ appId,
218745
+ stored: true,
218746
+ ...pendingApproval ? { pendingApproval: { modules: pendingApproval.modules, scopes: pendingApproval.scopes }, notice: pendingApproval.notice } : {}
218747
+ }));
218407
218748
  } catch (e) {
218408
218749
  res.writeHead(500).end(JSON.stringify({ error: String(e) }));
218409
218750
  }
@@ -218746,6 +219087,7 @@ var init_server3 = __esm({
218746
219087
  init_child_live_turn();
218747
219088
  init_registry_audit_sink();
218748
219089
  init_feishu_module_access();
219090
+ init_feishu_pending_approval();
218749
219091
  init_src3();
218750
219092
  init_github_app_inventory();
218751
219093
  init_github_app_timing();
@@ -286888,53 +287230,43 @@ function createConnectorRenewalRuntime(options) {
286888
287230
  return "sent";
286889
287231
  };
286890
287232
  const recipientsFor = async (companyId, store, actorId) => {
286891
- const out = /* @__PURE__ */ new Set();
286892
287233
  const by = await store.revealVariable(FEISHU_AUTHORIZED_BY_VAR, actorId).catch(() => null);
286893
- if (by && isHumanActorId(by)) out.add(by);
286894
- if (out.size === 0 && actorId) {
286895
- if (isHumanActorId(actorId)) out.add(actorId);
286896
- else {
286897
- const human = await options.boundHuman(actorId, companyId).catch(() => null);
286898
- if (human) out.add(human);
286899
- }
286900
- }
286901
- return [...out];
287234
+ if (by && isHumanActorId(by)) return { recipients: [by], authorizer: by };
287235
+ if (!actorId) return { recipients: [], authorizer: null };
287236
+ if (isHumanActorId(actorId)) return { recipients: [actorId], authorizer: null };
287237
+ const human = await options.boundHuman(actorId, companyId).catch(() => null);
287238
+ return { recipients: human ? [human] : [], authorizer: null };
286902
287239
  };
286903
- const recoverOpenId = async (companyId, app, recipients) => {
286904
- for (const human of recipients) {
286905
- const email2 = await options.memberEmail?.(human, companyId).catch(() => null);
286906
- if (!email2) continue;
286907
- const openId = await lookupFeishuOpenIdByEmail({ ...app, email: email2 });
286908
- if (openId) return openId;
286909
- }
286910
- return null;
287240
+ const recoverOpenId = async (companyId, app, authorizer) => {
287241
+ const email2 = await options.memberEmail?.(authorizer, companyId).catch(() => null);
287242
+ return email2 ? lookupFeishuOpenIdByEmail({ ...app, email: email2 }) : null;
286911
287243
  };
286912
287244
  const deliverInvalidNotice = async (companyId, store, actorId, marker) => {
286913
287245
  const prev = marker.notice ?? { inApp: false, dm: "pending", attempts: 0 };
286914
287246
  const state = { ...prev, attempts: prev.attempts + 1 };
286915
287247
  const who = `actor=${actorId ?? "-"} \u7B2C ${state.attempts}/${MAX_NOTICE_ATTEMPTS} \u6B21`;
286916
287248
  const name = await agentName(companyId, actorId);
286917
- const recipients = await recipientsFor(companyId, store, actorId);
287249
+ const { recipients, authorizer } = await recipientsFor(companyId, store, actorId);
287250
+ const unverified = !authorizer && !await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId).catch(() => "?");
286918
287251
  if (!state.inApp) {
286919
287252
  try {
286920
287253
  const ledger = await options.notifications(companyId);
286921
- if (ledger) {
286922
- const rows = [];
286923
- for (const recipient of recipients) {
286924
- rows.push(await ledger.insert({
286925
- recipient,
286926
- category: "connector",
286927
- kind: "feishu-auth-invalid",
286928
- // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效补发 / 重复扫描不重发(唯一约束)。
286929
- sourceKey: `connector:feishu:${actorId ?? "org"}@invalid:${marker.at}`,
286930
- title: `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
286931
- ...actorId ? { actorId } : {},
286932
- enteredAt: marker.at
286933
- }));
286934
- }
286935
- if (rows.length > 0) options.onNotifications?.(companyId, rows);
287254
+ if (!ledger) throw new Error("\u8BE5\u516C\u53F8\u7684\u901A\u77E5\u53F0\u8D26\u4E0D\u53EF\u7528");
287255
+ if (recipients.length === 0) throw new Error("\u6CA1\u6709\u6536\u4EF6\u4EBA\uFF08\u6CA1\u8BB0\u6388\u6743\u4EBA\u3001\u5458\u5DE5\u4E5F\u6CA1\u6709\u7ED1\u5B9A\u771F\u4EBA\uFF09");
287256
+ const rows = [];
287257
+ for (const recipient of recipients) {
287258
+ rows.push(await ledger.insert({
287259
+ recipient,
287260
+ category: "connector",
287261
+ kind: "feishu-auth-invalid",
287262
+ // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效补发 / 重复扫描不重发(唯一约束)。
287263
+ sourceKey: `connector:feishu:${actorId ?? "org"}@invalid:${marker.at}`,
287264
+ title: unverified ? `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u8BF7\u5230\u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5` : `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
287265
+ ...actorId ? { actorId } : {},
287266
+ enteredAt: marker.at
287267
+ }));
286936
287268
  }
286937
- if (recipients.length === 0) console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u7AD9\u5185\u901A\u77E5\u6CA1\u6709\u6536\u4EF6\u4EBA actor=${actorId ?? "-"}`);
287269
+ options.onNotifications?.(companyId, rows);
286938
287270
  state.inApp = true;
286939
287271
  } catch (err) {
286940
287272
  options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u7AD9\u5185\u901A\u77E5\u672A\u5199\u5165\uFF08${who}\uFF0C\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09: ${String(err)}`));
@@ -286943,12 +287275,13 @@ function createConnectorRenewalRuntime(options) {
286943
287275
  if (state.dm === "pending") {
286944
287276
  try {
286945
287277
  const app = await appCredentials(store, actorId);
286946
- const stored = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
286947
- const recovered = !stored && app ? await recoverOpenId(companyId, app, recipients) : null;
286948
- const openId = stored || recovered;
287278
+ const storedOpenId = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
287279
+ const recovered = !storedOpenId && app && authorizer ? await recoverOpenId(companyId, app, authorizer) : null;
287280
+ const openId = storedOpenId || recovered;
286949
287281
  if (!app || !openId) {
286950
287282
  state.dm = "unavailable";
286951
- console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u65E0\u4ECE\u53D1\u51FA\uFF08${app ? "\u6CA1\u8BB0\u6388\u6743\u4EBA\u3001\u901A\u8BAF\u5F55\u4E5F\u6CA1\u6309\u6210\u5458\u90AE\u7BB1\u627E\u5230\u672C\u4EBA" : "\u6CA1\u6709\u5E94\u7528\u51ED\u636E"}\uFF09actor=${actorId ?? "-"}\uFF1B\u53EA\u6709\u7AD9\u5185\u901A\u77E5`);
287283
+ const why = !app ? "\u6CA1\u6709\u5E94\u7528\u51ED\u636E" : unverified ? "\u6CA1\u8BB0\u539F\u6388\u6743\u4EBA\uFF0C\u6838\u5B9E\u4E0D\u4E86\u672C\u4EBA\uFF0C\u4E0D\u7B7E\u94FE\u63A5\uFF1B\u9700\u5230\u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5" : "\u901A\u8BAF\u5F55\u6CA1\u6309\u539F\u6388\u6743\u4EBA\u7684\u8D26\u53F7\u90AE\u7BB1\u627E\u5230\u4ED6";
287284
+ console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u65E0\u4ECE\u53D1\u51FA\uFF08${why}\uFF09actor=${actorId ?? "-"}\uFF1B\u53EA\u6709\u7AD9\u5185\u901A\u77E5`);
286952
287285
  } else {
286953
287286
  const token2 = await issueFeishuReauthToken(options.links, store, {
286954
287287
  companyId,
@@ -287005,6 +287338,122 @@ function createConnectorRenewalRuntime(options) {
287005
287338
  }
287006
287339
  for (const k2 of handledThisSweep) if (JSON.parse(k2)[0] === companyId) handledThisSweep.delete(k2);
287007
287340
  };
287341
+ const applicantOpenId = async (companyId, store, actorId, app, applicant) => {
287342
+ if (!applicant || !isHumanActorId(applicant)) return null;
287343
+ const by = await store.revealVariable(FEISHU_AUTHORIZED_BY_VAR, actorId).catch(() => null);
287344
+ const stored = by === applicant ? await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId).catch(() => null) : null;
287345
+ return stored || recoverOpenId(companyId, app, applicant);
287346
+ };
287347
+ const deliverApprovalNotice = async (companyId, store, actorId, pending, app) => {
287348
+ const prev = pending.notice ?? { inApp: false, dm: "pending", attempts: 0 };
287349
+ const state = { ...prev, attempts: prev.attempts + 1 };
287350
+ const who = `actor=${actorId ?? "-"} \u7B2C ${state.attempts}/${MAX_NOTICE_ATTEMPTS} \u6B21`;
287351
+ const name = await agentName(companyId, actorId);
287352
+ const modules = feishuPendingApprovalModules(pending);
287353
+ if (!state.inApp) {
287354
+ try {
287355
+ const ledger = await options.notifications(companyId);
287356
+ if (!ledger) throw new Error("\u8BE5\u516C\u53F8\u7684\u901A\u77E5\u53F0\u8D26\u4E0D\u53EF\u7528");
287357
+ const recipients = pending.applicant && isHumanActorId(pending.applicant) ? [pending.applicant] : (await recipientsFor(companyId, store, actorId)).recipients;
287358
+ if (recipients.length === 0) throw new Error("\u6CA1\u6709\u6536\u4EF6\u4EBA\uFF08\u6CA1\u8BB0\u7533\u8BF7\u4EBA\u3001\u5458\u5DE5\u4E5F\u6CA1\u6709\u7ED1\u5B9A\u771F\u4EBA\uFF09");
287359
+ const rows = [];
287360
+ for (const recipient of recipients) {
287361
+ rows.push(await ledger.insert({
287362
+ recipient,
287363
+ category: "connector",
287364
+ kind: "feishu-approval-granted",
287365
+ // 每次申请一条(键里带提交时刻);同一次批准补发 / 重复扫描不重发(唯一约束)。
287366
+ sourceKey: `connector:feishu:${actorId ?? "org"}@approved:${pending.at}`,
287367
+ title: `\u7BA1\u7406\u5458\u5DF2\u6279\u51C6\u300C${name}\u300D\u7533\u8BF7\u7684\u98DE\u4E66\u6743\u9650\uFF0C\u8865\u6388\u6743\u540E\u5373\u53EF\u4F7F\u7528`,
287368
+ ...actorId ? { actorId } : {},
287369
+ enteredAt: pending.approvedAt ?? new Date(now()).toISOString()
287370
+ }));
287371
+ }
287372
+ options.onNotifications?.(companyId, rows);
287373
+ state.inApp = true;
287374
+ } catch (err) {
287375
+ options.warn(new Error(`\u98DE\u4E66\u6743\u9650\u6279\u51C6\u7AD9\u5185\u901A\u77E5\u672A\u5199\u5165\uFF08${who}\uFF0C\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09: ${String(err)}`));
287376
+ }
287377
+ }
287378
+ if (state.dm === "pending") {
287379
+ try {
287380
+ const openId = await applicantOpenId(companyId, store, actorId, app, pending.applicant);
287381
+ if (!openId) {
287382
+ state.dm = "unavailable";
287383
+ console.warn(`[credential-refresh] \u98DE\u4E66\u6743\u9650\u6279\u51C6\u79C1\u4FE1\u65E0\u4ECE\u53D1\u51FA\uFF08${pending.applicant ? "\u901A\u8BAF\u5F55\u6CA1\u6309\u7533\u8BF7\u4EBA\u7684\u8D26\u53F7\u90AE\u7BB1\u627E\u5230\u4ED6" : "\u6CA1\u8BB0\u7533\u8BF7\u4EBA"}\uFF09actor=${actorId ?? "-"}\uFF1B\u53EA\u6709\u7AD9\u5185\u901A\u77E5\uFF0C\u9700\u5230\u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5`);
287384
+ } else {
287385
+ const token2 = options.links.issue({
287386
+ companyId,
287387
+ ...actorId ? { actorId } : {},
287388
+ connectorId: "feishu",
287389
+ purpose: "supplement",
287390
+ subject: openId
287391
+ });
287392
+ const link = options.apiBase ? feishuReauthUrl(options.apiBase, token2) : void 0;
287393
+ await notifyFeishuUser(store, actorId, feishuApprovalGrantedText({ agentName: name, modules, ...link ? { link } : {} }), openId);
287394
+ state.dm = "sent";
287395
+ }
287396
+ } catch (err) {
287397
+ options.warn(new Error(`\u98DE\u4E66\u6743\u9650\u6279\u51C6\u79C1\u4FE1\u53D1\u9001\u5931\u8D25\uFF08${who}\uFF0C\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09: ${String(err)}`));
287398
+ }
287399
+ }
287400
+ if ((!state.inApp || state.dm === "pending") && state.attempts >= MAX_NOTICE_ATTEMPTS) {
287401
+ options.warn(new Error(`\u98DE\u4E66\u6743\u9650\u6279\u51C6\u901A\u77E5\u8865\u53D1 ${state.attempts} \u6B21\u4ECD\u672A\u9001\u5230\uFF0C\u653E\u5F03 actor=${actorId ?? "-"} \u7AD9\u5185=${String(state.inApp)} \u79C1\u4FE1=${state.dm}`));
287402
+ }
287403
+ return state;
287404
+ };
287405
+ const savePendingApproval = async (store, actorId, at2, next) => {
287406
+ await withConnectorCredentialLock("feishu", actorId, async () => {
287407
+ const current = await readFeishuPendingApproval(store, actorId);
287408
+ if (current?.at !== at2) return;
287409
+ if (next) await writeFeishuPendingApproval(store, actorId, next);
287410
+ else await clearFeishuPendingApproval(store, actorId);
287411
+ });
287412
+ };
287413
+ const checkPendingApproval = async (companyId, store, actorId) => {
287414
+ const pending = await readFeishuPendingApproval(store, actorId);
287415
+ if (!pending) return;
287416
+ const t = now();
287417
+ const app = await appCredentials(store, actorId);
287418
+ if (!app || app.appId !== pending.appId) {
287419
+ console.warn(`[credential-refresh] \u98DE\u4E66\u5F85\u5BA1\u6279\u8BB0\u5F55\u4F5C\u5E9F\uFF08\u5E94\u7528\u51ED\u636E${app ? "\u5DF2\u6362\u6210\u522B\u7684\u5E94\u7528" : "\u4E0D\u5B58\u5728"}\uFF09actor=${actorId ?? "-"}`);
287420
+ await savePendingApproval(store, actorId, pending.at, null);
287421
+ return;
287422
+ }
287423
+ let next;
287424
+ if (!pending.approvedAt) {
287425
+ if (t - Date.parse(pending.at) > FEISHU_PENDING_APPROVAL_WATCH_MS) {
287426
+ options.warn(new Error(`\u98DE\u4E66\u5F85\u5BA1\u6279 ${pending.at} \u8D77 30 \u5929\u672A\u68C0\u6D4B\u5230\u6279\u51C6\uFF0C\u4E0D\u518D\u68C0\u6D4B actor=${actorId ?? "-"} \u6743\u9650=${pending.pendingScopes.join(",")}`));
287427
+ await savePendingApproval(store, actorId, pending.at, null);
287428
+ return;
287429
+ }
287430
+ const scopes = await (options.fetchAppScopes ?? fetchFeishuAppScopes)(app.appId, app.appSecret);
287431
+ if (!feishuPendingApprovalSettled(pending, scopes)) return;
287432
+ next = { ...pending, approvedAt: new Date(t).toISOString(), notice: { inApp: false, dm: "pending", attempts: 0 } };
287433
+ console.warn(`[credential-refresh] \u98DE\u4E66\u5F85\u5BA1\u6279\u5DF2\u6279\u51C6 actor=${actorId ?? "-"} \u6743\u9650=${pending.pendingScopes.join(",")}`);
287434
+ } else {
287435
+ if (t - Date.parse(pending.approvedAt) > FEISHU_APPROVED_SUPPLEMENT_KEEP_MS) {
287436
+ await savePendingApproval(store, actorId, pending.at, null);
287437
+ return;
287438
+ }
287439
+ const n = pending.notice;
287440
+ if (!n || n.inApp && n.dm !== "pending" || n.attempts >= MAX_NOTICE_ATTEMPTS) return;
287441
+ next = pending;
287442
+ }
287443
+ await savePendingApproval(store, actorId, pending.at, { ...next, notice: await deliverApprovalNotice(companyId, store, actorId, next, app) });
287444
+ };
287445
+ const checkPendingApprovals = async (companyId, store) => {
287446
+ const svc = await options.actors(companyId);
287447
+ if (!svc) return;
287448
+ const ids2 = [void 0, ...(await svc.listActors({ status: "active" })).map((a) => a.id)];
287449
+ for (const actorId of ids2) {
287450
+ try {
287451
+ await checkPendingApproval(companyId, store, actorId);
287452
+ } catch (err) {
287453
+ options.warn(new Error(`\u98DE\u4E66\u5F85\u5BA1\u6279\u68C0\u6D4B\u5931\u8D25\uFF08\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09actor=${actorId ?? "-"}: ${String(err)}`));
287454
+ }
287455
+ }
287456
+ };
287008
287457
  const onFeishuRenewed = async (e, store) => {
287009
287458
  if (await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, e.actorId)) return;
287010
287459
  const accessToken = e.outcome.credentials?.["user_access_token"];
@@ -287043,7 +287492,13 @@ function createConnectorRenewalRuntime(options) {
287043
287492
  onRenewed: async (e, store) => {
287044
287493
  if (e.connector.config.slug === "feishu") await onFeishuRenewed(e, store);
287045
287494
  },
287046
- afterCompany: retryPendingNotices,
287495
+ afterCompany: async (companyId, store) => {
287496
+ try {
287497
+ await retryPendingNotices(companyId, store);
287498
+ } finally {
287499
+ await checkPendingApprovals(companyId, store);
287500
+ }
287501
+ },
287047
287502
  warn: options.warn,
287048
287503
  ...options.now ? { now: options.now } : {}
287049
287504
  });
@@ -287055,7 +287510,7 @@ function createConnectorRenewalRuntime(options) {
287055
287510
  const store = await options.actors(claims.companyId);
287056
287511
  if (!store) return;
287057
287512
  const name = await agentName(claims.companyId, claims.actorId);
287058
- await notifyFeishuUser(store, claims.actorId, `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u6062\u590D\u3002`).catch(() => void 0);
287513
+ await notifyFeishuUser(store, claims.actorId, claims.purpose === "supplement" ? `\u300C${name}\u300D\u7684\u98DE\u4E66\u6743\u9650\u5DF2\u8865\u4E0A\uFF0C\u7BA1\u7406\u5458\u6279\u51C6\u7684\u529F\u80FD\u73B0\u5728\u53EF\u4EE5\u4F7F\u7528\u4E86\u3002` : `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u6062\u590D\u3002`).catch(() => void 0);
287059
287514
  },
287060
287515
  onRejected: async (claims, who) => {
287061
287516
  const store = await options.actors(claims.companyId);
@@ -300944,7 +301399,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
300944
301399
  }
300945
301400
  }
300946
301401
  }
300947
- var CLI_VERSION = "2.2.112" ? "2.2.112" : "dev";
301402
+ var CLI_VERSION = "2.2.114" ? "2.2.114" : "dev";
300948
301403
  function apiHeaders(token2, company) {
300949
301404
  const headers = {
300950
301405
  authorization: `Bearer ${token2}`,
@@ -305115,7 +305570,7 @@ function shimScript() {
305115
305570
  }
305116
305571
 
305117
305572
  // src/index.ts
305118
- var PKG_VERSION = true ? "2.2.112" : "dev";
305573
+ var PKG_VERSION = true ? "2.2.114" : "dev";
305119
305574
  var LOCAL_BIN = localBin();
305120
305575
  var NPM_PREFIX = npmPrefix();
305121
305576
  var INSTANCE = DEFAULT_INSTANCE;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oasis_test_v2",
3
- "version": "2.2.112",
3
+ "version": "2.2.114",
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": "278bdc2558b23bd381c4c3007a8d40330244edc1"
29
+ "sourceHead": "46e4f9275679ea8eec37f0f1e30b45cc48e3e70c"
30
30
  }
31
31
  }