cs-bots 0.1.0 → 0.7.4
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 +533 -0
- package/README.md +65 -10
- package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
- package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
- package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
- package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
- package/docs/ARCHITECTURE.md +277 -0
- package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
- package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
- package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
- package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
- package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
- package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
- package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
- package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
- package/package.json +4 -2
- package/src/admission.js +365 -0
- package/src/api.js +4097 -420
- package/src/board.js +883 -14
- package/src/botprofile.js +190 -3
- package/src/client.js +6596 -1082
- package/src/cycle.js +516 -0
- package/src/delivery-watch.js +255 -0
- package/src/dirbrowse.js +20 -0
- package/src/discipline.js +1188 -0
- package/src/fence.js +142 -0
- package/src/index.js +387 -4
- package/src/kernel.js +636 -24
- package/src/library.js +1224 -11
- package/src/memory.js +617 -37
- package/src/metrics.js +339 -0
- package/src/prompts.js +1318 -0
- package/src/reviewer.js +709 -0
- package/src/room.js +655 -14
- package/src/roster.js +346 -26
- package/src/routing/complexity.js +320 -0
- package/src/routing/hints.js +707 -0
- package/src/scratch.js +477 -0
- package/src/settings.js +97 -0
- package/src/stage.js +263 -0
- package/src/store.js +133 -1
- package/src/subscription.js +571 -0
- package/src/suggest.js +1203 -0
- package/src/teams.js +527 -0
- package/src/todo.js +1301 -0
- package/src/tools.js +734 -10
- package/src/verify.js +356 -0
package/src/botprofile.js
CHANGED
|
@@ -45,6 +45,15 @@ export const LIMITS = Object.freeze({
|
|
|
45
45
|
skillsCount: 50,
|
|
46
46
|
workflowsCount: 50,
|
|
47
47
|
skillBody: 20000,
|
|
48
|
+
// ── v2 档案增强(DESIGN-v2 特性 ③)──────────────────────────────────────
|
|
49
|
+
// 为什么给 title 120:它是「岗位名」,一行标题的容量;再长就不是标题而是描述了,
|
|
50
|
+
// 渲染成 `## <title>` 会把 AGENTS.md 的标题层级撑歪。
|
|
51
|
+
// persona / boundaries 各 2000:这两段是**每次请求都注入**的常驻上下文,
|
|
52
|
+
// 两段合计 4000 字符 ≈ 1000 token,已接近「不喧宾夺主」的上限。
|
|
53
|
+
title: 120,
|
|
54
|
+
persona: 2000,
|
|
55
|
+
boundaries: 2000,
|
|
56
|
+
disciplineIds: 50,
|
|
48
57
|
})
|
|
49
58
|
|
|
50
59
|
/** 档案校验错误。 */
|
|
@@ -97,16 +106,52 @@ function takeList(value, field, maxItems, maxEach) {
|
|
|
97
106
|
})
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
/** 档案模型认识的字段(唯一清单:normalizeProfile 与 strict 校验共用,避免两处口径分叉)。 */
|
|
110
|
+
export const PROFILE_FIELDS = Object.freeze([
|
|
111
|
+
'identity',
|
|
112
|
+
'experience',
|
|
113
|
+
'skills',
|
|
114
|
+
'workflows',
|
|
115
|
+
'memory',
|
|
116
|
+
'title',
|
|
117
|
+
'persona',
|
|
118
|
+
'boundaries',
|
|
119
|
+
'disciplineIds',
|
|
120
|
+
])
|
|
121
|
+
|
|
100
122
|
/**
|
|
101
123
|
* 规范化档案输入(只取认识的字段,其余忽略;越界一律抛错)。
|
|
124
|
+
*
|
|
125
|
+
* ── 为什么「未知字段」默认忽略,而不是默认拒绝(重要,别照 DESIGN 字面改回去)──
|
|
126
|
+
* DESIGN-v2 特性 ③ 写的是「未知字段仍拒绝」,但本仓既有实现与测试的口径是**忽略**:
|
|
127
|
+
* · `test/botprofile.test.mjs` 明确断言 `{ ..., junk: 1 }` 归一后 junk 被丢弃(不能变红);
|
|
128
|
+
* · `src/library.js:220` 的注释把「多给的键属于调用方的自由」当作**本仓通用约定**。
|
|
129
|
+
* 两者冲突时**向后兼容赢**:老调用方(含已部署的 UI/API)多传一个键不该一夜之间变成 400。
|
|
130
|
+
* 因此默认宽松,同时提供 `{ strict: true }` 给新入口(API 层想收紧时)按需启用——
|
|
131
|
+
* 这样「拒绝未知字段」是可交付的能力,且不必打破既有契约。
|
|
132
|
+
*
|
|
102
133
|
* @param {object} input
|
|
103
|
-
* @
|
|
134
|
+
* @param {{ strict?: boolean }} [options] strict=true → 出现未知字段即抛 PROFILE_UNKNOWN_FIELD
|
|
135
|
+
* @returns {{ identity?: string, experience?: string, skills?: string[], workflows?: string[],
|
|
136
|
+
* memory?: string[], title?: string, persona?: string, boundaries?: string,
|
|
137
|
+
* disciplineIds?: string[] }}
|
|
104
138
|
*/
|
|
105
|
-
export function normalizeProfile(input) {
|
|
139
|
+
export function normalizeProfile(input, options = {}) {
|
|
106
140
|
const src = input ?? {}
|
|
107
141
|
if (typeof src !== 'object') {
|
|
108
142
|
throw new ProfileError('profile 必须是对象', 'PROFILE_BAD_FIELD')
|
|
109
143
|
}
|
|
144
|
+
if (options?.strict) {
|
|
145
|
+
for (const key of Object.keys(src)) {
|
|
146
|
+
if (!PROFILE_FIELDS.includes(key)) {
|
|
147
|
+
throw new ProfileError(
|
|
148
|
+
`profile.${key} 不是认识的字段(合法字段:${PROFILE_FIELDS.join(' / ')})`,
|
|
149
|
+
'PROFILE_UNKNOWN_FIELD',
|
|
150
|
+
{ field: key, allowed: [...PROFILE_FIELDS] },
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
110
155
|
const out = {}
|
|
111
156
|
const identity = takeText(src.identity, 'identity', LIMITS.identity)
|
|
112
157
|
if (identity !== undefined) out.identity = identity
|
|
@@ -118,9 +163,115 @@ export function normalizeProfile(input) {
|
|
|
118
163
|
if (workflows !== undefined) out.workflows = workflows
|
|
119
164
|
const memory = takeList(src.memory, 'memory', LIMITS.memoryCount, LIMITS.memoryEach)
|
|
120
165
|
if (memory !== undefined) out.memory = memory
|
|
166
|
+
// ── v2 新增字段 ─────────────────────────────────────────────────────────
|
|
167
|
+
// 为什么 title/persona/boundaries 走 takeText 而不是 takeList:它们是**单段文本**,
|
|
168
|
+
// 超长必须报错(takeText 不截断),避免「用户以为写进去了,其实被砍掉一半」。
|
|
169
|
+
const title = takeText(src.title, 'title', LIMITS.title)
|
|
170
|
+
if (title !== undefined) out.title = title
|
|
171
|
+
const persona = takeText(src.persona, 'persona', LIMITS.persona)
|
|
172
|
+
if (persona !== undefined) out.persona = persona
|
|
173
|
+
const boundaries = takeText(src.boundaries, 'boundaries', LIMITS.boundaries)
|
|
174
|
+
if (boundaries !== undefined) out.boundaries = boundaries
|
|
175
|
+
// disciplineIds 是引用(纪律条目 id),单项长度按 64 与 skills 同口径
|
|
176
|
+
const disciplineIds = takeList(src.disciplineIds, 'disciplineIds', LIMITS.disciplineIds, 64)
|
|
177
|
+
if (disciplineIds !== undefined) out.disciplineIds = disciplineIds
|
|
121
178
|
return out
|
|
122
179
|
}
|
|
123
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 记忆条目去重键:`trim` + 压缩连续空白 + 大小写不敏感。
|
|
183
|
+
*
|
|
184
|
+
* 为什么归一化而不是直接比对原文:记忆是「人写/模型写」的混合输入,
|
|
185
|
+
* 同一条内容常带不同的首尾空白、多余的连续空白与大小写
|
|
186
|
+
* (`Production DB is read-only` vs ` production db is READ-ONLY `),
|
|
187
|
+
* 直接字面比对会让**同一条记忆反复入库**,把 50 条的预算耗在重复内容上。
|
|
188
|
+
*
|
|
189
|
+
* ⚠️ 注意口径边界:归一化只「压缩」空白、不「删除」空白 ——
|
|
190
|
+
* `生产库只读` 与 `生产库 只读`(中间多一个空格)归一后仍不相等,
|
|
191
|
+
* 那是内容差异而不是空白差异,按「不同条目」保留(误合并比漏合并更难发现)。
|
|
192
|
+
*
|
|
193
|
+
* @param {string} text
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function memoryKey(text) {
|
|
197
|
+
return String(text).replace(/\s+/g, ' ').trim().toLowerCase()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 把一批记忆条目合并进现有记忆(**纯函数,不写盘**)。
|
|
202
|
+
*
|
|
203
|
+
* 语义(DESIGN-v2 特性 ③ 固定签名):
|
|
204
|
+
* · `dedup=true`(缺省):按 `memoryKey` 归一后去重——既与 existing 比,也与本批先前条目比;
|
|
205
|
+
* · `dedup=false`:只跳过**空白条目**,重复内容原样保留(用于「我要留多份」的显式场景);
|
|
206
|
+
* · 空白条目(trim 后为空)不写入,计入 `skipped`——空串进 AGENTS.md 只会渲染出一个空 bullet;
|
|
207
|
+
* · 总量超 `LIMITS.memoryCount` 或单条超 `LIMITS.memoryEach` **抛错不截断**:
|
|
208
|
+
* 截断会让调用方以为写成功了,实际上下文里少了半句记忆(比报错危险得多)。
|
|
209
|
+
*
|
|
210
|
+
* 为什么返回 `memory` 全量数组而不是只返回新增:调用方(roster.addProfileMemory)
|
|
211
|
+
* 要拿它直接覆盖 `profile.memory`,返回全量可避免调用方再拼一次(拼错就丢数据)。
|
|
212
|
+
*
|
|
213
|
+
* @param {{ existing?: string[], entries?: string[], dedup?: boolean }} input
|
|
214
|
+
* @returns {{ memory: string[], added: number, skipped: number }}
|
|
215
|
+
*/
|
|
216
|
+
export function importMemoryToProfile({ existing, entries, dedup = true } = {}) {
|
|
217
|
+
const base = existing === undefined || existing === null ? [] : existing
|
|
218
|
+
if (!Array.isArray(base)) {
|
|
219
|
+
throw new ProfileError('existing 必须是数组', 'PROFILE_BAD_FIELD', { field: 'existing' })
|
|
220
|
+
}
|
|
221
|
+
const incoming = entries === undefined || entries === null ? [] : entries
|
|
222
|
+
if (!Array.isArray(incoming)) {
|
|
223
|
+
throw new ProfileError('entries 必须是数组', 'PROFILE_BAD_FIELD', { field: 'entries' })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const memory = []
|
|
227
|
+
const seen = new Set()
|
|
228
|
+
/** 单条校验(既有条目同样校验:已损坏的数据不该被继续放大)。 */
|
|
229
|
+
const pushChecked = (raw, index, field) => {
|
|
230
|
+
const text = String(raw ?? '')
|
|
231
|
+
if (text.length > LIMITS.memoryEach) {
|
|
232
|
+
throw new ProfileError(
|
|
233
|
+
`profile.${field}[${index}] 超长:${text.length} > ${LIMITS.memoryEach} 字符`,
|
|
234
|
+
'PROFILE_TOO_LONG',
|
|
235
|
+
{ field: `${field}[${index}]`, length: text.length, max: LIMITS.memoryEach },
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
return text
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < base.length; i += 1) {
|
|
242
|
+
const text = pushChecked(base[i], i, 'memory')
|
|
243
|
+
memory.push(text)
|
|
244
|
+
seen.add(memoryKey(text))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let added = 0
|
|
248
|
+
let skipped = 0
|
|
249
|
+
for (let i = 0; i < incoming.length; i += 1) {
|
|
250
|
+
const text = pushChecked(incoming[i], i, 'entries')
|
|
251
|
+
if (text.trim() === '') {
|
|
252
|
+
skipped += 1 // 空白条目没有信息量,留一行空 bullet 只会污染上下文
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const key = memoryKey(text)
|
|
256
|
+
if (dedup && seen.has(key)) {
|
|
257
|
+
skipped += 1
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
memory.push(text)
|
|
261
|
+
seen.add(key)
|
|
262
|
+
added += 1
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (memory.length > LIMITS.memoryCount) {
|
|
266
|
+
throw new ProfileError(
|
|
267
|
+
`profile.memory 条目过多:${memory.length} > ${LIMITS.memoryCount}(既有 ${base.length} + 新增 ${added})`,
|
|
268
|
+
'PROFILE_TOO_MANY',
|
|
269
|
+
{ field: 'memory', count: memory.length, maxItems: LIMITS.memoryCount },
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
return { memory, added, skipped }
|
|
273
|
+
}
|
|
274
|
+
|
|
124
275
|
/** 校验 cwd 可用于写档案(必须绝对路径且存在)。 */
|
|
125
276
|
export function assertWritableCwd(cwd) {
|
|
126
277
|
if (typeof cwd !== 'string' || cwd.trim() === '') {
|
|
@@ -143,13 +294,32 @@ export function assertWritableCwd(cwd) {
|
|
|
143
294
|
|
|
144
295
|
/**
|
|
145
296
|
* 把档案渲染成 AGENTS.md 里的一段 Markdown。
|
|
146
|
-
*
|
|
297
|
+
*
|
|
298
|
+
* 渲染顺序的「为什么」(先身份、再约束、后能力、最后记忆):
|
|
299
|
+
* 身份类(title/persona/boundaries)必须**靠前**——AGENTS.md 是顺序读的,
|
|
300
|
+
* 人格与红线写在能力清单后面,模型容易「先看到技能再看到红线」,错误的暗示顺序反过来。
|
|
301
|
+
*
|
|
302
|
+
* v2 新增字段的渲染位置:
|
|
303
|
+
* · `title` 当**二级小标题**(`## 岗位名`)紧跟 H1 —— 一个小标题就够,不再套三层;
|
|
304
|
+
* · `persona` / `boundaries` 各自成节(`## 人格设定` / `## 边界与红线`);
|
|
305
|
+
* · `disciplineIds` 渲染成「## 关联纪律」的 id 列表(条目正文由纪律体系注入,这里只留引用)。
|
|
306
|
+
*
|
|
307
|
+
* 向后兼容:老档案没有这些字段时**不渲染空节**(`_(未填写)_` 只保留 identity 一处,
|
|
308
|
+
* 避免老 Bot 的 AGENTS.md 突然多出四行「未填写」的无意义内容)。
|
|
309
|
+
*
|
|
310
|
+
* @param {{ identity?: string, experience?: string, skills?: string[], workflows?: string[],
|
|
311
|
+
* memory?: string[], title?: string, persona?: string, boundaries?: string,
|
|
312
|
+
* disciplineIds?: string[], botName?: string, preset?: string }} profile
|
|
147
313
|
* @returns {string}
|
|
148
314
|
*/
|
|
149
315
|
export function renderManagedSection(profile) {
|
|
150
316
|
const p = profile ?? {}
|
|
151
317
|
const lines = [MARK_BEGIN, '']
|
|
152
318
|
lines.push(`# 你的身份(由 cs-bots 维护)`, '')
|
|
319
|
+
// title 当小标题:老档案没有 title 时整行不出现,保持旧渲染结果不变
|
|
320
|
+
if (typeof p.title === 'string' && p.title.trim()) {
|
|
321
|
+
lines.push(`## ${p.title.trim()}`, '')
|
|
322
|
+
}
|
|
153
323
|
if (p.botName) lines.push(`- 角色名:**${p.botName}**`)
|
|
154
324
|
if (p.preset) lines.push(`- 预设:\`${p.preset}\``)
|
|
155
325
|
if (p.identity && p.identity.trim()) {
|
|
@@ -158,6 +328,16 @@ export function renderManagedSection(profile) {
|
|
|
158
328
|
lines.push('', '_(未填写身份描述)_')
|
|
159
329
|
}
|
|
160
330
|
|
|
331
|
+
// 人格设定放在经验之前:它是「我是谁」,经验是「我做过什么」,由内而外更符合注入顺序
|
|
332
|
+
if (typeof p.persona === 'string' && p.persona.trim()) {
|
|
333
|
+
lines.push('', '## 人格设定', '', p.persona.trim())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 红线紧跟人格:约束越早出现,越不容易被后面的任务描述冲淡
|
|
337
|
+
if (typeof p.boundaries === 'string' && p.boundaries.trim()) {
|
|
338
|
+
lines.push('', '## 边界与红线', '', p.boundaries.trim())
|
|
339
|
+
}
|
|
340
|
+
|
|
161
341
|
if (p.experience && p.experience.trim()) {
|
|
162
342
|
lines.push('', '## 经验与规矩', '', p.experience.trim())
|
|
163
343
|
}
|
|
@@ -168,6 +348,13 @@ export function renderManagedSection(profile) {
|
|
|
168
348
|
lines.push('', `技能文件位于本工作目录的 \`${SKILLS_DIR}/<技能名>/SKILL.md\`,用 skill 工具加载。`)
|
|
169
349
|
}
|
|
170
350
|
|
|
351
|
+
// 关联纪律只放 id:正文由纪律体系按层级合并注入,这里重复正文会导致「同一纪律两份、还可能互相矛盾」
|
|
352
|
+
if (Array.isArray(p.disciplineIds) && p.disciplineIds.length) {
|
|
353
|
+
lines.push('', '## 关联纪律', '')
|
|
354
|
+
for (const d of p.disciplineIds) lines.push(`- \`${d}\``)
|
|
355
|
+
lines.push('', '以上纪律条目由协作平台的纪律体系按「全局 → 工作区 → 房间 → Bot」层级合并注入,此处只登记关联关系。')
|
|
356
|
+
}
|
|
357
|
+
|
|
171
358
|
if (Array.isArray(p.workflows) && p.workflows.length) {
|
|
172
359
|
lines.push('', '## 该遵循的工作流', '')
|
|
173
360
|
for (const w of p.workflows) lines.push(`- \`${w}\``)
|