@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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/avatar-element.d.ts +33 -0
- package/dist/chunks/chunk-4VETYSWF.js +91 -0
- package/dist/chunks/chunk-4VETYSWF.js.map +7 -0
- package/dist/chunks/chunk-UAIDX54S.js +544 -0
- package/dist/chunks/chunk-UAIDX54S.js.map +7 -0
- package/dist/chunks/chunk-XKLOFX52.js +165 -0
- package/dist/chunks/chunk-XKLOFX52.js.map +7 -0
- package/dist/client.d.ts +140 -0
- package/dist/element.cjs +743 -0
- package/dist/element.cjs.map +7 -0
- package/dist/element.d.ts +2 -0
- package/dist/element.js +15 -0
- package/dist/element.js.map +7 -0
- package/dist/index.cjs +838 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +7 -0
- package/dist/loader.d.ts +1 -0
- package/dist/loader.global.js +3 -0
- package/dist/loader.global.js.map +7 -0
- package/dist/protocol-url.d.ts +24 -0
- package/dist/protocol.cjs +110 -0
- package/dist/protocol.cjs.map +7 -0
- package/dist/protocol.d.ts +271 -0
- package/dist/protocol.js +28 -0
- package/dist/protocol.js.map +7 -0
- package/dist/react.cjs +736 -0
- package/dist/react.cjs.map +7 -0
- package/dist/react.d.ts +78 -0
- package/dist/react.js +161 -0
- package/dist/react.js.map +7 -0
- package/package.json +97 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/*! @firetable/project-xiaochun v0.1.12 | MIT | https://github.com/FireTable/project-xiaochun */
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
XC_DEFAULT_ORIGIN: () => XC_DEFAULT_ORIGIN,
|
|
25
|
+
XC_DEFAULT_SRC: () => XC_DEFAULT_SRC,
|
|
26
|
+
XC_FRAME_TO_HOST: () => XC_FRAME_TO_HOST,
|
|
27
|
+
XC_HOST_TO_FRAME: () => XC_HOST_TO_FRAME,
|
|
28
|
+
XC_IMPLEMENTED_COMMANDS: () => XC_IMPLEMENTED_COMMANDS,
|
|
29
|
+
XC_PROTOCOL_MAPPING: () => XC_PROTOCOL_MAPPING,
|
|
30
|
+
XC_PROTOCOL_VERSION: () => XC_PROTOCOL_VERSION,
|
|
31
|
+
XC_UNSUPPORTED_COMMANDS: () => XC_UNSUPPORTED_COMMANDS,
|
|
32
|
+
XIAOCHUN_ELEMENT_TAG: () => XIAOCHUN_ELEMENT_TAG,
|
|
33
|
+
XIAOCHUN_PROTOCOL_SCHEME: () => XIAOCHUN_PROTOCOL_SCHEME,
|
|
34
|
+
createXiaochun: () => createXiaochun,
|
|
35
|
+
defineXiaochunElement: () => defineXiaochunElement,
|
|
36
|
+
isXcEnvelope: () => isXcEnvelope,
|
|
37
|
+
normalizeOrigin: () => normalizeOrigin,
|
|
38
|
+
parseProtocolUrl: () => parseProtocolUrl,
|
|
39
|
+
toProtocolUrl: () => toProtocolUrl,
|
|
40
|
+
xcMessage: () => xcMessage
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
|
+
|
|
44
|
+
// src/protocol.ts
|
|
45
|
+
var XC_PROTOCOL_VERSION = 1;
|
|
46
|
+
var XC_DEFAULT_ORIGIN = "https://xiaochun.firetable.tech";
|
|
47
|
+
var XC_DEFAULT_SRC = `${XC_DEFAULT_ORIGIN}/embed`;
|
|
48
|
+
var XC_HOST_TO_FRAME = [
|
|
49
|
+
"xc.init",
|
|
50
|
+
"xc.say",
|
|
51
|
+
"xc.audio",
|
|
52
|
+
"xc.audio.chunk",
|
|
53
|
+
"xc.audio.end",
|
|
54
|
+
"xc.motion",
|
|
55
|
+
"xc.expression",
|
|
56
|
+
"xc.lookAt",
|
|
57
|
+
"xc.pointer",
|
|
58
|
+
"xc.setModel",
|
|
59
|
+
"xc.setConfig",
|
|
60
|
+
"xc.mic",
|
|
61
|
+
"xc.pause",
|
|
62
|
+
"xc.resume",
|
|
63
|
+
"xc.destroy"
|
|
64
|
+
];
|
|
65
|
+
var XC_FRAME_TO_HOST = [
|
|
66
|
+
"xc.ready",
|
|
67
|
+
"xc.load.progress",
|
|
68
|
+
"xc.loaded",
|
|
69
|
+
"xc.state",
|
|
70
|
+
"xc.stt",
|
|
71
|
+
"xc.utterance",
|
|
72
|
+
"xc.hit-region",
|
|
73
|
+
"xc.error"
|
|
74
|
+
];
|
|
75
|
+
var XC_IMPLEMENTED_COMMANDS = [
|
|
76
|
+
"xc.say",
|
|
77
|
+
"xc.audio",
|
|
78
|
+
"xc.audio.chunk",
|
|
79
|
+
"xc.audio.end",
|
|
80
|
+
"xc.motion",
|
|
81
|
+
"xc.expression",
|
|
82
|
+
"xc.pointer",
|
|
83
|
+
"xc.setModel",
|
|
84
|
+
"xc.setConfig",
|
|
85
|
+
"xc.mic",
|
|
86
|
+
"xc.pause",
|
|
87
|
+
"xc.resume",
|
|
88
|
+
"xc.destroy"
|
|
89
|
+
];
|
|
90
|
+
var XC_UNSUPPORTED_COMMANDS = ["xc.lookAt"];
|
|
91
|
+
var XC_PROTOCOL_MAPPING = [
|
|
92
|
+
{ xc: "xc.say", note: 'mode:"speak" (\u9ED8\u8BA4)', action: "speak", url: "xiaochun://speak?text=\u2026" },
|
|
93
|
+
{ xc: "xc.say", note: 'mode:"chat" (\u8D70 LLM)', action: null, url: null },
|
|
94
|
+
{ xc: "xc.audio", note: "source \u4E3A URL \u5B57\u7B26\u4E32", action: "speak", url: "xiaochun://speak?audioUrl=\u2026[&text=\u2026]" },
|
|
95
|
+
{ xc: "xc.audio", note: "source \u4E3A ArrayBuffer / Blob", action: "audio", url: null },
|
|
96
|
+
{ xc: "xc.audio.chunk / xc.audio.end", note: "\u6D41\u5F0F PCM", action: "audio", url: null }
|
|
97
|
+
];
|
|
98
|
+
function xcMessage(type, payload, id) {
|
|
99
|
+
const m = { type, v: XC_PROTOCOL_VERSION, payload };
|
|
100
|
+
if (id !== void 0) m.id = id;
|
|
101
|
+
return m;
|
|
102
|
+
}
|
|
103
|
+
function isXcEnvelope(data) {
|
|
104
|
+
if (!data || typeof data !== "object") return false;
|
|
105
|
+
const d = data;
|
|
106
|
+
return typeof d.type === "string" && d.type.startsWith("xc.") && d.v === XC_PROTOCOL_VERSION;
|
|
107
|
+
}
|
|
108
|
+
function normalizeOrigin(input) {
|
|
109
|
+
if (!input || input === "*" || input === "null") return null;
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(input);
|
|
112
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return null;
|
|
113
|
+
return u.origin;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/client.ts
|
|
120
|
+
var FALLBACK_PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
|
121
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f6b8ae"/><stop offset="1" stop-color="#ea8377"/></linearGradient></defs><ellipse cx="60" cy="170" rx="34" ry="6" fill="#000" opacity=".12"/><circle cx="60" cy="48" r="26" fill="url(#g)" opacity=".75"/><path d="M26 160c2-40 18-62 34-62s32 22 34 62z" fill="url(#g)" opacity=".6"/></svg>'
|
|
122
|
+
);
|
|
123
|
+
var css = (v, d) => v === void 0 ? d : typeof v === "number" ? `${v}px` : v;
|
|
124
|
+
function createXiaochun(options) {
|
|
125
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
126
|
+
throw new Error("[project-xiaochun] createXiaochun must run in a browser");
|
|
127
|
+
}
|
|
128
|
+
const container = typeof options.container === "string" ? document.querySelector(options.container) : options.container;
|
|
129
|
+
if (!container) throw new Error(`[project-xiaochun] container not found: ${String(options.container)}`);
|
|
130
|
+
const src = options.src ?? XC_DEFAULT_SRC;
|
|
131
|
+
let srcUrl;
|
|
132
|
+
try {
|
|
133
|
+
srcUrl = new URL(src, window.location.href);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`[project-xiaochun] invalid src: ${src}`);
|
|
136
|
+
}
|
|
137
|
+
const frameOrigin = normalizeOrigin(options.origin ?? srcUrl.origin);
|
|
138
|
+
if (!frameOrigin) throw new Error("[project-xiaochun] invalid origin (wildcards are not allowed)");
|
|
139
|
+
const trusted = /* @__PURE__ */ new Set([frameOrigin]);
|
|
140
|
+
for (const o of options.allowedOrigins ?? []) {
|
|
141
|
+
const n = normalizeOrigin(o);
|
|
142
|
+
if (!n) throw new Error(`[project-xiaochun] invalid allowedOrigins entry: ${o} (wildcards are not allowed)`);
|
|
143
|
+
trusted.add(n);
|
|
144
|
+
}
|
|
145
|
+
const transparent = options.transparent ?? false;
|
|
146
|
+
const passthrough = options.passthrough ?? transparent;
|
|
147
|
+
const lazy = options.lazy ?? true;
|
|
148
|
+
const position = options.position ?? "inline";
|
|
149
|
+
const handshakeTimeout = options.handshakeTimeout ?? 2e4;
|
|
150
|
+
const wrapper = document.createElement("div");
|
|
151
|
+
wrapper.setAttribute("data-xiaochun", "");
|
|
152
|
+
const ws = wrapper.style;
|
|
153
|
+
ws.position = position === "inline" ? "relative" : "fixed";
|
|
154
|
+
ws.width = css(options.width, "320px");
|
|
155
|
+
ws.height = css(options.height, "480px");
|
|
156
|
+
ws.maxWidth = "100%";
|
|
157
|
+
ws.overflow = "visible";
|
|
158
|
+
ws.contain = "layout style";
|
|
159
|
+
ws.borderRadius = "var(--xc-radius, 0)";
|
|
160
|
+
ws.boxShadow = "var(--xc-shadow, none)";
|
|
161
|
+
if (!transparent) ws.background = "var(--xc-bg, transparent)";
|
|
162
|
+
if (position === "bottom-right") {
|
|
163
|
+
ws.right = "var(--xc-offset-x, 16px)";
|
|
164
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
165
|
+
}
|
|
166
|
+
if (position === "bottom-left") {
|
|
167
|
+
ws.left = "var(--xc-offset-x, 16px)";
|
|
168
|
+
ws.bottom = "var(--xc-offset-y, 16px)";
|
|
169
|
+
}
|
|
170
|
+
if (position !== "inline") ws.zIndex = `var(--xc-z-index, ${options.zIndex ?? 2147483e3})`;
|
|
171
|
+
ws.pointerEvents = "none";
|
|
172
|
+
wrapper.setAttribute("part", "wrapper");
|
|
173
|
+
container.appendChild(wrapper);
|
|
174
|
+
let placeholderEl = null;
|
|
175
|
+
if (options.placeholder !== false) {
|
|
176
|
+
if (options.placeholder instanceof HTMLElement) placeholderEl = options.placeholder;
|
|
177
|
+
else {
|
|
178
|
+
const img = document.createElement("img");
|
|
179
|
+
img.src = options.placeholder || FALLBACK_PLACEHOLDER;
|
|
180
|
+
img.alt = "Project XiaoChun";
|
|
181
|
+
img.decoding = "async";
|
|
182
|
+
img.width = 320;
|
|
183
|
+
img.height = 480;
|
|
184
|
+
placeholderEl = img;
|
|
185
|
+
}
|
|
186
|
+
const ps = placeholderEl.style;
|
|
187
|
+
placeholderEl.setAttribute("part", "placeholder");
|
|
188
|
+
ps.borderRadius = "var(--xc-radius, 0)";
|
|
189
|
+
ps.position = "absolute";
|
|
190
|
+
ps.inset = "0";
|
|
191
|
+
ps.width = "100%";
|
|
192
|
+
ps.height = "100%";
|
|
193
|
+
ps.objectFit = "contain";
|
|
194
|
+
ps.transition = "opacity .25s ease";
|
|
195
|
+
ps.pointerEvents = "auto";
|
|
196
|
+
ps.cursor = lazy === false ? "default" : "pointer";
|
|
197
|
+
placeholderEl.addEventListener("click", () => activate());
|
|
198
|
+
wrapper.appendChild(placeholderEl);
|
|
199
|
+
}
|
|
200
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
201
|
+
const emit = (ev, payload) => {
|
|
202
|
+
listeners.get(ev)?.forEach((cb) => {
|
|
203
|
+
try {
|
|
204
|
+
cb(payload);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error("[project-xiaochun] listener error", e);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
let resolveReady;
|
|
211
|
+
let rejectReady;
|
|
212
|
+
const ready = new Promise((res, rej) => {
|
|
213
|
+
resolveReady = res;
|
|
214
|
+
rejectReady = rej;
|
|
215
|
+
});
|
|
216
|
+
ready.catch(() => {
|
|
217
|
+
});
|
|
218
|
+
let iframe = null;
|
|
219
|
+
let port = null;
|
|
220
|
+
let destroyed = false;
|
|
221
|
+
let userPaused = false;
|
|
222
|
+
let autoPaused = false;
|
|
223
|
+
let loaded = false;
|
|
224
|
+
let lastHit = false;
|
|
225
|
+
const queue = [];
|
|
226
|
+
const pending = /* @__PURE__ */ new Map();
|
|
227
|
+
let idSeq = 0;
|
|
228
|
+
let handshakeTimer = null;
|
|
229
|
+
let io = null;
|
|
230
|
+
let idleHandle = null;
|
|
231
|
+
const postToFrame = (type, payload, id, transfer) => {
|
|
232
|
+
if (!port) {
|
|
233
|
+
queue.push({ type, payload, id, transfer });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
port.postMessage(xcMessage(type, payload, id), transfer ?? []);
|
|
237
|
+
};
|
|
238
|
+
const send = (type, payload, wait = false, transfer, fixedId) => {
|
|
239
|
+
if (destroyed) return Promise.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
240
|
+
if (!iframe) activate();
|
|
241
|
+
const id = fixedId ?? `c${++idSeq}`;
|
|
242
|
+
const p = wait ? new Promise((resolve, reject) => pending.set(id, { resolve, reject, sawEnd: false })) : Promise.resolve();
|
|
243
|
+
postToFrame(type, payload, id, transfer);
|
|
244
|
+
return p;
|
|
245
|
+
};
|
|
246
|
+
const toBuffer = (d) => d instanceof ArrayBuffer ? d : d.buffer.slice(d.byteOffset, d.byteOffset + d.byteLength);
|
|
247
|
+
const bindAbort = (signal, id) => {
|
|
248
|
+
if (!signal) return;
|
|
249
|
+
const abort = () => {
|
|
250
|
+
if (!destroyed) postToFrame("xc.audio.end", { abort: true }, id);
|
|
251
|
+
};
|
|
252
|
+
if (signal.aborted) abort();
|
|
253
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
254
|
+
};
|
|
255
|
+
const speakAudio = async (source, opts = {}) => {
|
|
256
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
257
|
+
if (!iframe) activate();
|
|
258
|
+
const { signal, transfer, fetch: fetchWhere, ...rest } = opts;
|
|
259
|
+
const payload = { ...rest };
|
|
260
|
+
const list = [];
|
|
261
|
+
if (typeof source === "string") {
|
|
262
|
+
if (fetchWhere === "frame") payload.source = source;
|
|
263
|
+
else {
|
|
264
|
+
const res = await fetch(source);
|
|
265
|
+
if (!res.ok) throw new Error(`[project-xiaochun] audio fetch failed: HTTP ${res.status}`);
|
|
266
|
+
const buf = await res.arrayBuffer();
|
|
267
|
+
payload.source = buf;
|
|
268
|
+
list.push(buf);
|
|
269
|
+
payload.mimeType ?? (payload.mimeType = res.headers.get("content-type") ?? void 0);
|
|
270
|
+
}
|
|
271
|
+
} else if (typeof Blob !== "undefined" && source instanceof Blob) {
|
|
272
|
+
payload.source = source;
|
|
273
|
+
payload.mimeType ?? (payload.mimeType = source.type || void 0);
|
|
274
|
+
} else {
|
|
275
|
+
const buf = toBuffer(source);
|
|
276
|
+
payload.source = buf;
|
|
277
|
+
if (!(source instanceof ArrayBuffer) || transfer !== false) list.push(buf);
|
|
278
|
+
}
|
|
279
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
280
|
+
const id = `c${++idSeq}`;
|
|
281
|
+
const done = send("xc.audio", payload, true, list, id);
|
|
282
|
+
bindAbort(signal, id);
|
|
283
|
+
return done;
|
|
284
|
+
};
|
|
285
|
+
const speakAudioStream = (opts) => {
|
|
286
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
287
|
+
const { signal, transfer, fetch: _f, mimeType: _m, ...rest } = opts;
|
|
288
|
+
const id = `c${++idSeq}`;
|
|
289
|
+
let started = false;
|
|
290
|
+
let ended = false;
|
|
291
|
+
let fmt = rest.format === "pcm16" || rest.format === "float32" ? rest.format : null;
|
|
292
|
+
let done = Promise.resolve();
|
|
293
|
+
const handle2 = {
|
|
294
|
+
write(data) {
|
|
295
|
+
if (ended) throw new Error("[project-xiaochun] audio stream already ended");
|
|
296
|
+
if (destroyed) throw new Error("[project-xiaochun] instance destroyed");
|
|
297
|
+
if (data instanceof Int16Array) fmt ?? (fmt = "pcm16");
|
|
298
|
+
else if (data instanceof Float32Array) fmt ?? (fmt = "float32");
|
|
299
|
+
if (!fmt) throw new Error("[project-xiaochun] speakAudioStream: set opts.format when writing raw ArrayBuffers");
|
|
300
|
+
const buf = toBuffer(data);
|
|
301
|
+
const body = { data: buf, format: fmt, sampleRate: rest.sampleRate, channels: rest.channels };
|
|
302
|
+
if (!started) {
|
|
303
|
+
started = true;
|
|
304
|
+
body.text = rest.text;
|
|
305
|
+
body.motion = rest.motion;
|
|
306
|
+
body.lipsync = rest.lipsync;
|
|
307
|
+
done = send("xc.audio.chunk", body, true, [buf], id);
|
|
308
|
+
bindAbort(signal, id);
|
|
309
|
+
} else {
|
|
310
|
+
postToFrame("xc.audio.chunk", body, id, !(data instanceof ArrayBuffer) || transfer !== false ? [buf] : []);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
end() {
|
|
314
|
+
if (ended) return;
|
|
315
|
+
ended = true;
|
|
316
|
+
if (started) postToFrame("xc.audio.end", {}, id);
|
|
317
|
+
},
|
|
318
|
+
abort() {
|
|
319
|
+
if (ended && !started) return;
|
|
320
|
+
ended = true;
|
|
321
|
+
if (started) postToFrame("xc.audio.end", { abort: true }, id);
|
|
322
|
+
},
|
|
323
|
+
get done() {
|
|
324
|
+
return done;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
return handle2;
|
|
328
|
+
};
|
|
329
|
+
function buildUrl() {
|
|
330
|
+
const u = new URL(srcUrl.href);
|
|
331
|
+
u.searchParams.set("host", window.location.origin);
|
|
332
|
+
if (transparent) u.searchParams.set("transparent", "1");
|
|
333
|
+
if (options.ui) u.searchParams.set("ui", "1");
|
|
334
|
+
if (options.lang) u.searchParams.set("lang", options.lang);
|
|
335
|
+
if (options.heavy) u.searchParams.set("heavy", options.heavy);
|
|
336
|
+
if (options.controls) u.searchParams.set("controls", "1");
|
|
337
|
+
if (options.model) u.searchParams.set("outfit", options.model);
|
|
338
|
+
return u.href;
|
|
339
|
+
}
|
|
340
|
+
function activate() {
|
|
341
|
+
if (destroyed || iframe) return;
|
|
342
|
+
cancelIdle();
|
|
343
|
+
io?.disconnect();
|
|
344
|
+
io = null;
|
|
345
|
+
const f = document.createElement("iframe");
|
|
346
|
+
f.title = "Project XiaoChun";
|
|
347
|
+
f.loading = "lazy";
|
|
348
|
+
f.allow = "microphone; autoplay";
|
|
349
|
+
f.referrerPolicy = "strict-origin-when-cross-origin";
|
|
350
|
+
if (options.sandbox !== false) {
|
|
351
|
+
f.setAttribute("sandbox", options.sandbox ?? "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox");
|
|
352
|
+
}
|
|
353
|
+
f.setAttribute("allowtransparency", "true");
|
|
354
|
+
f.setAttribute("scrolling", "no");
|
|
355
|
+
const s = f.style;
|
|
356
|
+
s.position = "absolute";
|
|
357
|
+
s.inset = "0";
|
|
358
|
+
s.width = "100%";
|
|
359
|
+
s.height = "100%";
|
|
360
|
+
s.border = "0";
|
|
361
|
+
s.background = "transparent";
|
|
362
|
+
s.colorScheme = "normal";
|
|
363
|
+
s.borderRadius = "var(--xc-radius, 0)";
|
|
364
|
+
f.setAttribute("part", "iframe");
|
|
365
|
+
s.opacity = "0";
|
|
366
|
+
s.transition = "opacity .25s ease";
|
|
367
|
+
s.pointerEvents = passthrough ? "none" : "auto";
|
|
368
|
+
f.src = buildUrl();
|
|
369
|
+
iframe = f;
|
|
370
|
+
wrapper.appendChild(f);
|
|
371
|
+
if (placeholderEl) placeholderEl.style.cursor = "default";
|
|
372
|
+
handshakeTimer = setTimeout(() => {
|
|
373
|
+
if (!port) emit("error", { code: "timeout", message: `no xc.ready from ${frameOrigin} within ${handshakeTimeout}ms` });
|
|
374
|
+
}, handshakeTimeout);
|
|
375
|
+
setupAutoPause();
|
|
376
|
+
setupPassthrough();
|
|
377
|
+
}
|
|
378
|
+
const onWindowMessage = (ev) => {
|
|
379
|
+
if (destroyed || !iframe || ev.source !== iframe.contentWindow) return;
|
|
380
|
+
if (!trusted.has(ev.origin)) {
|
|
381
|
+
emit("error", { code: "origin_denied", message: `ignored message from untrusted origin ${ev.origin}` });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const data = ev.data;
|
|
385
|
+
if (!isXcEnvelope(data) || data.type !== "xc.ready" || port) return;
|
|
386
|
+
const ch = new MessageChannel();
|
|
387
|
+
port = ch.port1;
|
|
388
|
+
port.onmessage = onPortMessage;
|
|
389
|
+
iframe.contentWindow.postMessage(
|
|
390
|
+
xcMessage("xc.init", { hostOrigin: window.location.origin }),
|
|
391
|
+
ev.origin,
|
|
392
|
+
[ch.port2]
|
|
393
|
+
);
|
|
394
|
+
if (handshakeTimer) {
|
|
395
|
+
clearTimeout(handshakeTimer);
|
|
396
|
+
handshakeTimer = null;
|
|
397
|
+
}
|
|
398
|
+
emit("handshake", data.payload);
|
|
399
|
+
for (const m of queue.splice(0)) port.postMessage(xcMessage(m.type, m.payload, m.id), m.transfer ?? []);
|
|
400
|
+
syncPause();
|
|
401
|
+
};
|
|
402
|
+
window.addEventListener("message", onWindowMessage);
|
|
403
|
+
function onPortMessage(ev) {
|
|
404
|
+
const data = ev.data;
|
|
405
|
+
if (!isXcEnvelope(data)) return;
|
|
406
|
+
switch (data.type) {
|
|
407
|
+
case "xc.load.progress":
|
|
408
|
+
emit("progress", data.payload);
|
|
409
|
+
break;
|
|
410
|
+
case "xc.loaded":
|
|
411
|
+
if (!loaded) {
|
|
412
|
+
loaded = true;
|
|
413
|
+
if (iframe) iframe.style.opacity = "1";
|
|
414
|
+
if (placeholderEl) {
|
|
415
|
+
const el = placeholderEl;
|
|
416
|
+
el.style.opacity = "0";
|
|
417
|
+
el.style.pointerEvents = "none";
|
|
418
|
+
setTimeout(() => el.remove(), 300);
|
|
419
|
+
placeholderEl = null;
|
|
420
|
+
}
|
|
421
|
+
resolveReady();
|
|
422
|
+
}
|
|
423
|
+
emit("ready", data.payload);
|
|
424
|
+
break;
|
|
425
|
+
case "xc.state":
|
|
426
|
+
emit("state", data.payload);
|
|
427
|
+
break;
|
|
428
|
+
case "xc.stt":
|
|
429
|
+
emit("stt", data.payload);
|
|
430
|
+
break;
|
|
431
|
+
case "xc.utterance": {
|
|
432
|
+
emit("utterance", data.payload);
|
|
433
|
+
const p = data.id ? pending.get(data.id) : void 0;
|
|
434
|
+
if (p && data.payload.phase === "end") {
|
|
435
|
+
pending.delete(data.id);
|
|
436
|
+
p.resolve();
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "xc.hit-region": {
|
|
441
|
+
const { hit } = data.payload;
|
|
442
|
+
emit("hit-region", data.payload);
|
|
443
|
+
if (passthrough && iframe && hit !== lastHit) {
|
|
444
|
+
lastHit = hit;
|
|
445
|
+
iframe.style.pointerEvents = hit ? "auto" : "none";
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "xc.error": {
|
|
450
|
+
const { code, message, command } = data.payload;
|
|
451
|
+
emit("error", { code, message, command });
|
|
452
|
+
const p = data.id ? pending.get(data.id) : void 0;
|
|
453
|
+
if (p && data.id) {
|
|
454
|
+
pending.delete(data.id);
|
|
455
|
+
p.reject(new Error(`[${code}] ${message}`));
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
default:
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
let pointerRaf = 0;
|
|
464
|
+
let lastPtr = null;
|
|
465
|
+
const onHostPointer = (e) => {
|
|
466
|
+
if (!iframe || !port || iframe.style.pointerEvents === "auto") return;
|
|
467
|
+
lastPtr = { x: e.clientX, y: e.clientY };
|
|
468
|
+
if (pointerRaf) return;
|
|
469
|
+
pointerRaf = requestAnimationFrame(() => {
|
|
470
|
+
pointerRaf = 0;
|
|
471
|
+
if (!iframe || !port || !lastPtr) return;
|
|
472
|
+
const r = iframe.getBoundingClientRect();
|
|
473
|
+
const x = lastPtr.x - r.left, y = lastPtr.y - r.top;
|
|
474
|
+
if (x < 0 || y < 0 || x > r.width || y > r.height) {
|
|
475
|
+
if (lastHit) {
|
|
476
|
+
lastHit = false;
|
|
477
|
+
iframe.style.pointerEvents = "none";
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
port.postMessage(xcMessage("xc.pointer", { x, y }));
|
|
482
|
+
});
|
|
483
|
+
};
|
|
484
|
+
function setupPassthrough() {
|
|
485
|
+
if (passthrough) window.addEventListener("pointermove", onHostPointer, { passive: true });
|
|
486
|
+
}
|
|
487
|
+
let pauseIo = null;
|
|
488
|
+
function setupAutoPause() {
|
|
489
|
+
if (options.autoPause === false || typeof IntersectionObserver === "undefined") return;
|
|
490
|
+
pauseIo = new IntersectionObserver((entries) => {
|
|
491
|
+
const visible = entries[entries.length - 1]?.isIntersecting ?? true;
|
|
492
|
+
autoPaused = !visible;
|
|
493
|
+
syncPause();
|
|
494
|
+
});
|
|
495
|
+
pauseIo.observe(wrapper);
|
|
496
|
+
}
|
|
497
|
+
let sentPaused = false;
|
|
498
|
+
function syncPause() {
|
|
499
|
+
if (!port) return;
|
|
500
|
+
const want = userPaused || autoPaused;
|
|
501
|
+
if (want === sentPaused) return;
|
|
502
|
+
sentPaused = want;
|
|
503
|
+
port.postMessage(xcMessage(want ? "xc.pause" : "xc.resume"));
|
|
504
|
+
}
|
|
505
|
+
function cancelIdle() {
|
|
506
|
+
if (idleHandle === null) return;
|
|
507
|
+
const w = window;
|
|
508
|
+
if (w.cancelIdleCallback) w.cancelIdleCallback(idleHandle);
|
|
509
|
+
else clearTimeout(idleHandle);
|
|
510
|
+
idleHandle = null;
|
|
511
|
+
}
|
|
512
|
+
function scheduleLazy() {
|
|
513
|
+
if (lazy === false) {
|
|
514
|
+
activate();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (lazy === "click") return;
|
|
518
|
+
const go = () => {
|
|
519
|
+
const w = window;
|
|
520
|
+
idleHandle = w.requestIdleCallback ? w.requestIdleCallback(() => {
|
|
521
|
+
idleHandle = null;
|
|
522
|
+
activate();
|
|
523
|
+
}, { timeout: 3e3 }) : setTimeout(activate, 1200);
|
|
524
|
+
};
|
|
525
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
526
|
+
go();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
io = new IntersectionObserver((entries) => {
|
|
530
|
+
if (entries.some((e) => e.isIntersecting)) {
|
|
531
|
+
io?.disconnect();
|
|
532
|
+
io = null;
|
|
533
|
+
go();
|
|
534
|
+
}
|
|
535
|
+
}, { rootMargin: `${options.lazyMargin ?? 200}px` });
|
|
536
|
+
io.observe(wrapper);
|
|
537
|
+
}
|
|
538
|
+
let handle = null;
|
|
539
|
+
if (options.draggable && position !== "inline") {
|
|
540
|
+
handle = document.createElement("div");
|
|
541
|
+
handle.setAttribute("aria-label", "drag");
|
|
542
|
+
Object.assign(handle.style, {
|
|
543
|
+
position: "absolute",
|
|
544
|
+
left: "0",
|
|
545
|
+
top: "0",
|
|
546
|
+
width: "28px",
|
|
547
|
+
height: "28px",
|
|
548
|
+
cursor: "grab",
|
|
549
|
+
pointerEvents: "auto",
|
|
550
|
+
borderRadius: "8px",
|
|
551
|
+
background: "rgba(0,0,0,.25)",
|
|
552
|
+
color: "#fff",
|
|
553
|
+
font: "14px/28px system-ui",
|
|
554
|
+
textAlign: "center",
|
|
555
|
+
userSelect: "none",
|
|
556
|
+
touchAction: "none",
|
|
557
|
+
zIndex: "2",
|
|
558
|
+
opacity: "0.5"
|
|
559
|
+
});
|
|
560
|
+
handle.textContent = "\u283F";
|
|
561
|
+
let drag = null;
|
|
562
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
563
|
+
const r = wrapper.getBoundingClientRect();
|
|
564
|
+
drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };
|
|
565
|
+
handle.setPointerCapture(e.pointerId);
|
|
566
|
+
});
|
|
567
|
+
handle.addEventListener("pointermove", (e) => {
|
|
568
|
+
if (!drag) return;
|
|
569
|
+
ws.left = `${Math.min(Math.max(0, e.clientX - drag.dx), window.innerWidth - 40)}px`;
|
|
570
|
+
ws.top = `${Math.min(Math.max(0, e.clientY - drag.dy), window.innerHeight - 40)}px`;
|
|
571
|
+
ws.right = "auto";
|
|
572
|
+
ws.bottom = "auto";
|
|
573
|
+
});
|
|
574
|
+
const end = () => {
|
|
575
|
+
drag = null;
|
|
576
|
+
};
|
|
577
|
+
handle.addEventListener("pointerup", end);
|
|
578
|
+
handle.addEventListener("pointercancel", end);
|
|
579
|
+
wrapper.appendChild(handle);
|
|
580
|
+
}
|
|
581
|
+
scheduleLazy();
|
|
582
|
+
const api = {
|
|
583
|
+
element: wrapper,
|
|
584
|
+
get iframe() {
|
|
585
|
+
return iframe;
|
|
586
|
+
},
|
|
587
|
+
ready,
|
|
588
|
+
activate,
|
|
589
|
+
say: (text, opts) => send("xc.say", { text, mode: opts?.mode }, true),
|
|
590
|
+
speakAudio,
|
|
591
|
+
speakAudioStream,
|
|
592
|
+
motion: (m) => send("xc.motion", typeof m === "string" ? m.startsWith("http") || m.startsWith("/") ? { url: m } : { name: m } : m),
|
|
593
|
+
expression: (name) => send("xc.expression", { name }),
|
|
594
|
+
lookAt: (x, y) => send("xc.lookAt", { x, y }),
|
|
595
|
+
setModel: (m) => send("xc.setModel", typeof m === "string" ? /^(https?:)?\/|\.vrm/i.test(m) ? { url: m } : { outfit: m } : m),
|
|
596
|
+
setConfig: (cfg) => send("xc.setConfig", cfg),
|
|
597
|
+
startListening: () => send("xc.mic", { enabled: true }),
|
|
598
|
+
stopListening: () => send("xc.mic", { enabled: false }),
|
|
599
|
+
mic: (enabled) => send("xc.mic", { enabled }),
|
|
600
|
+
pause() {
|
|
601
|
+
userPaused = true;
|
|
602
|
+
syncPause();
|
|
603
|
+
},
|
|
604
|
+
resume() {
|
|
605
|
+
userPaused = false;
|
|
606
|
+
syncPause();
|
|
607
|
+
},
|
|
608
|
+
destroy() {
|
|
609
|
+
if (destroyed) return;
|
|
610
|
+
try {
|
|
611
|
+
port?.postMessage(xcMessage("xc.destroy"));
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
destroyed = true;
|
|
615
|
+
cancelIdle();
|
|
616
|
+
io?.disconnect();
|
|
617
|
+
pauseIo?.disconnect();
|
|
618
|
+
if (handshakeTimer) clearTimeout(handshakeTimer);
|
|
619
|
+
if (pointerRaf) cancelAnimationFrame(pointerRaf);
|
|
620
|
+
window.removeEventListener("message", onWindowMessage);
|
|
621
|
+
window.removeEventListener("pointermove", onHostPointer);
|
|
622
|
+
try {
|
|
623
|
+
port?.close();
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
port = null;
|
|
627
|
+
for (const p of pending.values()) p.reject(new Error("[project-xiaochun] instance destroyed"));
|
|
628
|
+
pending.clear();
|
|
629
|
+
queue.length = 0;
|
|
630
|
+
if (!loaded) rejectReady(new Error("[project-xiaochun] destroyed before ready"));
|
|
631
|
+
wrapper.remove();
|
|
632
|
+
iframe = null;
|
|
633
|
+
emit("destroy", void 0);
|
|
634
|
+
listeners.clear();
|
|
635
|
+
},
|
|
636
|
+
on(event, cb) {
|
|
637
|
+
let set = listeners.get(event);
|
|
638
|
+
if (!set) {
|
|
639
|
+
set = /* @__PURE__ */ new Set();
|
|
640
|
+
listeners.set(event, set);
|
|
641
|
+
}
|
|
642
|
+
set.add(cb);
|
|
643
|
+
return () => {
|
|
644
|
+
set.delete(cb);
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
return api;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/avatar-element.ts
|
|
652
|
+
var OBSERVED = [
|
|
653
|
+
"src",
|
|
654
|
+
"model",
|
|
655
|
+
"lang",
|
|
656
|
+
"mic",
|
|
657
|
+
"transparent",
|
|
658
|
+
"draggable",
|
|
659
|
+
"position",
|
|
660
|
+
"size",
|
|
661
|
+
"lazy",
|
|
662
|
+
"paused",
|
|
663
|
+
"placeholder",
|
|
664
|
+
"heavy",
|
|
665
|
+
"ui",
|
|
666
|
+
"controls",
|
|
667
|
+
"allowed-origins"
|
|
668
|
+
];
|
|
669
|
+
var REBUILD = /* @__PURE__ */ new Set(["src", "draggable", "position", "size", "lazy", "placeholder", "heavy", "ui", "controls", "allowed-origins", "transparent"]);
|
|
670
|
+
function boolAttr(el, name, dflt) {
|
|
671
|
+
const v = el.getAttribute(name);
|
|
672
|
+
if (v === null) return dflt;
|
|
673
|
+
return v !== "false";
|
|
674
|
+
}
|
|
675
|
+
function parseSize(v) {
|
|
676
|
+
if (!v) return { width: "320px", height: "480px" };
|
|
677
|
+
const m = v.trim().split(/x/i);
|
|
678
|
+
const px = (s) => /^\d+(\.\d+)?$/.test(s) ? `${s}px` : s;
|
|
679
|
+
if (m.length >= 2) return { width: px(m[0]), height: px(m[1]) };
|
|
680
|
+
const w = parseFloat(m[0]);
|
|
681
|
+
return Number.isFinite(w) && /^\d+(\.\d+)?$/.test(m[0]) ? { width: `${w}px`, height: `${Math.round(w * 1.5)}px` } : { width: m[0], height: "480px" };
|
|
682
|
+
}
|
|
683
|
+
function createElementClass() {
|
|
684
|
+
class XiaochunAvatarElementImpl extends HTMLElement {
|
|
685
|
+
constructor() {
|
|
686
|
+
super(...arguments);
|
|
687
|
+
/** 底层 SDK 实例 (连接 DOM 后可用)。 */
|
|
688
|
+
this.client = null;
|
|
689
|
+
this.mountEl = null;
|
|
690
|
+
this.rebuildScheduled = false;
|
|
691
|
+
}
|
|
692
|
+
static get observedAttributes() {
|
|
693
|
+
return [...OBSERVED];
|
|
694
|
+
}
|
|
695
|
+
connectedCallback() {
|
|
696
|
+
if (!this.shadowRoot) {
|
|
697
|
+
const root = this.attachShadow({ mode: "open" });
|
|
698
|
+
root.innerHTML = '<style>:host{display:inline-block;max-width:100%}:host([hidden]){display:none}</style><div part="mount"></div>';
|
|
699
|
+
}
|
|
700
|
+
this.mountEl = this.shadowRoot.querySelector("div");
|
|
701
|
+
this.build();
|
|
702
|
+
}
|
|
703
|
+
disconnectedCallback() {
|
|
704
|
+
this.destroy();
|
|
705
|
+
}
|
|
706
|
+
attributeChangedCallback(name, oldV, newV) {
|
|
707
|
+
if (!this.client || oldV === newV) return;
|
|
708
|
+
if (REBUILD.has(name)) {
|
|
709
|
+
if (!this.rebuildScheduled) {
|
|
710
|
+
this.rebuildScheduled = true;
|
|
711
|
+
queueMicrotask(() => {
|
|
712
|
+
this.rebuildScheduled = false;
|
|
713
|
+
if (this.isConnected) {
|
|
714
|
+
this.destroy();
|
|
715
|
+
this.build();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const c = this.client;
|
|
722
|
+
if (name === "model" && newV) void c.setModel(newV).catch(() => {
|
|
723
|
+
});
|
|
724
|
+
else if (name === "lang" && newV) void c.setConfig({ lang: newV }).catch(() => {
|
|
725
|
+
});
|
|
726
|
+
else if (name === "mic") void c.mic(boolAttr(this, "mic", false)).catch(() => {
|
|
727
|
+
});
|
|
728
|
+
else if (name === "paused") {
|
|
729
|
+
if (boolAttr(this, "paused", false)) c.pause();
|
|
730
|
+
else c.resume();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
build() {
|
|
734
|
+
if (!this.mountEl || this.client) return;
|
|
735
|
+
const { width, height } = parseSize(this.getAttribute("size"));
|
|
736
|
+
const placeholder = this.getAttribute("placeholder");
|
|
737
|
+
const allowed = this.getAttribute("allowed-origins");
|
|
738
|
+
const lazyAttr = this.getAttribute("lazy");
|
|
739
|
+
const lazy = lazyAttr === "click" ? "click" : lazyAttr === "false" ? false : true;
|
|
740
|
+
const c = createXiaochun({
|
|
741
|
+
container: this.mountEl,
|
|
742
|
+
src: this.getAttribute("src") || void 0,
|
|
743
|
+
allowedOrigins: allowed ? allowed.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
744
|
+
lazy,
|
|
745
|
+
placeholder: placeholder === "none" ? false : placeholder || void 0,
|
|
746
|
+
transparent: boolAttr(this, "transparent", true),
|
|
747
|
+
// 悬浮头像默认透明
|
|
748
|
+
width,
|
|
749
|
+
height,
|
|
750
|
+
position: this.getAttribute("position") ?? "inline",
|
|
751
|
+
draggable: boolAttr(this, "draggable", false),
|
|
752
|
+
lang: this.getAttribute("lang") ?? void 0,
|
|
753
|
+
model: this.getAttribute("model") ?? void 0,
|
|
754
|
+
heavy: this.getAttribute("heavy") ?? void 0,
|
|
755
|
+
ui: boolAttr(this, "ui", false),
|
|
756
|
+
controls: boolAttr(this, "controls", false)
|
|
757
|
+
});
|
|
758
|
+
this.client = c;
|
|
759
|
+
const fwd = (ev, name) => c.on(ev, (detail) => this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })));
|
|
760
|
+
fwd("ready", "xc-ready");
|
|
761
|
+
fwd("progress", "xc-progress");
|
|
762
|
+
fwd("state", "xc-state");
|
|
763
|
+
fwd("stt", "xc-stt");
|
|
764
|
+
fwd("utterance", "xc-utterance");
|
|
765
|
+
fwd("error", "xc-error");
|
|
766
|
+
if (boolAttr(this, "paused", false)) c.pause();
|
|
767
|
+
if (boolAttr(this, "mic", false)) void c.ready.then(() => c.mic(true)).catch(() => {
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
say(text, opts) {
|
|
771
|
+
return this.requireClient().say(text, opts);
|
|
772
|
+
}
|
|
773
|
+
speakAudio(source, opts) {
|
|
774
|
+
return this.requireClient().speakAudio(source, opts);
|
|
775
|
+
}
|
|
776
|
+
speakAudioStream(opts) {
|
|
777
|
+
return this.requireClient().speakAudioStream(opts);
|
|
778
|
+
}
|
|
779
|
+
motion(m) {
|
|
780
|
+
return this.requireClient().motion(m);
|
|
781
|
+
}
|
|
782
|
+
expression(name) {
|
|
783
|
+
return this.requireClient().expression(name);
|
|
784
|
+
}
|
|
785
|
+
destroy() {
|
|
786
|
+
this.client?.destroy();
|
|
787
|
+
this.client = null;
|
|
788
|
+
}
|
|
789
|
+
requireClient() {
|
|
790
|
+
if (!this.client) throw new Error("<xiaochun-avatar> is not connected");
|
|
791
|
+
return this.client;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return XiaochunAvatarElementImpl;
|
|
795
|
+
}
|
|
796
|
+
var XIAOCHUN_ELEMENT_TAG = "xiaochun-avatar";
|
|
797
|
+
function defineXiaochunElement(tag = XIAOCHUN_ELEMENT_TAG) {
|
|
798
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return null;
|
|
799
|
+
const existing = customElements.get(tag);
|
|
800
|
+
if (existing) return existing;
|
|
801
|
+
const ctor = createElementClass();
|
|
802
|
+
customElements.define(tag, ctor);
|
|
803
|
+
return ctor;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/protocol-url.ts
|
|
807
|
+
var XIAOCHUN_PROTOCOL_SCHEME = "xiaochun";
|
|
808
|
+
function toProtocolUrl(a) {
|
|
809
|
+
if (a.action !== "speak") throw new Error(`[project-xiaochun] unsupported action: ${String(a.action)}`);
|
|
810
|
+
if (!a.text && !a.audioUrl && !a.file) throw new Error("[project-xiaochun] speak needs text, audioUrl or file");
|
|
811
|
+
if (a.audioUrl && !/^https?:\/\//i.test(a.audioUrl)) throw new Error("[project-xiaochun] audioUrl must be http(s)");
|
|
812
|
+
const q = new URLSearchParams();
|
|
813
|
+
if (a.text) q.set("text", a.text);
|
|
814
|
+
if (a.audioUrl) q.set("audioUrl", a.audioUrl);
|
|
815
|
+
if (a.file) q.set("file", a.file);
|
|
816
|
+
return `${XIAOCHUN_PROTOCOL_SCHEME}://speak?${q.toString().replace(/\+/g, "%20")}`;
|
|
817
|
+
}
|
|
818
|
+
function parseProtocolUrl(url) {
|
|
819
|
+
let u;
|
|
820
|
+
try {
|
|
821
|
+
u = new URL(url);
|
|
822
|
+
} catch {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
if (u.protocol !== `${XIAOCHUN_PROTOCOL_SCHEME}:`) return null;
|
|
826
|
+
const name = u.hostname || u.pathname.replace(/^\/+/, "");
|
|
827
|
+
const action = name === "action" ? u.searchParams.get("action") : name;
|
|
828
|
+
if (action !== "speak") return null;
|
|
829
|
+
const out = { action: "speak" };
|
|
830
|
+
const text = u.searchParams.get("text");
|
|
831
|
+
const audioUrl = u.searchParams.get("audioUrl");
|
|
832
|
+
const file = u.searchParams.get("file");
|
|
833
|
+
if (text) out.text = text;
|
|
834
|
+
if (audioUrl) out.audioUrl = audioUrl;
|
|
835
|
+
if (file) out.file = file;
|
|
836
|
+
return out.text || out.audioUrl || out.file ? out : null;
|
|
837
|
+
}
|
|
838
|
+
//# sourceMappingURL=index.cjs.map
|