File size: 8,661 Bytes
deedb97 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | /**
* 工作流共享工具函数
*
* 供 architecture-workflow / chapter-workflow 等多个工作流复用的通用逻辑
*
* 核心组件:
* 1. withRetry — 通用异步重试包装器
* 2. PostProcessPipeline — 后处理流水线(注册 → 执行 → 持久化 → 修复)
*/
import type { StepCallbacks } from '../../stores/workflow-store'
import { ipc } from '../ipc-client'
// ===== 文本处理通用工具 =====
/**
* 剥除文本中可能包含的 <think>...</think> 思维链标签
* 用于清洗大模型在生成正文时输出的思维链,避免其被持久化写入磁盘文件
*/
export function stripThinkingTags(text: string): string {
if (!text) return text
// 支持只有 <think> 没有闭合标签的情况
return text.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, '').trim()
}
// ===== 通用重试包装器 =====
/**
* 带重试的异步操作包装器
* @param fn 要执行的异步函数
* @param maxRetries 最大重试次数(不含首次执行)
* @param label 操作标签(用于日志)
* @param callbacks 步骤回调(用于输出日志)
* @returns 成功返回 { ok: true },全部失败返回 { ok: false, error }
*/
export async function withRetry(
fn: () => Promise<void>,
maxRetries: number,
label: string,
callbacks: StepCallbacks,
): Promise<{ ok: boolean; error?: string; attempts: number }> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
await fn()
return { ok: true, attempts: attempt + 1 }
} catch (err) {
const errMsg = err instanceof Error ? err.message : String(err)
if (attempt < maxRetries) {
callbacks.log(` ⚠️ ${label} 第${attempt + 1}次失败,正在重试...(${errMsg})`)
} else {
return { ok: false, error: errMsg, attempts: attempt + 1 }
}
}
}
return { ok: false, error: '未知错误', attempts: maxRetries + 1 }
}
// ===== 后处理流水线 =====
/** 单个后处理步骤定义 */
export interface PostProcessStep {
/** 唯一标识,如 'chapter_notes' */
key: string
/** 展示名称,如 '📋 章节要点' */
label: string
/** 关键步骤(失败阻断下游工作流) */
critical: boolean
/** 步骤执行器 */
executor: (callbacks: StepCallbacks) => Promise<void>
}
/** 单步后处理执行结果(持久化到状态文件) */
export interface PostProcessStepResult {
label: string
critical: boolean
ok: boolean
completedAt?: string
error?: string
lastAttemptAt: string
attemptCount: number
}
/** 后处理状态(持久化到 .vela/post_process/{scope}.json) */
export interface PostProcessStatus {
/** 唯一标识,如 'chapter_1_finalize' */
scope: string
/** 来源描述,如 '第1章定稿' */
sourceLabel: string
/** 首次执行时间 */
createdAt: string
/** 最后更新时间 */
updatedAt: string
/** 各步骤执行结果 */
steps: Record<string, PostProcessStepResult>
/** 所有关键步骤是否通过 */
allCriticalPassed: boolean
}
/** 解析原有 scope 字符串为 sourceType 和 sourceId */
function parseScope(scope: string): { sourceType: string; sourceId: string } {
const match = scope.match(/^chapter_(\d+)_finalize$/)
if (match) return { sourceType: 'chapter_finalize', sourceId: match[1] }
return { sourceType: 'unknown', sourceId: scope }
}
/** 读取后处理状态 (向后兼容 UI) */
export async function readPostProcessStatus(
_projectPath: string,
scope: string,
): Promise<PostProcessStatus | null> {
try {
const { sourceType, sourceId } = parseScope(scope)
const run = await ipc.invoke('db:post-process-get-latest-run', sourceType, sourceId)
if (!run) return null
const steps = await ipc.invoke('db:post-process-get-steps', run.id)
const status: PostProcessStatus = {
scope,
sourceLabel: run.sourceLabel,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
allCriticalPassed: run.allCriticalPassed,
steps: {}
}
for (const s of steps) {
status.steps[s.stepKey] = {
label: s.label,
critical: s.critical,
ok: s.ok,
completedAt: s.completedAt || undefined,
error: s.errorMsg || undefined,
lastAttemptAt: s.lastAttemptAt || '',
attemptCount: s.attemptCount
}
}
return status
} catch {
return null
}
}
/** 快捷检查:所有关键步骤是否通过 */
export async function isAllCriticalPassed(
_projectPath: string,
scope: string,
): Promise<boolean> {
const { sourceType, sourceId } = parseScope(scope)
return await ipc.invoke('db:post-process-is-all-passed', sourceType, sourceId)
}
/** 提取失败步骤的展示标签列表 */
export function getFailedStepLabels(status: PostProcessStatus): string[] {
return Object.values(status.steps)
.filter(s => !s.ok)
.map(s => s.label)
}
/** 获取章节定稿后处理的 scope 标识 */
export function getChapterFinalizeScope(chapterNumber: number): string {
return `chapter_${chapterNumber}_finalize`
}
// ===== 流水线执行器 =====
export interface PipelineOptions {
/** 每步重试次数,默认 2 */
retryCount?: number
/** true = 只重跑失败步骤(修复模式) */
onlyFailed?: boolean
}
/**
* 执行后处理流水线
*
* @param projectPath 项目路径(用于状态文件读写)
* @param scope 状态文件唯一标识
* @param sourceLabel 来源描述(展示用)
* @param steps 步骤列表
* @param callbacks 工作流回调
* @param options 可选配置
* @returns 完整的后处理状态
*/
export async function runPostProcessPipeline(
projectPath: string,
scope: string,
sourceLabel: string,
steps: PostProcessStep[],
callbacks: StepCallbacks,
options?: PipelineOptions,
): Promise<PostProcessStatus> {
const retryCount = options?.retryCount ?? 2
const onlyFailed = options?.onlyFailed ?? false
const { sourceType, sourceId } = parseScope(scope)
// 判断是否存在已有 instance
let run = await ipc.invoke('db:post-process-get-latest-run', sourceType, sourceId)
if (!onlyFailed || !run) {
// 新建跑批
callbacks.log(` 初始化后处理跑批...`)
const createRes = await ipc.invoke('db:post-process-create-run', {
triggerSourceType: sourceType,
triggerSourceId: sourceId,
sourceLabel,
steps: steps.map(s => ({ key: s.key, label: s.label, critical: s.critical }))
})
if (!createRes.success || !createRes.id) {
throw new Error(`创建跑批失败: ${createRes.error}`)
}
run = await ipc.invoke('db:post-process-get-latest-run', sourceType, sourceId)
}
if (!run) throw new Error('跑批获取异常')
const runId = run.id
const runSteps = await ipc.invoke('db:post-process-get-steps', runId)
const stepMap = new Map((runSteps as unknown as Array<Record<string, unknown>>).map((s) => [s.stepKey, s]))
for (const step of steps) {
const existingStep = stepMap.get(step.key)
// 修复模式:跳过已成功的步骤
if (onlyFailed && existingStep?.ok) {
callbacks.log(` ⏭️ ${step.label} — 已成功,跳过`)
continue
}
const result = await withRetry(() => step.executor(callbacks), retryCount, step.label, callbacks)
if (result.ok) {
await ipc.invoke('db:post-process-mark-step-ok', runId, step.key)
} else {
await ipc.invoke('db:post-process-mark-step-failed', runId, step.key, result.error || '未知错误')
}
}
// 返回最终状态汇总供 UI 展示
const status = await readPostProcessStatus(projectPath, scope)
if (!status) {
throw new Error('汇总状态获取失败')
}
// 最终汇总
const failedSteps = Object.values(status.steps).filter(s => !s.ok)
const successSteps = Object.values(status.steps).filter(s => s.ok)
callbacks.log('')
callbacks.log(`━━━━━━━━━━ ${sourceLabel} 后处理汇总 ━━━━━━━━━━`)
for (const [, r] of Object.entries(status.steps)) {
callbacks.log(` ${r.ok ? '✅' : '❌'} ${r.label}${r.ok ? '' : ` — ${r.error}`}`)
}
callbacks.log(`━━━━━━━━━━ ${successSteps.length}/${Object.keys(status.steps).length} 成功 ━━━━━━━━━━`)
if (failedSteps.length > 0) {
const failedLabels = failedSteps.map(r => r.label).join('、')
callbacks.log(`⚠️ 以下后处理步骤失败:${failedLabels}`)
if (failedSteps.some(s => s.critical)) {
callbacks.log('💡 存在关键步骤失败,后续流程可能被阻断。请在对应页面使用「重试」功能修复')
}
}
return status
}
|