AI写作:技能检索进度反馈与超时兜底
检索等待时 SkillStepsCard 显示 spinner;单技能检索 25s 超时避免卡死;前后端增加 skill-timing 调试日志。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
23d6f9efc2
commit
633a350bfc
4 files changed
+65
-11
No files matched your search
@@ -32,6 +32,14 @@ export function SkillStepsCard({ steps, inProgress }: SkillStepsCardProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
if (steps.length === 0 && !inProgress) return null;
|
||||
|
||||
// 进行中的步骤:最后一条是 invoke_skill 且还没出对应的 skill_result,
|
||||
// 说明该技能的 web 检索正在 await 中——把它的序号徽标换成 spinner,
|
||||
// 让每次(可能数秒~数十秒的)检索等待都有"正在调用技能X检索中"的实时反馈。
|
||||
const activeIdx =
|
||||
inProgress && steps.length > 0 && steps[steps.length - 1].type === 'invoke_skill'
|
||||
? steps.length - 1
|
||||
: -1;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-violet-200 bg-violet-50/40 dark:border-violet-900/40 dark:bg-violet-950/20 overflow-hidden">
|
||||
<button
|
||||
@@ -63,19 +71,22 @@ export function SkillStepsCard({ steps, inProgress }: SkillStepsCardProps) {
|
||||
const label = STEP_LABEL[step.type] ?? step.type;
|
||||
const isStopEarly = step.type === 'stop_early';
|
||||
const isResult = step.type === 'skill_result';
|
||||
const isActive = idx === activeIdx;
|
||||
return (
|
||||
<li key={idx} className="flex items-start gap-2 text-[11px]">
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 mt-0.5 inline-flex items-center justify-center size-4 rounded-full text-[10px] font-medium',
|
||||
isStopEarly
|
||||
? 'bg-amber-100 dark:bg-amber-900/60 text-amber-600 dark:text-amber-300'
|
||||
: isResult
|
||||
? 'bg-emerald-100 dark:bg-emerald-900/60 text-emerald-600 dark:text-emerald-300'
|
||||
: 'bg-violet-100 dark:bg-violet-900/60 text-violet-600 dark:text-violet-300',
|
||||
isActive
|
||||
? 'bg-violet-500 text-white'
|
||||
: isStopEarly
|
||||
? 'bg-amber-100 dark:bg-amber-900/60 text-amber-600 dark:text-amber-300'
|
||||
: isResult
|
||||
? 'bg-emerald-100 dark:bg-emerald-900/60 text-emerald-600 dark:text-emerald-300'
|
||||
: 'bg-violet-100 dark:bg-violet-900/60 text-violet-600 dark:text-violet-300',
|
||||
)}
|
||||
>
|
||||
{idx + 1}
|
||||
{isActive ? <Loader2 className="size-2.5 animate-spin" /> : idx + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
@@ -137,7 +148,9 @@ export function SkillStepsCard({ steps, inProgress }: SkillStepsCardProps) {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{inProgress && steps.length > 0 && (
|
||||
{inProgress && steps.length > 0 && activeIdx < 0 && (
|
||||
// 当前不在某个技能的检索等待中(最后一条是 result / select 等),
|
||||
// 提示还有后续步骤;正在检索时由该步自身的 spinner 体现进行中,不重复。
|
||||
<li className="flex items-center gap-2 text-[11px] text-muted-foreground italic">
|
||||
<Loader2 className="size-3 animate-spin shrink-0" />
|
||||
更多步骤生成中…
|
||||
|
||||
@@ -634,12 +634,25 @@ export function useAIWriting() {
|
||||
dispatch({ type: 'set_session', sessionId: meta.thread_id });
|
||||
},
|
||||
onCustomEvent(event: unknown) {
|
||||
// 技能检索时机 trace:开 window.__AI_WRITING_DEBUG__ = true 后,控制台按
|
||||
// performance.now() 打印每条 custom 事件到达时刻 + type,用来钉死
|
||||
// skill_search_step(技能检索过程卡)与 skill_select_chunk(筛选技能打字)
|
||||
// 的实际到达顺序,区分"后端未实时 emit" vs "前端渲染滞后"。
|
||||
if (typeof window !== 'undefined' && (window as { __AI_WRITING_DEBUG__?: boolean }).__AI_WRITING_DEBUG__) {
|
||||
const t = (event as { type?: string } | null)?.type;
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[ai-writing:custom] +${Math.round(performance.now())}ms type=${t ?? '?'}`, event);
|
||||
}
|
||||
const pe = customToProgressEvent(event, state.sessionId ?? '');
|
||||
if (pe) dispatch({ type: 'handle', event: pe });
|
||||
},
|
||||
onUpdateEvent(data: Record<string, unknown> | undefined) {
|
||||
// data 形如 { node_name: { ...node_output } },可能多个节点同一 step
|
||||
if (!data) return;
|
||||
if (typeof window !== 'undefined' && (window as { __AI_WRITING_DEBUG__?: boolean }).__AI_WRITING_DEBUG__) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(`[ai-writing:updates] +${Math.round(performance.now())}ms nodes=${Object.keys(data).join(',')}`);
|
||||
}
|
||||
// 兜底:updates 通道可能直接带 ``__interrupt__`` 数组(节点进 interrupt 时
|
||||
// LangGraph 推 ``{__interrupt__: [...]}``),SDK 主路径只在 values 通道
|
||||
// 暴露 stream.interrupt —— 万一后端 serialize 又把 __interrupt__ 滤掉,
|
||||
|
||||
Reference in new issue
Block a user