dsh-agent-foreman 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +235 -3
- package/README.zh.md +250 -0
- package/cordis.patch.yml +11 -0
- package/docs/settings-roster.png +0 -0
- package/icon.svg +8 -0
- package/lib/catalog.js +137 -0
- package/lib/client.js +433 -0
- package/lib/command.js +101 -0
- package/lib/engine.js +1292 -0
- package/lib/index.js +173 -0
- package/lib/page.js +881 -0
- package/lib/routes.js +525 -0
- package/lib/skill.js +79 -0
- package/lib/store.js +705 -0
- package/lib/team.js +1102 -0
- package/lib/tools.js +506 -0
- package/lib/whitelist.js +320 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-agent-foreman —「包工头」。
|
|
3
|
+
*
|
|
4
|
+
* 一句话:父智能体派单,承包商子智能体干活(每人可单独指定 provider / 模型 /
|
|
5
|
+
* 思考等级),产物回流成一块共享看板,父智能体逐个验收、不合格打回返工。
|
|
6
|
+
*
|
|
7
|
+
* ## 为什么不是"专家团"
|
|
8
|
+
*
|
|
9
|
+
* 宿主自带的智能体团队(`@deepseek-ai/dsh-experimental-agent-team`)里,队员只有
|
|
10
|
+
* 名字、描述和提示词——真正决定"谁干活"的模型不可选:所有队员都跑父会话的模型,
|
|
11
|
+
* 于是"多招几个人"只是多花 token 买同一颗脑子。Agency 那类"领域专家"更远一层:
|
|
12
|
+
* 专家是 persona 模板,用户其实不知道自己买的是哪个模型。
|
|
13
|
+
*
|
|
14
|
+
* 本插件把身份换成了**路由**:承包商 = 名字 + provider/模型/思考等级。名册是用户
|
|
15
|
+
* 在设置页里填的,派单时逐单套用。这样"派给便宜的、快的、或者更强的那一档"真的
|
|
16
|
+
* 会发生,而不是换一段提示词。
|
|
17
|
+
*
|
|
18
|
+
* ## 为什么只有四个工具
|
|
19
|
+
*
|
|
20
|
+
* 父模型要做的只有两件事:拆单、验收。其余能力(指定 persona、工具白名单、
|
|
21
|
+
* 后台续跑、跨轮次持久派遣)都会让"这一轮到底做完了没有"变得含糊。看板是
|
|
22
|
+
* 一次派单的结果快照,跟着工具返回走,不留在宿主里假装还在跑。
|
|
23
|
+
*
|
|
24
|
+
* ## 组成
|
|
25
|
+
*
|
|
26
|
+
* - `lib/store.js` 名册(roster.json)与台账(runs.jsonl)的持久化;
|
|
27
|
+
* - `lib/catalog.js` 从宿主 `llm` 服务读真实模型目录;
|
|
28
|
+
* - `lib/engine.js` 并发派单、超时、拓扑分层、返工;
|
|
29
|
+
* - `lib/tools.js` 四个工具的定义与描述(这是父模型看的契约);
|
|
30
|
+
* - `lib/routes.js` 设置页与 JSON 接口(带跨站防护);
|
|
31
|
+
* - `lib/page.js` 设置页本体(纯 HTML,无构建);
|
|
32
|
+
* - `lib/client.js` 浏览器侧:把设置页放进「设置 → 包工头」。
|
|
33
|
+
*
|
|
34
|
+
* @module dsh-agent-foreman
|
|
35
|
+
*/
|
|
36
|
+
import z from '@deepseek-ai/schemastery';
|
|
37
|
+
import { ForemanStore, resolveDataDir } from './store.js';
|
|
38
|
+
import { SKILL_DESCRIPTION, SKILL_NAME, SKILL_SOURCE, foremanSkillContent, languageOf } from './skill.js';
|
|
39
|
+
import { ForemanEngine } from './engine.js';
|
|
40
|
+
import { registerForemanCommand } from './command.js';
|
|
41
|
+
import { foremanTools } from './tools.js';
|
|
42
|
+
import { mountRoutes } from './routes.js';
|
|
43
|
+
import { syncWhitelist } from './whitelist.js';
|
|
44
|
+
|
|
45
|
+
export const name = 'dsh-agent-foreman';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 必需服务:工具注册表。
|
|
49
|
+
*
|
|
50
|
+
* `subagents` / `webServer` / `llm` 都**不**列在这里——它们在调用点按需读取,
|
|
51
|
+
* 这样即使某个宿主没有 webServer(无头 profile),派单工具仍然可用;没有
|
|
52
|
+
* llm 时设置页退化成手填模型 id,而不是整块插件不加载。
|
|
53
|
+
*/
|
|
54
|
+
export const inject = ['tools'];
|
|
55
|
+
|
|
56
|
+
export const Config = z.object({
|
|
57
|
+
/**
|
|
58
|
+
* 数据目录覆盖。留空走 `%LOCALAPPDATA%\dsh-agent-foreman`。
|
|
59
|
+
* 名册是跨 profile 共享的:换一个 profile 不该丢掉队伍。
|
|
60
|
+
*/
|
|
61
|
+
dataDir: z.string().default(''),
|
|
62
|
+
/**
|
|
63
|
+
* 首次安装时预置的承包商。只在名册文件为空时写入,之后以设置页为准。
|
|
64
|
+
* 例:`[{ name: 'coder', role: '力工', provider: 'deepseek-official', model: 'deepseek-flash' }]`
|
|
65
|
+
*
|
|
66
|
+
* `role` 是岗位(派单时 `contractor` 可以写岗位名,在同一岗位的启用实例间轮转);
|
|
67
|
+
* 缺省时按名字充当岗位。`prompt` 是写给"这个人"的常驻说明,和岗位是两件事。
|
|
68
|
+
*/
|
|
69
|
+
members: z.array(z.object({
|
|
70
|
+
name: z.string(),
|
|
71
|
+
role: z.string().default(''),
|
|
72
|
+
prompt: z.string().default(''),
|
|
73
|
+
provider: z.string().default(''),
|
|
74
|
+
model: z.string().default(''),
|
|
75
|
+
reasoningEffort: z.string().default(''),
|
|
76
|
+
maxParallel: z.number().default(1),
|
|
77
|
+
enabled: z.boolean().default(true),
|
|
78
|
+
note: z.string().default(''),
|
|
79
|
+
})).default([]),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 挂载插件。
|
|
84
|
+
* @param ctx - cordis 上下文。
|
|
85
|
+
* @param config - 已解析的插件配置。
|
|
86
|
+
*/
|
|
87
|
+
export function apply(ctx, config) {
|
|
88
|
+
const logger = ctx.logger;
|
|
89
|
+
const configured = typeof config?.dataDir === 'string' ? config.dataDir.trim() : '';
|
|
90
|
+
const dataDir = configured !== '' ? configured : resolveDataDir(ctx.get('baseDir'));
|
|
91
|
+
const store = new ForemanStore(dataDir);
|
|
92
|
+
|
|
93
|
+
// 首次安装的种子名册:名册为空才写,避免覆盖用户在设置页里的改动。
|
|
94
|
+
if (Array.isArray(config?.members) && config.members.length > 0 && store.members().length === 0) {
|
|
95
|
+
try {
|
|
96
|
+
store.saveMembers(config.members);
|
|
97
|
+
logger?.info?.(`dsh-agent-foreman: 从插件配置写入了 ${config.members.length} 个承包商`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger?.warn?.(`dsh-agent-foreman: 预置名册无效 — ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 名册是子智能体白名单的唯一来源:把名册里的 provider/model 同步进宿主的
|
|
105
|
+
// `subagentModelSelection`,否则宿主只认它自己那份配置,派单会悄悄跑错模型。
|
|
106
|
+
// 这里**不能**把该服务写进 `inject`:宿主没有这个模块时整块插件不该消失。
|
|
107
|
+
// 同步失败只记日志——设置页和派单都得照常可用。
|
|
108
|
+
{
|
|
109
|
+
const result = syncWhitelist(ctx, store, logger);
|
|
110
|
+
if (result.ok === true && result.changed === true)
|
|
111
|
+
logger?.info?.(`dsh-agent-foreman: 已把名册 ${result.routes.length} 条路由同步进子智能体白名单`);
|
|
112
|
+
else if (result.ok === false)
|
|
113
|
+
logger?.debug?.(`dsh-agent-foreman: 子智能体白名单未同步 — ${result.reason ?? '未知原因'}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const engine = new ForemanEngine({ ctx, store, logger });
|
|
117
|
+
const tools = ctx.get('tools');
|
|
118
|
+
const disposers = [];
|
|
119
|
+
for (const definition of foremanTools({ ctx, store, engine, logger })) {
|
|
120
|
+
try {
|
|
121
|
+
disposers.push(tools.register(definition));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
// 单个工具注册失败不该带走其它三个,更不该带走设置页。
|
|
125
|
+
logger?.error?.(`dsh-agent-foreman: 注册工具 ${definition.name} 失败 — ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
ctx.effect(() => () => {
|
|
129
|
+
for (const dispose of disposers) {
|
|
130
|
+
try {
|
|
131
|
+
dispose?.();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// 卸载期重复释放可以忽略。
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
disposers.length = 0;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// `/foreman` 入口:优先注册原生命令(菜单里有标题、能进前排,靠 agent.steer 把
|
|
141
|
+
// 指令送进本回合);宿主没有 commands 服务时退回 skill(正文由宿主注入,省事但只有 id)。
|
|
142
|
+
const commandRelease = registerForemanCommand(ctx, { logger });
|
|
143
|
+
if (commandRelease !== undefined) {
|
|
144
|
+
ctx.effect(() => commandRelease);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
ctx.inject(['skills'], (skillCtx) => {
|
|
148
|
+
const service = skillCtx.skills;
|
|
149
|
+
if (typeof service?.register !== 'function')
|
|
150
|
+
return undefined;
|
|
151
|
+
const release = service.register({
|
|
152
|
+
name: SKILL_NAME,
|
|
153
|
+
description: SKILL_DESCRIPTION,
|
|
154
|
+
// source 必须给:宿主在加载 skill 时才校验它,缺了会直接让本轮报错。
|
|
155
|
+
source: SKILL_SOURCE,
|
|
156
|
+
content: foremanSkillContent(languageOf(ctx)),
|
|
157
|
+
});
|
|
158
|
+
if (typeof release === 'function')
|
|
159
|
+
skillCtx.effect(() => release);
|
|
160
|
+
return undefined;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// HTTP 表面单独一个依赖作用域:没有 webServer 的宿主仍然能派单。
|
|
165
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
166
|
+
const release = mountRoutes(webCtx, { store, engine, logger });
|
|
167
|
+
webCtx.effect(() => release);
|
|
168
|
+
return undefined;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const members = store.members();
|
|
172
|
+
logger?.info?.(`dsh-agent-foreman: 名册 ${members.length} 人(启用 ${members.filter(member => member.enabled).length} 人),数据目录 ${dataDir}`);
|
|
173
|
+
}
|