Buddy 应用(专家包)技术内幕
这不是一份「点这里、填那个」的操作手册。它要回答的是:一个 Buddy 应用(专家 / 专家团)在磁盘上是什么、在进程里变成什么、以及中间那段代码在干什么。 读完你应当能独立于任何 UI,用文本编辑器从零造出一个能在 WorkBuddy 里真正跑起来的专家。
本文所有结论均来自对本机 WorkBuddy 安装包的实证取证 —— 包括逆向主进程 app.asar、命令行内核
codebuddy-lite-wb.mjs、创作脚本 expert-manager/scripts/*.py,以及对 16 个真实落盘专家包的统计。
凡与官方文档口径不一致处,均以代码证据为准,并在文中明确标出。
0 阅读前提与取证方法
在往下读之前,先交代本文的三条取证路径,方便你自己复核:
resources/plugins/workbuddy-builtin/skills/expert-manager/含
SKILL.md + 4 份 references + 7 个 Python 脚本。这是「应该长什么样」的权威来源。
app.asar(96 MB,未解包,需按字节偏移取证);
插件 / 智能体内核在 cli/dist/codebuddy-lite-wb.mjs(9.5 MB)。这是「实际怎么跑」的来源。
~/.workbuddy/plugins/marketplaces/experts/plugins/ 下 16 个真实专家包,
共 45 个 Agent MD。这是「别人怎么写的」的来源。
WorkBuddy 桌面端是 Electron 应用,业务代码经过打包压缩。app.asar 未解包且单行可达数十万字符,
无法按行号引用,只能按字节偏移定位。文中涉及主进程的代码片段均标注偏移值,
CLI 侧则可按行号(文件共 2678 行)+ 字符位置引用。
凡文中出现的机制描述,均可追溯到具体文件与片段。查不到证据的,明确标注「未找到证据」,不做脑补。 第 11 节专门罗列了官方规范与代码实现相矛盾的 12 处,这些是踩坑高发区。
1 它到底是什么
1.1 四个容易混淆的概念
WorkBuddy 里有四套可插拔机制,它们共享「Markdown 定义 + 目录扫描」这一思路,但运行时待遇完全不同。 分不清这四者,是绝大多数困惑的源头。
| 概念 | 物理位置 | 运行时载体 | 装载时机 | 能否被 spawn |
|---|---|---|---|---|
| Skill 技能 |
~/.workbuddy/skills/*/或插件内 skills/ |
指令文本注入上下文 (含 scripts/ 可执行脚本) |
按需 命中描述才加载 | 否 是工具箱不是角色 |
| Custom Agent 用户级智能体 |
~/.workbuddy/agents/*.md或项目 .workbuddy/agents/ |
独立 agent 配置 | 启动时 全部注册 | 是 |
| Expert 专家 / Buddy |
插件包 <expert>/agents/*.md |
插件组件 → 装到默认主 agent 上 | 选中时 动态切换 | 是(作为主会话人格) |
| Team 专家团 |
expertType: "team" 的专家包 |
主理人 agent + N 个团员 agent | 选中时 另开 Agent Teams 开关 | 是(成员被逐个 spawn) |
两者都能产出「可 spawn 的智能体」,但机制是两套独立实现:
- 用户级 Agent 走
CustomAgentsProductProvider,直接扫<home>/agents目录,不读 plugin.json、不带 PLUGIN_TAG。 - 专家包走
PluginsProductProvider→AgentExtensionLoader,必须依附于一个插件包,带PLUGIN_TAG,且受 marketplace 生命周期管理(可下载、可更新、可切换)。
所以「专家」的附加价值不在 MD 本身,而在它是一等公民插件:有版本、有市场、能分发、能打命名空间。
1.2 一句话总纲
专家包 = 一个标准的 WorkBuddy 插件,只是额外带了一个 expertType 标记,并且把其中一个 Agent MD 提升为会话的默认人格。
理解这一句,后面所有机制都是它的推论:为什么要注册进 marketplace(因为是插件)、
为什么 bin/ 会进 PATH(因为是插件)、为什么技能有命名空间前缀(因为是插件)、
为什么 team 需要开关(因为是插件之上的额外标记)。
把这句话展开成三层管线,就是本文的骨架:
对于本地自建的专家,PHASE 3(分发)是完全跳过的 —— 你的目录就在
my-experts/plugins/ 里躺着,没有下载、没有解压、没有 .downloaded_at。
直接从 PHASE 2 跳到 PHASE 4。这也是为什么本地专家改完立刻生效。
2 包体解剖:磁盘上长什么样
2.1 目录树
自建专家必须放在 $WORKBUDDY_CONFIG_DIR/plugins/marketplaces/my-experts/plugins/<name>/
(默认即 ~/.workbuddy/plugins/marketplaces/my-experts/plugins/)。
放在别处,扫描器根本不会去看它。init_expert.py 里对此做了硬检查,传错 --path 直接退出:
expected_path = get_expert_plugins_dir()
resolved_path = Path(output_path).expanduser().resolve()
if resolved_path != expected_path.resolve():
print(f"❌ Error: --path 必须为专家目录: {expected_path}")
print(f" 非专家目录下创建的专家无法被检测到。")
sys.exit(1)
2.2 plugin.json 字段全解
这是整个包的中枢。下面按运行时实际如何使用来分类,而非按文档顺序 —— 标注为「展示态」的字段不参与运行时行为。
身份与类型(运行时强依赖)
| 字段 | 必填 | 作用与运行时待遇 |
|---|---|---|
name | 必需 | 包唯一标识,kebab-case 正则 ^[a-z0-9][a-z0-9-]*[a-z0-9]$,至少 2 字符。用于拼 "<name>@<marketplace>" 这个插件 ID。 |
version | 必需 | 语义化版本。官方包用它比对本地落盘版本与 marketplace 条目版本,不一致则重新 materialize。 |
description | 必需 | 英文一句话。它是 marketplace.json 条目的 description,也是卡片兜底文案。 |
expertType | 必需 | agent 或 team。这是唯一的语义开关:决定卡片显示「专家」还是「专家团」,决定主进程是否注入 CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS=1 与 X-Expert-Team-Task 头。 |
agentName | 必需 | 主 Agent 名,必须等于 agents/ 下某个 MD 的文件名(不含 .md)。激活时它随 /plugins/switch 请求送给 CLI。禁止用通用名 team-lead。 |
teamInfo | team 必需 | {leadAgent, memberAgents[]}。运行时只认 leadAgent;memberAgents 在整个 app.asar 中出现 0 次,是纯文档/市场字段(详见 §11 第 4 条)。 |
资源声明(决定加载什么)
| 字段 | 必填 | 作用与运行时待遇 |
|---|---|---|
agents | 可选 | 路径数组,如 ["./agents/x.md"]。可以不写 —— 加载器读完此字段后还会兜底扫描 <plugin>/agents/ 整个目录。实证:官方 software-company 包就没有这个字段,照样加载 5 个成员。 |
skills | 可选 | 技能目录数组。同理,加载器也会兜底扫 <plugin>/skills/。 |
bin | 死字段 | 规范里有,扫描器会归一化,但 CLI 侧无任何消费者。bin/ 目录本身是约定自动探测的,不需要也不受此字段影响。 |
展示态字段(不参与运行时行为)
| 字段 | 约束 | 说明 |
|---|---|---|
displayName | 必需 | {en, zh}。卡片标题(花名)。Team 型必须与 profession 完全一致,校验器直接判 error。 |
profession | 必需 | {en, zh}。职业头衔。 |
displayDescription | 40-50 字 | {en, zh}。中文超出 40–50 字区间校验器只给 warning,不阻断。 |
categoryId | 必需 | 12 个分类之一;但实际还存在第 13 个 13-TencentZone(见 §14)。 |
avatar | 可选 | 相对路径。会被解析成 file:// URL,超过 5 MB 丢弃。注意:官方落盘包普遍不含 avatars 目录(16 个包中 5 个无头像),说明市场包走远程 URL 分发。 |
defaultInitPrompt | 必需 | {en, zh}。必须与 quickPrompts[0] 一致(不一致只 warning)。 |
tags | 恰好 3 | 擅长领域标签。多于或少于 3 个 = error。 |
quickPrompts | 恰好 3 | 推荐提示词。渲染层会用 quickPrompts[0] 覆盖 defaultInitPrompt,所以两者写不一样时,界面上显示的是前者。 |
plugin | 必需 | 必须等于 name。不一致 = error。 |
members[] | team 必需 | 每个含 {id, name, profession, avatar, role},role 取 lead/member,主理人也必须在内且 role: "lead"。仅用于渲染头像墙,运行时不消费。 |
2.3 Agent MD 的格式
每个 agents/*.md 文件就是一个智能体定义。结构极简:YAML frontmatter 管元数据,正文就是 system prompt。
---
name: code-reviewer # 【必需】必须等于文件名(不含 .md)
description: Reviews code for bugs, security and performance issues.
displayName:
en: "Code Review Expert"
zh: "代码审查专家"
profession:
en: "Senior Code Reviewer"
zh: "资深代码审查师"
maxTurns: 50 # 可选,正整数
skills: [ast-scan] # 可选,预加载的技能
---
# 代码审查专家 - 审稿人
你是一名资深代码审查师……(这段就是 system prompt)
## 核心能力
1. **缺陷识别**:……
2. **安全审计**:……
## 工作流程
1. 先通读 diff 全貌
2. 逐文件审查
3. 汇总输出
## 输出规范
- 按严重程度分级
- 每条给出行号与修改建议
## 注意事项
- 不做主观风格争论
frontmatter 字段的真实待遇
| 字段 | 是否生效 | 运行时处理 |
|---|---|---|
name | 生效 | 成为 subagent_type 的名字。缺失时回退用文件名(去掉 .md)。 |
description | 生效 | 缺失时回退用 name。加载器还会自动追加来源标签,例如 "... (my-experts:agents)",用于在模型面前区分同名 agent。 |
instructions(正文) | 生效 | 核心:整段正文就是 system prompt。且会做变量替换(见下)。 |
tools | 禁用 | 规范明令禁止在 frontmatter 声明。运行时虽会解析,但工具权限由系统统一分配。 |
maxTurns | 生效 | 必须正整数,否则丢弃。主理人建议 150–200(因为要编排多轮),普通成员 50。 |
skills | 生效 | 预加载技能,解析时会尝试加插件名前缀(见 §8)。 |
effort | 生效 | 推理强度,经 normalizeReasoningEffort 归一化。 |
background | 生效 | 布尔值,是否后台运行。 |
isolation | 生效 | 只有 "worktree" 值被接受,其它一律丢弃。 |
initialPrompt | 生效 | 智能体启动后的首条输入。 |
memory | 生效 | 记忆配置。 |
color | 生效 | 缺失时用 generateColorFromName(name) 按名字生成。 |
permissionMode | 忽略 | 这三个字段即使写了也被静默忽略并打 warning:[AgentExtensionLoader] plugin agent 'x' sets permissionMode, which is ignored for plugin agents.这是有意设计 —— 插件不应越权改宿主权限模型。 |
hooks | 忽略 | |
mcpServers | 忽略 |
正文里的 ${CODEBUDDY_PLUGIN_ROOT}(以及兼容别名 ${CLAUDE_PLUGIN_ROOT})会被替换成插件根目录的绝对路径。
这是让 MD 能引用包内资源的关键 —— 比如 请读取 ${CODEBUDDY_PLUGIN_ROOT}/references/rules.md。
没有它,智能体无法定位自己的包在哪。
2.4 Team 型的额外约定
{team}-team-lead.md。
用通用的 team-lead.md 会被校验器判 error —— 因为同名会跨包冲突。
members[] 含主理人(role: "lead"),但 teamInfo.memberAgents 不含主理人。校验器会检查「members 里至少有一个 lead」以及「memberAgents 不含 lead」。
{"agent": "{team}-team-lead"}。
但官方落盘包里一个都不存在(见 §11 第 5 条),它本质是过渡产物。
3 创建流水线:七个脚本
创作期由 expert-manager 这个技能驱动,共 5 个 Python 脚本(外加 2 个测试脚本)串成流水线。
它们全部是纯 stdlib 实现,无第三方依赖。
resources/plugins/workbuddy-builtin/skills/expert-manager/
├── SKILL.md 179 行 # 流程与铁律
├── references/
│ ├── plugin-json-spec.md 184 行 # plugin.json 字段规范 + 两套模板
│ ├── agent-md-spec.md 179 行 # Agent MD 结构(普通 / 主理人)
│ ├── team-spec.md 120 行 # 协作铁律、命名、SOP 编排
│ └── avatar-spec.md 162 行 # 头像 prompt 构建策略
└── scripts/
├── init_expert.py 485 行 # ① 生成骨架
├── validate_expert.py 381 行 # ② 校验
├── register_expert.py 271 行 # ③ 注册
├── package_expert.py 121 行 # ④ 打包
├── batch_create.py 141 行 # 批量编排(模板)
├── test_register_expert.py 78 行
└── test_package_expert.py 95 行
-
init_expert.py — 生成骨架
按
--type agent|team生成目录 + 带[TODO]占位符的plugin.json与 MD 模板。 Agent 型产出 1 个 MD;Team 型产出{team}-team-lead.md+ 一个member-placeholder.md(需自行改名)+settings.json。 同时校验--path必须是专家目录、name必须是 kebab-case。 -
填充内容
把
[TODO]换成真实内容。这一步由 AI 或人完成,是唯一有创造力含量的环节。 正文质量直接决定专家质量 —— 因为正文就是 system prompt,没有中间层。 -
ImageGen — 生成头像
按
avatar-spec.md从 MD 内容反推 prompt:读角色定义 → 提核心能力关键词 → 转视觉元素 → 拼装。 Agent 型 1 张;Team 型 N+1 张(团队整体 + 主理人 + 每个团员)。同团队共用风格锚定词与背景色调。 -
validate_expert.py — 校验
会做五大类检查:位置(必须在 my-experts 目录下)、结构(禁止
hooks/、commands/、.lsp.json,禁止资源目录藏在.codebuddy-plugin/内)、 字段(必填项、kebab-case、tags/quickPrompts 恰好 3 个、team 的 profession ≡ displayName)、 一致性(agentName有对应 MD、frontmattername≡ 文件名)、 禁用项(frontmatter 不得有tools)。 -
register_expert.py — 注册
二次校验(重点查
[TODO]残留)后,向marketplace.json写入条目{"name": ..., "source": "./plugins/<dir>", "description": ...}, 并向专家目录写入.created-by-session标记文件(值取自环境变量CODEBUDDY_SESSION_ID)。 -
package_expert.py — 打包
先跑一遍完整校验,通过后打 zip。会自动跳过隐藏文件(
.codebuddy-plugin除外)、__pycache__、node_modules、.gitkeep、.DS_Store。 -
批量(可选)
batch_create.py提供模板:对每个专家串行执行 init → 填充 → validate → register, 任一步失败即停止。规范明令禁止「只写文件跳过校验注册」和「直接改 marketplace.json」。
第 ⑤ 步不是可选项。扫描器以 marketplace.json 为唯一入口,文件缺失直接返回空列表;
即使目录和 plugin.json 都完好,只要 marketplace.json 里没条目,专家在界面上就不存在。
跳过这一步直接手搓文件,是最常见的「我明明写了为什么看不见」的原因。
3.1 注册脚本的关键片段
def register_expert(expert_dir, marketplace_dir, session_id=None):
manifest_path = marketplace_dir / '.codebuddy-plugin' / 'marketplace.json'
...
source = f"./plugins/{expert_dir.name}"
# 已存在则更新,否则追加 —— 保证幂等
for p in plugins:
if p.get('source') == source or p.get('name') == expert_name:
p['name'] = expert_name
p['source'] = source
p['description'] = expert_description
...
return True
plugins.append({"name": expert_name,
"source": source,
"description": expert_description})
if session_id:
_write_session_marker(expert_dir, session_id) # 写 .created-by-session
它是专家目录下的一个独立文件,内容就是会话 ID 字符串。
用途是「预览态」状态机:主进程在会话结束后会把它 rename 成 .pending-preview-session 或直接删除。
它不参与「我的专家 vs 市场专家」的判定 —— 那个判定靠 marketplace === 'my-experts'。
4 分发与版本:市场包如何落到磁盘
本地自建专家跳过本节。但如果你要分发,或者想理解官方专家的行为,这一节解释它们从哪来。
4.1 两套落盘位置
~/.workbuddy/plugins/marketplaces/experts/plugins/<bundleName>/扁平结构,无 version 层级
~/.workbuddy/plugins/marketplaces/my-experts/plugins/<dirName>/~/.workbuddy/app/cache/experts/manifest.json(1.2 MB 目录清单)~/.workbuddy/app/cache/experts/expert-bundle-map.json
网上(以及早期版本)流传的缓存路径是 plugins/cache/experts/<name>/<version>/。
这个目录确实存在(本机有 4 个包),但它不是运行时主路径。
真正被扫描和使用的是 marketplaces/experts/plugins/<bundleName>/ —— 无版本层级,
一个包一份,更新时原地覆盖。想找当前生效的官方包,去后者。
4.2 下载与新鲜度判定
async ensureDownloaded(e, t, n){
let r = this.getPluginDir(e);
if (await this.deps.hasPluginManifestAsync(r)){
if (t && await this.isLocalStale(r, t))
console.log(`...Local cache stale, re-downloading: ${e}`);
else return await this.logBundleSnapshot(`cache`, e, r), r
}
let o = this.downloadAndExtract(e, r, n);
...
}
async isLocalStale(e, t){
let n = x.join(e, `.downloaded_at`),
r = await O.readFile(n, `utf-8`),
i = new Date(r.trim()).getTime();
return new Date(t).getTime() > i // 服务端 updatedAt > 本地时间戳 → 过期
}
流程要点:
- 命中缓存且未过期 → 直接用;过期 → 重新下载。
- 下载 → 先备份改名 → 解压(支持 tar/zip,含 zip-slip 路径穿越防护)→
normalizeExtractedPluginRoot(若清单在子目录则提升一层)→ 校验 plugin.json 存在 → 删除备份 → 写.downloaded_at(ISO 时间戳)。 - 下载失败且有旧缓存 → 告警并回落使用陈旧缓存,不阻断使用。
- URL 选择:企业网关(
operation-platform,需通过isTrustedDownloadUrl校验)优先,失败回落 COS/bundles/<name>.tar.gz。
expert-bundle-map.json 维护市场 ID 到目录名的映射,且可以多对一:
{
"ex_XoPVlmyOgAY9": "fullstack-dev",
"SoftwareCompany": "software-company",
"ex_jPdPfIC04OKD": "software-company", // ← 两个市场 ID 指向同一目录
"ex_NsJv0iGhmXQh": "up-stock-strategy-analyze"
}
说明市场侧存在 ID 迁移/别名机制,落盘后统一收敛到目录名。
5 激活:点击专家的那一瞬间发生了什么
这是全链路最精妙的一环。很多人以为「选中专家」只是记了个 UI 状态,实际上它触发了一次 跨进程的配置写入 + HTTP 调用。整个过程分四步。
5.1 第一步:主进程准备事实
点击后,主进程的 createConversationExpertRuntimePreparer 开始工作(app.asar 偏移 ~76308100):
async function readPluginManifestFromDir(e){
for (let t of Je){ // Je = [".codebuddy-plugin", ".workbuddy-plugin", ...]
let n = T.join(e, t, Ye); // Ye = "plugin.json"
try{
let e = await E.readFile(n, `utf8`), t = JSON.parse(e);
return {name:..., id:..., agentName:...,
expertType: optionalExpertType(t.expertType), // ← 类型在此被读取
marketExpertId:..., sourceId:..., displayName:t.displayName}
}catch(e){ if(e.code === `ENOENT`) continue; return }
}
}
主进程从这里只拿 name / id / agentName / expertType / marketExpertId / sourceId / displayName,
categoryId、tags、quickPrompts、members 都不进这条记录。
它们只在面向 UI 的完整 DTO 构造(readCustomExpertFromDir)里才被读取。
这是两条独立的数据流:精简流走运行时,完整流走渲染。
随后是落盘状态机,它定义了包的三态:
if (!a || !o) return { status: `incomplete-names` }; // 缺 name 或 agentName
return await hasMarketplaceBundleEntry(e, n)
? { status: `complete`, candidate: s } // 目录在 + 已注册
: { status: `needs-register`, candidate: s }; // 目录在 + 未注册
同时,针对 team 型,主进程会先解析出主理人 MD 的路径用于提示词预览:
// resolveExpertAgentPromptSource
let o = a.expertType === `team`
? a.teamInfo?.leadAgent // team → 直接用 leadAgent
: await this.resolveExpertAgentName(i, a, e); // agent → 回退链
let s = await resolveExpertAgentMarkdownPath(i, o);
return s ? {agentName: o, filePath: s} : void 0
// resolveExpertAgentName 的回退链
async resolveExpertAgentName(e, t, n){
if (t.agentName) return t.agentName; // ① plugin.json
if (t.expertType === `team` && t.teamInfo?.leadAgent) return t.teamInfo.leadAgent; // ②
let r = this.toKebabCase(n); return await this.resolveAgentNameFromPluginFiles(e, t, r) // ③ 猜
}
注意区分:主进程读 MD 只是为了在界面上给用户看「这个专家的开场白/人设」
(resolvePreparedExpertReminder)。
真正把 MD 内容装进模型的是后面 CLI 那条链路。两条链路各自独立读盘,互不共享。
5.2 第二步:写入会话配置
function applyPreparedExpertSettings(e, t, n){
let r = e ?? {},
i = asRecord(r.enabledPlugins),
a = asRecord(r.extraKnownMarketplaces),
o = asRecord(r.env),
s = ...(o.CODEBUDDY_CUSTOM_HEADERS),
c = [`X-Expert-Id: ${t.canonical.id}`,
...t.expertType === `team` ? [`X-Expert-Team-Task: true`] : []],
l = t.canonical.marketplace;
return {...r,
enabledPlugins: d.w(i, `${t.pluginName}@${l}`), // 启用 @
extraKnownMarketplaces: {...a, [l]: {source: {source: `directory`,
url: n.pluginService.getExpertMarketplacePath(l)}}},
env: {...o, CODEBUDDY_CUSTOM_HEADERS: [s, ...c].filter(Boolean).join(`\n`)}}
}
三件事:
- 启用插件:把
"<pluginName>@<marketplace>"加入enabledPlugins。这就是插件 ID 的形式,也是后面 CLI 认它的凭据。 - 声明额外市场:把该 marketplace 的目录路径告诉 CLI,让它知道去哪找包。
- 注入 HTTP 头:
X-Expert-Id标识当前专家,team 额外加X-Expert-Team-Task: true(供后端识别团队任务)。
5.3 第三步:HTTP 切换(真正的激活)
let _ = `${s}/api/v1/plugins/switch`, v = {persist: !1};
g.length > 0 && (v.disable = g[0]); // 关掉前一个
n && (v.enable = `${n}@${d}`); // 启用这个包
r && (v.agentName = r); // ← 主理人 / 主 agent 名
let y = n ? `${n}@${d}` : ...;
y && (v.sourcePluginId = y);
a !== void 0 && (v.internalModelRequestHeaders = a);
await fetch(_, {method: `POST`, body: JSON.stringify(v)})
CLI 侧对应两个入口:
async setVolatilePluginAgent(t, i = {}){
let n = this.normalizePluginAgentSetting(t), ...
this.volatilePluginAgentName = n;
this.volatilePluginSourcePluginId = s;
this.volatilePluginInternalModelRequestHeaders = c;
...
}
请求体里 persist: false 是关键:这次切换只作用于当前会话,不落盘。
这解释了一个常见现象:你在 A 会话选了专家,开个新会话又是默认助手。
专家是会话级的,不像技能那样全局常驻。
5.4 第四步:settings.json 的真相
规范说 team 型必须有 settings.json(内容 {"agent": "xxx-team-lead"})。CLI 确实读它,但待遇微妙:
let L = t.settings;
try{
let t = ed.join(i, "settings.json");
await ef.Sn.access(t);
let n = JSON.parse(await ef.Sn.readFile(t, "utf-8")),
s = {...n.agent ? {agent: n.agent} : {},
...void 0 !== n.subagentStatusLine
? {subagentStatusLine: n.subagentStatusLine} : {}}; // ← 只认这 2 个 key
Object.keys(s).length > 0 && (L = {...L, ...s})
}catch{}
return {..., settings: L}
主进程启动时全盘扫描 plugins/*/settings.json,把这种「只有一个 agent key 的生成物」
视为遗留产物并删除(readGeneratedAgentOverride + removePersistedGeneratedAgentOverrides,
app.asar 偏移 ~84212259)。
这也解释了本机实测:16 个官方落盘包里,settings.json 一个都不存在。
结论:settings.json 是创作期的合规要求,运行时的过渡产物。
真正决定激活的是 volatile agentName(走 HTTP 切换)。
你写它是为了过校验,不必指望它生效。
6 核心:Markdown 如何变成活的智能体
这是整个机制的心脏。前面所有流程都是为这一刻铺路:把 agents/*.md 这个纯文本文件,
变成模型可以 Agent({subagent_type: "xxx"}) 调度的真实智能体。
6.1 加载器:声明 + 兜底双通道
async loadPluginComponents(t, i){
h.push(this.agentLoader.load(i, n, this.normalizePathConfig(t.agents), p) ...)
h.push(this.skillLoader.load(i, n, this.normalizePathConfig(t.skills), p) ...)
...
}
// normalizePathConfig:string → [string];Array → filter(string);否则 undefined
进入 AgentExtensionLoader 后才是重点:
async load(t, i, n, s){ // t=插件根目录, i="name@marketplace", n=agents路径列表
if (n && n.length > 0)
c.push(...await this.loadFromPaths(t, i, n, p, h)); // ① 按 plugin.json 声明加载
let s = ed.join(t, tK); // tK = "agents"
if (await this.pathExists(s) && !p.has(s))
c.push(...await this.scanAgentsDirectory(s, i, p, t, h)); // ② 兜底扫整个目录
}
本机 16 个官方包中,software-company 的 plugin.json 完全没有 agents 字段,
但它的 agents/ 目录里有 5 个 MD,运行时照常加载全部成员。
对工程师的意义:你可以不写 agents 声明,只管往 agents/ 里扔 MD。
但反过来,声明了却不存在的路径会报 error。所以最省心的做法是——声明与目录保持一致,
或者干脆只放目录。
6.2 解析:frontmatter → 配置对象
let n = await eu.MarkdownUtils.extractFrontMatterFull(i); // 解析 YAML frontmatter
n.parseError && this.logger.warn(`[AgentLoader] Malformed YAML frontmatter ...`);
let E = n.data,
R = ed.basename(A).replace(/\.md$/, ""),
P = E.name || R, // name ← frontmatter,缺失回退文件名
I = this.generateSourceLabel(A, S),
O = E.description || P;
O.includes(I) || (O = `${O} ${I}`); // description 自动追加来源标签
let M = eu.MarkdownUtils.parseListField(E.tools),
L = ...parseListField(E.disallowedTools),
$ = ...parseListField(E.skills),
F = normalizeReasoningEffort(E.effort),
U = "worktree" === E.isolation ? E.isolation : void 0,
B = eu.MarkdownUtils.parsePositiveIntField(E.maxTurns), // 非正整数 → 丢弃
W = eu.MarkdownUtils.parseBooleanField(E.background),
q = E.initialPrompt, G = tG.Ic(E.memory), z = E.model?.trim();
for (let t of ["permissionMode", "hooks", "mcpServers"]){ // 这三个被忽略
...this.logger.warn(`[AgentExtensionLoader] plugin agent '${P}' sets ${t},
which is ignored for plugin agents.`)
}
let J = n.content.trim(), // ← MD 正文
Y = c ? await tV.C(J, p, V, c) : tV.r(J, p, V); // ← 变量替换
return {
name: P, description: O, instructions: Y, // ★ 正文 → instructions(system prompt)
...N && {tools: N}, ...L.length && {disallowedTools: L},
models: z ? [z] : [],
asTool: !0, // ★★ 关键开关
color: E.color || generateColorFromName(P),
tags: [ej.UxB, ej.HOt, tz.PLUGIN_TAG, S], // ★ S = "pluginName@marketplace"
...F && {effort: F}, ...void 0 !== B && {maxTurns: B}, ...W && {background: W},
...q && {initialPrompt: q}, ...$.length && {skills: $},
...G && {memory: G}, ...U && {isolation: U}
}
变量替换紧跟其后:
E = E.replace(/\$\{CODEBUDDY_PLUGIN_ROOT\}/g, t)
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, t)
6.3 三个关键产物
这既是它的力量(完全可控),也是它的陷阱(废话会消耗 context 且稀释指令)。
true,不可配置。这是该 agent 能否出现在
Agent 工具的可派生清单里的唯一开关。
tags 数组第 4 项(末位)是 "<pluginName>@<marketplace>"。
它是技能命名空间解析的依据(见 §8),也是运行时识别来源的标记。
6.4 注册到 Agent 工具:可 spawn 的秘密
配置对象造出来后,还要过三关才能真正被模型调用。
let t = await this.pluginManager.getActivePlugins(); ...
for (let {components: i} of t){
let {settings: t, ...n} = i; // ← settings 被剥离,其余并入
s = await this.productMerger.merge(s, {...n, mergeStrategy: SmartMerge})
}
let c = s?.components.agents ?? [];
for (let t of c){
let s = t?.name;
if (s && !this.agentConfigMap.has(s)){ // ← 先到先得,重名不覆盖
this.agentConfigMap.set(s, n);
i.push(n)
}
}
A = [
"Launch a detached worker...",
"",
"Available agent types:",
...q.filter(t => !0 === t.asTool || t.name === nI.QYL.GENERAL_PURPOSE)
.map(t => `- ${t.name}${i ? `: ${i}` : ""}${n}`),
...
].join("\n")
清单超长时会被 truncateAgentsByCharBudget(line 1854)截断。
这解释了为什么插件开太多时,某些 agent 会从清单里消失 —— 不是没加载,是被预算裁掉了。
至此,模型看到的 Agent 工具描述里就有了 - security-reviewer: 安全审计专家 这一行,
于是它能发出 Agent({subagent_type: "security-reviewer", name: "security-reviewer", prompt: "..."})。
Team 型规范里反复强调「调度成员时 name 与 subagent_type 都要传 MD 文件名」,
根因就在这里:Agent 工具的清单是按 name 字段索引的,
传中文名或自创名会匹配不到,系统只能退化处理(可能生成无意义名称或退化成 general-purpose)。
7 Team 的运行时保障:只有两个,都是硬编码
如果你以为专家团有什么精巧的编排引擎,会失望。运行时的全部贡献只有两点。
let l = `1`, u = `0`, d = `team`;
function resolveAgentTeamsEnv(e = {}){
return e.expertType === d ? l : e.disableAgentTeams ?? !0 ? u : l
}
var f = `CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS`;
CLI 侧判定:isEnabled(){ return "0" !== process.env.CODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS }
即 expertType === "team" → 环境变量置 1 → TeamCreate / Agent / SendMessage 三个工具可用。
X-Expert-Team-Task: true,随请求发往后端。
用途是让服务端知道当前会话是团队任务(可能用于计量、路由或后端侧的差异化处理), 不影响本地编排行为。
代码证据:
memberAgents在整个app.asar(96 MB)中出现 0 次。唯一被读的是teamInfo.leadAgent。expertType在 CLI(9.5 MB)中出现 0 次。CLI 只认标准插件组件。members[]只被归一化(id从agent字段回填、avatar 转 URL)后透传给渲染层做头像墙,运行时不消费。
所以「4 条正则 + 5 条红线 + Phase 编排」全部是写在主理人 MD 正文里的自然语言约定, 靠模型的指令遵循能力实现。这就是官方文档那句 代码 = SOP(团队) 的真实含义 —— 它是 prompt,不是 code。
7.1 这带来什么工程后果
| 你写下的东西 | 谁来保证 | 失效时会怎样 |
|---|---|---|
| 必须先 TeamCreate | 仅 prompt | 模型可能跳过建团直接开写,输出变成「主理人一人分饰多角」的单人独白 |
| 成员产出不得代写 | 仅 prompt | 模型为了「高效」直接替团员写出 PRD/架构/代码,团队形同虚设 |
| 信息经主理人中转 | 仅 prompt | 成员间直连或被跳过,上下文断裂,产出对不上 |
| 阶段顺序 | 仅 prompt | 跳阶段推进,下游拿到不完整输入 |
| 阶段完成要通报用户 | 仅 prompt | 长时间静默,用户不知道进度 |
| TeamCreate/Agent/SendMessage 工具存在 | 代码保障 | 这是唯一硬保障:环境变量置 1 后工具才出现在工具清单里 |
既然没有代码强制,那让团队真的跑起来的唯一手段就是把约束写得足够具体、足够强、足够不容绕过。
这也是为什么官方规范要塞进那么多 ❌ 禁止… 条目,且要求把「Agent ID 清单」逐条列明 ——
这是在用 prompt 长度和具体性对抗模型的偷懒倾向。
写团队时删掉这些约束 = 团队退化。
8 技能、脚本与命名空间
8.1 技能:同样有声明 + 兜底双通道
async load(t, i, n, s){
...
let s = ed.join(t, rr); // rr = "skills"
if (await this.pathExists(s) && !p.has(s))
n = await this.scanSkillsDirectory(s, i, p, void 0, 0, t, h); // 兜底扫目录
let S = ed.join(t, "SKILL.md"); // 单文件技能形式
if (!p.has(S) && await this.pathExists(S))
n = await this.parseSkillFile(S, i, void 0, t, h);
}
8.2 命名空间前缀:一个容易搞错的机制
官方文档说 plugin.json 的 name 是「技能命名空间前缀」。这话在效果上对,但机制不是那样。
function relativeSkillDirSegments(t, i, n = s){
return i ? n.relative(i, t).split(n.sep).filter(Boolean) : []
}
function deriveSkillNameFromPath(t, i, n = s){
let c = n.dirname(t);
return relativeSkillDirSegments(c, i, n).join(":") || n.basename(c)
}
技能名是相对插件根目录的路径段用 : 连接(如 skills:ast-scan)。而消费端解析时:
resolveSkillNameForPreload(t, i, n){
let s = i.find(i => i.name === t); if (s) return s; // ① 原名直查
if (!t.includes(":") && n?.tags?.includes(ey.PLUGIN_TAG)){
let s = n.tags[n.tags.length - 1]; // ★ 取 tags 末位 = "pluginName@marketplace"
if (s && s !== ey.PLUGIN_TAG){
let n = s.split("@")[0], c = `${n}:${t}`; // ② 拼前缀
let p = i.find(t => t.name === c); if (p) return p
}
}
if (!t.includes(":")){ // ③ 唯一后缀匹配
let s = i.filter(i => i.name.endsWith(`:${t}`));
if (1 === s.length) return s[0];
...
}
}
当你在 Agent MD 的 frontmatter 写 skills: [ast-scan] 时,解析顺序是:
① 直接找名为 ast-scan 的 → ② 找 <插件名>:ast-scan → ③ 唯一后缀匹配。
所以前缀来自 agent 的 tags 末位,而非从 plugin.json 现读。
两者通常相等(因为规范强制 plugin 字段 = name),但机制独立。
工程含义:技能名在包内最好全局唯一,否则第 ③ 步的「唯一后缀匹配」会失效。
8.3 bin/:约定优于配置
async getPluginBinPaths(){
let t = await this.getActivePlugins(), i = [];
for (let {plugin: n} of t)
if (n.installedPath){
let t = ed.join(n.installedPath, "bin"),
s = await ec.stat(t).catch(() => void 0);
s?.isDirectory() && i.push(t) // ← 目录存在就收,不看 plugin.json
}
return i
}
function buildPluginPathEnv(t){
let i = process.env.PATH || "";
if ("win32" !== process.platform)
return [...t, i].filter(Boolean).join(":")
...
}
async function getBashPluginEnv(t){
let i = [];
if (t) try{ i.push(...await t.getPluginBinPaths()) }catch{}
i.push(...(process.env.WORKBUDDY_EXTRA_PATHS ?? "").split(...).filter(Boolean));
return i.length ? {PATH: buildPluginPathEnv(i)} : void 0
}
bin/ 是纯约定目录自动探测,与 agents/skills 的「声明 + 兜底」不同。
只要插件根下有 bin/ 目录,它就会被前置到 Bash 工具的 PATH。
而 plugin.json 里的 bin 字段虽然会被扫描器归一化,CLI 侧无任何消费者 —— 写了也没用。
注意 PATH 是前置的,意味着包内脚本可以遮蔽系统同名命令。命名要有前缀意识。
9 端到端时序图
把前面所有环节串成一条完整链路。建议对照这条链路来定位问题。
【渲染层】用户点选专家
│ contexts-BNN7VV-R.js: setSelectedExpertForSession({expertId, expertMarketplace, runtimeIdentity})
▼
【主进程】createConversationExpertRuntimePreparer app.asar ~76308000
├─ resolveExpertLocation → 定位包目录
├─ activateExpert / ensureDownloaded
│ └─ 缓存命中? → 检查 .downloaded_at 新鲜度 → 过期则重下
├─ resolveExpertAgentMarkdownPath(<root>/agents/<leadAgent|agentName>.md)
└─ assemblePreparedFacts → {pluginName, agentName, expertType, promptText}
▼
【主进程】applyPreparedExpertSettings app.asar ~76323000
├─ sessionSettings.enabledPlugins["<plugin>@<marketplace>"]
├─ extraKnownMarketplaces[marketplace] = {source: directory, url: ...}
└─ env.CODEBUDDY_CUSTOM_HEADERS += "X-Expert-Id: ..."
+= "X-Expert-Team-Task: true" (仅 team)
▼
【主进程】ExpertPluginActivation app.asar ~89018600
└─ POST <acp>/api/v1/plugins/switch
{persist:false, enable:"<plugin>@<mk>", agentName:"...",
sourcePluginId:"...", internalModelRequestHeaders:[...]}
▼
【CLI】switchPlugins + setVolatilePluginAgent mjs 964
▼
【CLI】loadPluginComponents mjs 2511
├─ AgentExtensionLoader.load
│ ├─ 读 plugin.json 的 agents[] ← 通道 ①
│ ├─ 兜底扫 <plugin>/agents/ 目录(深度≤16) ← 通道 ②
│ └─ parseAgentFile
│ ├─ extractFrontMatterFull → {name, description, maxTurns, skills, ...}
│ ├─ 正文 → 变量替换 ${CODEBUDDY_PLUGIN_ROOT}
│ └─ 产出 {name, instructions, asTool:true, tags:[...,"<plugin>@<mk>"]}
├─ SkillExtensionLoader.load(读 skills[] + 兜底扫 skills/)
└─ 读 <plugin>/settings.json(仅 agent / subagentStatusLine 两个 key)
▼
【CLI】PluginsProductProvider.provide → product.agents mjs 984
▼
【CLI】AgentManager.registerPluginAgents → agentConfigMap mjs 970
▼
【CLI】applyPluginAgentSetting mjs 970
└─ 把该 agent 的 instructions 覆盖到 default agent(成为会话人格)
▼
【CLI】生成 Agent 工具描述 mjs 1852
└─ "Available agent types:\n- security-reviewer: ...\n- ..."
▼
【模型】Agent({subagent_type:"security-reviewer", name:"security-reviewer", prompt:"..."})
注意链路里 MD 被读了两次,目的完全不同:
- 主进程读(
resolveExpertAgentMarkdownPath)→ 只给 UI 显示提示词预览。 - CLI 读(
AgentExtensionLoader)→ 才是真正装进模型的通道。
改了 MD 后界面没变化?可能是主进程的 DTO 有缓存(customExpertDtoCache);
改了 MD 后行为没变化?那是 CLI 侧没重新加载(需要重新激活)。两者不是一回事。
10 动手:三种典型场景
10.1 先做选型决策
动手前先回答三个问题,它们决定包的形态:
① 单角色还是多角色? —— 判断标准:有没有用户会直接问它的问题? 每个独立成 agent 的角色, 都应覆盖一个完整的「分析域」。域内多能力归并以一个 agent,跨域才拆。拆太细会导致主理人调度开销吃掉收益。
② 要不要带专属工具? —— 需要确定性的、模型不擅长的计算(如 AST 解析、指标计算、格式校验)就放
skills/ 或 bin/;只是文字工作则不需要。
③ 输出是否需要稳定结构? —— 需要则在 MD 的「输出规范」里写死模板(表格/JSON schema/Mermaid), 不要依赖模型自由发挥。
expertType: agent1 个 MD,零协作开销。
适用:代码审查、合同审阅、SQL 优化、翻译润色、教案生成。
典型规模:1 个 MD,100–300 行。
expertType: team主理人 + N 团员,SOP 编排。
适用:软件开发全流程、交易分析、深度研究、营销战役。
典型规模:5–13 个 MD,主理人 200 行 + 各团员 100 行。
skills/ + bin/。适用:需要真实执行能力的场景 —— 静态分析、数据库巡检、报表生成、批量处理。
关键:用
${CODEBUDDY_PLUGIN_ROOT} 定位包内资源。
10.2 场景 A:单领域专家(代码审查专家)
第一步:初始化
cd <技能目录>/scripts
python3 init_expert.py code-reviewer \
--type agent \
--path ~/.workbuddy/plugins/marketplaces/my-experts/plugins
# 产出:
# ~/.workbuddy/plugins/marketplaces/my-experts/plugins/code-reviewer/
# ├── .codebuddy-plugin/plugin.json (全是 [TODO])
# ├── agents/code-reviewer.md (全是 [TODO])
# ├── avatars/.gitkeep
# └── README.md
第二步:填 plugin.json
{
"name": "code-reviewer",
"version": "1.0.0",
"description": "Senior code reviewer covering correctness, security, performance and maintainability.",
"author": { "name": "Your Name", "email": "you@example.com" },
"agents": ["./agents/code-reviewer.md"],
"expertType": "agent",
"agentName": "code-reviewer",
"displayName": {
"en": "Senior Reviewer",
"zh": "审稿人"
},
"profession": {
"en": "Senior Code Reviewer",
"zh": "资深代码审查师"
},
"displayDescription": {
"en": "Senior code reviewer skilled at correctness, security and performance across languages. Delivers severity-ranked findings with concrete fixes.",
"zh": "资深代码审查师,精通正确性与安全性能缺陷识别,按严重程度分级输出可直接落地的修改建议。"
},
"avatar": "avatars/expert.png",
"categoryId": "02-Engineering",
"defaultInitPrompt": {
"en": "Review this diff and list issues by severity",
"zh": "审查这段代码,按严重程度列出问题"
},
"plugin": "code-reviewer",
"tags": [
{ "en": "Code Review", "zh": "代码审查" },
{ "en": "Security Audit", "zh": "安全审计" },
{ "en": "Performance", "zh": "性能分析" }
],
"quickPrompts": [
{ "en": "Review this diff and list issues by severity",
"zh": "审查这段代码,按严重程度列出问题" },
{ "en": "Check this code for security vulnerabilities like injection and XSS",
"zh": "检查这段代码是否存在注入、XSS 等安全漏洞" },
{ "en": "Find performance bottlenecks in this hot path",
"zh": "找出这段热点代码的性能瓶颈" }
]
}
name与plugin必须完全相等,且 kebab-case。agentName必须等于agents/下的文件名(不含 .md)。tags与quickPrompts恰好各 3 个,多一个少一个都是 error。defaultInitPrompt与quickPrompts[0]逐字相同(界面显示的是后者)。displayDescription.zh控制在 40–50 字(上例 45 字)。categoryId必须是 12 个合法值之一(见附录 A)。- 所有 i18n 字段都要有
en与zh,缺一即 error。
第三步:写 Agent MD
---
name: code-reviewer
description: Reviews code for correctness, security, performance and maintainability issues, output severity-ranked findings with concrete fixes.
displayName:
en: "Senior Reviewer"
zh: "审稿人"
profession:
en: "Senior Code Reviewer"
zh: "资深代码审查师"
maxTurns: 50
---
# 资深代码审查师 - 审稿人
你是一名资深代码审查师。你的职责是从正确性、安全性、性能和可维护性四个维度审查代码,
输出可执行的修改建议,而不是主观风格评判。
## 核心能力
1. **正确性审查**:识别逻辑错误、边界条件缺失、空值处理不当、并发竞态、错误 swallowed。
2. **安全审计**:识别注入(SQL/命令/模板)、XSS、越权、敏感信息泄漏、依赖漏洞。
3. **性能分析**:识别 N+1 查询、循环内重复计算、不必要同步阻塞、内存泄漏风险。
4. **可维护性**:识别重复代码、过深嵌套、命名失义、缺少错误处理。
## 工作流程
1. **通读全貌**:先读完整段代码或 diff,理解意图,不急于下结论。
2. **逐项审查**:按上述四个维度各扫一遍,记录候选问题。
3. **去伪存真**:剔除误报与纯风格问题。只保留能说清危害的问题。
4. **定位与建议**:每条问题给出文件行号、危害说明、以及具体改法。
5. **汇总输出**:按下列模板输出。
## 输出规范
严格按以下结构输出,不要省略任何一节:
### 总体结论
一句话:这段代码是否可以合并,以及最关键的阻塞问题是什么。
### 问题清单
| 严重程度 | 位置 | 类别 | 问题描述 | 建议改法 |
|---------|------|------|---------|---------|
| 阻塞 | `path/to/file.ts:42` | 安全 | 用户输入直接拼接进 SQL | 改用参数化查询 |
严重程度只取四档:`阻塞` / `严重` / `一般` / `建议`。
类别只取四档:`正确性` / `安全` / `性能` / `可维护性`。
### 亮点
列出 1-3 处写得好的地方。不要为了凑数而编造。
## 注意事项
- **不做主观风格争论**:缩进、引号、分号这类交给 linter,不出现在报告里。
- **不确定的标注不确定**:疑似问题必须标注「待确认」,不得伪装成确定结论。
- **给出行号**:没有行号的问题视为无效。
- **代码必须通过工具读取**:不要凭印象推断代码内容。
- 输出语言与用户提问语言保持一致。
- frontmatter 没有
tools—— 规范明令禁止(§2.3)。 - 「工作流程」给了确定性步骤 —— 模型不会跳过通读直接输出。
- 「输出规范」写死了模板与枚举值 —— 保证多次调用输出结构稳定,可被下游解析。
- 「注意事项」明确列了禁止项 —— 抑制模型的编造与废话倾向。
- 没有废话 —— 每句话都是指令,因为正文直进 context,无中间层压缩。
第四步:头像 → 校验 → 注册
# 4. 生成头像(用 ImageGen,size 设为 1024x1024)
# prompt 从 MD 内容反推,例:
# "Professional cartoon-style illustration avatar, a senior code reviewer,
# wearing glasses, focused and meticulous expression,
# code diff lines and warning badges floating in background.
# Bust shot, facing forward. Clean simple blue-purple-toned background.
# High quality, professional, natural."
# → 保存到 avatars/expert.png
# 5. 校验
python3 validate_expert.py ~/.workbuddy/plugins/marketplaces/my-experts/plugins/code-reviewer
# 6. 注册(会二次校验 + 写 marketplace.json + 写 .created-by-session)
python3 register_expert.py ~/.workbuddy/plugins/marketplaces/my-experts/plugins/code-reviewer
# 7.(可选)打包分享
python3 package_expert.py ~/.workbuddy/plugins/marketplaces/my-experts/plugins/code-reviewer ./dist
10.3 场景 B:专家团(代码质量攻坚团)
Team 型的关键在于主理人 MD 的 SOP 与约束写得够不够硬 —— 因为这是唯一的保障。
目录结构
plugin.json 要点
{
"name": "code-quality-team",
"version": "1.0.0",
"description": "Multi-role team that audits and fixes code quality issues through a SOP pipeline.",
"agents": [
"./agents/code-quality-team-lead.md",
"./agents/cq-security-auditor.md",
"./agents/cq-perf-analyst.md",
"./agents/cq-maintain-reviewer.md",
"./agents/cq-fix-writer.md"
],
"expertType": "team",
"agentName": "code-quality-team-lead",
"teamInfo": {
"leadAgent": "code-quality-team-lead",
"memberAgents": [
"cq-security-auditor",
"cq-perf-analyst",
"cq-maintain-reviewer",
"cq-fix-writer"
]
},
"displayName": { "en": "Code Quality Team", "zh": "代码质量攻坚团" },
"profession": { "en": "Code Quality Team", "zh": "代码质量攻坚团" }, ← 必须与 displayName 一致
...
"members": [
{ "id": "code-quality-team-lead", "name": {"en": "Shen", "zh": "沈把关"},
"profession": {"en": "Quality Director", "zh": "质量总监"},
"avatar": "avatars/code-quality-team-lead.png", "role": "lead" },
{ "id": "cq-security-auditor", "name": {"en": "Yu", "zh": "于无隙"},
"profession": {"en": "Security Auditor", "zh": "安全审计师"},
"avatar": "avatars/cq-security-auditor.png", "role": "member" },
...
]
}
profession必须与displayName逐字相等(校验器判 error,不是 warning)。members[]必须包含主理人且role: "lead";teamInfo.memberAgents必须不含主理人。- 必须有
settings.json,且{"agent": ...}等于agentName。
主理人 MD 骨架(这是团队的灵魂)
---
name: code-quality-team-lead
description: Lead that orchestrates a code quality pipeline across security, performance, maintainability and fix implementation.
displayName:
en: "Shen"
zh: "沈把关"
profession:
en: "Quality Director"
zh: "质量总监"
maxTurns: 180
---
# 代码质量攻坚团 - 主理人
你是代码质量攻坚团的主理人沈把关 · 质量总监。你负责按 SOP 编排团队完成代码质量攻坚,
你是协调者,不是执行者。
## 团队成员
| Agent ID | 姓名 | 职责 | 什么时候调它 |
|---------|------|------|------------|
| cq-security-auditor | 于无隙 | 安全审计 | 需要找注入/XSS/越权/泄漏 |
| cq-perf-analyst | 马立行 | 性能分析 | 需要找瓶颈/慢查询/内存问题 |
| cq-maintain-reviewer | 易读通 | 可维护性 | 需要找重复/嵌套/命名问题 |
| cq-fix-writer | 何补拙 | 修复实施 | 拿到问题清单后要落地修改 |
## 工作流路由(收到请求时首先判断)
| 场景 | 判定条件 | 走哪条 |
|------|---------|--------|
| 全量攻坚 | 用户要求完整审查 + 修复 | 标准 SOP |
| 只查不改 | 用户只问「有什么问题」 | 并行审查,跳过 cq-fix-writer |
| 单一维度 | 用户只问安全 / 只问性能 | 直调单一成员 |
| 已知问题求修 | 用户已经列出问题清单 | 直调 cq-fix-writer |
## 标准 SOP
### Phase 1(并行,无数据依赖)
同一条消息内同时 spawn:
- cq-security-auditor → 安全问题清单
- cq-perf-analyst → 性能问题清单
- cq-maintain-reviewer → 可维护性问题清单
**三个成员互不通信,全部回传给你。**
### Phase 2(串行,依赖 Phase 1)
你把 Phase 1 的完整产出原文合并成一份问题清单,交给 cq-fix-writer:
- 按严重程度排序
- 标注哪些需要修、哪些仅建议
- 带上每个问题的原文位置信息
cq-fix-writer 输出修改方案 + 具体代码。
### Phase 3:最终报告
你汇总所有产出,向用户输出交付总结。
## 团队协作机制(铁律)
你必须走正式的团队协作流程,严禁简化或跳过:
1. **建立团队**:任务开始时由你亲自创建团队(TeamCreate),命名 `cq-<项目简称>`。
团队创建必须且只能由你执行,严禁委派任何成员创建团队。
2. **调度成员**:按 SOP 阶段将成员拉入协作、下发独立任务。
成员作为独立协作方输出专业产出,不得由你代写。
3. **消息中转**:成员产出回传给你,由你汇总、转交下一阶段。
所有跨成员信息流必须经你中转,不得互相直连。
4. **成员结论为准**:任何专业产出必须由对应成员输出后再采信,你只做编排与汇编。
### 严禁行为
- ❌ 禁止跳过 TeamCreate,直接自己模拟成员发言或并行写出多角色内容
- ❌ 禁止自己代写任何团队成员的专业产出
- ❌ 禁止未完成前序阶段就跳到后续阶段
- ❌ 禁止让成员互相直连通信
- ❌ 禁止 spawn 主理人自己
### 子任务命名(CRITICAL)
调度每位成员时,必须在 Agent 工具的 `name` 参数传入该成员的 Agent ID,
`subagent_type` 也传入相同的值。禁止省略 name,禁止用中文名或自创名称:
- `name: "cq-security-auditor", subagent_type: "cq-security-auditor"`
- `name: "cq-perf-analyst", subagent_type: "cq-perf-analyst"`
- `name: "cq-maintain-reviewer", subagent_type: "cq-maintain-reviewer"`
- `name: "cq-fix-writer", subagent_type: "cq-fix-writer"`
## 协作规则
1. 每阶段结束后,将完整产出原文传递给下一阶段成员,不要只传摘要。
2. 每完成一个阶段向用户简要通报进度。
3. 所有输出使用与用户原始需求相同的语言。
4. 最终报告必须包含:TL;DR、问题清单、修改文件清单、遗留问题、用户下一步建议。
每个团员 MD 必须包含这 6 项,缺一项团队就会退化:
- 角色定义(一句话说清你是谁)
- 擅长领域(3–5 个具体能力点)
- 分析框架(分步骤流程)
- 数据获取方式(具体工具调用)
- 结构化输出模板(表格/分段)
- SendMessage 回传要求 —— 明文写「分析完成后必须通过 SendMessage 将完整结果回传给主理人」
第 6 项最容易被漏掉。没有它,团员产出可能不回传,主理人拿不到东西,整条链路断掉。
10.4 场景 C:带专属工具的专家
当任务里有确定性计算时,别让模型用自然语言硬算 —— 给它脚本。
---
name: db-guardian
description: Database health guardian that diagnoses slow queries, index issues and connection pool saturation.
displayName:
en: "Guardian"
zh: "守库人"
profession:
en: "Database Reliability Engineer"
zh: "数据库可靠性工程师"
maxTurns: 60
skills: [slowlog-scan] # 预加载本包技能,会自动解析为 db-guardian:slowlog-scan
---
# 数据库可靠性工程师 - 守库人
...
## 数据获取方式
扫描慢日志时,必须调用脚本而非自己推算:
${CODEBUDDY_PLUGIN_ROOT}/skills/slowlog-scan/scripts/scan.py --top 20 --unit ms
执行计划分析使用 PATH 中已就绪的工具:
dbstat --explain "<sql>"
诊断规则参考:
${CODEBUDDY_PLUGIN_ROOT}/references/rules.md
${CODEBUDDY_PLUGIN_ROOT}→ 被替换成包根目录绝对路径,让 MD 能引用包内任何资源。skills: [slowlog-scan]→ 解析时自动尝试db-guardian:slowlog-scan前缀(§8.2)。bin/dbstat→ 无需任何声明,插件激活时自动前置到 Bash 的 PATH(§8.3)。
把大段规则放 references/ 而不是 MD 正文 —— 正文常驻 context,参考资料按需读取。
11 12 个反直觉的坑
以下每一条都是官方文档口径与代码实现不一致,或者文档没写但会咬人的地方。按踩坑频率排序。
| # | 直觉 / 文档说法 | 代码实证 | 后果 |
|---|---|---|---|
| 1 | 把专家目录放哪都行,注册了就能用 | 扫描器硬编码只扫 marketplaces/my-experts/plugins;init_expert.py 对 --path 做硬检查直接 sys.exit(1);validate_expert.py 也判定「不在专家目录下将无法被检测到」 |
专家根本不出现 |
| 2 | 写好 plugin.json 就够了 | 注册是必经步骤。扫描以 marketplace.json 为唯一入口,if (!await pathExists(marketplace.json)) return [] |
目录齐全但界面无此专家 |
| 3 | agents 字段必须写明每个 MD |
AgentExtensionLoader.load 在读完声明后还会兜底扫 <plugin>/agents/(深度≤16)。实证:官方 software-company 就没有此字段 |
可省;但声明了不存在的路径反而报错 |
| 4 | teamInfo.memberAgents 是成员清单,运行时会照着调 |
该字段在 96 MB 的 app.asar 中出现 0 次。运行时唯一读的是 teamInfo.leadAgent。members[] 只用于渲染头像墙 |
团队编排无任何运行时保障 |
| 5 | settings.json 是 team 生效的关键 |
CLI 确实读(只认 agent/subagentStatusLine 两个 key),但主进程启动时主动删除这类单 key 文件。本机 16 个官方包一个都没有。真正生效的是 volatile agentName |
写了也不指望它生效 |
| 6 | frontmatter 里写 tools 可以限制工具 |
规范明令禁止。且 permissionMode/hooks/mcpServers 三个字段写了会被静默忽略并打 warning:「is ignored for plugin agents」 |
配置无效,且不易察觉 |
| 7 | 改了 MD 立刻生效 | 有两套读取与缓存:主进程 DTO 有 customExpertDtoCache(影响界面),CLI 侧需重新激活(影响行为)。本地包无热重载 |
改了没反应,需重新选择专家 |
| 8 | 选了专家会一直生效 | POST /plugins/switch 带 persist: false,切换是 volatile(会话级) |
新会话回到默认助手 |
| 9 | 技能前缀来自 plugin.json 的 name |
实际来自 agent tags 末位的 "<pluginName>@<marketplace>" 拆分;deriveSkillNameFromPath 用的是相对路径段 join(:) |
效果相同,机制不同 |
| 10 | bin 字段声明 bin/ 目录 |
bin/ 是约定目录自动探测(getPluginBinPaths 直接 join 不看清单)。plugin.json 的 bin 字段无任何消费者 |
该字段是死字段 |
| 11 | 专家缓存路径是 cache/experts/<name>/<version>/ |
真实落盘是 marketplaces/experts/plugins/<bundleName>/,无 version 层级。cache/experts/ 是历史遗留;app/cache/experts/ 才存 manifest.json + expert-bundle-map.json |
改错地方,改了不生效 |
| 12 | categoryId 只有 12 个分类 | 本机实证存在第 13 个 13-TencentZone(andon-q-expert),官方文档未列出 |
照抄文档会漏掉合法值 |
Agent 工具的「可派生类型清单」是拼进工具描述字符串的,且有字符预算上限
(truncateAgentsByCharBudget)。同时启用多个专家包时,
超出预算的 agent 会被静默截断 —— 它们确实加载了,但模型看不到、也就无从调用。
工程建议:一个团队控制在 5–7 个成员(官方 software-company 是 5 个,
trading-agent 是 13 个属于上限特例),并给每个 agent 起短而有区分度的名字,
因为名字本身也占预算。
12 排错手册
| 现象 | 排查顺序 |
|---|---|
| 专家在界面里找不到 |
① 目录是否在 my-experts/plugins/ 下 →
② marketplace.json 是否存在且有该条目(source 形如 ./plugins/<dir>)→
③ 条目的 source 与目录名是否对得上 →
④ plugin.json 是否合法 JSON 且 name/agentName 齐全
|
| validate 报 name 相关错 |
① name 是否 kebab-case 且 ≥2 字符 →
② plugin 是否等于 name →
③ agentName 是否有对应 agents/<name>.md →
④ MD 的 frontmatter name 是否等于文件名 →
⑤ 文件名是否是 team-lead.md(需加前缀)
|
| validate 报数量/长度错 |
tags 与 quickPrompts 必须恰好 3 个;
displayDescription.zh 40–50 字(仅 warning);
team 型 profession ≡ displayName
|
| 专家存在,但行为不对 |
① 是否真的选中了该专家(切换是会话级 volatile)→
② 改完 MD 后是否重新激活(无热重载)→
③ 正文是否真的进了 context(确认 MD 路径被加载)→
④ 正文里是否有tools/hooks/mcpServers 被忽略
|
| 团队退化成「主理人独白」 | 这是prompt 失效而非代码问题。检查主理人 MD 是否包含: TeamCreate 强制条款、代写禁令、消息中转条款、Agent ID 逐条列举、团员 MD 的 SendMessage 回传要求。 删掉任一条都会显著提高退化概率 |
| 团员产出不回传 / 断链 |
① 团员 MD 是否明文写了 SendMessage 回传要求 →
② 主理人调度时 name 与 subagent_type 是否都传了 Agent ID(不是中文名)→
③ Agent ID 是否等于 MD 文件名
|
| 成员在 Agent 工具清单里消失 |
① Agent 工具描述被字符预算截断(成员过多或名字过长)→
② agentConfigMap 先到先得,与其它插件/用户级 agent 重名会被挤掉 →
③ 名字是否被 truncateAgentsByCharBudget 裁掉
|
| 技能引用不上 |
① frontmatter 的 skills 名字 → 直查 → <插件名>:<技能名> → 唯一后缀匹配,三步都失败才落空 →
② 包内技能名是否唯一(重名会让第三步失效)→
③ skills/ 目录是否存在(有兜底扫描)
|
| bin 脚本执行不了 |
① bin/ 是否真的在插件根目录(不是在 .codebuddy-plugin/ 里)→
② 脚本是否有执行权限 →
③ 注意 PATH 是前置的,确认没有与系统命令冲突
|
| 注册脚本提示找不到会话 ID |
环境变量 CODEBUDDY_SESSION_ID 未注入。这不影响 marketplace.json 写入,
只是不生成 .created-by-session,专家仍可正常使用
|
A 速查表
A.1 命令速查
SCRIPTS="<技能目录>/scripts"
XP="~/.workbuddy/plugins/marketplaces/my-experts/plugins"
# ① 初始化
python3 $SCRIPTS/init_expert.py <name> --type agent|team --path $XP
# ② 校验
python3 $SCRIPTS/validate_expert.py $XP/<name>
# ③ 注册(必经)
python3 $SCRIPTS/register_expert.py $XP/<name>
# ④ 打包
python3 $SCRIPTS/package_expert.py $XP/<name> ./dist
# ⑤ 批量
python3 $SCRIPTS/batch_create.py
A.2 关键路径速查
~/.workbuddy/plugins/marketplaces/my-experts/plugins/<name>/~/.workbuddy/plugins/marketplaces/my-experts/.codebuddy-plugin/marketplace.json~/.workbuddy/plugins/marketplaces/experts/plugins/<bundleName>/~/.workbuddy/app/cache/experts/{manifest.json, expert-bundle-map.json}~/.workbuddy/agents/*.md(独立机制,不是专家包)~/.workbuddy/skills/<name>/SKILL.mdCODEBUDDY_CODE_EXPERIMENTAL_AGENT_TEAMS=1(team 型自动置 1)${CODEBUDDY_PLUGIN_ROOT} → 插件根目录绝对路径A.3 行业分类(categoryId)
| categoryId | 分类 | 适用场景 |
|---|---|---|
01-ProductDesign | 产品设计 | UI/UX 设计、产品规划、原型、交互设计 |
02-Engineering | 技术工程 | 编程开发、架构设计、DevOps、技术选型 |
03-GameSpatial | 游戏空间 | 游戏开发、3D 建模、虚拟现实、游戏设计 |
04-DataAI | 数据智能 | 数据分析、机器学习、大模型应用、BI |
05-MarketingGrowth | 营销增长 | 品牌营销、用户增长、广告投放、SEO |
06-ContentCreative | 内容创作 | 文案写作、视频脚本、创意策划、翻译 |
07-SalesCommerce | 销售商务 | 销售策略、商务谈判、客户管理、电商 |
08-FinanceInvestment | 金融投资 | 投资分析、财务管理、风控、量化交易 |
09-OperationsHR | 运营人力 | 项目运营、人力资源、组织管理、培训 |
10-ProjectQuality | 项目质量 | 项目管理、质量保障、测试、流程优化 |
11-SecurityCompliance | 法务安全 | 信息安全、合规审查、法务咨询、隐私保护 |
12-IndustryConsultant | 行业顾问 | 跨行业咨询、战略规划、不属于以上明确分类的 |
13-TencentZone | (文档未列) | 实证存在于官方包 andon-q-expert,官方文档未收录 |
A.4 头像背景色调映射
同一团队的头像必须共用风格锚定词,背景色调由 categoryId 决定:
| categoryId | 背景色调 |
|---|---|
| 01 / 02 / 03 | warm orange-coral / blue-purple / purple-red gradient |
| 04 / 05 / 06 | cyan-teal / red-orange / pink-magenta |
| 07 / 08 / 09 | golden-amber / dark blue with gold accent / navy slate-blue |
| 10 / 11 / 12 | green-emerald / dark grey-blue / deep teal with silver accent |
B 本机实证数据
以下是对这台机器上 16 个真实落盘专家包的统计。它们是「别人怎么写」的一手参考, 也可用来校准自己的包规模。
B.1 包清单
| 包名 | 类型 | categoryId | 结构特征 |
|---|---|---|---|
software-company | team | (无) | 无 agents 字段、无 avatars —— 纯靠目录兜底 + 远程头像 |
trading-agent | team | 08 | 13 个 MD / 12 memberAgents / 13 members —— 规模上限 |
stock-partner-team | team | 08 | 7 个 MD / 7 members,8 张头像(唯一带全量头像的团队) |
fullstack-dev | agent | 02 | 1 MD + 1 头像,字段最完整的范例 |
andon-q-expert | agent | 13-TencentZone | 唯一使用文档外分类的包 |
senior-developer | agent | 02 | 1 MD + 1 头像 |
up-stock-strategy-analyze | agent | — | 1 MD + 1 头像 + 附带 skills 包 |
workspace-builder | agent | — | 1 MD + 1 头像 |
content-creator | agent | 06 | 1 MD + 1 头像 |
news-buddy | agent | 06 | 1 MD + 1 头像 |
data-analytics-reporter | agent | 04 | 1 MD + 1 头像 |
ui-designer | agent | — | 1 MD + 1 头像 |
xiaohongshu-operations-expert | agent | — | 1 MD + 1 头像 |
meituan-living-assistant | agent | 07 | 1 MD,无头像 |
data-analysis | agent | (无) | 1 MD,无头像、无 categoryId |
ppt-implement | agent | (无) | 1 MD,无头像、无 categoryId |
.downloaded_at 标记(每包一个)B.2 从数据里读出的三条结论
- 头像并非必需 —— 16 个包中 5 个没有 avatars 目录,仍能正常工作。说明市场包的头像走远程 URL 分发,落盘可省。
- categoryId 可有可无 —— 至少 3 个包没写。它主要用于市场分类陈列,本地使用不影响功能。
- 团队规模差异极大 —— 从 5 人到 13 人都有。结合 §11 的字符预算截断风险,5–7 人是更安全区间。
C 心智模型总结
如果只记住六句话:
agents/*.md 的每个字都直接进入模型上下文。
正文质量 = 专家质量,这是唯一的杠杆。
expertType: team 对运行时的全部贡献:开一个环境变量开关 + 打一个 HTTP 头。
SOP、铁律、红线全是 prompt。团队跑不跑得起来,取决于 MD 写得多硬。
persist: false 意味着切会话即失效。这不是缺陷而是设计 ——
专家是临时人格,不是常驻配置。
marketplace.json 为唯一入口。目录、plugin.json、MD 都齐全,
只要清单里没条目,专家就等于不存在。
C.1 给工程师的迁移建议
如果你要在自己的产品里实现类似机制,这套设计里最值得借鉴的三点:
- 文件即配置:整个包无需一行代码、无需构建、无需 schema 注册,纯文本即可分发。
代价是没有编译期检查,所以才需要
validate_expert.py这类外部校验来补位。 - 把插件系统当作底座:专家不自建加载器、不自建市场、不自建生命周期, 而是复用插件基础设施换取版本、分发、命名空间、PATH 注入等能力。典型的复用优于新建。
- 把编排留给模型:团队 SOP 不写进代码而写进 prompt,换来零开发成本的领域适配 —— 换个 MD 就从交易团变成开发团。代价是没有确定性保障,需要在 prompt 里用冗余约束来补偿。 这个取舍是这类系统的核心设计张力。
取证说明:本文结论基于对本机 WorkBuddy(macOS / darwin)安装包的静态分析,包含
app.asar(96,506,567 字节,主进程,未解包)的按字节偏移取证、
cli/dist/codebuddy-lite-wb.mjs(9,485,334 字节,2678 行)的行内定位、
resources/plugins/workbuddy-builtin/skills/expert-manager/ 下全部脚本与规范文档的通读,
以及对 ~/.workbuddy/plugins/marketplaces/ 下 16 个真实专家包的统计。
文中代码片段均经过精简(变量名保留压缩后的短名以保真,部分做了格式化以提高可读性)。 标注「未找到证据」处为诚实留白,未做推测。若版本更新导致行为变化,请以实际代码为准 —— 这也是本文反复提供取证路径、而非只给结论的原因。