cs-bots 0.1.1 → 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/roster.js
CHANGED
|
@@ -58,7 +58,28 @@ import {
|
|
|
58
58
|
writeJsonAtomic,
|
|
59
59
|
} from './store.js'
|
|
60
60
|
import * as kernel from './kernel.js'
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
normalizeProfile,
|
|
63
|
+
writeAgentsFile,
|
|
64
|
+
readAgentsFile,
|
|
65
|
+
writeSkill,
|
|
66
|
+
listSkills,
|
|
67
|
+
importMemoryToProfile,
|
|
68
|
+
} from './botprofile.js'
|
|
69
|
+
|
|
70
|
+
// ── store.appendAudit 软导入(P1-D §4)───────────────────────────────────────
|
|
71
|
+
// 软用:拿不到就只 warn 一次 + 计数,绝不阻断主流程,也绝不自己另造审计格式。
|
|
72
|
+
// 动态 `await import` 而非具名静态 import:store.js 正被并行修改,静态具名 import 在
|
|
73
|
+
// 对方重构掉该导出时是**加载期**失败(整个服务起不来);动态判定只少一条审计。
|
|
74
|
+
let storeAppendAudit = /** @type {null | ((dir: string, entry: object) => unknown)} */ (null)
|
|
75
|
+
let storeAuditMissing = false
|
|
76
|
+
try {
|
|
77
|
+
const storeMod = await import('./store.js')
|
|
78
|
+
if (typeof storeMod.appendAudit === 'function') storeAppendAudit = storeMod.appendAudit
|
|
79
|
+
else storeAuditMissing = true
|
|
80
|
+
} catch {
|
|
81
|
+
storeAuditMissing = true
|
|
82
|
+
}
|
|
62
83
|
|
|
63
84
|
// ── 常量 ────────────────────────────────────────────────────────────────────
|
|
64
85
|
|
|
@@ -75,7 +96,23 @@ const MAX_LABEL = 24
|
|
|
75
96
|
/** 租约缺省 TTL(ms),与 board 的 300s 对齐。 */
|
|
76
97
|
const DEFAULT_LEASE_MS = 300_000
|
|
77
98
|
/** 事件动作集合(审计流可枚举)。 */
|
|
78
|
-
const ACTIONS = [
|
|
99
|
+
const ACTIONS = [
|
|
100
|
+
'create',
|
|
101
|
+
'rename',
|
|
102
|
+
'retire',
|
|
103
|
+
'set_cwd',
|
|
104
|
+
'claim',
|
|
105
|
+
'release',
|
|
106
|
+
'heartbeat',
|
|
107
|
+
'lease_expired',
|
|
108
|
+
// v2 档案增强:三种「改档案」的入口各自留痕,避免审计流里只能看到一条笼统的 profile_updated
|
|
109
|
+
// (排查「这条记忆谁加进来的」「技能怎么关联上的」时,动作名是第一手证据)。
|
|
110
|
+
'profile_updated',
|
|
111
|
+
'profile_memory_added',
|
|
112
|
+
'profile_skills_linked',
|
|
113
|
+
// 改造07(借鉴 Super-agent 的 prepare command):开工前准备命令的变更留痕
|
|
114
|
+
'prepare_command_set',
|
|
115
|
+
]
|
|
79
116
|
|
|
80
117
|
// ── 错误 ────────────────────────────────────────────────────────────────────
|
|
81
118
|
|
|
@@ -325,6 +362,8 @@ export function createRoster(ctx, config = {}) {
|
|
|
325
362
|
badLines: 0,
|
|
326
363
|
skippedEvents: 0,
|
|
327
364
|
snapshotWriteFailures: 0,
|
|
365
|
+
auditWriteFailures: 0,
|
|
366
|
+
auditWarnedOnce: false,
|
|
328
367
|
lastReplayAt: /** @type {number|undefined} */ (undefined),
|
|
329
368
|
}
|
|
330
369
|
|
|
@@ -392,6 +431,38 @@ export function createRoster(ctx, config = {}) {
|
|
|
392
431
|
diagnostics.snapshotWriteFailures += 1
|
|
393
432
|
warn(`[cs-bots/roster] 快照写入失败(${snapPath})——事件流已落盘,replay() 可完整恢复`)
|
|
394
433
|
}
|
|
434
|
+
// ── 审计接线(P1-D §4)────────────────────────────────────────────────
|
|
435
|
+
// 软用 store.appendAudit:拿不到/写失败 → 只 warn 一次 + 计数,绝不阻断主流程。
|
|
436
|
+
if (storeAuditMissing) {
|
|
437
|
+
diagnostics.auditWriteFailures += 1
|
|
438
|
+
if (!diagnostics.auditWarnedOnce) {
|
|
439
|
+
diagnostics.auditWarnedOnce = true
|
|
440
|
+
warn('[cs-bots/roster] store.appendAudit 尚不可用 → 审计接线跳过;主流程不受影响')
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
try {
|
|
444
|
+
const digest = (b) => (b
|
|
445
|
+
? { id: b.id, name: b.name ?? null, state: b.state ?? null, rev: b.rev }
|
|
446
|
+
: null)
|
|
447
|
+
const result = storeAppendAudit(stateDir, {
|
|
448
|
+
ts: at, actor: byId, action, target: botId, before: digest(before), after: digest(after),
|
|
449
|
+
})
|
|
450
|
+
const failed = result === false || Boolean(result && typeof result === 'object' && result.ok === false)
|
|
451
|
+
if (failed) {
|
|
452
|
+
diagnostics.auditWriteFailures += 1
|
|
453
|
+
if (!diagnostics.auditWarnedOnce) {
|
|
454
|
+
diagnostics.auditWarnedOnce = true
|
|
455
|
+
warn('[cs-bots/roster] store.appendAudit 返回失败——主流程不受影响')
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch (error) {
|
|
459
|
+
diagnostics.auditWriteFailures += 1
|
|
460
|
+
if (!diagnostics.auditWarnedOnce) {
|
|
461
|
+
diagnostics.auditWarnedOnce = true
|
|
462
|
+
warn(`[cs-bots/roster] store.appendAudit 抛错:${error?.message ?? error}——主流程不受影响`)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
395
466
|
return clone(after)
|
|
396
467
|
}
|
|
397
468
|
|
|
@@ -551,6 +622,33 @@ export function createRoster(ctx, config = {}) {
|
|
|
551
622
|
return list({ state: 'retired' })
|
|
552
623
|
}
|
|
553
624
|
|
|
625
|
+
/**
|
|
626
|
+
* 按**会话 id** 判定「这个会话是不是本平台注册的 Bot」(含已退役)。
|
|
627
|
+
*
|
|
628
|
+
* 为什么需要(2026-11-04,复杂度路由 P3-fix):宿主 `llm/stream` 请求里**只有
|
|
629
|
+
* `sessionId`**,而注册表的主键是 `botId`。路由要区分「主对话」(人类会话 ⇒ 平台层
|
|
630
|
+
* 注入 paid_allowed)与「Bot 会话」(子智能体 ⇒ 默认 free_only),就必须有这条查询。
|
|
631
|
+
*
|
|
632
|
+
* 为什么按 `sessionId` **字段**匹配而不是 `bots.get(id)`:`create()` 里宿主可能把
|
|
633
|
+
* 请求的 sessionId 改写成别的值(`pickSessionId` 已按「以宿主返回为准」处理并留档),
|
|
634
|
+
* 拿 botId 当键查会在这种情形下漏判。
|
|
635
|
+
*
|
|
636
|
+
* 为什么包含退役 Bot:退役只释放名字与槽位,**会话本身还在宿主编排里把手上的活干完**,
|
|
637
|
+
* 这段时间它仍是子智能体 —— 按主对话给付费授权属于越权。
|
|
638
|
+
*
|
|
639
|
+
* 纯读、**不抛**(非法入参返回 false)。
|
|
640
|
+
*
|
|
641
|
+
* @param {string} sessionId
|
|
642
|
+
* @returns {boolean}
|
|
643
|
+
*/
|
|
644
|
+
function hasSession(sessionId) {
|
|
645
|
+
if (typeof sessionId !== 'string' || sessionId === '') return false
|
|
646
|
+
for (const b of bots.values()) {
|
|
647
|
+
if (b?.sessionId === sessionId) return true
|
|
648
|
+
}
|
|
649
|
+
return false
|
|
650
|
+
}
|
|
651
|
+
|
|
554
652
|
/**
|
|
555
653
|
* 注册表统计。
|
|
556
654
|
* 口径说明:`total` = **含 retired 的全部 Bot**;`byState` 是全部状态的分布;
|
|
@@ -596,7 +694,7 @@ export function createRoster(ctx, config = {}) {
|
|
|
596
694
|
* @throws {RosterError} 缺字段 / 非法 cwd / 重名 / 会话创建失败 / 事件落盘失败
|
|
597
695
|
*/
|
|
598
696
|
async function create(input = {}) {
|
|
599
|
-
const { name, cwd, label, model, instructions, preset, permission, profile, byId, now } = input ?? {}
|
|
697
|
+
const { name, cwd, label, model, instructions, preset, permission, profile, prepareCommand, byId, now } = input ?? {}
|
|
600
698
|
const by = requireString(byId, 'byId', 'create')
|
|
601
699
|
const displayName = requireString(name, 'name', 'create')
|
|
602
700
|
const dir = assertBotCwd(cwd, 'create')
|
|
@@ -626,6 +724,7 @@ export function createRoster(ctx, config = {}) {
|
|
|
626
724
|
handle = await kernel.createSession(ctx, {
|
|
627
725
|
sessionId: id,
|
|
628
726
|
cwd: dir,
|
|
727
|
+
warn,
|
|
629
728
|
...(preset !== undefined && preset !== null && preset !== '' ? { agentPreset: String(preset) } : {}),
|
|
630
729
|
})
|
|
631
730
|
} catch (error) {
|
|
@@ -659,6 +758,12 @@ export function createRoster(ctx, config = {}) {
|
|
|
659
758
|
if (roleLabel !== undefined) bot.label = roleLabel
|
|
660
759
|
if (mdl !== undefined) bot.model = mdl
|
|
661
760
|
if (inst !== undefined) bot.instructions = inst
|
|
761
|
+
// 改造07:开工准备命令(借鉴 Super-agent 的 prepare command)
|
|
762
|
+
if (prepareCommand !== undefined && prepareCommand !== null && prepareCommand !== '') {
|
|
763
|
+
bot.prepareCommand = String(prepareCommand).trim()
|
|
764
|
+
bot.prepareCommandSetAt = at
|
|
765
|
+
bot.prepareCommandSetBy = by
|
|
766
|
+
}
|
|
662
767
|
// ── 档案(身份/经验/技能/预制记忆)→ 落到官方机制上 ──────────────────
|
|
663
768
|
if (profile !== undefined) {
|
|
664
769
|
try {
|
|
@@ -790,7 +895,10 @@ export function createRoster(ctx, config = {}) {
|
|
|
790
895
|
*/
|
|
791
896
|
function getProfile(id) {
|
|
792
897
|
const bid = requireString(id, 'id', 'getProfile')
|
|
793
|
-
|
|
898
|
+
// ⚠️ 修复(2026-11-04 测试):原为 `index.get(bid)` —— 本函数作用域内只有
|
|
899
|
+
// `bots` Map,没有 `index` ⇒ 每次调用必然 ReferenceError(HTTP 400
|
|
900
|
+
// `API_ERROR: index is not defined`),Bot 档案抽屉打开即报错。
|
|
901
|
+
const bot = bots.get(bid)
|
|
794
902
|
if (!bot) {
|
|
795
903
|
throw new RosterError(`roster.getProfile:Bot ${bid} 不存在`, 'ROSTER_BOT_NOT_FOUND', { id: bid })
|
|
796
904
|
}
|
|
@@ -807,57 +915,231 @@ export function createRoster(ctx, config = {}) {
|
|
|
807
915
|
}
|
|
808
916
|
|
|
809
917
|
/**
|
|
810
|
-
*
|
|
918
|
+
* 退役 Bot 不可改档案 —— 三个改档案入口(setProfile / addProfileMemory / linkSkills)
|
|
919
|
+
* 共用同一守卫:**同一语义只有一处判定**,否则日后加新入口时漏一个就会给退役 Bot 改档案。
|
|
811
920
|
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
921
|
+
* 错误码沿用 setProfile 既有的 `ROSTER_BOT_RETIRED`(不是 assertNotRetired 的
|
|
922
|
+
* `ROSTER_RETIRED`):API 层与 UI 已按前者识别「退役导致不可改档案」,改码会让提示退化成通用错误。
|
|
923
|
+
*
|
|
924
|
+
* @param {object} current
|
|
925
|
+
* @param {string} action
|
|
814
926
|
*/
|
|
815
|
-
function
|
|
816
|
-
const { id, profile, byId, now } = input ?? {}
|
|
817
|
-
const current = loadForCas(input, 'setProfile')
|
|
818
|
-
const by = requireString(byId, 'byId', 'setProfile')
|
|
927
|
+
function assertProfileEditable(current, action) {
|
|
819
928
|
if (current.state === 'retired') {
|
|
820
929
|
throw new RosterError(
|
|
821
|
-
`roster
|
|
930
|
+
`roster.${action}:Bot ${current.id} 已退役(名字 ${current.name})——退役 Bot 不可改档案`,
|
|
822
931
|
'ROSTER_BOT_RETIRED',
|
|
823
932
|
{ id: current.id },
|
|
824
933
|
)
|
|
825
934
|
}
|
|
826
|
-
|
|
935
|
+
}
|
|
827
936
|
|
|
828
|
-
|
|
937
|
+
/**
|
|
938
|
+
* 把档案写进 AGENTS.md(受管区块),并给**缺失的**技能补一个骨架 SKILL.md。
|
|
939
|
+
*
|
|
940
|
+
* 为什么抽成一个函数:setProfile / addProfileMemory / linkSkills 三条入口必须**同一套落盘口径**。
|
|
941
|
+
* 复制三份的下场是「同一种档案,三个入口写出三种 AGENTS.md」——历史上已经吃过一次
|
|
942
|
+
* (getProfile 引用了不存在的 index 变量,单点 bug 让整个抽屉打不开)。
|
|
943
|
+
*
|
|
944
|
+
* 为什么技能骨架只在文件缺失时补:已存在的 SKILL.md 里可能是用户/模型写好的正文,
|
|
945
|
+
* 旧实现无条件覆盖成骨架,等于每次存档案就静默删掉一次技能内容(数据损失且无提示)。
|
|
946
|
+
*
|
|
947
|
+
* @param {object} current Bot 记录(只读 cwd / name / preset)
|
|
948
|
+
* @param {object} profile 归一后的档案
|
|
949
|
+
* @param {string} where 错误信息里的动作名
|
|
950
|
+
* @param {{ strictSkills?: boolean }} [options] strictSkills=true → 技能落盘失败即抛错
|
|
951
|
+
* @returns {{ path: string }} writeAgentsFile 的返回
|
|
952
|
+
*/
|
|
953
|
+
function writeProfileFiles(current, profile, where, options = {}) {
|
|
829
954
|
let written
|
|
830
955
|
try {
|
|
831
956
|
written = writeAgentsFile(current.cwd, {
|
|
832
|
-
...
|
|
957
|
+
...profile,
|
|
833
958
|
botName: current.name,
|
|
834
959
|
preset: current.preset,
|
|
835
960
|
})
|
|
836
|
-
for (const skillName of nextProfile.skills ?? []) {
|
|
837
|
-
try {
|
|
838
|
-
writeSkill(current.cwd, skillName, { body: `# ${skillName}\n\n(在此写技能说明)` })
|
|
839
|
-
} catch (error) {
|
|
840
|
-
warn(`[cs-bots/roster] 技能 ${skillName} 落盘失败:${error?.message ?? error}`)
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
961
|
} catch (error) {
|
|
844
962
|
throw new RosterError(
|
|
845
|
-
`roster
|
|
963
|
+
`roster.${where}:写 AGENTS.md 失败(cwd=${current.cwd})→ ${error?.message ?? error}`,
|
|
846
964
|
'ROSTER_PROFILE_WRITE_FAILED',
|
|
847
965
|
{ id: current.id, cwd: current.cwd, cause: String(error?.message ?? error) },
|
|
848
966
|
)
|
|
849
967
|
}
|
|
850
968
|
|
|
851
|
-
|
|
969
|
+
let present = new Set()
|
|
970
|
+
try {
|
|
971
|
+
present = new Set(listSkills(current.cwd))
|
|
972
|
+
} catch (error) {
|
|
973
|
+
// 列技能失败不阻塞:下一步 writeSkill 会给出带 code 的真实失败原因
|
|
974
|
+
warn(`[cs-bots/roster] 列技能失败(${current.cwd}):${error?.message ?? error}`)
|
|
975
|
+
}
|
|
976
|
+
for (const skillName of profile.skills ?? []) {
|
|
977
|
+
if (present.has(skillName)) continue
|
|
978
|
+
try {
|
|
979
|
+
writeSkill(current.cwd, skillName, { body: `# ${skillName}\n\n(在此写技能说明)` })
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (options.strictSkills) {
|
|
982
|
+
// 技能名非法与写盘失败要给不同 code:前者是调用方参数错(改参数即可),后者是环境问题
|
|
983
|
+
const badName = error?.code === 'PROFILE_BAD_SKILL_NAME'
|
|
984
|
+
throw new RosterError(
|
|
985
|
+
`roster.${where}:技能 ${JSON.stringify(skillName)} 落盘失败 → ${error?.message ?? error}`,
|
|
986
|
+
badName ? 'ROSTER_BAD_SKILL_NAME' : 'ROSTER_SKILL_WRITE_FAILED',
|
|
987
|
+
{
|
|
988
|
+
id: current.id,
|
|
989
|
+
cwd: current.cwd,
|
|
990
|
+
skill: skillName,
|
|
991
|
+
causeCode: error?.code,
|
|
992
|
+
cause: String(error?.message ?? error),
|
|
993
|
+
},
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
// 非显式技能操作(setProfile 顺带补骨架)失败只降级 warn:
|
|
997
|
+
// AGENTS.md 已写成功,档案本身有效,缺一个骨架文件不该让整次保存失败。
|
|
998
|
+
warn(`[cs-bots/roster] 技能 ${skillName} 落盘失败(不影响档案):${error?.message ?? error}`)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return written
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* 档案变更的统一收尾:rev+1 + 审计字段 + 提交事件。
|
|
1006
|
+
*
|
|
1007
|
+
* @param {object} current
|
|
1008
|
+
* @param {object} nextProfile
|
|
1009
|
+
* @param {string} by
|
|
1010
|
+
* @param {number} at
|
|
1011
|
+
* @param {{ path: string }} written
|
|
1012
|
+
* @param {string} action
|
|
1013
|
+
* @returns {object} 变更后的 Bot(克隆)
|
|
1014
|
+
*/
|
|
1015
|
+
function commitProfileChange(current, nextProfile, by, at, written, action = 'profile_updated') {
|
|
852
1016
|
const before = clone(current)
|
|
853
1017
|
const next = clone(current)
|
|
854
1018
|
next.profile = nextProfile
|
|
855
1019
|
next.profilePath = written.path
|
|
856
|
-
delete next.profileError
|
|
1020
|
+
delete next.profileError // 落盘成功即清掉历史失败标记,否则记录会永远带着一条过期错误
|
|
857
1021
|
next.rev = current.rev + 1
|
|
858
1022
|
next.updatedAt = at
|
|
859
1023
|
next.updatedBy = by
|
|
860
|
-
return commit(current.id,
|
|
1024
|
+
return commit(current.id, action, by, at, before, next)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* 改一个 Bot 的档案(身份/经验/技能/预制记忆 + v2 的 title/persona/boundaries/disciplineIds)
|
|
1029
|
+
* → 重写 AGENTS.md 托管段。
|
|
1030
|
+
*
|
|
1031
|
+
* **只动托管段**,用户在 AGENTS.md 段外写的内容原样保留。
|
|
1032
|
+
*
|
|
1033
|
+
* 语义是**浅合并**:`{ ...旧档案, ...归一后的新档案 }`。故传 `{ persona: '...' }`
|
|
1034
|
+
* 不会清掉 identity —— 想清空某个字段传空串(`''` 会被 takeText 接受);
|
|
1035
|
+
* 想整份替换请先 `get()` 再传完整对象(UI 的档案抽屉就是这么做的)。
|
|
1036
|
+
*
|
|
1037
|
+
* @param {{ id: string, profile: object, byId?: string, expectedRev?: number, now?: number }} input
|
|
1038
|
+
*/
|
|
1039
|
+
function setProfile(input = {}) {
|
|
1040
|
+
const { profile, byId, now } = input ?? {}
|
|
1041
|
+
const current = loadForCas(input, 'setProfile')
|
|
1042
|
+
const by = requireString(byId, 'byId', 'setProfile')
|
|
1043
|
+
assertProfileEditable(current, 'setProfile')
|
|
1044
|
+
const nextProfile = { ...(current.profile ?? {}), ...normalizeProfile(profile) }
|
|
1045
|
+
|
|
1046
|
+
// 先落盘,成功再记事件(避免"记录说改了但文件没变")
|
|
1047
|
+
const written = writeProfileFiles(current, nextProfile, 'setProfile')
|
|
1048
|
+
const at = normalizeNow(now, 'setProfile')
|
|
1049
|
+
return commitProfileChange(current, nextProfile, by, at, written)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* 往 Bot 的预制记忆里**追加**一批条目(v2,DESIGN-v2 特性 ③)。
|
|
1054
|
+
*
|
|
1055
|
+
* 与 setProfile 的分工:setProfile 是「整份档案覆盖」,本函数是「只加记忆、别的不动」——
|
|
1056
|
+
* 记忆是持续生长的(会话里捞到一条值得常驻的结论就顺手加进来),
|
|
1057
|
+
* 让调用方每次先 get 再整份回写,等于把并发 CAS 的窗口人为拉长,容易互相覆盖。
|
|
1058
|
+
*
|
|
1059
|
+
* 去重、空白、超限的规则全在 `botprofile.importMemoryToProfile`(纯函数,可单独测)。
|
|
1060
|
+
*
|
|
1061
|
+
* 无新增(全是重复/空白)时**不写事件、不 bump rev**:审计流里一条「什么都没改」的记录
|
|
1062
|
+
* 只会污染历史,而且调用方手里的 expectedRev 还能继续用,不必再 get() 一次。
|
|
1063
|
+
*
|
|
1064
|
+
* @param {{ id: string, entries: string[], dedup?: boolean, byId: string,
|
|
1065
|
+
* expectedRev: number, now?: number }} input
|
|
1066
|
+
* @returns {{ bot: object, added: number, skipped: number }}
|
|
1067
|
+
* @throws {RosterError} 不存在 / CAS 冲突 / 已退役 / 落盘失败
|
|
1068
|
+
* @throws {ProfileError} 条目超长或总量超限(不静默截断)
|
|
1069
|
+
*/
|
|
1070
|
+
function addProfileMemory(input = {}) {
|
|
1071
|
+
const { entries, dedup, byId, now } = input ?? {}
|
|
1072
|
+
const current = loadForCas(input, 'addProfileMemory')
|
|
1073
|
+
const by = requireString(byId, 'byId', 'addProfileMemory')
|
|
1074
|
+
assertProfileEditable(current, 'addProfileMemory')
|
|
1075
|
+
|
|
1076
|
+
const profile = current.profile ?? {}
|
|
1077
|
+
const existing = Array.isArray(profile.memory) ? profile.memory : []
|
|
1078
|
+
const merged = importMemoryToProfile({ existing, entries, dedup: dedup !== false })
|
|
1079
|
+
|
|
1080
|
+
if (merged.added === 0) {
|
|
1081
|
+
return { bot: clone(current), added: 0, skipped: merged.skipped }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const nextProfile = { ...profile, memory: merged.memory }
|
|
1085
|
+
const written = writeProfileFiles(current, nextProfile, 'addProfileMemory')
|
|
1086
|
+
const at = normalizeNow(now, 'addProfileMemory')
|
|
1087
|
+
const bot = commitProfileChange(current, nextProfile, by, at, written, 'profile_memory_added')
|
|
1088
|
+
return { bot, added: merged.added, skipped: merged.skipped }
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* 把技能**关联**到 Bot:写进 `bot.profile.skills` 并落 `.agents/skills/<名>/SKILL.md`(v2)。
|
|
1093
|
+
*
|
|
1094
|
+
* 为什么是「并集」而不是「替换」:函数名是 link(关联),语义是**追加关联**;
|
|
1095
|
+
* 已关联的技能不会被这次调用摘掉。要整体替换技能清单请用 setProfile({ skills })。
|
|
1096
|
+
*
|
|
1097
|
+
* 为什么技能文件写失败要抛错(strictSkills):技能能被官方 `skills.list({ cwd })` 发现
|
|
1098
|
+
* **全靠那个文件**;文件写不成却把名字记进 profile,等于给 Bot 一个「查无此技能」的悬空关联,
|
|
1099
|
+
* 比直接报错更坏(模型会一直找不到技能却不知道为什么)。
|
|
1100
|
+
*
|
|
1101
|
+
* 已关联且文件都在时:不写事件、不 bump rev(幂等,允许 UI 重复点「关联」)。
|
|
1102
|
+
*
|
|
1103
|
+
* @param {{ id: string, skills: string[], byId: string, expectedRev: number, now?: number }} input
|
|
1104
|
+
* @returns {object} 变更后的 Bot(无变更时返回当前记录)
|
|
1105
|
+
* @throws {RosterError} 不存在 / CAS 冲突 / 已退役 / 技能名非法 / 落盘失败
|
|
1106
|
+
*/
|
|
1107
|
+
function linkSkills(input = {}) {
|
|
1108
|
+
const { skills, byId, now } = input ?? {}
|
|
1109
|
+
const current = loadForCas(input, 'linkSkills')
|
|
1110
|
+
const by = requireString(byId, 'byId', 'linkSkills')
|
|
1111
|
+
assertProfileEditable(current, 'linkSkills')
|
|
1112
|
+
|
|
1113
|
+
if (!Array.isArray(skills) || skills.length === 0) {
|
|
1114
|
+
throw new RosterError(
|
|
1115
|
+
`roster.linkSkills:skills 必填且不能为空(收到 ${JSON.stringify(skills)})——`
|
|
1116
|
+
+ '空数组没有可关联对象,多半是调用方漏传参数',
|
|
1117
|
+
'ROSTER_BAD_FIELD',
|
|
1118
|
+
{ field: 'skills', where: 'linkSkills' },
|
|
1119
|
+
)
|
|
1120
|
+
}
|
|
1121
|
+
// 复用档案校验口径(条数 / 单项长度),避免为「关联技能」另造一套规则后与 setProfile 分叉
|
|
1122
|
+
const incoming = normalizeProfile({ skills }).skills ?? []
|
|
1123
|
+
|
|
1124
|
+
const profile = current.profile ?? {}
|
|
1125
|
+
const existing = Array.isArray(profile.skills) ? profile.skills : []
|
|
1126
|
+
const nextSkills = [...existing]
|
|
1127
|
+
let changed = false
|
|
1128
|
+
for (const name of incoming) {
|
|
1129
|
+
if (nextSkills.includes(name)) continue
|
|
1130
|
+
nextSkills.push(name)
|
|
1131
|
+
changed = true
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// 先落盘(AGENTS.md + 技能文件),成功再记事件
|
|
1135
|
+
const nextProfile = { ...profile, skills: nextSkills }
|
|
1136
|
+
const written = writeProfileFiles(current, nextProfile, 'linkSkills', { strictSkills: true })
|
|
1137
|
+
if (!changed) {
|
|
1138
|
+
// 技能清单没变(可能只是补写了缺失的技能文件)→ 不改 rev,调用方 CAS 继续有效
|
|
1139
|
+
return clone(current)
|
|
1140
|
+
}
|
|
1141
|
+
const at = normalizeNow(now, 'linkSkills')
|
|
1142
|
+
return commitProfileChange(current, nextProfile, by, at, written, 'profile_skills_linked')
|
|
861
1143
|
}
|
|
862
1144
|
|
|
863
1145
|
function setCwd(input = {}) {
|
|
@@ -889,6 +1171,39 @@ export function createRoster(ctx, config = {}) {
|
|
|
889
1171
|
return commit(current.id, 'setCwd', by, at, before, next)
|
|
890
1172
|
}
|
|
891
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* 设置/清除开工准备命令(改造07 · 借鉴 Super-agent 的 prepare command)。
|
|
1176
|
+
*
|
|
1177
|
+
* prepare command 在 Bot 首次 claim 任务时执行,用于 `git worktree`、拉代码等
|
|
1178
|
+
* 开工前准备。失败会阻塞 claim(任务保持 pending,由上层记录原因)。
|
|
1179
|
+
*
|
|
1180
|
+
* @param {{ id: string, prepareCommand?: string|null, byId: string, now?: number }} input
|
|
1181
|
+
* @returns {object} Bot
|
|
1182
|
+
* @throws {RosterError} 已退役 / CAS 冲突
|
|
1183
|
+
*/
|
|
1184
|
+
function setPrepareCommand(input = {}) {
|
|
1185
|
+
const { prepareCommand, byId, now } = input ?? {}
|
|
1186
|
+
const current = loadForCas(input, 'setPrepareCommand')
|
|
1187
|
+
const by = requireString(byId, 'byId', 'setPrepareCommand')
|
|
1188
|
+
assertNotRetired(current, 'setPrepareCommand', '设置准备命令')
|
|
1189
|
+
|
|
1190
|
+
const at = normalizeNow(now, 'setPrepareCommand')
|
|
1191
|
+
const before = clone(current)
|
|
1192
|
+
const next = clone(current)
|
|
1193
|
+
next.rev = current.rev + 1
|
|
1194
|
+
if (prepareCommand === null || prepareCommand === undefined || prepareCommand === '') {
|
|
1195
|
+
delete next.prepareCommand
|
|
1196
|
+
delete next.prepareCommandSetAt
|
|
1197
|
+
delete next.prepareCommandSetBy
|
|
1198
|
+
} else {
|
|
1199
|
+
const cmd = String(prepareCommand).trim()
|
|
1200
|
+
next.prepareCommand = cmd
|
|
1201
|
+
next.prepareCommandSetAt = at
|
|
1202
|
+
next.prepareCommandSetBy = by
|
|
1203
|
+
}
|
|
1204
|
+
return commit(current.id, 'prepare_command_set', by, at, before, next)
|
|
1205
|
+
}
|
|
1206
|
+
|
|
892
1207
|
/**
|
|
893
1208
|
* 取租约(§3.1 草案未列,见文件头「必要增量」第 3 条)。
|
|
894
1209
|
*
|
|
@@ -1090,11 +1405,16 @@ export function createRoster(ctx, config = {}) {
|
|
|
1090
1405
|
get,
|
|
1091
1406
|
list,
|
|
1092
1407
|
listRetired,
|
|
1408
|
+
hasSession,
|
|
1093
1409
|
rename,
|
|
1094
1410
|
retire,
|
|
1095
1411
|
setCwd,
|
|
1412
|
+
setPrepareCommand,
|
|
1096
1413
|
getProfile,
|
|
1097
1414
|
setProfile,
|
|
1415
|
+
// v2 档案增强(DESIGN-v2 特性 ③):追加预制记忆 / 关联技能(都走 CAS)
|
|
1416
|
+
addProfileMemory,
|
|
1417
|
+
linkSkills,
|
|
1098
1418
|
claim,
|
|
1099
1419
|
release,
|
|
1100
1420
|
heartbeat,
|