@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 +21 -0
- package/README.md +7 -2
- package/cordis.patch.yml +3 -0
- package/lib/index.d.ts +129 -0
- package/lib/index.js +507 -0
- package/package.json +45 -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,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @deepmaid/maid-setup
|
|
2
2
|
|
|
3
|
-
|
|
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。
|
package/cordis.patch.yml
ADDED
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|