@deepmaid/maid-curator 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +59 -2
- package/cordis.patch.yml +3 -0
- package/lib/index.d.ts +335 -0
- package/lib/index.js +472 -0
- package/package.json +41 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kenyonxu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @deepmaid/maid-curator
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
共享知识层三闸门之「策展修订」(M3b 任务 10/11/12):扫描待分拣事实 → 词表提案(纯函数)→
|
|
4
|
+
dry-run 报告 → 人审点头 → 守卫批执行。分层:
|
|
5
|
+
|
|
6
|
+
| 层 | 文件 | 内容 |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| 服务函数 | `src/scan.ts` / `src/propose.ts` / `src/execute.ts` | 任务 10/11:扫描鸭子面收口、词表提案、dry-run 渲染 + 幂等批执行 |
|
|
9
|
+
| HTTP 预留面 | `src/mslm-http.ts` | MSLM daemon 策展修订客户端(MCP 面不可达的清标注/迁 shared,本批无调用路径) |
|
|
10
|
+
| cordis 插件 | `src/plugin.ts` | 任务 12:`ctx.provide('maidCurator', service)` 四操作服务面 + `curator_run` scoped 工具 |
|
|
11
|
+
| 技能本体 | 伴生仓 `~/github/maid-corps-private/skills/curator/SKILL.md` | 铁律 7:技能属女仆职责线,照 M2a 先例落伴生仓,本仓不入库 |
|
|
12
|
+
|
|
13
|
+
## 挂载方法(宿主组合)
|
|
14
|
+
|
|
15
|
+
照 doris-web 宿主组合同款 patch 条目(`maid-memory` **须先于** `maid-curator` 装配——
|
|
16
|
+
maidMemory 探测在装配时一次完成,maid-cron 同款纪律):
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
- insert:
|
|
20
|
+
- id: deepmaid-maid-curator
|
|
21
|
+
name: './maid-curator.plugin.mjs' # 或 '@deepmaid/maid-curator'
|
|
22
|
+
config:
|
|
23
|
+
# guide: { fallback: curate, rules: [...] } # 词表覆写(缺省公共仓合成词表;
|
|
24
|
+
# # 女仆私人判定规则经宿主配置注入,铁律 7)
|
|
25
|
+
# namespace: maid-legacy # 管理面覆写:显式指定策展目标 ns
|
|
26
|
+
# (缺席时按调用会话 preset 经
|
|
27
|
+
# maidMemory.resolveNamespace 解析)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
插件注册:
|
|
31
|
+
|
|
32
|
+
- **服务**:`ctx.maidCurator` —— `scan(ns)` / `propose(ns)` / `dryRun(ns)` / `apply(ns, approved)`
|
|
33
|
+
四操作,供宿主侧程序化消费。
|
|
34
|
+
- **工具**:`curator_run`(照 maid-memory `buildMemoryTools` 的 defineTool 先例,schema 静态,
|
|
35
|
+
缓存纪律):`mode: 'scan' | 'dry-run' | 'apply'` + `approved?: string[]`(apply 必填非空,
|
|
36
|
+
主人批准的 fact_id 列表)。dsh 技能是文档+既有工具、不能直接调 cordis 服务——策展技能
|
|
37
|
+
指女仆经此工具走「扫描 → 提案 → dry-run 报告落盘 → 等主人点头 → apply」全流程。
|
|
38
|
+
|
|
39
|
+
## 技能挂载(零新增配置)
|
|
40
|
+
|
|
41
|
+
技能本体在伴生仓 `maid-corps-private/skills/curator/SKILL.md`,经 dsh-skill-filesystem
|
|
42
|
+
`customSkillDirs` 发现——M2a 已挂伴生仓 `skills/` 目录(zhihui preset 先例),本技能自动
|
|
43
|
+
可发现,无新增配置。依赖方向铁律(伴生仓 README):**公共仓机制 ← 伴生仓内容**——本仓
|
|
44
|
+
不 import/引用伴生仓任何东西,只约定技能文档调用 `curator_run` 工具。
|
|
45
|
+
|
|
46
|
+
## 宿主白名单前置(任务 11 移交,必读)
|
|
47
|
+
|
|
48
|
+
mslm 车道宿主的 `SLM_MCP_TOOLS`(工具注册过滤)**须含 `update_memory` 与 `delete_memory`**
|
|
49
|
+
——策展执行面经 MCP 原位修订/删除,不在名单内则工具对 provider 不可见,apply 逐条 failed。
|
|
50
|
+
缺省白名单已含(task 11 扩入);存量部署若显式覆写过 `SLM_MCP_TOOLS` 且未含此二工具,
|
|
51
|
+
需自行补齐后重启宿主。
|
|
52
|
+
|
|
53
|
+
## 测试面
|
|
54
|
+
|
|
55
|
+
- `tests/plugin.test.ts`:cordis 真 Context 挂载冒烟(服务四操作在场、`curator_run` 注册、
|
|
56
|
+
disposer 链、maidMemory 缺席降级、ns 解析/覆写、guide 注入)+ local 假件全链
|
|
57
|
+
**scan → dry-run → apply → 复扫归零 → 重复 apply 全 skipped**(任务 11 移交的
|
|
58
|
+
update_memory 实回首验)。
|
|
59
|
+
- `tests/shape.test.ts`:插件导出形状、`curator_run` schema 静态断言(枚举三值/字节稳定)、
|
|
60
|
+
伴生仓 SKILL.md 存在性(伴生仓在场时断言,公共仓 CI 无伴生仓自动跳过)。
|
package/cordis.patch.yml
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { ToolDefinition } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
//#region ../maid-memory/src/types.d.ts
|
|
3
|
+
/** maid-memory 契约:唯一事实来源,后续任务(T2-T5)与 provider 包全部 import 此处。 */
|
|
4
|
+
interface ProviderCapabilities {
|
|
5
|
+
readonly forget: boolean;
|
|
6
|
+
readonly sharedScopes: boolean;
|
|
7
|
+
readonly supportsRecent: boolean;
|
|
8
|
+
}
|
|
9
|
+
type ProviderStatus = 'cold' | 'ready' | 'degraded';
|
|
10
|
+
/**
|
|
11
|
+
* 召回命中(M3b 任务 9 扩):scope/provenanceKind 透传 SLM 读面回显(docs/mslm-provenance-需求书
|
|
12
|
+
* §7 R4:'scope'/'provenance_kind' 两键,additive;缺席 = 个人记忆,渲染层按 scope==='global'
|
|
13
|
+
* 分流「背景事实」小节,不依赖 provenanceKind 做行为分支(v1)。
|
|
14
|
+
*/
|
|
15
|
+
interface RecallHit {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly title: string;
|
|
18
|
+
readonly content: string;
|
|
19
|
+
readonly score: number;
|
|
20
|
+
readonly createdAt?: string;
|
|
21
|
+
readonly scope?: string;
|
|
22
|
+
readonly provenanceKind?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 读通道滤镜参数(M3b 任务 9,任务 5 裁定移交;round 1 修订回规格 §3.1 原文语义):
|
|
26
|
+
* excludeTags = tag 命中条目**降权排尾**(稳定排序,不剔除——「命中集不做硬删」),与任务 6
|
|
27
|
+
* 降级链「自队尾向前逐命中降档」协同 → 被降权条目先降档先出预算;preferRecentDays =
|
|
28
|
+
* createdAt 窗口内排序靠前(排序加权不过滤);组合时降权强于时窗提升。local 假件忽略之;
|
|
29
|
+
* memory_search 工具面不走本通道。
|
|
30
|
+
*/
|
|
31
|
+
interface RecallOpts {
|
|
32
|
+
readonly excludeTags?: readonly string[];
|
|
33
|
+
readonly preferRecentDays?: number;
|
|
34
|
+
}
|
|
35
|
+
interface RecentMemory {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly content: string;
|
|
39
|
+
readonly createdAt?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 策展扫描条目(M3b 任务 10,三闸门之策展修订服务面):provider.scanUnsorted 的浅投影。
|
|
43
|
+
* scope/provenanceKind 词表语义同 RecallHit 回显(docs/mslm-provenance-需求书 §7:'world'/
|
|
44
|
+
* 'private'/'curated'/'legacy');扫描集固定 scope='global' 且未标注(provenance_kind=null),
|
|
45
|
+
* 故 provenanceKind 通常缺席——字段保留只为容读面回显毛边,不承担行为分支。
|
|
46
|
+
*/
|
|
47
|
+
interface CuratorFact {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly content: string;
|
|
50
|
+
readonly scope: string;
|
|
51
|
+
readonly provenanceKind?: string;
|
|
52
|
+
}
|
|
53
|
+
interface MemoryRecord {
|
|
54
|
+
readonly title: string;
|
|
55
|
+
readonly content: string;
|
|
56
|
+
readonly tags: readonly string[];
|
|
57
|
+
readonly importance: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 写入门 opts(M3b 任务 8,规格 §3 闸门 1):scope=global 只在守卫放行后出现(降级路径/缺省
|
|
61
|
+
* 均 personal);provenanceKind 用 SLM 落地版受控词表(docs/mslm-provenance-需求书 §7:
|
|
62
|
+
* 'world'/'private'/'curated'/'legacy',非需求书原文的 world-fact/maid-private)。
|
|
63
|
+
*/
|
|
64
|
+
interface WriteOpts {
|
|
65
|
+
readonly scope?: 'personal' | 'global';
|
|
66
|
+
readonly provenanceKind?: string;
|
|
67
|
+
}
|
|
68
|
+
interface WriteReceipt {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly deduped: boolean;
|
|
71
|
+
readonly scope?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 原位修订 opts(M3b 任务 11,闸门三策展执行):键缺席 = 不变(fork update_memory 落地版
|
|
75
|
+
* 语义,docs/mslm-provenance-需求书 §7);scope/标注类修订 in-place(fact_id 不变迁);
|
|
76
|
+
* content 修订走 correction 链(fact_id 会变迁),仅确需改写正文时携带,语义调用方自担。
|
|
77
|
+
* MCP 面不能清标注、不能迁 scope='shared'(清标注走 daemon HTTP 预留面),故本面无
|
|
78
|
+
* provenanceKind:null 形态、shared 迁移不经此。
|
|
79
|
+
*/
|
|
80
|
+
interface UpdateOpts {
|
|
81
|
+
readonly scope?: 'personal' | 'shared' | 'global';
|
|
82
|
+
readonly provenanceKind?: string;
|
|
83
|
+
readonly content?: string;
|
|
84
|
+
}
|
|
85
|
+
interface MaidMemoryProvider {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly capabilities: ProviderCapabilities;
|
|
88
|
+
status(): Promise<ProviderStatus>;
|
|
89
|
+
recall(ns: string, query: string, limit: number, opts?: RecallOpts): Promise<RecallHit[]>;
|
|
90
|
+
write(ns: string, records: MemoryRecord[], opts?: WriteOpts): Promise<WriteReceipt[]>;
|
|
91
|
+
forget?(ns: string, recordId: string): Promise<void>;
|
|
92
|
+
recent?(ns: string, limit: number): Promise<RecentMemory[]>;
|
|
93
|
+
/**
|
|
94
|
+
* 原位修订(M3b 任务 11):按 factId 精确修订既有事实的 scope/标注(/正文)。可选 =
|
|
95
|
+
* 能力声明(方法在场即能力,照 scanUnsorted 先例,不设独立 capabilities 位);缺席时
|
|
96
|
+
* 策展执行方逐条记错误不中断。mslm 实现 = MCP update_memory(remember 路由同款
|
|
97
|
+
* per-request profile_id 穿透);local 假件 = 内存态合成集同步。
|
|
98
|
+
*/
|
|
99
|
+
update?(ns: string, factId: string, opts: UpdateOpts): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* 策展扫描(M3b 任务 10):该命名空间 scope='global' 且未标注(provenance_kind=null)的
|
|
102
|
+
* 待分拣集。可选 = 能力声明(方法在场即能力,不设独立 capabilities 位——策展是低频管理
|
|
103
|
+
* 面不是读写主路);缺席时调用方按 [] 优雅降级,不视作故障。mslm 实现 = MCP list_recent
|
|
104
|
+
* 策展筛选参数(fork 落地);local 假件 = 合成 canned 集。
|
|
105
|
+
*/
|
|
106
|
+
scanUnsorted?(ns: string): Promise<CuratorFact[]>;
|
|
107
|
+
dispose(): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/propose.d.ts
|
|
111
|
+
/**
|
|
112
|
+
* maid-curator 分拣提案(M3b 任务 10,三闸门之策展修订的服务面,任务 11 技能消费)。
|
|
113
|
+
*
|
|
114
|
+
* proposeDisposition 是**纯函数**:关键词族 + 正则匹配,不调 LLM——LLM 只在技能层辅助
|
|
115
|
+
* 批量阅读,判定核心规则化可测(缓存纪律同构:确定性输入无旁路)。动作三值:
|
|
116
|
+
* - curate:留在共享层,补 curated 标注(升格);
|
|
117
|
+
* - personalize:降回个人层(scope 迁移);
|
|
118
|
+
* - delete:删除(唯一不可逆动作,证据要求最硬)。
|
|
119
|
+
*/
|
|
120
|
+
type ProposalAction = 'curate' | 'personalize' | 'delete';
|
|
121
|
+
interface Proposal {
|
|
122
|
+
readonly factId: string;
|
|
123
|
+
readonly action: ProposalAction;
|
|
124
|
+
readonly reason: string;
|
|
125
|
+
}
|
|
126
|
+
/** 单条分拣规则:keywords = 子串匹配(大小写不敏感);patterns = 正则源串(i 旗)。
|
|
127
|
+
* 空白关键词/坏正则视同未命中(fail-safe:坏指南最多误留,永不误删)。 */
|
|
128
|
+
interface GuideRule {
|
|
129
|
+
readonly id: string;
|
|
130
|
+
readonly action: ProposalAction;
|
|
131
|
+
readonly keywords?: readonly string[];
|
|
132
|
+
readonly patterns?: readonly string[];
|
|
133
|
+
}
|
|
134
|
+
/** 分拣指南(词表映射规则的结构化形态):rules 依声明序求值首中即停——优先序由指南
|
|
135
|
+
* 作者掌握,破坏性规则排前须显式声明;无命中落 fallback,缺省 'curate'(宁升不删)。 */
|
|
136
|
+
interface CurationGuide {
|
|
137
|
+
readonly fallback?: ProposalAction;
|
|
138
|
+
readonly rules?: readonly GuideRule[];
|
|
139
|
+
}
|
|
140
|
+
/** 兜底缺省(M3b 任务 10 裁定):'curate'——留共享层补标注。delete 不可逆、personalize
|
|
141
|
+
* 迁移有observable 后果,规则未说清的事实宁可原样升格,分拣报告里再由人/知惠复核。 */
|
|
142
|
+
declare const DEFAULT_FALLBACK_ACTION: ProposalAction;
|
|
143
|
+
/**
|
|
144
|
+
* 对单条待分拣事实产出处置提案。求值:guide.rules 依声明序首中即停;无命中走兜底
|
|
145
|
+
* (guide.fallback,缺省 curate)。reason 恒非空、可溯源(命中规则 id + 命中通道,或兜底
|
|
146
|
+
* 说明),供任务 11 策展报告审计。参数取 Pick<CuratorFact,'id'|'content'>——CuratorFact
|
|
147
|
+
* 天然满足,测试/调用方可传最小结构。
|
|
148
|
+
*/
|
|
149
|
+
declare function proposeDisposition(fact: Pick<CuratorFact, 'id' | 'content'>, guide: CurationGuide): Proposal;
|
|
150
|
+
/**
|
|
151
|
+
* 默认分拣指南(公共仓示例面,铁律 7:只放 Doris/合成词表)。
|
|
152
|
+
* 规则序 = 优先序:过时(唯一破坏性)→ 女仆私人 → 世界常识;无命中兜底 curate。
|
|
153
|
+
*/
|
|
154
|
+
declare const defaultCurationGuide: CurationGuide;
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/scan.d.ts
|
|
157
|
+
/**
|
|
158
|
+
* maid-curator 扫描(M3b 任务 10):拉取某命名空间 scope='global' 且未标注
|
|
159
|
+
* (provenance_kind=null)的待分拣集。provider 能力面是 maid-memory 的可选方法
|
|
160
|
+
* scanUnsorted(mslm = MCP list_recent 策展筛选;local 假件 = 合成 canned 集)——
|
|
161
|
+
* 本模块只做鸭子面收口与缺席降级,不做任何过滤(过滤语义归 provider/SLM 服务端)。
|
|
162
|
+
*/
|
|
163
|
+
/** maid-memory 服务句柄的最小消费面(照 maid-cron EnginePorts.memory 鸭子类型先例,
|
|
164
|
+
* 不引 maid-memory 运行时依赖;真 MaidMemoryService 结构兼容)。 */
|
|
165
|
+
interface CuratorMemoryFace {
|
|
166
|
+
activeProvider(): MaidMemoryProvider | undefined;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 扫描待分拣集。ns 由调用方解析(命名空间解析单点在 maid-memory plugin 侧适配器,
|
|
170
|
+
* 同 maid-cron T16 裁定);provider 缺席或未实现 scanUnsorted → [](能力缺席是配置态,
|
|
171
|
+
* 不是故障——策展链路在无该能力的部署上静默无事可做)。provider 内部失败照原样抛出
|
|
172
|
+
* (fail-loud:空页与「无事可分拣」不可区分,SLM 扫描面同款裁定)。
|
|
173
|
+
*/
|
|
174
|
+
declare function scanUnsortedFacts(memory: CuratorMemoryFace | undefined, ns: string): Promise<CuratorFact[]>;
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/execute.d.ts
|
|
177
|
+
/**
|
|
178
|
+
* maid-curator 执行面(M3b 任务 11,三闸门之策展修订):dry-run 渲染 + 守卫批执行。
|
|
179
|
+
* 策展流第 3/4 步(阶段规格 §4.3):提案全量预览给主人逐类点头 → 批准子集落到
|
|
180
|
+
* provider 原位修订面(update/forget)。失败不毁档:dry-run 在前、人审在中间、
|
|
181
|
+
* 执行逐条 fail-open,单条失败不阻断批次。
|
|
182
|
+
*/
|
|
183
|
+
/** 批执行单条失败记录(fail-open:记错误继续,不中断批次)。 */
|
|
184
|
+
interface ApplyFailure {
|
|
185
|
+
readonly factId: string;
|
|
186
|
+
readonly error: string;
|
|
187
|
+
}
|
|
188
|
+
/** 批执行报告:applied/skipped 为计数,failed 逐条可溯源。 */
|
|
189
|
+
interface ApplyReport {
|
|
190
|
+
readonly applied: number;
|
|
191
|
+
readonly skipped: number;
|
|
192
|
+
readonly failed: readonly ApplyFailure[];
|
|
193
|
+
}
|
|
194
|
+
/** applyProposals 消费的 provider 最小面(照 scan.ts CuratorMemoryFace 先例鸭子收口,
|
|
195
|
+
* 不引 maid-memory 运行时依赖;真 MaidMemoryProvider 结构兼容)。 */
|
|
196
|
+
interface CuratorApplyFace {
|
|
197
|
+
/** 幂等守卫数据源:在场时 apply 先重扫,已离开待分拣集的 factId 直接 skipped。 */
|
|
198
|
+
scanUnsorted?(ns: string): Promise<CuratorFact[]>;
|
|
199
|
+
update?(ns: string, factId: string, opts: UpdateOpts): Promise<void>;
|
|
200
|
+
forget?(ns: string, recordId: string): Promise<void>;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* dry-run 报告(markdown 表格:动作/原文摘录 ≤80 字/理由)。全部提案逐行在场——包括
|
|
204
|
+
* 扫描集里已找不到对应条目的提案(占位含 factId,不静默丢提案:预览与实际执行面之间
|
|
205
|
+
* 的漂移要在人眼前可见)。facts 是扫描快照(通常就是产提案的那次 scanUnsorted 结果)。
|
|
206
|
+
*/
|
|
207
|
+
declare function renderDryRun(proposals: readonly Proposal[], facts: readonly CuratorFact[]): string;
|
|
208
|
+
/**
|
|
209
|
+
* 守卫批执行:只执行 approved 内的提案;逐条 fail-open(失败记 {factId, error} 继续,
|
|
210
|
+
* 不中断批次)。幂等语义:provider 带 scanUnsorted 时,apply 先重扫待分拣集——已处置
|
|
211
|
+
* (升格/降格/删除后离开 global+null 集)或本就不在集内的 factId 直接 skipped,重复
|
|
212
|
+
* apply 同批第二次全部 skipped。守卫扫描本身失败则整批抛出(fail-loud:delete 不可逆,
|
|
213
|
+
* 守卫立不起来就不动手);provider 缺 scanUnsorted = 守卫缺席降级,批准项直接执行。
|
|
214
|
+
* provider 缺对应能力方法(update/forget)按逐条失败记录,不视作批次故障。
|
|
215
|
+
*/
|
|
216
|
+
declare function applyProposals(provider: CuratorApplyFace, ns: string, proposals: readonly Proposal[], approved: ReadonlySet<string>): Promise<ApplyReport>;
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/mslm-http.d.ts
|
|
219
|
+
/**
|
|
220
|
+
* MSLM daemon HTTP 面客户端(**预留**,M3b 任务 10):仅封装、本批无任何调用路径,
|
|
221
|
+
* 策展执行器(任务 11+)消费。存在的理由——SLM §7 注意事项①:MCP 面不能清标注、
|
|
222
|
+
* 不能迁 scope=shared(update_memory 的 "" 语义是「不变」);清标注(provenance_kind:
|
|
223
|
+
* null)与 shared 迁移(带 shared_with)只能走 daemon HTTP 面 PATCH /api/memories/{id}。
|
|
224
|
+
*
|
|
225
|
+
* fork 契约(源码 routes/memories.py edit_memory,2026-09-13 落地版):
|
|
226
|
+
* - `?profile_id=` 是**查询参数**(per-request 路由,缺席 = daemon 活跃档);
|
|
227
|
+
* - curation-only body(无 content)= 原位列更新,fact_id 不变迁,plain 200;
|
|
228
|
+
* - provenance_kind 传 JSON null 或空串 = 显式清标注;scope 可携 shared_with;
|
|
229
|
+
* - 词表外取值 400(受控策展面不容模糊);
|
|
230
|
+
* - daemon 绑定 127.0.0.1:8765(SLM_DAEMON_PORT env 可覆写,本客户端同读)。
|
|
231
|
+
*/
|
|
232
|
+
interface MslmHttpOptions {
|
|
233
|
+
/** 缺省 http://127.0.0.1:<SLM_DAEMON_PORT ?? 8765>(fork daemon.py 同源缺省) */
|
|
234
|
+
readonly baseUrl?: string;
|
|
235
|
+
readonly fetchImpl?: typeof fetch;
|
|
236
|
+
readonly timeoutMs?: number;
|
|
237
|
+
}
|
|
238
|
+
/** curation-only 修订面(不带 content——content 修订走 correction 链 fact_id 会变迁,
|
|
239
|
+
* 不是策展通道;真要带时本类型留 content 键给上层,语义自担)。 */
|
|
240
|
+
interface MslmCurationPatch {
|
|
241
|
+
readonly scope?: 'personal' | 'shared' | 'global';
|
|
242
|
+
/** 受控词表 'world' | 'private' | 'curated' | 'legacy';null = 显式清标注 */
|
|
243
|
+
readonly provenanceKind?: string | null;
|
|
244
|
+
/** scope='shared' 时必填(fork 语义);字符串(逗号分隔)或数组 */
|
|
245
|
+
readonly sharedWith?: readonly string[] | string;
|
|
246
|
+
readonly content?: string;
|
|
247
|
+
}
|
|
248
|
+
interface MslmCurateOptions {
|
|
249
|
+
/** 命名空间锚定(查询参数);缺席 = daemon 活跃档(现状字节不变) */
|
|
250
|
+
readonly profileId?: string;
|
|
251
|
+
}
|
|
252
|
+
interface MslmHttpClient {
|
|
253
|
+
/** PATCH /api/memories/{factId}:原位策展修订,成功回解析后的响应体,非 2xx fail-loud。 */
|
|
254
|
+
curateFact(factId: string, patch: MslmCurationPatch, opts?: MslmCurateOptions): Promise<Record<string, unknown>>;
|
|
255
|
+
}
|
|
256
|
+
declare function defaultMslmBaseUrl(): string;
|
|
257
|
+
declare function createMslmHttpClient(options?: MslmHttpOptions): MslmHttpClient;
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/plugin.d.ts
|
|
260
|
+
/**
|
|
261
|
+
* maid-curator cordis 插件形态(M3b 任务 12,三闸门之策展修订的挂载面)。
|
|
262
|
+
*
|
|
263
|
+
* 任务 10/11 的服务函数在此组装为一个 cordis 服务 + 一个 scoped 工具:
|
|
264
|
+
* - `ctx.provide('maidCurator', service)`:四操作服务面(scan/propose/dryRun/apply),
|
|
265
|
+
* 供宿主侧程序化消费(照 maid-anchor 的 apply 形态,单文件插件);
|
|
266
|
+
* - `curator_run` 工具(照 maid-memory buildMemoryTools 的 defineTool 先例):dsh 技能是
|
|
267
|
+
* 文档+既有工具,不能直接调 cordis 服务——策展技能本体(伴生仓 skills/curator/SKILL.md,
|
|
268
|
+
* 铁律 7)指女仆经此工具走完「扫描→提案→dry-run 报告落盘→等主人点头→apply」全流程。
|
|
269
|
+
* schema 静态(mode 三值枚举 + approved fact_id 列表,缓存纪律:会话内字节稳定)。
|
|
270
|
+
*
|
|
271
|
+
* maidMemory 消费走 `ctx.reflect.get('maidMemory', false)`(maid-cron 同款纪律):兄弟
|
|
272
|
+
* fiber 的插件 store 对本 fiber proxy GET 不可达,免 inject 全局直读;装配时探一次,
|
|
273
|
+
* maid-memory 须先于 maid-curator 装配(组装顺序由 host 保证,README 记述)。缺席 →
|
|
274
|
+
* warn 一次,scan/dry-run 优雅降级为空(scan.ts 能力缺席语义),apply fail-loud 拒绝。
|
|
275
|
+
*
|
|
276
|
+
* ns 解析单点收口(照 maid-cron T16 裁定):config.namespace 覆写(管理面挂载,如替
|
|
277
|
+
* 冻结女仆分拣遗产集)> 会话 preset 经 maidMemory.resolveNamespace(allowMaids 守卫在
|
|
278
|
+
* 此生效);两者都解析不出 → fail-loud 拒绝(策展是写操作,不静默错命名空间)。
|
|
279
|
+
*
|
|
280
|
+
* apply 语义:从**当次重扫**重新生成确定性提案再按 approved 子集执行——proposeDisposition
|
|
281
|
+
* 是纯函数,同事实同指南必同提案;dry-run 与 apply 之间的漂移由 applyProposals 的幂等
|
|
282
|
+
* 守卫重扫兜底(已处置/消失的 factId 直接 skipped,不静默丢)。不认同某条的**动作**时
|
|
283
|
+
* 正确路径是调词表指南(config.guide)重跑 dry-run,而不是在 apply 时改动作。
|
|
284
|
+
*/
|
|
285
|
+
declare const name = "maid-curator";
|
|
286
|
+
declare const inject: string[];
|
|
287
|
+
interface MaidCuratorConfig {
|
|
288
|
+
/** 词表映射指南(结构化,rules 依声明序首中即停)。缺省 defaultCurationGuide(公共仓
|
|
289
|
+
* 合成词表);女仆私人判定规则(如知惠专属 personalize 词表)经宿主/组合配置注入——
|
|
290
|
+
* 铁律 7:私有内容不落本仓。 */
|
|
291
|
+
readonly guide?: CurationGuide;
|
|
292
|
+
/** 命名空间覆写(管理面挂载):显式指定策展目标 ns(如分拣历史遗产集),不走会话
|
|
293
|
+
* preset 解析。缺席时按调用会话解析。 */
|
|
294
|
+
readonly namespace?: string;
|
|
295
|
+
}
|
|
296
|
+
/** maidMemory 服务的策展消费面(鸭子收口,照 scan.ts CuratorMemoryFace 先例,不引
|
|
297
|
+
* maid-memory 运行时依赖;真 MaidMemoryService 结构兼容)。 */
|
|
298
|
+
interface CuratorMemoryServiceFace extends CuratorMemoryFace {
|
|
299
|
+
resolveNamespace?(agentPreset: string | undefined): string | undefined;
|
|
300
|
+
}
|
|
301
|
+
/** 一次策展计划:待分拣事实快照 + 确定性提案(同输入必同输出,纯函数产物)。 */
|
|
302
|
+
interface CuratorPlan {
|
|
303
|
+
readonly facts: readonly CuratorFact[];
|
|
304
|
+
readonly proposals: readonly Proposal[];
|
|
305
|
+
}
|
|
306
|
+
/** dry-run 产物:计划 + 给主人过目的 markdown 报告。 */
|
|
307
|
+
interface CuratorDryRun extends CuratorPlan {
|
|
308
|
+
readonly report: string;
|
|
309
|
+
}
|
|
310
|
+
interface MaidCuratorService {
|
|
311
|
+
/** 扫描待分拣集(scope='global' 且未标注);provider/能力缺席 → [](配置态降级)。 */
|
|
312
|
+
scan(ns: string): Promise<readonly CuratorFact[]>;
|
|
313
|
+
/** 扫描 + 词表提案(纯函数,不调 LLM)。 */
|
|
314
|
+
propose(ns: string): Promise<CuratorPlan>;
|
|
315
|
+
/** 扫描 + 提案 + dry-run 报告(markdown 表格)。 */
|
|
316
|
+
dryRun(ns: string): Promise<CuratorDryRun>;
|
|
317
|
+
/** 守卫批执行:从当次重扫重新提案,只执行 approved 内的 factId(幂等经重扫,任务 11);
|
|
318
|
+
* 已离开待分拣集的 approved id 计入 skipped(重复 apply 同批第二次全 skipped)。 */
|
|
319
|
+
apply(ns: string, approved: readonly string[]): Promise<ApplyReport>;
|
|
320
|
+
}
|
|
321
|
+
declare function createMaidCuratorService(memory: CuratorMemoryServiceFace | undefined, guide?: CurationGuide): MaidCuratorService;
|
|
322
|
+
/** 工具装配依赖:ns 解析(config.namespace 覆写 / 会话 preset 单点解析)。 */
|
|
323
|
+
interface CuratorToolDeps {
|
|
324
|
+
resolveNamespace(exec: unknown): string;
|
|
325
|
+
}
|
|
326
|
+
/** 恰一个 scoped 工具(照 maid-memory「恰好两个工具」纪律):三模式覆盖策展全流程。 */
|
|
327
|
+
declare function buildCuratorTool(service: MaidCuratorService, deps: CuratorToolDeps): ToolDefinition[];
|
|
328
|
+
declare function registerCuratorTool(ctx: {
|
|
329
|
+
tools: {
|
|
330
|
+
register(definition: ToolDefinition): () => void;
|
|
331
|
+
};
|
|
332
|
+
}, service: MaidCuratorService, deps: CuratorToolDeps): () => void;
|
|
333
|
+
declare const apply: (ctx: any, config: MaidCuratorConfig) => (() => void);
|
|
334
|
+
//#endregion
|
|
335
|
+
export { type ApplyFailure, type ApplyReport, type CurationGuide, type CuratorApplyFace, type CuratorDryRun, type CuratorFact, type CuratorMemoryFace, type CuratorMemoryServiceFace, type CuratorPlan, type CuratorToolDeps, DEFAULT_FALLBACK_ACTION, type GuideRule, type MaidCuratorConfig, type MaidCuratorService, type MslmCurateOptions, type MslmCurationPatch, type MslmHttpClient, type MslmHttpOptions, type Proposal, type ProposalAction, apply, applyProposals, buildCuratorTool, createMaidCuratorService, createMslmHttpClient, defaultCurationGuide, defaultMslmBaseUrl, inject, name, proposeDisposition, registerCuratorTool, renderDryRun, scanUnsortedFacts };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
//#region src/propose.ts
|
|
3
|
+
/** 兜底缺省(M3b 任务 10 裁定):'curate'——留共享层补标注。delete 不可逆、personalize
|
|
4
|
+
* 迁移有observable 后果,规则未说清的事实宁可原样升格,分拣报告里再由人/知惠复核。 */
|
|
5
|
+
const DEFAULT_FALLBACK_ACTION = "curate";
|
|
6
|
+
function matchRule(rule, contentLower) {
|
|
7
|
+
for (const keyword of rule.keywords ?? []) {
|
|
8
|
+
const needle = keyword.trim().toLowerCase();
|
|
9
|
+
if (needle === "") continue;
|
|
10
|
+
if (contentLower.includes(needle)) return {
|
|
11
|
+
rule,
|
|
12
|
+
channel: "keyword",
|
|
13
|
+
hit: keyword
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
for (const source of rule.patterns ?? []) {
|
|
17
|
+
if (source.trim() === "") continue;
|
|
18
|
+
let re;
|
|
19
|
+
try {
|
|
20
|
+
re = new RegExp(source, "i");
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (re.test(contentLower)) return {
|
|
25
|
+
rule,
|
|
26
|
+
channel: "pattern",
|
|
27
|
+
hit: source
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 对单条待分拣事实产出处置提案。求值:guide.rules 依声明序首中即停;无命中走兜底
|
|
33
|
+
* (guide.fallback,缺省 curate)。reason 恒非空、可溯源(命中规则 id + 命中通道,或兜底
|
|
34
|
+
* 说明),供任务 11 策展报告审计。参数取 Pick<CuratorFact,'id'|'content'>——CuratorFact
|
|
35
|
+
* 天然满足,测试/调用方可传最小结构。
|
|
36
|
+
*/
|
|
37
|
+
function proposeDisposition(fact, guide) {
|
|
38
|
+
const contentLower = fact.content.toLowerCase();
|
|
39
|
+
for (const rule of guide.rules ?? []) {
|
|
40
|
+
const matched = matchRule(rule, contentLower);
|
|
41
|
+
if (matched !== void 0) return {
|
|
42
|
+
factId: fact.id,
|
|
43
|
+
action: matched.rule.action,
|
|
44
|
+
reason: `命中规则「${matched.rule.id}」(${matched.channel === "keyword" ? "关键词" : "模式"}「${matched.hit}」)`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const action = guide.fallback ?? "curate";
|
|
48
|
+
return {
|
|
49
|
+
factId: fact.id,
|
|
50
|
+
action,
|
|
51
|
+
reason: action === "curate" ? "无规则命中,走兜底「curate」(fail-safe 宁升不删)" : `无规则命中,走兜底「${action}」(guide.fallback 显式配置)`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 默认分拣指南(公共仓示例面,铁律 7:只放 Doris/合成词表)。
|
|
56
|
+
* 规则序 = 优先序:过时(唯一破坏性)→ 女仆私人 → 世界常识;无命中兜底 curate。
|
|
57
|
+
*/
|
|
58
|
+
const defaultCurationGuide = {
|
|
59
|
+
fallback: "curate",
|
|
60
|
+
rules: [
|
|
61
|
+
{
|
|
62
|
+
id: "stale-explicit",
|
|
63
|
+
action: "delete",
|
|
64
|
+
keywords: [
|
|
65
|
+
"已停用",
|
|
66
|
+
"已过期",
|
|
67
|
+
"已失效",
|
|
68
|
+
"已作废",
|
|
69
|
+
"已更换",
|
|
70
|
+
"已搬离",
|
|
71
|
+
"已注销"
|
|
72
|
+
],
|
|
73
|
+
patterns: ["不再(适用|使用|有效|成立)", "(旧|原)(手机号|电话|号码|住址|地址|座位|工位|工作单位)"]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "maid-private",
|
|
77
|
+
action: "personalize",
|
|
78
|
+
keywords: [
|
|
79
|
+
"与主人讨论过",
|
|
80
|
+
"和主人聊过",
|
|
81
|
+
"陪主人",
|
|
82
|
+
"私下",
|
|
83
|
+
"悄悄话",
|
|
84
|
+
"两人之间"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "world-knowledge",
|
|
89
|
+
action: "curate",
|
|
90
|
+
keywords: [
|
|
91
|
+
"主人吃",
|
|
92
|
+
"主人喝",
|
|
93
|
+
"主人喜欢",
|
|
94
|
+
"主人讨厌",
|
|
95
|
+
"主人习惯",
|
|
96
|
+
"主人作息",
|
|
97
|
+
"口味",
|
|
98
|
+
"偏好",
|
|
99
|
+
"过敏",
|
|
100
|
+
"深蓝馆"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/scan.ts
|
|
107
|
+
/**
|
|
108
|
+
* 扫描待分拣集。ns 由调用方解析(命名空间解析单点在 maid-memory plugin 侧适配器,
|
|
109
|
+
* 同 maid-cron T16 裁定);provider 缺席或未实现 scanUnsorted → [](能力缺席是配置态,
|
|
110
|
+
* 不是故障——策展链路在无该能力的部署上静默无事可做)。provider 内部失败照原样抛出
|
|
111
|
+
* (fail-loud:空页与「无事可分拣」不可区分,SLM 扫描面同款裁定)。
|
|
112
|
+
*/
|
|
113
|
+
async function scanUnsortedFacts(memory, ns) {
|
|
114
|
+
const provider = memory?.activeProvider();
|
|
115
|
+
if (provider === void 0 || provider.scanUnsorted === void 0) return [];
|
|
116
|
+
return provider.scanUnsorted(ns);
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/execute.ts
|
|
120
|
+
const EXCERPT_MAX = 80;
|
|
121
|
+
/** 单元格卫生:竖线转义 + 换行折叠(表格行结构不破);摘录截断 ≤80 字(码点计,79 + 省略号)。 */
|
|
122
|
+
function excerpt(content) {
|
|
123
|
+
const flat = content.replace(/\s+/g, " ").trim().replace(/\|/g, "\\|");
|
|
124
|
+
const cps = Array.from(flat);
|
|
125
|
+
return cps.length <= EXCERPT_MAX ? flat : `${cps.slice(0, 79).join("")}…`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* dry-run 报告(markdown 表格:动作/原文摘录 ≤80 字/理由)。全部提案逐行在场——包括
|
|
129
|
+
* 扫描集里已找不到对应条目的提案(占位含 factId,不静默丢提案:预览与实际执行面之间
|
|
130
|
+
* 的漂移要在人眼前可见)。facts 是扫描快照(通常就是产提案的那次 scanUnsorted 结果)。
|
|
131
|
+
*/
|
|
132
|
+
function renderDryRun(proposals, facts) {
|
|
133
|
+
const byId = new Map(facts.map((f) => [f.id, f]));
|
|
134
|
+
const lines = [
|
|
135
|
+
`## 策展提案 dry-run(${proposals.length} 条)`,
|
|
136
|
+
"",
|
|
137
|
+
"| 动作 | 原文摘录 | 理由 |",
|
|
138
|
+
"| --- | --- | --- |"
|
|
139
|
+
];
|
|
140
|
+
for (const proposal of proposals) {
|
|
141
|
+
const fact = byId.get(proposal.factId);
|
|
142
|
+
const cell = fact !== void 0 ? excerpt(fact.content) : `(条目已不在扫描集:${proposal.factId})`;
|
|
143
|
+
lines.push(`| ${proposal.action} | ${cell} | ${proposal.reason} |`);
|
|
144
|
+
}
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
/** 动作 → 原位修订参数(阶段规格 §4.3 词表映射):curate = 留共享层补 curated 标注
|
|
148
|
+
* (scope 维持 global);personalize = 迁 personal + 打 private(升降 scope + 打标)。 */
|
|
149
|
+
function patchFor(action) {
|
|
150
|
+
if (action === "curate") return { provenanceKind: "curated" };
|
|
151
|
+
if (action === "personalize") return {
|
|
152
|
+
scope: "personal",
|
|
153
|
+
provenanceKind: "private"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 守卫批执行:只执行 approved 内的提案;逐条 fail-open(失败记 {factId, error} 继续,
|
|
158
|
+
* 不中断批次)。幂等语义:provider 带 scanUnsorted 时,apply 先重扫待分拣集——已处置
|
|
159
|
+
* (升格/降格/删除后离开 global+null 集)或本就不在集内的 factId 直接 skipped,重复
|
|
160
|
+
* apply 同批第二次全部 skipped。守卫扫描本身失败则整批抛出(fail-loud:delete 不可逆,
|
|
161
|
+
* 守卫立不起来就不动手);provider 缺 scanUnsorted = 守卫缺席降级,批准项直接执行。
|
|
162
|
+
* provider 缺对应能力方法(update/forget)按逐条失败记录,不视作批次故障。
|
|
163
|
+
*/
|
|
164
|
+
async function applyProposals(provider, ns, proposals, approved) {
|
|
165
|
+
let stillUnsorted;
|
|
166
|
+
if (provider.scanUnsorted !== void 0) stillUnsorted = new Set((await provider.scanUnsorted(ns)).map((f) => f.id));
|
|
167
|
+
let applied = 0;
|
|
168
|
+
let skipped = 0;
|
|
169
|
+
const failed = [];
|
|
170
|
+
for (const proposal of proposals) {
|
|
171
|
+
if (!approved.has(proposal.factId)) {
|
|
172
|
+
skipped += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (stillUnsorted !== void 0 && !stillUnsorted.has(proposal.factId)) {
|
|
176
|
+
skipped += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
if (proposal.action === "delete") {
|
|
181
|
+
if (provider.forget === void 0) throw new Error("provider 未实现 forget,delete 不可执行");
|
|
182
|
+
await provider.forget(ns, proposal.factId);
|
|
183
|
+
} else {
|
|
184
|
+
if (provider.update === void 0) throw new Error("provider 未实现 update,curate/personalize 不可执行");
|
|
185
|
+
await provider.update(ns, proposal.factId, patchFor(proposal.action));
|
|
186
|
+
}
|
|
187
|
+
applied += 1;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
failed.push({
|
|
190
|
+
factId: proposal.factId,
|
|
191
|
+
error: error instanceof Error ? error.message : String(error)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
applied,
|
|
197
|
+
skipped,
|
|
198
|
+
failed
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/mslm-http.ts
|
|
203
|
+
function defaultMslmBaseUrl() {
|
|
204
|
+
const raw = process.env.SLM_DAEMON_PORT?.trim();
|
|
205
|
+
return `http://127.0.0.1:${raw !== void 0 && raw !== "" ? raw : "8765"}`;
|
|
206
|
+
}
|
|
207
|
+
function createMslmHttpClient(options = {}) {
|
|
208
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
209
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
210
|
+
return { async curateFact(factId, patch, opts = {}) {
|
|
211
|
+
const base = (options.baseUrl ?? defaultMslmBaseUrl()).replace(/\/+$/, "");
|
|
212
|
+
const qs = opts.profileId !== void 0 && opts.profileId !== "" ? `?profile_id=${encodeURIComponent(opts.profileId)}` : "";
|
|
213
|
+
const body = {};
|
|
214
|
+
if (patch.scope !== void 0) body.scope = patch.scope;
|
|
215
|
+
if (patch.provenanceKind !== void 0) body.provenance_kind = patch.provenanceKind;
|
|
216
|
+
if (patch.sharedWith !== void 0) body.shared_with = patch.sharedWith;
|
|
217
|
+
if (patch.content !== void 0) body.content = patch.content;
|
|
218
|
+
const response = await doFetch(`${base}/api/memories/${encodeURIComponent(factId)}${qs}`, {
|
|
219
|
+
method: "PATCH",
|
|
220
|
+
headers: { "content-type": "application/json" },
|
|
221
|
+
body: JSON.stringify(body),
|
|
222
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
223
|
+
});
|
|
224
|
+
const text = await response.text();
|
|
225
|
+
if (!response.ok) throw new Error(`maid-curator: PATCH /api/memories/${factId} 失败(HTTP ${response.status}):${text.slice(0, 300)}`);
|
|
226
|
+
if (text.trim() === "") return {};
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(text);
|
|
229
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
230
|
+
} catch {
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
} };
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/plugin.ts
|
|
237
|
+
/**
|
|
238
|
+
* maid-curator cordis 插件形态(M3b 任务 12,三闸门之策展修订的挂载面)。
|
|
239
|
+
*
|
|
240
|
+
* 任务 10/11 的服务函数在此组装为一个 cordis 服务 + 一个 scoped 工具:
|
|
241
|
+
* - `ctx.provide('maidCurator', service)`:四操作服务面(scan/propose/dryRun/apply),
|
|
242
|
+
* 供宿主侧程序化消费(照 maid-anchor 的 apply 形态,单文件插件);
|
|
243
|
+
* - `curator_run` 工具(照 maid-memory buildMemoryTools 的 defineTool 先例):dsh 技能是
|
|
244
|
+
* 文档+既有工具,不能直接调 cordis 服务——策展技能本体(伴生仓 skills/curator/SKILL.md,
|
|
245
|
+
* 铁律 7)指女仆经此工具走完「扫描→提案→dry-run 报告落盘→等主人点头→apply」全流程。
|
|
246
|
+
* schema 静态(mode 三值枚举 + approved fact_id 列表,缓存纪律:会话内字节稳定)。
|
|
247
|
+
*
|
|
248
|
+
* maidMemory 消费走 `ctx.reflect.get('maidMemory', false)`(maid-cron 同款纪律):兄弟
|
|
249
|
+
* fiber 的插件 store 对本 fiber proxy GET 不可达,免 inject 全局直读;装配时探一次,
|
|
250
|
+
* maid-memory 须先于 maid-curator 装配(组装顺序由 host 保证,README 记述)。缺席 →
|
|
251
|
+
* warn 一次,scan/dry-run 优雅降级为空(scan.ts 能力缺席语义),apply fail-loud 拒绝。
|
|
252
|
+
*
|
|
253
|
+
* ns 解析单点收口(照 maid-cron T16 裁定):config.namespace 覆写(管理面挂载,如替
|
|
254
|
+
* 冻结女仆分拣遗产集)> 会话 preset 经 maidMemory.resolveNamespace(allowMaids 守卫在
|
|
255
|
+
* 此生效);两者都解析不出 → fail-loud 拒绝(策展是写操作,不静默错命名空间)。
|
|
256
|
+
*
|
|
257
|
+
* apply 语义:从**当次重扫**重新生成确定性提案再按 approved 子集执行——proposeDisposition
|
|
258
|
+
* 是纯函数,同事实同指南必同提案;dry-run 与 apply 之间的漂移由 applyProposals 的幂等
|
|
259
|
+
* 守卫重扫兜底(已处置/消失的 factId 直接 skipped,不静默丢)。不认同某条的**动作**时
|
|
260
|
+
* 正确路径是调词表指南(config.guide)重跑 dry-run,而不是在 apply 时改动作。
|
|
261
|
+
*/
|
|
262
|
+
const name = "maid-curator";
|
|
263
|
+
const inject = ["tools"];
|
|
264
|
+
function createMaidCuratorService(memory, guide = defaultCurationGuide) {
|
|
265
|
+
const plan = async (ns) => {
|
|
266
|
+
const facts = await scanUnsortedFacts(memory, ns);
|
|
267
|
+
return {
|
|
268
|
+
facts,
|
|
269
|
+
proposals: facts.map((fact) => proposeDisposition(fact, guide))
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
scan: (ns) => scanUnsortedFacts(memory, ns),
|
|
274
|
+
propose: plan,
|
|
275
|
+
dryRun: async (ns) => {
|
|
276
|
+
const p = await plan(ns);
|
|
277
|
+
return {
|
|
278
|
+
...p,
|
|
279
|
+
report: renderDryRun(p.proposals, p.facts)
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
apply: async (ns, approved) => {
|
|
283
|
+
const provider = memory?.activeProvider();
|
|
284
|
+
if (provider === void 0) throw new Error("maid-curator: 无可用记忆 provider,策展执行被拒绝");
|
|
285
|
+
const { proposals } = await plan(ns);
|
|
286
|
+
const report = await applyProposals(provider, ns, proposals, new Set(approved));
|
|
287
|
+
const known = new Set(proposals.map((p) => p.factId));
|
|
288
|
+
const dropped = [...new Set(approved)].filter((id) => !known.has(id));
|
|
289
|
+
return {
|
|
290
|
+
...report,
|
|
291
|
+
skipped: report.skipped + dropped.length
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const RENDER_EXCERPT_MAX = 120;
|
|
297
|
+
/** render 行内摘录截断(码点计;表格转义是 renderDryRun 的事,这里只防长文刷屏)。 */
|
|
298
|
+
function excerptOf(content) {
|
|
299
|
+
const cps = Array.from(content.replace(/\s+/g, " ").trim());
|
|
300
|
+
return cps.length <= RENDER_EXCERPT_MAX ? cps.join("") : `${cps.slice(0, 119).join("")}…`;
|
|
301
|
+
}
|
|
302
|
+
/** 恰一个 scoped 工具(照 maid-memory「恰好两个工具」纪律):三模式覆盖策展全流程。 */
|
|
303
|
+
function buildCuratorTool(service, deps) {
|
|
304
|
+
return [defineTool({
|
|
305
|
+
name: "curator_run",
|
|
306
|
+
description: "策展你自己的共享知识层(跨会话记忆里全体女仆共享的事实分拣修订)。mode=scan:扫描待分拣事实并给出词表提案;mode=dry-run:产出完整分拣报告(给主人过目,报告必须先呈给主人);mode=apply:执行主人批准的子集(approved 填主人点头的 fact_id 列表)。铁律:绝不在主人明确点头之前执行 apply;delete 不可逆,证据不足宁可留待复核。",
|
|
307
|
+
parameters: {
|
|
308
|
+
mode: {
|
|
309
|
+
type: "string",
|
|
310
|
+
required: true,
|
|
311
|
+
enum: [
|
|
312
|
+
"scan",
|
|
313
|
+
"dry-run",
|
|
314
|
+
"apply"
|
|
315
|
+
],
|
|
316
|
+
description: "scan=扫描+词表提案;dry-run=分拣报告;apply=执行批准子集"
|
|
317
|
+
},
|
|
318
|
+
approved: {
|
|
319
|
+
type: "array",
|
|
320
|
+
items: { type: "string" },
|
|
321
|
+
description: "仅 mode=apply 必填:主人批准的 fact_id 列表(非空;主人未批准任何条目时不要调用 apply)"
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
output: {
|
|
325
|
+
schema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
additionalProperties: false,
|
|
328
|
+
properties: {
|
|
329
|
+
mode: {
|
|
330
|
+
type: "string",
|
|
331
|
+
required: true
|
|
332
|
+
},
|
|
333
|
+
count: {
|
|
334
|
+
type: "integer",
|
|
335
|
+
description: "scan/dry-run:待分拣条数"
|
|
336
|
+
},
|
|
337
|
+
facts: {
|
|
338
|
+
type: "json",
|
|
339
|
+
description: "scan/dry-run:待分拣事实(id/content/scope)"
|
|
340
|
+
},
|
|
341
|
+
proposals: {
|
|
342
|
+
type: "json",
|
|
343
|
+
description: "scan/dry-run:确定性提案(factId/action/reason)"
|
|
344
|
+
},
|
|
345
|
+
report: {
|
|
346
|
+
type: "string",
|
|
347
|
+
description: "dry-run:markdown 分拣报告"
|
|
348
|
+
},
|
|
349
|
+
applied: {
|
|
350
|
+
type: "integer",
|
|
351
|
+
description: "apply:已执行条数"
|
|
352
|
+
},
|
|
353
|
+
skipped: {
|
|
354
|
+
type: "integer",
|
|
355
|
+
description: "apply:跳过条数(未批准/已处置)"
|
|
356
|
+
},
|
|
357
|
+
failed: {
|
|
358
|
+
type: "json",
|
|
359
|
+
description: "apply:逐条失败记录(factId/error)"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
render: (_args, value) => {
|
|
364
|
+
const v = value;
|
|
365
|
+
if (v.mode === "apply") {
|
|
366
|
+
const failed = v.failed ?? [];
|
|
367
|
+
const lines = [`策展执行完成:已执行 ${v.applied ?? 0} 条,跳过 ${v.skipped ?? 0} 条,失败 ${failed.length} 条。`];
|
|
368
|
+
for (const failure of failed) lines.push(`- ${failure.factId}:${failure.error}`);
|
|
369
|
+
return [{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: lines.join("\n")
|
|
372
|
+
}];
|
|
373
|
+
}
|
|
374
|
+
if (v.mode === "dry-run") return [{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: `${v.report ?? ""}\n\n(dry-run 报告:须呈主人过目并等点头后,才能以 mode=apply 执行,approved 填主人批准的 fact_id)`
|
|
377
|
+
}];
|
|
378
|
+
const proposals = new Map((v.proposals ?? []).map((p) => [p.factId, p]));
|
|
379
|
+
const lines = [`策展扫描:待分拣 ${v.count ?? 0} 条。`];
|
|
380
|
+
if ((v.count ?? 0) === 0) lines.push("共享层暂无待分拣事实,无事可做。");
|
|
381
|
+
for (const fact of v.facts ?? []) {
|
|
382
|
+
lines.push(`- ${fact.id}:${excerptOf(fact.content)}`);
|
|
383
|
+
const proposal = proposals.get(fact.id);
|
|
384
|
+
if (proposal !== void 0) lines.push(` → 词表提案:${proposal.action}(${proposal.reason})`);
|
|
385
|
+
}
|
|
386
|
+
return [{
|
|
387
|
+
type: "text",
|
|
388
|
+
text: lines.join("\n")
|
|
389
|
+
}];
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
execute: async (args, exec) => {
|
|
393
|
+
const typed = args;
|
|
394
|
+
const ns = deps.resolveNamespace(exec);
|
|
395
|
+
if (typed.mode === "apply") {
|
|
396
|
+
const approved = typed.approved ?? [];
|
|
397
|
+
if (approved.length === 0) throw new Error("maid-curator: mode=apply 须携带非空 approved(主人批准的 fact_id 列表);主人未批准任何条目时不要调用 apply");
|
|
398
|
+
const report = await service.apply(ns, approved);
|
|
399
|
+
return {
|
|
400
|
+
mode: "apply",
|
|
401
|
+
applied: report.applied,
|
|
402
|
+
skipped: report.skipped,
|
|
403
|
+
failed: report.failed
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (typed.mode === "dry-run") {
|
|
407
|
+
const dry = await service.dryRun(ns);
|
|
408
|
+
return {
|
|
409
|
+
mode: "dry-run",
|
|
410
|
+
count: dry.facts.length,
|
|
411
|
+
facts: dry.facts,
|
|
412
|
+
proposals: dry.proposals,
|
|
413
|
+
report: dry.report
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const plan = await service.propose(ns);
|
|
417
|
+
return {
|
|
418
|
+
mode: "scan",
|
|
419
|
+
count: plan.facts.length,
|
|
420
|
+
facts: plan.facts,
|
|
421
|
+
proposals: plan.proposals
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
})];
|
|
425
|
+
}
|
|
426
|
+
function registerCuratorTool(ctx, service, deps) {
|
|
427
|
+
const disposers = buildCuratorTool(service, deps).map((definition) => ctx.tools.register(definition));
|
|
428
|
+
return () => {
|
|
429
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function resolveSessionPreset(events, headerPreset) {
|
|
433
|
+
let selected;
|
|
434
|
+
let sawConversation = false;
|
|
435
|
+
let midSessionSwitch = false;
|
|
436
|
+
for (const entry of events ?? []) {
|
|
437
|
+
const event = entry;
|
|
438
|
+
if (event?.type === "turn/start" || event?.type === "turn/end" || event?.type === "user/message" || event?.type === "assistant/message") sawConversation = true;
|
|
439
|
+
if (event?.type === "agent-preset/selected" && typeof event.data?.agentPreset === "string") {
|
|
440
|
+
selected = event.data.agentPreset;
|
|
441
|
+
if (sawConversation) midSessionSwitch = true;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
preset: selected ?? headerPreset,
|
|
446
|
+
midSessionSwitch
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const apply = (ctx, config) => {
|
|
450
|
+
const logger = ctx.logger?.("maid-curator") ?? console;
|
|
451
|
+
const guide = typeof config?.guide === "object" && config.guide !== null ? config.guide : defaultCurationGuide;
|
|
452
|
+
const fixedNs = typeof config?.namespace === "string" && config.namespace.trim() !== "" ? config.namespace : void 0;
|
|
453
|
+
const memory = ctx.reflect.get("maidMemory", false);
|
|
454
|
+
if (memory === void 0) logger.warn("maid-curator: maidMemory 未挂载——scan/dry-run 将返回空,apply 将被拒绝(maid-memory 须先于 maid-curator 装配)");
|
|
455
|
+
const service = createMaidCuratorService(memory, guide);
|
|
456
|
+
const disposeProvide = ctx.provide("maidCurator", service);
|
|
457
|
+
const disposeTools = registerCuratorTool(ctx, service, { resolveNamespace: (exec) => {
|
|
458
|
+
if (fixedNs !== void 0) return fixedNs;
|
|
459
|
+
const session = exec?.agent?.session;
|
|
460
|
+
const resolution = resolveSessionPreset(session?.snapshotEvents?.(), session?.header?.agentPreset);
|
|
461
|
+
if (resolution.midSessionSwitch) throw new Error(`maid-curator: 会话存在 0.1.5 遗留中途切换(头部 ${JSON.stringify(session?.header?.agentPreset)}),策展操作被拒绝;请新建会话`);
|
|
462
|
+
const ns = memory?.resolveNamespace?.(resolution.preset);
|
|
463
|
+
if (ns === void 0) throw new Error(`maid-curator: 无法为该会话解析记忆命名空间(agentPreset=${JSON.stringify(resolution.preset)}),策展操作被拒绝`);
|
|
464
|
+
return ns;
|
|
465
|
+
} });
|
|
466
|
+
return () => {
|
|
467
|
+
disposeTools();
|
|
468
|
+
disposeProvide();
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
//#endregion
|
|
472
|
+
export { DEFAULT_FALLBACK_ACTION, apply, applyProposals, buildCuratorTool, createMaidCuratorService, createMslmHttpClient, defaultCurationGuide, defaultMslmBaseUrl, inject, name, proposeDisposition, registerCuratorTool, renderDryRun, scanUnsortedFacts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepmaid/maid-curator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"cordis.patch.yml"
|
|
17
|
+
],
|
|
18
|
+
"dsh": {
|
|
19
|
+
"engines": {
|
|
20
|
+
"dsh": ">=0.2.1-rc.2 <0.3"
|
|
21
|
+
},
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
},
|
|
25
|
+
"meta": {
|
|
26
|
+
"title": "Maid Curator",
|
|
27
|
+
"description": "策展技能包:共享层三闸门的策展执行面(策展扫描/遗产分拣技能,namespace 可覆写)。关闭=策展技能从女仆工具面消失,共享知识层治理失工具。"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
35
|
+
"tsdown": "0.22.14",
|
|
36
|
+
"typescript": "6.0.3"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown"
|
|
43
|
+
}
|
|
6
44
|
}
|