@deepmaid/maid-setup 0.0.0-stage → 0.1.0-alpha.2

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 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,8 @@
1
- # Temporary Holding Version
1
+ # @deepmaid/maid-setup
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DeepMaid 首次迎接与设置服务(M3j)。职责:稳定 hostId、公共 Doris 模板目录(只读 `@deepmaid/maid-doris`)、可恢复的迎接/换模板事务(journal + 备份 + 崩溃回滚)、状态与预检。M3k 将在同一服务上扩展状态服务与 MSLM 向导。
4
+
5
+ - 挂载:`maid-identity` 之后(inject);`agentPresets`/`maidComposer`/`llm` 经 reflect 软依赖。
6
+ - apply 期零副作用(不创建 Doris、不改默认女仆、不开端口);创建期恢复未完成事务是唯一写操作。
7
+ - 不静态 import `node:sqlite`——嵌入式状态不可用时排障面仍可用。
8
+ - MIT;engines: dsh >=0.2.1-rc.2 <0.3。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: deepmaid-maid-setup
3
+ name: '@deepmaid/maid-setup'
package/lib/index.d.ts ADDED
@@ -0,0 +1,129 @@
1
+ import { MaidIdentity, ManagedPaths, OnboardingStatus, PersonaTemplatePreview } from "@deepmaid/maid-identity";
2
+ //#region src/types.d.ts
3
+ /** host agent-preset-registry 只读鸭子面(缺席 → 状态 blocked)。 */
4
+ interface AgentPresetsFace {
5
+ list(): Promise<ReadonlyArray<{
6
+ id: string;
7
+ name?: string;
8
+ }>>;
9
+ readonly defaultId: string;
10
+ }
11
+ /** createMaidSetupService 消费的最小 identity 面(真插件形态由宿主装配提供)。 */
12
+ type SetupIdentityFace = Pick<MaidIdentity, 'get' | 'list' | 'reload'> & Partial<Pick<MaidIdentity, 'getDefaultMaid' | 'setDefaultMaid'>>;
13
+ interface MaidSetupDeps {
14
+ identity: SetupIdentityFace;
15
+ paths: ManagedPaths;
16
+ agentPresets(): AgentPresetsFace | undefined;
17
+ composer?(): {
18
+ invalidate?(maidId: string): void;
19
+ } | undefined;
20
+ llm?(): unknown;
21
+ warn?(message: string): void;
22
+ /** 测试注入缝(照 distiller/judge 先例):maid commit 后、pack commit 前的崩溃窗口。 */
23
+ hooks?: {
24
+ afterMaidCommit?(): void;
25
+ };
26
+ }
27
+ type MaidSetupErrorCode = 'invalid-template' | 'invalid-operation' | 'conflict' | 'confirmation-required' | 'blocked' | 'operation-failed';
28
+ declare class MaidSetupError extends Error {
29
+ readonly code: MaidSetupErrorCode;
30
+ constructor(code: MaidSetupErrorCode, message: string, options?: ErrorOptions);
31
+ }
32
+ interface MaidSetupService {
33
+ /** 稳定 hostId(首次生成后长期固定;规格 §9.2 落点 4)。 */
34
+ hostId(): string;
35
+ onboardingStatus(): Promise<OnboardingStatus>;
36
+ personaTemplates(): Promise<PersonaTemplatePreview[]>;
37
+ activateDoris(input: {
38
+ operationId: string;
39
+ templateId: string;
40
+ }): Promise<OnboardingStatus>;
41
+ changeDorisTemplate(input: {
42
+ operationId: string;
43
+ templateId: string;
44
+ confirmCustomized?: boolean;
45
+ }): Promise<OnboardingStatus>;
46
+ }
47
+ //#endregion
48
+ //#region src/service.d.ts
49
+ declare function createMaidSetupService(deps: MaidSetupDeps): MaidSetupService;
50
+ //#endregion
51
+ //#region src/host-identity.d.ts
52
+ declare function ensureStableHostId(file: string): string;
53
+ //#endregion
54
+ //#region src/catalog.d.ts
55
+ /**
56
+ * M3j 公共模板目录(规格 §6.1/§8):只读 @deepmaid/maid-doris 包内 catalog 与
57
+ * templates——离线、不外呼;id 白名单字符;root 必须落在包根内(防路径拼接逃逸)。
58
+ * 不静态 import 内容包(保持「maid-setup 不随内容包膨胀」的装配关系)。
59
+ */
60
+ interface DorisCatalogEntry {
61
+ id: string;
62
+ version: number;
63
+ name: string;
64
+ tagline: string;
65
+ preview: Array<{
66
+ user: string;
67
+ assistant: string;
68
+ }>;
69
+ /** 相对包根的 root(templates/<id>/root)。 */
70
+ root: string;
71
+ }
72
+ declare function dorisCatalogRoot(): string;
73
+ declare function readDorisCatalog(warn: (message: string) => void): DorisCatalogEntry[];
74
+ declare function templateDirOf(entry: DorisCatalogEntry): string;
75
+ //#endregion
76
+ //#region src/template-hash.d.ts
77
+ declare function hashManagedContent(maidDir: string, packDir: string): string;
78
+ //#endregion
79
+ //#region src/transactions.d.ts
80
+ /**
81
+ * M3j 可恢复迎接/换模板事务(规格 §8.2):
82
+ * staging 物化 → 隔离校验(composer validate + 独立 loadRegistry)→ 备份(change)→
83
+ * maid rename → pack rename → identity.reload → 默认女仆(仅缺席时)→ completed;
84
+ * 每步先写 journal 再执行下一步,任何失败按 journal 逆回滚(删除 createdTargets、
85
+ * 恢复 backups、还原默认女仆),回滚失败保留现场并上抛。恢复入口在 service 创建期
86
+ * 扫描 .transactions:completed 清理、非 completed 回滚、坏 journal 清退。
87
+ */
88
+ interface TransactionJournal {
89
+ operationId: string;
90
+ kind: 'activate' | 'change';
91
+ templateId: string;
92
+ phase: 'prepared' | 'maid-committed' | 'pack-committed' | 'default-committed' | 'completed';
93
+ createdTargets: string[];
94
+ backups: Array<{
95
+ target: string;
96
+ backup: string;
97
+ }>;
98
+ previousDefaultMaid?: string;
99
+ changedDefault: boolean;
100
+ }
101
+ interface TransactionPorts {
102
+ paths: ManagedPaths;
103
+ identity: SetupIdentityFace;
104
+ entry: DorisCatalogEntry;
105
+ warn(message: string): void;
106
+ hooks?: {
107
+ afterMaidCommit?(): void;
108
+ };
109
+ }
110
+ declare function runDorisTransaction(input: {
111
+ operationId: string;
112
+ kind: 'activate' | 'change';
113
+ entry: DorisCatalogEntry;
114
+ }, ports: TransactionPorts): void;
115
+ /** service 创建期恢复:completed → 清理;非 completed → 回滚;坏/残留 staging → 清退。 */
116
+ declare function recoverDorisTransactions(paths: ManagedPaths, identity: SetupIdentityFace, warn: (message: string) => void): void;
117
+ //#endregion
118
+ //#region src/index.d.ts
119
+ declare const name = "maid-setup";
120
+ declare const inject: string[];
121
+ /**
122
+ * 宿主接线(M3j 规格 §6.2):identity 必在(inject);agentPresets/maidComposer/llm 经
123
+ * reflect 免 inject 直读(缺席容忍——状态 blocked / composer 失效仅影响换模板后的
124
+ * 缓存失效)。apply 不创建 Doris、不改默认女仆、不开端口;创建期恢复未完成事务是
125
+ * 唯一写操作。
126
+ */
127
+ declare function apply(ctx: any): void;
128
+ //#endregion
129
+ export { type AgentPresetsFace, type DorisCatalogEntry, type MaidSetupDeps, MaidSetupError, type MaidSetupErrorCode, type MaidSetupService, type SetupIdentityFace, type TransactionJournal, type TransactionPorts, apply, createMaidSetupService, dorisCatalogRoot, ensureStableHostId, hashManagedContent, inject, name, readDorisCatalog, recoverDorisTransactions, runDorisTransaction, templateDirOf };
package/lib/index.js ADDED
@@ -0,0 +1,507 @@
1
+ import { createRequire } from "node:module";
2
+ import { managedPathsOf } from "@deepmaid/maid-identity/managed-paths";
3
+ import { accessSync, constants, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+ import { createHash, randomUUID } from "node:crypto";
6
+ import { parse, stringify } from "yaml";
7
+ import { validateMaidDeclaration } from "@deepmaid/maid-composer";
8
+ import { loadRegistry } from "@deepmaid/maid-identity/registry";
9
+ //#region src/host-identity.ts
10
+ /**
11
+ * M3j 稳定 hostId(规格 §9.2 落点 4):$DSH_HOME/deepmaid/state/host.json 持有
12
+ * {version:1, hostId:'host_<uuid>'};首次生成原子落盘(tmp+rename),此后任何重建
13
+ * 都复用同一值——eventId 命名空间不漂移,M3k 连接包沿用。坏文件(解析失败/坏型)
14
+ * → 重写新生成值(fail-forward:状态文件可重建,身份不可复用旧坏值)。
15
+ */
16
+ const HOST_ID_RE = /^host_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
+ function ensureStableHostId(file) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
20
+ if (parsed?.version === 1 && typeof parsed.hostId === "string" && HOST_ID_RE.test(parsed.hostId)) return parsed.hostId;
21
+ } catch {}
22
+ const hostId = `host_${randomUUID()}`;
23
+ mkdirSync(dirname(file), { recursive: true });
24
+ const temp = `${file}.${randomUUID()}.tmp`;
25
+ writeFileSync(temp, `${JSON.stringify({
26
+ version: 1,
27
+ hostId
28
+ }, null, 2)}\n`, "utf8");
29
+ renameSync(temp, file);
30
+ return hostId;
31
+ }
32
+ //#endregion
33
+ //#region src/catalog.ts
34
+ const REQUIRE = createRequire(import.meta.url);
35
+ const PKG_ROOT = dirname(REQUIRE.resolve("@deepmaid/maid-doris/package.json"));
36
+ const TEMPLATE_ID_RE = /^[a-z][a-z0-9-]*$/;
37
+ function dorisCatalogRoot() {
38
+ return PKG_ROOT;
39
+ }
40
+ function isEntry(value) {
41
+ if (typeof value !== "object" || value === null) return false;
42
+ const e = value;
43
+ return typeof e.id === "string" && TEMPLATE_ID_RE.test(e.id) && typeof e.version === "number" && Number.isInteger(e.version) && e.version >= 1 && typeof e.name === "string" && e.name.length > 0 && typeof e.tagline === "string" && Array.isArray(e.preview) && typeof e.root === "string" && !e.root.startsWith("/");
44
+ }
45
+ function readDorisCatalog(warn) {
46
+ const file = resolve(PKG_ROOT, "catalog.json");
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(readFileSync(file, "utf8"));
50
+ } catch (error) {
51
+ throw new Error(`maid-setup: cannot read @deepmaid/maid-doris catalog (${error.message})`, { cause: error });
52
+ }
53
+ const templates = parsed?.templates;
54
+ if (!Array.isArray(templates)) throw new Error("maid-setup: maid-doris catalog.json 缺 templates 数组");
55
+ const entries = [];
56
+ for (const item of templates) {
57
+ if (!isEntry(item)) {
58
+ warn(`maid-setup: catalog 跳过坏条目 ${JSON.stringify(item)?.slice(0, 80)}`);
59
+ continue;
60
+ }
61
+ const dir = resolve(PKG_ROOT, item.root);
62
+ if (relative(PKG_ROOT, dir).startsWith("..")) throw new Error(`maid-setup: catalog 条目 '${item.id}' 的 root 越出包根`);
63
+ if (!existsSync(resolve(dir, "maids/doris/maid.yml"))) throw new Error(`maid-setup: 模板 '${item.id}' 缺 maids/doris/maid.yml`);
64
+ entries.push(item);
65
+ }
66
+ if (entries.length === 0) throw new Error("maid-setup: maid-doris catalog 为空");
67
+ return entries;
68
+ }
69
+ function templateDirOf(entry) {
70
+ return resolve(PKG_ROOT, entry.root);
71
+ }
72
+ function catalogEntryOf(id, warn) {
73
+ if (!TEMPLATE_ID_RE.test(id)) throw new Error(`maid-setup: 非法模板 id '${id}'`);
74
+ const entry = readDorisCatalog(warn).find((candidate) => candidate.id === id);
75
+ if (entry === void 0) throw new Error(`maid-setup: 模板 '${id}' 不在 catalog`);
76
+ return entry;
77
+ }
78
+ //#endregion
79
+ //#region src/template-hash.ts
80
+ /**
81
+ * M3j 受管内容校验值(规格 §8.1/§8.3):只覆盖「换模板会触及的表达资产白名单」——
82
+ * agent.cordis.yml、seeds/**、gestures.yml、当前 voice pack 的 pack.yml,加 maid.yml
83
+ * 的 voice.pack/personaTemplate 投影(剔除 contentHash 自身)。数值配置(emotion/
84
+ * calendar)与身份键不参与——换模板不动它们,hash 不因它们漂移。确定性:相对路径
85
+ * 排序 + base64 内容 + 投影 JSON,同内容同 hash。
86
+ */
87
+ function filesUnder(dir, base) {
88
+ if (!existsSync(dir)) return [];
89
+ return readdirSync(dir, {
90
+ recursive: true,
91
+ withFileTypes: true
92
+ }).filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name)).map((file) => relative(base, file)).sort();
93
+ }
94
+ function hashManagedContent(maidDir, packDir) {
95
+ const parts = [];
96
+ for (const rel of ["agent.cordis.yml", "gestures.yml"]) {
97
+ const file = join(maidDir, rel);
98
+ if (existsSync(file)) parts.push(`${rel}\0${readFileSync(file).toString("base64")}`);
99
+ }
100
+ for (const rel of filesUnder(join(maidDir, "seeds"), maidDir)) parts.push(`${rel}\0${readFileSync(join(maidDir, rel)).toString("base64")}`);
101
+ const packFile = join(packDir, "pack.yml");
102
+ if (existsSync(packFile)) parts.push(`packs/${relative(packDir, packFile)}\0${readFileSync(packFile).toString("base64")}`);
103
+ let projection = {};
104
+ try {
105
+ const maid = parse(readFileSync(join(maidDir, "maid.yml"), "utf8"));
106
+ const voice = maid?.voice;
107
+ const template = maid?.personaTemplate;
108
+ projection = {
109
+ ...typeof voice?.pack === "string" ? { voicePack: voice.pack } : {},
110
+ ...typeof template?.id === "string" && typeof template?.version === "number" ? {
111
+ templateId: template.id,
112
+ templateVersion: template.version
113
+ } : {}
114
+ };
115
+ } catch {}
116
+ parts.push(`maid.yml@projection\0${JSON.stringify(projection)}`);
117
+ const canonical = parts.join("\n");
118
+ return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
119
+ }
120
+ //#endregion
121
+ //#region src/types.ts
122
+ var MaidSetupError = class extends Error {
123
+ code;
124
+ constructor(code, message, options) {
125
+ super(message, options);
126
+ this.code = code;
127
+ this.name = "MaidSetupError";
128
+ }
129
+ };
130
+ //#endregion
131
+ //#region src/transactions.ts
132
+ const DORIS = "doris";
133
+ function journalPath(txDir) {
134
+ return join(txDir, "journal.json");
135
+ }
136
+ function writeJournal(txDir, journal) {
137
+ writeFileSync(journalPath(txDir), `${JSON.stringify(journal, null, 2)}\n`, "utf8");
138
+ }
139
+ function rollback(journal, ports) {
140
+ for (const target of [...journal.createdTargets].reverse()) rmSync(target, {
141
+ recursive: true,
142
+ force: true
143
+ });
144
+ for (const { target, backup } of [...journal.backups].reverse()) if (existsSync(backup)) cpSync(backup, target, { recursive: true });
145
+ if (journal.changedDefault) ports.identity.setDefaultMaid?.(journal.previousDefaultMaid);
146
+ ports.identity.reload();
147
+ }
148
+ /** 隔离校验:composer 声明门槛 + 独立注册表装载(doris 可解析)。 */
149
+ function validateStaged(maidDir, ports) {
150
+ validateMaidDeclaration(maidDir);
151
+ const registry = loadRegistry([join(maidDir, "..")], { warn: ports.warn });
152
+ try {
153
+ if (registry.get(DORIS) === void 0) throw new MaidSetupError("operation-failed", `maid-setup: staging 装载失败:'${DORIS}' 未注册`);
154
+ } finally {
155
+ registry.dispose();
156
+ }
157
+ }
158
+ /** maid.yml 元数据写回(两段式):先写 voice.pack/personaTemplate(id/version),再以
159
+ * 此投影计算受管 hash 落 contentHash——一次写完会把旧投影烤进 hash(change 场景)。 */
160
+ function stampMaidYml(maidDir, entry, packDir) {
161
+ const file = join(maidDir, "maid.yml");
162
+ const raw = parse(readFileSync(file, "utf8"));
163
+ raw.voice = {
164
+ ...raw.voice ?? {},
165
+ pack: `doris-${entry.id}`
166
+ };
167
+ raw.personaTemplate = {
168
+ id: entry.id,
169
+ version: entry.version
170
+ };
171
+ writeFileSync(file, stringify(raw), "utf8");
172
+ const contentHash = hashManagedContent(maidDir, packDir);
173
+ raw.personaTemplate.contentHash = contentHash;
174
+ writeFileSync(file, stringify(raw), "utf8");
175
+ }
176
+ function runDorisTransaction(input, ports) {
177
+ const { paths, entry } = ports;
178
+ const templateMaidDir = join(templateDirOf(entry), "maids", DORIS);
179
+ const templatePackDir = join(templateDirOf(entry), "packs", `doris-${entry.id}`);
180
+ const managedMaidDir = join(paths.maids, DORIS);
181
+ const packTarget = join(paths.packs, "packs", `doris-${entry.id}`);
182
+ const change = input.kind === "change";
183
+ const txDir = join(paths.transactionsDir, input.operationId);
184
+ const staging = join(txDir, "staging");
185
+ mkdirSync(staging, { recursive: true });
186
+ const journal = {
187
+ operationId: input.operationId,
188
+ kind: input.kind,
189
+ templateId: entry.id,
190
+ phase: "prepared",
191
+ createdTargets: [],
192
+ backups: [],
193
+ changedDefault: false
194
+ };
195
+ let keepScene = false;
196
+ try {
197
+ const stagedMaid = join(staging, "maids", DORIS);
198
+ const stagedPack = join(staging, "packs", `doris-${entry.id}`);
199
+ mkdirSync(join(staging, "maids"), { recursive: true });
200
+ mkdirSync(join(staging, "packs"), { recursive: true });
201
+ cpSync(templateMaidDir, stagedMaid, { recursive: true });
202
+ cpSync(templatePackDir, stagedPack, { recursive: true });
203
+ if (change) {
204
+ for (const kept of ["emotion.yml", "calendar.yml"]) {
205
+ const from = join(managedMaidDir, kept);
206
+ if (existsSync(from)) cpSync(from, join(stagedMaid, kept));
207
+ }
208
+ const currentMaidYml = readFileSync(join(managedMaidDir, "maid.yml"), "utf8");
209
+ writeFileSync(join(stagedMaid, "maid.yml"), currentMaidYml);
210
+ }
211
+ stampMaidYml(stagedMaid, entry, stagedPack);
212
+ validateStaged(stagedMaid, ports);
213
+ writeJournal(txDir, journal);
214
+ if (change) {
215
+ const backupDir = join(paths.backupsDir, input.operationId);
216
+ mkdirSync(backupDir, { recursive: true });
217
+ const maidBackup = join(backupDir, "maids", DORIS);
218
+ mkdirSync(join(backupDir, "maids"), { recursive: true });
219
+ cpSync(managedMaidDir, maidBackup, { recursive: true });
220
+ journal.backups.push({
221
+ target: managedMaidDir,
222
+ backup: maidBackup
223
+ });
224
+ if (existsSync(packTarget)) {
225
+ const packBackup = join(backupDir, "packs", `doris-${entry.id}`);
226
+ mkdirSync(join(backupDir, "packs"), { recursive: true });
227
+ cpSync(packTarget, packBackup, { recursive: true });
228
+ journal.backups.push({
229
+ target: packTarget,
230
+ backup: packBackup
231
+ });
232
+ }
233
+ writeJournal(txDir, journal);
234
+ }
235
+ mkdirSync(paths.maids, { recursive: true });
236
+ mkdirSync(join(paths.packs, "packs"), { recursive: true });
237
+ rmSync(managedMaidDir, {
238
+ recursive: true,
239
+ force: true
240
+ });
241
+ renameSync(stagedMaid, managedMaidDir);
242
+ journal.createdTargets.push(managedMaidDir);
243
+ journal.phase = "maid-committed";
244
+ writeJournal(txDir, journal);
245
+ ports.hooks?.afterMaidCommit?.();
246
+ rmSync(packTarget, {
247
+ recursive: true,
248
+ force: true
249
+ });
250
+ renameSync(stagedPack, packTarget);
251
+ journal.createdTargets.push(packTarget);
252
+ journal.phase = "pack-committed";
253
+ writeJournal(txDir, journal);
254
+ ports.identity.reload();
255
+ if (ports.identity.get(DORIS) === void 0) throw new MaidSetupError("operation-failed", "maid-setup: 提交后 identity 回读失败");
256
+ if (ports.identity.getDefaultMaid?.() === void 0) {
257
+ ports.identity.setDefaultMaid?.(DORIS);
258
+ journal.changedDefault = true;
259
+ journal.phase = "default-committed";
260
+ writeJournal(txDir, journal);
261
+ }
262
+ journal.phase = "completed";
263
+ writeJournal(txDir, journal);
264
+ } catch (error) {
265
+ try {
266
+ rollback(journal, ports);
267
+ rmSync(txDir, {
268
+ recursive: true,
269
+ force: true
270
+ });
271
+ } catch (rollbackError) {
272
+ keepScene = true;
273
+ ports.warn(`maid-setup: 事务 ${input.operationId} 回滚失败(${rollbackError.message}),现场保留于 ${txDir}`);
274
+ throw new MaidSetupError("operation-failed", `maid-setup: 事务 ${input.operationId} 失败且回滚未完成(现场保留于 ${txDir})`, { cause: rollbackError });
275
+ }
276
+ throw error;
277
+ } finally {
278
+ if (!keepScene) rmSync(txDir, {
279
+ recursive: true,
280
+ force: true
281
+ });
282
+ }
283
+ }
284
+ /** service 创建期恢复:completed → 清理;非 completed → 回滚;坏/残留 staging → 清退。 */
285
+ function recoverDorisTransactions(paths, identity, warn) {
286
+ if (!existsSync(paths.transactionsDir)) return;
287
+ const ports = {
288
+ paths,
289
+ identity,
290
+ warn,
291
+ entry: void 0
292
+ };
293
+ for (const name of readdirSync(paths.transactionsDir)) {
294
+ const txDir = join(paths.transactionsDir, name);
295
+ let journal;
296
+ try {
297
+ journal = JSON.parse(readFileSync(journalPath(txDir), "utf8"));
298
+ } catch {}
299
+ try {
300
+ if (journal === void 0 || journal.phase === "completed") rmSync(txDir, {
301
+ recursive: true,
302
+ force: true
303
+ });
304
+ else {
305
+ rollback(journal, ports);
306
+ rmSync(txDir, {
307
+ recursive: true,
308
+ force: true
309
+ });
310
+ }
311
+ } catch (error) {
312
+ warn(`maid-setup: 事务 ${name} 恢复失败(${error.message}),现场保留于 ${txDir}`);
313
+ }
314
+ }
315
+ }
316
+ //#endregion
317
+ //#region src/service.ts
318
+ /**
319
+ * M3j service 面(规格 §5.4/§8):状态识别由真实注册表+内容文件+预检推导;事务经
320
+ * inFlight/completed 幂等(同 id 并发=同一 Promise,成功后重放=回读状态,失败
321
+ * 不缓存可同 id 重试);同一时刻同一 Doris 的事务串行。恢复未完成事务是创建期的
322
+ * 唯一写操作——apply 期不创建 Doris、不改默认女仆、不开端口(规格 §6.2)。
323
+ */
324
+ const OPERATION_ID_RE = /^[A-Za-z0-9_-]{8,80}$/;
325
+ function nodeSupported() {
326
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
327
+ return major === 22 && minor >= 19 || major >= 24;
328
+ }
329
+ function createMaidSetupService(deps) {
330
+ const warn = deps.warn ?? (() => {});
331
+ const hostId = ensureStableHostId(deps.paths.hostFile);
332
+ const recoveryBlockers = [];
333
+ recoverDorisTransactions(deps.paths, deps.identity, (message) => {
334
+ recoveryBlockers.push({
335
+ code: "recovery-failed",
336
+ detail: message
337
+ });
338
+ warn(message);
339
+ });
340
+ const managedMaidDir = join(deps.paths.maids, "doris");
341
+ const managedMaidFile = join(managedMaidDir, "maid.yml");
342
+ const packDirOf = (packId) => join(deps.paths.packs, "packs", packId);
343
+ const modelOf = () => deps.llm?.() !== void 0 ? {
344
+ state: "unknown",
345
+ detail: "DSH 已配置主对话模型(未验证连通)"
346
+ } : {
347
+ state: "missing",
348
+ detail: "尚未配置主对话模型——请先在 DSH 设置中配置"
349
+ };
350
+ async function blockersOf() {
351
+ const blockers = [...recoveryBlockers];
352
+ if (!nodeSupported()) blockers.push({
353
+ code: "node-version",
354
+ detail: `Node ${process.versions.node} 不满足(node:sqlite 需 ≥22.19 或 ≥24)`
355
+ });
356
+ try {
357
+ accessSync(deps.paths.maids, constants.W_OK);
358
+ } catch {
359
+ blockers.push({
360
+ code: "maids-not-writable",
361
+ detail: `托管目录不可写:${deps.paths.maids}`
362
+ });
363
+ }
364
+ const presets = deps.agentPresets();
365
+ if (presets === void 0) blockers.push({
366
+ code: "agent-presets-unavailable",
367
+ detail: "agentPresets 服务缺席(宿主未装载 preset 名册)"
368
+ });
369
+ else if (!(await presets.list()).some((preset) => preset.id === "standard")) blockers.push({
370
+ code: "standard-missing",
371
+ detail: "standard 基底不在 preset 名册(迎接需 standard 工作基底)"
372
+ });
373
+ return blockers;
374
+ }
375
+ async function statusOf() {
376
+ const defaultMaid = deps.identity.getDefaultMaid?.() ?? null;
377
+ const record = deps.identity.get("doris");
378
+ const model = modelOf();
379
+ if (record === void 0) return {
380
+ kind: "uninitialized",
381
+ maidId: null,
382
+ defaultMaid,
383
+ blockers: await blockersOf(),
384
+ model
385
+ };
386
+ const base = {
387
+ maidId: "doris",
388
+ defaultMaid,
389
+ blockers: await blockersOf(),
390
+ model
391
+ };
392
+ if (!existsSync(managedMaidFile)) return {
393
+ kind: "existing",
394
+ ...base
395
+ };
396
+ const template = record.personaTemplate;
397
+ if (template === void 0) return {
398
+ kind: "existing",
399
+ ...base
400
+ };
401
+ const packId = record.voice?.pack ?? `doris-${template.id}`;
402
+ return {
403
+ kind: hashManagedContent(managedMaidDir, packDirOf(packId)) === template.contentHash ? "ready" : "customized",
404
+ ...base,
405
+ currentTemplate: template
406
+ };
407
+ }
408
+ let lane = Promise.resolve();
409
+ const inFlight = /* @__PURE__ */ new Map();
410
+ const completed = /* @__PURE__ */ new Map();
411
+ function requireOperationId(operationId) {
412
+ if (!OPERATION_ID_RE.test(operationId)) throw new MaidSetupError("invalid-operation", `maid-setup: 非法 operationId '${operationId}'(须 8-80 位 [A-Za-z0-9_-])`);
413
+ }
414
+ function runTransaction(input) {
415
+ const existing = completed.get(input.operationId);
416
+ if (existing !== void 0) return Promise.resolve(existing);
417
+ const pending = inFlight.get(input.operationId);
418
+ if (pending !== void 0) return pending;
419
+ const run = (async () => {
420
+ requireOperationId(input.operationId);
421
+ const status = await statusOf();
422
+ if (status.kind === "blocked") throw new MaidSetupError("blocked", `maid-setup: 当前部署阻塞(${status.blockers.map((b) => b.code).join(", ")})`);
423
+ if (input.kind === "activate") {
424
+ if (status.kind !== "uninitialized") throw new MaidSetupError("conflict", `maid-setup: Doris 已存在(${status.kind}),迎接仅适用于未初始化部署;更换请走 changeDorisTemplate`);
425
+ } else {
426
+ if (status.kind === "uninitialized" || status.kind === "existing") throw new MaidSetupError("conflict", `maid-setup: 无受管 Doris(${status.kind}),更换模板需要先完成迎接`);
427
+ if (status.kind === "customized" && input.confirmCustomized !== true) throw new MaidSetupError("confirmation-required", "maid-setup: 受管内容已被自定义,更换模板将覆盖表达资产——需显式确认(确认前自动备份)");
428
+ }
429
+ let entry;
430
+ try {
431
+ entry = catalogEntryOf(input.templateId, warn);
432
+ } catch (error) {
433
+ throw new MaidSetupError("invalid-template", error.message, { cause: error });
434
+ }
435
+ runDorisTransaction({
436
+ operationId: input.operationId,
437
+ kind: input.kind,
438
+ entry
439
+ }, {
440
+ paths: deps.paths,
441
+ identity: deps.identity,
442
+ entry,
443
+ warn,
444
+ ...deps.hooks !== void 0 ? { hooks: deps.hooks } : {}
445
+ });
446
+ deps.composer?.()?.invalidate?.("doris");
447
+ const after = await statusOf();
448
+ completed.set(input.operationId, after);
449
+ return after;
450
+ })();
451
+ inFlight.set(input.operationId, run);
452
+ const enqueue = (promise) => {
453
+ lane = lane.then(() => promise.then(() => {}, () => {}));
454
+ return promise;
455
+ };
456
+ return enqueue(run.finally(() => {
457
+ inFlight.delete(input.operationId);
458
+ }));
459
+ }
460
+ return {
461
+ hostId: () => hostId,
462
+ onboardingStatus: statusOf,
463
+ personaTemplates: async () => readDorisCatalog(warn).map((entry) => ({
464
+ id: entry.id,
465
+ version: entry.version,
466
+ name: entry.name,
467
+ tagline: entry.tagline,
468
+ preview: entry.preview
469
+ })),
470
+ activateDoris: (input) => runTransaction({
471
+ ...input,
472
+ kind: "activate"
473
+ }),
474
+ changeDorisTemplate: (input) => runTransaction({
475
+ ...input,
476
+ kind: "change"
477
+ })
478
+ };
479
+ }
480
+ //#endregion
481
+ //#region src/index.ts
482
+ const name = "maid-setup";
483
+ const inject = ["maidIdentity"];
484
+ /**
485
+ * 宿主接线(M3j 规格 §6.2):identity 必在(inject);agentPresets/maidComposer/llm 经
486
+ * reflect 免 inject 直读(缺席容忍——状态 blocked / composer 失效仅影响换模板后的
487
+ * 缓存失效)。apply 不创建 Doris、不改默认女仆、不开端口;创建期恢复未完成事务是
488
+ * 唯一写操作。
489
+ */
490
+ function apply(ctx) {
491
+ const logger = ctx.logger?.("maid-setup") ?? console;
492
+ const warn = (m) => logger.warn(m);
493
+ const service = createMaidSetupService({
494
+ identity: ctx.maidIdentity,
495
+ paths: managedPathsOf(ctx),
496
+ agentPresets: () => ctx.reflect?.get?.("agentPresets", false),
497
+ composer: () => ctx.reflect?.get?.("maidComposer", false),
498
+ llm: () => ctx.reflect?.get?.("llm", false),
499
+ warn
500
+ });
501
+ const dispose = ctx.provide("maidSetup", service);
502
+ ctx.effect(() => () => {
503
+ dispose();
504
+ }, "maid-setup: unload");
505
+ }
506
+ //#endregion
507
+ export { MaidSetupError, apply, createMaidSetupService, dorisCatalogRoot, ensureStableHostId, hashManagedContent, inject, name, readDorisCatalog, recoverDorisTransactions, runDorisTransaction, templateDirOf };
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@deepmaid/maid-setup",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.2",
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 Setup",
27
+ "description": "首次迎接与设置服务:稳定 hostId、公共 Doris 模板目录、可恢复迎接/换模板事务与状态预检。关闭=迎接向导与模板更换不可用,已物化女仆不受影响。"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
32
+ "yaml": "2.9.0",
33
+ "@deepmaid/maid-composer": "0.1.0-alpha.2",
34
+ "@deepmaid/maid-doris": "0.1.0-alpha.2",
35
+ "@deepmaid/maid-identity": "0.1.0-alpha.2"
36
+ },
37
+ "devDependencies": {
38
+ "@deepseek-ai/cordis": "4.1.0",
39
+ "tsdown": "0.22.14",
40
+ "typescript": "6.0.3"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsdown"
47
+ }
6
48
  }