diff --git a/content/chapters/chapter1.json b/content/chapters/chapter1.json index 6a42eed..60124b1 100644 --- a/content/chapters/chapter1.json +++ b/content/chapters/chapter1.json @@ -45,7 +45,15 @@ "dimensions": ["attachment", "self_disclosure"], "followUp": { "maxQuestions": 1, - "strategy": "Jev 检测五感/人物/时空/情绪缺失维度,馆长口吻追问一句(MVP 未接,占位)" + "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,馆长口吻模板追问一句;记不清了合法" + }, + "followUps": { + "visual": "闭上眼再想一想——那个画面里,最显眼的是什么颜色,或者什么物件?", + "person": "这段离别里,你最想再见到的那个人,是谁?", + "time_place": "那是哪一年的事?在什么地方?", + "emotion": "现在回想起来,心里最先浮上来的,是什么感觉?", + "auditory": "那段记忆里有没有声音?一句话,或是一种声响。", + "olfactory_gustatory": "那天有没有什么气味或味道,你到现在还记得?" }, "variants": [ { "signature": "温暖×永失", "variantId": "sealed_love_tomb", "label": "「爱被封存」墓室", "tint": "#d4a96a" }, diff --git a/content/chapters/chapter2.json b/content/chapters/chapter2.json index 1c97a7c..734bd03 100644 --- a/content/chapters/chapter2.json +++ b/content/chapters/chapter2.json @@ -50,7 +50,15 @@ "dimensions": ["values", "emotional_stability"], "followUp": { "maxQuestions": 1, - "strategy": "Jev 检测五感/人物/时空/情绪缺失维度,老人口吻追问一句(MVP 未接,占位)" + "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,老人口吻模板追问一句;记不清了合法" + }, + "followUps": { + "visual": "老朽眼拙,你再说说——那场景里最扎眼的是个什么物件?", + "person": "这事儿里头,牵着的是哪个人?说个称呼也成。", + "time_place": "是哪一年的事?在哪个地界?", + "emotion": "讲完了,心里是个什么滋味?别怕说出来。", + "auditory": "那会儿耳边有什么声响没有?一句话也算。", + "olfactory_gustatory": "人呐,记性最靠的是鼻子和舌头——那阵子有什么味道?" }, "variants": [ { "signature": "自责", "variantId": "intel_full", "label": "老人展开整张手绘城图(情报详细度·上)", "tint": "#6a8a6a" }, @@ -116,7 +124,15 @@ "dimensions": ["attachment", "reciprocity"], "followUp": { "maxQuestions": 1, - "strategy": "Jev 检测五感/人物/时空/情绪缺失维度,温简口吻追问一句(MVP 未接,占位)" + "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,温简口吻模板追问一句;记不清了合法" + }, + "followUps": { + "visual": "那年的画面里,你印象最深的一幕是什么样子的?", + "person": "那段日子里,有没有谁一直陪着你?", + "time_place": "那是哪一年,在什么地方?", + "emotion": "那时候最难熬的,是哪种感觉?", + "auditory": "有没有一个声音,到现在还忘不掉?", + "olfactory_gustatory": "那段日子有什么气味或味道,留在记忆里了吗?" }, "variants": [ { "signature": "深暴露", "variantId": "wenjian_close_early", "label": "温简当夜就悄悄告诉你一个档案馆的秘密", "tint": "#d4844b" }, diff --git a/content/chapters/chapter3.json b/content/chapters/chapter3.json index ed01eba..a9905d6 100644 --- a/content/chapters/chapter3.json +++ b/content/chapters/chapter3.json @@ -41,7 +41,15 @@ "dimensions": ["emotional_stability", "attachment"], "followUp": { "maxQuestions": 1, - "strategy": "Jev 检测五感/人物/时空/情绪缺失维度,幻境中故人口吻追问一句(MVP 未接,占位)" + "strategy": "Jev 六维缺失检测(五感/人物/时空/情绪),按幻境渲染价值取最高缺口,幻境中故人口吻模板追问一句;记不清了合法" + }, + "followUps": { + "visual": "雾里的影像还模糊——再想一想,当时眼前最清楚的是什么?", + "person": "幻境里还站着一个人。那个人是谁?", + "time_place": "幻境在等一个坐标:那是何时、何地?", + "emotion": "把那场告别说完——你最后想说的那句话,带着什么心情?", + "auditory": "幻境里太安静了。当时有什么声音吗?", + "olfactory_gustatory": "幻境还原不了气味——除非你现在想起来。" }, "variantFrom": "C1-02", "variants": [ diff --git a/package.json b/package.json index e00a991..bae924e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@tauri-apps/api": "^2.12.1", + "@tauri-apps/plugin-sql": "^2.5.0", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2593ca3..a9f3a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@tauri-apps/api': specifier: ^2.12.1 version: 2.12.1 + '@tauri-apps/plugin-sql': + specifier: ^2.5.0 + version: 2.5.0 react: specifier: ^19.2.8 version: 19.3.0 @@ -507,6 +510,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-sql@2.5.0': + resolution: {integrity: sha512-wHzfXAgMbn3G1xWQWM1LlWLJkBEfO2CkAWvu+VYigRBXBocPXZqvGEkYWSM1RrZSSc4iJYAWXkFZrgTyu8zBkw==} + '@types/node@24.19.1': resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==} @@ -979,6 +985,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.12.1 '@tauri-apps/cli-win32-x64-msvc': 2.12.1 + '@tauri-apps/plugin-sql@2.5.0': + dependencies: + '@tauri-apps/api': 2.12.1 + '@types/node@24.19.1': dependencies: undici-types: 7.24.6 diff --git a/scripts/verify-live.ts b/scripts/verify-live.ts index 7c471a4..4f3d320 100644 --- a/scripts/verify-live.ts +++ b/scripts/verify-live.ts @@ -62,9 +62,16 @@ console.log(' 抽取:', JSON.stringify(profile)); await engine.confirmIdentity(profile); while (engine.currentNode().type !== 'narration') engine.advance(); const farewell = '外婆走的那天我在外地考试,没赶上最后一面。后来我每年清明都回去,院子里的桂花树还在。'; -const result = await engine.submitNarration(farewell); +let result = await engine.submitNarration(farewell); console.log('\n[2] C1-02 情绪签名 → 场景变体'); console.log(' 输入:', farewell); +if (!result && engine.state.pendingFollowUp) { + // 追问补全(六维缺失检测):真 key 下可能挂起追问,回答一句完成流程 + const p = engine.state.pendingFollowUp; + console.log(` 追问(${p.dimension}): ${p.prompt}`); + result = await engine.answerFollowUp('院子里是桂花的甜香,风一吹就往屋里钻。'); +} +if (!result) throw new Error('讲述流程未完成'); console.log(` 签名: ${result.variant.signature} → 变体: ${result.variant.variantId}(${result.variant.label})`); // --- 3. 画像报告 7 维 score --- diff --git a/scripts/verify.ts b/scripts/verify.ts index 85901f5..dc7f28e 100644 --- a/scripts/verify.ts +++ b/scripts/verify.ts @@ -2,14 +2,21 @@ * 内容完整性与引擎流程验证(不走浏览器): * 1. 章节 JSON 的选择点 ID、维度标签与 docs/story-framework.md 第 4 节一一对应; * 2. 节点图完整(startNodeId 存在、所有 next 跳转有效、全部节点可达); - * 3. 用 MockJevClient + 内存 EventStore 跑通三章 → 16 个选择点全部落事件 → Jev 7 维打分。 + * 3. 用 MockJevClient + 内存 EventStore 跑通三章 → 16 个选择点全部落事件 → Jev 7 维打分; + * 4. 追问补全流程:缺失检测 → 追问事件 / 跳过事件("记不清了"合法)→ 回答合并进讲述文本; + * 5. EventStore 各实现(含 SqliteEventStore)的 append-only 性质(接口层面断言无 update/delete; + * SQLite 实连只能在 Tauri 运行时内进行,Node 下不实例化)。 * 运行:pnpm verify */ import { readFileSync } from 'node:fs'; import { StoryEngine } from '../src/engine/engine'; +import { LocalStorageEventStore } from '../src/engine/eventStore'; import type { EventStore, StoryEvent } from '../src/engine/eventStore'; +import { SqliteEventStore } from '../src/engine/sqliteEventStore'; import type { Chapter, ChoiceNode, NarrationNode } from '../src/engine/types'; +import { FOLLOW_UP_DIMENSIONS } from '../src/engine/types'; import { MockJevClient } from '../src/services/jev'; +import type { JevAnswer, JevQuestion } from '../src/services/jev'; // story-framework.md 第 4 节的权威选择点地图(ID → 期望维度标签) const EXPECTED: Record = { @@ -113,6 +120,12 @@ for (const n of narrationNodes) { ); } check(narrationNodes.find((n) => n.pointId === 'C3-02')?.variantFrom === 'C1-02', 'C3-02 变体由 C1-02 驱动(variantFrom)'); +for (const n of narrationNodes) { + check( + FOLLOW_UP_DIMENSIONS.every((d) => typeof n.followUps?.[d] === 'string' && n.followUps[d]!.length > 0), + `${n.pointId}: 六维 followUps 追问模板齐全`, + ); +} // --- 4. Mock 模式端到端跑通三章 --- const store = new MemoryStore(); @@ -127,21 +140,27 @@ while (!engine.state.finished && guard++ < 200) { const profile = await engine.extractIdentity('我叫阿黎,29岁,在杭州做互联网产品经理,硕士学历'); await engine.confirmIdentity(profile); } else if (node.type === 'narration') { - // C2-05 走"沉默"路径验证合法选项,其余讲述 + // C2-05 走"沉默"路径验证合法选项,其余讲述;Mock 检测到缺失维度则回答追问 if ((node as NarrationNode).pointId === 'C2-05') await engine.skipNarration(); - else await engine.submitNarration('(验证用占位讲述)'); + else { + await engine.submitNarration('(验证用占位讲述)'); + if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); + } } else { engine.advance(); } } check(engine.state.finished, '三章流程跑通(Mock 模式)'); -const eventPoints = store.events.map((e) => e.pointId).sort(); +const eventPoints = store.events + .filter((e) => e.kind === 'choice' || e.kind === 'narration' || e.kind === 'profile') + .map((e) => e.pointId) + .sort(); check( JSON.stringify(eventPoints) === JSON.stringify(expectedIds), `事件轨迹覆盖全部 ${expectedIds.length} 个选择点`, ); -const c305 = store.events.find((e) => e.pointId === 'C3-02'); -const c102 = store.events.find((e) => e.pointId === 'C1-02'); +const c305 = store.events.find((e) => e.pointId === 'C3-02' && e.kind === 'narration'); +const c102 = store.events.find((e) => e.pointId === 'C1-02' && e.kind === 'narration'); check(!!c305?.variantId && c305.variantId === c102?.variantId, 'C3-02 幻境变体复用 C1-02 情绪解析结果'); // --- 5. 画像报告:7 维 score 问题 --- @@ -167,15 +186,114 @@ while (engine.currentNode().type !== 'choice') { await engine.confirmIdentity(await engine.extractIdentity('占位自我介绍')); } else if (n.type === 'narration') { await engine.submitNarration('(验证用占位讲述)'); + if (engine.state.pendingFollowUp) await engine.answerFollowUp('(验证用追问回答)'); } else { engine.advance(); } } const firstChoice = engine.currentNode() as ChoiceNode; await engine.choose(firstChoice.options[0].id); -check(store.events.length === before + 3, '重玩追加新事件(profile + narration + choice),旧事件不变'); +const added = store.events.slice(before); +check( + added.filter((e) => e.kind === 'profile').length === 1 && + added.filter((e) => e.kind === 'narration').length === 1 && + added.filter((e) => e.kind === 'choice').length === 1, + `重玩追加新事件(profile + narration + choice,含追问事件共 ${added.length} 条),旧事件不变`, +); check((await store.list(oldSession)).length === before, '旧轨迹事件完整保留'); +// --- 7. 追问补全流程(可控 noul 概率的 stub 驱动三条路径) --- +/** noul 概率可控的 Jev stub:missing 里的维度返回 0.1(缺失),其余 0.9;非 noul 题走 Mock */ +class FixedNoulJev extends MockJevClient { + constructor(private missing: string[]) { + super(); + } + override async ask(state: unknown, questions: Record): Promise> { + const out = await super.ask(state, questions); + for (const [id, q] of Object.entries(questions)) { + if (q.type === 'noul') { + out[id] = { type: 'noul', probability: this.missing.includes(id) ? 0.1 : 0.9, confidence: 0.9 }; + } + } + return out; + } +} + +/** 推进到指定节点(途中选择点取第一项、身份点直接确认、讲述点沉默略过) */ +async function walkTo(eng: StoryEngine, nodeId: string) { + let g = 0; + while (eng.currentNode().id !== nodeId && g++ < 60) { + const n = eng.currentNode(); + if (n.type === 'choice') await eng.choose((n as ChoiceNode).options[0].id); + else if (n.type === 'identity') await eng.confirmIdentity(await eng.extractIdentity('占位自我介绍')); + else if (n.type === 'narration') await eng.skipNarration(); + else eng.advance(); + } +} + +// 7a. 缺失检测 → 追问挂起 → 回答合并 +{ + const s = new MemoryStore(); + const eng = new StoryEngine(chapters, s, new FixedNoulJev(['visual'])); + await walkTo(eng, 'c1-02'); + const c102 = chapters[0].nodes.find((n) => n.id === 'c1-02') as NarrationNode; + const r = await eng.submitNarration('去年夏天我离开了家乡'); + const pending = eng.state.pendingFollowUp; + check(r === null && pending?.dimension === 'visual', '缺失检测:视觉缺失(最高优先级)→ 追问挂起,不立即落 narration 事件'); + check(pending?.prompt === c102.followUps.visual, '追问话术取自节点 JSON 的 followUps 模板'); + check(eng.state.nodeId === 'c1-02', '追问挂起期间节点不推进'); + let threw = false; + try { + await eng.submitNarration('重复提交'); + } catch { + threw = true; + } + check(threw, '追问挂起期间重复提交被拒(预算纪律:一个讲述点一次追问)'); + const res = await eng.answerFollowUp('站台是灰蓝色的'); + const narrationEv = s.events.find((e) => e.kind === 'narration'); + const followupEv = s.events.find((e) => e.kind === 'followup'); + check(!!followupEv && followupEv.followUpDimension === 'visual', '追问回答落 followup 事件(含缺失维度)'); + check( + !!narrationEv?.transcript?.includes('去年夏天我离开了家乡') && narrationEv.transcript.includes('站台是灰蓝色的'), + '追问回答合并进讲述文本(state 合并后进情绪签名选择)', + ); + check(!!res.variant && eng.state.pendingFollowUp === null && eng.state.nodeId === 'c1-d4', '追问收尾:变体解析完成并推进,无第二次追问'); +} + +// 7b. "记不清了"跳过 → followup_skipped 事件 +{ + const s = new MemoryStore(); + const eng = new StoryEngine(chapters, s, new FixedNoulJev(['person'])); + await walkTo(eng, 'c2-02'); + await eng.submitNarration('有件事我一直后悔'); + check(eng.state.pendingFollowUp?.dimension === 'person', '缺失检测:人物缺失 → 追问挂起'); + await eng.skipFollowUp(); + const skipped = s.events.find((e) => e.kind === 'followup_skipped'); + check(!!skipped && skipped.followUpDimension === 'person' && skipped.transcript === null, '跳过落 followup_skipped 事件(对什么沉默也是数据)'); + const narrationEv = s.events.find((e) => e.kind === 'narration' && e.pointId === 'C2-02'); + check(narrationEv?.transcript === '有件事我一直后悔', '跳过后讲述原文不变,流程继续'); +} + +// 7c. 六维俱全 → 不追问,直接走完 +{ + const s = new MemoryStore(); + const eng = new StoryEngine(chapters, s, new FixedNoulJev([])); + await walkTo(eng, 'c2-02'); + const r = await eng.submitNarration('一段细节齐全的讲述'); + check(r !== null && eng.state.pendingFollowUp === null, '无缺失维度时不触发追问'); + check(!s.events.some((e) => e.kind === 'followup' || e.kind === 'followup_skipped'), '无追问时不产生追问事件'); +} + +// --- 8. append-only:EventStore 实现不存在 update/delete 路径 --- +// (SQLite 实连只能在 Tauri 运行时内进行,这里只做接口层面断言) +for (const cls of [LocalStorageEventStore, SqliteEventStore, MemoryStore]) { + const props = Object.getOwnPropertyNames(cls.prototype).filter((p) => p !== 'constructor'); + check( + !props.some((p) => ['update', 'delete', 'remove', 'set'].includes(p)), + `${cls.name}: 只有 ${props.sort().join('/')}(append-only,无 update/delete)`, + ); +} + if (failures > 0) { console.error(`\n${failures} 项校验失败`); process.exit(1); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6e7eb47..743d9a9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -32,6 +32,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -74,8 +80,15 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-sql", ] +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + [[package]] name = "atk" version = "0.18.2" @@ -99,6 +112,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -129,6 +151,12 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -177,6 +205,30 @@ dependencies = [ "objc2", ] +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "brotli" version = "9.0.0" @@ -345,6 +397,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.45" @@ -367,6 +425,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.2" @@ -426,6 +490,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.2" @@ -444,6 +523,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -565,6 +653,17 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -602,7 +701,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -680,13 +781,19 @@ checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" dependencies = [ "bit-set", "cssparser", - "foldhash", + "foldhash 0.2.0", "html5ever", "precomputed-hash", "selectors", "tendril", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dpi" version = "0.1.2" @@ -723,6 +830,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + [[package]] name = "embed-resource" version = "3.0.11" @@ -770,6 +886,27 @@ dependencies = [ "typeid", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.5.0" @@ -821,12 +958,29 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -876,6 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -895,6 +1050,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.34" @@ -1049,6 +1215,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -1226,12 +1403,32 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -1250,6 +1447,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.39.0" @@ -1695,6 +1919,15 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" +dependencies = [ + "spin", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -1744,13 +1977,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ + "bitflags 2.13.2", "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] @@ -1785,6 +2038,16 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.3" @@ -1894,12 +2157,47 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.8", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1907,6 +2205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2173,6 +2472,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2191,11 +2496,20 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2261,12 +2575,39 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.10.1" @@ -2336,6 +2677,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2434,6 +2784,65 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2449,6 +2858,15 @@ dependencies = [ "bitflags 2.13.2", ] +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "redox_users" version = "0.5.3" @@ -2542,6 +2960,43 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust_decimal" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.8", + "rand 0.9.5", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2563,6 +3018,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2753,6 +3214,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.24.0" @@ -2817,6 +3290,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2834,6 +3318,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -2857,6 +3351,9 @@ name = "smallvec" version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -2883,7 +3380,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -2916,6 +3413,224 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.2", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.21", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.2", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.8", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.21", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.2", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.8", + "rust_decimal", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.21", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.21", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2946,12 +3661,29 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.8" @@ -3235,6 +3967,27 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-sql" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "811fca8f6b80ca3026496db4612d93000ce96915ac6e36f7adc73b8232aee9f0" +dependencies = [ + "futures-core", + "indexmap 2.14.2", + "log", + "rust_decimal", + "serde", + "serde_json", + "sqlx", + "tauri", + "tauri-plugin", + "thiserror 2.0.21", + "time", + "tokio", + "uuid", +] + [[package]] name = "tauri-runtime" version = "2.12.1" @@ -3446,6 +4199,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.19" @@ -3606,10 +4370,23 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3659,12 +4436,33 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -3714,6 +4512,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -3780,6 +4584,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -3789,6 +4599,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -3959,6 +4770,16 @@ dependencies = [ "windows-core", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4116,6 +4937,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4149,6 +4979,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4189,6 +5034,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4201,6 +5052,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4213,6 +5070,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4231,6 +5094,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4243,6 +5112,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4255,6 +5130,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4267,6 +5148,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4401,6 +5288,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -4422,6 +5329,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.5" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ef4f18a..4cc0ec7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,3 +23,4 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "2.12.1" } tauri-plugin-log = "2" +tauri-plugin-sql = { version = "2", features = ["sqlite"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index c135d7f..dd32767 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,7 @@ "main" ], "permissions": [ - "core:default" + "core:default", + "sql:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a650cf5..ab48c7b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_sql::Builder::default().build()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( diff --git a/src/App.tsx b/src/App.tsx index 9476266..c168ea5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import './App.css'; import { StoryEngine } from './engine/engine'; import { LocalStorageEventStore } from './engine/eventStore'; +import { SqliteEventStore, isTauriRuntime } from './engine/sqliteEventStore'; import type { Chapter } from './engine/types'; import { getJevClient } from './services/jev'; import { StoryPlayer } from './ui/StoryPlayer'; @@ -15,8 +16,9 @@ const chapters = [chapter1, chapter2, chapter3] as Chapter[]; export default function App() { const [screen, setScreen] = useState<'story' | 'report'>('story'); // 引擎与存储整个会话只建一次;重玩走 engine.replay()(新 sessionId,旧事件保留) + // 运行时选择:Tauri 原生端用 SQLite,浏览器 dev 用 localStorage(同一 append-only 接口) const { engine, store, jev } = useMemo(() => { - const store = new LocalStorageEventStore(); + const store = isTauriRuntime() ? new SqliteEventStore() : new LocalStorageEventStore(); const jev = getJevClient(); return { engine: new StoryEngine(chapters, store, jev), store, jev }; }, []); diff --git a/src/engine/engine.ts b/src/engine/engine.ts index f1833f8..d7405f6 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -1,4 +1,5 @@ -import type { Chapter, IdentityProfile, NarrationNode, SceneVariant, StoryNode } from './types'; +import type { Chapter, FollowUpDimension, IdentityProfile, NarrationNode, SceneVariant, StoryNode } from './types'; +import { FOLLOW_UP_DIMENSIONS, FOLLOW_UP_PRIORITY } from './types'; import type { EventStore, StoryEvent } from './eventStore'; import { makeEvent } from './eventStore'; import type { JevClient, JevQuestion } from '../services/jev'; @@ -9,6 +10,8 @@ export interface EngineState { nodeId: string; /** 当前场景变体(讲述点情绪签名解析结果,驱动 UI 场景配色) */ activeVariant: SceneVariant | null; + /** 追问补全:讲述后检测到缺失维度时挂起,等用户回答或"记不清了" */ + pendingFollowUp: PendingFollowUp | null; /** 全部三章完成后为 true → 进入画像报告 */ finished: boolean; } @@ -18,6 +21,31 @@ export interface NarrationResult { event: StoryEvent; } +/** 一次挂起的追问:原始讲述文本先存着,回答合并后才落 narration 事件 */ +export interface PendingFollowUp { + pointId: string; + dimension: FollowUpDimension; + /** 模板化追问话术(取自节点 JSON 的 followUps) */ + prompt: string; + /** 已问次数(本轮上限 1,字段预留第二次追问位置) */ + asked: number; + /** 用户最初的讲述文本,追问回答将合并进来 */ + baseTranscript: string; +} + +/** 六维缺失检测的 noul 问题文案(一次 Jev 调用并行问完) */ +const MISSING_DETECTION_INSTRUCTIONS: Record = { + visual: '这段讲述是否包含视觉细节(颜色、形状、光线、物件外观等)', + auditory: '这段讲述是否包含听觉细节(声音、语气、环境音等)', + olfactory_gustatory: '这段讲述是否包含嗅觉或味觉细节(气味、味道等)', + person: '这段讲述是否提及具体的人物', + time_place: '这段讲述是否提及具体的时间或地点', + emotion: '这段讲述是否有明确的情绪表达', +}; + +/** noul 概率低于此阈值视为该维度缺失 */ +const MISSING_THRESHOLD = 0.5; + function newSessionId(): string { return typeof crypto !== 'undefined' && 'randomUUID' in crypto ? crypto.randomUUID() @@ -49,6 +77,7 @@ export class StoryEngine { chapterId: start.id, nodeId: start.startNodeId, activeVariant: null, + pendingFollowUp: null, finished: false, }; } @@ -104,12 +133,96 @@ export class StoryEngine { } /** - * V 类讲述:落事件(transcript 为 ASR 占位,沉默为 null), - * 并解析情绪签名 → 场景变体(ADR-0003 通道 1,只做选择)。 + * V 类讲述提交。追问补全流程(CONTEXT「追问补全」): + * 文本讲述 → Jev 六维缺失检测 → 有缺口则挂起追问(返回 null, + * state.pendingFollowUp 挂起,UI 展示追问话术)→ answerFollowUp/skipFollowUp 收尾; + * 无缺口或沉默则直接走完:落 narration 事件 + 情绪签名→场景变体(ADR-0003 通道 1)。 */ - async submitNarration(transcript: string | null): Promise { + async submitNarration(transcript: string | null): Promise { const node = this.currentNode(); if (node.type !== 'narration') throw new Error('当前节点不是讲述点'); + if (this.state.pendingFollowUp) throw new Error('有未处理的追问,请先 answerFollowUp / skipFollowUp'); + if (transcript === null) return this.finalizeNarration(node, null); + const missing = await this.detectMissing(node, transcript); + if (missing) { + this.state.pendingFollowUp = { + pointId: node.pointId, + dimension: missing, + prompt: node.followUps[missing]!, + asked: 1, + baseTranscript: transcript, + }; + return null; + } + return this.finalizeNarration(node, transcript); + } + + /** 追问被回答:落 followup 事件,回答合并进讲述文本,再走情绪签名选择 */ + async answerFollowUp(answer: string): Promise { + const { node, pending } = this.takePendingFollowUp(); + await this.store.append( + makeEvent({ + sessionId: this.state.sessionId, + chapterId: this.state.chapterId, + kind: 'followup', + pointId: node.pointId, + dimensions: node.dimensions, + followUpDimension: pending.dimension, + transcript: answer, + }), + ); + this.state.pendingFollowUp = null; + return this.finalizeNarration(node, `${pending.baseTranscript}\n${answer}`); + } + + /** "记不清了"是合法回答(ADR-0004)——跳过也落事件,对什么沉默是数据 */ + async skipFollowUp(): Promise { + const { node, pending } = this.takePendingFollowUp(); + await this.store.append( + makeEvent({ + sessionId: this.state.sessionId, + chapterId: this.state.chapterId, + kind: 'followup_skipped', + pointId: node.pointId, + dimensions: node.dimensions, + followUpDimension: pending.dimension, + transcript: null, + }), + ); + this.state.pendingFollowUp = null; + return this.finalizeNarration(node, pending.baseTranscript); + } + + private takePendingFollowUp(): { node: NarrationNode; pending: PendingFollowUp } { + const pending = this.state.pendingFollowUp; + if (!pending) throw new Error('当前没有待处理的追问'); + const node = this.currentNode(); + if (node.type !== 'narration' || node.pointId !== pending.pointId) { + throw new Error('追问挂起状态与当前节点不一致'); + } + return { node, pending }; + } + + /** + * 缺失检测:一次 Jev 调用并行问 6 个 noul 问题;概率 <0.5 视为缺失; + * 按幻境渲染价值优先级取一个(FOLLOW_UP_PRIORITY),且节点 JSON 里要有对应模板。 + * 预算纪律:每个讲述点最多追问 1 次(maxQuestions ≥2 的第二轮留给后续迭代)。 + */ + private async detectMissing(node: NarrationNode, transcript: string): Promise { + if (node.followUp.maxQuestions < 1) return null; + const questions: Record = Object.fromEntries( + FOLLOW_UP_DIMENSIONS.map((d) => [d, { type: 'noul', instructions: MISSING_DETECTION_INSTRUCTIONS[d] }]), + ); + const answers = await this.jev.ask({ transcript, pointId: node.pointId }, questions); + for (const dim of FOLLOW_UP_PRIORITY) { + const a = answers[dim]; + if (node.followUps[dim] && a?.type === 'noul' && a.probability < MISSING_THRESHOLD) return dim; + } + return null; + } + + /** 讲述收尾:落 narration 事件、解析情绪签名 → 场景变体、推进节点 */ + private async finalizeNarration(node: NarrationNode, transcript: string | null): Promise { const variant = await this.resolveVariant(node, transcript); const event = makeEvent({ sessionId: this.state.sessionId, @@ -127,9 +240,11 @@ export class StoryEngine { return { variant, event }; } - /** 沉默永远是合法选项(CONTEXT:在什么面前沉默本身也是数据) */ + /** 沉默永远是合法选项(CONTEXT:在什么面前沉默本身也是数据);沉默路径不触发追问 */ async skipNarration(): Promise { - return this.submitNarration(null); + const r = await this.submitNarration(null); + if (!r) throw new Error('unreachable: 沉默路径不触发追问'); + return r; } private async resolveVariant(node: NarrationNode, transcript: string | null): Promise { @@ -268,6 +383,7 @@ export class StoryEngine { this.state.chapterId = chapter.id; this.state.nodeId = chapter.startNodeId; this.state.activeVariant = null; + this.state.pendingFollowUp = null; this.state.finished = false; } } diff --git a/src/engine/eventStore.ts b/src/engine/eventStore.ts index 576344b..4630c41 100644 --- a/src/engine/eventStore.ts +++ b/src/engine/eventStore.ts @@ -1,4 +1,4 @@ -import type { DimensionTag, IdentityProfile } from './types'; +import type { DimensionTag, FollowUpDimension, IdentityProfile } from './types'; /** * 选择/讲述事件存储(ADR-0004:历史不可涂改)。 @@ -17,7 +17,7 @@ export interface StoryEvent { sessionId: string; ts: number; chapterId: string; - kind: 'choice' | 'narration' | 'profile'; + kind: 'choice' | 'narration' | 'profile' | 'followup' | 'followup_skipped'; /** 选择点 ID,如 C1-03 */ pointId: string; dimensions: DimensionTag[]; @@ -27,7 +27,9 @@ export interface StoryEvent { /** narration 事件:选中的场景变体(情绪签名 → variantId) */ variantId?: string; emotionSignature?: string; - /** 讲述转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */ + /** followup / followup_skipped 事件:追问针对的缺失维度 */ + followUpDimension?: FollowUpDimension; + /** 讲述/追问的转写文本(Web Speech API 只给文本,不留音频,符合 ADR-0004 即焚原则;沉默为 null) */ transcript?: string | null; /** profile 事件:用户确认后的身份登记字段(匹配硬过滤字段) */ profile?: IdentityProfile; @@ -66,7 +68,7 @@ export class LocalStorageEventStore implements EventStore { } /** - * Tauri 原生端实现预留:后续用 tauri-plugin-sql(SQLite)实现同一接口, - * 表结构即 StoryEvent 一行一条,只写 INSERT/SELECT,不建 UPDATE/DELETE 路径。 - * 见 README「已知缺口」。 + * Tauri 原生端实现:见 ./sqliteEventStore.ts(tauri-plugin-sql + SQLite, + * 同一 EventStore 接口,SQL 层同样只写 INSERT/SELECT)。 + * 运行时选择在 App.tsx:window.__TAURI__ 存在用 SqliteEventStore,否则 localStorage。 */ diff --git a/src/engine/sqliteEventStore.ts b/src/engine/sqliteEventStore.ts new file mode 100644 index 0000000..38f9e0d --- /dev/null +++ b/src/engine/sqliteEventStore.ts @@ -0,0 +1,55 @@ +import type { EventStore, StoryEvent } from './eventStore'; +import type Database from '@tauri-apps/plugin-sql'; + +/** Tauri 原生端运行时判定(浏览器 dev 无 window.__TAURI__) */ +export function isTauriRuntime(): boolean { + return typeof window !== 'undefined' && '__TAURI__' in window; +} + +const DB_PATH = 'sqlite:events.db'; + +/** + * Tauri 原生端事件存储:tauri-plugin-sql(sqlx + SQLite)。 + * 与 LocalStorageEventStore 实现同一 EventStore 接口——ADR-0004 的 + * append-only 约束在 SQL 层同样成立:本类只写 INSERT/SELECT, + * 不存在 UPDATE/DELETE 路径。重玩 = 新 sessionId 的新行,旧行原样保留。 + * payload 为 StoryEvent 整行 JSON(事件 schema 演进不动表结构)。 + */ +export class SqliteEventStore implements EventStore { + private db: Promise; + + constructor(path: string = DB_PATH) { + // 动态 import:浏览器 dev 永不加载本类,避免把插件打进 web 路径的初始化 + this.db = (async () => { + const { default: Database } = await import('@tauri-apps/plugin-sql'); + const db = await Database.load(path); + await db.execute(` + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sessionId TEXT NOT NULL, + chapterId TEXT NOT NULL, + pointId TEXT NOT NULL, + payload TEXT NOT NULL, + createdAt INTEGER NOT NULL + ) + `); + return db; + })(); + } + + async append(event: StoryEvent): Promise { + const db = await this.db; + await db.execute( + 'INSERT INTO events (sessionId, chapterId, pointId, payload, createdAt) VALUES ($1, $2, $3, $4, $5)', + [event.sessionId, event.chapterId, event.pointId, JSON.stringify(event), event.ts], + ); + } + + async list(sessionId?: string): Promise { + const db = await this.db; + const rows = sessionId + ? await db.select<{ payload: string }[]>('SELECT payload FROM events WHERE sessionId = $1 ORDER BY id', [sessionId]) + : await db.select<{ payload: string }[]>('SELECT payload FROM events ORDER BY id'); + return rows.map((r) => JSON.parse(r.payload) as StoryEvent); + } +} diff --git a/src/engine/types.ts b/src/engine/types.ts index b3ecb03..9a8e0e3 100644 --- a/src/engine/types.ts +++ b/src/engine/types.ts @@ -61,7 +61,28 @@ export interface SceneVariant { tint: string; } -/** 语音讲述点(V 类):提示语 + 追问策略占位 + 情绪签名→变体选择 */ +/** 追问缺失检测的六个维度(五感拆三 + 人物 + 时空 + 情绪) */ +export const FOLLOW_UP_DIMENSIONS = [ + 'visual', // 视觉细节 + 'auditory', // 听觉细节 + 'olfactory_gustatory', // 嗅觉或味觉细节 + 'person', // 具体人物 + 'time_place', // 具体时间地点 + 'emotion', // 明确情绪表达 +] as const; +export type FollowUpDimension = (typeof FOLLOW_UP_DIMENSIONS)[number]; + +/** 追问选择优先级(对幻境渲染价值排序):视觉 > 人物 > 时空 > 情绪 > 听觉 > 嗅觉味觉 */ +export const FOLLOW_UP_PRIORITY: FollowUpDimension[] = [ + 'visual', + 'person', + 'time_place', + 'emotion', + 'auditory', + 'olfactory_gustatory', +]; + +/** 语音讲述点(V 类):提示语 + 追问补全 + 情绪签名→变体选择 */ export interface NarrationNode extends BaseNode { type: 'narration'; pointId: string; @@ -70,8 +91,10 @@ export interface NarrationNode extends BaseNode { /** 讲述提示语 */ prompt: string; dimensions: DimensionTag[]; - /** 追问策略占位(Jev 缺失维度检测 + 馆长口吻追问,至多 1-2 句,MVP 未接) */ + /** 追问策略:Jev 六维缺失检测 + NPC 口吻模板追问,至多 1-2 句(工程实现先 1 句) */ followUp: { maxQuestions: number; strategy: string }; + /** 模板化追问话术,按缺失维度取一条(LLM 口吻化生成后续再接) */ + followUps: Partial>; /** 预写场景变体表 */ variants: SceneVariant[]; /** diff --git a/src/ui/StoryPlayer.tsx b/src/ui/StoryPlayer.tsx index 3a411fc..2aa67ae 100644 --- a/src/ui/StoryPlayer.tsx +++ b/src/ui/StoryPlayer.tsx @@ -1,8 +1,18 @@ import { useState } from 'react'; -import type { StoryEngine } from '../engine/engine'; -import type { ChoiceNode, IdentityNode, IdentityProfile, NarrationNode } from '../engine/types'; +import type { StoryEngine, PendingFollowUp } from '../engine/engine'; +import type { ChoiceNode, FollowUpDimension, IdentityNode, IdentityProfile, NarrationNode } from '../engine/types'; import { VoiceInput } from './VoiceInput'; +/** 追问维度展示名 */ +const FOLLOW_UP_LABELS: Record = { + visual: '画面', + auditory: '声音', + olfactory_gustatory: '气味与味道', + person: '人物', + time_place: '时间与地点', + emotion: '情绪', +}; + /** speaker/sprite → 立绘占位色(无图片素材,CSS 色块) */ const SPRITE_COLORS: Record = { curator: '#6a5a8a', @@ -61,8 +71,11 @@ export function StoryPlayer({ engine, onFinish }: Props) { {node.type === 'narration' && ( { await engine.submitNarration(text); rerender(); }} onSkip={async () => { await engine.skipNarration(); rerender(); }} + onFollowUpAnswer={async (text) => { await engine.answerFollowUp(text); rerender(); }} + onFollowUpSkip={async () => { await engine.skipFollowUp(); rerender(); }} /> )} @@ -122,14 +135,39 @@ function ChoiceView({ node, onPick }: { node: ChoiceNode; onPick: (id: string) = function NarrationView({ node, + pendingFollowUp, onSubmit, onSkip, + onFollowUpAnswer, + onFollowUpSkip, }: { node: NarrationNode; + pendingFollowUp: PendingFollowUp | null; onSubmit: (text: string) => void; onSkip: () => void; + onFollowUpAnswer: (text: string) => void; + onFollowUpSkip: () => void; }) { const [busy, setBusy] = useState(false); + + // 追问补全阶段:讲述已收到,NPC 就缺失的细节追问一句(至多 1 句;"记不清了"合法) + if (pendingFollowUp) { + return ( +
+
{node.mechanism} · 追问({FOLLOW_UP_LABELS[pendingFollowUp.dimension]})
+

{pendingFollowUp.prompt}

+ { setBusy(true); await onFollowUpAnswer(t); setBusy(false); }} + /> + +

追问至多 {node.followUp.maxQuestions} 句;想不起来可以直说,也是合法回答。

+
+ ); + } + return (
{node.mechanism} · 语音讲述点
@@ -138,7 +176,7 @@ function NarrationView({ -

讲述后档案馆将就缺失的细节追问至多 {node.followUp.maxQuestions} 句(追问未接,占位)

+

讲述后档案馆可能就缺失的细节追问至多 {node.followUp.maxQuestions} 句。

); } diff --git a/src/ui/report/ReportPage.tsx b/src/ui/report/ReportPage.tsx index 0e89265..f844433 100644 --- a/src/ui/report/ReportPage.tsx +++ b/src/ui/report/ReportPage.tsx @@ -26,6 +26,12 @@ function buildState(events: StoryEvent[]): string { if (e.kind === 'choice') { return `[${e.pointId}] 选择「${e.optionLabel}」(维度: ${e.dimensions.join('/') || '字段采集'})`; } + if (e.kind === 'followup') { + return `[${e.pointId}] 追问(${e.followUpDimension})回答: ${e.transcript}`; + } + if (e.kind === 'followup_skipped') { + return `[${e.pointId}] 追问(${e.followUpDimension})回答: 记不清了(跳过)`; + } const t = e.transcript === null ? '(沉默)' : `(讲述: ${e.transcript})`; return `[${e.pointId}] 讲述${t} 情绪签名: ${e.emotionSignature ?? '未知'}`; });