@olares/lares-core 0.15.2 → 0.15.4

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/brand/manifest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { PRODUCT_NAME, THEME_COLOR } from "./identity.js";
2
- import { MARK_PATH } from "../icons/mark.js";
2
+ import { MARK_PATH, MARK_TYPE } from "../icons/mark.js";
3
3
 
4
4
  export const MANIFEST_PATH = "/lares/manifest.webmanifest";
5
5
 
@@ -11,5 +11,5 @@ export const MANIFEST = {
11
11
  scope: "/",
12
12
  display: "fullscreen",
13
13
  theme_color: THEME_COLOR,
14
- icons: [{ src: MARK_PATH, sizes: "any", type: "image/svg+xml", purpose: "any" }],
14
+ icons: [{ src: MARK_PATH, sizes: "any", type: MARK_TYPE, purpose: "any" }],
15
15
  };
@@ -7,6 +7,31 @@ function errorCode(payload) {
7
7
  : "file_upload_failed";
8
8
  }
9
9
 
10
+ /**
11
+ * `fetch` can only authenticate with the Olares cookie, and a host whose page is
12
+ * cross-origin to the Host — a packaged LarePass desktop build is a `file://`
13
+ * page — has none, so the upload answers 401 while every RPC call succeeds.
14
+ * When the host injects a `request` port the upload rides the same auth as the
15
+ * rest of the client; without one this stays the plain cookie fetch.
16
+ */
17
+ async function postUpload(options, url, headers, body, signal) {
18
+ if (typeof options.request === "function") {
19
+ const res = await options.request(url, { method: "POST", headers, body, signal });
20
+ return {
21
+ ok: Boolean(res?.ok),
22
+ status: Number(res?.status) || 0,
23
+ // The port already parsed the body; an empty one arrives as undefined.
24
+ payload: res?.body ?? null,
25
+ };
26
+ }
27
+ const res = await fetch(url, { method: "POST", headers, body, signal });
28
+ return {
29
+ ok: res.ok,
30
+ status: res.status,
31
+ payload: await res.json().catch(() => null),
32
+ };
33
+ }
34
+
10
35
  export async function uploadFile(file, sessionId, options = {}) {
11
36
  const controller = new AbortController();
12
37
  const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
@@ -14,21 +39,21 @@ export async function uploadFile(file, sessionId, options = {}) {
14
39
  if (options.signal?.aborted) controller.abort(options.signal.reason);
15
40
  else options.signal?.addEventListener("abort", onAbort, { once: true });
16
41
  try {
17
- const response = await fetch(options.url ?? FILES_UPLOAD_PATH, {
18
- method: "POST",
19
- headers: {
42
+ const { ok, status, payload } = await postUpload(
43
+ options,
44
+ options.url ?? FILES_UPLOAD_PATH,
45
+ {
20
46
  "content-type": file.type || "application/octet-stream",
21
47
  "x-lares-file-name": encodeURIComponent(file.name || "file"),
22
48
  "x-lares-session-id": sessionId,
23
49
  "x-lares-upload-request-id": options.requestId ?? crypto.randomUUID(),
24
50
  },
25
- body: file,
26
- signal: controller.signal,
27
- });
28
- const payload = await response.json().catch(() => null);
29
- if (!response.ok) {
51
+ file,
52
+ controller.signal,
53
+ );
54
+ if (!ok) {
30
55
  const error = new Error(errorCode(payload));
31
- error.status = response.status;
56
+ error.status = status;
32
57
  throw error;
33
58
  }
34
59
  if (!payload || typeof payload.path !== "string") throw new Error("file_upload_failed");
package/icons/mark.js CHANGED
@@ -1,21 +1,38 @@
1
1
  import { PRODUCT_NAME } from "../brand/identity.js";
2
2
 
3
3
  /**
4
- * Product mark: favicon, PWA icon, CSS splash, and slot occupants.
5
- * An eave over a resident orb — two strokes, readable at 20px collapsed rail.
4
+ * Product mark: favicon, PWA icon, CSS splash, and slot occupants. Consumers
5
+ * cache this URL, so the path and the SVG encoding are fixed — publishing the
6
+ * artwork at a new path leaves stale favicons behind.
7
+ * Geometry is the design delivery verbatim (ids renamed, aria-label injected).
8
+ * Keep it a vector — favicon rasterizers drop <image> data URIs.
6
9
  */
10
+ export const MARKET_ICON_URL = "https://app.cdn.olares.com/appstore/lares/icon_new.png";
11
+
7
12
  export const MARK_PATH = "/lares/mark.svg";
13
+ export const MARK_TYPE = "image/svg+xml";
8
14
 
9
- export const MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="${PRODUCT_NAME}">
15
+ export const MARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256" fill="none" role="img" aria-label="${PRODUCT_NAME}">
10
16
  <defs>
11
- <linearGradient id="lares-mark" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
12
- <stop offset="0" stop-color="#6366F1"/>
13
- <stop offset="1" stop-color="#22D3EE"/>
17
+ <filter id="lares-mark-shadow" x="0" y="0" width="256" height="257.829" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
18
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
19
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
20
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
21
+ <feOffset dy="1.82857"/>
22
+ <feGaussianBlur stdDeviation="0.914286"/>
23
+ <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
24
+ <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/>
25
+ <feBlend mode="normal" in2="shape" result="effect1_innerShadow"/>
26
+ </filter>
27
+ <linearGradient id="lares-mark" x1="128" y1="0" x2="128" y2="256" gradientUnits="userSpaceOnUse">
28
+ <stop stop-color="#FFED52"/>
29
+ <stop offset="1" stop-color="#EBD304"/>
14
30
  </linearGradient>
15
31
  </defs>
16
- <rect width="32" height="32" rx="8" fill="url(#lares-mark)"/>
17
- <path fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" d="M6.8 17.4 16 9.2l9.2 8.2"/>
18
- <circle cx="16" cy="22" r="3.2" fill="#fff"/>
32
+ <g filter="url(#lares-mark-shadow)">
33
+ <rect width="256" height="256" fill="url(#lares-mark)"/>
34
+ <path fill="#252121" d="M83.5926 125.858C85.7382 144.539 100.433 157.223 120.829 157.998L128.063 158.273H142.829C170.407 158.273 186.978 179.868 179.84 206.507H115.676C86.0222 206.506 68.2049 183.286 75.8798 154.643L83.5926 125.858ZM104.576 47.5429C133.193 47.5429 150.386 69.9505 142.98 97.5918L128.319 152.309C99.7025 152.309 82.5086 129.9 89.915 102.259L100.338 63.3618C102.534 55.1648 97.833 48.3464 89.5437 47.7079L87.3945 47.5429H104.576Z"/>
35
+ </g>
19
36
  </svg>`;
20
37
 
21
38
  /** Inline form for CSS `background-image`, which cannot reference a route that may 404 mid-boot. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olares/lares-core",
3
- "version": "0.15.2",
3
+ "version": "0.15.4",
4
4
  "description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
5
5
  "type": "module",
6
6
  "repository": {
package/router/shim.js CHANGED
@@ -43,7 +43,9 @@ export function shimBudget(suffix) {
43
43
  };
44
44
  }
45
45
 
46
+ /** @returns {Record<string, string>} */
46
47
  export function shimRequestHeaders(incoming, env = process.env) {
48
+ /** @type {Record<string, string>} */
47
49
  const headers = {};
48
50
  for (const [key, value] of Object.entries(incoming)) {
49
51
  if (value == null || DROPPED_REQ.has(key.toLowerCase())) continue;
@@ -107,6 +109,36 @@ function serveCachedModels(req, res) {
107
109
  );
108
110
  }
109
111
 
112
+ function downstreamCancellation(req, res) {
113
+ const controller = new AbortController();
114
+ let finished = false;
115
+
116
+ const cleanup = () => {
117
+ req.off("aborted", cancel);
118
+ res.off("finish", finish);
119
+ res.off("close", close);
120
+ };
121
+ const cancel = () => {
122
+ if (!controller.signal.aborted) {
123
+ controller.abort(new Error("LLM downstream disconnected"));
124
+ }
125
+ cleanup();
126
+ };
127
+ const finish = () => {
128
+ finished = true;
129
+ cleanup();
130
+ };
131
+ const close = () => {
132
+ if (!finished) cancel();
133
+ else cleanup();
134
+ };
135
+
136
+ req.once("aborted", cancel);
137
+ res.once("finish", finish);
138
+ res.once("close", close);
139
+ return controller;
140
+ }
141
+
110
142
  export function proxyToRouter(req, res, env = process.env) {
111
143
  if (isModelsGet(req)) {
112
144
  serveCachedModels(req, res);
@@ -121,6 +153,7 @@ export function proxyToRouter(req, res, env = process.env) {
121
153
  const headers = shimRequestHeaders(req.headers, env);
122
154
  const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
123
155
  const method = (req.method ?? "GET").toUpperCase();
156
+ const cancellation = downstreamCancellation(req, res);
124
157
 
125
158
  const run = async () => {
126
159
  let body;
@@ -132,16 +165,22 @@ export function proxyToRouter(req, res, env = process.env) {
132
165
  if (!budget.audio && carriesWebpImage(body)) body = await transcodeWebpImages(body);
133
166
  headers["content-length"] = String(body.length);
134
167
  }
168
+ if (cancellation.signal.aborted) return;
135
169
 
136
170
  const upstream = transport(
137
171
  target,
138
- { method, headers, timeout: budget.timeoutMs },
172
+ { method, headers, timeout: budget.timeoutMs, signal: cancellation.signal },
139
173
  (up) => {
174
+ if (cancellation.signal.aborted) {
175
+ up.destroy();
176
+ return;
177
+ }
140
178
  res.writeHead(up.statusCode ?? 502, shimResponseHeaders(up.headers));
141
179
  up.pipe(res);
142
180
  },
143
181
  );
144
182
  upstream.on("error", (err) => {
183
+ if (cancellation.signal.aborted) return;
145
184
  if (res.headersSent) {
146
185
  res.destroy();
147
186
  return;
@@ -161,6 +200,7 @@ export function proxyToRouter(req, res, env = process.env) {
161
200
  };
162
201
 
163
202
  void run().catch((err) => {
203
+ if (cancellation.signal.aborted) return;
164
204
  if (res.headersSent) {
165
205
  res.destroy();
166
206
  return;
package/voice/client.js CHANGED
@@ -62,23 +62,39 @@ export function mergeTranscript(draft, transcript) {
62
62
  return `${base} ${text}`;
63
63
  }
64
64
 
65
+ function transcribeErrorCode(payload) {
66
+ const code = payload && typeof payload === "object" ? payload.error?.code : undefined;
67
+ return typeof code === "string" ? code : "voice_failed";
68
+ }
69
+
70
+ /**
71
+ * The only voice call that carries a body, and so the only one that cannot go
72
+ * through `createHostSettings`. Like the upload it must accept the host's
73
+ * `request` port: a cross-origin host page sends no Olares cookie, so a plain
74
+ * `fetch` here answers 401 even though `/config`, `/status` and `/models` — which
75
+ * do ride the port — all succeed.
76
+ */
65
77
  export async function postTranscribe(blob, language, signal, options = {}) {
66
78
  const query = language ? `?language=${encodeURIComponent(language)}` : "";
67
79
  const base = String(options.baseUrl ?? API).replace(/\/$/, "");
68
- const res = await fetch(`${base}/transcribe${query}`, {
69
- method: "POST",
70
- headers: { "content-type": blob.type || "audio/webm" },
71
- body: blob,
72
- signal,
73
- });
80
+ const url = `${base}/transcribe${query}`;
81
+ const headers = { "content-type": blob.type || "audio/webm" };
82
+
83
+ if (typeof options.request === "function") {
84
+ const res = await options.request(url, { method: "POST", headers, body: blob, signal });
85
+ if (!res?.ok) throw new Error(transcribeErrorCode(res?.body));
86
+ return String(res?.body?.text ?? "").trim();
87
+ }
88
+
89
+ const res = await fetch(url, { method: "POST", headers, body: blob, signal });
74
90
  if (!res.ok) {
75
- let code = "voice_failed";
91
+ let payload = null;
76
92
  try {
77
- code = (await res.json())?.error?.code ?? code;
93
+ payload = await res.json();
78
94
  } catch {
79
- /* keep default */
95
+ /* fall back to the generic code */
80
96
  }
81
- throw new Error(code);
97
+ throw new Error(transcribeErrorCode(payload));
82
98
  }
83
99
  return String((await res.json())?.text ?? "").trim();
84
100
  }