@klarkxy/dsh-mood 0.1.0

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/lib/index.js ADDED
@@ -0,0 +1,1261 @@
1
+ import { CHAT_EVENTS_SLOT, CONTRACT_SECTION, DEFAULT_QUESTION, MAX_PROJECT_ID_CHARS, MOOD_AI_PLUGIN, MOOD_ANALYZE_PURPOSE, MOOD_PLUGIN, MOOD_RPC_CHANNEL, MOOD_SOURCE_KIND, PROMPT_VERSION, SCHEMA_VERSION, cloneContract, defaultSettings, excerptOf, fail, isMoodMode, ok, parseSessionId, projectIdFromCwd, readinessLabel, sessionIdOf } from "./contracts.js";
2
+ import { registerHostRpc } from "@klarkxy/dsh-ai-services/host-rpc";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ import { randomUUID } from "node:crypto";
5
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
6
+ import { z } from "zod";
7
+ //#region src/evidence.ts
8
+ function textFromContent(content) {
9
+ if (typeof content === "string") return content;
10
+ if (!Array.isArray(content)) return "";
11
+ const parts = [];
12
+ for (const block of content) {
13
+ if (!block || typeof block !== "object") continue;
14
+ const row = block;
15
+ if (row.type === "text" && typeof row.text === "string") parts.push(row.text);
16
+ }
17
+ return parts.join("\n");
18
+ }
19
+ function asRecord$1(value) {
20
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
21
+ }
22
+ function messageSource(value) {
23
+ const row = asRecord$1(value);
24
+ return asRecord$1(row?.source) ?? asRecord$1(asRecord$1(row?.message)?.source);
25
+ }
26
+ function isHumanUserMessage(message) {
27
+ return message?.source?.kind === "user";
28
+ }
29
+ function isMoodMessage(message) {
30
+ if (message?.source?.kind !== "plugin:@klarkxy/dsh-mood" || message.source.plugin !== "@klarkxy/dsh-mood") return false;
31
+ if (message.source.form === "snapshot") return Boolean(message.source.sections?.some((section) => section.name === CONTRACT_SECTION));
32
+ return true;
33
+ }
34
+ function incomingAreAuxiliaryOnly(messages) {
35
+ if (messages.length === 0) return true;
36
+ return messages.every((message) => !isHumanUserMessage(message));
37
+ }
38
+ function humanTextOf(message) {
39
+ return textFromContent(message.content);
40
+ }
41
+ function messageIdOf(value) {
42
+ if (typeof value === "string" && value.length > 0) return value;
43
+ }
44
+ function collectClaimedHumans(messages) {
45
+ const humans = [];
46
+ for (const message of messages) {
47
+ if (!isHumanUserMessage(message)) continue;
48
+ const id = messageIdOf(message.id);
49
+ if (!id) continue;
50
+ const text = humanTextOf(message);
51
+ if (!text.trim()) continue;
52
+ humans.push({
53
+ id,
54
+ text
55
+ });
56
+ }
57
+ return humans;
58
+ }
59
+ function latestUserSeq(events) {
60
+ let seq = 0;
61
+ for (const event of events) if (event.type === "user/message" && messageSource(event.data)?.kind === "user") seq = event.seq;
62
+ return seq;
63
+ }
64
+ /** Immutable claimed user-message ids only. Previous turn seq is not this request. */
65
+ function sourceVersionOf(humans) {
66
+ return humans.map((item) => item.id).filter(Boolean).join("|");
67
+ }
68
+ function evidenceForRequest(sessionId, events, claimed) {
69
+ const ids = new Set(claimed.map((item) => item.id));
70
+ if (ids.size === 0) return [];
71
+ const refs = [];
72
+ for (const event of events) {
73
+ if (event.type !== "user/message") continue;
74
+ const data = asRecord$1(event.data);
75
+ const id = messageIdOf(data?.id);
76
+ if (!id || !ids.has(id)) continue;
77
+ if (messageSource(event.data)?.kind !== "user") continue;
78
+ const excerpt = excerptOf(textFromContent(data?.content));
79
+ refs.push(excerpt ? {
80
+ sessionId,
81
+ seq: event.seq,
82
+ kind: "user",
83
+ excerpt
84
+ } : {
85
+ sessionId,
86
+ seq: event.seq,
87
+ kind: "user"
88
+ });
89
+ }
90
+ return refs;
91
+ }
92
+ //#endregion
93
+ //#region src/inject.ts
94
+ function contractMessageInput(text) {
95
+ return {
96
+ source: {
97
+ kind: MOOD_SOURCE_KIND,
98
+ plugin: MOOD_PLUGIN,
99
+ form: "snapshot",
100
+ sections: [{
101
+ name: CONTRACT_SECTION,
102
+ text
103
+ }]
104
+ },
105
+ content: [{
106
+ type: "text",
107
+ text
108
+ }]
109
+ };
110
+ }
111
+ function createMoodContextMessage(text) {
112
+ return createUserMessage(contractMessageInput(text));
113
+ }
114
+ function formatContract(contract, clarification = []) {
115
+ const lines = [
116
+ `【需求约定 · ${readinessLabel(contract.readiness)}】`,
117
+ contract.goal ? `目标:${contract.goal}` : "",
118
+ contract.deliverables.length ? `交付:${contract.deliverables.join(";")}` : "",
119
+ contract.inScope.length ? `范围内:${contract.inScope.join(";")}` : "",
120
+ contract.outOfScope.length ? `范围外:${contract.outOfScope.join(";")}` : "",
121
+ contract.constraints.length ? `约束:${contract.constraints.join(";")}` : "",
122
+ contract.acceptance.length ? `验收:${contract.acceptance.join(";")}` : "",
123
+ contract.assumptions.length ? `假定:${contract.assumptions.join(";")}` : ""
124
+ ];
125
+ if (clarification.length) {
126
+ lines.push("澄清:");
127
+ for (const item of clarification) {
128
+ const answer = item.answer ? ` → ${item.answer}` : "";
129
+ lines.push(`- [${item.status}] ${item.question}${answer}`);
130
+ }
131
+ } else if (contract.questions.length) lines.push(`待确认:${contract.questions.join(";")}`);
132
+ if (contract.evidence.length) lines.push("证据:" + contract.evidence.map((item) => `#${item.seq}${item.excerpt ? `「${item.excerpt}」` : ""}`).join(" "));
133
+ lines.push("本约定不能代替文件修改或发布审批。未确认前不要当作已授权。");
134
+ return lines.filter(Boolean).join("\n");
135
+ }
136
+ function mergeContractMessage(messages, extra) {
137
+ return [...messages.filter((message) => !isMoodMessage(message)), extra];
138
+ }
139
+ //#endregion
140
+ //#region src/analyze.ts
141
+ function asString$1(value) {
142
+ return typeof value === "string" ? value.trim() : "";
143
+ }
144
+ function asList(value, max = 16) {
145
+ if (!Array.isArray(value)) return [];
146
+ const items = [];
147
+ for (const entry of value) {
148
+ const text = asString$1(entry);
149
+ if (!text) continue;
150
+ items.push(text.slice(0, 400));
151
+ if (items.length >= max) break;
152
+ }
153
+ return items;
154
+ }
155
+ function parseAnalysis(text) {
156
+ const trimmed = text.trim();
157
+ if (!trimmed) return void 0;
158
+ const raw = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/)?.[1]?.trim() ?? trimmed;
159
+ const start = raw.indexOf("{");
160
+ const end = raw.lastIndexOf("}");
161
+ if (start < 0 || end <= start) return void 0;
162
+ try {
163
+ const parsed = JSON.parse(raw.slice(start, end + 1));
164
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
165
+ const row = parsed;
166
+ const goal = asString$1(row.goal).slice(0, 2e3);
167
+ const questions = asList(row.questions, 3);
168
+ return {
169
+ goal,
170
+ deliverables: asList(row.deliverables),
171
+ inScope: asList(row.inScope),
172
+ outOfScope: asList(row.outOfScope),
173
+ constraints: asList(row.constraints),
174
+ acceptance: asList(row.acceptance),
175
+ assumptions: asList(row.assumptions),
176
+ questions
177
+ };
178
+ } catch {
179
+ return;
180
+ }
181
+ }
182
+ function boundQuestions(questions, kind) {
183
+ const unique = [];
184
+ for (const question of questions) {
185
+ const text = question.trim();
186
+ if (!text) continue;
187
+ if (unique.includes(text)) continue;
188
+ unique.push(text.slice(0, 400));
189
+ if (unique.length === 3) return unique;
190
+ }
191
+ if (unique.length === 0 && (kind === "material" || kind === "risk")) return [DEFAULT_QUESTION];
192
+ return unique;
193
+ }
194
+ const ANALYZE_SYSTEM = [
195
+ "你在确认写作任务需求。只根据给定的真实用户原文归纳。",
196
+ "不要编造未给出的文件、人物、字数或验收条件。",
197
+ "不要把需求确认写成文件修改或发布授权。",
198
+ "返回一个 JSON 对象,不要 Markdown。",
199
+ "键:goal, deliverables, inScope, outOfScope, constraints, acceptance, assumptions, questions。",
200
+ `questions 最多 3 条,仅在存在实质含糊或风险时提出;原文已经可执行则 questions 为空数组。`
201
+ ].join("\n");
202
+ function thinContract(input) {
203
+ return {
204
+ id: input.id,
205
+ sessionId: input.sessionId,
206
+ sourceVersion: input.sourceVersion,
207
+ revision: input.revision,
208
+ goal: input.goal.slice(0, 2e3),
209
+ deliverables: input.extra?.deliverables ?? [],
210
+ inScope: input.extra?.inScope ?? [],
211
+ outOfScope: input.extra?.outOfScope ?? [],
212
+ constraints: input.extra?.constraints ?? [],
213
+ acceptance: input.extra?.acceptance ?? [],
214
+ assumptions: input.extra?.assumptions ?? [],
215
+ questions: input.extra?.questions ?? [],
216
+ evidence: input.evidence,
217
+ readiness: input.readiness,
218
+ updatedAt: input.now
219
+ };
220
+ }
221
+ function contractFromDraft(input) {
222
+ const draft = input.draft;
223
+ return thinContract({
224
+ id: input.id,
225
+ sessionId: input.sessionId,
226
+ sourceVersion: input.sourceVersion,
227
+ revision: input.revision,
228
+ goal: draft?.goal || input.goalFallback,
229
+ evidence: input.evidence,
230
+ readiness: input.readiness,
231
+ now: input.now,
232
+ extra: {
233
+ deliverables: draft?.deliverables ?? [],
234
+ inScope: draft?.inScope ?? [],
235
+ outOfScope: draft?.outOfScope ?? [],
236
+ constraints: draft?.constraints ?? [],
237
+ acceptance: draft?.acceptance ?? [],
238
+ assumptions: draft?.assumptions ?? [],
239
+ questions: input.questions
240
+ }
241
+ });
242
+ }
243
+ const analyzePurpose = {
244
+ id: "mood.analyze",
245
+ label: "需求澄清",
246
+ defaultTarget: {
247
+ kind: "role",
248
+ role: "normal"
249
+ },
250
+ maxOutputTokens: 1024,
251
+ timeoutMs: 6e4
252
+ };
253
+ //#endregion
254
+ //#region src/trigger.ts
255
+ const ACK = /^(好的?|嗯+|行|ok|okay|收到|谢谢[.。!!]?|thanks[.!]?)$/i;
256
+ const CONTINUE = /^(继续|接着(?:做|写|改)?|go on|continue)$/i;
257
+ const VAGUE = /帮我(?:看|改|写|弄)?一下|改一下|写一下|处理一下|优化一下|弄好|随便|看情况|你决定|都行|无所谓|或者就|怎么写都行|看着办|帮我改改|改改/;
258
+ const RISK = /删除全部|全部删除|清空(?:全书|所有|全部)?|覆盖原文|覆盖所有|重写全|全部重写|并发布|发布到|永久删除|替换所有/;
259
+ const PATH = /\.[A-Za-z][A-Za-z0-9]{0,7}\b|第[一二三四五六七八九十百0-9]+章|[A-Za-z]:\\|\//;
260
+ const BOUND = /不超过|不少于|至少|最多|保持|不要|仅|只|必须|字以内|<=|≥|\d+\s*(?:字|句|段|行)/;
261
+ const NAMED = /[\u4e00-\u9fff]{2,6}(?:的)?(?:对白|对话|语气|出场)|“[^”]{2,40}”/;
262
+ const EXPLAIN = /解释什么是|^什么是|explain\s+what/i;
263
+ const TRANSLATE = /翻译成|译成|translate\s+.+\s+into/i;
264
+ const LOCAL_EDIT = /把第[一二三四五六七八九十百0-9]+[行句段]|修正拼写|重命名为|把错字|typo/i;
265
+ function classifyRequest(text, options = {}) {
266
+ const trimmed = text.replace(/\s+/g, " ").trim();
267
+ if (!trimmed) return "skip";
268
+ if (ACK.test(trimmed)) return "skip";
269
+ if (CONTINUE.test(trimmed)) return options.hasConfirmedContract ? "skip" : "material";
270
+ if (RISK.test(trimmed)) return "risk";
271
+ if (EXPLAIN.test(trimmed) || TRANSLATE.test(trimmed) || LOCAL_EDIT.test(trimmed)) return "clear";
272
+ if (VAGUE.test(trimmed)) return "material";
273
+ const specific = PATH.test(trimmed) || NAMED.test(trimmed);
274
+ const bounded = BOUND.test(trimmed);
275
+ if (specific && bounded && trimmed.length >= 16) return "clear";
276
+ if (specific || bounded) return "mild";
277
+ return "clear";
278
+ }
279
+ function shouldAnalyze(kind, mode, pendingManual) {
280
+ if (kind === "skip") return false;
281
+ if (pendingManual) return true;
282
+ if (mode === "manual") return false;
283
+ if (mode === "strict") return true;
284
+ return kind === "material" || kind === "risk";
285
+ }
286
+ function shouldWriteClearContract(kind, pendingManual, mode = "auto") {
287
+ return kind === "clear" && !pendingManual && mode === "auto";
288
+ }
289
+ function isBlockingKind(kind) {
290
+ return kind === "material" || kind === "risk";
291
+ }
292
+ //#endregion
293
+ //#region src/questions.ts
294
+ /** Native option label; selecting it without custom text is not an answer. */
295
+ const ASK_DETAIL_OPTION = "补充说明";
296
+ function toAskItems(items) {
297
+ return items.filter((item) => item.status === "pending").map((item) => ({
298
+ id: item.id,
299
+ header: "澄清",
300
+ question: item.question,
301
+ options: [{
302
+ label: ASK_DETAIL_OPTION,
303
+ description: "在自定义输入中写明范围、约束或验收标准"
304
+ }]
305
+ }));
306
+ }
307
+ function pendingClarifications(questions) {
308
+ return questions.map((question, index) => ({
309
+ id: `q${index + 1}`,
310
+ question,
311
+ status: "pending"
312
+ }));
313
+ }
314
+ function answerText(item) {
315
+ const selected = item.selected.map((entry) => entry.trim()).filter(Boolean).filter((entry) => entry !== ASK_DETAIL_OPTION);
316
+ const custom = item.custom?.trim() ?? "";
317
+ if (custom && selected.length) return `${selected.join(";")};${custom}`;
318
+ if (custom) return custom;
319
+ return selected.join(";");
320
+ }
321
+ function applyAnswers(items, answer) {
322
+ const byId = new Map((answer?.answers ?? []).map((entry) => [entry.id, entry]));
323
+ return items.map((item) => {
324
+ if (item.status !== "pending") return item;
325
+ const entry = byId.get(item.id);
326
+ if (!entry) return {
327
+ ...item,
328
+ status: "skipped"
329
+ };
330
+ const text = answerText(entry);
331
+ if (!text) return {
332
+ ...item,
333
+ status: "skipped"
334
+ };
335
+ return {
336
+ ...item,
337
+ status: "answered",
338
+ answer: text.slice(0, 400)
339
+ };
340
+ });
341
+ }
342
+ function markClarification(items, status) {
343
+ return items.map((item) => item.status === "pending" ? {
344
+ ...item,
345
+ status
346
+ } : item);
347
+ }
348
+ function readinessAfterAnswers(items, kind = "mild") {
349
+ if (items.some((item) => item.status === "pending")) return "pending";
350
+ if (items.length === 0) return "user-confirmed";
351
+ if (items.every((item) => item.status === "answered")) return "user-confirmed";
352
+ if (isBlockingKind(kind)) return "pending";
353
+ return "disclosed-assumptions";
354
+ }
355
+ function answeredNotes(items) {
356
+ return items.filter((item) => item.status === "answered" && item.answer).map((item) => `${item.question}:${item.answer}`);
357
+ }
358
+ function isAskAborted(error) {
359
+ if (!error || typeof error !== "object") return false;
360
+ const row = error;
361
+ if (row.code === "ASK_ABORTED" || row.code === "ASK_CANCELLED") return true;
362
+ const name = String(row.name ?? "");
363
+ return name === "AbortError" || name === "TimeoutError";
364
+ }
365
+ function isAbortLike(error, signal) {
366
+ if (signal?.aborted) return true;
367
+ if (!error || typeof error !== "object") return false;
368
+ const row = error;
369
+ const name = String(row.name ?? "");
370
+ const code = String(row.code ?? "");
371
+ return name === "AbortError" || name === "TimeoutError" || code === "ABORT_ERR" || code === "ABORTED" || isAskAborted(error);
372
+ }
373
+ //#endregion
374
+ //#region src/schema.ts
375
+ function asRecord(value) {
376
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
377
+ }
378
+ function asString(value, max) {
379
+ if (typeof value !== "string") return void 0;
380
+ const text = value.trim();
381
+ if (text.length > max) return void 0;
382
+ return text;
383
+ }
384
+ function asStringList(value, maxItems, maxChars) {
385
+ if (!Array.isArray(value) || value.length > maxItems) return void 0;
386
+ const items = [];
387
+ for (const entry of value) {
388
+ if (typeof entry !== "string" || entry.length > maxChars) return void 0;
389
+ items.push(entry);
390
+ }
391
+ return items;
392
+ }
393
+ function parseModeUpdate(payload) {
394
+ const row = asRecord(payload);
395
+ if (!row) return void 0;
396
+ if (typeof row.expectedRevision !== "number" || !Number.isInteger(row.expectedRevision) || row.expectedRevision < 0) return void 0;
397
+ if (!isMoodMode(row.mode)) return void 0;
398
+ return {
399
+ expectedRevision: row.expectedRevision,
400
+ mode: row.mode
401
+ };
402
+ }
403
+ function parseManual(payload) {
404
+ const sessionId = parseSessionId(payload);
405
+ return sessionId ? { sessionId } : void 0;
406
+ }
407
+ const PATCH_KEYS = [
408
+ "goal",
409
+ "deliverables",
410
+ "inScope",
411
+ "outOfScope",
412
+ "constraints",
413
+ "acceptance",
414
+ "assumptions",
415
+ "questions"
416
+ ];
417
+ function parseEdit(payload) {
418
+ const row = asRecord(payload);
419
+ if (!row) return void 0;
420
+ const sessionId = parseSessionId(row);
421
+ if (!sessionId) return void 0;
422
+ if (typeof row.expectedRevision !== "number" || !Number.isInteger(row.expectedRevision) || row.expectedRevision < 0) return void 0;
423
+ const patchRow = asRecord(row.patch);
424
+ if (!patchRow) return void 0;
425
+ const patch = {};
426
+ for (const key of Object.keys(patchRow)) if (!PATCH_KEYS.includes(key)) return void 0;
427
+ if (patchRow.goal !== void 0) {
428
+ const goal = asString(patchRow.goal, 2e3);
429
+ if (goal === void 0) return void 0;
430
+ patch.goal = goal;
431
+ }
432
+ for (const key of [
433
+ "deliverables",
434
+ "inScope",
435
+ "outOfScope",
436
+ "constraints",
437
+ "acceptance",
438
+ "assumptions",
439
+ "questions"
440
+ ]) {
441
+ if (patchRow[key] === void 0) continue;
442
+ const list = asStringList(patchRow[key], 16, 400);
443
+ if (!list) return void 0;
444
+ patch[key] = list;
445
+ }
446
+ if (Object.keys(patch).length === 0) return void 0;
447
+ return {
448
+ sessionId,
449
+ expectedRevision: row.expectedRevision,
450
+ patch
451
+ };
452
+ }
453
+ //#endregion
454
+ //#region src/service.ts
455
+ var MoodService = class {
456
+ options;
457
+ settings;
458
+ sessions = /* @__PURE__ */ new Map();
459
+ storageFailed = false;
460
+ active = true;
461
+ ai;
462
+ unregisterPurpose;
463
+ pending = Promise.resolve();
464
+ now;
465
+ nextId;
466
+ constructor(options) {
467
+ this.options = options;
468
+ const loaded = cloneState(options.store?.load() ?? {
469
+ settings: defaultSettings(),
470
+ sessions: {}
471
+ });
472
+ this.settings = loaded.settings;
473
+ for (const [sessionId, row] of Object.entries(loaded.sessions)) this.sessions.set(sessionId, {
474
+ ...row,
475
+ clarification: row.clarification ?? [],
476
+ pendingManual: Boolean(row.pendingManual),
477
+ generation: 0
478
+ });
479
+ this.now = options.now ?? Date.now;
480
+ this.nextId = options.id ?? randomUUID;
481
+ this.syncAi();
482
+ }
483
+ getContract(sessionId) {
484
+ const contract = this.sessions.get(sessionId)?.contract;
485
+ return contract ? cloneContract(contract) : void 0;
486
+ }
487
+ status(sessionId) {
488
+ const status = {
489
+ settings: { ...this.settings },
490
+ storageFailed: this.storageFailed
491
+ };
492
+ if (!sessionId) return status;
493
+ const row = this.ensure(sessionId);
494
+ status.session = this.view(sessionId, row);
495
+ return status;
496
+ }
497
+ isActive() {
498
+ return this.active;
499
+ }
500
+ async dispose() {
501
+ this.active = false;
502
+ for (const row of this.sessions.values()) {
503
+ row.generation += 1;
504
+ row.work?.abort();
505
+ row.work = void 0;
506
+ }
507
+ this.detachAi();
508
+ await this.pending;
509
+ }
510
+ async call(endpoint, payload, signal) {
511
+ if (!this.active) return fail("MOOD_DISABLED", "需求澄清已关闭。");
512
+ if (signal.aborted) return fail("MOOD_CANCELLED", "请求已取消。");
513
+ try {
514
+ if (endpoint === "status") return ok(this.status(parseSessionId(payload)));
515
+ if (endpoint === "contract") {
516
+ const sessionId = this.hostSessionId(parseSessionId(payload), false);
517
+ if (!sessionId) return fail("MOOD_INVALID", "缺少会话。");
518
+ return ok(this.getContract(sessionId) ?? null);
519
+ }
520
+ if (endpoint === "mode") {
521
+ const parsed = parseModeUpdate(payload);
522
+ if (!parsed) return fail("MOOD_INVALID", "模式格式无效。");
523
+ return ok(await this.updateMode(parsed.mode, parsed.expectedRevision));
524
+ }
525
+ if (endpoint === "manual") {
526
+ const parsed = parseManual(payload);
527
+ if (!parsed) return fail("MOOD_INVALID", "缺少会话。");
528
+ const sessionId = this.hostSessionId(parsed.sessionId, true);
529
+ if (!sessionId) return fail("MOOD_SESSION_NOT_FOUND", "会话不在当前 Host。");
530
+ return ok(await this.requestManual(sessionId));
531
+ }
532
+ if (endpoint === "retry") {
533
+ const parsed = parseManual(payload);
534
+ if (!parsed) return fail("MOOD_INVALID", "缺少会话。");
535
+ const sessionId = this.hostSessionId(parsed.sessionId, true);
536
+ if (!sessionId) return fail("MOOD_SESSION_NOT_FOUND", "会话不在当前 Host。");
537
+ return ok(await this.requestRetry(sessionId));
538
+ }
539
+ if (endpoint === "edit") {
540
+ const parsed = parseEdit(payload);
541
+ if (!parsed) return fail("MOOD_INVALID", "修订格式无效。");
542
+ const sessionId = this.hostSessionId(parsed.sessionId, true);
543
+ if (!sessionId) return fail("MOOD_SESSION_NOT_FOUND", "会话不在当前 Host。");
544
+ return ok(await this.editContract(sessionId, parsed.expectedRevision, parsed.patch));
545
+ }
546
+ return fail("MOOD_INVALID", "未知操作。");
547
+ } catch (error) {
548
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "MOOD_FAILED";
549
+ return fail(code, error instanceof Error ? error.message : "需求澄清操作失败。");
550
+ }
551
+ }
552
+ async handlePreStep(payload, next) {
553
+ const inner = await next();
554
+ if (!this.active) return inner;
555
+ if (inner.kind !== "enter") return inner;
556
+ payload.signal.throwIfAborted();
557
+ const sessionId = sessionIdOf(payload.agent);
558
+ if (!sessionId) return inner;
559
+ const claimedMessages = payload.messages;
560
+ const state = this.ensure(sessionId);
561
+ state.projectId = projectIdFromCwd(payload.agent.session?.header?.cwd ?? payload.agent.session?.meta?.cwd) ?? projectIdFromCwd(this.options.liveSession?.(sessionId)?.header?.cwd ?? this.options.liveSession?.(sessionId)?.meta?.cwd) ?? state.projectId;
562
+ if (incomingAreAuxiliaryOnly(claimedMessages) && !state.pendingManual) {
563
+ if (this.isBlocked(state)) return { kind: "reject" };
564
+ return this.enter(inner, state.contract, state.clarification);
565
+ }
566
+ const claimed = collectClaimedHumans(claimedMessages);
567
+ const sourceVersion = sourceVersionOf(claimed);
568
+ const evidence = evidenceForRequest(sessionId, this.options.readEvents(sessionId) ?? [], claimed);
569
+ const text = claimed.at(-1)?.text ?? "";
570
+ if (claimed.length) this.supersedeIfNew(sessionId, sourceVersion);
571
+ const live = this.ensure(sessionId);
572
+ if (live.lastHandledVersion === sourceVersion && !live.pendingManual && isSettled(live.contract?.readiness)) return this.enter(inner, live.contract, live.clarification);
573
+ const kind = classifyRequest(text, { hasConfirmedContract: isSettled(live.contract?.readiness) });
574
+ if (kind === "skip" && !live.pendingManual) return this.enter(inner, live.contract, live.clarification);
575
+ if (shouldWriteClearContract(kind, live.pendingManual, this.settings.mode)) {
576
+ const generation = live.generation;
577
+ const contract = thinContract({
578
+ id: this.nextId(),
579
+ sessionId,
580
+ sourceVersion,
581
+ revision: nextRevision(live.contract),
582
+ goal: text.slice(0, 2e3),
583
+ evidence,
584
+ readiness: "clear-request",
585
+ now: this.now()
586
+ });
587
+ if (!await this.commitIfCurrent(sessionId, generation, {
588
+ contract,
589
+ clarification: [],
590
+ lastHandledVersion: sourceVersion,
591
+ pendingManual: false,
592
+ heldRequest: void 0,
593
+ projectId: live.projectId
594
+ })) return this.afterStale(inner);
595
+ return this.enter(inner, contract, []);
596
+ }
597
+ if (!shouldAnalyze(kind, this.settings.mode, live.pendingManual)) return inner;
598
+ const generation = live.generation;
599
+ live.inflightVersion = sourceVersion;
600
+ const work = this.replaceWork(sessionId);
601
+ const combined = combineSignals(payload.signal, work.signal, this.ai?.signal);
602
+ let draft = void 0;
603
+ try {
604
+ draft = await this.runAnalysis({
605
+ sessionId,
606
+ sourceVersion,
607
+ text,
608
+ evidence,
609
+ signal: combined,
610
+ generation
611
+ });
612
+ } catch (error) {
613
+ if (!this.generationCurrent(sessionId, generation)) return this.afterStale(inner);
614
+ const status = isAbortLike(error, combined) ? "cancelled" : "pending";
615
+ await this.blockUnresolved({
616
+ sessionId,
617
+ generation,
618
+ sourceVersion,
619
+ text,
620
+ evidence,
621
+ kind,
622
+ claimed: claimedMessages,
623
+ status,
624
+ inner
625
+ });
626
+ return { kind: "reject" };
627
+ }
628
+ if (!this.generationCurrent(sessionId, generation)) return this.afterStale(inner);
629
+ const questions = boundQuestions(draft?.questions ?? [], kind);
630
+ const clarification = pendingClarifications(questions);
631
+ let contract = contractFromDraft({
632
+ id: this.nextId(),
633
+ sessionId,
634
+ sourceVersion,
635
+ revision: nextRevision(this.ensure(sessionId).contract),
636
+ goalFallback: text.slice(0, 2e3),
637
+ evidence,
638
+ draft,
639
+ questions,
640
+ readiness: questions.length || isBlockingKind(kind) ? "pending" : "disclosed-assumptions",
641
+ now: this.now()
642
+ });
643
+ if (questions.length > 0) {
644
+ const asked = await this.askOnce(payload.agent, clarification, combined);
645
+ if (!this.generationCurrent(sessionId, generation)) return this.afterStale(inner);
646
+ if (asked.kind === "aborted" || asked.kind === "failed") {
647
+ const status = asked.kind === "aborted" ? "cancelled" : "pending";
648
+ await this.blockUnresolved({
649
+ sessionId,
650
+ generation,
651
+ sourceVersion,
652
+ text,
653
+ evidence,
654
+ kind,
655
+ claimed: claimedMessages,
656
+ status,
657
+ inner,
658
+ contract,
659
+ clarification: asked.kind === "aborted" ? markClarification(clarification, "cancelled") : clarification
660
+ });
661
+ return { kind: "reject" };
662
+ }
663
+ const answered = applyAnswers(clarification, asked.answer);
664
+ const readiness = readinessAfterAnswers(answered, kind);
665
+ if (isBlockingKind(kind) && readiness !== "user-confirmed") {
666
+ contract = {
667
+ ...contract,
668
+ readiness: "pending",
669
+ assumptions: draft?.assumptions ?? contract.assumptions,
670
+ constraints: [...contract.constraints, ...answeredNotes(answered)],
671
+ questions: answered.filter((item) => item.status !== "answered").map((item) => item.question),
672
+ updatedAt: this.now()
673
+ };
674
+ await this.commitIfCurrent(sessionId, generation, {
675
+ contract,
676
+ clarification: answered,
677
+ pendingManual: false,
678
+ lastHandledVersion: void 0,
679
+ heldRequest: holdOf(sourceVersion, kind, claimedMessages),
680
+ projectId: this.ensure(sessionId).projectId
681
+ });
682
+ return { kind: "reject" };
683
+ }
684
+ contract = {
685
+ ...contract,
686
+ readiness,
687
+ assumptions: [...contract.assumptions, ...isBlockingKind(kind) ? [] : answered.filter((item) => item.status === "skipped").map((item) => `未回答:${item.question}`)],
688
+ constraints: [...contract.constraints, ...answeredNotes(answered)],
689
+ questions: answered.filter((item) => item.status !== "answered").map((item) => item.question),
690
+ updatedAt: this.now()
691
+ };
692
+ if (!await this.commitIfCurrent(sessionId, generation, {
693
+ contract,
694
+ clarification: answered,
695
+ lastHandledVersion: sourceVersion,
696
+ pendingManual: false,
697
+ heldRequest: void 0,
698
+ projectId: this.ensure(sessionId).projectId
699
+ })) return this.afterStale(inner);
700
+ return this.enter(inner, contract, answered);
701
+ }
702
+ if (isBlockingKind(kind) && contract.readiness === "pending") {
703
+ await this.commitIfCurrent(sessionId, generation, {
704
+ contract,
705
+ clarification,
706
+ pendingManual: false,
707
+ lastHandledVersion: void 0,
708
+ heldRequest: holdOf(sourceVersion, kind, claimedMessages),
709
+ projectId: this.ensure(sessionId).projectId
710
+ });
711
+ return { kind: "reject" };
712
+ }
713
+ if (!await this.commitIfCurrent(sessionId, generation, {
714
+ contract,
715
+ clarification,
716
+ lastHandledVersion: sourceVersion,
717
+ pendingManual: false,
718
+ heldRequest: void 0,
719
+ projectId: this.ensure(sessionId).projectId
720
+ })) return this.afterStale(inner);
721
+ return this.enter(inner, contract, clarification);
722
+ }
723
+ async runAnalysis(input) {
724
+ const scope = this.ai;
725
+ if (!scope?.active) return void 0;
726
+ input.signal.throwIfAborted();
727
+ const result = await scope.run({
728
+ purpose: MOOD_ANALYZE_PURPOSE,
729
+ sessionId: input.sessionId,
730
+ sourceVersion: input.sourceVersion,
731
+ promptVersion: PROMPT_VERSION,
732
+ schemaVersion: SCHEMA_VERSION,
733
+ system: ANALYZE_SYSTEM,
734
+ input: JSON.stringify({
735
+ text: input.text,
736
+ evidence: input.evidence
737
+ }),
738
+ signal: input.signal,
739
+ priority: "interactive",
740
+ isCurrent: () => this.generationCurrent(input.sessionId, input.generation) && !input.signal.aborted
741
+ });
742
+ if (result.receipt.status !== "success") return void 0;
743
+ if (!this.generationCurrent(input.sessionId, input.generation)) return void 0;
744
+ return parseAnalysis(result.text);
745
+ }
746
+ async askOnce(agent, clarification, signal) {
747
+ const ask = this.options.askUser;
748
+ const items = toAskItems(clarification);
749
+ if (!items.length || !ask) return { kind: "failed" };
750
+ try {
751
+ return {
752
+ kind: "answered",
753
+ answer: await ask({
754
+ agent,
755
+ questions: items,
756
+ signal
757
+ })
758
+ };
759
+ } catch (error) {
760
+ if (isAbortLike(error, signal)) return { kind: "aborted" };
761
+ return { kind: "failed" };
762
+ }
763
+ }
764
+ afterStale(inner) {
765
+ if (!this.active) return inner;
766
+ return { kind: "reject" };
767
+ }
768
+ enter(decision, contract, clarification) {
769
+ if (!this.active || !contract) return decision;
770
+ if (contract.readiness === "pending" || contract.readiness === "cancelled" || contract.readiness === "stale") return decision;
771
+ const text = formatContract(contract, clarification);
772
+ const extra = this.options.createInjectMessage ? this.options.createInjectMessage(text) : createMoodContextMessage(text);
773
+ return {
774
+ ...decision,
775
+ messages: mergeContractMessage(decision.messages ?? [], extra)
776
+ };
777
+ }
778
+ async blockUnresolved(input) {
779
+ if (!this.generationCurrent(input.sessionId, input.generation)) return;
780
+ const state = this.ensure(input.sessionId);
781
+ const questions = boundQuestions(input.contract?.questions ?? [], input.kind);
782
+ const clarification = input.clarification ?? markClarification(pendingClarifications(questions), input.status === "pending" ? "pending" : input.status);
783
+ const contract = input.contract ? {
784
+ ...input.contract,
785
+ readiness: input.status,
786
+ questions,
787
+ updatedAt: this.now()
788
+ } : thinContract({
789
+ id: this.nextId(),
790
+ sessionId: input.sessionId,
791
+ sourceVersion: input.sourceVersion,
792
+ revision: nextRevision(state.contract),
793
+ goal: input.text.slice(0, 2e3),
794
+ evidence: input.evidence,
795
+ readiness: input.status,
796
+ now: this.now(),
797
+ extra: { questions }
798
+ });
799
+ const hold = isBlockingKind(input.kind) || clarification.length > 0 ? holdOf(input.sourceVersion, input.kind, input.claimed) : void 0;
800
+ await this.commitIfCurrent(input.sessionId, input.generation, {
801
+ contract,
802
+ clarification,
803
+ pendingManual: false,
804
+ lastHandledVersion: void 0,
805
+ heldRequest: hold,
806
+ projectId: state.projectId
807
+ });
808
+ input.inner;
809
+ }
810
+ async updateMode(mode, expectedRevision) {
811
+ return this.serialize(async () => {
812
+ this.assertLive();
813
+ if (expectedRevision !== this.settings.revision) coded("MOOD_STALE", "需求澄清设置已更新,请刷新后重试。");
814
+ const proposed = this.snapshot();
815
+ proposed.settings = {
816
+ mode,
817
+ revision: this.settings.revision + 1
818
+ };
819
+ await this.persistProposed(proposed);
820
+ this.commitState(proposed);
821
+ return this.status();
822
+ });
823
+ }
824
+ async requestManual(sessionId) {
825
+ const state = this.ensure(sessionId);
826
+ const generation = state.generation;
827
+ const held = state.heldRequest;
828
+ if (!await this.commitIfCurrent(sessionId, generation, {
829
+ pendingManual: true,
830
+ lastHandledVersion: void 0,
831
+ heldRequest: held,
832
+ clarification: state.clarification,
833
+ contract: state.contract,
834
+ projectId: state.projectId
835
+ })) coded("MOOD_CANCELLED", "会话已切换,未执行手动分析。");
836
+ if (held?.messages.length) await this.resumeExact(sessionId, held.messages);
837
+ return this.status(sessionId);
838
+ }
839
+ async requestRetry(sessionId) {
840
+ const state = this.ensure(sessionId);
841
+ const held = state.heldRequest;
842
+ if (!held?.messages.length) coded("MOOD_NOT_FOUND", "没有可按原请求重试的内容。");
843
+ const generation = state.generation;
844
+ if (!await this.commitIfCurrent(sessionId, generation, {
845
+ pendingManual: false,
846
+ lastHandledVersion: void 0,
847
+ heldRequest: held,
848
+ clarification: state.clarification.map((item) => item.status === "cancelled" || item.status === "stale" ? {
849
+ ...item,
850
+ status: "pending",
851
+ answer: void 0
852
+ } : item),
853
+ contract: state.contract ? {
854
+ ...state.contract,
855
+ readiness: "pending",
856
+ updatedAt: this.now()
857
+ } : state.contract,
858
+ projectId: state.projectId
859
+ })) coded("MOOD_CANCELLED", "会话已切换,未重试。");
860
+ await this.resumeExact(sessionId, held.messages);
861
+ return this.status(sessionId);
862
+ }
863
+ async resumeExact(sessionId, messages) {
864
+ const resume = this.options.resumeHeld;
865
+ if (!resume) coded("MOOD_NO_RESUME", "当前 Host 不能按原请求恢复。");
866
+ await resume(sessionId, messages);
867
+ }
868
+ async editContract(sessionId, expectedRevision, patch) {
869
+ const state = this.ensure(sessionId);
870
+ const generation = state.generation;
871
+ const current = state.contract;
872
+ if (!current) coded("MOOD_NOT_FOUND", "还没有可修订的约定。");
873
+ if (current.revision !== expectedRevision) coded("MOOD_STALE", "约定已更新,请刷新后重试。");
874
+ const evidence = [...current.evidence, {
875
+ sessionId,
876
+ seq: latestUserSeq(this.options.readEvents(sessionId) ?? []),
877
+ kind: "manual",
878
+ excerpt: excerptOf(patch.goal ?? "修订约定")
879
+ }];
880
+ const contract = {
881
+ ...current,
882
+ ...patch,
883
+ revision: current.revision + 1,
884
+ readiness: "user-confirmed",
885
+ evidence,
886
+ updatedAt: this.now()
887
+ };
888
+ if (!await this.commitIfCurrent(sessionId, generation, {
889
+ contract,
890
+ clarification: state.clarification.map((item) => item.status === "pending" ? {
891
+ ...item,
892
+ status: "answered",
893
+ answer: "作者直接修订约定"
894
+ } : item),
895
+ pendingManual: false,
896
+ lastHandledVersion: state.lastHandledVersion,
897
+ heldRequest: void 0,
898
+ projectId: state.projectId
899
+ })) coded("MOOD_CANCELLED", "会话已切换,未保存修订。");
900
+ return this.status(sessionId);
901
+ }
902
+ hostSessionId(sessionId, required) {
903
+ if (!sessionId) return void 0;
904
+ if (!this.options.liveSession) return sessionId;
905
+ const live = this.options.liveSession(sessionId);
906
+ if (!live) return required ? void 0 : sessionId;
907
+ return live.id != null ? String(live.id) : sessionId;
908
+ }
909
+ ensure(sessionId) {
910
+ const existing = this.sessions.get(sessionId);
911
+ if (existing) return existing;
912
+ const created = {
913
+ clarification: [],
914
+ pendingManual: false,
915
+ generation: 0
916
+ };
917
+ this.sessions.set(sessionId, created);
918
+ return created;
919
+ }
920
+ view(sessionId, row) {
921
+ return {
922
+ sessionId,
923
+ ...row.projectId ? { projectId: row.projectId } : {},
924
+ ...row.contract ? { contract: cloneContract(row.contract) } : {},
925
+ clarification: row.clarification.map((item) => ({ ...item })),
926
+ pendingManual: row.pendingManual,
927
+ held: Boolean(row.heldRequest?.messages.length)
928
+ };
929
+ }
930
+ isBlocked(state) {
931
+ if (!state.heldRequest) return false;
932
+ const readiness = state.contract?.readiness;
933
+ return readiness === "pending" || readiness === "cancelled" || state.clarification.some((item) => item.status === "pending" || item.status === "cancelled");
934
+ }
935
+ async commitIfCurrent(sessionId, generation, patch) {
936
+ return this.serialize(async () => {
937
+ if (!this.generationCurrent(sessionId, generation)) return false;
938
+ const proposed = this.snapshot();
939
+ proposed.sessions[sessionId] = storedOf({
940
+ ...this.ensure(sessionId),
941
+ ...patch
942
+ });
943
+ await this.persistProposed(proposed);
944
+ if (!this.generationCurrent(sessionId, generation)) return false;
945
+ this.commitState(proposed);
946
+ return true;
947
+ });
948
+ }
949
+ supersedeIfNew(sessionId, sourceVersion) {
950
+ const state = this.ensure(sessionId);
951
+ const previous = state.inflightVersion ?? state.heldRequest?.sourceVersion ?? state.contract?.sourceVersion;
952
+ if (!previous || previous === sourceVersion) return;
953
+ this.supersede(sessionId);
954
+ }
955
+ supersede(sessionId) {
956
+ const state = this.ensure(sessionId);
957
+ state.generation += 1;
958
+ state.work?.abort();
959
+ state.work = void 0;
960
+ state.inflightVersion = void 0;
961
+ state.heldRequest = void 0;
962
+ state.lastHandledVersion = void 0;
963
+ if (state.contract && state.contract.readiness !== "stale" && state.contract.readiness !== "cancelled") state.contract = {
964
+ ...state.contract,
965
+ readiness: "stale",
966
+ updatedAt: this.now()
967
+ };
968
+ state.clarification = markClarification(state.clarification, "stale");
969
+ }
970
+ replaceWork(sessionId) {
971
+ const state = this.ensure(sessionId);
972
+ state.work?.abort();
973
+ const work = new AbortController();
974
+ state.work = work;
975
+ return work;
976
+ }
977
+ generationCurrent(sessionId, generation) {
978
+ if (!this.active) return false;
979
+ const state = this.sessions.get(sessionId);
980
+ return Boolean(state && state.generation === generation);
981
+ }
982
+ snapshot() {
983
+ const sessions = {};
984
+ for (const [sessionId, row] of this.sessions) sessions[sessionId] = storedOf(row);
985
+ return cloneState({
986
+ settings: this.settings,
987
+ sessions
988
+ });
989
+ }
990
+ commitState(proposed) {
991
+ this.settings = proposed.settings;
992
+ const next = /* @__PURE__ */ new Map();
993
+ for (const [sessionId, row] of Object.entries(proposed.sessions)) {
994
+ const previous = this.sessions.get(sessionId);
995
+ next.set(sessionId, {
996
+ ...row,
997
+ generation: previous?.generation ?? 0,
998
+ work: previous?.work,
999
+ inflightVersion: previous?.inflightVersion
1000
+ });
1001
+ }
1002
+ this.sessions = next;
1003
+ }
1004
+ async persistProposed(proposed) {
1005
+ this.assertLive();
1006
+ if (!this.options.store) {
1007
+ this.storageFailed = false;
1008
+ return;
1009
+ }
1010
+ try {
1011
+ await this.options.store.save(cloneState(proposed));
1012
+ this.storageFailed = false;
1013
+ } catch (error) {
1014
+ this.storageFailed = true;
1015
+ if (error && typeof error === "object" && "code" in error) throw error;
1016
+ coded("MOOD_STORAGE", "需求澄清保存失败,已保留原内容。");
1017
+ }
1018
+ }
1019
+ assertLive() {
1020
+ if (!this.active) coded("MOOD_DISABLED", "需求澄清已关闭。");
1021
+ }
1022
+ serialize(run) {
1023
+ const task = this.pending.then(run, run);
1024
+ this.pending = task.then(() => {}, () => {});
1025
+ return task;
1026
+ }
1027
+ syncAi() {
1028
+ if (!this.active) {
1029
+ this.detachAi();
1030
+ return;
1031
+ }
1032
+ if (this.ai) return;
1033
+ const scope = this.options.activateAi?.();
1034
+ if (!scope) return;
1035
+ this.ai = scope;
1036
+ this.unregisterPurpose = scope.registerPurpose(analyzePurpose);
1037
+ }
1038
+ detachAi() {
1039
+ this.unregisterPurpose?.();
1040
+ this.unregisterPurpose = void 0;
1041
+ this.ai?.dispose();
1042
+ this.ai = void 0;
1043
+ }
1044
+ };
1045
+ function isSettled(readiness) {
1046
+ return readiness === "user-confirmed" || readiness === "clear-request" || readiness === "disclosed-assumptions";
1047
+ }
1048
+ function nextRevision(contract) {
1049
+ return (contract?.revision ?? 0) + 1;
1050
+ }
1051
+ function holdOf(sourceVersion, kind, messages) {
1052
+ return {
1053
+ sourceVersion,
1054
+ trigger: kind === "risk" || kind === "material" || kind === "mild" || kind === "clear" ? kind : "material",
1055
+ messages: structuredClone(messages)
1056
+ };
1057
+ }
1058
+ function storedOf(row) {
1059
+ return {
1060
+ clarification: row.clarification,
1061
+ pendingManual: row.pendingManual,
1062
+ ...row.contract ? { contract: row.contract } : {},
1063
+ ...row.lastHandledVersion !== void 0 ? { lastHandledVersion: row.lastHandledVersion } : {},
1064
+ ...row.projectId ? { projectId: row.projectId } : {},
1065
+ ...row.heldRequest ? { heldRequest: row.heldRequest } : {}
1066
+ };
1067
+ }
1068
+ function cloneState(state) {
1069
+ return structuredClone(state);
1070
+ }
1071
+ function coded(code, message) {
1072
+ throw Object.assign(new Error(message), { code });
1073
+ }
1074
+ function combineSignals(...signals) {
1075
+ const live = signals.filter((item) => Boolean(item));
1076
+ if (live.length === 0) return new AbortController().signal;
1077
+ if (live.length === 1) return live[0];
1078
+ return AbortSignal.any(live);
1079
+ }
1080
+ //#endregion
1081
+ //#region src/storage.ts
1082
+ const evidenceSchema = z.object({
1083
+ sessionId: z.string().min(1).max(200),
1084
+ seq: z.number().int().nonnegative(),
1085
+ kind: z.enum([
1086
+ "user",
1087
+ "tool",
1088
+ "turn",
1089
+ "manual"
1090
+ ]),
1091
+ excerpt: z.string().max(200).optional()
1092
+ }).strict();
1093
+ const contractSchema = z.object({
1094
+ id: z.string().min(1).max(120),
1095
+ sessionId: z.string().min(1).max(200),
1096
+ sourceVersion: z.string().min(1).max(400),
1097
+ revision: z.number().int().nonnegative(),
1098
+ goal: z.string().max(2e3),
1099
+ deliverables: z.array(z.string().max(400)).max(16),
1100
+ inScope: z.array(z.string().max(400)).max(16),
1101
+ outOfScope: z.array(z.string().max(400)).max(16),
1102
+ constraints: z.array(z.string().max(400)).max(16),
1103
+ acceptance: z.array(z.string().max(400)).max(16),
1104
+ assumptions: z.array(z.string().max(400)).max(16),
1105
+ questions: z.array(z.string().max(400)).max(8),
1106
+ evidence: z.array(evidenceSchema).max(32),
1107
+ readiness: z.enum([
1108
+ "pending",
1109
+ "clear-request",
1110
+ "user-confirmed",
1111
+ "disclosed-assumptions",
1112
+ "cancelled",
1113
+ "stale"
1114
+ ]),
1115
+ updatedAt: z.number().int().nonnegative()
1116
+ }).strict();
1117
+ const clarificationSchema = z.object({
1118
+ id: z.string().min(1).max(80),
1119
+ question: z.string().max(400),
1120
+ status: z.enum([
1121
+ "pending",
1122
+ "answered",
1123
+ "skipped",
1124
+ "cancelled",
1125
+ "stale"
1126
+ ]),
1127
+ answer: z.string().max(400).optional()
1128
+ }).strict();
1129
+ const heldRequestSchema = z.object({
1130
+ sourceVersion: z.string().min(1).max(400),
1131
+ trigger: z.enum([
1132
+ "material",
1133
+ "risk",
1134
+ "mild",
1135
+ "clear"
1136
+ ]),
1137
+ messages: z.array(z.unknown()).max(32)
1138
+ }).strict();
1139
+ const sessionSchema = z.object({
1140
+ contract: contractSchema.optional(),
1141
+ clarification: z.array(clarificationSchema).max(8),
1142
+ lastHandledVersion: z.string().max(400).optional(),
1143
+ pendingManual: z.boolean(),
1144
+ projectId: z.string().max(MAX_PROJECT_ID_CHARS).optional(),
1145
+ heldRequest: heldRequestSchema.optional()
1146
+ }).strict();
1147
+ const settingsSchema = z.object({
1148
+ revision: z.number().int().nonnegative(),
1149
+ mode: z.enum([
1150
+ "auto",
1151
+ "manual",
1152
+ "strict"
1153
+ ])
1154
+ }).strict();
1155
+ const moodStateSchema = z.object({
1156
+ settings: settingsSchema,
1157
+ sessions: z.record(z.string().min(1).max(200), sessionSchema)
1158
+ }).strict();
1159
+ const moodDomain = defineDomain({
1160
+ name: "dsh_editor_mood",
1161
+ version: 1,
1162
+ tables: { state: domainTable(moodStateSchema) }
1163
+ });
1164
+ function emptyMoodState() {
1165
+ return {
1166
+ settings: defaultSettings(),
1167
+ sessions: {}
1168
+ };
1169
+ }
1170
+ function domainStore(domain) {
1171
+ const table = domain.table("state");
1172
+ return {
1173
+ load() {
1174
+ const row = table.get("global");
1175
+ if (!row) return emptyMoodState();
1176
+ return {
1177
+ settings: {
1178
+ ...defaultSettings(),
1179
+ ...row.settings
1180
+ },
1181
+ sessions: { ...row.sessions ?? {} }
1182
+ };
1183
+ },
1184
+ async save(state) {
1185
+ await table.put("global", {
1186
+ settings: { ...state.settings },
1187
+ sessions: { ...state.sessions }
1188
+ });
1189
+ }
1190
+ };
1191
+ }
1192
+ //#endregion
1193
+ //#region src/index.ts
1194
+ const name = MOOD_PLUGIN;
1195
+ const inject = [
1196
+ "aiServices",
1197
+ "storageDomain",
1198
+ "sessions",
1199
+ "userQuestions",
1200
+ "agents",
1201
+ "connection",
1202
+ "webServer"
1203
+ ];
1204
+ async function apply(ctx) {
1205
+ const host = ctx;
1206
+ if (!host.storageDomain || !host.aiServices || !host.sessions || !host.userQuestions || !host.agents) throw new Error("dsh-mood requires Host aiServices, storageDomain, sessions, userQuestions, and agents");
1207
+ const domain = await host.storageDomain.open(moodDomain);
1208
+ const service = new MoodService({
1209
+ store: domainStore(domain),
1210
+ readEvents: (sessionId) => readSessionEvents(host, sessionId),
1211
+ liveSession: (sessionId) => readLiveSession(host, sessionId),
1212
+ activateAi: () => host.aiServices.activate(MOOD_PLUGIN),
1213
+ askUser: (request) => host.userQuestions.ask(request),
1214
+ createInjectMessage: (text) => createMoodContextMessage(text),
1215
+ resumeHeld: async (sessionId, messages) => {
1216
+ resumeHeldOnHost(host.agents, sessionId, messages);
1217
+ }
1218
+ });
1219
+ ctx.provide("aiMood", service);
1220
+ ctx.effect(() => async () => {
1221
+ await service.dispose();
1222
+ await domain.close();
1223
+ }, "dsh-mood.dispose");
1224
+ ctx.effect(() => registerHostRpc(host, MOOD_RPC_CHANNEL, (endpoint, payload, signal) => service.call(endpoint, payload, signal)), "dsh-mood.rpc");
1225
+ ctx.effect(() => {
1226
+ const offStep = listen(ctx, "agent/pre-step", async (payload, next) => service.handlePreStep(payload, next));
1227
+ return () => {
1228
+ offStep?.();
1229
+ };
1230
+ }, "dsh-mood.pre-step");
1231
+ }
1232
+ function readLiveSession(host, sessionId) {
1233
+ return host.sessions.get(sessionId) ?? host.sessions.get(String(sessionId));
1234
+ }
1235
+ function readSessionEvents(host, sessionId) {
1236
+ return readLiveSession(host, sessionId)?.snapshotEvents?.();
1237
+ }
1238
+ /** Bound invoke: native steer/followup call this.send on the live Agents.get instance. */
1239
+ function resumeHeldOnHost(agents, sessionId, messages) {
1240
+ const agent = agents.get(sessionId) ?? agents.get(String(sessionId));
1241
+ if (!agent) throw Object.assign(/* @__PURE__ */ new Error("当前 Host 没有可恢复的会话。"), { code: "MOOD_SESSION_NOT_FOUND" });
1242
+ const humans = messages.filter((message) => isHumanUserMessage(message));
1243
+ if (!humans.length) throw Object.assign(/* @__PURE__ */ new Error("没有可重试的原请求。"), { code: "MOOD_NOT_FOUND" });
1244
+ if (typeof agent.steer === "function") {
1245
+ for (const human of humans) agent.steer(human);
1246
+ return;
1247
+ }
1248
+ if (typeof agent.followup === "function") {
1249
+ for (const human of humans) agent.followup(human);
1250
+ return;
1251
+ }
1252
+ throw Object.assign(/* @__PURE__ */ new Error("当前会话不能按原请求重试。"), { code: "MOOD_NO_RESUME" });
1253
+ }
1254
+ function listen(ctx, name, handler) {
1255
+ const off = ctx.on.call(ctx, name, handler);
1256
+ return typeof off === "function" ? off : void 0;
1257
+ }
1258
+ //#endregion
1259
+ export { CHAT_EVENTS_SLOT, MOOD_AI_PLUGIN, MOOD_PLUGIN, MOOD_RPC_CHANNEL, MoodService, apply, createMoodContextMessage, defaultSettings, domainStore, inject, moodDomain, name, projectIdFromCwd, resumeHeldOnHost };
1260
+
1261
+ //# sourceMappingURL=index.js.map