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:
claude-code-best
2026-06-13 20:07:18 +08:00
parent 91cffe16e2
commit d236880bc3
106 changed files with 16127 additions and 834 deletions

View 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
/** resumetrue 时载入既有 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')
}