Spaces:
Running
Running
| import fs from 'node:fs/promises'; | |
| import os from 'node:os'; | |
| import path from 'node:path'; | |
| import { | |
| CAPABILITIES, | |
| REQUIRED_SCOPE_GROUPS, | |
| REQUIRED_SKILLS, | |
| STATUS_CACHE_MS | |
| } from './lark-cli-definitions.js'; | |
| import { runLarkCli } from './lark-cli-runner.js'; | |
| let statusCache = { at: 0, value: null }; | |
| export function resetLarkDocsStatusCache() { | |
| statusCache = { at: 0, value: null }; | |
| } | |
| export function envValue(...keys) { | |
| for (const key of keys) { | |
| const value = String(process.env[key] || '').trim(); | |
| if (value) { | |
| return value; | |
| } | |
| } | |
| return ''; | |
| } | |
| function parseScopes(value) { | |
| if (Array.isArray(value)) { | |
| return value.map((scope) => String(scope || '').trim()).filter(Boolean); | |
| } | |
| return String(value || '') | |
| .split(/\s+/) | |
| .map((scope) => scope.trim()) | |
| .filter(Boolean); | |
| } | |
| function larkScopeStatus(grantedScopes = []) { | |
| const granted = new Set(grantedScopes); | |
| const groups = REQUIRED_SCOPE_GROUPS.map((group) => { | |
| const missing = group.scopes.filter((scope) => !granted.has(scope)); | |
| return { | |
| id: group.id, | |
| label: group.label, | |
| ok: missing.length === 0, | |
| missing | |
| }; | |
| }); | |
| return { | |
| groups, | |
| missingScopes: groups.flatMap((group) => group.missing), | |
| slidesAuthorized: Boolean(groups.find((group) => group.id === 'slides')?.ok), | |
| sheetsAuthorized: Boolean(groups.find((group) => group.id === 'sheets')?.ok) | |
| }; | |
| } | |
| export async function larkCliVersion() { | |
| const result = await runLarkCli(['--version'], { timeoutMs: 8000 }); | |
| if (!result.ok) { | |
| return { installed: false, version: '', error: result.error || result.stderr }; | |
| } | |
| const match = result.stdout.match(/(\d+\.\d+\.\d+)/); | |
| return { installed: true, version: match?.[1] || result.stdout.trim(), error: '' }; | |
| } | |
| async function larkSkillsInstalled() { | |
| const root = path.join(os.homedir(), '.agents', 'skills'); | |
| const missing = []; | |
| for (const skill of REQUIRED_SKILLS) { | |
| try { | |
| await fs.access(path.join(root, skill, 'SKILL.md')); | |
| } catch { | |
| missing.push(skill); | |
| } | |
| } | |
| return { | |
| installed: missing.length === 0, | |
| missing, | |
| root | |
| }; | |
| } | |
| export async function larkConfigStatus() { | |
| const appId = envValue('LARK_APP_ID', 'CODEXMOBILE_FEISHU_APP_ID'); | |
| const appSecret = envValue('LARK_APP_SECRET', 'CODEXMOBILE_FEISHU_APP_SECRET'); | |
| const show = await runLarkCli(['config', 'show'], { timeoutMs: 10000 }); | |
| const config = show.json || {}; | |
| return { | |
| configured: show.ok || Boolean(appId && appSecret), | |
| configReady: show.ok, | |
| appId: config.appId || appId || '', | |
| brand: config.brand || 'feishu', | |
| defaultAs: config.defaultAs || '', | |
| workspace: config.workspace || '', | |
| hasEnvCredentials: Boolean(appId && appSecret), | |
| error: show.ok ? '' : show.error || show.stderr || show.stdout | |
| }; | |
| } | |
| function authUserFromStatus(data) { | |
| const candidate = data?.user || data?.currentUser || data?.current_user || data || {}; | |
| const name = | |
| candidate.name || | |
| candidate.userName || | |
| candidate.user_name || | |
| candidate.enName || | |
| candidate.en_name || | |
| candidate.email || | |
| candidate.openId || | |
| candidate.open_id || | |
| candidate.userOpenId || | |
| ''; | |
| return name | |
| ? { | |
| name, | |
| email: candidate.email || candidate.enterpriseEmail || candidate.enterprise_email || '', | |
| openId: candidate.openId || candidate.open_id || candidate.userOpenId || candidate.user_open_id || '' | |
| } | |
| : null; | |
| } | |
| async function larkAuthStatusRaw() { | |
| const result = await runLarkCli(['auth', 'status'], { timeoutMs: 10000 }); | |
| const data = result.json || {}; | |
| const text = `${result.stdout}\n${result.stderr}\n${result.error || ''}`; | |
| const noUser = /no user logged in|only bot/i.test(text); | |
| const connected = Boolean(result.ok && !noUser && (data.identity === 'user' || data.user || data.currentUser || data.openId || data.open_id)); | |
| return { | |
| connected, | |
| identity: data.identity || '', | |
| defaultAs: data.defaultAs || '', | |
| user: connected ? authUserFromStatus(data) || { name: '已授权用户', email: '', openId: '' } : null, | |
| scopes: parseScopes(data.scope || data.scopes), | |
| tokenStatus: data.tokenStatus || data.token_status || '', | |
| expiresAt: data.expiresAt || data.expires_at || '', | |
| refreshExpiresAt: data.refreshExpiresAt || data.refresh_expires_at || '', | |
| error: result.ok ? '' : result.error || result.stderr || result.stdout, | |
| note: data.note || '' | |
| }; | |
| } | |
| async function larkAuthStatus() { | |
| const auth = await larkAuthStatusRaw(); | |
| const tokenStatus = String(auth.tokenStatus || ''); | |
| if (!auth.connected || !/needs_refresh|expired/i.test(tokenStatus)) { | |
| return auth; | |
| } | |
| const verifiedResult = await runLarkCli(['auth', 'status', '--verify'], { timeoutMs: 15000 }); | |
| const verifiedData = verifiedResult.json || {}; | |
| const verified = verifiedData.verified; | |
| const verifyError = verifiedData.verifyError || verifiedData.verify_error || verifiedResult.error || verifiedResult.stderr || ''; | |
| const requiresReauth = | |
| verified === false && | |
| /need_user_authorization|invalid_grant|token unusable|20064/i.test(String(verifyError || '')); | |
| return { | |
| ...auth, | |
| connected: requiresReauth ? false : auth.connected, | |
| user: requiresReauth ? null : auth.user, | |
| verified, | |
| verifyError, | |
| error: requiresReauth ? verifyError || 'Feishu authorization expired' : auth.error | |
| }; | |
| } | |
| export async function getLarkDocsStatusState(options = {}, pendingAuth = () => null) { | |
| const { authenticated = true, force = false } = options; | |
| const now = Date.now(); | |
| if (!force && statusCache.value && now - statusCache.at <= STATUS_CACHE_MS) { | |
| return authenticated ? statusCache.value : { ...statusCache.value, connected: false, user: null, authPending: null }; | |
| } | |
| const cli = await larkCliVersion(); | |
| const skills = await larkSkillsInstalled(); | |
| const config = cli.installed | |
| ? await larkConfigStatus() | |
| : { | |
| configured: Boolean(envValue('LARK_APP_ID', 'CODEXMOBILE_FEISHU_APP_ID') && envValue('LARK_APP_SECRET', 'CODEXMOBILE_FEISHU_APP_SECRET')), | |
| configReady: false, | |
| appId: envValue('LARK_APP_ID', 'CODEXMOBILE_FEISHU_APP_ID'), | |
| brand: 'feishu', | |
| defaultAs: '', | |
| workspace: '', | |
| hasEnvCredentials: Boolean(envValue('LARK_APP_ID', 'CODEXMOBILE_FEISHU_APP_ID') && envValue('LARK_APP_SECRET', 'CODEXMOBILE_FEISHU_APP_SECRET')), | |
| error: cli.error | |
| }; | |
| const auth = authenticated && cli.installed && config.configured | |
| ? await larkAuthStatus() | |
| : { connected: false, user: null, identity: '', defaultAs: '', note: '', error: '', scopes: [] }; | |
| const enabled = Boolean(cli.installed && skills.installed && config.configured && auth.connected); | |
| const scopeStatus = enabled | |
| ? larkScopeStatus(auth.scopes) | |
| : { groups: [], missingScopes: [], slidesAuthorized: false, sheetsAuthorized: false }; | |
| const authorizationReady = enabled && scopeStatus.missingScopes.length === 0; | |
| const capabilities = enabled | |
| ? CAPABILITIES.filter((capability) => { | |
| if (capability.id.startsWith('slides.')) { | |
| return scopeStatus.slidesAuthorized; | |
| } | |
| if (capability.id.startsWith('sheets.')) { | |
| return scopeStatus.sheetsAuthorized; | |
| } | |
| return true; | |
| }) | |
| : []; | |
| const status = { | |
| provider: 'feishu', | |
| integration: 'lark-cli', | |
| label: '飞书文档', | |
| configured: config.configured, | |
| configReady: config.configReady, | |
| connected: authenticated ? auth.connected : false, | |
| user: authenticated ? auth.user : null, | |
| cliInstalled: cli.installed, | |
| cliVersion: cli.version, | |
| skillsInstalled: skills.installed, | |
| missingSkills: skills.missing, | |
| identity: auth.identity || config.defaultAs || '', | |
| defaultAs: auth.defaultAs || config.defaultAs || '', | |
| workspace: config.workspace, | |
| homeUrl: process.env.CODEXMOBILE_FEISHU_DOCS_URL || 'https://docs.feishu.cn/', | |
| capabilities, | |
| codexEnabled: enabled, | |
| authorizationReady, | |
| scopeGroups: authenticated ? scopeStatus.groups : [], | |
| missingScopes: authenticated ? scopeStatus.missingScopes : [], | |
| slidesAuthorized: authenticated ? scopeStatus.slidesAuthorized : false, | |
| sheetsAuthorized: authenticated ? scopeStatus.sheetsAuthorized : false, | |
| tokenStatus: authenticated ? auth.tokenStatus : '', | |
| expiresAt: authenticated ? auth.expiresAt : '', | |
| authPending: authenticated ? pendingAuth() : null, | |
| error: cli.error || config.error || auth.error || '' | |
| }; | |
| if (authenticated) { | |
| statusCache = { at: now, value: status }; | |
| } | |
| return authenticated ? status : { ...status, connected: false, user: null, authPending: null }; | |
| } | |