@olares/lares-core 0.15.6 → 0.15.9
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/README.md +2 -1
- package/brand/identity.js +33 -3
- package/drive/cat.js +62 -0
- package/drive/download.js +3 -3
- package/drive/execute.js +8 -2
- package/drive/files-path.js +33 -0
- package/drive/flowstudio-files.js +87 -0
- package/drive/ls.js +3 -3
- package/drive/paths.js +23 -13
- package/drive/tools.js +4 -2
- package/entrance/surface.js +96 -0
- package/files/app-link.js +64 -0
- package/files/backend-client.js +266 -0
- package/files/deliverables.js +55 -1
- package/files/intake.js +0 -63
- package/files/mention.js +1 -51
- package/files/preview-groups.js +71 -3
- package/files/preview-local.js +69 -0
- package/files/preview-workspace.js +114 -13
- package/files/preview.js +171 -16
- package/files/produced-gate.js +197 -0
- package/files/produced-mentions.js +19 -0
- package/files/published-tools.js +69 -0
- package/files/published-turn-data.js +84 -0
- package/i18n/chat-model.js +5 -5
- package/i18n/mcp.js +83 -0
- package/i18n/mobile.js +100 -2
- package/i18n/preview.js +10 -0
- package/i18n/search.js +13 -7
- package/i18n/skills.js +31 -0
- package/i18n/voice.js +88 -19
- package/larepass/chat.js +9 -0
- package/larepass/host.js +7 -2
- package/larepass/mux.js +210 -2
- package/larepass/process-fold.js +239 -0
- package/larepass/queue.js +44 -0
- package/larepass/rpc.js +50 -3
- package/larepass/runtime.js +215 -83
- package/larepass/session-cache.js +6 -0
- package/larepass/settings.js +15 -7
- package/larepass/transcript.js +49 -11
- package/mcp/approval.js +152 -0
- package/mcp/client.js +38 -0
- package/mcp/config.js +139 -0
- package/media/catalog-row.js +244 -0
- package/media/generation.js +707 -0
- package/media/tool.js +92 -0
- package/olares/entrance.js +67 -15
- package/olares/identity.js +13 -136
- package/olares/session-identity.js +12 -12
- package/olares/trusted-host.js +0 -29
- package/package.json +48 -4
- package/router/catalog-cache.d.ts +1 -1
- package/router/catalog-cache.js +25 -14
- package/router/catalog-sync.js +8 -3
- package/router/catalog.d.ts +6 -1
- package/router/catalog.js +18 -2
- package/router/gateway.d.ts +9 -1
- package/router/gateway.js +40 -4
- package/router/gemini-schema.js +125 -0
- package/router/generation-guard.js +82 -0
- package/router/models.d.ts +6 -0
- package/router/models.js +22 -3
- package/router/search.js +10 -2
- package/router/session-model.js +53 -0
- package/router/shim.js +104 -3
- package/router/sref.js +126 -0
- package/router/stt.js +43 -12
- package/router/tts.js +118 -0
- package/search/client.js +5 -3
- package/search/config.js +53 -39
- package/search/menu.js +45 -10
- package/skills/client.js +42 -0
- package/skills/packs.js +40 -0
- package/skills/state.d.ts +22 -0
- package/skills/state.js +142 -0
- package/tools/crypto-uuid.js +3 -6
- package/tools/head-script.js +15 -0
- package/voice/agent.js +50 -0
- package/voice/capture.js +250 -0
- package/voice/client.js +126 -24
- package/voice/config.js +11 -1
- package/voice/duplex.js +44 -0
- package/voice/endpoint.js +57 -0
- package/voice/host/bluetooth-ready.py +1196 -0
- package/voice/host/install-host.sh +102 -0
- package/voice/input.js +4 -0
- package/voice/install.js +146 -0
- package/voice/languages.js +11 -1
- package/voice/link.js +86 -0
- package/voice/pcm.js +100 -0
- package/voice/playback.js +7 -0
- package/voice/problem.js +46 -0
- package/voice/recorder.js +79 -4
- package/voice/script.js +35 -0
- package/voice/service.js +253 -10
- package/voice/session.js +128 -0
- package/voice/speech.js +93 -0
- package/files/preview-cache.js +0 -100
package/media/tool.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { producedEditCard } from "../drive/execute.js";
|
|
2
|
+
import { workspaceRootFromSession } from "../workspace/env.js";
|
|
3
|
+
import { MEDIA_GENERATION_TIMEOUT_MS, runMediaGeneration } from "./generation.js";
|
|
4
|
+
|
|
5
|
+
export const MEDIA_GENERATE_TOOL = "media_generate";
|
|
6
|
+
|
|
7
|
+
export const MEDIA_GENERATE_PROMPT = [
|
|
8
|
+
"Generate or edit images, video, music, and 3D with media_generate, not with curl and a sleep loop.",
|
|
9
|
+
"Name the catalog row by model. Pass the user's prompt unchanged, images as reference_images",
|
|
10
|
+
"(workspace paths) for an edit or I2V / R2V, an optional mask_image, and params for the row's",
|
|
11
|
+
"tunables, keyed by flowstudio.parameters[].key. The tool reads the row and takes the route,",
|
|
12
|
+
"the operation, and where each file goes. A supplied image on an image_generation row is an edit.",
|
|
13
|
+
"It submits as the logged-in user, waits, and publishes every output itself: do not",
|
|
14
|
+
"workspace_publish or download its files again. If a turn ended before a generation",
|
|
15
|
+
"finished, resume it with generation_id instead of generating it again.",
|
|
16
|
+
].join(" ");
|
|
17
|
+
|
|
18
|
+
/** @param deps - seams for tests: fetch, sleep, env, cat. */
|
|
19
|
+
export function mediaGenerateDefinition(deps = {}) {
|
|
20
|
+
return {
|
|
21
|
+
name: MEDIA_GENERATE_TOOL,
|
|
22
|
+
description:
|
|
23
|
+
"Create one image, video, music, or 3D generation through Router and wait until it finishes."
|
|
24
|
+
+ " Every output is published for preview when the call returns. For image-to-video or an"
|
|
25
|
+
+ " image edit, pass the user's image as reference_images. Pass generation_id alone to wait"
|
|
26
|
+
+ " for a generation an earlier turn started.",
|
|
27
|
+
parameters: {
|
|
28
|
+
model: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description:
|
|
31
|
+
"<provider>/<model> exactly as the catalog row lists it. The row decides everything else"
|
|
32
|
+
+ " about the call. Required unless generation_id is set.",
|
|
33
|
+
},
|
|
34
|
+
prompt: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "The user's prompt, unchanged.",
|
|
37
|
+
},
|
|
38
|
+
reference_images: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: { type: "string" },
|
|
41
|
+
description:
|
|
42
|
+
"Workspace image paths for an edit or I2V / R2V. The tool places them on the row.",
|
|
43
|
+
},
|
|
44
|
+
mask_image: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Optional workspace mask image.",
|
|
47
|
+
},
|
|
48
|
+
params: {
|
|
49
|
+
type: "object",
|
|
50
|
+
additionalProperties: true,
|
|
51
|
+
description:
|
|
52
|
+
"Tunables keyed by the row's flowstudio.parameters[].key. The tool sends them as"
|
|
53
|
+
+ " flowstudio.params. Match the user's words to each entry's label; for a select send"
|
|
54
|
+
+ " the option's value. A row without flowstudio.parameters takes no params.",
|
|
55
|
+
},
|
|
56
|
+
generation_id: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Resume waiting for this generation instead of starting a new one.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
output: {
|
|
62
|
+
schema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: "string", required: true },
|
|
67
|
+
status: { type: "string", required: true },
|
|
68
|
+
files: { type: "array", items: { type: "string" }, required: true },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
render: (_args, value) => [{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: `Generation ${value.id} ${value.status}; published ${value.files.join(", ") || "no files"}.`,
|
|
74
|
+
}],
|
|
75
|
+
},
|
|
76
|
+
timeoutMs: MEDIA_GENERATION_TIMEOUT_MS,
|
|
77
|
+
execute: (args, exec) => runMediaGeneration(args, {
|
|
78
|
+
session: exec?.agent?.session,
|
|
79
|
+
callId: exec?.callId,
|
|
80
|
+
workspaceRoot: workspaceRootFromSession(exec, deps.env),
|
|
81
|
+
signal: exec?.signal,
|
|
82
|
+
}, deps),
|
|
83
|
+
presentCall: presentMediaGenerate,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function presentMediaGenerate(args) {
|
|
88
|
+
const target = String(args?.generation_id ?? "").trim()
|
|
89
|
+
? `generation ${args.generation_id}`
|
|
90
|
+
: `media with ${String(args?.model ?? "")}`;
|
|
91
|
+
return producedEditCard(`Generate ${target}`.slice(0, 160));
|
|
92
|
+
}
|
package/olares/entrance.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Olares 入口域推导(client 侧共享源码,由各插件 client 构建时内联)。
|
|
3
3
|
*
|
|
4
|
-
* Olares 给每个应用入口分配
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 首段换成 `router` 即 Router 控制台。
|
|
4
|
+
* Olares 给每个应用入口分配 `<第三级域名>.<用户域>`。用户域由 Olares ID 决定
|
|
5
|
+
* (olares.com、大陆的 olares.cn、局域网 olares.local,或自定义域名),zone 不可
|
|
6
|
+
* 枚举,只能从当前入口地址里取;scheme 同理——局域网入口未必是 https。
|
|
8
7
|
*
|
|
9
8
|
* 端口转发 / IP 直连等非入口访问下无法推断,返回空串,调用方应隐藏入口。
|
|
10
9
|
*/
|
|
@@ -16,25 +15,78 @@ export function loopbackWebUrl(port) {
|
|
|
16
15
|
return `http://127.0.0.1:${String(port)}`;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const ROUTER_LABEL = "router";
|
|
20
|
-
const OLARES_ZONES = new Set(["olares.com", "olares.local"]);
|
|
21
18
|
const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
22
19
|
// Router 控制台是 history 模式 SPA,入口即顶层路由路径。
|
|
23
20
|
const ROUTER_ROUTE = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*$/;
|
|
24
21
|
|
|
22
|
+
// 用户域是 `<用户名>.<zone>`,zone 本身至少两段,所以不足三段的不是用户域;
|
|
23
|
+
// 末段全是数字则是 IP 字面量。
|
|
24
|
+
function userDomainLabels(domain) {
|
|
25
|
+
const labels = String(domain ?? "").trim().toLowerCase().split(".");
|
|
26
|
+
if (labels.length < 3 || !/[a-z]/.test(labels[labels.length - 1])) return [];
|
|
27
|
+
return labels.every((label) => DNS_LABEL.test(label)) ? labels : [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 入口 hostname 去掉应用自己的第三级域名,剩下的就是用户域。
|
|
32
|
+
* @param {string} hostname 入口 hostname(不含端口)
|
|
33
|
+
* @returns {string} `<用户名>.<zone>`;非入口访问时为空串
|
|
34
|
+
*/
|
|
35
|
+
export function userDomainOf(hostname) {
|
|
36
|
+
const labels = String(hostname ?? "").trim().toLowerCase().split(".");
|
|
37
|
+
if (!DNS_LABEL.test(labels[0] ?? "")) return "";
|
|
38
|
+
return userDomainLabels(labels.slice(1).join(".")).join(".");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} label 应用的第三级域名,如 files
|
|
43
|
+
* @param {{domain?: string, protocol?: string, port?: string}} entrance
|
|
44
|
+
* @returns {string} 同一入口区下该应用的 origin;无法推断时为空串
|
|
45
|
+
*/
|
|
46
|
+
export function entranceOrigin(label, { domain, protocol = "https:", port = "" } = {}) {
|
|
47
|
+
const scheme = String(protocol ?? "").trim().toLowerCase();
|
|
48
|
+
if (!["http:", "https:"].includes(scheme)) return "";
|
|
49
|
+
if (!DNS_LABEL.test(String(label ?? ""))) return "";
|
|
50
|
+
// 入口永远在 scheme 默认端口上,带端口的是转发或本地调试,不是入口。
|
|
51
|
+
if (String(port ?? "")) return "";
|
|
52
|
+
const labels = userDomainLabels(domain);
|
|
53
|
+
return labels.length ? `${scheme}//${label}.${labels.join(".")}` : "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const ROUTER_CONSOLE_GLOBAL = "__LARES_ROUTER_CONSOLE__";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Router 控制台的入口不能靠当前页面域名推:它是 Router chart 申领的第三级域名
|
|
60
|
+
* `router.<用户域>`,只登记在公网 zone,局域网 DNS 里没有这条记录(同一入口在
|
|
61
|
+
* 节点 ingress 上是通的,但 `router.<用户>.olares.local` 解析不出来)。权威来源
|
|
62
|
+
* 只有 Host 侧的 `LLM_GATEWAY_URL`——chart 用 `.Values.user.zone` 渲染,天然跟着
|
|
63
|
+
* 用户的 .com / .cn / 自定义域名走。
|
|
64
|
+
*
|
|
65
|
+
* @param {string} [gatewayUrl] Router 数据面 URL,或已解析出的控制台 origin
|
|
66
|
+
* @returns {string} 控制台 origin;集群外(如本地 `router-svc.router-shared`)为空串
|
|
67
|
+
*/
|
|
68
|
+
export function routerConsoleOrigin(gatewayUrl) {
|
|
69
|
+
let url;
|
|
70
|
+
try {
|
|
71
|
+
url = new URL(String(gatewayUrl ?? ""));
|
|
72
|
+
} catch {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
const [label, ...domain] = url.hostname.toLowerCase().split(".");
|
|
76
|
+
return entranceOrigin(label, { domain: domain.join("."), protocol: url.protocol, port: url.port });
|
|
77
|
+
}
|
|
78
|
+
|
|
25
79
|
/**
|
|
26
|
-
* @param {string}
|
|
27
|
-
* @param {string} [
|
|
28
|
-
* @returns {string} Router 控制台 URL
|
|
80
|
+
* @param {string} route Router 控制台顶层路由,如 llm / audio / tools
|
|
81
|
+
* @param {string} [origin] Host 注入的控制台 origin
|
|
82
|
+
* @returns {string} Router 控制台 URL;无入口时为空串
|
|
29
83
|
*/
|
|
30
|
-
export function routerConsoleUrl(route = "",
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
if (!OLARES_ZONES.has(labels.slice(-2).join("."))) return "";
|
|
84
|
+
export function routerConsoleUrl(route = "", origin = globalThis[ROUTER_CONSOLE_GLOBAL]) {
|
|
85
|
+
const base = routerConsoleOrigin(origin);
|
|
86
|
+
if (!base) return "";
|
|
34
87
|
const path = String(route).trim().toLowerCase();
|
|
35
88
|
if (path && !ROUTER_ROUTE.test(path)) return "";
|
|
36
|
-
|
|
37
|
-
return path ? `${origin}/${path}` : origin;
|
|
89
|
+
return path ? `${base}/${path}` : base;
|
|
38
90
|
}
|
|
39
91
|
|
|
40
92
|
/** 新标签页打开 Router 控制台;无入口时静默忽略。 */
|
package/olares/identity.js
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Olares edge identity
|
|
3
|
-
*
|
|
2
|
+
* Olares edge identity: who the reverse proxy says this request is, and the
|
|
3
|
+
* browser credential it carried.
|
|
4
|
+
*
|
|
5
|
+
* The olares-cli session inside the container is NOT derived from this. It
|
|
6
|
+
* comes from the credential app-service mounts for `permission.loginOlaresCLI`
|
|
7
|
+
* (/olares/credentials), which is the only source that knows the user's full
|
|
8
|
+
* Olares ID and can refresh itself.
|
|
4
9
|
*/
|
|
5
|
-
import { createCipheriv, randomBytes } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
const MASTER_KEY_BYTES = 32;
|
|
10
|
-
const IV_BYTES = 12;
|
|
11
|
-
const SERVICE = "olares-cli";
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
|
-
* @typedef {{ user: string, token: string
|
|
12
|
+
* @typedef {{ user: string, token: string }} OlaresIdentity
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
|
-
/** @param {OlaresIdentity} identity */
|
|
18
|
-
export function identityAvailable(identity) {
|
|
19
|
-
return Boolean(identity.user && identity.token && identity.terminus);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
/** @param {string} cookieHeader */
|
|
23
16
|
function parseCookies(cookieHeader) {
|
|
24
17
|
/** @type {Record<string, string>} */
|
|
@@ -31,31 +24,16 @@ function parseCookies(cookieHeader) {
|
|
|
31
24
|
return out;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
/**
|
|
35
|
-
function
|
|
36
|
-
return user.split("@", 1)[0]?.trim().toLowerCase() ?? "";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {string} hostHeader
|
|
41
|
-
* @param {string} user
|
|
42
|
-
*/
|
|
43
|
-
function terminusFromHost(hostHeader, user) {
|
|
44
|
-
const host = hostHeader.trim().split(":")[0]?.toLowerCase() ?? "";
|
|
45
|
-
const parts = host.split(".");
|
|
46
|
-
if (parts.length < 3) return "";
|
|
47
|
-
const terminus = parts.slice(1).join(".");
|
|
48
|
-
const label = userLabel(user);
|
|
49
|
-
if (!label || terminus.split(".", 1)[0] !== label) return "";
|
|
50
|
-
return terminus;
|
|
27
|
+
/** Username without domain; Olares may send `user` or `user@olares.com`. */
|
|
28
|
+
export function olaresUsername(user) {
|
|
29
|
+
return String(user || "").split("@", 1)[0]?.trim().toLowerCase() ?? "";
|
|
51
30
|
}
|
|
52
31
|
|
|
53
32
|
/**
|
|
54
33
|
* @param {import('node:http').IncomingHttpHeaders | Headers | Record<string, string | string[] | undefined>} headers
|
|
55
|
-
* @param {string | null} [userDomain]
|
|
56
34
|
* @returns {OlaresIdentity}
|
|
57
35
|
*/
|
|
58
|
-
export function identityFromHeaders(headers
|
|
36
|
+
export function identityFromHeaders(headers) {
|
|
59
37
|
const get = (name) => {
|
|
60
38
|
if (typeof Headers !== "undefined" && headers instanceof Headers) {
|
|
61
39
|
return headers.get(name) ?? "";
|
|
@@ -85,106 +63,5 @@ export function identityFromHeaders(headers, userDomain) {
|
|
|
85
63
|
}
|
|
86
64
|
if (!token) token = get("x-authorization").trim();
|
|
87
65
|
|
|
88
|
-
|
|
89
|
-
if (!terminus) terminus = terminusFromHost(get("x-forwarded-host"), user);
|
|
90
|
-
if (!terminus && user) terminus = `${userLabel(user)}.olares.com`;
|
|
91
|
-
|
|
92
|
-
return { user, token, terminus };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** @param {string} account */
|
|
96
|
-
function safeFileName(account) {
|
|
97
|
-
return `${account.replace(/[^a-zA-Z0-9._-]/g, "_")}.enc`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @param {string} plaintext
|
|
102
|
-
* @param {Buffer} key
|
|
103
|
-
*/
|
|
104
|
-
function encryptAesGcm(plaintext, key) {
|
|
105
|
-
const iv = randomBytes(IV_BYTES);
|
|
106
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
107
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
108
|
-
const tag = cipher.getAuthTag();
|
|
109
|
-
return Buffer.concat([iv, ciphertext, tag]);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** @param {OlaresIdentity} identity */
|
|
113
|
-
function ownerHome(identity) {
|
|
114
|
-
const root = (
|
|
115
|
-
process.env.LARES_CLI_ROOT
|
|
116
|
-
?? process.env.OLARES_CLI_ROOT
|
|
117
|
-
?? "/data/cli"
|
|
118
|
-
).replace(/\/+$/, "");
|
|
119
|
-
const label = userLabel(identity.user) || "anonymous";
|
|
120
|
-
return join(root, label);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Materialize per-user olares-cli profile + Linux file keychain from the edge
|
|
125
|
-
* access token so bash/`olares-cli` authenticates without interactive login.
|
|
126
|
-
*
|
|
127
|
-
* @param {OlaresIdentity} identity
|
|
128
|
-
*/
|
|
129
|
-
export function ensureCliProfile(identity) {
|
|
130
|
-
if (!identityAvailable(identity)) {
|
|
131
|
-
throw new Error("Olares edge identity incomplete (need user + auth_token + terminus)");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const home = ownerHome(identity);
|
|
135
|
-
const configDir = join(home, ".olares-cli");
|
|
136
|
-
const dataDir = join(home, "keychain");
|
|
137
|
-
const serviceDir = join(dataDir, SERVICE);
|
|
138
|
-
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
139
|
-
mkdirSync(serviceDir, { recursive: true, mode: 0o700 });
|
|
140
|
-
|
|
141
|
-
const configPath = join(configDir, "config.json");
|
|
142
|
-
const config = {
|
|
143
|
-
currentProfile: identity.user,
|
|
144
|
-
profiles: [
|
|
145
|
-
{
|
|
146
|
-
olaresId: identity.user,
|
|
147
|
-
backendVersion: "1.12.7",
|
|
148
|
-
backendVersionRefreshedAt: Math.floor(Date.now() / 1000),
|
|
149
|
-
},
|
|
150
|
-
],
|
|
151
|
-
};
|
|
152
|
-
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
153
|
-
|
|
154
|
-
const masterPath = join(serviceDir, "master.key");
|
|
155
|
-
let master;
|
|
156
|
-
if (existsSync(masterPath)) {
|
|
157
|
-
master = readFileSync(masterPath);
|
|
158
|
-
if (master.length !== MASTER_KEY_BYTES) {
|
|
159
|
-
master = randomBytes(MASTER_KEY_BYTES);
|
|
160
|
-
writeFileSync(masterPath, master, { mode: 0o600 });
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
master = randomBytes(MASTER_KEY_BYTES);
|
|
164
|
-
writeFileSync(masterPath, master, { mode: 0o600 });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const stored = {
|
|
168
|
-
olaresId: identity.user,
|
|
169
|
-
accessToken: identity.token,
|
|
170
|
-
refreshToken: "",
|
|
171
|
-
grantedAt: Date.now(),
|
|
172
|
-
invalidatedAt: 0,
|
|
173
|
-
};
|
|
174
|
-
const enc = encryptAesGcm(JSON.stringify(stored), master);
|
|
175
|
-
const tokenPath = join(serviceDir, safeFileName(identity.user));
|
|
176
|
-
const tmp = `${tokenPath}.${process.pid}.tmp`;
|
|
177
|
-
writeFileSync(tmp, enc, { mode: 0o600 });
|
|
178
|
-
renameSync(tmp, tokenPath);
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
home,
|
|
182
|
-
dataDir,
|
|
183
|
-
env: {
|
|
184
|
-
HOME: home,
|
|
185
|
-
OLARES_CLI_HOME: configDir,
|
|
186
|
-
OLARES_CLI_DATA_DIR: dataDir,
|
|
187
|
-
OLARES_CLI_REMOTE_ONLY: "1",
|
|
188
|
-
},
|
|
189
|
-
};
|
|
66
|
+
return { user, token };
|
|
190
67
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ensureCliProfile,
|
|
3
|
-
identityAvailable,
|
|
4
|
-
} from "./identity.js";
|
|
1
|
+
import { olaresUsername } from "./identity.js";
|
|
5
2
|
|
|
6
3
|
/** @type {Map<string, import('./identity.js').OlaresIdentity>} */
|
|
7
4
|
const bySession = new Map();
|
|
@@ -10,16 +7,21 @@ const bySession = new Map();
|
|
|
10
7
|
let latest = null;
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* The Router end user for calls that carry no request of their own. The
|
|
11
|
+
* olares-cli session is not set up here: it comes from the credential
|
|
12
|
+
* app-service mounts, which already names the full Olares ID.
|
|
13
|
+
*
|
|
15
14
|
* @param {import('./identity.js').OlaresIdentity} identity
|
|
16
15
|
*/
|
|
16
|
+
function applyIdentityEnv(identity) {
|
|
17
|
+
const user = olaresUsername(identity.user);
|
|
18
|
+
if (user) process.env.OLARES_USERNAME = user;
|
|
19
|
+
}
|
|
20
|
+
|
|
17
21
|
export function rememberSessionIdentity(sessionId, identity) {
|
|
18
22
|
if (sessionId) bySession.set(sessionId, identity);
|
|
19
23
|
latest = identity;
|
|
20
|
-
|
|
21
|
-
const profile = ensureCliProfile(identity);
|
|
22
|
-
Object.assign(process.env, profile.env);
|
|
24
|
+
applyIdentityEnv(identity);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -28,9 +30,7 @@ export function rememberSessionIdentity(sessionId, identity) {
|
|
|
28
30
|
*/
|
|
29
31
|
export function rememberRequestIdentity(identity) {
|
|
30
32
|
latest = identity;
|
|
31
|
-
|
|
32
|
-
const profile = ensureCliProfile(identity);
|
|
33
|
-
Object.assign(process.env, profile.env);
|
|
33
|
+
applyIdentityEnv(identity);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/** @param {string} sessionId */
|
package/olares/trusted-host.js
CHANGED
|
@@ -1,32 +1,3 @@
|
|
|
1
|
-
export function trustedEntranceHosts(env = process.env) {
|
|
2
|
-
return (env.DSH_TRUSTED_HOSTS ?? "")
|
|
3
|
-
.split(",")
|
|
4
|
-
.map((s) => s.trim().toLowerCase())
|
|
5
|
-
.filter(Boolean);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Hostname of a bare `host[:port]` authority, or null when unparsable. */
|
|
9
|
-
export function hostnameOf(authority) {
|
|
10
|
-
try {
|
|
11
|
-
return new URL(`http://${authority}`).hostname.toLowerCase();
|
|
12
|
-
} catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function viaOlaresEntrance(hostHeader, entranceHosts) {
|
|
18
|
-
if (!hostHeader) return false;
|
|
19
|
-
const hostname = hostnameOf(hostHeader);
|
|
20
|
-
if (!hostname) return false;
|
|
21
|
-
return entranceHosts.some((entry) => hostnameOf(entry) === hostname);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function shouldRewriteApiLoopback(req, entranceHosts) {
|
|
25
|
-
const path = new URL(req.url ?? "/", "http://x").pathname;
|
|
26
|
-
if (!path.startsWith("/api")) return false;
|
|
27
|
-
return viaOlaresEntrance(req.headers.host, entranceHosts);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
1
|
export function loopbackAuthority(port) {
|
|
31
2
|
return `127.0.0.1:${port}`;
|
|
32
3
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olares/lares-core",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.9",
|
|
4
4
|
"description": "Lares 共享业务核心:Router、workspace、文件/媒体、Olares 身份(无 UI、无平台壳)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,23 +15,27 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"brand",
|
|
17
17
|
"drive",
|
|
18
|
+
"entrance",
|
|
18
19
|
"files",
|
|
19
20
|
"i18n",
|
|
20
21
|
"icons",
|
|
21
22
|
"larepass",
|
|
22
23
|
"media",
|
|
24
|
+
"mcp",
|
|
23
25
|
"olares",
|
|
24
26
|
"router",
|
|
25
27
|
"search",
|
|
26
28
|
"tools",
|
|
27
29
|
"voice",
|
|
28
|
-
"workspace"
|
|
30
|
+
"workspace",
|
|
31
|
+
"skills"
|
|
29
32
|
],
|
|
30
33
|
"exports": {
|
|
31
34
|
"./tools/http": "./tools/http.js",
|
|
32
35
|
"./tools/json-file": "./tools/json-file.js",
|
|
33
36
|
"./tools/async": "./tools/async.js",
|
|
34
37
|
"./tools/crypto-uuid": "./tools/crypto-uuid.js",
|
|
38
|
+
"./tools/head-script": "./tools/head-script.js",
|
|
35
39
|
"./brand/identity": "./brand/identity.js",
|
|
36
40
|
"./brand/manifest": "./brand/manifest.js",
|
|
37
41
|
"./icons/mark": "./icons/mark.js",
|
|
@@ -41,8 +45,10 @@
|
|
|
41
45
|
"./i18n/files": "./i18n/files.js",
|
|
42
46
|
"./i18n/voice": "./i18n/voice.js",
|
|
43
47
|
"./i18n/search": "./i18n/search.js",
|
|
48
|
+
"./i18n/skills": "./i18n/skills.js",
|
|
44
49
|
"./i18n/preview": "./i18n/preview.js",
|
|
45
50
|
"./i18n/mobile": "./i18n/mobile.js",
|
|
51
|
+
"./i18n/mcp": "./i18n/mcp.js",
|
|
46
52
|
"./router/catalog": "./router/catalog.js",
|
|
47
53
|
"./router/catalog-cache": {
|
|
48
54
|
"types": "./router/catalog-cache.d.ts",
|
|
@@ -55,7 +61,11 @@
|
|
|
55
61
|
},
|
|
56
62
|
"./router/search": "./router/search.js",
|
|
57
63
|
"./router/shim": "./router/shim.js",
|
|
64
|
+
"./router/gemini-schema": "./router/gemini-schema.js",
|
|
65
|
+
"./router/generation-guard": "./router/generation-guard.js",
|
|
66
|
+
"./router/sref": "./router/sref.js",
|
|
58
67
|
"./router/stt": "./router/stt.js",
|
|
68
|
+
"./router/tts": "./router/tts.js",
|
|
59
69
|
"./router/catalog-sync": "./router/catalog-sync.js",
|
|
60
70
|
"./router/session-model": "./router/session-model.js",
|
|
61
71
|
"./workspace/env": "./workspace/env.js",
|
|
@@ -69,13 +79,19 @@
|
|
|
69
79
|
"./files/upload-http": "./files/upload-http.js",
|
|
70
80
|
"./files/limits": "./files/limits.js",
|
|
71
81
|
"./files/upload-client": "./files/upload-client.js",
|
|
82
|
+
"./files/backend-client": "./files/backend-client.js",
|
|
83
|
+
"./files/app-link": "./files/app-link.js",
|
|
72
84
|
"./files/preview": "./files/preview.js",
|
|
85
|
+
"./files/preview-local": "./files/preview-local.js",
|
|
73
86
|
"./files/preview-workspace": "./files/preview-workspace.js",
|
|
74
87
|
"./files/preview-tabs": "./files/preview-tabs.js",
|
|
75
88
|
"./files/preview-groups": "./files/preview-groups.js",
|
|
76
|
-
"./files/preview-cache": "./files/preview-cache.js",
|
|
77
89
|
"./files/markdown": "./files/markdown.js",
|
|
78
90
|
"./files/deliverables": "./files/deliverables.js",
|
|
91
|
+
"./files/published-tools": "./files/published-tools.js",
|
|
92
|
+
"./files/published-turn-data": "./files/published-turn-data.js",
|
|
93
|
+
"./files/produced-gate": "./files/produced-gate.js",
|
|
94
|
+
"./files/produced-mentions": "./files/produced-mentions.js",
|
|
79
95
|
"./files/disposition": "./files/disposition.js",
|
|
80
96
|
"./files/intake": "./files/intake.js",
|
|
81
97
|
"./files/draft-images": "./files/draft-images.js",
|
|
@@ -84,6 +100,8 @@
|
|
|
84
100
|
"./drive/paths": "./drive/paths.js",
|
|
85
101
|
"./drive/download": "./drive/download.js",
|
|
86
102
|
"./drive/ls": "./drive/ls.js",
|
|
103
|
+
"./drive/cat": "./drive/cat.js",
|
|
104
|
+
"./drive/flowstudio-files": "./drive/flowstudio-files.js",
|
|
87
105
|
"./drive/url-download": "./drive/url-download.js",
|
|
88
106
|
"./drive/execute": "./drive/execute.js",
|
|
89
107
|
"./drive/tools": "./drive/tools.js",
|
|
@@ -92,15 +110,34 @@
|
|
|
92
110
|
"./voice/service": "./voice/service.js",
|
|
93
111
|
"./voice/client": "./voice/client.js",
|
|
94
112
|
"./voice/recorder": "./voice/recorder.js",
|
|
113
|
+
"./voice/endpoint": "./voice/endpoint.js",
|
|
114
|
+
"./voice/speech": "./voice/speech.js",
|
|
115
|
+
"./voice/script": "./voice/script.js",
|
|
116
|
+
"./voice/pcm": "./voice/pcm.js",
|
|
117
|
+
"./voice/link": "./voice/link.js",
|
|
118
|
+
"./voice/session": "./voice/session.js",
|
|
119
|
+
"./voice/duplex": "./voice/duplex.js",
|
|
120
|
+
"./voice/input": "./voice/input.js",
|
|
121
|
+
"./voice/playback": "./voice/playback.js",
|
|
122
|
+
"./voice/problem": "./voice/problem.js",
|
|
123
|
+
"./voice/capture": "./voice/capture.js",
|
|
124
|
+
"./voice/install": "./voice/install.js",
|
|
95
125
|
"./search/config": "./search/config.js",
|
|
96
126
|
"./search/menu": "./search/menu.js",
|
|
97
127
|
"./search/client": "./search/client.js",
|
|
98
128
|
"./router/models-client": "./router/models-client.js",
|
|
99
129
|
"./media/ffmpeg-run": "./media/ffmpeg-run.js",
|
|
100
130
|
"./media/router-images": "./media/router-images.js",
|
|
131
|
+
"./media/generation": "./media/generation.js",
|
|
132
|
+
"./media/tool": "./media/tool.js",
|
|
133
|
+
"./mcp/config": "./mcp/config.js",
|
|
134
|
+
"./mcp/client": "./mcp/client.js",
|
|
135
|
+
"./mcp/approval": "./mcp/approval.js",
|
|
101
136
|
"./olares/identity": "./olares/identity.js",
|
|
102
137
|
"./olares/session-identity": "./olares/session-identity.js",
|
|
103
138
|
"./olares/entrance": "./olares/entrance.js",
|
|
139
|
+
"./entrance/surface": "./entrance/surface.js",
|
|
140
|
+
"./log/report": "./log/report.js",
|
|
104
141
|
"./olares/trusted-host": "./olares/trusted-host.js",
|
|
105
142
|
"./larepass/host": "./larepass/host.js",
|
|
106
143
|
"./larepass/rpc": "./larepass/rpc.js",
|
|
@@ -108,6 +145,7 @@
|
|
|
108
145
|
"./larepass/tool-row": "./larepass/tool-row.js",
|
|
109
146
|
"./larepass/stage-copy": "./larepass/stage-copy.js",
|
|
110
147
|
"./larepass/transcript": "./larepass/transcript.js",
|
|
148
|
+
"./larepass/process-fold": "./larepass/process-fold.js",
|
|
111
149
|
"./larepass/chat": "./larepass/chat.js",
|
|
112
150
|
"./larepass/mux": "./larepass/mux.js",
|
|
113
151
|
"./larepass/session-cache": "./larepass/session-cache.js",
|
|
@@ -122,6 +160,12 @@
|
|
|
122
160
|
"./larepass/references": "./larepass/references.js",
|
|
123
161
|
"./larepass/submission-settings": "./larepass/submission-settings.js",
|
|
124
162
|
"./larepass/turn-metrics": "./larepass/turn-metrics.js",
|
|
125
|
-
"./larepass/context-occupancy": "./larepass/context-occupancy.js"
|
|
163
|
+
"./larepass/context-occupancy": "./larepass/context-occupancy.js",
|
|
164
|
+
"./skills/packs": "./skills/packs.js",
|
|
165
|
+
"./skills/state": {
|
|
166
|
+
"types": "./skills/state.d.ts",
|
|
167
|
+
"default": "./skills/state.js"
|
|
168
|
+
},
|
|
169
|
+
"./skills/client": "./skills/client.js"
|
|
126
170
|
}
|
|
127
171
|
}
|
|
@@ -23,7 +23,7 @@ export class CatalogCache {
|
|
|
23
23
|
seed(payload: unknown): void;
|
|
24
24
|
snapshot(): CatalogSnapshot;
|
|
25
25
|
restore(snap: CatalogSnapshot | { payload: unknown; rows?: RouterCatalogRow[]; fetchedAt?: number } | null): void;
|
|
26
|
-
get(): Promise<CatalogSnapshot>;
|
|
26
|
+
get(options?: { refresh?: boolean }): Promise<CatalogSnapshot>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export const catalogCache: CatalogCache;
|
package/router/catalog-cache.js
CHANGED
|
@@ -56,8 +56,32 @@ export class CatalogCache {
|
|
|
56
56
|
this.primed = false;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Marking the seed consumed is the point: a caller that invalidates wants
|
|
61
|
+
* Router, and the boot seed would otherwise satisfy the very first reload
|
|
62
|
+
* of a fresh dsh child with the state the caller just declared stale.
|
|
63
|
+
*/
|
|
59
64
|
invalidate() {
|
|
60
65
|
this.fetchedAt = 0;
|
|
66
|
+
this.primed = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {{ refresh?: boolean }} [options]
|
|
71
|
+
* `refresh` skips TTL and the boot seed so a user click hits Router now.
|
|
72
|
+
*/
|
|
73
|
+
async get(options = {}) {
|
|
74
|
+
if (options.refresh) this.invalidate();
|
|
75
|
+
if (this.payload && this.fetchedAt !== 0 && this.now() - this.fetchedAt < this.ttlMs) {
|
|
76
|
+
return this.snapshot();
|
|
77
|
+
}
|
|
78
|
+
if (this.inflight) return this.inflight;
|
|
79
|
+
const pending = this.load();
|
|
80
|
+
const shared = pending.finally(() => {
|
|
81
|
+
if (this.inflight === shared) this.inflight = null;
|
|
82
|
+
});
|
|
83
|
+
this.inflight = shared;
|
|
84
|
+
return shared;
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
/**
|
|
@@ -92,19 +116,6 @@ export class CatalogCache {
|
|
|
92
116
|
writeCatalogSeed(snap.payload, this.dataDir);
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
async get() {
|
|
96
|
-
if (this.payload && this.fetchedAt !== 0 && this.now() - this.fetchedAt < this.ttlMs) {
|
|
97
|
-
return this.snapshot();
|
|
98
|
-
}
|
|
99
|
-
if (this.inflight) return this.inflight;
|
|
100
|
-
const pending = this.load();
|
|
101
|
-
const shared = pending.finally(() => {
|
|
102
|
-
if (this.inflight === shared) this.inflight = null;
|
|
103
|
-
});
|
|
104
|
-
this.inflight = shared;
|
|
105
|
-
return shared;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
119
|
async load() {
|
|
109
120
|
if (!this.primed) {
|
|
110
121
|
this.primed = true;
|
|
@@ -114,7 +125,7 @@ export class CatalogCache {
|
|
|
114
125
|
return this.snapshot();
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
|
-
const response = await this.fetchImpl(`${routerGatewayUrl()}/models
|
|
128
|
+
const response = await this.fetchImpl(`${routerGatewayUrl()}/models`, {
|
|
118
129
|
method: "GET",
|
|
119
130
|
headers: routerHeaders(),
|
|
120
131
|
signal: AbortSignal.timeout(15_000),
|