Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
5.91 kB
import { useEffect, useRef } from 'react';
import { Check, ChevronLeft, Loader2, RefreshCw, ShieldCheck, X } from 'lucide-react';
import { FeishuLogoIcon } from '../FeishuLogoIcon.jsx';
export function DocsPanel({ open, docs, busy, error, onClose, onConnect, onDisconnect, onOpenHome, onOpenAuth, onRefresh }) {
const closeButtonRef = useRef(null);
useEffect(() => {
if (open) {
closeButtonRef.current?.focus();
}
}, [open]);
if (!open) {
return null;
}
const cliInstalled = Boolean(docs?.cliInstalled);
const skillsInstalled = Boolean(docs?.skillsInstalled);
const configured = Boolean(docs?.configured);
const connected = Boolean(docs?.connected);
const authorizationReady = connected && Boolean(docs?.authorizationReady);
const missingScopes = Array.isArray(docs?.missingScopes) ? docs.missingScopes : [];
const needsExtraAuth = connected && (!authorizationReady || missingScopes.length > 0);
const slidesAuthorized = connected && Boolean(docs?.slidesAuthorized);
const sheetsAuthorized = connected && Boolean(docs?.sheetsAuthorized);
const authPending = docs?.authPending;
const setupItems = [
{ id: 'cli', label: 'lark-cli', ok: cliInstalled },
{ id: 'skills', label: '官方 skills', ok: skillsInstalled },
{ id: 'config', label: 'App 凭证', ok: configured },
{ id: 'auth', label: '用户授权', ok: connected },
{ id: 'slides', label: 'PPT 权限', ok: slidesAuthorized },
{ id: 'sheets', label: '表格权限', ok: sheetsAuthorized }
];
const subtitle = connected
? needsExtraAuth
? '待补权限'
: ''
: authPending?.status === 'polling'
? '等待授权'
: configured
? '未连接'
: '未配置';
const summary = authPending?.status === 'polling'
? '授权页已打开,完成后回到这里刷新状态。'
: connected
? needsExtraAuth
? '飞书账号已连接,但部分文档权限还没授权。补充授权后,Codex 可完整操作飞书文档、PPT、表格和云空间文件。'
: 'Codex 已可操作飞书文档、PPT、表格和云空间文件。'
: !cliInstalled
? '本机还没有检测到 lark-cli。'
: !skillsInstalled
? '官方文档 skills 还没有安装完整。'
: configured
? '连接飞书账号后,Codex 才能以你的身份操作文档、PPT 和表格。'
: '请先在后端配置飞书 App ID 和 Secret。';
const canConnect = cliInstalled && skillsInstalled && configured;
return (
<section className="docs-panel" role="dialog" aria-modal="true" aria-label="飞书文档">
<header className="docs-panel-header">
<button ref={closeButtonRef} className="icon-button" type="button" onClick={onClose} aria-label="关闭文档">
<ChevronLeft size={22} />
</button>
<div className="docs-panel-title">
<strong>飞书文档</strong>
{subtitle ? <span>{subtitle}</span> : null}
</div>
<button className="icon-button" type="button" onClick={onClose} aria-label="关闭文档">
<X size={20} />
</button>
</header>
<div className="docs-panel-body">
<div className="docs-status-state">
<div className="docs-status-icon">
<FeishuLogoIcon size={58} />
</div>
<h2>飞书文档</h2>
<p>{summary}</p>
{error ? <div className="docs-panel-error">{error}</div> : null}
{authPending?.verificationUrl && (!connected || needsExtraAuth) ? (
<div className="docs-auth-box">
<span>授权码 {authPending.userCode || '已生成'}</span>
<button type="button" onClick={() => onOpenAuth(authPending.verificationUrl)}>
打开授权页
</button>
</div>
) : null}
<div className="docs-check-list">
{setupItems.map((item) => (
<div key={item.id} className={item.ok ? 'is-ok' : ''}>
{item.ok ? <Check size={15} /> : <X size={15} />}
<span>{item.label}</span>
</div>
))}
</div>
{needsExtraAuth && missingScopes.length ? (
<div className="docs-scope-hint">
缺少 {missingScopes.slice(0, 4).join('、')}
</div>
) : null}
<div className="docs-panel-actions">
{connected ? (
<>
<button type="button" onClick={needsExtraAuth ? onConnect : onOpenHome} disabled={needsExtraAuth && busy}>
{needsExtraAuth ? (
busy ? <Loader2 className="spin" size={16} /> : <ShieldCheck size={16} />
) : (
<FeishuLogoIcon size={18} />
)}
{needsExtraAuth ? '补充授权' : '打开飞书'}
</button>
<button type="button" onClick={onDisconnect} disabled={busy}>
{busy ? <Loader2 className="spin" size={16} /> : <X size={16} />}
断开
</button>
<button type="button" onClick={onRefresh} disabled={busy}>
<RefreshCw size={16} />
刷新
</button>
</>
) : (
<>
<button type="button" onClick={onConnect} disabled={!canConnect || busy}>
{busy ? <Loader2 className="spin" size={16} /> : <ShieldCheck size={16} />}
连接飞书
</button>
<button type="button" onClick={onRefresh} disabled={busy}>
<RefreshCw size={16} />
刷新
</button>
</>
)}
</div>
</div>
</div>
</section>
);
}