mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
feat(workflow): add workflow engine, /workflows panel, /ultracode skill
将 feat/sdk-backend 分支中 workflow 相关的 20 个 commit 压缩为单 commit: - 工作流引擎核心:phase / agent / parallel / pipeline 编排原语(packages/workflow-engine/) - /workflows 面板:三区焦点布局(顶部 run tabs + 左侧 phase 侧栏 + 右侧 agent 列表) - /ultracode skill:多 agent workflow 编排入口 - 进度存储 / journal / notification 系统 - WorkflowService 生命周期管理 + SentryErrorBoundary - 脚本沙箱:禁用 dynamic import()、JSON args 防御性归一化 - journal 与 named-workflow 路径统一在 projectRoot - 错误处理:parallel/pipeline hooks 错误日志、failure routing、semaphore abort - workflow 工具升级为 core 工具 + PascalCase 命名 Co-Authored-By: glm-5.1 <zai-org@claude-code-best.win>
This commit is contained in:
148
packages/workflow-engine/src/engine/runWorkflow.ts
Normal file
148
packages/workflow-engine/src/engine/runWorkflow.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { WORKFLOW_DIR_NAME } from '../constants.js'
|
||||
import type { HostHandle, WorkflowPorts } from '../ports.js'
|
||||
import type { JournalEntry, WorkflowRunResult } from '../types.js'
|
||||
import { createEngineContext } from './context.js'
|
||||
import { WorkflowAbortedError, WorkflowError } from './errors.js'
|
||||
import { makeHooks, type SubWorkflowRunner } from './hooks.js'
|
||||
import { resolveNamedWorkflow } from './namedWorkflows.js'
|
||||
import { parseScript, type ParsedScript } from './script.js'
|
||||
|
||||
export type RunWorkflowOptions = {
|
||||
/** 已解析好的脚本源码。 */
|
||||
script: string
|
||||
args?: unknown
|
||||
runId: string
|
||||
workflowName?: string
|
||||
ports: WorkflowPorts
|
||||
host: HostHandle
|
||||
signal: AbortSignal
|
||||
cwd: string
|
||||
budgetTotal: number | null
|
||||
/** resume:true 时载入既有 journal 重放。 */
|
||||
resume?: boolean
|
||||
/** resume 时脚本源码 hash 是否变化。true 则忽略 journal 全重跑。 */
|
||||
scriptChanged?: boolean
|
||||
}
|
||||
|
||||
export async function runWorkflow(
|
||||
opts: RunWorkflowOptions,
|
||||
): Promise<WorkflowRunResult> {
|
||||
const { ports } = opts
|
||||
|
||||
let parsed: ParsedScript
|
||||
try {
|
||||
parsed = parseScript(opts.script)
|
||||
} catch (e) {
|
||||
const error = (e as Error).message
|
||||
ports.progressEmitter.emit({
|
||||
type: 'run_done',
|
||||
runId: opts.runId,
|
||||
status: 'failed',
|
||||
error,
|
||||
})
|
||||
return { status: 'failed', error }
|
||||
}
|
||||
|
||||
const workflowName = opts.workflowName ?? parsed.meta?.name ?? 'workflow'
|
||||
|
||||
// 载入 journal(仅 resume 且脚本未变)
|
||||
let journal: JournalEntry[] = []
|
||||
let journalInvalidated = false
|
||||
if (opts.resume && !opts.scriptChanged) {
|
||||
journal = await ports.journalStore.read(opts.runId)
|
||||
} else if (opts.scriptChanged) {
|
||||
await ports.journalStore.truncate(opts.runId)
|
||||
journalInvalidated = true
|
||||
}
|
||||
|
||||
const ctx = createEngineContext({
|
||||
ports,
|
||||
host: opts.host,
|
||||
signal: opts.signal,
|
||||
runId: opts.runId,
|
||||
workflowName,
|
||||
cwd: opts.cwd,
|
||||
budgetTotal: opts.budgetTotal,
|
||||
journal,
|
||||
})
|
||||
if (journalInvalidated) ctx.journalInvalidated = true
|
||||
|
||||
ports.progressEmitter.emit({
|
||||
type: 'run_started',
|
||||
runId: opts.runId,
|
||||
workflowName,
|
||||
meta: parsed.meta,
|
||||
})
|
||||
|
||||
// 子 workflow 执行器:复用同一 ctx(共享 journal/并发/预算/计数),临时 +1 depth
|
||||
const runSubWorkflow: SubWorkflowRunner = async sub => {
|
||||
const script = await resolveSubScript(sub, opts.cwd)
|
||||
let subParsed: ParsedScript
|
||||
try {
|
||||
subParsed = parseScript(script)
|
||||
} catch (e) {
|
||||
throw new WorkflowError(`子 workflow 脚本错误:${(e as Error).message}`)
|
||||
}
|
||||
const prevDepth = ctx.resources.depth
|
||||
ctx.resources.depth += 1
|
||||
try {
|
||||
const subHooks = makeHooks(ctx, runSubWorkflow)
|
||||
return await subParsed.execute(subHooks, sub.args, ctx.resources.budget)
|
||||
} finally {
|
||||
ctx.resources.depth = prevDepth
|
||||
}
|
||||
}
|
||||
|
||||
const hooks = makeHooks(ctx, runSubWorkflow)
|
||||
|
||||
try {
|
||||
const returnValue = await parsed.execute(
|
||||
hooks,
|
||||
opts.args,
|
||||
ctx.resources.budget,
|
||||
)
|
||||
ports.progressEmitter.emit({
|
||||
type: 'run_done',
|
||||
runId: opts.runId,
|
||||
status: 'completed',
|
||||
returnValue,
|
||||
})
|
||||
return { status: 'completed', returnValue }
|
||||
} catch (e) {
|
||||
if (e instanceof WorkflowAbortedError) {
|
||||
ports.progressEmitter.emit({
|
||||
type: 'run_done',
|
||||
runId: opts.runId,
|
||||
status: 'killed',
|
||||
})
|
||||
return { status: 'killed' }
|
||||
}
|
||||
const error = (e as Error).message
|
||||
ports.progressEmitter.emit({
|
||||
type: 'run_done',
|
||||
runId: opts.runId,
|
||||
status: 'failed',
|
||||
error,
|
||||
})
|
||||
return { status: 'failed', error }
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveSubScript(
|
||||
sub: { name?: string; scriptPath?: string; script?: string },
|
||||
cwd: string,
|
||||
): Promise<string> {
|
||||
if (sub.script) return sub.script
|
||||
if (sub.scriptPath) return await readFile(sub.scriptPath, 'utf-8')
|
||||
if (sub.name) {
|
||||
const found = await resolveNamedWorkflow(
|
||||
join(cwd, WORKFLOW_DIR_NAME),
|
||||
sub.name,
|
||||
)
|
||||
if (!found) throw new WorkflowError(`子 workflow "${sub.name}" 未找到`)
|
||||
return found.content
|
||||
}
|
||||
throw new WorkflowError('workflow() 需要 name 或 scriptPath')
|
||||
}
|
||||
Reference in New Issue
Block a user