GitHub Trending · kgoedecke/doop · 2026-08-25

Doop Resident Agent:从设计卡片到后台执行

拆解 Doop 如何把 Board、角色 pipeline、反馈队列和 resident worker 串成可恢复的 agent 流程。

01

后台 agent 的调度边界

不是无限自动执行

resident agent 的第一道边界是配置和并发。server/resident.ts:42 在缺少 OpenAI key 时跳过运行,server/resident.ts:56 使用 runningCanvases 防止同一 canvas 并发,server/resident.ts:69 的 sweep 只处理等待中的 work,server/resident.ts:75 从 pendingWorkAgents 取任务。这让后台自动化更像队列 worker,而不是浏览器里突然出现的自由 agent。

server/resident.ts:69后台 sweep 限制每个 canvas 的运行
69/** Work every agent that has something waiting, one at a time. Re-checking the
70 *  queue between runs is what carries a card into its next pipeline stage. */
71async function sweep(canvasId: string) {
72  running.add(canvasId)
73  try {
74    for (let i = 0; i < MAX_SWEEP_RUNS; i++) {
75      const next = actions.pendingWorkAgents(canvasId)[0]
76      if (!next) break
77      await runAgent(canvasId, next)
78    }
79  } finally {
80    running.delete(canvasId)
81    if (queued.delete(canvasId)) onFeedback(canvasId)
82  }
83}

enqueueCanvas 才会真正排队 server/resident.ts:77。 失败只记录 warning server/resident.ts:81。 系统 prompt 规定 resident 工作职责 server/resident.ts:85。 pendingWorkAgents 负责发现待处理角色 server/actions.ts:168。 只返回状态为 waiting 的 agent server/actions.ts:183。

02

任务卡片进入队列

Board 是 agent pipeline 的产品入口

用户从 Board 创建卡片,src/components/Board.tsx:119 收集 brief,src/components/Board.tsx:125 通过 API 入队;服务端的 server/actions.ts:569 创建 queued card,server/actions.ts:605 写成 task event,server/actions.ts:616 广播。这个路径让设计需求先成为产品状态,再交给 agent,而不是直接塞进一次模型调用。

server/actions.ts:569用户卡片变成 agent queued work
569export function addQueuedCard(
570  canvasId: string,
571  title: string,
572  from: string,
573  agents?: unknown,
574  attachments?: unknown,
575): AgentTask | undefined {
576  const clean = title.trim().slice(0, 200)
577  if (!clean || !store.getCanvas(canvasId)) return undefined
578  const pipeline = normalizePipeline(agents)
579  /* reference-image frame ids: only frames that actually live on this canvas */
580  const refs = (Array.isArray(attachments) ? attachments : [])
581    .filter((a): a is string => typeof a === 'string')
582    .filter((id, i, arr) => arr.indexOf(id) === i && store.getFrame(id)?.canvasId === canvasId)
583    .slice(0, 4)
584  const list = taskLog.get(canvasId) ?? []
585  const duplicate = list.find(
586    (t) =>
587      t.queuedBy === from &&
588      !t.endedAt &&
589      t.status === clean &&
590      pipelineOf(t).join(',') === pipeline.join(',') &&
591      (t.attachments ?? []).join(',') === refs.join(','),
592  )
593  if (duplicate) return duplicate
594  const card: AgentTask = {
595    id: nanoid(8),
596    agentName: '',
597    color: colorFor(from),
598    status: clean,
599    startedAt: Date.now(),
600    queuedBy: from,
601    pipeline,
602    stage: 0,
603    ...(refs.length > 0 ? { attachments: refs } : {}),
604  }
605  list.unshift(card)
606  if (list.length > 100) list.length = 100
607  taskLog.set(canvasId, list)
608  persist.saveTask(canvasId, card)
609  broadcast(canvasId, { type: 'task', task: card })
610  logActivity(
611    canvasId,
612    resolveActor({ name: from, kind: 'user' }),
613    `queued a card for ${pipeline.map(roleName).join(' → ')}: “${clean}”`,
614  )
615  /* the resident agents pick queued cards up instantly (no-op without a key) */
616  import('./resident.ts').then((r) => r.onFeedback(canvasId)).catch(() => {})
617  return card
618}

Board 按角色聚合 task src/components/Board.tsx:98。 待办数量影响列展示 src/components/Board.tsx:105。 pipeline UI 暴露角色列表 src/components/Board.tsx:209。 角色 selection 进入提交状态 src/components/Board.tsx:224。 resident 为卡片任务生成路由策略 server/resident.ts:216。

03

反馈和评论汇入同一轮

Agent 看到的是合并后的工作包

runAgent 开始后,server/resident.ts:176 领取任务反馈,server/resident.ts:179 领取元素评论,server/resident.ts:181 领取卡片任务,随后 server/resident.ts:186 写入 heartbeat。它把三种输入合并到同一轮推理里,避免“反馈系统”和“卡片系统”变成两个互相抢状态的执行器。

server/resident.ts:170runAgent 合并反馈、评论和卡片任务
170async function runAgent(canvasId: string, agentName: string) {
171  const anthropic = getClient()
172  if (!anthropic) return
173  const role = roleByAgentName(agentName) ?? roleById(DEFAULT_ROLE_ID)!
174  const actor = actions.resolveActor({ name: role.name, kind: 'agent' })
175
176  /* claim this agent's open work — the UI flips to "picked up" instantly.
177     Claiming happens before the try so an agent with nothing to do never
178     shows up in presence. */
179  const claimed = actions.takeFeedbackFor(canvasId, role.name)
180  const comments = actions.takeAgentCommentsFor(canvasId, role.name)
181  const cards = actions.takeQueuedCardsFor(canvasId, role.name)
182  if (claimed.length === 0 && comments.length === 0 && cards.length === 0) return
183
184  /* presence otherwise only refreshes on tool activity, and the sweep's TTL
185     is shorter than a big generation turn */
186  const heartbeat = setInterval(() => actions.heartbeatAgent(canvasId, actor), 15_000)

任务反馈有独立记录 server/actions.ts:259。 agent 可以 take feedback server/actions.ts:300。 反馈完成后被标记 resolved server/actions.ts:326。 元素评论带 selector server/actions.ts:397。 agent 可以领取元素评论 server/actions.ts:430。

04

角色模型

预设角色让队列更像设计团队

虽然角色定义在共享类型里,但实际消费发生在服务端和 Board:server/resident.ts:142 根据角色选择策略,server/resident.ts:226 把路由结果写入上下文,src/components/Board.tsx:209 让用户选择 pipeline。结合 server/actions.ts:546 的 endAgentTasks,这套设计支持“某个角色完成某段工作”的可见过程。

shared/agents.ts:26内置设计团队角色与 pipeline presets
26export const AGENT_ROLES: AgentRole[] = [
27  {
28    id: 'doop',
29    name: 'Doop',
30    emoji: '✦',
31    blurb: 'Generalist designer — makes the thing',
32    brief:
33      'You are the generalist designer. You originate work: new frames, layouts, full redesigns, and any change that does not belong to a specialist. Build the deliverable the card asks for, complete and presentable.',
34    aliases: ['design', 'designer'],
35  },
36  {
37    id: 'ux',
38    name: 'UX Lead',
39    emoji: '⌘',
40    blurb: 'Flow, hierarchy, states, affordances',
41    brief:
42      'You are the UX lead. Judge the design as an interface someone has to use: is the primary action obvious, is the reading order the decision order, are labels unambiguous, are empty/loading/error/disabled states accounted for, are targets and affordances legible as interactive? Fix structure and hierarchy — do not restyle for taste.',
43    aliases: ['usability'],
44  },
45  {
46    id: 'copy',
47    name: 'Copywriter',
48    emoji: '✎',
49    blurb: 'Headlines, microcopy, labels, CTAs',
50    brief:
51      'You are the copywriter. Own every word in the frame: headline, subhead, body, labels, button text, empty states, error messages. Make it specific, human and short — cut filler, kill vague verbs, prefer concrete nouns. Keep the layout intact; if a rewrite changes the line count, keep it inside the existing box.',
52    aliases: ['content', 'words'],
53  },
54  {
55    id: 'brand',
56    name: 'Brand Compliance',
57    emoji: '◈',
58    blurb: 'Palette, type, logo, tone of voice',
59    brief:
60      'You are brand compliance. Check the frame against the brand as it is established on this canvas — take the dominant palette, type family, scale, radii and tone from the other frames and any frame that reads as a brand or style guide. Flag and fix off-palette colours, stray fonts, inconsistent radii or shadows, misused logo treatment, and off-tone voice. Do not redesign; bring the frame into line.',
61    reviewer: true,
62    aliases: ['branding'],
63  },
64  {
65    id: 'a11y',
66    name: 'Accessibility',
67    emoji: '◎',
68    blurb: 'Contrast, semantics, focus, target size',
69    brief:
70      'You are the accessibility reviewer. Check contrast against WCAG AA (4.5:1 body, 3:1 large text and meaningful UI edges), semantic structure (one h1, ordered headings, landmarks, real buttons and labels over styled divs), alt text on meaningful images, visible focus styles, and touch targets of at least 44px. Fix what fails without changing the design intent.',
71    reviewer: true,
72    aliases: ['accessible', 'accessibility'],
73  },
74  {
75    id: 'polish',
76    name: 'Visual Polish',
77    emoji: '✧',
78    blurb: 'Spacing rhythm, alignment, final detail',
79    brief:
80      'You are the polish pass, and you go last. Tighten optical alignment, enforce one spacing scale, even out the type ramp, balance weights and densities, fix ragged edges and orphaned words, and make sure nothing overflows or collides at the frame size. Small, surgical changes only — never restructure.',
81    reviewer: true,
82    aliases: ['polish', 'detail'],
83  },
84]
85
86export const DEFAULT_ROLE_ID = 'doop'
87
88/** Ready-made pipelines for the board's compose box. */
89export const PIPELINE_PRESETS: { id: string; label: string; roles: string[] }[] = [
90  { id: 'solo', label: 'Just Doop', roles: ['doop'] },
91  { id: 'ship', label: 'Design → polish', roles: ['doop', 'polish'] },
92  { id: 'full', label: 'Full pipeline', roles: ['doop', 'copy', 'brand', 'a11y', 'polish'] },
93  { id: 'audit', label: 'Review only', roles: ['ux', 'brand', 'a11y'] },
94]

任务可以按 agent role 结束 server/actions.ts:546。 结束事件会进入 log server/actions.ts:556。 结束后继续广播 server/actions.ts:562。 任务状态进入前端 store src/lib/store.ts:123。 反馈状态也在同一 store src/lib/store.ts:150。