@stofone/dsh-agent-roster 0.0.0-stage → 0.5.1
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/CHANGELOG.md +157 -0
- package/LICENSE +21 -0
- package/README.md +290 -3
- package/README.zh.md +282 -0
- package/bin/cli.mjs +325 -0
- package/client.js +1950 -0
- package/cordis.patch.yml +9 -0
- package/examples/core-review/roster.yaml +82 -0
- package/examples/research/roster.yaml +42 -0
- package/icon.svg +11 -0
- package/index/registry.json +97 -0
- package/index.js +298 -0
- package/lib/bridge.js +233 -0
- package/lib/config-view.js +76 -0
- package/lib/format.js +223 -0
- package/lib/home.js +108 -0
- package/lib/http.js +122 -0
- package/lib/json.js +89 -0
- package/lib/library.js +161 -0
- package/lib/parse.js +148 -0
- package/lib/registry.js +285 -0
- package/lib/remote.js +623 -0
- package/lib/render.js +343 -0
- package/lib/serialize.js +92 -0
- package/lib/service.js +421 -0
- package/lib/teams-state.js +146 -0
- package/lib/tools.js +601 -0
- package/lib/validate.js +747 -0
- package/lib/verify.js +164 -0
- package/lib/write.js +290 -0
- package/locale/en.json +245 -0
- package/locale/zh.json +245 -0
- package/package.json +98 -4
- package/schema/roster.v1.schema.json +193 -0
package/lib/bridge.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桥接层:把静态档案翻译成 dsh-agent-teams 可以执行的调用参数,并如实标注每个字段的归宿。
|
|
3
|
+
*
|
|
4
|
+
* 这一层刻意**只做翻译与核对,不做调用**:真正的成员创建仍由队长模型发起
|
|
5
|
+
* `agent_teams_add_member`。原因是档案层不知道团队此刻处于什么阶段(暂存/运行/已归档),
|
|
6
|
+
* 也不应该替运行时决定"现在创建一个成员"。工具只输出参数,模型原样传入,运行时决定成败。
|
|
7
|
+
*
|
|
8
|
+
* 三个诚实状态必须被区分,否则档案会给人"能力已被运行时保证"的错觉:
|
|
9
|
+
*
|
|
10
|
+
* - `applied`:运行时状态里的值与档案一致(读取的是 team.json,不是我们的请求);
|
|
11
|
+
* - `overridden` / `omitted`:运行时最终采用了别的值,或根本没带这个字段;
|
|
12
|
+
* - `not-carried`:agent-teams 没有对应承载(toolScope、maxTokens 等),档案只是声明。
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-agent-roster/bridge
|
|
15
|
+
*/
|
|
16
|
+
import { FIELD_BINDING } from "./format.js";
|
|
17
|
+
|
|
18
|
+
/** 成员字段 → 调用参数名 / 运行时状态键。 */
|
|
19
|
+
const CARRIED_FIELDS = [
|
|
20
|
+
{ field: "name", param: "name", stateKey: "name" },
|
|
21
|
+
{ field: "role", param: "role", stateKey: "role" },
|
|
22
|
+
{ field: "systemPrompt", param: "executionPrompt", stateKey: "executionPrompt" },
|
|
23
|
+
{ field: "provider", param: "provider", stateKey: "provider" },
|
|
24
|
+
{ field: "model", param: "model", stateKey: "model" },
|
|
25
|
+
{ field: "reasoningEffort", param: "reasoning_effort", stateKey: "reasoningEffort" },
|
|
26
|
+
{ field: "toolPolicy", param: "tool_policy", stateKey: "toolPolicy" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** 桥接状态的中文标签,工具输出与 UI 共用。 */
|
|
30
|
+
export const BINDING_STATUS = {
|
|
31
|
+
pending: "已生成参数,运行时尚未回执",
|
|
32
|
+
applied: "运行时取值与档案一致",
|
|
33
|
+
overridden: "运行时实际取值与档案不同",
|
|
34
|
+
omitted: "运行时未携带该字段",
|
|
35
|
+
unconfirmed: "缺少运行时状态,无法核对",
|
|
36
|
+
"not-carried": "agent-teams 无承载,仅声明",
|
|
37
|
+
"config-required": "需要落到部署配置才生效",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 为一份档案里的一个成员生成桥接结果。
|
|
42
|
+
*
|
|
43
|
+
* 字段值缺失时一律写 `null` 而不是 `undefined`:工具返回值要能无损往返 JSON,
|
|
44
|
+
* 带 `undefined` 的键会被序列化丢掉,运行时因此判整次调用失败。
|
|
45
|
+
*
|
|
46
|
+
* @param {object} roster 已校验的档案
|
|
47
|
+
* @param {object} member 已校验的成员
|
|
48
|
+
* @returns {{ args: object, planMember: object, binding: object }}
|
|
49
|
+
*/
|
|
50
|
+
export function buildMemberBinding(roster, member) {
|
|
51
|
+
const args = { name: member.name, role: member.role, executionPrompt: member.systemPrompt };
|
|
52
|
+
if (member.provider !== undefined) args.provider = member.provider;
|
|
53
|
+
if (member.model !== undefined) args.model = member.model;
|
|
54
|
+
if (member.reasoningEffort !== undefined) args.reasoning_effort = member.reasoningEffort;
|
|
55
|
+
if (member.toolPolicy !== undefined) args.tool_policy = member.toolPolicy;
|
|
56
|
+
|
|
57
|
+
const planMember = { name: member.name, role: member.role };
|
|
58
|
+
if (member.provider !== undefined) planMember.provider = member.provider;
|
|
59
|
+
if (member.model !== undefined) planMember.model = member.model;
|
|
60
|
+
if (member.reasoningEffort !== undefined) planMember.reasoning_effort = member.reasoningEffort;
|
|
61
|
+
if (member.toolPolicy !== undefined) planMember.tool_policy = member.toolPolicy;
|
|
62
|
+
|
|
63
|
+
const binding = {
|
|
64
|
+
rosterId: roster.id,
|
|
65
|
+
rosterVersion: roster.version,
|
|
66
|
+
member: member.name,
|
|
67
|
+
origin: provenanceOf(roster, member),
|
|
68
|
+
fields: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const { field, param, stateKey } of CARRIED_FIELDS) {
|
|
72
|
+
const carried = args[param] !== undefined;
|
|
73
|
+
const source = member.inherited?.includes(field) ? "defaults" : "member";
|
|
74
|
+
if (!carried && field !== "name" && field !== "role" && field !== "systemPrompt") {
|
|
75
|
+
binding.fields.push({
|
|
76
|
+
field,
|
|
77
|
+
param,
|
|
78
|
+
stateKey,
|
|
79
|
+
source,
|
|
80
|
+
status: "omitted",
|
|
81
|
+
requested: null,
|
|
82
|
+
actual: null,
|
|
83
|
+
note: "档案未指定,运行时将沿用队长默认",
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!carried) {
|
|
88
|
+
// name/role/executionPrompt 永远有值,走到这里说明数据损坏,明确报出来而不是静默跳过。
|
|
89
|
+
binding.fields.push({
|
|
90
|
+
field,
|
|
91
|
+
param,
|
|
92
|
+
stateKey,
|
|
93
|
+
source,
|
|
94
|
+
status: "omitted",
|
|
95
|
+
requested: null,
|
|
96
|
+
actual: null,
|
|
97
|
+
note: "档案缺少该必填字段",
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
binding.fields.push({
|
|
102
|
+
field,
|
|
103
|
+
param,
|
|
104
|
+
stateKey,
|
|
105
|
+
source,
|
|
106
|
+
status: "pending",
|
|
107
|
+
requested: args[param],
|
|
108
|
+
actual: null,
|
|
109
|
+
note: "",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const [field, kind] of Object.entries(FIELD_BINDING)) {
|
|
114
|
+
if (kind === "direct") continue;
|
|
115
|
+
const value = member[field];
|
|
116
|
+
if (value === undefined) continue;
|
|
117
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
118
|
+
if (typeof value === "string" && value === "") continue;
|
|
119
|
+
binding.fields.push({
|
|
120
|
+
field,
|
|
121
|
+
param: null,
|
|
122
|
+
stateKey: null,
|
|
123
|
+
source: member.inherited?.includes(field) ? "defaults" : "member",
|
|
124
|
+
status: kind === "config" ? "config-required" : "not-carried",
|
|
125
|
+
requested: value,
|
|
126
|
+
actual: null,
|
|
127
|
+
note:
|
|
128
|
+
kind === "config"
|
|
129
|
+
? field === "fallback"
|
|
130
|
+
? "备用路由需写进成员创建后的部署配置(本插件不代写配置)"
|
|
131
|
+
: "该字段需落到部署配置才会生效"
|
|
132
|
+
: "agent-teams 成员无此承载,档案仅作能力声明与校验依据",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { args, planMember, binding };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 用一份真实的团队状态(team.json 的 members[])核对该成员的桥接结果。
|
|
141
|
+
*
|
|
142
|
+
* 比对的是运行时**持久化的实际值**,不是我们发出去的请求参数——这是"验证"与"自我报告"的分界。
|
|
143
|
+
*
|
|
144
|
+
* @param {object} binding `buildMemberBinding` 产出的 binding
|
|
145
|
+
* @param {object | null} teamMember team.json 里同名的成员记录
|
|
146
|
+
* @returns {object} 同一个 binding(就地更新),便于链式使用
|
|
147
|
+
*/
|
|
148
|
+
export function reconcileBinding(binding, teamMember) {
|
|
149
|
+
const found = teamMember ?? null;
|
|
150
|
+
for (const entry of binding.fields) {
|
|
151
|
+
if (entry.param === null) continue;
|
|
152
|
+
if (found === null) {
|
|
153
|
+
entry.status = "unconfirmed";
|
|
154
|
+
entry.note = "团队状态里没有同名成员";
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const actual = found[entry.stateKey];
|
|
158
|
+
entry.actual = actual ?? null;
|
|
159
|
+
if (actual === undefined || actual === null || actual === "") {
|
|
160
|
+
entry.status = "omitted";
|
|
161
|
+
entry.note = "运行时没有记录该字段";
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
entry.status = valueMatches(entry.field, entry.requested, actual) ? "applied" : "overridden";
|
|
165
|
+
entry.note = entry.status === "applied" ? "" : "运行时采用了不同的值";
|
|
166
|
+
}
|
|
167
|
+
return binding;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 生成绑定摘要:按状态统计,供工具输出和索引里的 `binding` 段使用。
|
|
172
|
+
*
|
|
173
|
+
* @param {object} binding
|
|
174
|
+
* @returns {{ counts: Record<string, number>, carried: number, applied: number, degraded: number }}
|
|
175
|
+
*/
|
|
176
|
+
export function summarizeBinding(binding) {
|
|
177
|
+
const counts = {};
|
|
178
|
+
for (const entry of binding.fields) {
|
|
179
|
+
counts[entry.status] = (counts[entry.status] ?? 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
const carried = binding.fields.filter((entry) => entry.param !== null).length;
|
|
182
|
+
const applied = counts.applied ?? 0;
|
|
183
|
+
const degraded = (counts.overridden ?? 0) + (counts.omitted ?? 0) + (counts.unconfirmed ?? 0);
|
|
184
|
+
return { counts, carried, applied, degraded };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 人读的一行:`model 已应用 deepseek/deepseek-v4.1-flash(来源:member)`。 */
|
|
188
|
+
export function describeField(entry) {
|
|
189
|
+
const label = entry.field.padEnd(15, " ");
|
|
190
|
+
const source = entry.source === "defaults" ? "继承 defaults" : "成员自带";
|
|
191
|
+
const value = entry.actual !== null ? entry.actual : entry.requested;
|
|
192
|
+
const rendered =
|
|
193
|
+
entry.field === "systemPrompt"
|
|
194
|
+
? `${String(value ?? "").length} 字符`
|
|
195
|
+
: value === null || value === undefined
|
|
196
|
+
? "—"
|
|
197
|
+
: typeof value === "string"
|
|
198
|
+
? value
|
|
199
|
+
: JSON.stringify(value);
|
|
200
|
+
const note = entry.note === "" ? "" : `(${entry.note})`;
|
|
201
|
+
return `${label}${BINDING_STATUS[entry.status] ?? entry.status} ${rendered} · ${source}${note}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 团队状态是否与档案一致:只统计"被运行时承载"的字段。
|
|
206
|
+
*
|
|
207
|
+
* 存在 overridden / omitted / unconfirmed 时返回 false——降级必须被看见。
|
|
208
|
+
*/
|
|
209
|
+
export function isBindingFaithful(binding) {
|
|
210
|
+
const summary = summarizeBinding(binding);
|
|
211
|
+
return (summary.counts.overridden ?? 0) === 0 && (summary.counts.unconfirmed ?? 0) === 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `roster:<id>@<version>`——出现在输出、提交信息与绑定记录里的出处标识。 */
|
|
215
|
+
export function provenanceOf(roster, member) {
|
|
216
|
+
const suffix = member.inherited?.length ? `+${member.inherited.join(",")}` : "";
|
|
217
|
+
return `roster:${roster.id}@${roster.version}${suffix}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 比较字段值。`reasoningEffort` 容许运行时把 "default" 解析成具体档位,
|
|
222
|
+
* 其余字段要求严格相等:档案说用什么模型,运行时就得记录那个模型。
|
|
223
|
+
*/
|
|
224
|
+
function valueMatches(field, requested, actual) {
|
|
225
|
+
if (requested === undefined || requested === null) return false;
|
|
226
|
+
if (field === "systemPrompt") return String(requested).trim() === String(actual).trim();
|
|
227
|
+
if (field === "reasoningEffort") {
|
|
228
|
+
if (requested === actual) return true;
|
|
229
|
+
// "default" 表示"由目标模型决定",运行时落成任何具体档位都算忠实执行。
|
|
230
|
+
return requested === "default" || actual === "default";
|
|
231
|
+
}
|
|
232
|
+
return String(requested) === String(actual);
|
|
233
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置读取视图:把插件配置读成"每次访问都取当前值"的形态。
|
|
3
|
+
*
|
|
4
|
+
* 声明为 `volatile` 的字段在运行时拿到的**不是快照值**,而是一个引用对象(`.get()`):
|
|
5
|
+
* 宿主用它承载"改设置立即生效"。所以这里不能读一次就存下来——否则用户改了档案库
|
|
6
|
+
* 目录却要重启宿主,那正是 volatile 想避免的事。
|
|
7
|
+
*
|
|
8
|
+
* 三种输入都要能接:引用对象(volatile)、普通值(非 volatile)、缺省(未配置)。
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-agent-roster/config-view
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 读一个配置单元:引用对象取值、普通值原样、缺省用兜底。
|
|
15
|
+
*
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {unknown} cell 配置字段
|
|
18
|
+
* @param {T} fallback 缺省值
|
|
19
|
+
* @returns {T}
|
|
20
|
+
*/
|
|
21
|
+
function read(cell, fallback) {
|
|
22
|
+
if (cell === undefined || cell === null) return fallback;
|
|
23
|
+
if (typeof cell === "object" && typeof cell.get === "function") {
|
|
24
|
+
const value = cell.get();
|
|
25
|
+
return value === undefined || value === null ? fallback : value;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(cell) && cell.length === 0) return fallback;
|
|
28
|
+
return cell;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 把 Config 解析结果包成带 getter 的视图。
|
|
33
|
+
*
|
|
34
|
+
* 只转发插件真正使用的字段;其余一律不透传,避免下游误把引用对象当数据用。
|
|
35
|
+
*
|
|
36
|
+
* @param {object} config `apply(ctx, config)` 收到的配置
|
|
37
|
+
* @returns {{
|
|
38
|
+
* rosterDirs: string[],
|
|
39
|
+
* knownToolPolicies: string[],
|
|
40
|
+
* agentTeamsStateDir: string,
|
|
41
|
+
* indexes: unknown,
|
|
42
|
+
* region: string,
|
|
43
|
+
* }}
|
|
44
|
+
*/
|
|
45
|
+
export function configView(config) {
|
|
46
|
+
const source = config ?? {};
|
|
47
|
+
return {
|
|
48
|
+
get rosterDirs() {
|
|
49
|
+
// 缺省 = 空串 = 机器级全局库($DSH_HOME/agent-roster),与 0.3.0 起的默认一致。
|
|
50
|
+
const dirs = read(source.rosterDirs, [""]);
|
|
51
|
+
return Array.isArray(dirs) ? dirs : [""];
|
|
52
|
+
},
|
|
53
|
+
get knownToolPolicies() {
|
|
54
|
+
const policies = read(source.knownToolPolicies, []);
|
|
55
|
+
return Array.isArray(policies) ? policies : [];
|
|
56
|
+
},
|
|
57
|
+
get agentTeamsStateDir() {
|
|
58
|
+
const dir = read(source.agentTeamsStateDir, ".agent-teams");
|
|
59
|
+
return typeof dir === "string" && dir !== "" ? dir : ".agent-teams";
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* 索引源。三种取值都必须原样保留:
|
|
63
|
+
* `undefined`/空数组 = 官方源;显式 `false` = 完全关掉远端;数组 = 自定义源。
|
|
64
|
+
*/
|
|
65
|
+
get indexes() {
|
|
66
|
+
if (typeof source.indexes === "object" && source.indexes !== null && typeof source.indexes.get === "function") {
|
|
67
|
+
return source.indexes.get();
|
|
68
|
+
}
|
|
69
|
+
return source.indexes;
|
|
70
|
+
},
|
|
71
|
+
get region() {
|
|
72
|
+
const region = read(source.region, "auto");
|
|
73
|
+
return typeof region === "string" && region !== "" ? region : "auto";
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/format.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 档案与索引的格式常量:标识、上限、模式、字段白名单与绑定类别。
|
|
3
|
+
*
|
|
4
|
+
* 这个模块只有数据、没有逻辑,被校验器、桥接层、UI 共同引用;
|
|
5
|
+
* 改这里等于改格式,必须同步 bump {@link ROSTER_KIND} 或索引格式。
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-agent-roster/format
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 档案文档格式标识(出现在每份 roster.yaml 的 kind 字段)。 */
|
|
11
|
+
export const ROSTER_KIND = "dsh-agent-roster/v1";
|
|
12
|
+
|
|
13
|
+
/** 索引文档格式标识(由 scan CLI / GitHub Action 产出)。 */
|
|
14
|
+
export const INDEX_KIND = "dsh-agent-roster-index/v1";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 公共档案库目录(registry)的格式标识。
|
|
18
|
+
*
|
|
19
|
+
* 与 {@link INDEX_KIND} 的区别:index 是“一个档案库扫描后的清单”(roster 指针),
|
|
20
|
+
* registry 是“许多档案库的登记簿”(collection 指针)——后者才是订阅/共享的入口。
|
|
21
|
+
*/
|
|
22
|
+
export const REGISTRY_KIND = "dsh-agent-roster-registry/v1";
|
|
23
|
+
|
|
24
|
+
/** 单份档案文档的字节上限,超过直接拒绝,避免把仓库当数据库用。 */
|
|
25
|
+
export const MAX_DOCUMENT_BYTES = 256 * 1024;
|
|
26
|
+
|
|
27
|
+
/** 结构与长度上限。 */
|
|
28
|
+
export const LIMITS = {
|
|
29
|
+
members: 32,
|
|
30
|
+
systemPromptChars: 8000,
|
|
31
|
+
titleChars: 120,
|
|
32
|
+
descriptionChars: 500,
|
|
33
|
+
roleChars: 120,
|
|
34
|
+
idChars: 64,
|
|
35
|
+
tagChars: 32,
|
|
36
|
+
tagsPerEntry: 16,
|
|
37
|
+
notesChars: 500,
|
|
38
|
+
reasonChars: 400,
|
|
39
|
+
aliasesPerMember: 8,
|
|
40
|
+
skillsPerMember: 16,
|
|
41
|
+
examplesPerMember: 5,
|
|
42
|
+
exampleChars: 200,
|
|
43
|
+
captainChars: 4000,
|
|
44
|
+
collections: 500,
|
|
45
|
+
rostersPerCollection: 200,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** 形态模式。 */
|
|
49
|
+
export const PATTERNS = {
|
|
50
|
+
/** roster id 与成员 name 共用:kebab-case,2~64 字符。 */
|
|
51
|
+
id: /^[a-z0-9][a-z0-9-]{1,63}$/,
|
|
52
|
+
/** LLM provider 路由名。 */
|
|
53
|
+
provider: /^[a-z0-9][a-z0-9._-]{0,63}$/,
|
|
54
|
+
/** 模型 id:允许厂商前缀与版本号里的点、斜杠、冒号。 */
|
|
55
|
+
model: /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,119}$/,
|
|
56
|
+
/** 工具名(命名空间分隔符,精确匹配)。 */
|
|
57
|
+
toolName: /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$/,
|
|
58
|
+
/** 工具匹配模式:精确名,或末尾带一个 `*` 的前缀通配(如 `git:*`、`files*`)。 */
|
|
59
|
+
toolPattern: /^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,126}\*?$/,
|
|
60
|
+
/** 精确 SemVer。 */
|
|
61
|
+
semver: /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/,
|
|
62
|
+
/** 40 位小写 commit SHA(索引指针只接受这种精确形态)。 */
|
|
63
|
+
commit: /^[0-9a-f]{40}$/,
|
|
64
|
+
/** 标签令牌。 */
|
|
65
|
+
tag: /^[a-z0-9][a-z0-9-]{0,31}$/,
|
|
66
|
+
/** 技能名:与 DSH 技能目录同名,允许点与下划线(如 `frontend-skill`、`eastmoney-quant`)。 */
|
|
67
|
+
skill: /^[a-z0-9][a-z0-9._-]{0,63}$/,
|
|
68
|
+
/** 仓库坐标:`github:owner/repo` 或 `owner/repo`(只允许 GitHub 与普通字符)。 */
|
|
69
|
+
repo: /^(?:github:)?[A-Za-z0-9][A-Za-z0-9_.-]{0,38}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/,
|
|
70
|
+
/** git ref:分支、标签或 40 位 commit(不接受 `..` 之类会让 URL 跑出仓库的写法)。 */
|
|
71
|
+
ref: /^(?:[A-Za-z0-9][A-Za-z0-9._-]{0,127}|[0-9a-f]{40})$/,
|
|
72
|
+
/** 仓库内相对目录:不允许绝对路径、`..` 与反斜杠。 */
|
|
73
|
+
relPath: /^(?:\/)?(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]*$/,
|
|
74
|
+
/** npm 包名(含 scoped)。 */
|
|
75
|
+
npmName: /^(?:@[a-z0-9][a-z0-9._-]{0,63}\/)?[a-z0-9][a-z0-9._-]{0,99}$/,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** reasoningEffort 的封闭枚举;未知值一律拒绝,不做静默降级。 */
|
|
79
|
+
export const REASONING_EFFORTS = ["low", "medium", "high", "default"];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 成员字段 → 桥接生效方式。
|
|
83
|
+
*
|
|
84
|
+
* - `direct`:原样进入 agent-teams 的工具参数;
|
|
85
|
+
* - `config`:运行时没有对应工具参数,只能生成配置片段,属「待 owner 落配置」;
|
|
86
|
+
* - `declaration`:运行时无承载,仅作文档与校验依据,必须标注未生效。
|
|
87
|
+
*/
|
|
88
|
+
export const FIELD_BINDING = {
|
|
89
|
+
name: "direct",
|
|
90
|
+
role: "direct",
|
|
91
|
+
systemPrompt: "direct",
|
|
92
|
+
provider: "direct",
|
|
93
|
+
model: "direct",
|
|
94
|
+
reasoningEffort: "direct",
|
|
95
|
+
toolPolicy: "direct",
|
|
96
|
+
fallback: "config",
|
|
97
|
+
toolScope: "declaration",
|
|
98
|
+
maxTokens: "declaration",
|
|
99
|
+
title: "declaration",
|
|
100
|
+
tags: "declaration",
|
|
101
|
+
notes: "declaration",
|
|
102
|
+
description: "declaration",
|
|
103
|
+
aliases: "declaration",
|
|
104
|
+
skills: "declaration",
|
|
105
|
+
examples: "declaration",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** 成员允许的字段(未知键一律拒绝,防止拼写错误静默失效)。 */
|
|
109
|
+
export const MEMBER_FIELDS = Object.keys(FIELD_BINDING);
|
|
110
|
+
|
|
111
|
+
/** 一个档案目录里按顺序探测的档案文件名:<root>/<roster-id>/roster.{yaml,yml,json}。 */
|
|
112
|
+
export const MEMBER_FILE_NAMES = ["roster.yaml", "roster.yml", "roster.json"];
|
|
113
|
+
|
|
114
|
+
/** 团队级默认值允许的字段:只放真正参与桥接的成员字段。 */
|
|
115
|
+
export const DEFAULTS_FIELDS = [
|
|
116
|
+
"provider",
|
|
117
|
+
"model",
|
|
118
|
+
"reasoningEffort",
|
|
119
|
+
"toolPolicy",
|
|
120
|
+
"toolScope",
|
|
121
|
+
"fallback",
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
/** 档案文档允许的顶层字段。 */
|
|
125
|
+
export const ROSTER_FIELDS = [
|
|
126
|
+
"$schema",
|
|
127
|
+
"kind",
|
|
128
|
+
"roster",
|
|
129
|
+
"version",
|
|
130
|
+
"title",
|
|
131
|
+
"description",
|
|
132
|
+
"license",
|
|
133
|
+
"maintainers",
|
|
134
|
+
"tags",
|
|
135
|
+
"defaults",
|
|
136
|
+
"members",
|
|
137
|
+
"captain",
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 队长段允许的字段。
|
|
142
|
+
*
|
|
143
|
+
* 队长就是创建团队的那个会话本身,agent-teams 没有 `captain` 成员行,
|
|
144
|
+
* 也没有任何工具参数能改写它的路由,所以这里全是「声明 + 落地提示」:
|
|
145
|
+
* 真正生效要改 profile 的 `agent-default-model` 与 agent preset 的 persona。
|
|
146
|
+
*/
|
|
147
|
+
export const CAPTAIN_FIELDS = [
|
|
148
|
+
"name",
|
|
149
|
+
"description",
|
|
150
|
+
"systemPrompt",
|
|
151
|
+
"personaPrefix",
|
|
152
|
+
"provider",
|
|
153
|
+
"model",
|
|
154
|
+
"reasoningEffort",
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
/** 队长字段 → 生效方式(与 FIELD_BINDING 同语义)。 */
|
|
158
|
+
export const CAPTAIN_BINDING = {
|
|
159
|
+
name: "declaration",
|
|
160
|
+
description: "declaration",
|
|
161
|
+
systemPrompt: "declaration",
|
|
162
|
+
personaPrefix: "config",
|
|
163
|
+
provider: "config",
|
|
164
|
+
model: "config",
|
|
165
|
+
reasoningEffort: "config",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** 索引文档允许的顶层字段。 */
|
|
169
|
+
export const INDEX_FIELDS = [
|
|
170
|
+
"$schema",
|
|
171
|
+
"format",
|
|
172
|
+
"generatedAt",
|
|
173
|
+
"generator",
|
|
174
|
+
"source",
|
|
175
|
+
"rosters",
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
/** registry 文档允许的顶层字段。 */
|
|
179
|
+
export const REGISTRY_FIELDS = [
|
|
180
|
+
"$schema",
|
|
181
|
+
"kind",
|
|
182
|
+
"version",
|
|
183
|
+
"generatedAt",
|
|
184
|
+
"generator",
|
|
185
|
+
"source",
|
|
186
|
+
"collections",
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* registry 里一条 collection 允许的字段。
|
|
191
|
+
*
|
|
192
|
+
* `repo` + `ref` + `path` 决定去哪个仓库的哪个目录取档案;
|
|
193
|
+
* `rosters` 是目录里真的有哪几份(索引不撒谎,客户端据此逐份拉取并自行校验)。
|
|
194
|
+
*/
|
|
195
|
+
export const COLLECTION_FIELDS = [
|
|
196
|
+
"id",
|
|
197
|
+
"title",
|
|
198
|
+
"description",
|
|
199
|
+
"repo",
|
|
200
|
+
"ref",
|
|
201
|
+
"path",
|
|
202
|
+
"rosters",
|
|
203
|
+
"tags",
|
|
204
|
+
"license",
|
|
205
|
+
"maintainers",
|
|
206
|
+
"homepage",
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 档案字段 → dsh-agent-teams 成员参数名。
|
|
211
|
+
*
|
|
212
|
+
* 只有 direct 字段出现在这里;config/declaration 字段没有运行时参数,
|
|
213
|
+
* 由 `bridge.js` 的 binding 报告如实标注。
|
|
214
|
+
*/
|
|
215
|
+
export const AGENT_TEAMS_FIELD = {
|
|
216
|
+
name: "name",
|
|
217
|
+
role: "role",
|
|
218
|
+
systemPrompt: "executionPrompt",
|
|
219
|
+
provider: "provider",
|
|
220
|
+
model: "model",
|
|
221
|
+
reasoningEffort: "reasoning_effort",
|
|
222
|
+
toolPolicy: "tool_policy",
|
|
223
|
+
};
|
package/lib/home.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 数据目录(harness home)与全局档案库的解析。
|
|
3
|
+
*
|
|
4
|
+
* 档案库默认**不跟着工作区走**:一座机器一份,放在 DSH 的数据目录下
|
|
5
|
+
* (`$DSH_HOME/agent-roster`,默认 `~/.dsh/agent-roster`)。docker 部署时
|
|
6
|
+
* `$DSH_HOME` 通常就是被挂载的那个卷,因此档案天然落在持久化位置。
|
|
7
|
+
*
|
|
8
|
+
* 这里的解析规则与宿主 `@deepseek-ai/dsh-home-paths` 保持一致(配置 > $DSH_HOME > `~/.dsh`),
|
|
9
|
+
* 但不 import 宿主包:插件要能在没有宿主的单测和 CLI 里独立跑。
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-agent-roster/home
|
|
12
|
+
*/
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import nodePath from "node:path";
|
|
15
|
+
|
|
16
|
+
/** 默认数据目录名(与宿主一致)。 */
|
|
17
|
+
export const DSH_HOME_DIR_NAME = ".dsh";
|
|
18
|
+
|
|
19
|
+
/** 覆盖数据目录的环境变量(与宿主一致)。 */
|
|
20
|
+
export const DSH_HOME_ENV = "DSH_HOME";
|
|
21
|
+
|
|
22
|
+
/** 全局档案库在数据目录下的目录名。 */
|
|
23
|
+
export const GLOBAL_ROSTER_DIR_NAME = "agent-roster";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 展开 `~`、`~/`、`~\` 前缀。
|
|
27
|
+
*
|
|
28
|
+
* @param {string} value
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function expandHomePath(value) {
|
|
32
|
+
if (typeof value !== "string") return value;
|
|
33
|
+
if (value === "~") return os.homedir();
|
|
34
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return nodePath.join(os.homedir(), value.slice(2));
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析 DSH 数据目录:显式配置 > `$DSH_HOME` > `~/.dsh`。
|
|
40
|
+
*
|
|
41
|
+
* @param {string | undefined} configured 显式覆盖(通常是插件配置里的绝对路径)
|
|
42
|
+
* @param {Record<string, string | undefined>} [env]
|
|
43
|
+
* @returns {string} 绝对路径
|
|
44
|
+
*/
|
|
45
|
+
export function resolveDshHome(configured, env = process.env) {
|
|
46
|
+
const explicit = typeof configured === "string" ? configured.trim() : "";
|
|
47
|
+
if (explicit !== "") return nodePath.resolve(expandHomePath(explicit));
|
|
48
|
+
const fromEnv = typeof env?.[DSH_HOME_ENV] === "string" ? env[DSH_HOME_ENV].trim() : "";
|
|
49
|
+
if (fromEnv !== "") return nodePath.resolve(expandHomePath(fromEnv));
|
|
50
|
+
return nodePath.join(os.homedir(), DSH_HOME_DIR_NAME);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 全局档案库默认根目录:`<DSH_HOME>/agent-roster`。
|
|
55
|
+
*
|
|
56
|
+
* @param {Record<string, string | undefined>} [env]
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
export function globalRosterRoot(env = process.env) {
|
|
60
|
+
return nodePath.join(resolveDshHome(undefined, env), GLOBAL_ROSTER_DIR_NAME);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 配置项是不是"与工作区无关"的全局目录。
|
|
65
|
+
*
|
|
66
|
+
* 空串 = 内置全局目录;`~` 开头的路径与绝对路径也都是全局的。
|
|
67
|
+
* 只有相对路径(如 `rosters`、`examples`)才按工作区解析——那是给"仓库内示例库"
|
|
68
|
+
* 这类有意为之的用法留的口子,不是默认行为。
|
|
69
|
+
*
|
|
70
|
+
* @param {string | undefined} spec
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isGlobalDirSpec(spec) {
|
|
74
|
+
if (typeof spec !== "string") return true;
|
|
75
|
+
const trimmed = spec.trim();
|
|
76
|
+
if (trimmed === "") return true;
|
|
77
|
+
return trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") || nodePath.isAbsolute(trimmed);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 把一个目录配置解析成绝对路径。
|
|
82
|
+
*
|
|
83
|
+
* @param {string | undefined} spec 目录配置(空串 = 内置全局目录)
|
|
84
|
+
* @param {string} [workspacePath] 相对路径的基准工作区
|
|
85
|
+
* @param {Record<string, string | undefined>} [env]
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function resolveDirSpec(spec, workspacePath = process.cwd(), env = process.env) {
|
|
89
|
+
const trimmed = typeof spec === "string" ? spec.trim() : "";
|
|
90
|
+
if (trimmed === "") return globalRosterRoot(env);
|
|
91
|
+
if (trimmed === "~") return os.homedir();
|
|
92
|
+
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return nodePath.resolve(expandHomePath(trimmed));
|
|
93
|
+
if (nodePath.isAbsolute(trimmed)) return nodePath.resolve(trimmed);
|
|
94
|
+
return nodePath.resolve(workspacePath, trimmed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 给用户展示的数据目录写法(不暴露机器绝对路径)。
|
|
99
|
+
*
|
|
100
|
+
* @param {string} home
|
|
101
|
+
* @param {Record<string, string | undefined>} [env]
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function displayDshHome(home, env = process.env) {
|
|
105
|
+
const fromEnv = typeof env?.[DSH_HOME_ENV] === "string" ? env[DSH_HOME_ENV].trim() : "";
|
|
106
|
+
if (fromEnv !== "") return "$DSH_HOME";
|
|
107
|
+
return `~/${DSH_HOME_DIR_NAME}`;
|
|
108
|
+
}
|