@firetable/project-xiaochun 0.1.12

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.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * protocol.ts — Project XiaoChun `<iframe>` 嵌入协议 (常量 + 类型, 零运行时依赖)。
3
+ *
4
+ * 单一事实源: SDK (本包) 与 `/embed` 页面 (主仓库 src/embed/) 共用这一份。
5
+ * 所有消息名统一 `xc.` 前缀; 信封 `{ type, v, id?, payload? }`。
6
+ *
7
+ * 通道:
8
+ * 1. 握手: iframe → 宿主 `xc.ready` (window.postMessage, targetOrigin = 宿主 origin)
9
+ * 宿主 → iframe `xc.init` (window.postMessage, targetOrigin = iframe origin, 转移 MessagePort)
10
+ * 2. 握手后全部走 MessageChannel 端口 (端口本身即凭证, 不再依赖 `*`)
11
+ */
12
+ /** 协议版本; 不兼容变更时 +1。 */
13
+ export declare const XC_PROTOCOL_VERSION: 1;
14
+ /** 官方部署的 origin / embed 入口。 */
15
+ export declare const XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
16
+ export declare const XC_DEFAULT_SRC = "https://xiaochun.firetable.tech/embed";
17
+ /** 宿主 → iframe 命令。 */
18
+ export declare const XC_HOST_TO_FRAME: readonly ["xc.init", "xc.say", "xc.audio", "xc.audio.chunk", "xc.audio.end", "xc.motion", "xc.expression", "xc.lookAt", "xc.pointer", "xc.setModel", "xc.setConfig", "xc.mic", "xc.pause", "xc.resume", "xc.destroy"];
19
+ /** iframe → 宿主 事件。 */
20
+ export declare const XC_FRAME_TO_HOST: readonly ["xc.ready", "xc.load.progress", "xc.loaded", "xc.state", "xc.stt", "xc.utterance", "xc.hit-region", "xc.error"];
21
+ export type XcHostMessageType = (typeof XC_HOST_TO_FRAME)[number];
22
+ export type XcFrameMessageType = (typeof XC_FRAME_TO_HOST)[number];
23
+ /** 当前 /embed 已实现的命令 (xc.init 是握手, 不计入)。 */
24
+ export declare const XC_IMPLEMENTED_COMMANDS: readonly ["xc.say", "xc.audio", "xc.audio.chunk", "xc.audio.end", "xc.motion", "xc.expression", "xc.pointer", "xc.setModel", "xc.setConfig", "xc.mic", "xc.pause", "xc.resume", "xc.destroy"];
25
+ /**
26
+ * 暂不支持 (底层无对应能力), /embed 回 `xc.error{code:'unsupported'}`。
27
+ * TODO(xc.lookAt): 视线目前由相机 + 随机扫视驱动 (GazeController), 没有"外部指定注视点"的入口;
28
+ * 需要先在 gaze 层加 overrideTarget 再开放。
29
+ */
30
+ export declare const XC_UNSUPPORTED_COMMANDS: readonly ["xc.lookAt"];
31
+ /**
32
+ * xc.* ↔ xiaochun:// 对照表 (同一语义的两个传输层, 主应用内走同一个 handler: src/core/protocol/handler.ts)。
33
+ * - `action`: 内部 protocol action 对象 (src/core/protocol/types.ts); null = 没有 deep link 对应物。
34
+ * - `url`: 对应的 deep link 形式 (OS 级, 只在 Tauri 壳生效; iframe 不会响应 xiaochun://)。
35
+ */
36
+ export declare const XC_PROTOCOL_MAPPING: readonly [{
37
+ readonly xc: "xc.say";
38
+ readonly note: "mode:\"speak\" (默认)";
39
+ readonly action: "speak";
40
+ readonly url: "xiaochun://speak?text=…";
41
+ }, {
42
+ readonly xc: "xc.say";
43
+ readonly note: "mode:\"chat\" (走 LLM)";
44
+ readonly action: null;
45
+ readonly url: null;
46
+ }, {
47
+ readonly xc: "xc.audio";
48
+ readonly note: "source 为 URL 字符串";
49
+ readonly action: "speak";
50
+ readonly url: "xiaochun://speak?audioUrl=…[&text=…]";
51
+ }, {
52
+ readonly xc: "xc.audio";
53
+ readonly note: "source 为 ArrayBuffer / Blob";
54
+ readonly action: "audio";
55
+ readonly url: null;
56
+ }, {
57
+ readonly xc: "xc.audio.chunk / xc.audio.end";
58
+ readonly note: "流式 PCM";
59
+ readonly action: "audio";
60
+ readonly url: null;
61
+ }];
62
+ export type XcErrorCode = 'unsupported' | 'bad_request' | 'not_ready' | 'origin_denied' | 'failed';
63
+ export type XcLang = 'zh-CN' | 'en' | 'ja';
64
+ export type XcPhase = 'idle' | 'loading' | 'thinking' | 'speaking' | 'listening' | 'paused';
65
+ /** lazy = 不预热 WebLLM / EMAGE, 首次互动再加载 (默认); eager = 立即预热。 */
66
+ export type XcHeavyMode = 'lazy' | 'eager';
67
+ export interface XcEnvelope<T extends string = string, P = unknown> {
68
+ type: T;
69
+ v: typeof XC_PROTOCOL_VERSION;
70
+ /** 宿主命令可带 id; 对应的 xc.error / xc.utterance 会回带同一个 id。 */
71
+ id?: string;
72
+ payload: P;
73
+ }
74
+ export interface XcConfig {
75
+ lang?: XcLang;
76
+ /** 背景透明 (叠在宿主页面上)。 */
77
+ transparent?: boolean;
78
+ /** 是否显示 iframe 内置 UI (ChatBar)。默认 false。 */
79
+ ui?: boolean;
80
+ /** 重资源加载策略, 见 XcHeavyMode。 */
81
+ heavy?: XcHeavyMode;
82
+ }
83
+ export interface XcInitPayload {
84
+ /** 宿主声明的自身 origin (iframe 会与 event.origin 交叉校验)。 */
85
+ hostOrigin: string;
86
+ config?: XcConfig;
87
+ }
88
+ export interface XcSayPayload {
89
+ text: string;
90
+ /**
91
+ * 'speak' (默认): 直接 TTS + 动作, 不经过 LLM。
92
+ * 'chat': 当作用户输入走 WebLLM / 自定义 provider (会触发大模型加载)。
93
+ */
94
+ mode?: 'speak' | 'chat';
95
+ }
96
+ /** 宿主音频的编码: encoded = 容器格式 (mp3/wav/ogg/aac/webm… 由浏览器 decodeAudioData 决定); pcm16 / float32 = 无头原始 PCM (小端, 交错)。 */
97
+ export type XcAudioFormat = 'encoded' | 'pcm16' | 'float32';
98
+ export interface XcAudioOptions {
99
+ /** 气泡里显示的文字 (可选; 不影响音频与动作)。 */
100
+ text?: string;
101
+ /** 是否由 EMAGE 根据这段音频生成全身动作, 默认 true。false = 只播放音频 (此时完全不加载 EMAGE)。 */
102
+ motion?: boolean;
103
+ /** 是否驱动口型 (音量 RMS → 'aa'), 默认 true。 */
104
+ lipsync?: boolean;
105
+ }
106
+ /**
107
+ * xc.audio — 整段音频: iframe 内解码 → 16 kHz → EMAGE 窗口推理 + 同一段音频播放 + 口型, 播完回 xc.utterance{phase:'end'}。
108
+ * 信封 `id` 用于关联 xc.utterance / xc.error。大 ArrayBuffer 请放进 postMessage 的 transfer 列表 (SDK 默认这么做)。
109
+ */
110
+ export interface XcAudioPayload extends XcAudioOptions {
111
+ /** ArrayBuffer / Blob (structured clone) 或 URL 字符串 (iframe 内 fetch, 仅 https 或同源, 需 CORS)。 */
112
+ source: ArrayBuffer | Blob | string;
113
+ /** MIME 提示, 如 'audio/mpeg'。仅作信息, 解码以浏览器嗅探为准。 */
114
+ mimeType?: string;
115
+ /** 默认 'encoded'。 */
116
+ format?: XcAudioFormat;
117
+ /** 仅原始 PCM 需要 (默认 16000); 容器格式忽略 (以文件内的采样率为准)。范围 8000~96000。 */
118
+ sampleRate?: number;
119
+ /** 原始 PCM 的声道数 (交错), 1 或 2, 默认 1; 内部下混为单声道。 */
120
+ channels?: 1 | 2;
121
+ }
122
+ /**
123
+ * xc.audio.chunk — 流式音频分块 (原始 PCM)。信封 `id` = 流 id: 同一个 id 的第一个 chunk 开启一次说话 (回 xc.utterance start),
124
+ * 之后的 chunk 追加; 以 xc.audio.end 收尾。每个 chunk 的 `data` 应放进 transfer 列表。
125
+ * 选项 (text/motion/lipsync) 只在第一个 chunk 里生效; sampleRate/format 整条流必须一致。
126
+ */
127
+ export interface XcAudioChunkPayload extends XcAudioOptions {
128
+ data: ArrayBuffer;
129
+ format: Exclude<XcAudioFormat, 'encoded'>;
130
+ /** 8000~96000。 */
131
+ sampleRate: number;
132
+ channels?: 1 | 2;
133
+ }
134
+ export interface XcAudioEndPayload {
135
+ /** true = 立即打断 (丢弃未播放的音频); 默认 false = 播完已收到的音频后结束。 */
136
+ abort?: boolean;
137
+ }
138
+ export interface XcMotionPayload {
139
+ /** .vrma URL (https:// 或同源路径)。与 stop 二选一。 */
140
+ url?: string;
141
+ /** 内置动作名, 目前仅 'thinking'。 */
142
+ name?: 'thinking';
143
+ stop?: boolean;
144
+ loop?: boolean;
145
+ /** 淡入淡出秒数, 范围 0.26~3, 调大更柔和但响应更慢。 */
146
+ fadeDuration?: number;
147
+ /** 播放倍速, 范围 0.25~3。 */
148
+ timeScale?: number;
149
+ mask?: 'all' | 'upperBody';
150
+ }
151
+ export interface XcExpressionPayload {
152
+ name: 'neutral' | 'happy' | 'angry' | 'sad' | 'relaxed' | 'surprised';
153
+ }
154
+ /** xc.lookAt — TODO: 暂不支持, 收到会返回 unsupported。 */
155
+ export interface XcLookAtPayload {
156
+ /** 归一化 (-1..1), 相对 iframe 中心。 */
157
+ x: number;
158
+ y: number;
159
+ }
160
+ export interface XcPointerPayload {
161
+ /** iframe 内 client 坐标 (px)。SDK 会把宿主 pointer 事件换算好再发。 */
162
+ x: number;
163
+ y: number;
164
+ }
165
+ export interface XcSetModelPayload {
166
+ /** 完整 .vrm / .vrmaddon / .vrmbase URL (需 CORS)。 */
167
+ url?: string;
168
+ /** 内置服装 key, 见主仓库 APP_CONFIG.model.addons, 如 'xiaochun_maid'。 */
169
+ outfit?: string;
170
+ name?: string;
171
+ }
172
+ export interface XcMicPayload {
173
+ enabled: boolean;
174
+ }
175
+ export interface XcReadyPayload {
176
+ version: string;
177
+ protocol: typeof XC_PROTOCOL_VERSION;
178
+ capabilities: {
179
+ commands: string[];
180
+ unsupported: string[];
181
+ stt: boolean;
182
+ transparent: boolean;
183
+ /** 宿主音频能力 (xc.audio / xc.audio.chunk)。旧版 /embed 没有这个字段。 */
184
+ audio?: {
185
+ formats: XcAudioFormat[];
186
+ streaming: boolean;
187
+ maxSeconds: number;
188
+ };
189
+ };
190
+ }
191
+ export interface XcLoadProgressPayload {
192
+ phase: 'model';
193
+ /** 0~100 */
194
+ progress: number;
195
+ }
196
+ export interface XcLoadedPayload {
197
+ model: string;
198
+ }
199
+ export interface XcStatePayload {
200
+ phase: XcPhase;
201
+ paused: boolean;
202
+ heavy: XcHeavyMode;
203
+ }
204
+ export type XcSttPayload = {
205
+ kind: 'state';
206
+ state: 'idle' | 'loading' | 'listening' | 'recognizing' | 'error';
207
+ } | {
208
+ kind: 'progress';
209
+ percent: number;
210
+ } | {
211
+ kind: 'text';
212
+ text: string;
213
+ };
214
+ export interface XcUtterancePayload {
215
+ phase: 'start' | 'end';
216
+ text: string;
217
+ /** 'text' = xc.say (TTS); 'audio' = xc.audio / xc.audio.chunk (宿主音频, 没有走 TTS)。旧版 /embed 没有这个字段。 */
218
+ kind?: 'text' | 'audio';
219
+ }
220
+ export interface XcHitRegionPayload {
221
+ hit: boolean;
222
+ x: number;
223
+ y: number;
224
+ }
225
+ export interface XcErrorPayload {
226
+ code: XcErrorCode;
227
+ message: string;
228
+ /** 触发错误的命令名 (如 'xc.lookAt')。 */
229
+ command?: string;
230
+ }
231
+ export interface XcHostPayloadMap {
232
+ 'xc.init': XcInitPayload;
233
+ 'xc.say': XcSayPayload;
234
+ 'xc.audio': XcAudioPayload;
235
+ 'xc.audio.chunk': XcAudioChunkPayload;
236
+ 'xc.audio.end': XcAudioEndPayload;
237
+ 'xc.motion': XcMotionPayload;
238
+ 'xc.expression': XcExpressionPayload;
239
+ 'xc.lookAt': XcLookAtPayload;
240
+ 'xc.pointer': XcPointerPayload;
241
+ 'xc.setModel': XcSetModelPayload;
242
+ 'xc.setConfig': XcConfig;
243
+ 'xc.mic': XcMicPayload;
244
+ 'xc.pause': undefined;
245
+ 'xc.resume': undefined;
246
+ 'xc.destroy': undefined;
247
+ }
248
+ export interface XcFramePayloadMap {
249
+ 'xc.ready': XcReadyPayload;
250
+ 'xc.load.progress': XcLoadProgressPayload;
251
+ 'xc.loaded': XcLoadedPayload;
252
+ 'xc.state': XcStatePayload;
253
+ 'xc.stt': XcSttPayload;
254
+ 'xc.utterance': XcUtterancePayload;
255
+ 'xc.hit-region': XcHitRegionPayload;
256
+ 'xc.error': XcErrorPayload;
257
+ }
258
+ export type XcHostMessage = {
259
+ [K in XcHostMessageType]: XcEnvelope<K, XcHostPayloadMap[K]>;
260
+ }[XcHostMessageType];
261
+ export type XcFrameMessage = {
262
+ [K in XcFrameMessageType]: XcEnvelope<K, XcFramePayloadMap[K]>;
263
+ }[XcFrameMessageType];
264
+ export declare function xcMessage<K extends XcHostMessageType | XcFrameMessageType>(type: K, payload?: unknown, id?: string): XcEnvelope<K, any>;
265
+ /** 是否为合法 xc 信封 (只检查结构, 不检查 payload)。 */
266
+ export declare function isXcEnvelope(data: unknown): data is XcEnvelope;
267
+ /**
268
+ * 把用户给的 origin 规整成严格的 `scheme://host[:port]`。
269
+ * 拒绝 '*' / 通配 / 非 http(s) / 带路径的字符串 → 返回 null。
270
+ */
271
+ export declare function normalizeOrigin(input: string | null | undefined): string | null;
@@ -0,0 +1,28 @@
1
+ /*! @firetable/project-xiaochun v0.1.12 | MIT | https://github.com/FireTable/project-xiaochun */
2
+ import {
3
+ XC_DEFAULT_ORIGIN,
4
+ XC_DEFAULT_SRC,
5
+ XC_FRAME_TO_HOST,
6
+ XC_HOST_TO_FRAME,
7
+ XC_IMPLEMENTED_COMMANDS,
8
+ XC_PROTOCOL_MAPPING,
9
+ XC_PROTOCOL_VERSION,
10
+ XC_UNSUPPORTED_COMMANDS,
11
+ isXcEnvelope,
12
+ normalizeOrigin,
13
+ xcMessage
14
+ } from "./chunks/chunk-4VETYSWF.js";
15
+ export {
16
+ XC_DEFAULT_ORIGIN,
17
+ XC_DEFAULT_SRC,
18
+ XC_FRAME_TO_HOST,
19
+ XC_HOST_TO_FRAME,
20
+ XC_IMPLEMENTED_COMMANDS,
21
+ XC_PROTOCOL_MAPPING,
22
+ XC_PROTOCOL_VERSION,
23
+ XC_UNSUPPORTED_COMMANDS,
24
+ isXcEnvelope,
25
+ normalizeOrigin,
26
+ xcMessage
27
+ };
28
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }