@olares/lares-core 0.15.7 → 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 +96 -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/README.md
CHANGED
|
@@ -11,8 +11,9 @@ Import surfaces (`@olares/lares-core/<export>`):
|
|
|
11
11
|
- `workspace/*`: workspace boundaries, default seed, and dsh session-to-workspace resolution.
|
|
12
12
|
- `files/*`: intake, preview workspace, markdown rewrite, Host upload/preview HTTP.
|
|
13
13
|
- `drive/*`: path policy, tool execution, present cards, and agent tool definitions.
|
|
14
|
+
- `media/*`: ffmpeg encodes, Router-bound image re-encoding, and `media_generate`: submit, wait, publish, and the session ledger that recovers a generation an earlier turn never saw finish.
|
|
14
15
|
- `voice/*`, `search/*`: STT/search settings, Host payloads, and client API contracts.
|
|
15
|
-
- `olares/*`: Olares entrance identity
|
|
16
|
+
- `olares/*`: Olares entrance identity and trusted-host loopback. The olares-cli session is not one of them: it comes from the credential app-service mounts.
|
|
16
17
|
- `larepass/host`: find the Lares entrance from LarePass `myApps`, PC-test proxy URLs, Host probe, `hostConfigFromEnv` / `hostTarget` / `hostKey`, and `laresPortsFromAccount`.
|
|
17
18
|
- `larepass/rpc`, `larepass/transcript`, `larepass/context-provenance`, `larepass/tool-row`, `larepass/stage-copy`, `larepass/chat`, `larepass/mux`, `larepass/session-cache`, `larepass/runtime`, `larepass/settings`: dsh host-RPC envelope, session timeline fold (user / context / reasoning / tool / retry / files; same classification as dsh conversation nodes), English stage chrome copy and tool-row variants, browser mux as WebSocket (same frames as PC), per-session page cache (switch reads cache; history/mux write only on change), the LarePass chat runtime (UI only subscribes), and Host HTTP for model / voice / search settings.
|
|
18
19
|
- `brand/identity`, `brand/manifest`: product identity and PWA manifest consumed by the Host and clients.
|
package/brand/identity.js
CHANGED
|
@@ -19,12 +19,13 @@ export function identityPrompt() {
|
|
|
19
19
|
return [
|
|
20
20
|
`You are ${PRODUCT_NAME}, a helpful assistant running on ${PLATFORM_NAME}.`,
|
|
21
21
|
`Prefer olares-cli for ${PLATFORM_NAME} platform tasks when skills apply.`,
|
|
22
|
-
`olares-cli is on PATH
|
|
23
|
-
"For image, video, audio, or 3D generation and
|
|
22
|
+
`olares-cli is on PATH and already signed in as the user: ${PLATFORM_NAME} mounts this application's credential into the container. Never run profile login or profile import — a platform-issued profile refuses both.`,
|
|
23
|
+
"For image, video, audio, or 3D generation, creation, or image editing — including FlowStudio scenes and workflows (T2V, I2V, R2V) — follow the lares-media-create skill: list that family on Router, then call media_generate, which waits for the result and publishes it. Do not curl FlowStudio. Do not diagnose Router or sync catalogs unless that list is empty.",
|
|
24
24
|
"Use read/write/edit for files; use background jobs for long shell work.",
|
|
25
25
|
"User-visible files belong in the session workspace (cwd) as relative paths. Do not write them under /app: that is the application overlay, and the chat preview cannot open it.",
|
|
26
26
|
"For the current date or time, run `date`; the process TZ is the user's configured Olares time zone.",
|
|
27
27
|
"Images the user attaches are already in context; read_image is only for image files that exist on disk, never for an attachment.",
|
|
28
|
+
"Write every user-visible message — progress updates as well as the final answer — in the language of the user's latest message, whatever language tool output, skills, or errors are in.",
|
|
28
29
|
`If asked who you are, answer as ${PRODUCT_NAME} on ${PLATFORM_NAME}.`,
|
|
29
30
|
].join(" ");
|
|
30
31
|
}
|
|
@@ -49,7 +50,8 @@ export function agentsMarkdown() {
|
|
|
49
50
|
You are helping inside an ${PLATFORM_NAME} workspace via ${PRODUCT_NAME}.
|
|
50
51
|
|
|
51
52
|
- Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
|
|
52
|
-
-
|
|
53
|
+
- olares-cli is already signed in as the user from the credential ${PLATFORM_NAME} mounts into this container. Never run \`profile login\` or \`profile import\`; a platform-issued profile refuses both.
|
|
54
|
+
- For image / video / audio / 3D generation, including FlowStudio scenes and workflows, follow the lares-media-create skill: list that family, POST the matching Router shim route, land the file. Do not curl FlowStudio.
|
|
53
55
|
- Stay inside the workspace for file edits unless the user explicitly asks otherwise.
|
|
54
56
|
- Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
|
|
55
57
|
- Prefer structured fs tools (read / write / edit) over shell for file work.
|
|
@@ -65,9 +67,36 @@ export const LEGACY_AGENTS_SEEDS = [
|
|
|
65
67
|
|
|
66
68
|
You are helping inside an Olares workspace via Lares.
|
|
67
69
|
|
|
70
|
+
- Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
|
|
71
|
+
- For image / video / audio / 3D generation, including FlowStudio scenes and workflows, follow the lares-media-create skill: list that family, POST the matching Router shim route, land the file. Do not curl FlowStudio.
|
|
72
|
+
- Stay inside the workspace for file edits unless the user explicitly asks otherwise.
|
|
73
|
+
- Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
|
|
74
|
+
- Prefer structured fs tools (read / write / edit) over shell for file work.
|
|
75
|
+
- Long-running shell work can use background jobs; check results with job_output.
|
|
76
|
+
- \`@path\` in a user message is workspace-relative, not absolute; \`/id\` names a skill.
|
|
77
|
+
- If asked who you are, you are Lares on Olares.
|
|
78
|
+
`,
|
|
79
|
+
`# AGENTS.md
|
|
80
|
+
|
|
81
|
+
You are helping inside an Olares workspace via Lares.
|
|
82
|
+
|
|
83
|
+
- Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
|
|
84
|
+
- For image / video / audio / 3D generation, follow the lares-media-create skill: list that family, call Router, land the file.
|
|
85
|
+
- Stay inside the workspace for file edits unless the user explicitly asks otherwise.
|
|
86
|
+
- Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
|
|
87
|
+
- Prefer structured fs tools (read / write / edit) over shell for file work.
|
|
88
|
+
- Long-running shell work can use background jobs; check results with job_output.
|
|
89
|
+
- \`@path\` in a user message is workspace-relative, not absolute; \`/id\` names a skill.
|
|
90
|
+
- If asked who you are, you are Lares on Olares.
|
|
91
|
+
`,
|
|
92
|
+
`# AGENTS.md
|
|
93
|
+
|
|
94
|
+
You are helping inside an Olares workspace via Lares.
|
|
95
|
+
|
|
68
96
|
- Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
|
|
69
97
|
- For image / video / audio / 3D generation, follow the lares-media-create skill.
|
|
70
98
|
- Stay inside the workspace for file edits unless the user explicitly asks otherwise.
|
|
99
|
+
- Write files the user should open as workspace-relative paths (session cwd). \`/app\` is the running application and is not previewable.
|
|
71
100
|
- Prefer structured fs tools (read / write / edit) over shell for file work.
|
|
72
101
|
- Long-running shell work can use background jobs; check results with job_output.
|
|
73
102
|
- \`@path\` in a user message is workspace-relative, not absolute; \`/id\` names a skill.
|
|
@@ -78,6 +107,7 @@ You are helping inside an Olares workspace via Lares.
|
|
|
78
107
|
You are helping inside an Olares workspace via Lares.
|
|
79
108
|
|
|
80
109
|
- Prefer olares-cli skills for platform tasks (market, cluster, files, router, …).
|
|
110
|
+
- For image / video / audio / 3D generation, follow the lares-media-create skill.
|
|
81
111
|
- Stay inside the workspace for file edits unless the user explicitly asks otherwise.
|
|
82
112
|
- Prefer structured fs tools (read / write / edit) over shell for file work.
|
|
83
113
|
- Long-running shell work can use background jobs; check results with job_output.
|
package/drive/cat.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { parseFilesPath } from "./files-path.js";
|
|
3
|
+
|
|
4
|
+
const STDERR_LIMIT = 2000;
|
|
5
|
+
const STDOUT_LIMIT = 64 * 1024;
|
|
6
|
+
const CAT_TIMEOUT_MS = 8_000;
|
|
7
|
+
|
|
8
|
+
function lookupError(code, message) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read one files-backend file as UTF-8 text (`olares-cli files cat`).
|
|
16
|
+
* Identity is already in `process.env`.
|
|
17
|
+
* @param spawnFn - seam for tests; the real spawn otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export function runOlaresCat(source, options = {}) {
|
|
20
|
+
const path = parseFilesPath(source);
|
|
21
|
+
const { spawnFn = spawn } = options;
|
|
22
|
+
const timeout = AbortSignal.timeout(CAT_TIMEOUT_MS);
|
|
23
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawnFn("olares-cli", ["files", "cat", path], {
|
|
26
|
+
env: process.env,
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
...(signal ? { signal } : {}),
|
|
29
|
+
});
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
let truncated = false;
|
|
33
|
+
child.stdout?.on("data", (chunk) => {
|
|
34
|
+
if (truncated) return;
|
|
35
|
+
stdout += chunk;
|
|
36
|
+
if (stdout.length > STDOUT_LIMIT) {
|
|
37
|
+
truncated = true;
|
|
38
|
+
stdout = "";
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
child.stderr?.on("data", (chunk) => {
|
|
42
|
+
stderr = `${stderr}${chunk}`.slice(-STDERR_LIMIT);
|
|
43
|
+
});
|
|
44
|
+
child.on("error", (error) => {
|
|
45
|
+
reject(lookupError("files_unavailable", `olares-cli files cat failed: ${error.message}`));
|
|
46
|
+
});
|
|
47
|
+
child.on("close", (code) => {
|
|
48
|
+
if (truncated) {
|
|
49
|
+
reject(lookupError("files_unavailable", "olares-cli files cat output exceeded the read limit"));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (code !== 0) {
|
|
53
|
+
reject(lookupError(
|
|
54
|
+
"files_unavailable",
|
|
55
|
+
`olares-cli files cat exited ${code}: ${stderr.trim() || "no output"}`,
|
|
56
|
+
));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
resolve(stdout);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
package/drive/download.js
CHANGED
|
@@ -6,9 +6,9 @@ const STDERR_LIMIT = 2000;
|
|
|
6
6
|
/**
|
|
7
7
|
* Stream one files-backend file onto local disk.
|
|
8
8
|
*
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The CLI credential is the one app-service mounts into this container, not
|
|
10
|
+
* anything this process sets up, so the child inherits `process.env` and
|
|
11
|
+
* resolves the platform-issued profile the same way the bash tool does.
|
|
12
12
|
* @param spawnFn - seam for tests; the real spawn otherwise.
|
|
13
13
|
*/
|
|
14
14
|
export function runOlaresDownload(source, absolutePath, options = {}) {
|
package/drive/execute.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../workspace/path.js";
|
|
10
10
|
import { runOlaresDownload } from "./download.js";
|
|
11
11
|
import { statFilesFile } from "./ls.js";
|
|
12
|
+
import { isMediaDeliverablePath } from "../files/preview-groups.js";
|
|
12
13
|
import {
|
|
13
14
|
describeFetch,
|
|
14
15
|
describeFfmpegEncode,
|
|
@@ -21,6 +22,11 @@ import {
|
|
|
21
22
|
} from "./paths.js";
|
|
22
23
|
import { downloadUrl, saveDataUrl } from "./url-download.js";
|
|
23
24
|
|
|
25
|
+
/** Only image / video / audio / 3D fetches become Produced; research files do not. */
|
|
26
|
+
function producedFetchPath(destination) {
|
|
27
|
+
return destination && isMediaDeliverablePath(destination) ? destination : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
export async function executeDriveFetch(args, exec, download) {
|
|
25
31
|
const run = download ?? runOlaresDownload;
|
|
26
32
|
const { source, destination } = resolveFetch(args);
|
|
@@ -110,7 +116,7 @@ export function presentDriveFetch(args) {
|
|
|
110
116
|
const fetched = describeFetch(args);
|
|
111
117
|
return producedEditCard(
|
|
112
118
|
`Fetch ${fetched?.source ?? String(args.path ?? "")}`,
|
|
113
|
-
fetched?.destination,
|
|
119
|
+
producedFetchPath(fetched?.destination),
|
|
114
120
|
);
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -120,7 +126,7 @@ export function presentUrlFetch(args) {
|
|
|
120
126
|
fetched?.kind === "data"
|
|
121
127
|
? `Save ${fetched.destination}`
|
|
122
128
|
: `Download ${fetched?.source ?? String(args.url ?? "").slice(0, 160)}`,
|
|
123
|
-
fetched?.destination,
|
|
129
|
+
producedFetchPath(fetched?.destination),
|
|
124
130
|
);
|
|
125
131
|
}
|
|
126
132
|
|
package/drive/files-path.js
CHANGED
|
@@ -53,3 +53,36 @@ export function isFilesPath(value) {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A downloadable Files namespace, including directories and other addresses
|
|
59
|
+
* `parseFilesPath` rejects. Preview origin dispatch uses this so a Files
|
|
60
|
+
* folder never falls through to a workspace 404.
|
|
61
|
+
*/
|
|
62
|
+
export function isFilesNamespace(value) {
|
|
63
|
+
const source = canonicalizeFilesAddress(value);
|
|
64
|
+
if (source === "" || source.includes("\0")) return false;
|
|
65
|
+
if (source.startsWith("/") || /^[a-z]+:/i.test(source)) return false;
|
|
66
|
+
return DOWNLOADABLE.has(source.split("/")[0]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* dsh ChatView joins a relative path onto the session cwd before
|
|
71
|
+
* `workspaces.openPath`. A Files address is relative in its own namespace, so
|
|
72
|
+
* it arrives as `$cwd/drive/Home/…`. The suffix after `prefix` is that address
|
|
73
|
+
* when it belongs to a downloadable namespace — a directory stays Files, too.
|
|
74
|
+
*/
|
|
75
|
+
export function filesPathAfterPrefix(prefix, requestedPath) {
|
|
76
|
+
if (typeof prefix !== "string" || !prefix.trim()) return null;
|
|
77
|
+
if (typeof requestedPath !== "string" || !requestedPath) return null;
|
|
78
|
+
const root = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
79
|
+
const joined = requestedPath.replace(/\\/g, "/");
|
|
80
|
+
if (root === "" || !joined.startsWith(`${root}/`)) return null;
|
|
81
|
+
const rest = joined.slice(root.length + 1);
|
|
82
|
+
if (!isFilesNamespace(rest)) return null;
|
|
83
|
+
try {
|
|
84
|
+
return parseFilesPath(rest);
|
|
85
|
+
} catch {
|
|
86
|
+
return canonicalizeFilesAddress(rest);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { runOlaresCat } from "./cat.js";
|
|
2
|
+
|
|
3
|
+
/** Files-backend prefix of FlowStudio appData userData. */
|
|
4
|
+
export const FLOWSTUDIO_FILES_PREFIX = "drive/Data/flowstudio/userData";
|
|
5
|
+
|
|
6
|
+
function safeSegment(value) {
|
|
7
|
+
const text = String(value ?? "").trim();
|
|
8
|
+
if (!text || text === "." || text === "..") return "";
|
|
9
|
+
if (/[\\/\0]/.test(text)) return "";
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Files address of the one-line pointer FlowStudio writes per JobOutput id.
|
|
15
|
+
* Only needed behind a Router old enough to drop `files_path`; it still
|
|
16
|
+
* forwards `outputs[].id`, which is that UUID.
|
|
17
|
+
*/
|
|
18
|
+
export function flowstudioOutputPointerPath(owner, outputId) {
|
|
19
|
+
const user = safeSegment(owner);
|
|
20
|
+
const id = safeSegment(outputId);
|
|
21
|
+
if (!user || !id) return null;
|
|
22
|
+
return `${FLOWSTUDIO_FILES_PREFIX}/${user}/comfyui/outputs/.by-id/${id}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** First line of a pointer file must be the stored artifact's Files address. */
|
|
26
|
+
export function parseFlowstudioFilesPointer(text) {
|
|
27
|
+
const line = String(text ?? "").trim().split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
28
|
+
if (!line.startsWith(`${FLOWSTUDIO_FILES_PREFIX}/`)) return null;
|
|
29
|
+
if (line.includes("..") || line.includes("\\") || line.includes("\0")) return null;
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveFlowstudioFilesPath(owner, outputId, options = {}) {
|
|
34
|
+
const pointer = flowstudioOutputPointerPath(owner, outputId);
|
|
35
|
+
if (!pointer) return null;
|
|
36
|
+
try {
|
|
37
|
+
const text = await (options.cat ?? runOlaresCat)(pointer, options);
|
|
38
|
+
return parseFlowstudioFilesPointer(text);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Publish a completed generation's outputs at the top level with a
|
|
46
|
+
* `files_path` on each, which is the shape callers read.
|
|
47
|
+
*
|
|
48
|
+
* A current Router carries the address on the wire and there is nothing to
|
|
49
|
+
* resolve. An older one drops it, and the address is then recovered from the
|
|
50
|
+
* Files pointer named by `output.id`; an output with neither is published
|
|
51
|
+
* unchanged rather than held back, since `content_url` still fetches it.
|
|
52
|
+
*
|
|
53
|
+
* Hoisting is the other half. Router nests the FlowStudio adapter's untouched
|
|
54
|
+
* snapshot under `response`, so a FlowStudio job can list its outputs there
|
|
55
|
+
* instead, and those outputs have to reach the top level whether or not this
|
|
56
|
+
* function had anything to add to them.
|
|
57
|
+
*/
|
|
58
|
+
export async function attachFlowstudioFilesPaths(payload, owner, options = {}) {
|
|
59
|
+
if (payload == null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
60
|
+
return payload;
|
|
61
|
+
}
|
|
62
|
+
if (String(payload.status ?? "").toLowerCase() !== "completed") return payload;
|
|
63
|
+
const hoisted = !Array.isArray(payload.outputs);
|
|
64
|
+
const outputs = hoisted
|
|
65
|
+
? (Array.isArray(payload.response?.outputs) ? payload.response.outputs : [])
|
|
66
|
+
: payload.outputs;
|
|
67
|
+
if (outputs.length === 0) return payload;
|
|
68
|
+
let changed = false;
|
|
69
|
+
const next = [];
|
|
70
|
+
for (const output of outputs) {
|
|
71
|
+
if (output == null || typeof output !== "object" || Array.isArray(output)) {
|
|
72
|
+
next.push(output);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// FlowStudio serializes camelCase and Router relays snake_case, so an
|
|
76
|
+
// address can arrive under either name; callers read only the one.
|
|
77
|
+
const onWire = String(output.files_path ?? output.filesPath ?? "").trim();
|
|
78
|
+
const path = onWire || (await resolveFlowstudioFilesPath(owner, output.id, options));
|
|
79
|
+
if (!path || path === output.files_path) {
|
|
80
|
+
next.push(output);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
changed = true;
|
|
84
|
+
next.push({ ...output, files_path: path });
|
|
85
|
+
}
|
|
86
|
+
return changed || hoisted ? { ...payload, outputs: next } : payload;
|
|
87
|
+
}
|
package/drive/ls.js
CHANGED
|
@@ -33,19 +33,19 @@ function itemLeaf(item) {
|
|
|
33
33
|
return parts.at(-1) ?? "";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function isDirectoryItem(item) {
|
|
36
|
+
export function isDirectoryItem(item) {
|
|
37
37
|
if (item?.isDir === true || item?.isDirectory === true) return true;
|
|
38
38
|
const type = String(item?.type ?? "").toLowerCase();
|
|
39
39
|
if (type === "dir" || type === "directory") return true;
|
|
40
40
|
return typeof item?.name === "string" && item.name.endsWith("/");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function itemSize(item) {
|
|
43
|
+
export function itemSize(item) {
|
|
44
44
|
const n = Number(item?.size ?? item?.fileSize);
|
|
45
45
|
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function itemModifiedAt(item) {
|
|
48
|
+
export function itemModifiedAt(item) {
|
|
49
49
|
const t = Date.parse(item?.modified ?? item?.mtime ?? item?.modTime ?? "");
|
|
50
50
|
return Number.isFinite(t) ? t : 0;
|
|
51
51
|
}
|
package/drive/paths.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* An Olares files path (`<fileType>/<extend>/<subPath…>`) names one object on
|
|
3
3
|
* the user's files backend. Preview serves it in place; drive_fetch copies it
|
|
4
4
|
* into the session workspace only when a later edit or transcode needs a local
|
|
5
|
-
* file.
|
|
6
|
-
*
|
|
5
|
+
* file. The durable preview projection is rebuilt from successful persisted
|
|
6
|
+
* tool call/result pairs; `presentCall` only describes the transient tool row.
|
|
7
7
|
*/
|
|
8
8
|
import { posixBasename as basename, posixExtname as extname, sanitizeFilename } from "../files/filename.js";
|
|
9
|
-
import {
|
|
9
|
+
import { isFilesNamespace, parseFilesPath } from "./files-path.js";
|
|
10
10
|
|
|
11
|
-
export { isFilesPath, parseFilesPath } from "./files-path.js";
|
|
11
|
+
export { filesPathAfterPrefix, isFilesNamespace, isFilesPath, parseFilesPath } from "./files-path.js";
|
|
12
12
|
|
|
13
13
|
const DEFAULT_DIRECTORY = "downloads";
|
|
14
14
|
|
|
@@ -55,12 +55,23 @@ export const DRIVE_IMPORT_PROMPT = [
|
|
|
55
55
|
"Olares files paths (drive/…, sync/…, cache/…, external/…, and the cloud-account namespaces)",
|
|
56
56
|
"are previewable in this app. Any file under drive/Home is included — Documents, Pictures, Music,",
|
|
57
57
|
"Movies, Downloads, Code, and the rest — not only Downloads. After knowledge / Wise / yt-dlp /",
|
|
58
|
-
"torrent lands a file there, workspace_publish that files path immediately
|
|
59
|
-
"
|
|
58
|
+
"torrent lands a file there, workspace_publish that files path immediately. Do not copy it into",
|
|
59
|
+
"the workspace only to preview it, and do not close the reply with that path as inline code or a",
|
|
60
|
+
"link — the turn-tail preview already shows it. FlowStudio",
|
|
61
|
+
"generations whose poll JSON includes `files_path` (`drive/Data/flowstudio/…`) are the same",
|
|
62
|
+
"rule: publish that address; do not GET `/content` or copy the file into Home.",
|
|
60
63
|
"Direct public HTTP(S) file URLs and data: URLs use url_fetch; never curl, wget, or shell.",
|
|
61
64
|
"Give destination a meaningful filename with the correct extension whenever the URL path lacks one.",
|
|
65
|
+
"Produced chips only keep durable media from url_fetch / drive_fetch (image, video, audio, 3D).",
|
|
66
|
+
"Research fetches — git/GitHub APIs, chart YAML/JSON, READMEs, and other non-media references —",
|
|
67
|
+
"are workspace scratch (often under downloads/). They are not Produced; delete them when done.",
|
|
68
|
+
"Do not delete image / video / audio / 3D files the user should still open. Final artifacts the",
|
|
69
|
+
"user keeps (charts, app sources, outputs) belong outside downloads/ via write / edit, or via",
|
|
70
|
+
"workspace_publish when they already exist.",
|
|
71
|
+
"Before the turn closes, Lares re-checks durable Produced paths are still openable. If a check",
|
|
72
|
+
"fails, you will be steered to restore or republish — do not end while naming a broken path.",
|
|
62
73
|
"drive_fetch copies one files-backend file into the workspace when a later edit or transcode",
|
|
63
|
-
"(for example ffmpeg_encode) needs it there. url_fetch and ffmpeg_encode already publish
|
|
74
|
+
"(for example ffmpeg_encode) needs it there. url_fetch and ffmpeg_encode already publish media",
|
|
64
75
|
"workspace output. workspace_publish is for a file that already exists in the workspace or on",
|
|
65
76
|
"the files backend that those tools did not produce. Do not call it after drive_fetch, url_fetch,",
|
|
66
77
|
"or ffmpeg_encode. A subagent's own tool calls belong to its child turn: when a subagent reports",
|
|
@@ -72,11 +83,10 @@ export const DRIVE_IMPORT_PROMPT = [
|
|
|
72
83
|
"subtitles into an input video. Do not transcode merely so a file can be previewed: webm, mp4,",
|
|
73
84
|
"images, and audio preview as-is from a files path or after url_fetch. Do not run ffmpeg or ffprobe",
|
|
74
85
|
"in the shell for those jobs. Report the encoder and speed from the tool result.",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"player far below the path.",
|
|
86
|
+
"After workspace_publish (or url_fetch / ffmpeg_encode), stop. The turn-tail preview under the",
|
|
87
|
+
"reply is the surface for produced images, video, audio, and 3D. Do not also write the path as",
|
|
88
|
+
"markdown inline code, a hyperlink, or a trailing file chip: that duplicates the preview. Name the",
|
|
89
|
+
"file in ordinary prose if the user needs the title; never leave a produced path as the last token.",
|
|
80
90
|
].join(" ");
|
|
81
91
|
|
|
82
92
|
function parseDestination(value, source) {
|
|
@@ -193,7 +203,7 @@ export function describeUrlFetch(args) {
|
|
|
193
203
|
|
|
194
204
|
export function resolveWorkspacePublish(args) {
|
|
195
205
|
const raw = String(args?.path ?? "").trim().replace(/\\/g, "/");
|
|
196
|
-
if (
|
|
206
|
+
if (isFilesNamespace(raw)) {
|
|
197
207
|
return { path: parseFilesPath(raw), origin: "files" };
|
|
198
208
|
}
|
|
199
209
|
const segments = raw.split("/");
|
package/drive/tools.js
CHANGED
|
@@ -60,8 +60,10 @@ export function urlFetchDefinition(download) {
|
|
|
60
60
|
description:
|
|
61
61
|
"Download one public HTTP(S) URL, or a data: URL / base64 payload, into the session workspace"
|
|
62
62
|
+ " and publish it for preview. This is the default for a pasted or requested online file; do"
|
|
63
|
-
+ " not answer with only the URL. Use after web search, or when
|
|
64
|
-
+ "
|
|
63
|
+
+ " not answer with only the URL. Use after web search, or when a cloud generate returns a URL"
|
|
64
|
+
+ " or inline bytes. When a generation poll already returned an Olares files path"
|
|
65
|
+
+ " (`files_path` / `drive/…`), workspace_publish that path instead of downloading. Size is not"
|
|
66
|
+
+ " a reason to skip.",
|
|
65
67
|
parameters: {
|
|
66
68
|
url: {
|
|
67
69
|
type: "string",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin PC / phone split for the Lares entrance.
|
|
3
|
+
*
|
|
4
|
+
* FlowStudio serves both SPAs from `/` and picks the bundle per request
|
|
5
|
+
* (query, then cookie, then Client Hints, then User-Agent). Lares does the
|
|
6
|
+
* same: the phone page and the dsh desktop shell share the entrance host.
|
|
7
|
+
* Named routes (`/api`, `/llm`) never reach this choice.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MOBILE_UA_RE = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i;
|
|
11
|
+
const IPAD_DESKTOP_UA_RE = /macintosh.*mobile\/|mobile\/.*macintosh|iphone os|cpu os \d+/i;
|
|
12
|
+
|
|
13
|
+
export const UI_COOKIE_NAME = "lares_ui";
|
|
14
|
+
export const UI_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
15
|
+
|
|
16
|
+
const STATIC_SUFFIXES = [
|
|
17
|
+
".js", ".mjs", ".cjs", ".css", ".map", ".json", ".webmanifest",
|
|
18
|
+
".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".ico",
|
|
19
|
+
".woff", ".woff2", ".ttf", ".otf", ".txt", ".wasm",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function isMobileUserAgent(userAgent) {
|
|
23
|
+
if (!userAgent) return false;
|
|
24
|
+
if (MOBILE_UA_RE.test(userAgent)) return true;
|
|
25
|
+
return IPAD_DESKTOP_UA_RE.test(userAgent);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function forcedUi(value) {
|
|
29
|
+
const surface = String(value ?? "").trim().toLowerCase();
|
|
30
|
+
return surface === "mobile" || surface === "pc" ? surface : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function forcedUiFromCookie(cookieHeader) {
|
|
34
|
+
if (!cookieHeader) return null;
|
|
35
|
+
for (const part of String(cookieHeader).split(";")) {
|
|
36
|
+
const eq = part.indexOf("=");
|
|
37
|
+
if (eq === -1) continue;
|
|
38
|
+
if (part.slice(0, eq).trim() !== UI_COOKIE_NAME) continue;
|
|
39
|
+
return forcedUi(part.slice(eq + 1));
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{ userAgent?: string, cookie?: string, clientHint?: string, query?: string | URLSearchParams }} input
|
|
46
|
+
* @returns {{ surface: "mobile" | "pc", setCookie: "mobile" | "pc" | null }}
|
|
47
|
+
*/
|
|
48
|
+
export function resolveSpaSurface({ userAgent = "", cookie = "", clientHint = "", query = "" } = {}) {
|
|
49
|
+
const params = query instanceof URLSearchParams
|
|
50
|
+
? query
|
|
51
|
+
: new URLSearchParams(String(query ?? "").replace(/^\?/, ""));
|
|
52
|
+
const fromQuery = forcedUi(params.get("ui"));
|
|
53
|
+
if (fromQuery) return { surface: fromQuery, setCookie: fromQuery };
|
|
54
|
+
|
|
55
|
+
const fromCookie = forcedUiFromCookie(cookie);
|
|
56
|
+
if (fromCookie) return { surface: fromCookie, setCookie: null };
|
|
57
|
+
|
|
58
|
+
if (String(clientHint ?? "").trim() === "?1") return { surface: "mobile", setCookie: null };
|
|
59
|
+
|
|
60
|
+
return { surface: isMobileUserAgent(userAgent) ? "mobile" : "pc", setCookie: null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function surfaceFromRequest(req) {
|
|
64
|
+
const url = new URL(req?.url ?? "/", "http://lares.invalid");
|
|
65
|
+
const headers = req?.headers ?? {};
|
|
66
|
+
return resolveSpaSurface({
|
|
67
|
+
userAgent: headers["user-agent"] || "",
|
|
68
|
+
cookie: headers.cookie || "",
|
|
69
|
+
clientHint: headers["sec-ch-ua-mobile"] || "",
|
|
70
|
+
query: url.searchParams,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function uiCookieHeader(surface) {
|
|
75
|
+
return `${UI_COOKIE_NAME}=${surface}; Path=/; Max-Age=${UI_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** History routes fall back to index.html. Hashed assets must 404 as themselves. */
|
|
79
|
+
export function isMobileIndexPath(pathname) {
|
|
80
|
+
const path = String(pathname ?? "/").replace(/\\/g, "/").split("?")[0];
|
|
81
|
+
const name = path.split("/").pop()?.toLowerCase() ?? "";
|
|
82
|
+
if (!name || name === "index.html") return true;
|
|
83
|
+
return !STATIC_SUFFIXES.some((suffix) => name.endsWith(suffix));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function spaCacheControl(pathname) {
|
|
87
|
+
const normalized = String(pathname ?? "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
88
|
+
const name = normalized.split("/").pop()?.toLowerCase() ?? "";
|
|
89
|
+
if (!name || name === "index.html" || name.endsWith(".html")) {
|
|
90
|
+
return "no-cache, no-store, must-revalidate";
|
|
91
|
+
}
|
|
92
|
+
if (normalized.startsWith("assets/") || normalized.includes("/assets/")) {
|
|
93
|
+
return "public, max-age=31536000, immutable";
|
|
94
|
+
}
|
|
95
|
+
return "no-cache";
|
|
96
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseFilesPath } from "../drive/files-path.js";
|
|
2
|
+
import { entranceOrigin, userDomainOf } from "../olares/entrance.js";
|
|
3
|
+
|
|
4
|
+
const FILES_LABEL = "files";
|
|
5
|
+
|
|
6
|
+
function encoded(parts) {
|
|
7
|
+
return parts.map((part) => encodeURIComponent(part)).join("/");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function filesRoute(path) {
|
|
11
|
+
const [namespace, extend, ...rest] = parseFilesPath(path).split("/");
|
|
12
|
+
if (namespace === "drive") {
|
|
13
|
+
if (extend === "Data") return `/Data/${encoded(rest)}`;
|
|
14
|
+
if (extend === "Common") return `/Common/${encoded(rest)}`;
|
|
15
|
+
return `/Files/${encoded([extend, ...rest])}`;
|
|
16
|
+
}
|
|
17
|
+
if (namespace === "external") {
|
|
18
|
+
return `/Files/External/${encoded([extend, ...rest])}`;
|
|
19
|
+
}
|
|
20
|
+
if (namespace === "cache") return `/Cache/${encoded([extend, ...rest])}`;
|
|
21
|
+
if (["google", "dropbox", "awss3", "tencent"].includes(namespace)) {
|
|
22
|
+
return `/Drive/${namespace}/${encoded([extend, ...rest])}`;
|
|
23
|
+
}
|
|
24
|
+
if (namespace === "sync") {
|
|
25
|
+
const query = new URLSearchParams({ id: extend });
|
|
26
|
+
return `/Seahub/${encoded([extend, ...rest])}?${query}`;
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function originFromEntrance(entrance) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(String(entrance ?? ""));
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
return entranceOrigin(FILES_LABEL, {
|
|
39
|
+
domain: userDomainOf(url.hostname),
|
|
40
|
+
protocol: url.protocol,
|
|
41
|
+
port: url.port,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function originFromAccount(accountDomain, protocol) {
|
|
46
|
+
return entranceOrigin(FILES_LABEL, {
|
|
47
|
+
domain: accountDomain,
|
|
48
|
+
protocol: String(protocol ?? "https:").trim().toLowerCase(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function filesAppUrl(path, options = {}) {
|
|
53
|
+
let route;
|
|
54
|
+
try {
|
|
55
|
+
route = filesRoute(path);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
if (!route) return "";
|
|
60
|
+
const entrance = options.entrance ?? globalThis.location?.origin ?? "";
|
|
61
|
+
const origin = originFromEntrance(entrance)
|
|
62
|
+
|| originFromAccount(options.accountDomain, options.protocol);
|
|
63
|
+
return origin ? new URL(route, origin).href : "";
|
|
64
|
+
}
|